Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-07-01 00:12:41 +00:00
parent e023756b39
commit d592691fa5
55 changed files with 470 additions and 136 deletions

View File

@ -526,7 +526,6 @@ Layout/LineLength:
- 'ee/app/helpers/ee/notes_helper.rb'
- 'ee/app/helpers/ee/profiles_helper.rb'
- 'ee/app/helpers/ee/subscribable_banner_helper.rb'
- 'ee/app/helpers/epics_helper.rb'
- 'ee/app/helpers/gitlab_subscriptions/upcoming_reconciliation_helper.rb'
- 'ee/app/helpers/license_helper.rb'
- 'ee/app/helpers/projects/on_demand_scans_helper.rb'
@ -1222,7 +1221,6 @@ Layout/LineLength:
- 'ee/spec/lib/gitlab/custom_file_templates_spec.rb'
- 'ee/spec/lib/gitlab/data_builder/vulnerability_spec.rb'
- 'ee/spec/lib/gitlab/elastic/group_search_results_spec.rb'
- 'ee/spec/lib/gitlab/elastic/snippet_search_results_spec.rb'
- 'ee/spec/lib/gitlab/email/handler/create_note_handler_spec.rb'
- 'ee/spec/lib/gitlab/expiring_subscription_message_spec.rb'
- 'ee/spec/lib/gitlab/geo/event_gap_tracking_spec.rb'

View File

@ -318,7 +318,6 @@ RSpec/ContextWording:
- 'ee/spec/lib/gitlab/code_owners/users_loader_spec.rb'
- 'ee/spec/lib/gitlab/custom_file_templates_spec.rb'
- 'ee/spec/lib/gitlab/elastic/group_search_results_spec.rb'
- 'ee/spec/lib/gitlab/elastic/snippet_search_results_spec.rb'
- 'ee/spec/lib/gitlab/email/handler/create_note_handler_spec.rb'
- 'ee/spec/lib/gitlab/expiring_subscription_message_spec.rb'
- 'ee/spec/lib/gitlab/geo/cron_manager_spec.rb'

View File

@ -553,7 +553,6 @@ RSpec/FeatureCategory:
- 'ee/spec/lib/gitlab/custom_file_templates_spec.rb'
- 'ee/spec/lib/gitlab/customers_dot/jwt_spec.rb'
- 'ee/spec/lib/gitlab/elastic/elasticsearch_enabled_cache_spec.rb'
- 'ee/spec/lib/gitlab/elastic/snippet_search_results_spec.rb'
- 'ee/spec/lib/gitlab/email/handler/create_note_handler_spec.rb'
- 'ee/spec/lib/gitlab/expiring_subscription_message_spec.rb'
- 'ee/spec/lib/gitlab/favicon_spec.rb'

View File

@ -273,7 +273,6 @@ RSpec/NamedSubject:
- 'ee/spec/lib/ee/sidebars/projects/menus/security_compliance_menu_spec.rb'
- 'ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb'
- 'ee/spec/lib/elastic/latest/application_instance_proxy_spec.rb'
- 'ee/spec/lib/elastic/latest/epic_class_proxy_spec.rb'
- 'ee/spec/lib/elastic/latest/epic_instance_proxy_spec.rb'
- 'ee/spec/lib/elastic/latest/git_instance_proxy_spec.rb'
- 'ee/spec/lib/elastic/latest/routing_spec.rb'

View File

