Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-07-20 09:08:42 +00:00
parent 83cddbd523
commit 65a0673d76
64 changed files with 1472 additions and 396 deletions

View File

@ -222,6 +222,10 @@ export default {
step21: () => import(/* webpackChunkName: 'hl-step21' */ 'highlight.js/lib/languages/step21'),
stylus: () => import(/* webpackChunkName: 'hl-stylus' */ 'highlight.js/lib/languages/stylus'),
subunit: () => import(/* webpackChunkName: 'hl-subunit' */ 'highlight.js/lib/languages/subunit'),
svelte: () =>
import(
/* webpackChunkName: 'hl-svelte' */ '~/vue_shared/components/source_viewer/languages/svelte'
),
swift: () => import(/* webpackChunkName: 'hl-swift' */ 'highlight.js/lib/languages/swift'),
taggerscript: () =>
import(/* webpackChunkName: 'hl-taggerscript' */ 'highlight.js/lib/languages/taggerscript'),

View File

@ -1,8 +1,8 @@
<script>
import DiffCodeQualityItem from './diff_code_quality_item.vue';
import DiffInlineFindingsItem from './diff_inline_findings_item.vue';
export default {
components: { DiffCodeQualityItem },
components: { DiffInlineFindingsItem },
props: {
title: {
type: String,
@ -22,7 +22,7 @@ export default {
{{ title }}
</h4>
<ul class="gl-list-style-none gl-mb-0 gl-p-0">
<diff-code-quality-item
<diff-inline-findings-item
v-for="finding in findings"
:key="finding.description"
:finding="finding"

View File

@ -42,7 +42,7 @@ export default {
:size="12"
:name="enhancedFinding.name"
:class="enhancedFinding.class"
class="codequality-severity-icon"
class="inline-findings-severity-icon"
/>
</span>
<span

View File

@ -68,7 +68,7 @@ export default {
:size="12"
:name="severityIcon(drawer.severity)"
:class="severityClass(drawer.severity)"
class="codequality-severity-icon"
class="inline-findings-severity-icon"
/>
{{ drawer.severity }}

View File

@ -8,7 +8,6 @@ import { splitIntoChunks } from '~/vue_shared/components/source_viewer/workers/h
import LineHighlighter from '~/blob/line_highlighter';
import languageLoader from '~/content_editor/services/highlight_js_language_loader';
import Tracking from '~/tracking';
import { TEXT_FILE_TYPE } from '../constants';
/*
* This mixin is intended to be used as an interface between our highlight worker and Vue components
@ -37,8 +36,8 @@ export default {
this.trackEvent(EVENT_LABEL_FALLBACK, language);
this?.onError();
},
initHighlightWorker({ rawTextBlob, language, simpleViewer, fileType }) {
if (simpleViewer?.fileType !== TEXT_FILE_TYPE || !this.glFeatures.highlightJsWorker) return;
initHighlightWorker({ rawTextBlob, language, fileType }) {
if (language !== 'json' || !this.glFeatures.highlightJsWorker) return;
if (this.isUnsupportedLanguage(language)) {
this.handleUnsupportedLanguage(language);

View File

@ -1,10 +1,21 @@
<script>
import { GlEmptyState } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
import { STATUS_OPEN, STATUS_CLOSED, STATUS_ALL } from '~/issues/constants';
import {
OPERATORS_IS_NOT,
OPERATORS_IS_NOT_OR,
} from '~/vue_shared/components/filtered_search_bar/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import searchUsersQuery from '~/issues/list/queries/search_users.query.graphql';
import searchLabelsQuery from '~/issues/list/queries/search_labels.query.graphql';
import searchMilestonesQuery from '~/issues/list/queries/search_milestones.query.graphql';
import getServiceDeskIssuesQuery from '../queries/get_service_desk_issues.query.graphql';
import getServiceDeskIssuesCounts from '../queries/get_service_desk_issues_counts.query.graphql';
import {
@ -13,7 +24,21 @@ import {
noSearchNoFilterTitle,
searchPlaceholder,
SERVICE_DESK_BOT_USERNAME,
MAX_LIST_SIZE,
STATUS_OPEN,
STATUS_CLOSED,
STATUS_ALL,
WORKSPACE_PROJECT,
} from '../constants';
import {
searchWithinTokenBase,
assigneeTokenBase,
milestoneTokenBase,
labelTokenBase,
releaseTokenBase,
reactionTokenBase,
confidentialityTokenBase,
} from '../search_tokens';
import InfoBanner from './info_banner.vue';
export default {
@ -29,7 +54,12 @@ export default {
IssuableList,
InfoBanner,
},
mixins: [glFeatureFlagMixin()],
inject: [
'releasesPath',
'autocompleteAwardEmojisPath',
'hasIterationsFeature',
'groupPath',
'emptyStateSvgPath',
'isProject',
'isSignedIn',
@ -41,7 +71,6 @@ export default {
return {
serviceDeskIssues: [],
serviceDeskIssuesCounts: {},
searchTokens: [],
sortOptions: [],
state: STATUS_OPEN,
issuesError: null,
@ -112,8 +141,134 @@ export default {
isInfoBannerVisible() {
return this.isServiceDeskSupported && this.hasAnyIssues;
},
hasOrFeature() {
return this.glFeatures.orIssuableQueries;
},
searchTokens() {
const preloadedUsers = [];
if (gon.current_user_id) {
preloadedUsers.push({
id: convertToGraphQLId(TYPENAME_USER, gon.current_user_id),
name: gon.current_user_fullname,
username: gon.current_username,
avatar_url: gon.current_user_avatar_url,
});
}
const tokens = [
{
...searchWithinTokenBase,
},
{
...assigneeTokenBase,
operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
fetchUsers: this.fetchUsers,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
preloadedUsers,
},
{
...milestoneTokenBase,
fetchMilestones: this.fetchMilestones,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-milestone`,
},
{
...labelTokenBase,
operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
fetchLabels: this.fetchLabels,
fetchLatestLabels: this.glFeatures.frontendCaching ? this.fetchLatestLabels : null,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-label`,
},
];
if (this.isProject) {
tokens.push({
...releaseTokenBase,
fetchReleases: this.fetchReleases,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-release`,
});
}
if (this.isSignedIn) {
tokens.push({
...reactionTokenBase,
fetchEmojis: this.fetchEmojis,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-my_reaction`,
});
tokens.push({
...confidentialityTokenBase,
});
}
tokens.sort((a, b) => a.title.localeCompare(b.title));
return tokens;
},
},
created() {
this.cache = {};
},
methods: {
fetchWithCache(path, cacheName, searchKey, search) {
if (this.cache[cacheName]) {
const data = search
? fuzzaldrinPlus.filter(this.cache[cacheName], search, { key: searchKey })
: this.cache[cacheName].slice(0, MAX_LIST_SIZE);
return Promise.resolve(data);
}
return axios.get(path).then(({ data }) => {
this.cache[cacheName] = data;
return data.slice(0, MAX_LIST_SIZE);
});
},
fetchUsers(search) {
return this.$apollo
.query({
query: searchUsersQuery,
variables: { fullPath: this.fullPath, search, isProject: this.isProject },
})
.then(({ data }) =>
data[WORKSPACE_PROJECT]?.[`${WORKSPACE_PROJECT}Members`].nodes.map(
(member) => member.user,
),
);
},
fetchMilestones(search) {
return this.$apollo
.query({
query: searchMilestonesQuery,
variables: { fullPath: this.fullPath, search, isProject: this.isProject },
})
.then(({ data }) => data[WORKSPACE_PROJECT]?.milestones.nodes);
},
fetchEmojis(search) {
return this.fetchWithCache(this.autocompleteAwardEmojisPath, 'emojis', 'name', search);
},
fetchReleases(search) {
return this.fetchWithCache(this.releasesPath, 'releases', 'tag', search);
},
fetchLabelsWithFetchPolicy(search, fetchPolicy = fetchPolicies.CACHE_FIRST) {
return this.$apollo
.query({
query: searchLabelsQuery,
variables: { fullPath: this.fullPath, search, isProject: this.isProject },
fetchPolicy,
})
.then(({ data }) => data[WORKSPACE_PROJECT]?.labels.nodes)
.then((labels) =>
// TODO remove once we can search by title-only on the backend
// https://gitlab.com/gitlab-org/gitlab/-/issues/346353
labels.filter((label) => label.title.toLowerCase().includes(search.toLowerCase())),
);
},
fetchLabels(search) {
return this.fetchLabelsWithFetchPolicy(search);
},
fetchLatestLabels(search) {
return this.fetchLabelsWithFetchPolicy(search, fetchPolicies.NETWORK_ONLY);
},
handleClickTab(state) {
if (this.state === state) {
return;

View File

@ -1,6 +1,11 @@
import { __, s__ } from '~/locale';
export const SERVICE_DESK_BOT_USERNAME = 'support-bot';
export const MAX_LIST_SIZE = 10;
export const STATUS_ALL = 'all';
export const STATUS_CLOSED = 'closed';
export const STATUS_OPEN = 'opened';
export const WORKSPACE_PROJECT = 'project';
export const errorFetchingCounts = __('An error occurred while getting issue counts');
export const errorFetchingIssues = __('An error occurred while loading issues');
@ -15,3 +20,7 @@ export const infoBannerUserNote = s__(
);
export const enableServiceDesk = s__('ServiceDesk|Enable Service Desk');
export const learnMore = __('Learn more');
export const titles = __('Titles');
export const descriptions = __('Descriptions');
export const no = __('No');
export const yes = __('Yes');

View File

@ -12,6 +12,12 @@ export async function mountServiceDeskListApp() {
}
const {
projectDataReleasesPath,
projectDataAutocompleteAwardEmojisPath,
projectDataHasIterationsFeature,
projectDataHasIssueWeightsFeature,
projectDataHasIssuableHealthStatusFeature,
projectDataGroupPath,
projectDataEmptyStateSvgPath,
projectDataFullPath,
projectDataIsProject,
@ -36,6 +42,12 @@ export async function mountServiceDeskListApp() {
defaultClient: await gqlClient(),
}),
provide: {
releasesPath: projectDataReleasesPath,
autocompleteAwardEmojisPath: projectDataAutocompleteAwardEmojisPath,
hasIterationsFeature: parseBoolean(projectDataHasIterationsFeature),
hasIssueWeightsFeature: parseBoolean(projectDataHasIssueWeightsFeature),
hasIssuableHealthStatusFeature: parseBoolean(projectDataHasIssuableHealthStatusFeature),
groupPath: projectDataGroupPath,
emptyStateSvgPath: projectDataEmptyStateSvgPath,
fullPath: projectDataFullPath,
isProject: parseBoolean(projectDataIsProject),

View File

@ -0,0 +1,97 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
import {
OPERATORS_IS,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_CONFIDENTIAL,
TOKEN_TITLE_LABEL,
TOKEN_TITLE_MILESTONE,
TOKEN_TITLE_MY_REACTION,
TOKEN_TITLE_RELEASE,
TOKEN_TITLE_SEARCH_WITHIN,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_SEARCH_WITHIN,
} from '~/vue_shared/components/filtered_search_bar/constants';
import { titles, descriptions, yes, no } from './constants';
const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue');
const EmojiToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue');
const LabelToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/label_token.vue');
const MilestoneToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue');
const ReleaseToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/release_token.vue');
export const searchWithinTokenBase = {
type: TOKEN_TYPE_SEARCH_WITHIN,
title: TOKEN_TITLE_SEARCH_WITHIN,
icon: 'search',
token: GlFilteredSearchToken,
unique: true,
operators: OPERATORS_IS,
options: [
{ icon: 'title', value: 'TITLE', title: titles },
{
icon: 'text-description',
value: 'DESCRIPTION',
title: descriptions,
},
],
};
export const assigneeTokenBase = {
type: TOKEN_TYPE_ASSIGNEE,
title: TOKEN_TITLE_ASSIGNEE,
icon: 'user',
token: UserToken,
dataType: 'user',
};
export const milestoneTokenBase = {
type: TOKEN_TYPE_MILESTONE,
title: TOKEN_TITLE_MILESTONE,
icon: 'clock',
token: MilestoneToken,
shouldSkipSort: true,
};
export const labelTokenBase = {
type: TOKEN_TYPE_LABEL,
title: TOKEN_TITLE_LABEL,
icon: 'labels',
token: LabelToken,
};
export const releaseTokenBase = {
type: TOKEN_TYPE_RELEASE,
title: TOKEN_TITLE_RELEASE,
icon: 'rocket',
token: ReleaseToken,
};
export const reactionTokenBase = {
type: TOKEN_TYPE_MY_REACTION,
title: TOKEN_TITLE_MY_REACTION,
icon: 'thumb-up',
token: EmojiToken,
unique: true,
};
export const confidentialityTokenBase = {
type: TOKEN_TYPE_CONFIDENTIAL,
title: TOKEN_TITLE_CONFIDENTIAL,
icon: 'eye-slash',
token: GlFilteredSearchToken,
unique: true,
operators: OPERATORS_IS,
options: [
{ icon: 'eye-slash', value: 'yes', title: yes },
{ icon: 'eye', value: 'no', title: no },
],
};

View File

@ -97,6 +97,7 @@ export const ROUGE_TO_HLJS_LANGUAGE_MAP = {
sql: 'sql',
stan: 'stan',
stata: 'stata',
svelte: 'svelte',
swift: 'swift',
tap: 'tap',
tcl: 'tcl',
@ -151,3 +152,5 @@ export const LEGACY_FALLBACKS = ['python', 'haml'];
export const CODEOWNERS_FILE_NAME = 'CODEOWNERS';
export const CODEOWNERS_LANGUAGE = 'codeowners';
export const SVELTE_LANGUAGE = 'svelte';

View File

@ -0,0 +1,81 @@
/*
Language: Svelte.js
Requires: xml, javascript, typescript, css, scss
Description: Components of Svelte Framework
*/
export default (hljs) => {
return {
subLanguage: 'xml',
contains: [
hljs.COMMENT('<!--', '-->', {
relevance: 11,
}),
{
begin: /^(\s*)(<script.*(lang="ts").*>)/gm,
end: /^(\s*)(<\/script>)/gm,
subLanguage: 'typescript',
excludeBegin: true,
excludeEnd: true,
relevance: 20,
contains: [
// special svelte $ syntax
{
begin: /^(\s*)(\$:)/gm,
end: /(\s*)/gm,
className: 'keyword',
},
],
},
{
begin: /^(\s*)(<script(\s*context="module")?.*>)/gm,
end: /^(\s*)(<\/script>)/gm,
subLanguage: 'javascript',
excludeBegin: true,
excludeEnd: true,
relevance: 15,
contains: [
// special svelte $ syntax
{
begin: /^(\s*)(\$:)/gm,
end: /(\s*)/gm,
className: 'keyword',
},
],
},
{
begin: /^(\s*)(<style.*(lang="scss"|type="text\/scss").*>)/gm,
end: /^(\s*)(<\/style>)/gm,
subLanguage: 'scss',
excludeBegin: true,
excludeEnd: true,
relevance: 20,
},
{
begin: /^(\s*)(<style.*>)/gm,
end: /^(\s*)(<\/style>)/gm,
subLanguage: 'css',
excludeBegin: true,
excludeEnd: true,
relevance: 15,
},
{
begin: /\{/gm,
end: /}/gm,
subLanguage: 'javascript',
contains: [
{
begin: /[{]/,
end: /[}]/,
skip: true,
},
{
begin: /([#:/@])(if|else|each|await|then|catch|debug|html)/gm,
className: 'keyword',
relevance: 10,
},
],
},
],
};
};

View File

@ -14,6 +14,7 @@ import {
LEGACY_FALLBACKS,
CODEOWNERS_FILE_NAME,
CODEOWNERS_LANGUAGE,
SVELTE_LANGUAGE,
} from './constants';
import Chunk from './components/chunk.vue';
import { registerPlugins } from './plugins/index';
@ -56,9 +57,15 @@ export default {
return this.content.split(/\r?\n/);
},
language() {
return this.blob.name === this.$options.codeownersFileName
? this.$options.codeownersLanguage
: ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language?.toLowerCase()];
if (this.blob.name && this.blob.name.endsWith(`.${SVELTE_LANGUAGE}`)) {
// override for svelte files until https://github.com/rouge-ruby/rouge/issues/1717 is resolved
return SVELTE_LANGUAGE;
} else if (this.blob.name === this.$options.codeownersFileName) {
// override for codeowners files
return this.$options.codeownersLanguage;
}
return ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language?.toLowerCase()];
},
lineNumbers() {
return this.splitContent.length;
@ -168,12 +175,36 @@ export default {
// If no language can be mapped to highlight.js we load all common languages else we load only the core (smallest footprint)
return !this.language ? import('highlight.js/lib/common') : import('highlight.js/lib/core');
},
async loadSubLanguages(languageDefinition) {
if (!languageDefinition?.contains) return;
// generate list of languages to load
const languages = new Set(
languageDefinition.contains
.filter((component) => Boolean(component.subLanguage))
.map((component) => component.subLanguage),
);
if (languageDefinition.subLanguage) {
languages.add(languageDefinition.subLanguage);
}
// load all sub-languages at once
await Promise.all(
[...languages].map(async (subLanguage) => {
const subLanguageDefinition = await languageLoader[subLanguage]();
this.hljs.registerLanguage(subLanguage, subLanguageDefinition.default);
}),
);
},
async loadLanguage() {
let languageDefinition;
try {
languageDefinition = await languageLoader[this.language]();
this.hljs.registerLanguage(this.language, languageDefinition.default);
await this.loadSubLanguages(this.hljs.getLanguage(this.language));
} catch (message) {
this.$emit('error', message);
}

View File

@ -543,8 +543,8 @@ class Namespace < ApplicationRecord
def changing_allow_descendants_override_disabled_shared_runners_is_allowed
return unless new_record? || changes.has_key?(:allow_descendants_override_disabled_shared_runners)
if shared_runners_enabled && !new_record?
errors.add(:allow_descendants_override_disabled_shared_runners, _('cannot be changed if shared runners are enabled'))
if shared_runners_enabled && allow_descendants_override_disabled_shared_runners
errors.add(:allow_descendants_override_disabled_shared_runners, _('can not be true if shared runners are enabled'))
end
if allow_descendants_override_disabled_shared_runners && has_parent? && parent.shared_runners_setting == SR_DISABLED_AND_UNOVERRIDABLE

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
module Packages
module Nuget
module V2
class ServiceIndexPresenter
include API::Helpers::RelatedResourcesHelpers
ROOT_ATTRIBUTES = {
xmlns: 'http://www.w3.org/2007/app',
'xmlns:atom' => 'http://www.w3.org/2005/Atom'
}.freeze
def initialize(project_or_group)
@project_or_group = project_or_group
end
def xml
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
xml.service(ROOT_ATTRIBUTES.merge('xml:base' => xml_base)) do
xml.workspace do
xml['atom'].title('Default', type: 'text')
xml.collection(href: 'Packages') do
xml['atom'].title('Packages', type: 'text')
end
end
end
end
end
private
attr_reader :project_or_group
def xml_base
base_path = case project_or_group
when Project
api_v4_projects_packages_nuget_v2_path(id: project_or_group.id)
when Group
api_v4_groups___packages_nuget_v2_path(id: project_or_group.id)
end
expose_url(base_path)
end
end
end
end
end

View File

@ -18,15 +18,7 @@ module Projects
end
def project_members
@project_members ||= sorted(get_project_members)
end
def get_project_members
members = Member.from_union([project_members_through_ancestral_groups,
project_members_through_invited_groups,
individual_project_members])
User.id_in(members.select(:user_id))
@project_members ||= sorted(project.authorized_users)
end
def all_members
@ -34,33 +26,5 @@ module Projects
[{ username: "all", name: "All Project and Group Members", count: project_members.count }]
end
private
def project_members_through_invited_groups
GroupMember
.active_without_invites_and_requests
.with_source_id(visible_groups.self_and_ancestors.pluck_primary_key)
.select(*GroupMember.cached_column_list)
end
def visible_groups
visible_groups = project.invited_groups
unless project.team.member?(current_user)
visible_groups = visible_groups.public_or_visible_to_user(current_user)
end
visible_groups
end
def project_members_through_ancestral_groups
members = project.group.present? ? project.group.members_with_parents : Member.none
members.select(*GroupMember.cached_column_list)
end
def individual_project_members
project.project_members.select(*GroupMember.cached_column_list)
end
end
end

View File

@ -1,7 +1,8 @@
- can_edit_max_page_size=can?(current_user, :update_max_pages_size)
- can_enforce_https_only=Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
- can_edit_max_page_size = can?(current_user, :update_max_pages_size)
- can_enforce_https_only = Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
- can_edit_unique_domain = Feature.enabled?(:pages_unique_domain, @project)
- return unless can_edit_max_page_size || can_enforce_https_only
- return unless can_edit_max_page_size || can_enforce_https_only || can_edit_unique_domain
= gitlab_ui_form_for @project, url: project_pages_path(@project), html: { class: 'inline', title: pages_https_only_title } do |f|
- if can_edit_max_page_size
= render_if_exists 'shared/pages/max_pages_size_input', form: f
@ -17,7 +18,7 @@
%p.gl-pl-6
= s_("GitLabPages|When enabled, all attempts to visit your website through HTTP are automatically redirected to HTTPS using a response with status code 301. Requires a valid certificate for all domains. %{docs_link_start}Learn more.%{link_end}").html_safe % { docs_link_start: docs_link_start, link_end: link_end }
- if Feature.enabled?(:pages_unique_domain, @project)
- if can_edit_unique_domain
.form-group
= f.fields_for :project_setting do |settings|
= settings.gitlab_ui_checkbox_component :pages_unique_domain_enabled,

View File

@ -273,6 +273,8 @@
- 1
- - groups_create_event
- 1
- - groups_enterprise_users_disassociate
- 1
- - groups_export_memberships
- 1
- - groups_schedule_bulk_repository_shard_moves

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddRuleIdxToScanResultPolicies < Gitlab::Database::Migration[2.1]
enable_lock_retries!
CONSTRAINT_NAME = "check_scan_result_policies_rule_idx_positive"
def up
add_column :scan_result_policies, :rule_idx, :smallint
end
def down
remove_column :scan_result_policies, :rule_idx
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddRuleIdxConstraintToScanResultPolicies < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
CONSTRAINT_NAME = "check_scan_result_policies_rule_idx_positive"
def up
add_check_constraint :scan_result_policies, "rule_idx IS NULL OR rule_idx >= 0", CONSTRAINT_NAME
end
def down
remove_check_constraint :scan_result_policies, CONSTRAINT_NAME
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddUniqueIndexToScanResultPoliciesOnPositionInConfiguration < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
INDEX_NAME = 'index_scan_result_policies_on_position_in_configuration'
COLUMNS = %i[security_orchestration_policy_configuration_id project_id orchestration_policy_idx rule_idx]
def up
add_concurrent_index :scan_result_policies, COLUMNS, unique: true, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :scan_result_policies, INDEX_NAME
end
end

View File

@ -0,0 +1 @@
5a4aff0810f41646e25614ad26a826434eb28e85a8f1393e4e1799ce6ab61ff7

View File

@ -0,0 +1 @@
ff621297609cbbcb09108981daacfacf341d1e39bbbbe71fe140c6a0b7af40eb

View File

@ -0,0 +1 @@
20e8e9e72d25ed720d4aa8c3045a8cc99c6e34cc9ac4f0973e39627300866f30

View File

@ -22432,7 +22432,9 @@ CREATE TABLE scan_result_policies (
age_interval smallint,
vulnerability_attributes jsonb DEFAULT '{}'::jsonb,
project_id bigint,
CONSTRAINT age_value_null_or_positive CHECK (((age_value IS NULL) OR (age_value >= 0)))
rule_idx smallint,
CONSTRAINT age_value_null_or_positive CHECK (((age_value IS NULL) OR (age_value >= 0))),
CONSTRAINT check_scan_result_policies_rule_idx_positive CHECK (((rule_idx IS NULL) OR (rule_idx >= 0)))
);
CREATE SEQUENCE scan_result_policies_id_seq
@ -32933,6 +32935,8 @@ CREATE UNIQUE INDEX index_sbom_sources_on_source_type_and_source ON sbom_sources
CREATE INDEX index_scan_result_policies_on_policy_configuration_id ON scan_result_policies USING btree (security_orchestration_policy_configuration_id);
CREATE UNIQUE INDEX index_scan_result_policies_on_position_in_configuration ON scan_result_policies USING btree (security_orchestration_policy_configuration_id, project_id, orchestration_policy_idx, rule_idx);
CREATE INDEX index_scan_result_policies_on_project_id ON scan_result_policies USING btree (project_id);
CREATE INDEX index_schema_inconsistencies_on_issue_id ON schema_inconsistencies USING btree (issue_id);

View File

@ -80,13 +80,22 @@ This writes the downloaded file to `MyNuGetPkg.1.3.0.17.nupkg` in the current di
## Upload a package file
> Introduced in GitLab 12.8.
> - Introduced in GitLab 12.8 for NuGet v3 feed.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416404) in GitLab 16.2 for NuGet v2 feed.
Upload a NuGet package file:
```plaintext
PUT projects/:id/packages/nuget
```
- For NuGet v3 feed:
```plaintext
PUT projects/:id/packages/nuget
```
- For NuGet V2 feed:
```plaintext
PUT projects/:id/packages/nuget/v2
```
| Attribute | Type | Required | Description |
| ----------------- | ------ | -------- | ----------- |
@ -95,12 +104,23 @@ PUT projects/:id/packages/nuget
| `package_version` | string | yes | The version of the package. |
| `package_filename`| string | yes | The name of the file. |
```shell
curl --request PUT \
--form 'package=@path/to/mynugetpkg.1.3.0.17.nupkg' \
--user <username>:<personal_access_token> \
"https://gitlab.example.com/api/v4/projects/1/packages/nuget/"
```
- For NuGet v3 feed:
```shell
curl --request PUT \
--form 'package=@path/to/mynugetpkg.1.3.0.17.nupkg' \
--user <username>:<personal_access_token> \
"https://gitlab.example.com/api/v4/projects/1/packages/nuget/"
```
- For NuGet v2 feed:
```shell
curl --request PUT \
--form 'package=@path/to/mynugetpkg.1.3.0.17.nupkg' \
--user <username>:<personal_access_token> \
"https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2"
```
## Upload a symbol package file
@ -158,6 +178,37 @@ The examples in this document all use the project-level prefix.
## Service Index
### V2 source feed/protocol
Returns an XML document that represents the service index of the v2 NuGet source feed.
Authentication is not required:
```plaintext
GET <route-prefix>/v2
```
Example Request:
```shell
curl "https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2"
```
Example response:
```xml
<?xml version="1.0" encoding="utf-8"?>
<service xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom" xml:base="https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2">
<workspace>
<atom:title type="text">Default</atom:title>
<collection href="Packages">
<atom:title type="text">Packages</atom:title>
</collection>
</workspace>
</service>
```
### V3 source feed/protocol
> - Introduced in GitLab 12.6.
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/214674) to be public in GitLab 16.1.

View File

@ -94,7 +94,7 @@ EE: true
uses system fonts for all text."
- Any client-facing change to our REST and GraphQL APIs **must** have a changelog entry.
See the [complete list what comprises a GraphQL breaking change](api_graphql_styleguide.md#breaking-changes).
- Any change that introduces an [advanced search migration](search/advanced_search_migration_styleguide.md#creating-a-new-advanced-search-migration)
- Any change that introduces an [advanced search migration](search/advanced_search_migration_styleguide.md#create-a-new-advanced-search-migration)
**must** have a changelog entry.
- A fix for a regression introduced and then fixed in the same release (such as
fixing a bug introduced during a monthly release candidate) **should not**

View File

@ -6,13 +6,25 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Advanced search migration style guide
## Creating a new advanced search migration
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/234046) in GitLab 13.6.
## Create a new advanced search migration
NOTE:
This functionality is only supported for indices created in GitLab 13.0 and later.
### With a script
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/414674) in GitLab 16.3.
Execute `scripts/elastic-migration` and follow the prompts to create:
- A migration file to define the migration: `ee/elastic/migrate/YYYYMMDDHHMMSS_migration_name.rb`
- A spec file to test the migration: `ee/spec/elastic/migrate/YYYYMMDDHHMMSS_migration_name_spec.rb`
- A dictionary file to identify the migration: `ee/elastic/docs/YYYYMMDDHHMMSS_migration_name.yml`
### Manually
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/234046) in GitLab 13.6.
In the [`ee/elastic/migrate/`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/elastic/migrate) folder, create a new file with the filename format `YYYYMMDDHHMMSS_migration_name.rb`. This format is the same for Rails database migrations.
```ruby

View File

@ -527,7 +527,29 @@ With reindex migrations running in the background, there's no need for a manual
intervention. This usually happens in situations where new features are added to
advanced search, which means adding or changing the way content is indexed.
To confirm that the advanced search migrations ran, you can check with:
### Migration dictionary files
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/414674) in GitLab 16.3.
Every migration has a corresponding dictionary file in the `ee/elastic/docs/` folder with the following information:
```yaml
name:
version:
description:
group:
milestone:
introduced_by_url:
obsolete:
marked_obsolete_by_url:
marked_obsolete_in_milestone:
```
You can use this information, for example, to identify when a migration was introduced or was marked as obsolete.
### Check for pending migrations
To check for pending advanced search migrations, run this command:
```shell
curl "$CLUSTER_URL/gitlab-production-migrations/_search?q=*" | jq .
@ -566,7 +588,7 @@ This should return something similar to:
}
```
To debug issues with the migrations you can check the [`elasticsearch.log` file](../../administration/logs/index.md#elasticsearchlog).
To debug issues with the migrations, check the [`elasticsearch.log`](../../administration/logs/index.md#elasticsearchlog) file.
### Retry a halted migration

View File

@ -76,7 +76,7 @@ Prerequisite:
To view a list of dashboards (both built-in and custom) for a project:
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. Select **Analyze > Dashboards**.
1. Select **Analyze > Analytics dashboards**.
1. From the list of available dashboards, select the dashboard you want to view.
## Change the location of dashboards
@ -174,7 +174,7 @@ create a `line_chart.yaml` file with the following required fields:
To create a custom dashboard:
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. Select **Analyze > Dashboards**.
1. Select **Analyze > Analytics dashboards**.
1. Select **New dashboard**.
1. In the **New dashboard** input, enter the name of the dashboard.
1. From the **Add visualizations** list on the right, select the visualizations to add to the dashboard.
@ -188,7 +188,7 @@ You can edit your custom dashboard's title and add or resize visualizations in t
To edit an existing custom dashboard:
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. Select **Analyze > Dashboards**.
1. Select **Analyze > Analytics dashboards**.
1. From the list of available dashboards, select a custom dashboard (one without the `By GitLab` label) you want to edit.
1. Select **Edit**.
1. Optional. Change the title of the dashboard.

View File

@ -225,8 +225,7 @@ After you set up your identity provider to work with GitLab, you must configure
1. In the **Default membership role** field, select the role to assign to new users.
The default role is **Guest**. In [GitLab 13.3](https://gitlab.com/gitlab-org/gitlab/-/issues/214523)
and later, group owners can set a default membership role other than **Guest**.
To do so, [configure the SAML SSO for the group](#configure-gitlab). That role
becomes the starting role of all users added to the group.
That role becomes the starting role of all users added to the group.
1. Select the **Enable SAML authentication for this group** checkbox.
1. Optional. Select:
- **Enforce SSO-only authentication for web activity for this group**.

View File

@ -70,6 +70,7 @@ You can now add a new source to NuGet with:
- [Visual Studio](#add-a-source-with-visual-studio)
- [.NET CLI](#add-a-source-with-the-net-cli)
- [Configuration file](#add-a-source-with-a-configuration-file)
- [Chocolatey CLI](#add-a-source-with-chocolatey-cli)
### Add a source with the NuGet CLI
@ -281,6 +282,22 @@ To use the [group-level](#use-the-gitlab-endpoint-for-nuget-packages) Package Re
export GITLAB_PACKAGE_REGISTRY_PASSWORD=<gitlab_personal_access_token or deploy_token>
```
### Add a source with Chocolatey CLI
You can add a source feed with the Chocolatey CLI. If you use Chocolatey CLI v1.x, you can add only a NuGet v2 source feed.
#### Configure a project-level endpoint
You need a project-level endpoint to publish NuGet packages to the Package Registry.
To use the [project-level](#use-the-gitlab-endpoint-for-nuget-packages) Package Registry as a source for Chocolatey:
- Add the Package Registry as a source with `choco`:
```shell
choco source add -n=gitlab -s "'https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/v2'" -u=<gitlab_username or deploy_token_username> -p=<gitlab_personal_access_token or deploy_token>
```
## Publish a NuGet package
Prerequisite:
@ -385,6 +402,31 @@ updated:
1. Commit the changes and push it to your GitLab repository to trigger a new CI/CD build.
### Publish a NuGet package with Chocolatey CLI
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416404) in GitLab 16.2.
Prerequisite:
- The project-level Package Registry is a source for Chocolatey.
To publish a package with the Chocolatey CLI:
```shell
choco push <package_file> --source <source_url> --api-key <gitlab_personal_access_token, deploy_token or job token>
```
In this command:
- `<package_file>` is your package filename and ends with `.nupkg`.
- `<source_url>` is the URL of the NuGet v2 feed Package Registry.
For example:
```shell
choco push MyPackage.1.0.0.nupkg --source "https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/v2" --api-key <gitlab_personal_access_token, deploy_token or job token>
```
### Publishing a package with the same name or version
When you publish a package with the same name or version as an existing package,

View File

@ -64,14 +64,29 @@ You also have access to the terminal and can install any necessary dependencies.
## Workspaces and projects
A workspace is scoped to a project. When you create a workspace, you must:
Workspaces are scoped to a project. When you create a workspace, you must:
- Assign the workspace to a specific project.
- Select a project with a `.devfile.yaml` file.
The workspace can then interact with the GitLab API based on the permissions granted to the current user.
## Deleting data associated with a workspace
### Open and manage workspaces from a project
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125331) in GitLab 16.2.
To open a workspace from a file or the repository file list:
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. In the upper right, select **Edit**.
1. From the dropdown list, under **Your workspaces**, select the workspace.
From the dropdown list, you can also:
- Restart, stop, or terminate an existing workspace.
- Create a new workspace.
### Deleting data associated with a workspace
When you delete a project, agent, user, or token associated with a workspace:

View File

@ -16,7 +16,7 @@ module API
included do
# https://docs.microsoft.com/en-us/nuget/api/service-index
desc 'The NuGet Service Index' do
desc 'The NuGet V3 Feed Service Index' do
detail 'This feature was introduced in GitLab 12.6'
success code: 200, model: ::API::Entities::Nuget::ServiceIndex
failure [
@ -34,6 +34,33 @@ module API
present ::Packages::Nuget::ServiceIndexPresenter.new(project_or_group_without_auth),
with: ::API::Entities::Nuget::ServiceIndex
end
desc 'The NuGet V2 Feed Service Index' do
detail 'This feature was introduced in GitLab 16.2'
success code: 200
failure [
{ code: 404, message: 'Not Found' }
]
tags %w[nuget_packages]
end
namespace '/v2' do
get format: :xml, urgency: :low do
env['api.format'] = :xml
content_type 'application/xml; charset=utf-8'
# needed to allow browser default inline styles in xml response
header 'Content-Security-Policy', "nonce-#{SecureRandom.base64(16)}"
track_package_event(
'cli_metadata',
:nuget,
**snowplow_gitlab_standard_context_without_auth.merge(category: 'API::NugetPackages', feed: 'v2')
)
present ::Packages::Nuget::V2::ServiceIndexPresenter
.new(project_or_group_without_auth)
.xml
end
end
end
end
end

View File

@ -98,6 +98,22 @@ module API
created!
end
def publish_package(symbol_package: false)
upload_nuget_package_file(symbol_package: symbol_package) do |package|
track_package_event(
symbol_package ? 'push_symbol_package' : 'push_package',
:nuget,
**{ category: 'API::NugetPackages',
project: package.project,
namespace: package.project.namespace }.tap { |args| args[:feed] = 'v2' if request.path.include?('nuget/v2') }
)
end
rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
forbidden!
end
def required_permission
:read_package
end
@ -179,7 +195,7 @@ module API
end
end
# To support an additional authentication option for download endpoints,
# To support an additional authentication option for publish endpoints,
# we redefine the `authenticate_with` method by combining the previous
# authentication option with the new one.
authenticate_with do |accept|
@ -191,7 +207,7 @@ module API
namespace '/nuget' do
# https://docs.microsoft.com/en-us/nuget/api/package-publish-resource
desc 'The NuGet Package Publish endpoint' do
desc 'The NuGet V3 Feed Package Publish endpoint' do
detail 'This feature was introduced in GitLab 12.6'
success code: 201
failure [
@ -207,19 +223,7 @@ module API
use :file_params
end
put urgency: :low do
upload_nuget_package_file do |package|
track_package_event(
'push_package',
:nuget,
category: 'API::NugetPackages',
project: package.project,
namespace: package.project.namespace
)
end
rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
forbidden!
publish_package
end
desc 'The NuGet Package Authorize endpoint' do
@ -252,19 +256,7 @@ module API
use :file_params
end
put 'symbolpackage', urgency: :low do
upload_nuget_package_file(symbol_package: true) do |package|
track_package_event(
'push_symbol_package',
:nuget,
category: 'API::NugetPackages',
project: package.project,
namespace: package.project.namespace
)
end
rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
forbidden!
publish_package(symbol_package: true)
end
desc 'The NuGet Symbol Package Authorize endpoint' do
@ -280,6 +272,42 @@ module API
put 'symbolpackage/authorize', urgency: :low do
authorize_nuget_upload
end
namespace '/v2' do
desc 'The NuGet V2 Feed Package Publish endpoint' do
detail 'This feature was introduced in GitLab 16.2'
success code: 201
failure [
{ code: 400, message: 'Bad Request' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not Found' }
]
tags %w[nuget_packages]
end
params do
use :file_params
end
put do
publish_package
end
desc 'The NuGet V2 Feed Package Authorize endpoint' do
detail 'This feature was introduced in GitLab 16.2'
success code: 200
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not Found' }
]
tags %w[nuget_packages]
end
put 'authorize', urgency: :low do
authorize_nuget_upload
end
end
end
end
end

View File

@ -404,9 +404,14 @@ module Gitlab
def revoke_token_family(token)
return unless Feature.enabled?(:pat_reuse_detection)
return unless access_token_rotation_request?
PersonalAccessTokens::RevokeTokenFamilyService.new(token).execute
end
def access_token_rotation_request?
current_request.path.match(%r{access_tokens/\d+/rotate$})
end
end
end
end

View File

@ -53985,6 +53985,9 @@ msgstr ""
msgid "can not be set for this type of note"
msgstr ""
msgid "can not be true if shared runners are enabled"
msgstr ""
msgid "can only be changed by a group admin."
msgstr ""
@ -54033,9 +54036,6 @@ msgstr ""
msgid "cannot be changed if a personal project has container registry tags."
msgstr ""
msgid "cannot be changed if shared runners are enabled"
msgstr ""
msgid "cannot be changed since member is associated with a custom role"
msgstr ""

View File

@ -36,7 +36,7 @@ gem "warning", "~> 1.3"
gem 'confiner', '~> 0.4'
gem 'chemlab', '~> 0.10'
gem 'chemlab', '~> 0.11', '>= 0.11.1'
gem 'chemlab-library-www-gitlab-com', '~> 0.1', '>= 0.1.1'
# dependencies for jenkins client

View File

@ -56,7 +56,7 @@ GEM
capybara-screenshot (1.0.26)
capybara (>= 1.0, < 4)
launchy
chemlab (0.10.0)
chemlab (0.11.1)
colorize (~> 0.8)
i18n (~> 1.8)
rake (>= 12, < 14)
@ -340,7 +340,7 @@ DEPENDENCIES
allure-rspec (~> 2.20.0)
capybara (~> 3.39.2)
capybara-screenshot (~> 1.0.26)
chemlab (~> 0.10)
chemlab (~> 0.11, >= 0.11.1)
chemlab-library-www-gitlab-com (~> 0.1, >= 0.1.1)
confiner (~> 0.4)
deprecation_toolkit (~> 2.0.3)

168
scripts/elastic-migration Executable file
View File

@ -0,0 +1,168 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# Generate an Elastic migration file, spec and dictionary record with the current timestamp.
require 'yaml'
require 'fileutils'
require 'uri'
require 'readline'
require 'active_support/core_ext/string'
class ElasticMigrationCreator
attr_reader :options
Options = Struct.new(
:name,
:description,
:group,
:introduced_by_url,
:milestone,
:obsolete,
:marked_obsolete_by_url,
:marked_obsolete_in_milestone
)
def initialize
@options = Options.new
end
def execute
options.name = read_name
options.description = read_description
options.group = read_group
options.introduced_by_url = read_introduced_by_url
options.milestone = read_milestone
$stdout.puts "\e[32mcreated\e[0m #{file_path}"
$stdout.puts "\e[32mcreated\e[0m #{spec_file_path}"
$stdout.puts "\e[32mcreated\e[0m #{dictionary_file_path}"
write
$stdout.puts "\n=> Please consult the documentation for Advanced Search Migrations: #{documentation_reference}"
end
private
def read_name
read_variable('name', 'Name of the migration in CamelCase').camelize
end
def read_description
read_variable('description', 'Description of what the migration does')
end
def read_group
read_variable('group', 'The group introducing a feature flag, like: `global search`')
end
def read_milestone
milestone = File.read('VERSION')
milestone.gsub(/^(\d+\.\d+).*$/, '\1').chomp
end
def read_variable(name, description)
$stdout.puts "\n>> #{description}:"
loop do
variable = Readline.readline('?> ', false)&.strip
return variable unless variable.empty?
warn "Error: #{name} is required."
end
end
def read_introduced_by_url
$stdout.puts
$stdout.puts ">> URL of the MR introducing the migration (enter to skip):"
loop do
introduced_by_url = Readline.readline('?> ', false)&.strip
introduced_by_url = nil if introduced_by_url.empty?
return introduced_by_url if introduced_by_url.nil? || introduced_by_url.start_with?('https://')
warn 'Error: URL needs to start with https://'
end
end
def write
# create migration file
FileUtils.mkdir_p(File.dirname(file_path))
File.write(file_path, file_contents)
# create spec
FileUtils.mkdir_p(File.dirname(spec_file_path))
File.write(spec_file_path, spec_contents)
# create dictionary file
FileUtils.mkdir_p(File.dirname(dictionary_file_path))
File.write(dictionary_file_path, dictionary_contents)
end
def timestamp
@timestamp ||= Time.now.strftime('%Y%m%d%H%M%S')
end
def file_name
@file_name ||= "#{timestamp}_#{options.name.dup.underscore}"
end
def file_path
"ee/elastic/migrate/#{file_name}.rb"
end
def spec_file_path
"ee/spec/elastic/migrate/#{file_name}_spec.rb"
end
def dictionary_file_path
"ee/elastic/docs/#{file_name}.yml"
end
def file_contents
"# frozen_string_literal: true
class #{options.name} < Elastic::Migration
end
"
end
def spec_contents
"# frozen_string_literal: true
require 'spec_helper'
require_relative 'migration_shared_examples'
require File.expand_path('#{file_path}')
RSpec.describe #{options.name}, feature_category: :#{options.group.parameterize.underscore} do
let(:version) { #{timestamp} }
end
"
end
def dictionary_contents
dictionary_config_hash.to_yaml
end
def dictionary_config_hash
{
'name' => options.name,
'version' => timestamp,
'description' => options.description,
'group' => "group::#{options.group}",
'milestone' => options.milestone,
'introduced_by_url' => options.introduced_by_url,
'obsolete' => false,
'marked_obsolete_by_url' => nil,
'marked_obsolete_in_milestone' => nil
}
end
def documentation_reference
'https://docs.gitlab.com/ee/development/search/advanced_search_migration_styleguide.html'
end
end
ElasticMigrationCreator.new.execute if $PROGRAM_NAME == __FILE__
# vim: ft=ruby

View File

@ -196,23 +196,28 @@ RSpec.describe Projects::AutocompleteSourcesController do
end
end
shared_examples 'only public members are returned for public project' do
shared_examples 'returns all members of public project' do
before do
stub_feature_flags(disable_all_mention: false)
end
it 'only returns public members' do
it 'returns members including those from invited private groups' do
get :members, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issuable_type }
expect(members_by_username('all').symbolize_keys).to include(
username: 'all',
name: 'All Project and Group Members',
count: 1)
count: 2)
expect(members_by_username(user.username).symbolize_keys).to include(
type: user.class.name,
name: user.name,
avatar_url: user.avatar_url)
expect(members_by_username(invited_private_member.username).symbolize_keys).to include(
type: invited_private_member.class.name,
name: invited_private_member.name,
avatar_url: invited_private_member.avatar_url)
end
context 'when `disable_all_mention` FF is enabled' do
@ -234,7 +239,7 @@ RSpec.describe Projects::AutocompleteSourcesController do
let(:issuable_type) { private_issue.class.name }
end
it_behaves_like 'only public members are returned for public project' do
it_behaves_like 'returns all members of public project' do
let(:issuable_type) { issue.class.name }
end
end
@ -244,7 +249,7 @@ RSpec.describe Projects::AutocompleteSourcesController do
let(:issuable_type) { private_work_item.class.name }
end
it_behaves_like 'only public members are returned for public project' do
it_behaves_like 'returns all members of public project' do
let(:issuable_type) { work_item.class.name }
end
end

View File

@ -65,18 +65,14 @@ FactoryBot.define do
end
end
trait :allow_descendants_override_disabled_shared_runners do
allow_descendants_override_disabled_shared_runners { true }
end
trait :disabled_and_unoverridable do
trait :shared_runners_disabled_and_unoverridable do
shared_runners_disabled
allow_descendants_override_disabled_shared_runners { false }
end
trait :disabled_and_overridable do
trait :shared_runners_disabled_and_overridable do
shared_runners_disabled
allow_descendants_override_disabled_shared_runners
allow_descendants_override_disabled_shared_runners { true }
end
trait :shared_runners_enabled do

View File

@ -40,9 +40,5 @@ FactoryBot.define do
trait :shared_runners_disabled do
shared_runners_enabled { false }
end
trait :allow_descendants_override_disabled_shared_runners do
allow_descendants_override_disabled_shared_runners { true }
end
end
end

View File

@ -1,6 +1,6 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
import DiffInlineFindingsItem from '~/diffs/components/diff_inline_findings_item.vue';
import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
import { multipleFindingsArrCodeQualityScale } from '../mock_data/diff_code_quality';
@ -14,7 +14,7 @@ const findDescriptionLinkSection = () => wrapper.findByTestId('description-butto
describe('DiffCodeQuality', () => {
const createWrapper = ({ glFeatures = {}, link = true } = {}) => {
return shallowMountExtended(DiffCodeQualityItem, {
return shallowMountExtended(DiffInlineFindingsItem, {
propsData: {
finding: codeQualityFinding,
link,
@ -30,7 +30,7 @@ describe('DiffCodeQuality', () => {
expect(findIcon().exists()).toBe(true);
expect(findIcon().attributes()).toMatchObject({
class: `codequality-severity-icon ${SEVERITY_CLASSES[codeQualityFinding.severity]}`,
class: `inline-findings-severity-icon ${SEVERITY_CLASSES[codeQualityFinding.severity]}`,
name: SEVERITY_ICONS[codeQualityFinding.severity],
size: '12',
});

View File

@ -1,12 +1,12 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DiffInlineFindings from '~/diffs/components/diff_inline_findings.vue';
import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
import DiffInlineFindingsItem from '~/diffs/components/diff_inline_findings_item.vue';
import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
import { multipleCodeQualityNoSast } from '../mock_data/diff_code_quality';
let wrapper;
const heading = () => wrapper.findByTestId('diff-inline-findings-heading');
const diffCodeQualityItems = () => wrapper.findAllComponents(DiffCodeQualityItem);
const diffInlineFindingsItems = () => wrapper.findAllComponents(DiffInlineFindingsItem);
describe('DiffInlineFindings', () => {
const createWrapper = () => {
@ -23,10 +23,10 @@ describe('DiffInlineFindings', () => {
expect(heading().text()).toBe(NEW_CODE_QUALITY_FINDINGS);
});
it('renders the correct number of DiffCodeQualityItem components with correct props', () => {
it('renders the correct number of DiffInlineFindingsItem components with correct props', () => {
wrapper = createWrapper();
expect(diffCodeQualityItems()).toHaveLength(multipleCodeQualityNoSast.codeQuality.length);
expect(diffCodeQualityItems().wrappers[0].props('finding')).toEqual(
expect(diffInlineFindingsItems()).toHaveLength(multipleCodeQualityNoSast.codeQuality.length);
expect(diffInlineFindingsItems().wrappers[0].props('finding')).toEqual(
wrapper.props('findings')[0],
);
});

View File

@ -30,7 +30,7 @@ exports[`FindingsDrawer matches the snapshot 1`] = `
</span>
<gl-icon-stub
class="codequality-severity-icon gl-text-orange-300"
class="inline-findings-severity-icon gl-text-orange-300"
data-testid="findings-drawer-severity-icon"
name="severity-low"
size="12"

View File

@ -2,13 +2,8 @@ import { shallowMount } from '@vue/test-utils';
import { splitIntoChunks } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
import highlightMixin from '~/repository/mixins/highlight_mixin';
import LineHighlighter from '~/blob/line_highlighter';
import Tracking from '~/tracking';
import { TEXT_FILE_TYPE } from '~/repository/constants';
import {
EVENT_ACTION,
EVENT_LABEL_FALLBACK,
LINES_PER_CHUNK,
} from '~/vue_shared/components/source_viewer/constants';
import { LINES_PER_CHUNK } from '~/vue_shared/components/source_viewer/constants';
const lineHighlighter = new LineHighlighter();
jest.mock('~/blob/line_highlighter', () => jest.fn().mockReturnValue({ highlightHash: jest.fn() }));
@ -24,7 +19,7 @@ describe('HighlightMixin', () => {
const hash = '#L50';
const contentArray = Array.from({ length: 140 }, () => 'newline'); // simulate 140 lines of code
const rawTextBlob = contentArray.join('\n');
const languageMock = 'javascript';
const languageMock = 'json';
const createComponent = ({ fileType = TEXT_FILE_TYPE, language = languageMock } = {}) => {
const simpleViewer = { fileType };
@ -50,26 +45,13 @@ describe('HighlightMixin', () => {
describe('initHighlightWorker', () => {
const firstSeventyLines = contentArray.slice(0, LINES_PER_CHUNK).join('\n');
it('does not instruct worker if file is not a text file', () => {
it('does not instruct worker if file is not a JSON file', () => {
workerMock.postMessage.mockClear();
createComponent({ fileType: 'markdown' });
createComponent({ language: 'javascript' });
expect(workerMock.postMessage).not.toHaveBeenCalled();
});
it('tracks event if a language is not supported and does not instruct worker', () => {
const unsupportedLanguage = 'some_unsupported_language';
const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
jest.spyOn(Tracking, 'event');
workerMock.postMessage.mockClear();
createComponent({ language: unsupportedLanguage });
expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
expect(onErrorMock).toHaveBeenCalled();
expect(workerMock.postMessage).not.toHaveBeenCalled();
});
it('generates a chunk for the first 70 lines of raw text', () => {
expect(splitIntoChunks).toHaveBeenCalledWith(languageMock, firstSeventyLines);
});

View File

@ -1,16 +1,28 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import * as Sentry from '@sentry/browser';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { STATUS_CLOSED, STATUS_OPEN } from '~/service_desk/constants';
import getServiceDeskIssuesQuery from '~/service_desk/queries/get_service_desk_issues.query.graphql';
import getServiceDeskIssuesCountsQuery from '~/service_desk/queries/get_service_desk_issues_counts.query.graphql';
import ServiceDeskListApp from '~/service_desk/components/service_desk_list_app.vue';
import InfoBanner from '~/service_desk/components/info_banner.vue';
import {
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_SEARCH_WITHIN,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
getServiceDeskIssuesQueryResponse,
getServiceDeskIssuesCountsQueryResponse,
@ -24,6 +36,10 @@ describe('ServiceDeskListApp', () => {
Vue.use(VueApollo);
const defaultProvide = {
releasesPath: 'releases/path',
autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
hasIterationsFeature: true,
groupPath: 'group/path',
emptyStateSvgPath: 'empty-state.svg',
isProject: true,
isSignedIn: true,
@ -34,28 +50,31 @@ describe('ServiceDeskListApp', () => {
const defaultQueryResponse = getServiceDeskIssuesQueryResponse;
const mockServiceDeskIssuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse);
const mockServiceDeskIssuesCountsQueryResponse = jest
const mockServiceDeskIssuesQueryResponseHandler = jest
.fn()
.mockResolvedValue(defaultQueryResponse);
const mockServiceDeskIssuesCountsQueryResponseHandler = jest
.fn()
.mockResolvedValue(getServiceDeskIssuesCountsQueryResponse);
const findIssuableList = () => wrapper.findComponent(IssuableList);
const findInfoBanner = () => wrapper.findComponent(InfoBanner);
const findLabelsToken = () =>
findIssuableList()
.props('searchTokens')
.find((token) => token.type === TOKEN_TYPE_LABEL);
const mountComponent = ({
const createComponent = ({
provide = {},
data = {},
serviceDeskIssuesQueryResponse = mockServiceDeskIssuesQueryResponse,
serviceDeskIssuesCountsQueryResponse = mockServiceDeskIssuesCountsQueryResponse,
stubs = {},
mountFn = shallowMount,
serviceDeskIssuesQueryResponseHandler = mockServiceDeskIssuesQueryResponseHandler,
serviceDeskIssuesCountsQueryResponseHandler = mockServiceDeskIssuesCountsQueryResponseHandler,
} = {}) => {
const requestHandlers = [
[getServiceDeskIssuesQuery, serviceDeskIssuesQueryResponse],
[getServiceDeskIssuesCountsQuery, serviceDeskIssuesCountsQueryResponse],
[getServiceDeskIssuesQuery, serviceDeskIssuesQueryResponseHandler],
[getServiceDeskIssuesCountsQuery, serviceDeskIssuesCountsQueryResponseHandler],
];
return mountFn(ServiceDeskListApp, {
return shallowMount(ServiceDeskListApp, {
apolloProvider: createMockApollo(
requestHandlers,
{},
@ -75,15 +94,11 @@ describe('ServiceDeskListApp', () => {
...defaultProvide,
...provide,
},
data() {
return data;
},
stubs,
});
};
beforeEach(() => {
wrapper = mountComponent();
wrapper = createComponent();
return waitForPromises();
});
@ -107,23 +122,82 @@ describe('ServiceDeskListApp', () => {
expect(findInfoBanner().exists()).toBe(true);
});
it('does not render when Service Desk is not supported and has any number of issues', async () => {
wrapper = createComponent({ provide: { isServiceDeskSupported: false } });
await waitForPromises();
expect(findInfoBanner().exists()).toBe(false);
});
it('does not render, when there are no issues', async () => {
wrapper = mountComponent({ provide: { hasAnyIssues: false } });
wrapper = createComponent({ provide: { hasAnyIssues: false } });
await waitForPromises();
expect(findInfoBanner().exists()).toBe(false);
});
});
describe('Events', () => {
describe('when "click-tab" event is emitted by IssuableList', () => {
beforeEach(() => {
mountComponent();
describe('Tokens', () => {
const mockCurrentUser = {
id: 1,
name: 'Administrator',
username: 'root',
avatar_url: 'avatar/url',
};
findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
describe('when user is signed out', () => {
beforeEach(() => {
wrapper = createComponent({ provide: { isSignedIn: false } });
});
it('updates ui to the new tab', () => {
it('does not render My-Reaction or Confidential tokens', () => {
expect(findIssuableList().props('searchTokens')).not.toMatchObject([
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers: [mockCurrentUser] },
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers: [mockCurrentUser] },
{ type: TOKEN_TYPE_MY_REACTION },
{ type: TOKEN_TYPE_CONFIDENTIAL },
]);
});
});
describe('when all tokens are available', () => {
beforeEach(() => {
window.gon = {
current_user_id: mockCurrentUser.id,
current_user_fullname: mockCurrentUser.name,
current_username: mockCurrentUser.username,
current_user_avatar_url: mockCurrentUser.avatar_url,
};
wrapper = createComponent();
});
it('renders all tokens alphabetically', () => {
const preloadedUsers = [
{ ...mockCurrentUser, id: convertToGraphQLId(TYPENAME_USER, mockCurrentUser.id) },
];
expect(findIssuableList().props('searchTokens')).toMatchObject([
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
{ type: TOKEN_TYPE_CONFIDENTIAL },
{ type: TOKEN_TYPE_LABEL },
{ type: TOKEN_TYPE_MILESTONE },
{ type: TOKEN_TYPE_MY_REACTION },
{ type: TOKEN_TYPE_RELEASE },
{ type: TOKEN_TYPE_SEARCH_WITHIN },
]);
});
});
});
describe('Events', () => {
describe('when "click-tab" event is emitted by IssuableList', () => {
it('updates ui to the new tab', async () => {
createComponent();
findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
await nextTick();
expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED);
});
});
@ -131,13 +205,13 @@ describe('ServiceDeskListApp', () => {
describe('Errors', () => {
describe.each`
error | mountOption | message
${'fetching issues'} | ${'serviceDeskIssuesQueryResponse'} | ${ServiceDeskListApp.i18n.errorFetchingIssues}
${'fetching issue counts'} | ${'serviceDeskIssuesCountsQueryResponse'} | ${ServiceDeskListApp.i18n.errorFetchingCounts}
`('when there is an error $error', ({ mountOption, message }) => {
error | responseHandler | message
${'fetching issues'} | ${'serviceDeskIssuesQueryResponseHandler'} | ${ServiceDeskListApp.i18n.errorFetchingIssues}
${'fetching issue counts'} | ${'serviceDeskIssuesCountsQueryResponseHandler'} | ${ServiceDeskListApp.i18n.errorFetchingCounts}
`('when there is an error $error', ({ responseHandler, message }) => {
beforeEach(() => {
wrapper = mountComponent({
[mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')),
wrapper = createComponent({
[responseHandler]: jest.fn().mockRejectedValue(new Error('ERROR')),
});
return waitForPromises();
});
@ -148,4 +222,30 @@ describe('ServiceDeskListApp', () => {
});
});
});
describe('When providing token for labels', () => {
it('passes function to fetchLatestLabels property if frontend caching is enabled', () => {
wrapper = createComponent({
provide: {
glFeatures: {
frontendCaching: true,
},
},
});
expect(typeof findLabelsToken().fetchLatestLabels).toBe('function');
});
it('passes null to fetchLatestLabels property if frontend caching is disabled', () => {
wrapper = createComponent({
provide: {
glFeatures: {
frontendCaching: false,
},
},
});
expect(findLabelsToken().fetchLatestLabels).toBe(null);
});
});
});

View File

@ -14,6 +14,7 @@ import {
LEGACY_FALLBACKS,
CODEOWNERS_FILE_NAME,
CODEOWNERS_LANGUAGE,
SVELTE_LANGUAGE,
} from '~/vue_shared/components/source_viewer/constants';
import waitForPromises from 'helpers/wait_for_promises';
import LineHighlighter from '~/blob/line_highlighter';
@ -120,6 +121,33 @@ describe('Source Viewer component', () => {
);
});
describe('sub-languages', () => {
const languageDefinition = {
subLanguage: 'xml',
contains: [{ subLanguage: 'javascript' }, { subLanguage: 'typescript' }],
};
beforeEach(async () => {
jest.spyOn(hljs, 'getLanguage').mockReturnValue(languageDefinition);
createComponent();
await waitForPromises();
});
it('registers the primary sub-language', () => {
expect(hljs.registerLanguage).toHaveBeenCalledWith(
languageDefinition.subLanguage,
expect.any(Function),
);
});
it.each(languageDefinition.contains)(
'registers the rest of the sub-languages',
({ subLanguage }) => {
expect(hljs.registerLanguage).toHaveBeenCalledWith(subLanguage, expect.any(Function));
},
);
});
it('registers json language definition if fileType is package_json', async () => {
await createComponent({ language: 'json', fileType: 'package_json' });
const languageDefinition = await import(`highlight.js/lib/languages/json`);
@ -146,6 +174,18 @@ describe('Source Viewer component', () => {
);
});
it('registers svelte language definition if file name ends with .svelte', async () => {
await createComponent({ name: `component.${SVELTE_LANGUAGE}` });
const languageDefinition = await import(
'~/vue_shared/components/source_viewer/languages/svelte'
);
expect(hljs.registerLanguage).toHaveBeenCalledWith(
SVELTE_LANGUAGE,
languageDefinition.default,
);
});
it('highlights the first chunk', () => {
expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
expect(findChunks().at(0).props('isFirstChunk')).toBe(true);

View File

@ -218,9 +218,9 @@ RSpec.describe Ci::RunnersHelper, feature_category: :runner_fleet do
using RSpec::Parameterized::TableSyntax
where(:shared_runners_setting, :is_disabled_and_unoverridable) do
:shared_runners_enabled | "false"
:disabled_and_overridable | "false"
:disabled_and_unoverridable | "true"
:shared_runners_enabled | "false"
:shared_runners_disabled_and_overridable | "false"
:shared_runners_disabled_and_unoverridable | "true"
end
with_them do

View File

@ -516,12 +516,32 @@ RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :system_access do
set_bearer_token(token_3.token)
end
it 'revokes the latest rotated token' do
expect(token_1).not_to be_revoked
context 'with url related to access tokens' do
before do
set_header('SCRIPT_NAME', "/personal_access_tokens/#{token_3.id}/rotate")
end
expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::RevokedError)
it 'revokes the latest rotated token' do
expect(token_1).not_to be_revoked
expect(token_1.reload).to be_revoked
expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::RevokedError)
expect(token_1.reload).to be_revoked
end
end
context 'with url not related to access tokens' do
before do
set_header('SCRIPT_NAME', '/epics/1')
end
it 'does not revoke the latest rotated token' do
expect(token_1).not_to be_revoked
expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::RevokedError)
expect(token_1.reload).not_to be_revoked
end
end
context 'when the feature flag is disabled' do

View File

@ -60,7 +60,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer, feature_cate
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
let_it_be(:group) do
create(:group, :disabled_and_unoverridable).tap { |g| g.add_maintainer(user) }
create(:group, :shared_runners_disabled_and_unoverridable).tap { |g| g.add_maintainer(user) }
end
before do

View File

@ -2389,7 +2389,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
end
context 'when parent has shared runners disabled but allows override' do
let(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
let(:parent) { create(:group, :shared_runners_disabled_and_overridable) }
let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
it 'is valid' do
@ -2415,7 +2415,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
context 'when namespace is a group' do
context 'without a parent' do
context 'with shared runners disabled' do
let(:namespace) { build(:group, :allow_descendants_override_disabled_shared_runners, :shared_runners_disabled) }
let(:namespace) { build(:group, :shared_runners_disabled_and_overridable) }
it 'is valid' do
expect(namespace).to be_valid
@ -2423,13 +2423,13 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
end
context 'with shared runners enabled' do
let(:namespace) { create(:namespace) }
let(:namespace) { build(:group) }
it 'is invalid' do
namespace.allow_descendants_override_disabled_shared_runners = true
expect(namespace).to be_invalid
expect(namespace.errors[:allow_descendants_override_disabled_shared_runners]).to include('cannot be changed if shared runners are enabled')
expect(namespace.errors[:allow_descendants_override_disabled_shared_runners]).to include('can not be true if shared runners are enabled')
end
end
end
@ -2437,7 +2437,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
context 'with a parent' do
context 'when parent does not allow shared runners' do
let(:parent) { create(:group, :shared_runners_disabled) }
let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) }
let(:group) { build(:group, :shared_runners_disabled_and_overridable, parent_id: parent.id) }
it 'is invalid' do
expect(group).to be_invalid
@ -2447,7 +2447,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
context 'when parent allows shared runners and setting to true' do
let(:parent) { create(:group, shared_runners_enabled: true) }
let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) }
let(:group) { build(:group, :shared_runners_disabled_and_overridable, parent_id: parent.id) }
it 'is valid' do
expect(group).to be_valid

View File

@ -7026,10 +7026,10 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
where(:shared_runners_setting, :project_shared_runners_enabled, :valid_record) do
:shared_runners_enabled | true | true
:shared_runners_enabled | false | true
:disabled_and_overridable | true | true
:disabled_and_overridable | false | true
:disabled_and_unoverridable | true | false
:disabled_and_unoverridable | false | true
:shared_runners_disabled_and_overridable | true | true
:shared_runners_disabled_and_overridable | false | true
:shared_runners_disabled_and_unoverridable | true | false
:shared_runners_disabled_and_unoverridable | false | true
end
with_them do

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Nuget::V2::ServiceIndexPresenter, feature_category: :package_registry do
let_it_be(:project) { build_stubbed(:project) }
let_it_be(:group) { build_stubbed(:group) }
describe '#xml' do
let(:project_or_group) { project }
let(:presenter) { described_class.new(project_or_group) }
let(:xml_doc) { Nokogiri::XML::Document.parse(presenter.xml.to_xml) }
let(:service_node) { xml_doc.at_xpath('//xmlns:service') }
it { expect(xml_doc.root.name).to eq('service') }
it 'includes the workspace and collection nodes' do
workspace = xml_doc.at_xpath('//xmlns:service/xmlns:workspace')
collection = xml_doc.at_xpath('//xmlns:service/xmlns:workspace/xmlns:collection')
expect(workspace).to be_present
expect(workspace.children).to include(collection)
expect(collection).to be_present
end
it 'sets the appropriate XML namespaces on the root node' do
expect(service_node.namespaces['xmlns']).to eq('http://www.w3.org/2007/app')
expect(service_node.namespaces['xmlns:atom']).to eq('http://www.w3.org/2005/Atom')
end
context 'when the presenter is initialized with a project' do
it 'sets the XML base path correctly for a project scope' do
expect(service_node['xml:base']).to include(expected_xml_base(project))
end
end
context 'when the presenter is initialized with a group' do
let(:project_or_group) { group }
it 'sets the XML base path correctly for a group scope' do
expect(service_node['xml:base']).to include(expected_xml_base(group))
end
end
end
def expected_xml_base(project_or_group)
case project_or_group
when Project
api_v4_projects_packages_nuget_v2_path(id: project_or_group.id)
when Group
api_v4_groups___packages_nuget_v2_path(id: project_or_group.id)
end
end
end

View File

@ -31,6 +31,12 @@ RSpec.describe API::NugetGroupPackages, feature_category: :package_registry do
end
end
describe 'GET /api/v4/groups/:id/-/packages/nuget/v2' do
it_behaves_like 'handling nuget service requests', v2: true do
let(:url) { "/groups/#{target.id}/-/packages/nuget/v2" }
end
end
describe 'GET /api/v4/groups/:id/-/packages/nuget/metadata/*package_name/index' do
it_behaves_like 'handling nuget metadata requests with package name',
example_names_with_status:

View File

@ -42,6 +42,14 @@ RSpec.describe API::NugetProjectPackages, feature_category: :package_registry do
it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
describe 'GET /api/v4/projects/:id/packages/nuget/v2' do
let(:url) { "/projects/#{target.id}/packages/nuget/v2" }
it_behaves_like 'handling nuget service requests', v2: true
it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/index' do
let(:url) { "/projects/#{target.id}/packages/nuget/metadata/#{package_name}/index.json" }
@ -183,75 +191,39 @@ RSpec.describe API::NugetProjectPackages, feature_category: :package_registry do
end
describe 'PUT /api/v4/projects/:id/packages/nuget/authorize' do
include_context 'workhorse headers'
let(:url) { "/projects/#{target.id}/packages/nuget/authorize" }
let(:headers) { {} }
subject { put api(url), headers: headers }
it_behaves_like 'nuget authorize upload endpoint'
it_behaves_like 'nuget authorize upload endpoint' do
let(:url) { "/projects/#{target.id}/packages/nuget/authorize" }
end
end
describe 'PUT /api/v4/projects/:id/packages/nuget' do
include_context 'workhorse headers'
let_it_be(:file_name) { 'package.nupkg' }
let(:url) { "/projects/#{target.id}/packages/nuget" }
let(:headers) { {} }
let(:params) { { package: temp_file(file_name) } }
let(:file_key) { :package }
let(:send_rewritten_field) { true }
subject do
workhorse_finalize(
api(url),
method: :put,
file_key: file_key,
params: params,
headers: headers,
send_rewritten_field: send_rewritten_field
)
it_behaves_like 'nuget upload endpoint' do
let(:url) { "/projects/#{target.id}/packages/nuget" }
end
it_behaves_like 'nuget upload endpoint'
end
describe 'PUT /api/v4/projects/:id/packages/nuget/symbolpackage/authorize' do
include_context 'workhorse headers'
let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage/authorize" }
let(:headers) { {} }
subject { put api(url), headers: headers }
it_behaves_like 'nuget authorize upload endpoint'
it_behaves_like 'nuget authorize upload endpoint' do
let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage/authorize" }
end
end
describe 'PUT /api/v4/projects/:id/packages/nuget/symbolpackage' do
include_context 'workhorse headers'
let_it_be(:file_name) { 'package.snupkg' }
let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage" }
let(:headers) { {} }
let(:params) { { package: temp_file(file_name) } }
let(:file_key) { :package }
let(:send_rewritten_field) { true }
subject do
workhorse_finalize(
api(url),
method: :put,
file_key: file_key,
params: params,
headers: headers,
send_rewritten_field: send_rewritten_field
)
it_behaves_like 'nuget upload endpoint', symbol_package: true do
let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage" }
end
end
it_behaves_like 'nuget upload endpoint', symbol_package: true
describe 'PUT /api/v4/projects/:id/packages/nuget/v2/authorize' do
it_behaves_like 'nuget authorize upload endpoint' do
let(:url) { "/projects/#{target.id}/packages/nuget/v2/authorize" }
end
end
describe 'PUT /api/v4/projects/:id/packages/nuget/v2' do
it_behaves_like 'nuget upload endpoint' do
let(:url) { "/projects/#{target.id}/packages/nuget/v2" }
end
end
def update_visibility_to(visibility)

View File

@ -67,6 +67,21 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
.and change { sub_group.shared_runners_enabled }.from(false).to(true)
.and change { project.shared_runners_enabled }.from(false).to(true)
end
context 'when already allowing descendants to override' do
let(:group) { create(:group, :shared_runners_disabled_and_overridable) }
it 'enables shared Runners for itself and descendants' do
expect do
expect(subject[:status]).to eq(:success)
reload_models(group, sub_group, project)
end.to change { group.shared_runners_enabled }.from(false).to(true)
.and change { group.allow_descendants_override_disabled_shared_runners }.from(true).to(false)
.and change { sub_group.shared_runners_enabled }.from(false).to(true)
.and change { project.shared_runners_enabled }.from(false).to(true)
end
end
end
context 'when group has pending builds' do
@ -101,7 +116,7 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
context 'disable shared Runners' do
let!(:group) { create(:group) }
let!(:sub_group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent: group) }
let!(:sub_group) { create(:group, :shared_runners_disabled_and_overridable, parent: group) }
let!(:sub_group2) { create(:group, parent: group) }
let!(:project) { create(:project, group: group, shared_runners_enabled: true) }
let!(:project2) { create(:project, group: sub_group2, shared_runners_enabled: true) }
@ -124,7 +139,7 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
end
context 'with override on self' do
let(:group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
let(:group) { create(:group, :shared_runners_disabled_and_overridable) }
it 'disables it' do
expect do
@ -172,7 +187,7 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
end
context 'when ancestor disables shared Runners but allows to override' do
let!(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
let!(:parent) { create(:group, :shared_runners_disabled_and_overridable) }
let!(:group) { create(:group, :shared_runners_disabled, parent: parent) }
let!(:project) { create(:project, shared_runners_enabled: false, group: group) }

View File

@ -120,120 +120,88 @@ RSpec.describe Projects::ParticipantsService, feature_category: :groups_and_proj
describe '#project_members' do
subject(:usernames) { service.project_members.map { |member| member[:username] } }
shared_examples 'return project members' do
context 'when there is a project in group namespace' do
let_it_be(:public_group) { create(:group, :public) }
let_it_be(:public_project) { create(:project, :public, namespace: public_group) }
context 'when there is a project in group namespace' do
let_it_be(:public_group) { create(:group, :public) }
let_it_be(:public_project, reload: true) { create(:project, :public, namespace: public_group) }
let_it_be(:public_group_owner) { create(:user) }
let_it_be(:public_group_owner) { create(:user) }
let(:service) { described_class.new(public_project, create(:user)) }
let(:service) { described_class.new(public_project, create(:user)) }
before do
public_group.add_owner(public_group_owner)
end
it 'returns members of a group' do
expect(usernames).to include(public_group_owner.username)
end
before do
public_group.add_owner(public_group_owner)
end
context 'when there is a private group and a public project' do
let_it_be(:public_group) { create(:group, :public) }
let_it_be(:private_group) { create(:group, :private, :nested) }
let_it_be(:public_project) { create(:project, :public, namespace: public_group) }
let_it_be(:project_issue) { create(:issue, project: public_project) }
let_it_be(:public_group_owner) { create(:user) }
let_it_be(:private_group_member) { create(:user) }
let_it_be(:public_project_maintainer) { create(:user) }
let_it_be(:private_group_owner) { create(:user) }
let_it_be(:group_ancestor_owner) { create(:user) }
before_all do
public_group.add_owner public_group_owner
private_group.add_developer private_group_member
public_project.add_maintainer public_project_maintainer
private_group.add_owner private_group_owner
private_group.parent.add_owner group_ancestor_owner
end
context 'when the private group is invited to the public project' do
before_all do
create(:project_group_link, group: private_group, project: public_project)
end
context 'when a user who is outside the public project and the private group is signed in' do
let(:service) { described_class.new(public_project, create(:user)) }
it 'does not return the private group' do
expect(usernames).not_to include(private_group.name)
end
it 'does not return private group members' do
expect(usernames).not_to include(private_group_member.username)
end
it 'returns the project maintainer' do
expect(usernames).to include(public_project_maintainer.username)
end
it 'returns project members from an invited public group' do
invited_public_group = create(:group, :public)
invited_public_group.add_owner create(:user)
create(:project_group_link, group: invited_public_group, project: public_project)
expect(usernames).to include(invited_public_group.users.first.username)
end
it 'does not return ancestors of the private group' do
expect(usernames).not_to include(group_ancestor_owner.username)
end
end
context 'when public project maintainer is signed in' do
let(:service) { described_class.new(public_project, public_project_maintainer) }
it 'returns private group members' do
expect(usernames).to include(private_group_member.username)
end
it 'returns members of the ancestral groups of the private group' do
expect(usernames).to include(group_ancestor_owner.username)
end
end
context 'when private group owner is signed in' do
let(:service) { described_class.new(public_project, private_group_owner) }
it 'returns private group members' do
expect(usernames).to include(private_group_member.username)
end
it 'returns ancestors of the the private group' do
expect(usernames).to include(group_ancestor_owner.username)
end
end
context 'when the namespace owner of the public project is signed in' do
let(:service) { described_class.new(public_project, public_group_owner) }
it 'returns private group members' do
expect(usernames).to include(private_group_member.username)
end
it 'does not return members of the ancestral groups of the private group' do
expect(usernames).to include(group_ancestor_owner.username)
end
end
end
it 'returns members of a group' do
expect(usernames).to include(public_group_owner.username)
end
end
it_behaves_like 'return project members'
context 'when there is a private group and a public project' do
let_it_be(:public_group) { create(:group, :public) }
let_it_be(:private_group) { create(:group, :private, :nested) }
let_it_be(:public_project, reload: true) { create(:project, :public, namespace: public_group) }
let_it_be(:project_issue) { create(:issue, project: public_project) }
let_it_be(:public_group_owner) { create(:user) }
let_it_be(:private_group_member) { create(:user) }
let_it_be(:public_project_maintainer) { create(:user) }
let_it_be(:private_group_owner) { create(:user) }
let_it_be(:group_ancestor_owner) { create(:user) }
before_all do
public_group.add_owner public_group_owner
private_group.add_developer private_group_member
public_project.add_maintainer public_project_maintainer
private_group.add_owner private_group_owner
private_group.parent.add_owner group_ancestor_owner
end
context 'when the private group is invited to the public project' do
before_all do
create(:project_group_link, group: private_group, project: public_project)
end
let(:service) { described_class.new(public_project, create(:user)) }
it 'does not return the private group' do
expect(usernames).not_to include(private_group.name)
end
it 'returns private group members' do
expect(usernames).to include(private_group_member.username)
end
it 'returns the project maintainer' do
expect(usernames).to include(public_project_maintainer.username)
end
it 'returns project members from an invited public group' do
invited_public_group = create(:group, :public)
invited_public_group.add_owner create(:user)
create(:project_group_link, group: invited_public_group, project: public_project)
expect(usernames).to include(invited_public_group.users.first.username)
end
it 'returns members of the ancestral groups of the private group' do
expect(usernames).to include(group_ancestor_owner.username)
end
it 'returns invited group members of the private group' do
invited_group = create(:group, :public)
create(:group_group_link, shared_group: private_group, shared_with_group: invited_group)
other_user = create(:user)
invited_group.add_guest(other_user)
expect(usernames).to include(other_user.username)
end
end
end
end
end

View File

@ -570,12 +570,12 @@ RSpec.describe Projects::TransferService, feature_category: :groups_and_projects
using RSpec::Parameterized::TableSyntax
where(:project_shared_runners_enabled, :shared_runners_setting, :expected_shared_runners_enabled) do
true | :disabled_and_unoverridable | false
false | :disabled_and_unoverridable | false
true | :disabled_and_overridable | true
false | :disabled_and_overridable | false
true | :shared_runners_enabled | true
false | :shared_runners_enabled | false
true | :shared_runners_disabled_and_unoverridable | false
false | :shared_runners_disabled_and_unoverridable | false
true | :shared_runners_disabled_and_overridable | true
false | :shared_runners_disabled_and_overridable | false
true | :shared_runners_enabled | true
false | :shared_runners_enabled | false
end
with_them do

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
RSpec.shared_examples 'handling nuget service requests' do
RSpec.shared_examples 'handling nuget service requests' do |v2: false|
subject { get api(url) }
it { is_expected.to have_request_urgency(v2 ? :low : :default) }
context 'with valid target' do
using RSpec::Parameterized::TableSyntax
@ -20,15 +22,17 @@ RSpec.shared_examples 'handling nuget service requests' do
end
with_them do
let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: :anonymous) }
subject { get api(url) }
let(:snowplow_gitlab_standard_context) do
snowplow_context(user_role: :anonymous).tap do |ctx|
ctx[:feed] = 'v2' if v2
end
end
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member], v2
end
end

View File

@ -18,7 +18,7 @@ RSpec.shared_examples 'rejects nuget packages access' do |user_type, status, add
end
end
RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true|
RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true, v2 = false|
context "for user type #{user_type}" do
before do
target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
@ -28,15 +28,22 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu
it_behaves_like 'a package tracking event', 'API::NugetPackages', 'cli_metadata'
it 'returns a valid json response' do
it 'returns a valid json or xml response' do
subject
expect(response.media_type).to eq('application/json')
expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index')
expect(json_response).to be_a(Hash)
if v2
expect(response.media_type).to eq('application/xml')
expect(body).to have_xpath('//service')
.and have_xpath('//service/workspace')
.and have_xpath('//service/workspace/collection[@href]')
else
expect(response.media_type).to eq('application/json')
expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index')
expect(json_response).to be_a(Hash)
end
end
context 'with invalid format' do
context 'with invalid format', unless: v2 do
let(:url) { "/#{target_type}/#{target.id}/packages/nuget/index.xls" }
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
@ -439,6 +446,13 @@ end
RSpec.shared_examples 'nuget authorize upload endpoint' do
using RSpec::Parameterized::TableSyntax
include_context 'workhorse headers'
let(:headers) { {} }
subject { put api(url), headers: headers }
it { is_expected.to have_request_urgency(:low) }
context 'with valid project' do
where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do
@ -517,6 +531,26 @@ end
RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
using RSpec::Parameterized::TableSyntax
include_context 'workhorse headers'
let(:headers) { {} }
let(:file_name) { symbol_package ? 'package.snupkg' : 'package.nupkg' }
let(:params) { { package: temp_file(file_name) } }
let(:file_key) { :package }
let(:send_rewritten_field) { true }
subject do
workhorse_finalize(
api(url),
method: :put,
file_key: file_key,
params: params,
headers: headers,
send_rewritten_field: send_rewritten_field
)
end
it { is_expected.to have_request_urgency(:low) }
context 'with valid project' do
where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do
@ -573,7 +607,12 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
end
let(:headers) { user_headers.merge(workhorse_headers) }
let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace, property: 'i_package_nuget_user' } }
let(:snowplow_gitlab_standard_context) do
{ project: project, user: user, namespace: project.namespace, property: 'i_package_nuget_user' }.tap do |ctx|
ctx[:feed] = 'v2' if url.include?('nuget/v2')
end
end
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
@ -604,4 +643,16 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
it_behaves_like 'returning response status', :bad_request
end
context 'when ObjectStorage::RemoteStoreError is raised' do
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
before do
allow_next_instance_of(::Packages::CreatePackageFileService) do |instance|
allow(instance).to receive(:execute).and_raise(ObjectStorage::RemoteStoreError)
end
end
it_behaves_like 'returning response status', :forbidden
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'projects/pages/_pages_settings', feature_category: :pages do
let_it_be(:project) { build_stubbed(:project, :repository) }
let_it_be(:user) { build_stubbed(:user) }
before do
assign(:project, project)
allow(view).to receive(:current_user).and_return(user)
end
context 'for pages unique domain' do
it 'shows the unique domain toggle' do
render
expect(rendered).to have_content('Use unique domain')
end
context 'when pages_unique_domain feature flag is disabled' do
it 'does not show the unique domain toggle' do
stub_feature_flags(pages_unique_domain: false)
# We have to use `view.render` because `render` causes issues
# https://github.com/rails/rails/issues/41320
expect(view.render('projects/pages/pages_settings')).to be_nil
end
end
end
end

View File

@ -285,6 +285,9 @@ func configureRoutes(u *upstream) {
// NuGet Artifact Repository
u.route("PUT", apiProjectPattern+`/packages/nuget/`, mimeMultipartUploader),
// NuGet v2 Artifact Repository
u.route("PUT", apiProjectPattern+`/packages/nuget/v2`, mimeMultipartUploader),
// PyPI Artifact Repository
u.route("POST", apiProjectPattern+`/packages/pypi`, mimeMultipartUploader),

View File

@ -159,6 +159,9 @@ func TestAcceleratedUpload(t *testing.T) {
{"PUT", "/api/v4/projects/9001/packages/nuget/v1/files", true},
{"PUT", "/api/v4/projects/group%2Fproject/packages/nuget/v1/files", true},
{"PUT", "/api/v4/projects/group%2Fsubgroup%2Fproject/packages/nuget/v1/files", true},
{"PUT", "/api/v4/projects/9001/packages/nuget/v2/files", true},
{"PUT", "/api/v4/projects/group%2Fproject/packages/nuget/v2/files", true},
{"PUT", "/api/v4/projects/group%2Fsubgroup%2Fproject/packages/nuget/v2/files", true},
{"POST", `/api/v4/groups/import`, true},
{"POST", `/api/v4/groups/import/`, true},
{"POST", `/api/v4/projects/import`, true},
@ -289,6 +292,8 @@ func TestUnacceleratedUploads(t *testing.T) {
{"POST", `/api/v4/projects/group/project/wikis/attachments`},
{"PUT", "/api/v4/projects/group/subgroup/project/packages/nuget/v1/files"},
{"PUT", "/api/v4/projects/group/project/packages/nuget/v1/files"},
{"POST", "/api/v4/projects/group/subgroup/project/packages/nuget/v2/files"},
{"POST", "/api/v4/projects/group/project/packages/nuget/v2/files"},
{"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`},
{"POST", `/api/v4/projects/group/project/packages/pypi`},
{"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`},