@ -1,4 +1,5 @@
import { __ } from '~/locale';
import groupsEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/empty-state/empty-groups-md.svg?url';
import { s__, __ } from '~/locale';
import {
SORT_LABEL_NAME,
SORT_LABEL_CREATED,
@ -7,6 +8,7 @@ import {
} from '~/groups_projects/constants';
import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
import { formatGraphQLGroups } from '~/vue_shared/components/groups_list/formatter';
import ResourceListsEmptyState from '~/vue_shared/components/resource_lists/empty_state.vue';
import adminGroupsQuery from './graphql/queries/groups.query.graphql';
const baseTab = {
@ -24,6 +26,7 @@ const baseTab = {
listItemClass: 'gl-px-5',
showGroupIcon: true,
},
emptyStateComponent: ResourceListsEmptyState,
query: adminGroupsQuery,
queryPath: 'groups',
};
@ -34,6 +37,14 @@ export const ACTIVE_TAB = {
value: 'active',
variables: { active: true },
countsQueryPath: 'active',
emptyStateComponentProps: {
svgPath: groupsEmptyStateIllustration,
title: s__("Groups|You don't have any active groups yet."),
description: s__(
'Organization|A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
),
'data-testid': 'groups-empty-state',
},
};
export const INACTIVE_TAB = {
@ -42,6 +53,11 @@ export const INACTIVE_TAB = {
value: 'inactive',
variables: { active: false },
countsQueryPath: 'inactive',
emptyStateComponentProps: {
svgPath: groupsEmptyStateIllustration,
title: s__("Groups|You don't have any inactive groups."),
description: s__('Groups|Groups that are archived or pending deletion will appear here.'),
},
};
export const SORT_OPTION_NAME = {

View File

@ -2,12 +2,14 @@
import { RUNNER_TYPES } from '../constants';
import RequiredFields from './runner_create_wizard_required_fields.vue';
import OptionalFields from './runner_create_wizard_optional_fields.vue';
import RunnerRegistration from './runner_create_wizard_registration.vue';
export default {
name: 'RunnerCreateWizard',
components: {
RequiredFields,
OptionalFields,
RunnerRegistration,
},
props: {
runnerType: {
@ -58,4 +60,9 @@ export default {
@next="onNext"
@back="onBack"
/>
<runner-registration
v-else-if="currentStep === 3"
:current-step="currentStep"
:steps-total="$options.stepsTotal"
/>
</template>

View File

@ -1,12 +1,19 @@
<script>
import { GlForm, GlButton, GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
import { createAlert } from '~/alert';
import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
import MultipleChoiceSelector from '~/vue_shared/components/multiple_choice_selector.vue';
import MultipleChoiceSelectorItem from '~/vue_shared/components/multiple_choice_selector_item.vue';
import runnerCreateMutation from '~/ci/runner/graphql/new/runner_create.mutation.graphql';
import { modelToUpdateMutationVariables } from 'ee_else_ce/ci/runner/runner_update_form_utils';
import { captureException } from '../sentry_utils';
import {
DEFAULT_ACCESS_LEVEL,
ACCESS_LEVEL_NOT_PROTECTED,
ACCESS_LEVEL_REF_PROTECTED,
GROUP_TYPE,
PROJECT_TYPE,
I18N_CREATE_ERROR,
} from '../constants';
export default {
@ -53,10 +60,30 @@ export default {
runUntagged: this.runUntagged,
locked: false,
tagList: this.tags,
maximumTimeout: '',
maximumTimeout: null,
},
saving: false,
};
},
computed: {
mutationInput() {
const { input } = modelToUpdateMutationVariables(this.runner);
if (this.runnerType === GROUP_TYPE) {
return {
...input,
groupId: this.groupId,
};
}
if (this.runnerType === PROJECT_TYPE) {
return {
...input,
projectId: this.projectId,
};
}
return input;
},
},
methods: {
onCheckboxesInput(checked) {
if (checked.includes(ACCESS_LEVEL_REF_PROTECTED))
@ -65,12 +92,55 @@ export default {
this.runner.paused = checked.includes('paused');
},
async onSubmit() {
this.saving = true;
this.runner.maximumTimeout = parseInt(this.runner.maximumTimeout, 10);
try {
const {
data: {
runnerCreate: { errors, runner },
},
} = await this.$apollo.mutate({
mutation: runnerCreateMutation,
variables: {
input: this.mutationInput,
},
});
if (errors?.length) {
this.onError(new Error(errors.join(' ')), true);
return;
}
if (!runner?.ephemeralRegisterUrl) {
// runner is missing information, report issue and
// fail navigation to register page.
this.onError(new Error(I18N_CREATE_ERROR));
return;
}
this.$emit('next');
// destroy the alert
createAlert({ message: null }).dismiss();
} catch (error) {
this.onError(error);
}
},
onError(error, isValidationError = false) {
if (!isValidationError) {
captureException({ error, component: this.$options.name });
}
createAlert({ message: error.message });
this.saving = false;
},
},
ACCESS_LEVEL_REF_PROTECTED,
};
</script>
<template>
<gl-form>
<gl-form @submit.prevent="onSubmit">
<multi-step-form-template
:title="s__('Runners|Optional configuration details')"
:current-step="currentStep"
@ -79,7 +149,7 @@ export default {
<template #form>
<multiple-choice-selector class="gl-mb-5" @input="onCheckboxesInput">
<multiple-choice-selector-item
:value="ACCESS_LEVEL_REF_PROTECTED"
:value="$options.ACCESS_LEVEL_REF_PROTECTED"
:title="s__('Runners|Protected')"
:description="s__('Runners|Use the runner on pipelines for protected branches only.')"
/>
@ -143,12 +213,12 @@ export default {
</gl-form-group>
</template>
<template #next>
<!-- [Next step] button will be un-disabled in https://gitlab.com/gitlab-org/gitlab/-/issues/396544 -->
<gl-button
category="primary"
variant="confirm"
:disabled="true"
type="submit"
class="js-no-auto-disable"
:loading="saving"
data-testid="next-button"
>
{{ __('Next step') }}

View File

@ -0,0 +1,37 @@
<script>
import { GlButton } from '@gitlab/ui';
import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
export default {
components: {
GlButton,
MultiStepFormTemplate,
},
props: {
currentStep: {
type: Number,
required: true,
},
stepsTotal: {
type: Number,
required: true,
},
},
};
</script>
<template>
<multi-step-form-template
:title="s__('Runners|Register your new runner')"
:current-step="currentStep"
:steps-total="stepsTotal"
>
<template #form>
<!-- Content will be added in https://gitlab.com/gitlab-org/gitlab/-/issues/396544 -->
</template>
<template #back>
<gl-button category="primary" variant="default" :disabled="true">
{{ s__('Runners|View runners') }}
</gl-button>
</template>
</multi-step-form-template>
</template>

View File

@ -11,6 +11,7 @@ import {
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { getMarkType, getMarkRange } from '@tiptap/core';
import { joinPaths } from '~/lib/utils/url_utility';
import Link from '../../extensions/link';
import EditorStateObserver from '../editor_state_observer.vue';
import BubbleMenu from './bubble_menu.vue';
@ -167,7 +168,11 @@ export default {
},
copyLinkHref() {
navigator.clipboard.writeText(this.linkCanonicalSrc);
const fullUrl = this.linkCanonicalSrc.startsWith('http')
? this.linkCanonicalSrc
: joinPaths(gon.gitlab_url, this.linkHref);
navigator.clipboard.writeText(fullUrl);
},
removeLink() {

View File

@ -10,6 +10,7 @@ export const OptionsMenuAdapter = {
clicks: {
toggleOptionsMenu(event, button) {
const menuContainer = this.diffElement.querySelector('[data-options-menu]');
if (!menuContainer) return;
const items = getMenuItems(menuContainer);
// eslint-disable-next-line no-new
new Vue({

View File

@ -1,5 +1,5 @@
<script>
import { GlCard, GlIcon, GlLink, GlButton, GlToggle, GlAlert } from '@gitlab/ui';
import { GlCard, GlIcon, GlLink, GlButton, GlToggle, GlAlert, GlExperimentBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import SetValidityChecks from '~/security_configuration/graphql/set_validity_checks.graphql';
@ -15,6 +15,7 @@ export default {
GlButton,
GlToggle,
GlAlert,
GlExperimentBadge,
ManageViaMr,
},
inject: [
@ -177,9 +178,10 @@ export default {
<h4 class="gl-mb-3 gl-text-base gl-font-bold">
{{ s__('SecretDetection|Validity checks') }}
<gl-experiment-badge />
</h4>
<p class="gl-mb-4 gl-text-secondary">
<p class="gl-mb-4 gl-text-base">
{{
s__(
'SecretDetection|Validate tokens using third-party API calls. When the pipeline is complete, your tokens are labeled Active, Possibly active, or Inactive. You must have pipeline secret detection enabled.',

View File

@ -91,6 +91,11 @@
font-weight: normal;
}
.rd-diff-file-link {
font: inherit;
color: inherit;
}
.rd-diff-file-header-submodule,
.rd-diff-file-title,
.rd-file-mode-change,

View File

@ -28,18 +28,20 @@
- if @diff_file.renamed_file?
- old_path, new_path = helpers.mark_inline_diffs(@diff_file.old_path, @diff_file.new_path)
%h2.rd-diff-file-title{ id: heading_id, aria: { label: moved_title_label } }><
= old_path
%span.rd-diff-file-moved>< →
= new_path
= link_to file_link, { class: 'rd-diff-file-link', target: '_blank' } do
= old_path
%span.rd-diff-file-moved>< →
= new_path
- else
%h2.rd-diff-file-title{ id: heading_id }><
- chunks = file_title_chunks
- chunks[:path_parts].each do |part|
= part
= '/'
-# allow paths to wrap around '/' symbols for better visuals
%wbr><
= chunks[:filename]
= link_to file_link, { class: 'rd-diff-file-link', target: '_blank' } do
- chunks = file_title_chunks
- chunks[:path_parts].each do |part|
= part
= '/'
-# allow paths to wrap around '/' symbols for better visuals
%wbr><
= chunks[:filename]
- if @diff_file.deleted_file?
%span.rd-diff-file-deleted><= _("deleted")
= copy_path_button
@ -52,11 +54,12 @@
%span.rd-lines-added +#{@diff_file.added_lines}
%span.rd-lines-removed #{@diff_file.removed_lines}
.rd-diff-file-options-menu
%div{ data: { options_menu: true } }
-# <script> here is likely the most effective way to minimize bytes:
-# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182850#note_2387011092
-# haml-lint:disable InlineJavaScript
%script{ type: "application/json" }
= menu_items.map { |item| item.except(:position) }.to_json.html_safe
- button_params = { icon: 'ellipsis_v', button_options: { data: { click: 'toggleOptionsMenu' }, aria: { label: s_('RapidDiffs|Show options') } } }
= render Pajamas::ButtonComponent.new(category: :tertiary, size: :small, **button_params)
- unless menu_items.empty?
%div{ data: { options_menu: true } }
-# <script> here is likely the most effective way to minimize bytes:
-# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182850#note_2387011092
-# haml-lint:disable InlineJavaScript
%script{ type: "application/json" }
= menu_items.map { |item| item.except(:position) }.to_json.html_safe
- button_params = { icon: 'ellipsis_v', button_options: { data: { click: 'toggleOptionsMenu' }, aria: { label: s_('RapidDiffs|Show options') } } }
= render Pajamas::ButtonComponent.new(category: :tertiary, size: :small, **button_params)

View File

@ -16,6 +16,13 @@ module RapidDiffs
{ path_parts: parts, filename: last }
end
def file_link
helpers.project_blob_path(
@diff_file.repository.project,
helpers.tree_join(@diff_file.content_sha, @diff_file.new_path)
)
end
def copy_path_button
clipboard_button(
text: @diff_file.file_path,
@ -29,21 +36,7 @@ module RapidDiffs
end
def menu_items
base_items = [
{
text: helpers.safe_format(
_('View file @ %{commitSha}'),
commitSha: Commit.truncate_sha(@diff_file.content_sha)
),
href: helpers.project_blob_path(
@diff_file.repository.project,
helpers.tree_join(@diff_file.content_sha, @diff_file.new_path)
),
position: 0
}
]
[*base_items, *@additional_menu_items].sort_by { |item| item[:position] || Float::INFINITY }
@additional_menu_items.sort_by { |item| item[:position] || Float::INFINITY }
end
def heading_id

View File

@ -302,7 +302,6 @@ module Gitlab
config.assets.precompile << "page_bundles/dev_ops_reports.css"
config.assets.precompile << "page_bundles/editor.css"
config.assets.precompile << "page_bundles/environments.css"
config.assets.precompile << "page_bundles/epics.css"
config.assets.precompile << "page_bundles/error_tracking_details.css"
config.assets.precompile << "page_bundles/escalation_policies.css"
config.assets.precompile << "page_bundles/graph_charts.css"

View File

@ -9,14 +9,6 @@ description: Information related to Test Reports, which relate historical test o
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31643
milestone: '13.0'
gitlab_schema: gitlab_main_cell
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: issue_id
table: issues
sharding_key: project_id
belongs_to: requirement_issue
sharding_key:
project_id: projects
table_size: small
desired_sharding_key_migration_job_name: BackfillRequirementsManagementTestReportsProjectId

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddRequirementsManagementTestReportsProjectIdNotNull < Gitlab::Database::Migration[2.3]
milestone '18.2'
disable_ddl_transaction!
def up
add_not_null_constraint :requirements_management_test_reports, :project_id
end
def down
remove_not_null_constraint :requirements_management_test_reports, :project_id
end
end

View File

@ -0,0 +1 @@
a848ae3e33bbb8077385785d8c70b6e1fbd90474b859c8e2c44fc2bef0a9c16a

View File

@ -22552,7 +22552,8 @@ CREATE TABLE requirements_management_test_reports (
build_id bigint,
issue_id bigint,
uses_legacy_iid boolean DEFAULT true NOT NULL,
project_id bigint
project_id bigint,
CONSTRAINT check_715b56da9a CHECK ((project_id IS NOT NULL))
);
CREATE SEQUENCE requirements_management_test_reports_id_seq

View File

@ -900,7 +900,7 @@ Query by `iid` field and document type. Requires `type` and `iid` fields.
#### `by_full_text`
Performs a full text search. This query will use `by_multi_match_query` or `by_simple_query_string` if Advanced search syntax is used in the query string. `by_multi_match_query` is behind the `search_uses_match_queries` feature flag.
Performs a full text search. This query will use `by_multi_match_query` or `by_simple_query_string` if Advanced search syntax is used in the query string.
#### `by_multi_match_query`

View File

@ -11,7 +11,17 @@ module Gitlab
ip = RequestContext.instance.client_ip
unique_ips = update_and_return_ips_count(user_id, ip)
raise TooManyIps.new(user_id, ip, unique_ips) if unique_ips > config.unique_ips_limit_per_user
if unique_ips > config.unique_ips_limit_per_user
Gitlab::AuthLogger.error(
message: 'too_many_ips',
remote_ip: ip,
unique_ips_count: unique_ips,
user_id: user_id,
**Gitlab::ApplicationContext.current
)
raise TooManyIps.new(user_id, ip, unique_ips)
end
end
end

View File

@ -31110,6 +31110,9 @@ msgstr ""
msgid "Groups|This will create a project %{path} and add a README.md."
msgstr ""
msgid "Groups|You don't have any active groups yet."
msgstr ""
msgid "Groups|You don't have any inactive groups."
msgstr ""
@ -53380,6 +53383,9 @@ msgstr ""
msgid "Runners|Register runner"
msgstr ""
msgid "Runners|Register your new runner"
msgstr ""
msgid "Runners|Registration token"
msgstr ""

View File

@ -60,7 +60,7 @@
"@gitlab/application-sdk-browser": "^0.3.4",
"@gitlab/at.js": "1.5.7",
"@gitlab/cluster-client": "^3.0.0",
"@gitlab/duo-ui": "^8.23.0",
"@gitlab/duo-ui": "^8.24.0",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/query-language-rust": "0.11.4",

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Manage', product_group: :import_and_integrate do
RSpec.describe 'Manage', product_group: :import do
describe 'GitHub import' do
include_context 'with github import'

View File

@ -18,7 +18,7 @@ module QA
custom_test_metrics: {
tags: { import_type: ENV["QA_IMPORT_TYPE"], import_repo: ENV["QA_LARGE_IMPORT_REPO"] || "rspec/rspec-core" }
} do
describe 'Project import', product_group: :import_and_integrate do
describe 'Project import', product_group: :import do
let!(:api_client) { Runtime::API::Client.as_admin }
let!(:user) { create(:user) }
let!(:user_api_client) do

View File

@ -7,7 +7,7 @@ module QA
:requires_admin,
:integrations,
:orchestrated,
product_group: :import_and_integrate,
product_group: :import,
feature_flag: { name: :auto_disabling_web_hooks }
) do
before(:context) do

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe "Manage", product_group: :import_and_integrate do
RSpec.describe "Manage", product_group: :import do
include_context "with gitlab group migration"
describe "Gitlab migration", :import, :orchestrated, requires_admin: 'creates a user via API' do

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Manage', product_group: :import_and_integrate do
RSpec.describe 'Manage', product_group: :import do
describe 'Gitlab migration', :import, :orchestrated, requires_admin: 'creates a user via API' do
include_context 'with gitlab project migration'

View File

@ -4,7 +4,7 @@
# rubocop:disable Rails/Pluck, Layout/LineLength, RSpec/MultipleMemoizedHelpers
module QA
RSpec.describe "Manage", :skip_live_env, product_group: :import_and_integrate,
RSpec.describe "Manage", :skip_live_env, product_group: :import,
only: { condition: -> { ENV["CI_PROJECT_NAME"] == "import-metrics" } },
custom_test_metrics: {
tags: { import_type: ENV["QA_IMPORT_TYPE"], import_repo: ENV["QA_LARGE_IMPORT_REPO"] || "migration-test-project" }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Manage', product_group: :import_and_integrate do
RSpec.describe 'Manage', product_group: :import do
describe 'Gitlab migration', :import, :orchestrated, requires_admin: 'creates a user via API' do
include_context 'with gitlab project migration'

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Manage', product_group: :import_and_integrate do
RSpec.describe 'Manage', product_group: :import do
describe 'Gitlab migration', :import, :orchestrated, requires_admin: 'creates a user via API' do
include_context 'with gitlab project migration'

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Manage', product_group: :import_and_integrate do
RSpec.describe 'Manage', product_group: :import do
describe 'Gitlab migration', :import, :orchestrated, requires_admin: 'creates a user via API' do
include_context 'with gitlab project migration'

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Manage', product_group: :import_and_integrate do
RSpec.describe 'Manage', product_group: :import do
describe 'Gitlab migration', :import, :orchestrated, requires_admin: 'creates a user via API' do
include_context 'with gitlab project migration'

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Manage', product_group: :import_and_integrate do
RSpec.describe 'Manage', product_group: :import do
describe 'Gitlab migration', :import, :orchestrated, requires_admin: 'creates a user via API' do
include_context 'with gitlab project migration'

View File

@ -4,7 +4,7 @@ module QA
RSpec.describe 'Manage', :requires_admin, :skip_live_env, only: {
condition: -> { ENV['QA_RUN_TYPE']&.match?("e2e-test-on-omnibus") }
} do
describe 'rate limits', product_group: :import_and_integrate do
describe 'rate limits', product_group: :import do
let(:rate_limited_user) { create(:user, :with_personal_access_token) }
let(:api_client) { rate_limited_user.api_client }
let!(:request) { Runtime::API::Request.new(api_client, '/users') }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Manage', :github, :requires_admin, product_group: :import_and_integrate do
RSpec.describe 'Manage', :github, :requires_admin, product_group: :import do
describe 'GitHub import',
quarantine: {
type: :investigating,

View File

@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage', :requires_admin, :skip_live_env do
describe 'Jenkins integration', product_group: :import_and_integrate do
describe 'Jenkins integration', product_group: :import do
let(:jenkins_server) { Service::DockerRun::Jenkins.new }
let(:jenkins_client) do

View File

@ -4,7 +4,7 @@ module QA
RSpec.describe 'Manage' do
include Support::API
describe 'Jira integration', :jira, :orchestrated, :requires_admin, product_group: :import_and_integrate do
describe 'Jira integration', :jira, :orchestrated, :requires_admin, product_group: :import do
let(:jira_project_key) { 'JITP' }
let(:project) { create(:project, name: 'project_with_jira_integration') }

View File

@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage' do
describe 'Jira issue import', :jira, :orchestrated, :requires_admin, product_group: :import_and_integrate do
describe 'Jira issue import', :jira, :orchestrated, :requires_admin, product_group: :import do
let(:jira_project_key) { "JITD" }
let(:jira_issue_title) { "[#{jira_project_key}-1] Jira to GitLab Test Issue" }
let(:jira_issue_description) { "This issue is for testing importing Jira issues to GitLab." }

View File

@ -24,7 +24,7 @@ module QA
end
end
RSpec.describe 'Manage', :orchestrated, :requires_admin, :smtp, product_group: :import_and_integrate do
RSpec.describe 'Manage', :orchestrated, :requires_admin, :smtp, product_group: :import do
describe 'Pipeline status emails' do
let(:executor) { "qa-runner-#{SecureRandom.hex(6)}" }
let(:emails) { %w[foo@bar.com baz@buzz.com] }

View File

@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage', only: { subdomain: "staging-ref" } do
describe 'Slack app integration', :slack, product_group: :import_and_integrate do
describe 'Slack app integration', :slack, product_group: :import do
context 'when using Slash commands' do
# state to be seeded in the Slack UI
let(:title) { "Issue - #{SecureRandom.hex(5)}" }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
describe 'Manage', product_group: :import_and_integrate do
describe 'Manage', product_group: :import do
describe 'Gitlab migration', :import, :orchestrated, requires_admin: 'creates a user via API' do
include_context "with gitlab group migration"

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
describe 'Manage', product_group: :import_and_integrate do
describe 'Manage', product_group: :import do
describe 'Gitlab migration',
feature_flag: {
name: [:importer_user_mapping, :bulk_import_importer_user_mapping],

View File

@ -7,8 +7,13 @@ RSpec.describe RapidDiffs::DiffFileHeaderComponent, type: :component, feature_ca
let(:header) { page.find('[data-testid="rd-diff-file-header"]') }
it "renders file path" do
project = diff_file.repository.project
namespace = project.namespace
href = "/#{namespace.to_param}/#{project.to_param}/-/blob/#{diff_file.content_sha}/#{diff_file.new_path}"
render_component
expect(header).to have_css('h2', text: diff_file.file_path)
link = header.find('h2 a')
expect(link.text).to eq(diff_file.file_path)
expect(link[:href]).to eq(href)
end
it "renders file toggle" do
@ -48,7 +53,7 @@ RSpec.describe RapidDiffs::DiffFileHeaderComponent, type: :component, feature_ca
allow(diff_file).to receive(:old_path).and_return(old)
allow(diff_file).to receive(:new_path).and_return(new)
render_component
expect(header).to have_css("h2[aria-label=\"File moved from #{old} to #{new}\"]", text: "#{old}#{new}")
expect(header).to have_css("h2[aria-label=\"File moved from #{old} to #{new}\"] a", text: "#{old}#{new}")
end
it "renders mode change" do
@ -83,20 +88,10 @@ RSpec.describe RapidDiffs::DiffFileHeaderComponent, type: :component, feature_ca
allow(diff_file).to receive(:content_sha).and_return(content_sha)
end
it 'renders menu toggle' do
it 'does not render menu toggle without options' do
render_component
expect(page).to have_css('button[data-click="toggleOptionsMenu"][aria-label="Show options"]')
end
it 'renders default menu items' do
render_component
options_menu_items = Gitlab::Json.parse(page.find('script', visible: false).text)
expect(options_menu_items.length).to eq(1)
expect(options_menu_items[0]['text']).to eq("View file @ #{content_sha}")
expect(options_menu_items[0]).not_to have_key('position')
expect(page).not_to have_css('button[data-click="toggleOptionsMenu"][aria-label="Show options"]')
end
it 'renders additional menu items with respective order' do
@ -104,12 +99,12 @@ RSpec.describe RapidDiffs::DiffFileHeaderComponent, type: :component, feature_ca
{
text: 'First item',
href: '/first',
position: -1
position: -100
},
{
text: 'Last item',
href: '/last',
position: 2
position: 100
}
]
@ -117,10 +112,9 @@ RSpec.describe RapidDiffs::DiffFileHeaderComponent, type: :component, feature_ca
options_menu_items = Gitlab::Json.parse(page.find('script', visible: false).text)
expect(options_menu_items.length).to eq(3)
expect(options_menu_items[0]['text']).to eq('First item')
expect(options_menu_items[1]['text']).to eq("View file @ #{content_sha}")
expect(options_menu_items[2]['text']).to eq('Last item')
expect(page).to have_css('button[data-click="toggleOptionsMenu"][aria-label="Show options"]')
expect(options_menu_items.first['text']).to eq('First item')
expect(options_menu_items.last['text']).to eq('Last item')
options_menu_items.each do |item|
expect(item).not_to have_key('position')

View File

@ -36,25 +36,8 @@ RSpec.describe RapidDiffs::MergeRequestDiffFileComponent, type: :component, feat
options_menu_items = Gitlab::Json.parse(page.find('script', visible: false).text)
expect(options_menu_items.length).to eq(2)
expect(options_menu_items[0]['text']).to eq('View file @ abc123')
expect(options_menu_items[1]['text']).to eq('Edit in single-file editor')
expect(options_menu_items[1]['href']).to include("#{edit_path_base}#{merge_request.iid}")
end
end
context 'with non text diff file' do
before do
allow(diff_file).to receive(:text?).and_return(false)
end
it 'renders no additional options' do
render_component
options_menu_items = Gitlab::Json.parse(page.find('script', visible: false).text)
expect(options_menu_items.length).to eq(1)
expect(options_menu_items[0]['text']).to eq('View file @ abc123')
expect(options_menu_items[0]['text']).to eq('Edit in single-file editor')
expect(options_menu_items[0]['href']).to include("#{edit_path_base}#{merge_request.iid}")
end
end
end

View File

@ -872,6 +872,23 @@ RSpec.describe ApplicationController, feature_category: :shared do
end
end
describe 'rescue_from Gitlab::Auth::TooManyIps' do
controller(described_class) do
skip_before_action :authenticate_user!
def index
raise Gitlab::Auth::TooManyIps.new(1, '1.2.3.4', 10)
end
end
it 'returns a 403' do
get :index
expect(response).to have_gitlab_http_status(:forbidden)
expect(response.headers['Retry-After']).to eq(Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window.to_s)
end
end
describe '#set_current_context' do
controller(described_class) do
feature_category :team_planning

View File

@ -1,5 +1,10 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import { GlEmptyState } from '@gitlab/ui';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import AdminGroupsApp from '~/admin/groups/index/components/app.vue';
import { createRouter } from '~/admin/groups/index/index';
import TabsWithList from '~/groups_projects/components/tabs_with_list.vue';
import { PAGINATION_TYPE_KEYSET } from '~/groups_projects/constants';
import { RECENT_SEARCHES_STORAGE_KEY_GROUPS } from '~/filtered_search/recent_searches_storage_keys';
@ -11,25 +16,57 @@ import {
FILTERED_SEARCH_NAMESPACE,
ADMIN_GROUPS_TABS,
FIRST_TAB_ROUTE_NAMES,
ADMIN_GROUPS_ROUTE_NAME,
} from '~/admin/groups/index/constants';
import adminGroupCountsQuery from '~/admin/groups/index/graphql/queries/group_counts.query.graphql';
import {
TIMESTAMP_TYPE_CREATED_AT,
TIMESTAMP_TYPE_UPDATED_AT,
} from '~/vue_shared/components/resource_lists/constants';
import adminGroupsQuery from '~/admin/groups/index/graphql/queries/groups.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
jest.mock(
'@gitlab/svgs/dist/illustrations/empty-state/empty-groups-md.svg?url',
() => 'empty-groups-mocked-illustration',
);
Vue.use(VueRouter);
Vue.use(VueApollo);
const defaultRoute = {
name: ADMIN_GROUPS_ROUTE_NAME,
};
describe('AdminGroupsApp', () => {
let wrapper;
let mockApollo;
const createComponent = () => {
wrapper = shallowMountExtended(AdminGroupsApp);
const createComponent = async ({
mountFn = shallowMountExtended,
adminGroupsQueryHandler = jest.fn(),
route = defaultRoute,
stubs = {},
} = {}) => {
mockApollo = createMockApollo([[adminGroupsQuery, adminGroupsQueryHandler]]);
const router = createRouter();
await router.push(route);
wrapper = mountFn(AdminGroupsApp, { stubs, apolloProvider: mockApollo, router });
};
beforeEach(() => {
createComponent();
const findTabByName = (name) =>
wrapper.findAllByRole('tab').wrappers.find((tab) => tab.text().includes(name));
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
afterEach(() => {
mockApollo = null;
});
it('renders TabsWithList component and passes correct props', () => {
it('renders TabsWithList component and passes correct props', async () => {
await createComponent();
expect(wrapper.findComponent(TabsWithList).props()).toMatchObject({
tabs: ADMIN_GROUPS_TABS,
filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY,
@ -50,4 +87,37 @@ describe('AdminGroupsApp', () => {
firstTabRouteNames: FIRST_TAB_ROUTE_NAMES,
});
});
describe('when there are no groups', () => {
beforeEach(async () => {
await createComponent({
mountFn: mountExtended,
adminGroupsQueryHandler: jest
.fn()
.mockResolvedValue({ data: { groups: { nodes: [], pageInfo: {} } } }),
});
await waitForPromises();
});
it('renders empty state on Active tab', () => {
expect(findEmptyState().props()).toMatchObject({
title: "You don't have any active groups yet.",
description:
'A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
svgPath: 'empty-groups-mocked-illustration',
});
});
it('renders empty state on Inactive tab', async () => {
await findTabByName('Inactive').trigger('click');
await waitForPromises();
expect(findEmptyState().props()).toMatchObject({
title: "You don't have any inactive groups.",
description: 'Groups that are archived or pending deletion will appear here.',
svgPath: 'empty-groups-mocked-illustration',
});
});
});
});

View File

@ -1,3 +1,4 @@
import { GlForm } from '@gitlab/ui';
import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RunnerCreateWizardOptionalFields from '~/ci/runner/components/runner_create_wizard_optional_fields.vue';
@ -10,6 +11,9 @@ describe('Create Runner Optional Fields', () => {
propsData: {
currentStep: 2,
stepsTotal: 3,
tags: 'tag1, tag2',
runUntagged: false,
runnerType: 'INSTANCE_TYPE',
},
});
};
@ -18,6 +22,7 @@ describe('Create Runner Optional Fields', () => {
createComponent();
});
const findForm = () => wrapper.findComponent(GlForm);
const findMultiStepFormTemplate = () => wrapper.findComponent(MultiStepFormTemplate);
const findNextButton = () => wrapper.findByTestId('next-button');
const findBackButton = () => wrapper.findByTestId('back-button');
@ -30,11 +35,15 @@ describe('Create Runner Optional Fields', () => {
stepsTotal: 3,
});
});
it('renders GlForm', () => {
expect(findForm().exists()).toBe(true);
});
});
it('renders the Next step button', () => {
expect(findNextButton().text()).toBe('Next step');
expect(findNextButton().props('disabled')).toBe(true);
expect(findNextButton().attributes('type')).toBe('submit');
});
describe('back button', () => {

View File

@ -0,0 +1,32 @@
import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RunnerCreateWizardRegistration from '~/ci/runner/components/runner_create_wizard_registration.vue';
describe('Create New Runner Registration', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(RunnerCreateWizardRegistration, {
propsData: {
currentStep: 2,
stepsTotal: 3,
},
});
};
beforeEach(() => {
createComponent();
});
const findMultiStepFormTemplate = () => wrapper.findComponent(MultiStepFormTemplate);
describe('form', () => {
it('passes the correct props to MultiStepFormTemplate', () => {
expect(findMultiStepFormTemplate().props()).toMatchObject({
title: 'Register your new runner',
currentStep: 2,
stepsTotal: 3,
});
});
});
});

View File

@ -1,6 +1,7 @@
import { GlLink, GlForm } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { createGon } from 'helpers/gon_helper';
import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble_menu.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
@ -62,6 +63,8 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
beforeEach(() => {
buildEditor();
window.gon = createGon(false);
tiptapEditor
.chain()
.setContent(
@ -190,14 +193,32 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
});
describe('copy button', () => {
it('copies the canonical link to clipboard', async () => {
it('copies the contextualized link to clipboard for project uploads', async () => {
document.body.dataset.page = 'projects:issues:show';
window.gon.current_project_id = '123';
await buildWrapperAndDisplayMenu();
jest.spyOn(navigator.clipboard, 'writeText');
await wrapper.findByTestId('copy-link-url').vm.$emit('click');
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('uploads/my_file.pdf');
const expectedUrl = `${window.gon.gitlab_url}/path/to/project/-/wikis/uploads/my_file.pdf`;
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expectedUrl);
});
it('copies the contextualized link to clipboard for group uploads', async () => {
document.body.dataset.page = 'groups:epics:show';
window.gon.current_group_id = '456';
await buildWrapperAndDisplayMenu();
jest.spyOn(navigator.clipboard, 'writeText');
await wrapper.findByTestId('copy-link-url').vm.$emit('click');
const expectedUrl = `${window.gon.gitlab_url}/path/to/project/-/wikis/uploads/my_file.pdf`;
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expectedUrl);
});
});

View File

@ -1,4 +1,4 @@
import { GlCard, GlIcon, GlLink, GlButton, GlAlert } from '@gitlab/ui';
import { GlCard, GlIcon, GlLink, GlButton, GlAlert, GlExperimentBadge } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@ -84,6 +84,7 @@ describe('PipelineSecretDetectionFeatureCard component', () => {
const findValidityChecksSection = () => wrapper.findByTestId('validity-checks-section');
const findValidityChecksToggle = () => wrapper.findByTestId('validity-checks-toggle');
const findValidityChecksAlert = () => wrapper.findComponent(GlAlert);
const findExperimentBadge = () => wrapper.findComponent(GlExperimentBadge);
afterEach(() => {
feature = undefined;
@ -250,6 +251,7 @@ describe('PipelineSecretDetectionFeatureCard component', () => {
createComponent({}, { validityChecksAvailable });
expect(findValidityChecksSection().exists()).toBe(shouldRender);
expect(findExperimentBadge().exists()).toBe(shouldRender);
},
);

View File

@ -54,6 +54,13 @@ RSpec.describe Gitlab::Auth::UniqueIpsLimiter, :clean_gitlab_redis_shared_state
expect(described_class.limit_user! { user }).to eq(user)
change_ip('ip3')
expect(Gitlab::AuthLogger).to receive(:error).with(hash_including(
message: 'too_many_ips',
remote_ip: 'ip3',
unique_ips_count: 3,
user_id: user.id
)).and_call_original
expect { described_class.limit_user! { user } }.to raise_error(Gitlab::Auth::TooManyIps)
end
end

View File

@ -950,7 +950,6 @@
- './ee/spec/lib/gitlab/elastic/bulk_indexer_spec.rb'
- './ee/spec/lib/gitlab/elastic/group_search_results_spec.rb'
- './ee/spec/lib/gitlab/elastic/project_search_results_spec.rb'
- './ee/spec/lib/gitlab/elastic/snippet_search_results_spec.rb'
- './ee/spec/lib/gitlab/email/handler/create_note_handler_spec.rb'
- './ee/spec/lib/gitlab/exclusive_lease_spec.rb'
- './ee/spec/lib/gitlab/expiring_subscription_message_spec.rb'

View File

@ -227,4 +227,47 @@ RSpec.shared_examples 'rich text editor - links' do
end
end
end
describe 'copy link functionality for uploaded files' do
before do
switch_to_content_editor
click_attachment_button
end
it 'copies the full contextualized URL for uploaded files', :js do
upload_asset 'sample.pdf'
wait_for_requests
expect(page).to have_css('[data-testid="content_editor_editablebox"] a[href]')
page.find('[data-testid="content_editor_editablebox"] a[href]').click
expect(page).to have_css('[data-testid="link-bubble-menu"]')
expect(page).to have_css('[data-testid="copy-link-url"]')
link_href = page.find('[data-testid="content_editor_editablebox"] a[href]')['href']
expect(link_href).to match(%r{https?://.*/-/(project|group)/\d+/uploads/\h{32}/sample\.pdf})
end
end
describe 'copy link functionality for external URLs' do
before do
switch_to_content_editor
end
it 'copies external URLs as-is', :js do
type_in_content_editor 'Link to [GitLab](https://gitlab.com)'
page.find('[data-testid="content_editor_editablebox"] a[href="https://gitlab.com"]').click
expect(page).to have_css('[data-testid="link-bubble-menu"]')
expect(page).to have_css('[data-testid="copy-link-url"]')
link_href = page.find('[data-testid="content_editor_editablebox"] a[href="https://gitlab.com"]')['href']
expect(link_href).to eq('https://gitlab.com/')
end
end
end

View File

@ -1397,10 +1397,10 @@
core-js "^3.29.1"
mitt "^3.0.1"
"@gitlab/duo-ui@^8.23.0":
version "8.23.0"
resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-8.23.0.tgz#eedfa98fa902f52b6cf7970399528f5d876d7371"
integrity sha512-QOurIDNzSrgRg40omKvdQbhHbuBuFc5dweH2NZjpXjj/hksWGWJntUuzF6LsksucBJ912hQ8Z34+i57BNBQO8g==
"@gitlab/duo-ui@^8.24.0":
version "8.24.0"
resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-8.24.0.tgz#096b410ea2483e0f632a23b69f97a7ae9ffc97c5"
integrity sha512-Vn0AEdy0+Bc4YBPwY8w07Em1n/E7vjQIsWP6AAztSQv4VO5JUVNRouZb/ZUSFJD0s8fGcv+tIH8uJNlFFim9Xw==
dependencies:
"@floating-ui/dom" "1.7.1"
echarts "^5.3.2"