Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-03-11 18:12:11 +00:00
parent 7083ff8c14
commit 08d259cc2b
83 changed files with 2474 additions and 600 deletions

View File

@ -588,7 +588,7 @@ group :test do
gem 'graphlyte', '~> 1.0.0', feature_category: :shared
gem 'shoulda-matchers', '~> 5.1.0', require: false, feature_category: :shared
gem 'shoulda-matchers', '~> 6.4.0', require: false, feature_category: :shared
gem 'email_spec', '~> 2.3.0', feature_category: :shared
gem 'webmock', '~> 3.25.0', feature_category: :shared
gem 'rails-controller-testing', feature_category: :shared

View File

@ -685,7 +685,7 @@
{"name":"sentry-ruby","version":"5.22.1","platform":"ruby","checksum":"ed77bdd76da7a4c6a3de43dc6d19d3c0412b2675b014a2654bc5bafd4d5b3289"},
{"name":"sentry-sidekiq","version":"5.22.1","platform":"ruby","checksum":"bd7a3f915e58e13ea67251d9a458667fc4bee6dfbbd12614c47daa239e822a89"},
{"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"},
{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"},
{"name":"shoulda-matchers","version":"6.4.0","platform":"ruby","checksum":"9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0"},
{"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"},
{"name":"sigdump","version":"0.2.5","platform":"ruby","checksum":"bb706c1cce70458b285d2c3a57121e801ccb79f68be7f7377692eb40b5437242"},
{"name":"signet","version":"0.18.0","platform":"ruby","checksum":"66cda8c2edc2dde25090b792e7e6fc9598c3c2bdd64ffacd89f1ffe3cb9cea3b"},

View File

@ -1751,7 +1751,7 @@ GEM
sentry-ruby (~> 5.22.1)
sidekiq (>= 3.0)
shellany (0.0.1)
shoulda-matchers (5.1.0)
shoulda-matchers (6.4.0)
activesupport (>= 5.2.0)
sidekiq-cron (1.12.0)
fugit (~> 1.8)
@ -2312,7 +2312,7 @@ DEPENDENCIES
sentry-rails (~> 5.22.0)
sentry-ruby (~> 5.22.0)
sentry-sidekiq (~> 5.22.0)
shoulda-matchers (~> 5.1.0)
shoulda-matchers (~> 6.4.0)
sidekiq!
sidekiq-cron (~> 1.12.0)
sigdump (~> 0.2.4)

View File

@ -696,7 +696,7 @@
{"name":"sentry-ruby","version":"5.22.1","platform":"ruby","checksum":"ed77bdd76da7a4c6a3de43dc6d19d3c0412b2675b014a2654bc5bafd4d5b3289"},
{"name":"sentry-sidekiq","version":"5.22.1","platform":"ruby","checksum":"bd7a3f915e58e13ea67251d9a458667fc4bee6dfbbd12614c47daa239e822a89"},
{"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"},
{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"},
{"name":"shoulda-matchers","version":"6.4.0","platform":"ruby","checksum":"9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0"},
{"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"},
{"name":"sigdump","version":"0.2.5","platform":"ruby","checksum":"bb706c1cce70458b285d2c3a57121e801ccb79f68be7f7377692eb40b5437242"},
{"name":"signet","version":"0.18.0","platform":"ruby","checksum":"66cda8c2edc2dde25090b792e7e6fc9598c3c2bdd64ffacd89f1ffe3cb9cea3b"},

View File

@ -1784,7 +1784,7 @@ GEM
sentry-ruby (~> 5.22.1)
sidekiq (>= 3.0)
shellany (0.0.1)
shoulda-matchers (5.1.0)
shoulda-matchers (6.4.0)
activesupport (>= 5.2.0)
sidekiq-cron (1.12.0)
fugit (~> 1.8)
@ -2346,7 +2346,7 @@ DEPENDENCIES
sentry-rails (~> 5.22.0)
sentry-ruby (~> 5.22.0)
sentry-sidekiq (~> 5.22.0)
shoulda-matchers (~> 5.1.0)
shoulda-matchers (~> 6.4.0)
sidekiq!
sidekiq-cron (~> 1.12.0)
sigdump (~> 0.2.4)

View File

@ -365,6 +365,7 @@ export default {
:class="{
'right-sidebar-expanded': isSidebarOpen,
'right-sidebar-collapsed': !isSidebarOpen,
'!gl-bottom-8': displayStickyFooter,
}"
:artifact-help-url="artifactHelpUrl"
data-testid="job-sidebar"

View File

@ -1,10 +1,8 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import Sidebar from '~/right_sidebar';
import { getSidebarOptions } from '~/sidebar/mount_sidebar';
import CsvImportExportButtons from './components/csv_import_export_buttons.vue';
import IssuableByEmail from './components/issuable_by_email.vue';
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableContext from './issuable_context';
@ -63,39 +61,6 @@ export function initCsvImportExportButtons() {
});
}
export function initIssuableByEmail() {
const el = document.querySelector('.js-issuable-by-email');
if (!el) {
return null;
}
Vue.use(GlToast);
const {
initialEmail,
issuableType,
emailsHelpPagePath,
quickActionsHelpPath,
markdownHelpPath,
resetPath,
} = el.dataset;
return new Vue({
el,
name: 'IssuableByEmailRoot',
provide: {
initialEmail,
issuableType,
emailsHelpPagePath,
quickActionsHelpPath,
markdownHelpPath,
resetPath,
},
render: (createElement) => createElement(IssuableByEmail),
});
}
export function initIssuableSidebar() {
const el = document.querySelector('.js-sidebar-options');

View File

@ -4,7 +4,6 @@ import VueRouter from 'vue-router';
import { parseBoolean } from '~/lib/utils/common_utils';
import { defaultClient } from '~/graphql_shared/issuable_client';
import MergeRequestsListApp from './components/merge_requests_list_app.vue';
import MoreactionsDropdown from './components/more_actions_dropdown.vue';
export async function mountMergeRequestListsApp({
getMergeRequestsQuery,
@ -94,36 +93,3 @@ export async function mountMergeRequestListsApp({
render: (createComponent) => createComponent(MergeRequestsListApp),
});
}
export async function mountMoreActionsDropdown() {
const el = document.querySelector('#js-vue-mr-list-more-actions');
if (!el) {
return null;
}
const {
isSignedIn,
showExportButton,
issuableType,
issuableCount,
email,
exportCsvPath,
rssUrl,
} = el.dataset;
return new Vue({
el,
name: 'MergeRequestsListMoreActions',
provide: {
isSignedIn: parseBoolean(isSignedIn),
showExportButton: parseBoolean(showExportButton),
issuableType,
issuableCount: Number(issuableCount),
email,
exportCsvPath,
rssUrl,
},
render: (createComponent) => createComponent(MoreactionsDropdown),
});
}

View File

@ -4,13 +4,11 @@ import { debounce, isEqual } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import { s__, __, sprintf } from '~/locale';
import createStore from '../stores';
const SEARCH_DEBOUNCE_MS = 250;
export default {
name: 'MilestoneCombobox',
store: createStore(),
components: {
GlCollapsibleListbox,
GlBadge,
@ -50,8 +48,8 @@ export default {
unselect: __('Unselect'),
},
computed: {
...mapState(['matches', 'selectedMilestones']),
...mapGetters(['isLoading']),
...mapState('milestoneCombobox', ['matches', 'selectedMilestones']),
...mapGetters('milestoneCombobox', ['isLoading']),
allMilestones() {
const { groupMilestones, projectMilestones } = this.matches || {};
const milestones = [];
@ -125,7 +123,7 @@ export default {
this.fetchMilestones();
},
methods: {
...mapActions([
...mapActions('milestoneCombobox', [
'setProjectId',
'setGroupId',
'setGroupMilestonesAvailable',

View File

@ -8,10 +8,19 @@ import createState from './state';
Vue.use(Vuex);
export const createMilestoneComboboxModule = () => ({
actions,
getters,
mutations,
state: createState(),
});
export default () =>
new Vuex.Store({
actions,
getters,
mutations,
state: createState(),
modules: {
milestoneCombobox: {
namespaced: true,
...createMilestoneComboboxModule(),
},
},
});

View File

@ -40,12 +40,16 @@ export default {
};
},
modelsCountLabel() {
return n__('MlModelRegistry|%d experiment', 'MlModelRegistry|%d experiments', this.count);
return n__(
'MlExperimentTracking|%d experiment',
'MlExperimentTracking|%d experiments',
this.count,
);
},
},
i18n: {
createTitle: s__('MlModelRegistry|Create'),
importMlflow: s__('MlModelRegistry|Create experiments using MLflow'),
createTitle: s__('MlExperimentTracking|Create'),
importMlflow: s__('MlExperimentTracking|Create experiments using MLflow'),
},
mlflowModalId: MLFLOW_USAGE_MODAL_ID,
};

View File

@ -77,8 +77,8 @@ export default {
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
},
i18n: {
noResultsMessage: s__('MlModelRegistry|No results'),
emptyFieldPlaceholder: s__('MlModelRegistry|Select a model'),
noResultsMessage: s__('MlExperimentTracking|No results'),
emptyFieldPlaceholder: s__('MlExperimentTracking|Select a model'),
},
};
</script>

View File

@ -43,13 +43,15 @@ export default {
versionDescription() {
if (this.latestVersion) {
return sprintf(
s__('MlModelRegistry|Must be a semantic version. Latest version is %{latestVersion}'),
s__(
'MlExperimentTracking|Must be a semantic version. Latest version is %{latestVersion}',
),
{
latestVersion: this.latestVersion,
},
);
}
return s__('MlModelRegistry|Must be a semantic version.');
return s__('MlExperimentTracking|Must be a semantic version.');
},
autocompleteDataSources() {
return gl.GfmAutoComplete?.dataSources;
@ -123,26 +125,28 @@ export default {
},
},
descriptionFormFieldProps: {
placeholder: s__('MlModelRegistry|Enter a model version description'),
placeholder: s__('MlExperimentTracking|Enter a model version description'),
id: 'model-version-description',
name: 'model-version-description',
},
i18n: {
actionPrimaryText: s__('MlModelRegistry|Promote'),
actionPrimaryText: s__('MlExperimentTracking|Promote'),
actionSecondaryText: __('Cancel'),
versionDescription: s__('MlModelRegistry|Enter a semantic version.'),
versionValid: s__('MlModelRegistry|Version is valid semantic version.'),
versionInvalid: s__('MlModelRegistry|Version is not a valid semantic version.'),
versionPlaceholder: s__('MlModelRegistry|For example 1.0.0'),
descriptionPlaceholder: s__('MlModelRegistry|Enter some description'),
title: s__('MlModelRegistry|Promote run'),
description: s__('MlModelRegistry|Complete the form below to promote run to a model version.'),
optionalText: s__('MlModelRegistry|(Optional)'),
versionLabelText: s__('MlModelRegistry|Version'),
versionDescriptionText: s__('MlModelRegistry|Description'),
modelSelectionLabelText: s__('MlModelRegistry|Model'),
versionDescription: s__('MlExperimentTracking|Enter a semantic version.'),
versionValid: s__('MlExperimentTracking|Version is valid semantic version.'),
versionInvalid: s__('MlExperimentTracking|Version is not a valid semantic version.'),
versionPlaceholder: s__('MlExperimentTracking|For example 1.0.0'),
descriptionPlaceholder: s__('MlExperimentTracking|Enter some description'),
title: s__('MlExperimentTracking|Promote run'),
description: s__(
'MlExperimentTracking|Complete the form below to promote run to a model version.',
),
optionalText: s__('MlExperimentTracking|(Optional)'),
versionLabelText: s__('MlExperimentTracking|Version'),
versionDescriptionText: s__('MlExperimentTracking|Description'),
modelSelectionLabelText: s__('MlExperimentTracking|Model'),
modelDescription: s__(
'MlModelRegistry|Select the model that will contain the new version. The run will move to the default experiment of that model.',
'MlExperimentTracking|Select the model that will contain the new version. The run will move to the default experiment of that model.',
),
},
};

View File

@ -55,11 +55,11 @@ export default {
const fields = range(maxStep + 1).map((step) => ({
key: step.toString(),
label: sprintf(s__('MlModelRegistry|Step %{step}'), { step }),
label: sprintf(s__('MlExperimentTracking|Step %{step}'), { step }),
...cssClasses,
}));
return [{ key: 'name', label: s__('MlModelRegistry|Metric'), ...cssClasses }, ...fields];
return [{ key: 'name', label: s__('MlExperimentTracking|Metric'), ...cssClasses }, ...fields];
},
metricsTableItems() {
const items = {};
@ -87,20 +87,20 @@ export default {
},
},
i18n: {
detailsLabel: s__('MlModelRegistry|Details & Metadata'),
artifactsLabel: s__('MlModelRegistry|Artifacts'),
mlflowIdLabel: s__('MlModelRegistry|MLflow run ID'),
ciSectionLabel: s__('MlModelRegistry|CI Info'),
detailsLabel: s__('MlExperimentTracking|Details & Metadata'),
artifactsLabel: s__('MlExperimentTracking|Artifacts'),
mlflowIdLabel: s__('MlExperimentTracking|MLflow run ID'),
ciSectionLabel: s__('MlExperimentTracking|CI Info'),
jobLabel: __('Job'),
ciUserLabel: s__('MlModelRegistry|Triggered by'),
ciUserLabel: s__('MlExperimentTracking|Triggered by'),
ciMrLabel: __('Merge request'),
parametersLabel: s__('MlModelRegistry|Parameters'),
performanceLabel: s__('MlModelRegistry|Performance'),
noParametersMessage: s__('MlModelRegistry|No logged parameters'),
noMetricsMessage: s__('MlModelRegistry|No logged metrics'),
noMetadataMessage: s__('MlModelRegistry|No logged metadata'),
noCiMessage: s__('MlModelRegistry|Run not linked to a CI build'),
noArtifactsMessage: s__('MlModelRegistry|No logged artifacts.'),
parametersLabel: s__('MlExperimentTracking|Parameters'),
performanceLabel: s__('MlExperimentTracking|Performance'),
noParametersMessage: s__('MlExperimentTracking|No logged parameters'),
noMetricsMessage: s__('MlExperimentTracking|No logged metrics'),
noMetadataMessage: s__('MlExperimentTracking|No logged metadata'),
noCiMessage: s__('MlExperimentTracking|Run not linked to a CI build'),
noArtifactsMessage: s__('MlExperimentTracking|No logged artifacts.'),
copyMessage: __('Copy MLflow run ID'),
},
};

View File

@ -68,7 +68,7 @@ export default {
},
error(error) {
this.errorMessage = sprintf(
s__('MlModelRegistry|Failed to load experiments with error: %{error}'),
s__('MlExperimentTracking|Failed to load experiments with error: %{error}'),
{ error: error.message },
);
Sentry.captureException(error);

View File

@ -1,32 +1,13 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import getMergeRequestsQuery from 'ee_else_ce/merge_requests/list/queries/group/get_merge_requests.query.graphql';
import getMergeRequestsCountsQuery from 'ee_else_ce/merge_requests/list/queries/group/get_merge_requests_counts.query.graphql';
import getMergeRequestsApprovalsQuery from 'ee_else_ce/merge_requests/list/queries/group/get_merge_requests_approvals.query.graphql';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import { initBulkUpdateSidebar } from '~/issuable';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown';
import { RESOURCE_TYPE_MERGE_REQUEST } from '~/vue_shared/components/new_resource_dropdown/constants';
import searchUserGroupProjectsWithMergeRequestsEnabled from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_group_projects_with_merge_requests_enabled.query.graphql';
import { mountMergeRequestListsApp } from '~/merge_requests/list';
const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_';
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
initBulkUpdateSidebar(ISSUABLE_BULK_UPDATE_PREFIX);
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
isGroupDecendent: true,
useDefaultState: true,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
initNewResourceDropdown({
resourceType: RESOURCE_TYPE_MERGE_REQUEST,
query: searchUserGroupProjectsWithMergeRequestsEnabled,
extractProjects: (data) => data?.group?.projects?.nodes,
});
mountMergeRequestListsApp({
getMergeRequestsQuery,
getMergeRequestsCountsQuery,

View File

@ -1,31 +1,15 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import getMergeRequestsQuery from 'ee_else_ce/merge_requests/list/queries/project/get_merge_requests.query.graphql';
import getMergeRequestsCountsQuery from 'ee_else_ce/merge_requests/list/queries/project/get_merge_requests_counts.query.graphql';
import getMergeRequestsApprovalsQuery from 'ee_else_ce/merge_requests/list/queries/project/get_merge_requests_approvals.query.graphql';
import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import { initBulkUpdateSidebar, initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { mountMoreActionsDropdown, mountMergeRequestListsApp } from '~/merge_requests/list';
import { initBulkUpdateSidebar } from '~/issuable';
import { mountMergeRequestListsApp } from '~/merge_requests/list';
initBulkUpdateSidebar('merge_request_');
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
IssuableFilteredSearchTokenKeys.removeTokensForKeys('iteration');
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
useDefaultState: true,
});
addShortcutsExtension(ShortcutsNavigation);
initIssuableByEmail();
initCsvImportExportButtons();
mountMoreActionsDropdown();
mountMergeRequestListsApp({
getMergeRequestsQuery,
getMergeRequestsApprovalsQuery,

View File

@ -33,7 +33,7 @@ export default {
const bars = [
{ name: s__('Pipeline|Successful'), data: [] },
{ name: s__('Pipeline|Failed'), data: [] },
{ name: s__('Pipeline|Other'), data: [] },
{ name: s__('Pipeline|Other (Cancelled, Skipped)'), data: [] },
];
this.timeSeries.forEach(({ label, successCount, failedCount, otherCount }) => {

View File

@ -1,6 +1,7 @@
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { createMilestoneComboboxModule } from '~/milestones/stores';
import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores';
import createEditNewModule from './stores/modules/edit_new';
@ -12,6 +13,7 @@ export default () => {
const store = createStore({
modules: {
milestoneCombobox: { namespaced: true, ...createMilestoneComboboxModule() },
editNew: createEditNewModule({ ...el.dataset, isExistingRelease: true }),
},
});

View File

@ -1,6 +1,7 @@
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { createMilestoneComboboxModule } from '~/milestones/stores';
import { createRefModule } from '../ref/stores';
import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores';
@ -13,6 +14,7 @@ export default () => {
const store = createStore({
modules: {
milestoneCombobox: { namespaced: true, ...createMilestoneComboboxModule() },
editNew: createEditNewModule({ ...el.dataset, isExistingRelease: false }),
ref: createRefModule(),
},

View File

@ -149,11 +149,7 @@ class GroupsController < Groups::ApplicationController
@badge_api_endpoint = expose_path(api_v4_groups_badges_path(id: @group.id))
end
def merge_requests
return if ::Feature.enabled?(:vue_merge_request_list, current_user)
render_merge_requests
end
def merge_requests; end
def projects
@projects = @group.projects.with_statistics.page(params[:page])

View File

@ -390,7 +390,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private
def set_issuables_index
return if ::Feature.enabled?(:vue_merge_request_list, current_user) && request.format.html?
return if request.format.html?
super
end

View File

@ -11,7 +11,7 @@ module UserSettings
def destroy
# params[:id] can be an Rack::Session::SessionId#private_id
ActiveSession.destroy_session(current_user, params[:id])
current_user.forget_me!
current_user.invalidate_all_remember_tokens!
respond_to do |format|
format.html { redirect_to user_settings_active_sessions_url, status: :found }

View File

@ -42,6 +42,11 @@ module Packages
scope :for_package_type, ->(package_type) { where(package_type: package_type) }
def self.for_delete_exists?(access_level:, package_name:, package_type:)
for_action_exists?(action: :delete, access_level: access_level, package_name: package_name,
package_type: package_type)
end
def self.for_action_exists?(action:, access_level:, package_name:, package_type:)
return false if [access_level, package_name, package_type].any?(&:blank?)

View File

@ -1215,6 +1215,19 @@ class User < ApplicationRecord
super if ::Gitlab::Database.read_write?
end
# This is a copy of #forget_me! without the check for `expire_all_remember_me_on_sign_out`
# https://github.com/heartcombo/devise/blob/v4.9.4/lib/devise/models/rememberable.rb#L58-L63
#
# We need a separate method because we disabled that setting but we also need to be able to
# manually expire these tokens when a session is manually destroyed
def invalidate_all_remember_tokens!
return unless persisted?
self.remember_token = nil if respond_to?(:remember_token)
self.remember_created_at = nil
save(validate: false)
end
# Override Devise Rememberable#remember_me?
#
# In Devise this method compares the remember me token received from the user session

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
module Packages
module Protection
class CheckDeleteRuleExistenceService < BaseProjectService
SUCCESS_RESPONSE_RULE_EXISTS = ServiceResponse.success(payload: { protection_rule_exists?: true }).freeze
SUCCESS_RESPONSE_RULE_DOESNT_EXIST = ServiceResponse.success(payload: { protection_rule_exists?: false }).freeze
ERROR_RESPONSE_UNAUTHORIZED = ServiceResponse.error(message: 'Unauthorized', reason: :unauthorized).freeze
ERROR_RESPONSE_INVALID_PACKAGE_TYPE = ServiceResponse.error(message: 'Invalid package type',
reason: :invalid_package_type).freeze
def execute
return ERROR_RESPONSE_INVALID_PACKAGE_TYPE unless package_type_allowed?
return ERROR_RESPONSE_UNAUTHORIZED unless current_user_can_destroy_package?
return SUCCESS_RESPONSE_RULE_DOESNT_EXIST if current_user.can_admin_all_resources?
user_project_authorization_access_level = current_user.max_member_access_for_project(project.id)
response = project.package_protection_rules.for_delete_exists?(
access_level: user_project_authorization_access_level,
package_name: params[:package_name],
package_type: params[:package_type]
)
service_response_for(response)
end
private
def package_type_allowed?
Packages::Protection::Rule.package_types.key?(params[:package_type])
end
def current_user_can_destroy_package?
can?(current_user, :destroy_package, project)
end
def service_response_for(protection_rule_exists)
protection_rule_exists ? SUCCESS_RESPONSE_RULE_EXISTS : SUCCESS_RESPONSE_RULE_DOESNT_EXIST
end
end
end
end

View File

@ -34,8 +34,7 @@ module Packages
return false if current_user.can_admin_all_resources?
user_project_authorization_access_level = current_user.max_member_access_for_project(project.id)
project.package_protection_rules
.for_action_exists?(
project.package_protection_rules.for_action_exists?(
action: :push,
access_level: user_project_authorization_access_level,
package_name: params[:package_name],

View File

@ -61,7 +61,8 @@
5,
6,
7,
8
8,
99
]
}
},

View File

@ -2,27 +2,6 @@
- page_title _("Merge requests")
- add_page_specific_style 'page_bundles/issuable_list'
- if Feature.enabled?(:vue_merge_request_list, @group)
.js-merge-request-list-root{ data: group_merge_requests_list_data(@group, current_user) }
- if has_bulk_update_permission
= render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :merge_requests
- else
- can_bulk_update = has_bulk_update_permission && issuables_count_for_state(:merge_requests, :all) > 0
.top-area
= render 'shared/issuable/nav', type: :merge_requests, display_count: !@search_timeout_occurred
- if current_user
.nav-controls
- if can_bulk_update
= render_if_exists 'projects/merge_requests/bulk_update_button'
= render 'shared/new_project_item_vue_select'
= render 'shared/issuable/search_bar', type: :merge_requests
- if can_bulk_update
= render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :merge_requests
- if @search_timeout_occurred
= render 'shared/dashboard/search_timeout_occurred'
- else
= render 'shared/merge_requests'
.js-merge-request-list-root{ data: group_merge_requests_list_data(@group, current_user) }
- if has_bulk_update_permission
= render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :merge_requests

View File

@ -1,10 +0,0 @@
= render_if_exists 'projects/merge_requests/merge_trains_button'
- if @can_bulk_update
= render Pajamas::ButtonComponent.new(type: :submit, button_options: { class: 'gl-mr-3 js-bulk-update-toggle' }) do
= _("Bulk edit")
- if merge_project && can?(@current_user, :create_merge_request_in, @project)
= render Pajamas::ButtonComponent.new(href: new_merge_request_path, variant: :confirm,
button_options: { data: { event_tracking: 'click_new_merge_request_list' } }) do
= _("New merge request")
#js-vue-mr-list-more-actions{ data: project_merge_requests_list_more_actions_data(@project, current_user) }

View File

@ -1,10 +1,6 @@
- @can_bulk_update = can?(current_user, :admin_merge_request, @project)
- merge_project = merge_request_source_project_for_project(@project)
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- issuable_type = 'merge_request'
- page_title _("Merge requests")
- new_merge_request_email = @project.new_issuable_address(current_user, 'merge_request')
- add_page_specific_style 'page_bundles/issuable_list'
- add_page_specific_style 'page_bundles/merge_request'
@ -13,27 +9,6 @@
= render 'projects/last_push'
- if Feature.enabled?(:vue_merge_request_list, @project)
.js-merge-request-list-root{ data: project_merge_requests_list_data(@project, current_user) }
- if @can_bulk_update
= render 'shared/issuable/bulk_update_sidebar', type: :merge_requests
- else
- if @project.merge_requests.exists?
.top-area
= render 'shared/issuable/nav', type: :merge_requests
.nav-controls
= render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path
= render 'shared/issuable/search_bar', type: :merge_requests
- if @can_bulk_update
= render 'shared/issuable/bulk_update_sidebar', type: :merge_requests
.merge-requests-holder
= render 'merge_requests', new_merge_request_path: new_merge_request_path
- else
= render 'shared/empty_states/merge_requests', button_path: new_merge_request_path
- if new_merge_request_email && can?(current_user, :create_merge_request_in, @project)
.gl-text-center.gl-pt-5.gl-pb-7
.js-issuable-by-email{ data: { initial_email: new_merge_request_email, issuable_type: issuable_type, emails_help_page_path: help_page_path('development/emails.md', anchor: 'email-namespace'), quick_actions_help_path: help_page_path('user/project/quick_actions.md'), markdown_help_path: help_page_path('user/markdown.md'), reset_path: new_issuable_address_project_path(@project, issuable_type: issuable_type) } }
.js-merge-request-list-root{ data: project_merge_requests_list_data(@project, current_user) }
- if @can_bulk_update
= render 'shared/issuable/bulk_update_sidebar', type: :merge_requests

View File

@ -11,11 +11,10 @@
= _('Update selected')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-bulk-update-menu-hide gl-float-right' }) do
= _('Cancel')
- if params[:state] != 'merged' || ::Feature.enabled?(:vue_merge_request_list, @project)
.block.js-status-dropdown-container
.title
= _('Status')
.js-status-dropdown
.block.js-status-dropdown-container
.title
= _('Status')
.js-status-dropdown
.block
.title
= _('Assignee')

View File

@ -101,19 +101,19 @@
= render_if_exists 'shared/projects/badges', project: project
- if stars
= link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' } do
= link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' }, aria: { label: format(n_('%{project} has %{number} star', '%{project} has %{number} stars', project.star_count), number: project.star_count, project: project.name) } do
= sprite_icon('star-o', size: 14, css_class: 'gl-mr-2')
= badge_count(project.star_count)
- if show_count?(disabled: !forks, compact_mode: compact_mode)
= link_to project_forks_path(project), class: "#{css_metadata_classes} forks", title: _('Forks'), data: { container: 'body', placement: 'top' } do
= link_to project_forks_path(project), class: "#{css_metadata_classes} forks", title: _('Forks'), data: { container: 'body', placement: 'top' }, aria: { label: format(n_('%{project} has %{number} fork', '%{project} has %{number} forks', project.forks_count), number: project.forks_count, project: project.name) } do
= sprite_icon('fork', size: 14, css_class: 'gl-mr-2')
= badge_count(project.forks_count)
- if show_count?(disabled: !merge_requests, compact_mode: compact_mode)
= link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' } do
= link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' }, aria: { label: format(n_('%{project} has %{number} merge request', '%{project} has %{number} merge requests', project.open_merge_requests_count), number: project.open_merge_requests_count, project: project.name) } do
= sprite_icon('merge-request', size: 14, css_class: 'gl-mr-2')
= badge_count(project.open_merge_requests_count)
- if show_count?(disabled: !issues, compact_mode: compact_mode)
= link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' } do
= link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' }, aria: { label: format(n_('%{project} has %{number} open issues', '%{project} has %{number} open issues', project.open_issues_count), number: project.open_issues_count, project: project.name) } do
= sprite_icon('issues', size: 14, css_class: 'gl-mr-2')
= badge_count(project.open_issues_count)
= render_if_exists 'shared/projects/actions', project: project

View File

@ -1,9 +0,0 @@
---
name: vue_merge_request_list
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/10827
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145168
rollout_issue_url:
milestone: '16.10'
group: group::code review
type: beta
default_enabled: true

View File

@ -0,0 +1,9 @@
---
name: packages_protected_packages_delete
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323970
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/179931
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/516215
milestone: '17.10'
group: group::package registry
type: gitlab_com_derisk
default_enabled: false

View File

@ -116,15 +116,15 @@ Devise.setup do |config|
# The time the user will be remembered without asking for credentials again.
# config.remember_for = 2.weeks
# If true, a valid remember token can be re-used between multiple browsers.
# config.remember_across_browsers = true
# Invalidates all the remember me tokens when the user signs out.
config.expire_all_remember_me_on_sign_out = false
# If true, extends the user's remember period when remembered via cookie.
config.extend_remember_period = true
# Options to be passed to the created cookie. For instance, you can set
# secure: true in order to force SSL only cookies.
# config.cookie_options = {}
# config.rememberable_options = {}
# When set to false, does not sign a user in automatically after their password is
# changed. Defaults to true, so a user is signed in automatically after a password

View File

@ -4,13 +4,24 @@ return if Gitlab::Utils.to_boolean(ENV['SKIP_CELL_CONFIG_VALIDATION'], default:
ValidationError = Class.new(StandardError)
print_error = ->(error_message) do
message = error_message
message += <<~MESSAGE if Gitlab.dev_or_test_env?
Make sure your development environment is up to date.
For example, on GDK, run: gdk update
MESSAGE
raise ValidationError, message
end
if Gitlab.config.cell.enabled
raise ValidationError, "Cell ID is not set to a valid positive integer" if Gitlab.config.cell.id.to_i < 1
print_error.call("Cell ID is not set to a valid positive integer.") if Gitlab.config.cell.id.to_i < 1
Settings.topology_service_settings.each do |setting|
setting_value = Gitlab.config.cell.topology_service_client.send(setting)
raise ValidationError, "Topology Service setting '#{setting}' is not set" if setting_value.blank?
print_error.call("Topology Service setting '#{setting}' is not set.") if setting_value.blank?
end
elsif Gitlab.config.cell.id.present?
raise ValidationError, "Cell ID is set but Cell is not enabled"
print_error.call("Cell ID is set but Cell is not enabled.")
end

View File

@ -1,41 +1,9 @@
# frozen_string_literal: true
class RemoveBrokenFkA2141b1522P < Gitlab::Database::Migration[2.2]
include Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
milestone '17.10'
disable_ddl_transaction!
SOURCE_TABLE_NAME = :p_ci_builds
TARGET_TABLE_NAME = :p_ci_pipelines
COLUMN = :auto_canceled_by_id
TARGET_COLUMN = :id
PARTITION_COLUMN = :auto_canceled_by_partition_id
PARTITION_TARGET_COLUMN = :partition_id
FK_NAME = :fk_a2141b1522_p
def up
with_lock_retries do
remove_foreign_key_if_exists(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
name: FK_NAME,
reverse_lock_order: true
)
end
end
def down
add_concurrent_partitioned_foreign_key(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
column: [PARTITION_COLUMN, COLUMN],
target_column: [PARTITION_TARGET_COLUMN, TARGET_COLUMN],
reverse_lock_order: true,
on_update: :cascade,
on_delete: :nullify,
name: FK_NAME,
validate: true
)
end
# No-op for https://gitlab.com/gitlab-com/gl-infra/production/-/issues/19464
def up; end
def down; end
end

View File

@ -40484,6 +40484,9 @@ ALTER TABLE ONLY ml_candidates
ALTER TABLE ONLY subscription_add_on_purchases
ADD CONSTRAINT fk_a1db288990 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE p_ci_builds
ADD CONSTRAINT fk_a2141b1522_p FOREIGN KEY (auto_canceled_by_partition_id, auto_canceled_by_id) REFERENCES p_ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE SET NULL;
ALTER TABLE ONLY protected_environment_approval_rules
ADD CONSTRAINT fk_a3cc825836 FOREIGN KEY (protected_environment_project_id) REFERENCES projects(id) ON DELETE CASCADE;

View File

@ -40892,6 +40892,7 @@ AI features that can be configured in the settings.
| <a id="aifeaturesduo_chat_explain_code"></a>`DUO_CHAT_EXPLAIN_CODE` | Duo chat explain code feature setting. |
| <a id="aifeaturesduo_chat_fix_code"></a>`DUO_CHAT_FIX_CODE` | Duo chat fix code feature setting. |
| <a id="aifeaturesduo_chat_refactor_code"></a>`DUO_CHAT_REFACTOR_CODE` | Duo chat refactor code feature setting. |
| <a id="aifeaturesduo_chat_troubleshoot_job"></a>`DUO_CHAT_TROUBLESHOOT_JOB` | Duo chat troubleshoot job feature setting. |
| <a id="aifeaturesduo_chat_write_tests"></a>`DUO_CHAT_WRITE_TESTS` | Duo chat write test feature setting. |
### `AiMessageRole`

View File

@ -433,12 +433,15 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
Can return the following status codes:
- `204 No Content`, if the package was deleted successfully.
- `404 Not Found`, if the package was not found.
- `204 No Content`: The package was deleted successfully.
- `403 Forbidden`: The package is protected from deletion.
- `404 Not Found`: The package was not found.
If [request forwarding](../user/packages/package_registry/supported_functionality.md#forwarding-requests) is enabled,
deleting a package can introduce a [dependency confusion risk](../user/packages/package_registry/supported_functionality.md#deleting-packages).
If a package is protected by a [protection rule](../user/packages/package_registry/package_protection_rules.md#protect-a-package), then deleting the package is forbidden.
## Delete a package file
{{< alert type="warning" >}}
@ -467,5 +470,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
Can return the following status codes:
- `204 No Content`: The package was deleted successfully.
- `403 Forbidden`: The user does not have permission to delete the file.
- `403 Forbidden`: The user does not have permission to delete the file or the package is protected from deletion.
- `404 Not Found`: The package or package file was not found.
If a package that a package file belongs to is protected by a [protection rule](../user/packages/package_registry/package_protection_rules.md#protect-a-package), then deleting the package file is forbidden.

View File

@ -282,6 +282,8 @@ build_submodule:
- unzip artifacts.zip
```
To fetch artifacts from a job in the same pipeline, use the [`needs:artifacts`](../yaml/_index.md#needsartifacts) keyword.
## Browse the contents of the artifacts archive
You can browse the contents of the artifacts from the UI without downloading the artifact locally,

View File

@ -739,8 +739,13 @@ subcommand. However, `GIT_SUBMODULE_UPDATE_FLAGS` flags are appended after a few
Git honors the last occurrence of a flag in the list of arguments, so manually
providing them in `GIT_SUBMODULE_UPDATE_FLAGS` overrides these default flags.
You can use this variable to fetch the latest remote `HEAD` instead of the tracked commit in the repository.
You can also use it to speed up the checkout by fetching submodules in multiple parallel jobs.
For example, you can use this variable to:
- Fetch the latest remote `HEAD` instead of the tracked commit in the
repository (default) to automatically updated all submodules with the
`--remote` flag.
- Speed up the checkout by fetching submodules in multiple parallel jobs with
the `--jobs 4` flag.
```yaml
variables:
@ -758,12 +763,35 @@ git submodule update --init --depth 50 --recursive --remote --jobs 4
{{< alert type="warning" >}}
You should be aware of the implications for the security, stability, and reproducibility of
your builds when using the `--remote` flag. In most cases, it is better to explicitly track
submodule commits as designed, and update them using an auto-remediation/dependency bot.
You should be aware of the implications for the security, stability, and
reproducibility of your builds when using the `--remote` flag. In most cases,
it is better to explicitly track submodule commits as designed, and update them
using an auto-remediation/dependency bot.
The `--remote` flag is not required to check out submodules at their committed
revisions. Use this flag only when you want to automatically updated submodules
to their latest remote versions.
{{< /alert >}}
The behavior of `--remote` depends on your Git version. Some Git versions might
fail, with the error below, when the branch in the superproject's `.gitmodules`
differs from the default branch of the submodule repository:
`fatal: Unable to find refs/remotes/origin/<branch> revision in submodule path '<submodule-path>'`
The runner implements a "best effort" fallback that attempts to
pull remote refs when the submodule update fails.
If this fallback does not work with your Git version, try one of the following
workarounds:
- Update the submodule repository's default branch to match the branch set in
`.gitmodules` in the superproject.
- Set `GIT_SUBMODULE_DEPTH` to `0`.
- Update the submodules separately and remove the `--remote` flag from
`GIT_SUBMODULE_UPDATE_FLAGS`.
### Rewrite submodule URLs to HTTPS
{{< history >}}

View File

@ -18,6 +18,10 @@ includes a task or reference topic.
The tech writing team sometimes uses the acronym `CTRT` to refer to the topic types.
The acronym refers to the first letter of each topic type.
<i class="fa-youtube-play" aria-hidden="true"></i>
For an overview, see [Editing for style and topic type](https://youtu.be/HehnjPgPWb0).
<!-- Video published on 2021-06-06 -->
## Other page and topic types
In addition to the four primary topic types, you can use the following:

View File

@ -59,3 +59,24 @@ Avoid these topic titles:
noun or phrase that someone would search for.
- `Use cases`. Instead, incorporate the information as part of the concept.
- `How it works`. Instead, use a noun followed by `workflow`. For example, `Merge request workflow`.
## Example
### Before
The following topic was trying to be all things to all people. It provided information about groups
and where to find them. It reiterated what was visible in the UI.
![An example concept and task](img/example_1.png)
### After
The information is easier to scan if you move it into concepts and [tasks](task.md).
#### Concept
![A concept example after it's been corrected](img/example_1_after_concept.png)
#### Task
![A task example after it's been corrected](img/example_1_after_task.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -34,3 +34,18 @@ Avoid these topic titles:
for a task, or information about a concept.
- `Limitations`. Instead, move the content near other similar information.
If you must, you can use the title `Known issues`.
## Example
### Before
This topic was a compilation of a variety of information and was difficult to scan.
![An example of a reference topic](img/reference_example1.png)
### After
The information in the **Overview** topic is now organized in a table
that's easy to scan. It also has a more searchable title.
![An example of a corrected reference topic](img/reference_example2.png)

View File

@ -163,3 +163,4 @@ how to write the phrase for each role.
## Related topics
- [How to write task steps](../styleguide/_index.md#navigation)
- [Before and after example](concept.md#example)

View File

@ -435,14 +435,37 @@ scope2 = User.select(*columns).where(id: [10, 11, 12]) # uses SELECT users.*
User.connection.execute(Gitlab::SQL::Union.new([scope1, scope2]).to_sql)
```
## Ordering by Creation Date
## Ordering by Creation Date (`created_at`)
When ordering records based on the time they were created, you can order
by the `id` column instead of ordering by `created_at`. Because IDs are always
unique and incremented in the order that rows are created, doing so produces the
exact same results. This also means there's no need to add an index on
`created_at` to ensure consistent performance as `id` is already indexed by
default.
In short, you should prefer `ORDER BY id` over `ORDER BY created_at` unless you
are sure it is going to cause problems for your feature.
There is a common user facing desire to provide data that is sorted by
`created_at`. It's common in paginated table views and paginated APIs to want
to see the most recent first (or oldest first). This usually results in us
wanting to add something like `ORDER BY created_at DESC LIMIT 20` to our
queries. Adding this query would mean that we need to add an index on
`created_at` (or composite index depending on the other filtering
requirements). Adding indexes comes with
[a cost](database/adding_database_indexes.md#maintenance-overhead).
Furthermore, since `created_at` usually isn't a unique column then sorting
and paginating over it would be unstable and we'd still need to add a
[tie-breaker column to the sort](database/pagination_performance_guidelines.md#tie-breaker-column)
(e.g. `ORDER BY created_at, id`) with an appropriate index for that.
But, for the majority of features our users find that `ORDER BY id` is a good
enough proxy for what they need. It's not technically always
true that ordering by `id` is exactly the same as ordering by `created_at` but
it is close enough and considering that `created_at` is almost never controlled
directly by users (ie. it's an internal implementation detail), then there is
rarely a case where the user actually cares about the difference between these
2 columns.
So there are at least 3 advantages to ordering by `id`:
1. As a primary key, it is already indexed, which may be sufficient for simple queries that don't have other filtering or sorting parameters.
1. If a composite index is required, indexes such as `btree (namespace_id, id)` are smaller than `btree (namespace_id, created_at, id)`.
1. It is unique and thus stable for sorting and paginating.
## Use `WHERE EXISTS` instead of `WHERE IN`

View File

@ -145,3 +145,78 @@ dast:
Adjusting these values may impact scan time because they adjust how long each browser waits for various activities to complete.
{{< /alert >}}
### Page readiness timeouts
Page readiness refers to the state when a page has loaded completely, its DOM has stabilized, and interactive elements are available. Proper page readiness detection is crucial for:
- **Scanning accuracy**: Analyzing pages before they're fully loaded can miss content or produce false negatives.
- **Crawl efficiency**: Waiting too long wastes scanning time, while not waiting enough misses dynamic content.
- **Modern web application support**: Single-page applications, AJAX-heavy sites, and progressive loading patterns require sophisticated readiness detection.
Using a sequence of optional configurable timeouts, the DAST scanner can detect when different parts of a page have loaded completely.
#### Timeout variables
Use the following CI/CD variables to customize DAST page readiness timeouts.
For a comprehensive list, see [Available CI/CD variables](variables.md).
| Timeout Variable | Default | Description |
|:-----------------|:--------|:------------|
| `DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT` | `15s` | The maximum amount of time to wait for a browser to navigate from one page to another. Used during the Document Load phase for full page loads. |
| `DAST_PAGE_READY_AFTER_ACTION_TIMEOUT` | `7s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis. Used as an alternative to `DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT` for in-page actions that don't trigger a full page load. |
| `DAST_PAGE_DOM_STABLE_WAIT` | `500ms` | Define how long to wait for updates to the DOM before checking a page is stable. Used at the beginning of the client-side render phase. |
| `DAST_PAGE_DOM_READY_TIMEOUT` | `6s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis after a navigation completes. Controls waiting for background data fetching and DOM rendering. |
| `DAST_PAGE_IS_LOADING_ELEMENT` | None | Selector that when no longer visible on the page, indicates to the analyzer that the page has finished loading and the scan can continue. Marks the end of the client-side render process. |
#### Page loading workflow
Modern web applications load in multiple stages. The DAST scanner has specific timeouts for
each step in the process:
1. **Document loading**: The browser fetches and processes the basic page structure.
1. Fetch HTML content from the server.
1. Load referenced CSS and JavaScript files.
1. Parse content and renders the initial page.
1. Trigger the standard "document ready" event.
This phase uses either `DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT` (for full page loads) or `DAST_PAGE_READY_AFTER_ACTION_TIMEOUT` (for in-page actions), which sets the maximum wait time for document loading.
1. **Client-Side rendering**: After initial loading, many single-page applications:
- Perform initial JavaScript execution (`DAST_PAGE_DOM_STABLE_WAIT`).
- Fetch background data with AJAX or other API calls.
- Render a DOM and performs updates based on fetched data (`DAST_PAGE_DOM_READY_TIMEOUT`).
- Display page loading indicators (`DAST_PAGE_IS_LOADING_ELEMENT`).
The scanner monitors these activities to determine when the page is ready for interaction.
The following chart illustrates the sequence timeouts used when crawling a page:
```mermaid
%%{init: {
"gantt": {
"leftPadding": 250,
"sectionFontSize": 15,
"topPadding": 40,
"fontFamily": "GitLab Sans"
}
}}%%
gantt
dateFormat YYYY-MM-DD
axisFormat
section Document load
DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT :done, nav1, 2024-01-01, 6d
Fetch HTML :active, nav1, 2024-01-01, 3d
Fetch CSS&JS :active, nav1, 2024-01-04, 3d
DocumentReady :milestone, nav1, 2024-01-07, 0d
section Load Data / Client-side render
DAST_PAGE_DOM_STABLE_WAIT :done, dom1, 2024-01-07, 3d
Initial JS Execution :active, dom1, 2024-01-07, 3d
DAST_PAGE_DOM_READY_TIMEOUT :done, ready1, 2024-01-10, 4d
Fetch Data :active, dom1, 2024-01-10, 2d
Render DOM :active, dom1, 2024-01-10, 2d
DAST_PAGE_IS_LOADING_ELEMENT :milestone, load1, 2024-01-14, 0d
```

View File

@ -302,6 +302,7 @@ refreshed.
{{< history >}}
- [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/16157) in GitLab 17.9 [with a flag](../../../administration/feature_flags.md) named `vulnerability_severity_override`. Disabled by default.
- [Enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated](https://issue-link) in GitLab 17.10.
{{< /history >}}
@ -309,7 +310,6 @@ refreshed.
The availability of this feature is controlled by a feature flag.
For more information, see the history.
This feature is available for testing, but not ready for production use.
{{< /alert >}}

View File

@ -0,0 +1,169 @@
---
stage: AI-powered
group: AI Framework
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "Learn how to use GitLab Duo AI-powered features to enhance your software development lifecycle."
title: 'GitLab Duo: Choose your path'
---
GitLab Duo is a suite of AI-powered features that assist you while you work in GitLab.
Select the path that best matches what you want to do:
{{< tabs >}}
{{< tab title="Get started" >}}
**Perfect for**: New users exploring GitLab Duo
Follow this path to learn how to:
- Use the variety of GitLab Duo features
- Get help from AI through GitLab Duo Chat
- Generate and improve code
[Start here: GitLab Duo →](_index.md)
{{< /tab >}}
{{< tab title="Enhance my coding" >}}
**Perfect for**: Developers looking to boost productivity
Follow this path to learn how to:
- Use Code Suggestions in your IDE
- Generate, understand, and refactor code
- Create tests automatically
[Start here: Code Suggestions →](../../user/project/repository/code_suggestions/_index.md)
{{< /tab >}}
{{< tab title="Improve code reviews" >}}
**Perfect for**: Reviewers and team leads
Follow this path to learn how to:
- Generate merge request descriptions
- Get AI-powered code reviews
- Summarize review comments and generate commit messages
[Start here: GitLab Duo in merge requests →](../../user/project/merge_requests/duo_in_merge_requests.md)
{{< /tab >}}
{{< tab title="Secure my application" >}}
**Perfect for**: Security and DevSecOps professionals
Follow this path to learn how to:
- Understand vulnerabilities
- Automatically generate fix suggestions
- Create merge requests to address security issues
[Start here: Vulnerability explanation and resolution →](../application_security/vulnerabilities/_index.md#explaining-a-vulnerability)
{{< /tab >}}
{{< /tabs >}}
## Quick start
Want to start using GitLab Duo right now? Here's how:
1. Open GitLab Duo Chat by selecting **GitLab Duo Chat** in the upper-right corner of the GitLab UI.
1. Ask a question about your project, code, or how to use GitLab.
1. Try one of the AI-powered features like Code Suggestions in your IDE, or use Chat to summarize a bulky issue.
[View all of the GitLab Duo possibilities →](_index.md)
## Common tasks
Need to do something specific? Here are some common tasks:
| Task | Description | Quick Guide |
|------|-------------|-------------|
| Get AI assistance | Ask GitLab Duo questions about code, projects, or GitLab | [GitLab Duo Chat →](../gitlab_duo_chat/_index.md) |
| Generate code | Get code suggestions as you type in your IDE | [Code Suggestions →](../../user/project/repository/code_suggestions/_index.md) |
| Understand code | Have code explained in plain language | [Code Explanation →](../../user/project/repository/code_explain.md) |
| Fix CI/CD issues | Analyze and fix failed jobs | [Root Cause Analysis →](../gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis) |
| Summarize changes | Generate descriptions for merge requests | [Merge Request Summary →](../../user/project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes) |
## How GitLab Duo integrates with your workflow
GitLab Duo is integrated with your development processes and is available:
- In the GitLab UI
- Through GitLab Duo Chat
- In IDE extensions
- In the CLI
## Experience levels
### For beginners
If you're new to GitLab Duo, start with these features:
- **[GitLab Duo Chat](../gitlab_duo_chat/_index.md)** - Ask questions about GitLab and get help with basic tasks
- **[Code Explanation](../../user/project/repository/code_explain.md)** - Understand code in files or merge requests
- **[Merge Request Summary](../../user/project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes)** - Generate descriptions for your changes automatically
### For intermediate users
After you're comfortable with the basics, try these more advanced features:
- **[Code Suggestions](../../user/project/repository/code_suggestions/_index.md)** - Get AI-powered code completion in your IDE
- **[Test Generation](../gitlab_duo_chat/examples.md#write-tests-in-the-ide)** - Create tests for your code automatically
- **[Root Cause Analysis](../gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis)** - Troubleshoot failed CI/CD jobs
### For advanced users
When you're ready to maximize your productivity with GitLab Duo:
- **[GitLab Duo Self-Hosted](../../administration/gitlab_duo_self_hosted/_index.md)** - Host LLMs on your own infrastructure
- **[GitLab Duo Workflow](../duo_workflow/_index.md)** - Automate tasks in your development workflow
- **[Vulnerability Resolution](../../user/application_security/vulnerabilities/_index.md#vulnerability-resolution)** - Automatically generate merge requests to fix security issues
## Best practices
Follow these tips for effective GitLab Duo usage:
1. **Be specific in your prompts**
- Provide clear context for better results
- Include relevant details about your code and objectives
- Use code task commands like `/explain`, `/refactor`, and `/tests` in Chat
1. **Improve code responsibly**
- Always review AI-generated code before using it
- Test generated code to ensure it works as expected
- Use vulnerability resolution with appropriate review
1. **Refine iteratively**
- If a response isn't helpful, refine your question
- Try breaking complex requests into smaller parts
- Add more details for better context
1. **Leverage Chat for learning**
- Ask about GitLab features you're not familiar with
- Get explanations for error messages and problems
- Learn best practices for your specific technology
## Next steps
Ready to dive deeper? Try these resources:
- [GitLab Duo use cases](use_cases.md) - Practical examples and exercises
- [Set up GitLab Duo Self-Hosted](../../administration/gitlab_duo_self_hosted/_index.md) - For complete control over your data
## Troubleshooting
Having issues? Check these common solutions:
- [GitLab Duo features don't work on self-managed](troubleshooting.md#gitlab-duo-features-do-not-work-on-self-managed)
- [GitLab Duo features not available for users](troubleshooting.md#gitlab-duo-features-not-available-for-users)
- [Run a health check](setup.md#run-a-health-check-for-gitlab-duo) to diagnose your GitLab Duo setup
Need more help? Search the GitLab documentation or [ask the GitLab community](https://forum.gitlab.com/).

View File

@ -18,6 +18,10 @@ module API
documentation: { example: '2021-01-01' }
optional :last_used_after, type: DateTime, desc: 'Filter tokens which were used after given datetime',
documentation: { example: '2022-01-01' }
optional :expires_before, type: Date, desc: 'Filter tokens which expire before given datetime',
documentation: { example: '2022-01-01' }
optional :expires_after, type: Date, desc: 'Filter tokens which expire after given datetime',
documentation: { example: '2021-01-01' }
optional :search, type: String, desc: 'Filters tokens by name', documentation: { example: 'token' }
optional :sort, type: String, desc: 'Sort tokens', documentation: { example: 'created_at_desc' }
end

View File

@ -67,6 +67,17 @@ module API
not_found! unless package
if Feature.enabled?(:packages_protected_packages_delete, user_project)
service_response =
Packages::Protection::CheckDeleteRuleExistenceService.new(
project: user_project,
current_user: current_user,
params: { package_name: package.name, package_type: package.package_type }
).execute
forbidden!('Package is deletion protected.') if service_response[:protection_rule_exists?]
end
package_file = package.installable_package_files
.find_by_id(params[:package_file_id])

View File

@ -139,6 +139,17 @@ module API
delete ':id/packages/:package_id' do
authorize_destroy_package!(user_project)
if Feature.enabled?(:packages_protected_packages_delete, user_project)
service_response =
Packages::Protection::CheckDeleteRuleExistenceService.new(
project: user_project,
current_user: current_user,
params: { package_name: package.name, package_type: package.package_type }
).execute
forbidden!('Package is deletion protected.') if service_response[:protection_rule_exists?]
end
destroy_conditionally!(package) do |package|
::Packages::MarkPackageForDestructionService.new(container: package, current_user: current_user).execute
end

View File

@ -10,6 +10,8 @@ module API
feature_category :system_access
helpers ::API::Helpers::PersonalAccessTokensHelpers
%w[project group].each do |source_type|
resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get list of all access tokens for the specified resource' do
@ -20,15 +22,14 @@ module API
end
params do
requires :id, types: [String, Integer], desc: "ID or URL-encoded path of the #{source_type}"
optional :state, type: String, desc: 'Filter tokens which are either active or inactive',
values: %w[active inactive], documentation: { example: 'active' }
use :access_token_params
end
get ":id/access_tokens" do
resource = find_source(source_type, params[:id])
next unauthorized! unless current_user.can?(:read_resource_access_tokens, resource)
tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false, state: params[:state] }).execute.preload_users
tokens = PersonalAccessTokensFinder.new(declared(params, include_missing: false).merge({ user: resource.bots, impersonation: false })).execute.preload_users
resource.members.load
present paginate(tokens), with: Entities::ResourceAccessToken, resource: resource

View File

@ -25,6 +25,8 @@ module Gitlab
track_analytics_event(event_name, send_snowplow_event, category: category,
additional_properties: additional_properties, **kwargs)
return unless event_definition
kwargs[:additional_properties] = additional_properties
event_definition.extra_tracking_classes.each do |tracking_class|
tracking_class.track_event(event_name, **kwargs)

View File

@ -36962,9 +36962,32 @@ msgstr ""
msgid "MissingSSHKeyWarningLink|You won't be able to pull or push repositories via SSH until you add an SSH key to your profile"
msgstr ""
msgid "MlExperimentTracking|%d experiment"
msgid_plural "MlExperimentTracking|%d experiments"
msgstr[0] ""
msgstr[1] ""
msgid "MlExperimentTracking|(Optional)"
msgstr ""
msgid "MlExperimentTracking|Artifacts"
msgstr ""
msgid "MlExperimentTracking|CI Info"
msgstr ""
msgid "MlExperimentTracking|Complete the form below to promote run to a model version."
msgstr ""
msgid "MlExperimentTracking|Create"
msgstr ""
msgid "MlExperimentTracking|Create an experiment using MLflow"
msgstr ""
msgid "MlExperimentTracking|Create experiments using MLflow"
msgstr ""
msgid "MlExperimentTracking|Creating an experiment"
msgstr ""
@ -36989,9 +37012,24 @@ msgstr ""
msgid "MlExperimentTracking|Deleting this run will delete the associated parameters, metrics, and metadata."
msgstr ""
msgid "MlExperimentTracking|Description"
msgstr ""
msgid "MlExperimentTracking|Details & Metadata"
msgstr ""
msgid "MlExperimentTracking|Download as CSV"
msgstr ""
msgid "MlExperimentTracking|Enter a model version description"
msgstr ""
msgid "MlExperimentTracking|Enter a semantic version."
msgstr ""
msgid "MlExperimentTracking|Enter some description"
msgstr ""
msgid "MlExperimentTracking|Experiment created %{timeAgo} by %{author}"
msgstr ""
@ -37007,9 +37045,15 @@ msgstr ""
msgid "MlExperimentTracking|Failed to load experiment candidates with error: %{message}"
msgstr ""
msgid "MlExperimentTracking|Failed to load experiments with error: %{error}"
msgstr ""
msgid "MlExperimentTracking|Failed to remove run"
msgstr ""
msgid "MlExperimentTracking|For example 1.0.0"
msgstr ""
msgid "MlExperimentTracking|Get started with model experiments!"
msgstr ""
@ -37019,39 +37063,93 @@ msgstr ""
msgid "MlExperimentTracking|Last activity"
msgstr ""
msgid "MlExperimentTracking|MLflow run ID"
msgstr ""
msgid "MlExperimentTracking|Metric"
msgstr ""
msgid "MlExperimentTracking|Model"
msgstr ""
msgid "MlExperimentTracking|Model experiments"
msgstr ""
msgid "MlExperimentTracking|Must be a semantic version."
msgstr ""
msgid "MlExperimentTracking|Must be a semantic version. Latest version is %{latestVersion}"
msgstr ""
msgid "MlExperimentTracking|Name"
msgstr ""
msgid "MlExperimentTracking|No candidates associated with this experiment"
msgstr ""
msgid "MlExperimentTracking|No logged artifacts."
msgstr ""
msgid "MlExperimentTracking|No logged experiment metadata"
msgstr ""
msgid "MlExperimentTracking|No logged metadata"
msgstr ""
msgid "MlExperimentTracking|No logged metrics"
msgstr ""
msgid "MlExperimentTracking|No logged parameters"
msgstr ""
msgid "MlExperimentTracking|No results"
msgstr ""
msgid "MlExperimentTracking|Number of runs"
msgstr ""
msgid "MlExperimentTracking|Overview"
msgstr ""
msgid "MlExperimentTracking|Parameters"
msgstr ""
msgid "MlExperimentTracking|Performance"
msgstr ""
msgid "MlExperimentTracking|Promote"
msgstr ""
msgid "MlExperimentTracking|Promote run"
msgstr ""
msgid "MlExperimentTracking|Run %{id}"
msgstr ""
msgid "MlExperimentTracking|Run not linked to a CI build"
msgstr ""
msgid "MlExperimentTracking|Run removed"
msgstr ""
msgid "MlExperimentTracking|Runs"
msgstr ""
msgid "MlExperimentTracking|Select a model"
msgstr ""
msgid "MlExperimentTracking|Select the model that will contain the new version. The run will move to the default experiment of that model."
msgstr ""
msgid "MlExperimentTracking|Step %{step}"
msgstr ""
msgid "MlExperimentTracking|To learn more about MLflow client compatibility, see %{linkStart}the documentation%{linkEnd}."
msgstr ""
msgid "MlExperimentTracking|Triggered by"
msgstr ""
msgid "MlExperimentTracking|Use candidates to track performance, parameters, and metadata"
msgstr ""
@ -37061,13 +37159,17 @@ msgstr ""
msgid "MlExperimentTracking|Value"
msgstr ""
msgid "MlExperimentTracking|by %{author}"
msgid "MlExperimentTracking|Version"
msgstr ""
msgid "MlModelRegistry|%d experiment"
msgid_plural "MlModelRegistry|%d experiments"
msgstr[0] ""
msgstr[1] ""
msgid "MlExperimentTracking|Version is not a valid semantic version."
msgstr ""
msgid "MlExperimentTracking|Version is valid semantic version."
msgstr ""
msgid "MlExperimentTracking|by %{author}"
msgstr ""
msgid "MlModelRegistry|%d model"
msgid_plural "MlModelRegistry|%d models"
@ -37109,18 +37211,12 @@ msgstr ""
msgid "MlModelRegistry|CI Info"
msgstr ""
msgid "MlModelRegistry|Complete the form below to promote run to a model version."
msgstr ""
msgid "MlModelRegistry|Create"
msgstr ""
msgid "MlModelRegistry|Create & import"
msgstr ""
msgid "MlModelRegistry|Create experiments using MLflow"
msgstr ""
msgid "MlModelRegistry|Create model"
msgstr ""
@ -37178,9 +37274,6 @@ msgstr ""
msgid "MlModelRegistry|Description"
msgstr ""
msgid "MlModelRegistry|Details & Metadata"
msgstr ""
msgid "MlModelRegistry|Drop or %{linkStart}select%{linkEnd} artifacts to attach"
msgstr ""
@ -37223,9 +37316,6 @@ msgstr ""
msgid "MlModelRegistry|Failed to delete model with error: %{message}"
msgstr ""
msgid "MlModelRegistry|Failed to load experiments with error: %{error}"
msgstr ""
msgid "MlModelRegistry|Failed to load model runs with error: %{message}"
msgstr ""
@ -37274,12 +37364,6 @@ msgstr ""
msgid "MlModelRegistry|Metadata"
msgstr ""
msgid "MlModelRegistry|Metric"
msgstr ""
msgid "MlModelRegistry|Model"
msgstr ""
msgid "MlModelRegistry|Model card"
msgstr ""
@ -37325,9 +37409,6 @@ msgstr ""
msgid "MlModelRegistry|No description available. To add a description, click \"Edit model\" above."
msgstr ""
msgid "MlModelRegistry|No logged artifacts."
msgstr ""
msgid "MlModelRegistry|No logged metadata"
msgstr ""
@ -37337,9 +37418,6 @@ msgstr ""
msgid "MlModelRegistry|No logged parameters"
msgstr ""
msgid "MlModelRegistry|No results"
msgstr ""
msgid "MlModelRegistry|No runs associated with this model"
msgstr ""
@ -37352,12 +37430,6 @@ msgstr ""
msgid "MlModelRegistry|Performance"
msgstr ""
msgid "MlModelRegistry|Promote"
msgstr ""
msgid "MlModelRegistry|Promote run"
msgstr ""
msgid "MlModelRegistry|Provide a subfolder name to organize your artifacts. Entering an existing subfolder's name will place artifacts in the existing folder"
msgstr ""
@ -37376,21 +37448,12 @@ msgstr ""
msgid "MlModelRegistry|Save changes"
msgstr ""
msgid "MlModelRegistry|Select a model"
msgstr ""
msgid "MlModelRegistry|Select the model that will contain the new version. The run will move to the default experiment of that model."
msgstr ""
msgid "MlModelRegistry|Setting up the client"
msgstr ""
msgid "MlModelRegistry|Something went wrong while trying to delete the model version. Please try again later."
msgstr ""
msgid "MlModelRegistry|Step %{step}"
msgstr ""
msgid "MlModelRegistry|Subfolder"
msgstr ""
@ -43467,7 +43530,7 @@ msgstr ""
msgid "Pipeline|Only the first 100 jobs per stage are displayed"
msgstr ""
msgid "Pipeline|Other"
msgid "Pipeline|Other (Cancelled, Skipped)"
msgstr ""
msgid "Pipeline|Passed"

View File

@ -65,7 +65,7 @@
"@gitlab/fonts": "^1.3.0",
"@gitlab/query-language-rust": "0.4.0",
"@gitlab/svgs": "3.123.0",
"@gitlab/ui": "110.0.0",
"@gitlab/ui": "110.1.0",
"@gitlab/vue-router-vue3": "npm:vue-router@4.5.0",
"@gitlab/vuex-vue3": "npm:vuex@4.1.0",
"@gitlab/web-ide": "^0.0.1-dev-20250309164831",

View File

@ -195,7 +195,6 @@ spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.j
spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
spec/frontend/ref/init_ambiguous_ref_modal_spec.js
spec/frontend/releases/components/app_edit_new_spec.js
spec/frontend/releases/components/asset_links_form_spec.js
spec/frontend/repository/components/table/index_spec.js
spec/frontend/repository/components/table/row_spec.js

View File

@ -523,92 +523,6 @@ RSpec.describe GroupsController, :with_current_organization, factory_default: :k
expect(response).to render_template('groups/merge_requests')
end
context 'sorting by votes' do
context 'when vue_merge_request_list is disabled' do
before do
stub_feature_flags(vue_merge_request_list: false)
end
it 'sorts most popular merge requests' do
get :merge_requests, params: { id: group.to_param, sort: 'upvotes_desc' }
expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
end
it 'sorts least popular merge requests' do
get :merge_requests, params: { id: group.to_param, sort: 'downvotes_desc' }
expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
end
end
end
context 'rendering views' do
before do
stub_feature_flags(vue_merge_request_list: false)
end
render_views
it 'displays MR counts in nav' do
get :merge_requests, params: { id: group.to_param }
expect(response.body).to have_content('Open 2 Merged 0 Closed 0 All 2')
expect(response.body).not_to have_content('Open Merged Closed All')
end
context 'when MergeRequestsFinder raises an exception' do
before do
allow_next_instance_of(MergeRequestsFinder) do |instance|
allow(instance).to receive(:count_by_state).and_raise(ActiveRecord::QueryCanceled)
end
end
it 'does not display MR counts in nav' do
get :merge_requests, params: { id: group.to_param }
expect(response.body).to have_content('Open Merged Closed All')
expect(response.body).not_to have_content('Open 0 Merged 0 Closed 0 All 0')
end
end
end
context 'when an ActiveRecord::QueryCanceled is raised' do
before do
stub_feature_flags(vue_merge_request_list: false)
allow_next_instance_of(Gitlab::IssuableMetadata) do |instance|
allow(instance).to receive(:data).and_raise(ActiveRecord::QueryCanceled)
end
end
it 'sets :search_timeout_occurred' do
get :merge_requests, params: { id: group.to_param }
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:search_timeout_occurred)).to eq(true)
end
it 'logs the exception' do
get :merge_requests, params: { id: group.to_param }
end
context 'rendering views' do
render_views
it 'shows error message' do
get :merge_requests, params: { id: group.to_param }
expect(response.body).to have_content('Too many results to display. Edit your search or add a filter.')
end
it 'does not display MR counts in nav' do
get :merge_requests, params: { id: group.to_param }
expect(response.body).to have_content('Open Merged Closed All')
expect(response.body).not_to have_content('Open 0 Merged 0 Closed 0 All 0')
end
end
end
end
describe 'DELETE #destroy' do

View File

@ -392,77 +392,6 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
expect(response).to render_template('projects/merge_requests/index')
end
context 'when vue_merge_request_list is disabled' do
before do
stub_feature_flags(vue_merge_request_list: false)
end
context 'when the test is flaky', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/450217' do
it_behaves_like "issuables list meta-data", :merge_request
end
it_behaves_like 'set sort order from user preference' do
let(:sorting_param) { 'updated_asc' }
end
context 'when page param' do
let(:last_page) { project.merge_requests.page.total_pages }
let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
it 'redirects to last_page if page number is larger than number of pages' do
get_merge_requests(last_page + 1)
expect(response).to redirect_to(project_merge_requests_path(project, page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
end
it 'redirects to specified page' do
get_merge_requests(last_page)
expect(assigns(:merge_requests).current_page).to eq(last_page)
expect(response).to have_gitlab_http_status(:ok)
end
it 'does not redirect to external sites when provided a host field' do
external_host = "www.example.com"
get :index,
params: {
namespace_id: project.namespace.to_param,
project_id: project,
state: 'opened',
page: (last_page + 1).to_param,
host: external_host
}
expect(response).to redirect_to(project_merge_requests_path(project, page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
end
end
context 'when filtering by opened state' do
context 'with opened merge requests' do
it 'lists those merge requests' do
expect(merge_request).to be_persisted
get_merge_requests
expect(assigns(:merge_requests)).to include(merge_request)
end
end
context 'with reopened merge requests' do
before do
merge_request.close!
merge_request.reopen!
end
it 'lists those merge requests' do
get_merge_requests
expect(assigns(:merge_requests)).to include(merge_request)
end
end
end
end
end
describe 'PUT update' do

View File

@ -47,24 +47,5 @@ RSpec.describe 'Groups > User sees users dropdowns in issuables list', :js, feat
end
end
end
context 'when vue_merge_request_list feature flag is disabled' do
before do
stub_feature_flags(vue_merge_request_list: false)
end
%w[author assignee].each do |dropdown|
describe "#{dropdown} dropdown" do
it 'only includes members of the project/group' do
visit merge_requests_group_path(group)
filtered_search.set("#{dropdown}:=")
expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name)
expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name)
end
end
end
end
end
end

View File

@ -1,4 +1,4 @@
import htmlMergeRequestList from 'test_fixtures/merge_requests/merge_request_list.html';
import htmlMergeRequestList from 'test_fixtures_static/merge_request_list.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import DropdownUser from '~/filtered_search/dropdown_user';
import DropdownUtils from '~/filtered_search/dropdown_utils';

View File

@ -1,4 +1,4 @@
import htmlMergeRequestList from 'test_fixtures/merge_requests/merge_request_list.html';
import htmlMergeRequestList from 'test_fixtures_static/merge_request_list.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import DropdownUtils from '~/filtered_search/dropdown_utils';

View File

@ -127,19 +127,6 @@ RSpec
end
end
it 'merge_requests/merge_request_list.html' do
stub_feature_flags(vue_merge_request_list: false)
create(:merge_request, source_project: project, target_project: project)
get :index, params: {
namespace_id: project.namespace.to_param,
project_id: project
}
expect(response).to be_successful
end
describe GraphQL::Query, type: :request do
include ApiHelpers
include GraphqlHelpers

File diff suppressed because one or more lines are too long

View File

@ -36,7 +36,7 @@ describe('PipelineStatusChart', () => {
bars: [
{ data: [], name: 'Successful' },
{ data: [], name: 'Failed' },
{ data: [], name: 'Other' },
{ data: [], name: 'Other (Cancelled, Skipped)' },
],
groupBy: [],
customPalette: ['#619025', '#b93d71', '#617ae2'],
@ -61,7 +61,7 @@ describe('PipelineStatusChart', () => {
expect(findStackedColumnChart().props('bars')).toEqual([
{ data: [10, 11], name: 'Successful' },
{ data: [20, 21], name: 'Failed' },
{ data: [30, 31], name: 'Other' },
{ data: [30, 31], name: 'Other (Cancelled, Skipped)' },
]);
});

View File

@ -7,6 +7,7 @@ import { nextTick } from 'vue';
import { GlDatepicker, GlFormCheckbox } from '@gitlab/ui';
import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import createMilestoneComboboxState from '~/milestones/stores/state';
import { convertOneReleaseGraphQLResponse } from '~/releases/util';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
@ -86,6 +87,25 @@ describe('Release edit/new component', () => {
merge(
{
modules: {
milestoneCombobox: {
namespaced: true,
actions: {
setProjectId: jest.fn(),
setGroupId: jest.fn(),
setGroupMilestonesAvailable: jest.fn(),
setSelectedMilestones: jest.fn(),
clearSelectedMilestones: jest.fn(),
toggleMilestones: jest.fn(),
search: jest.fn(),
fetchMilestones: jest.fn(),
fetchProjectMilestones: jest.fn(),
fetchGroupMilestones: jest.fn(),
searchProjectMilestones: jest.fn(),
searchGroupMilestones: jest.fn(),
},
state: createMilestoneComboboxState(),
getters: { isLoading: jest.fn() },
},
editNew: {
namespaced: true,
actions,
@ -239,6 +259,7 @@ describe('Release edit/new component', () => {
store: {
modules: {
editNew: {
namespaced: true,
state: { isExistingRelease: false },
},
},
@ -280,6 +301,7 @@ describe('Release edit/new component', () => {
store: {
modules: {
editNew: {
namespaced: true,
getters: {
isValid: () => true,
},
@ -300,6 +322,7 @@ describe('Release edit/new component', () => {
store: {
modules: {
editNew: {
namespaced: true,
getters: {
isValid: () => false,
},
@ -326,6 +349,7 @@ describe('Release edit/new component', () => {
store: {
modules: {
editNew: {
namespaced: true,
state: {
isFetchingTagNotes: true,
},
@ -355,6 +379,7 @@ describe('Release edit/new component', () => {
store: {
modules: {
editNew: {
namespaced: true,
state: {
isExistingRelease: false,
},

View File

@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe 'validate database config', feature_category: :cell do
include StubENV
let(:dev_message) do
"\nMake sure your development environment is up to date.\nFor example, on GDK, run: gdk update\n"
end
let(:rails_configuration) { Rails::Application::Configuration.new(Rails.root) }
let(:valid_topology_service_client_config) do
{
@ -71,22 +75,32 @@ RSpec.describe 'validate database config', feature_category: :cell do
end
it 'raises an exception' do
expect { validate_config }.to raise_error("Cell ID is set but Cell is not enabled")
expect { validate_config }.to raise_error("Cell ID is set but Cell is not enabled.#{dev_message}")
end
end
end
context 'when configuration is wrong' do
context 'when configuration is invalid' do
context 'when cell is enabled by cell id is not set' do
before do
stub_config(cell: { enabled: true, id: nil, topology_service_client: valid_topology_service_client_config })
end
it 'raises exception about missing cell id' do
expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer")
expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer.#{dev_message}")
end
it_behaves_like 'with SKIP_CELL_CONFIG_VALIDATION=true'
context 'when not dev environment' do
before do
stub_rails_env('production')
end
it 'raises exception about missing cell id' do
expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer.")
end
end
end
context 'when cell is enabled by cell id is not valid' do
@ -95,7 +109,7 @@ RSpec.describe 'validate database config', feature_category: :cell do
end
it 'raises exception about missing cell id' do
expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer")
expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer.#{dev_message}")
end
it_behaves_like 'with SKIP_CELL_CONFIG_VALIDATION=true'
@ -107,7 +121,7 @@ RSpec.describe 'validate database config', feature_category: :cell do
end
it 'raises exception about missing topology service client config' do
expect { validate_config }.to raise_error("Topology Service setting 'address' is not set")
expect { validate_config }.to raise_error("Topology Service setting 'address' is not set.#{dev_message}")
end
it_behaves_like 'with SKIP_CELL_CONFIG_VALIDATION=true'

View File

@ -770,6 +770,20 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
allow(event_definition).to receive(:extra_tracking_classes).and_return([custom_tracking_class])
end
context 'when event is not defined' do
let(:event_name) { 'an_event_that_does_not_exist' }
before do
allow(Gitlab::Tracking::EventDefinition).to receive(:find).with(event_name).and_return(nil)
end
it 'does not call custom classes' do
expect(custom_tracking_class).not_to receive(:track_event)
described_class.track_event(event_name, user: user, project: project)
end
end
it 'calls the custom classes' do
expect(custom_tracking_class).to receive(:track_event).with(event_name, **event_kwargs)

View File

@ -2624,7 +2624,7 @@ RSpec.describe User, feature_category: :user_profile do
end
end
describe '#forget_me!' do
describe '#invalidate_all_remember_tokens!' do
let(:user) { create(:user) }
context 'when remember me application setting is disabled' do
@ -2638,7 +2638,7 @@ RSpec.describe User, feature_category: :user_profile do
expect(user.remember_created_at).not_to be_nil
stub_application_setting(remember_me_enabled: false)
user.forget_me!
user.invalidate_all_remember_tokens!
expect(user.remember_created_at).to be_nil
end

View File

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe API::PackageFiles, feature_category: :package_registry do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let_it_be(:project) { create(:project, :public) }
let(:package) { create(:maven_package, project: project) }
describe 'GET /projects/:id/packages/:package_id/package_files' do
@ -274,5 +274,112 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do
end
end
end
context 'with package protection rule for different roles and package_name_patterns', :enable_admin_mode do
using RSpec::Parameterized::TableSyntax
let_it_be(:pat_project_maintainer) do
create(:personal_access_token, user: create(:user, maintainer_of: [project]))
end
let_it_be(:pat_project_owner) { create(:personal_access_token, user: create(:user, owner_of: [project])) }
let_it_be(:pat_instance_admin) { create(:personal_access_token, :admin_mode, user: create(:admin)) }
let_it_be(:headers_pat_project_maintainer) do
{ Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_project_maintainer.token }
end
let_it_be(:headers_pat_project_owner) do
{ Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_project_owner.token }
end
let_it_be(:headers_pat_instance_admin) do
{ Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_instance_admin.token }
end
let_it_be(:job_from_project_maintainer) do
create(:ci_build, :running, user: pat_project_maintainer.user, project: project)
end
let_it_be(:job_from_project_owner) { create(:ci_build, :running, user: pat_project_owner.user, project: project) }
let(:headers_job_token_from_maintainer) do
{ Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_from_project_maintainer.token }
end
let(:headers_job_token_from_owner) do
{ Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_from_project_owner.token }
end
let(:package_protection_rule) { create(:package_protection_rule, project: project) }
let(:package_name) { package.name }
let(:package_name_no_match) { "#{package_name}_no_match" }
subject do
delete api(url), headers: headers
response
end
shared_examples 'deleting package protected' do
it_behaves_like 'returning response status', :forbidden
it 'responds with correct error message' do
subject
expect(json_response).to include('message' => "403 Forbidden - Package is deletion protected.")
end
it { expect { subject }.not_to change { ::Packages::Package.pending_destruction.count } }
context 'when feature flag :packages_protected_packages_delete disabled' do
before do
stub_feature_flags(packages_protected_packages_delete: false)
end
it_behaves_like 'deleting package'
end
end
shared_examples 'deleting package' do
it_behaves_like 'returning response status', :no_content
it { expect { subject }.to change { package.package_files.pending_destruction.count }.by(1) }
end
where(:package_name_pattern, :minimum_access_level_for_delete, :headers, :shared_examples_name) do
ref(:package_name) | :owner | ref(:headers_job_token_from_maintainer) | 'deleting package protected'
ref(:package_name) | :owner | ref(:headers_job_token_from_owner) | 'deleting package'
ref(:package_name) | :owner | ref(:headers_pat_project_maintainer) | 'deleting package protected'
ref(:package_name) | :owner | ref(:headers_pat_project_owner) | 'deleting package'
ref(:package_name) | :owner | ref(:headers_pat_instance_admin) | 'deleting package'
ref(:package_name) | :admin | ref(:headers_pat_project_maintainer) | 'deleting package protected'
ref(:package_name) | :admin | ref(:headers_pat_project_owner) | 'deleting package protected'
ref(:package_name) | :admin | ref(:headers_job_token_from_owner) | 'deleting package protected'
ref(:package_name) | :admin | ref(:headers_pat_instance_admin) | 'deleting package'
ref(:package_name_no_match) | :owner | ref(:headers_pat_project_owner) | 'deleting package'
end
with_them do
before do
package_protection_rule.update!(
package_name_pattern: package_name_pattern,
package_type: package.package_type,
minimum_access_level_for_delete: minimum_access_level_for_delete
)
end
it_behaves_like params[:shared_examples_name]
end
context 'for package with unsupported package type for package protection rule' do
let_it_be(:nuget_package) { create(:nuget_package, project: project) }
let(:package) { nuget_package }
let(:package_file_id) { nuget_package.package_files.first.id }
let(:headers) { headers_pat_project_maintainer }
it_behaves_like 'deleting package'
end
end
end
end

View File

@ -180,6 +180,37 @@ RSpec.describe API::PersonalAccessTokens, :aggregate_failures, feature_category:
end
end
context 'filter with expires parameter' do
let_it_be(:token1) { create(:personal_access_token, expires_at: Date.new(2022, 01, 01)) }
context 'test expires_before' do
where(:expires_at, :status, :result_count, :result) do
'2022-01-02' | :ok | 1 | lazy { [token1.id] }
'2022-01-01' | :ok | 0 | lazy { [] }
'2022-01-01T12:30:24' | :ok | 0 | lazy { [] }
'asdf' | :bad_request | 1 | { "error" => "expires_before is invalid" }
end
with_them do
it_behaves_like 'response as expected', expires_before: params[:expires_at]
end
end
context 'test expires_after' do
where(:expires_at, :status, :result_count, :result) do
'2022-01-03' | :ok | 1 | lazy { [current_users_token.id] }
'2022-01-01' | :ok | 2 | lazy { [token1.id, current_users_token.id] }
'2022-01-01T12:30:26' | :ok | 2 | lazy { [token1.id, current_users_token.id] }
(DateTime.now + 1).to_s | :ok | 1 | lazy { [current_users_token.id] }
'asdf' | :bad_request | 1 | { "error" => "expires_after is invalid" }
end
with_them do
it_behaves_like 'response as expected', expires_after: params[:expires_at]
end
end
end
context 'filter with search parameter' do
let_it_be(:token1) { create(:personal_access_token, name: 'test_1') }
let_it_be(:token2) { create(:personal_access_token, name: 'test_2') }

View File

@ -735,6 +735,94 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'with package protection rule for different roles and package_name_patterns', :enable_admin_mode do
let_it_be(:pat_project_maintainer) { create(:personal_access_token, user: create(:user, maintainer_of: [project])) }
let_it_be(:pat_project_owner) { create(:personal_access_token, user: create(:user, owner_of: [project])) }
let_it_be(:pat_instance_admin) { create(:personal_access_token, :admin_mode, user: create(:admin)) }
let_it_be(:headers_pat_project_maintainer) { { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_project_maintainer.token } }
let_it_be(:headers_pat_project_owner) { { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_project_owner.token } }
let_it_be(:headers_pat_instance_admin) { { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_instance_admin.token } }
let_it_be(:job_from_project_maintainer) { create(:ci_build, :running, user: pat_project_maintainer.user, project: project) }
let_it_be(:job_from_project_owner) { create(:ci_build, :running, user: pat_project_owner.user, project: project) }
let_it_be(:headers_job_token_from_maintainer) { { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_from_project_maintainer.token } }
let_it_be(:headers_job_token_from_owner) { { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_from_project_owner.token } }
let(:package_protection_rule) { create(:package_protection_rule, project: project) }
let(:package_name) { package1.name }
let(:package_name_no_match) { "#{package_name}_no_match" }
subject do
delete api(package_url), headers: headers
response
end
shared_examples 'deleting package protected' do
it_behaves_like 'returning response status', :forbidden
it do
subject
expect(json_response).to include('message' => "403 Forbidden - Package is deletion protected.")
end
it { expect { subject }.not_to change { ::Packages::Package.pending_destruction.count } }
context 'when feature flag :packages_protected_packages_delete disabled' do
before do
stub_feature_flags(packages_protected_packages_delete: false)
end
it_behaves_like 'deleting package'
end
end
shared_examples 'deleting package' do
it_behaves_like 'returning response status', :no_content
it { expect { subject }.to change { ::Packages::Package.pending_destruction.count }.by(1) }
end
where(:package_name_pattern, :minimum_access_level_for_delete, :headers, :shared_examples_name) do
ref(:package_name) | :owner | ref(:headers_job_token_from_maintainer) | 'deleting package protected'
ref(:package_name) | :owner | ref(:headers_job_token_from_owner) | 'deleting package'
ref(:package_name) | :owner | ref(:headers_pat_project_maintainer) | 'deleting package protected'
ref(:package_name) | :owner | ref(:headers_pat_project_owner) | 'deleting package'
ref(:package_name) | :owner | ref(:headers_pat_instance_admin) | 'deleting package'
ref(:package_name) | :admin | ref(:headers_job_token_from_owner) | 'deleting package protected'
ref(:package_name) | :admin | ref(:headers_pat_project_maintainer) | 'deleting package protected'
ref(:package_name) | :admin | ref(:headers_pat_project_owner) | 'deleting package protected'
ref(:package_name) | :admin | ref(:headers_pat_instance_admin) | 'deleting package'
ref(:package_name_no_match) | :owner | ref(:headers_pat_project_owner) | 'deleting package'
end
with_them do
before do
package_protection_rule.update!(
package_name_pattern: package_name_pattern,
package_type: package1.package_type,
minimum_access_level_for_delete: minimum_access_level_for_delete
)
end
it_behaves_like params[:shared_examples_name]
end
context 'for package with unsupported package type for package protection rule' do
let_it_be(:golang_package) { create(:golang_package, project: project, name: "#{project.root_namespace.path}/golang.org/x/pkg") }
let(:headers) { headers_pat_project_maintainer }
subject do
delete api("/projects/#{project.id}/packages/#{golang_package.id}"), headers: headers
response
end
it_behaves_like 'deleting package'
end
end
end
context 'with a maven package' do

View File

@ -4,7 +4,7 @@ require "spec_helper"
RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:user_non_priviledged) { create(:user) }
let_it_be(:user_non_privileged) { create(:user) }
shared_examples 'resource access token API' do |source_type|
context "GET #{source_type}s/:id/access_tokens" do
@ -13,7 +13,11 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
context "when the user has valid permissions" do
let_it_be(:project_bot) { create(:user, :project_bot, bot_namespace: namespace) }
let_it_be(:active_access_tokens) { create_list(:personal_access_token, 5, user: project_bot) }
let_it_be(:expired_token) { create(:personal_access_token, :expired, user: project_bot) }
let_it_be(:expired_token) do
create(:personal_access_token, :expired, expires_at: 2.days.ago, last_used_at: 2.days.ago, name: 'a_test_1',
user: project_bot)
end
let_it_be(:revoked_token) { create(:personal_access_token, :revoked, user: project_bot) }
let_it_be(:inactive_access_tokens) { [expired_token, revoked_token] }
let_it_be(:all_access_tokens) { active_access_tokens + inactive_access_tokens }
@ -122,39 +126,128 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
end
end
context 'when state param is set to inactive' do
let(:params) { { state: 'inactive' } }
it 'returns only inactive tokens' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params
context 'when filtering by revoked' do
it 'returns non-revoked tokens when revoked is false' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { revoked: false }
token_ids = json_response.map { |token| token['id'] }
expect(token_ids).to match_array(all_access_tokens.pluck(:id).reject { |n| n == revoked_token.id })
end
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/resource_access_tokens')
expect(token_ids).to match_array(inactive_access_tokens.pluck(:id))
it 'returns revoked tokens when revoked is true' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { revoked: true }
token_ids = json_response.map { |token| token['id'] }
expect(token_ids).to match_array([revoked_token.id])
end
end
context 'when state param is set to active' do
let(:params) { { state: 'active' } }
context 'when filtering by state' do
context 'when state param is set to inactive' do
let(:params) { { state: 'inactive' } }
it 'returns only active tokens' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params
it 'returns only inactive tokens' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params
token_ids = json_response.map { |token| token['id'] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/resource_access_tokens')
expect(token_ids).to match_array(inactive_access_tokens.pluck(:id))
end
end
context 'when state param is set to active' do
let(:params) { { state: 'active' } }
it 'returns only active tokens' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params
token_ids = json_response.map { |token| token['id'] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/resource_access_tokens')
expect(token_ids).to match_array(active_access_tokens.pluck(:id))
end
end
end
context 'when filtering by created dates' do
it 'returns tokens created before specified date' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { created_before: 1.day.ago }
expect(json_response).to be_empty
end
it 'returns tokens created after specified date' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { created_after: 1.day.ago }
token_ids = json_response.map { |token| token['id'] }
expect(token_ids).to match_array(all_access_tokens.pluck(:id))
end
end
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/resource_access_tokens')
expect(token_ids).to match_array(active_access_tokens.pluck(:id))
context 'when filtering by last used dates' do
it 'returns tokens last used before specified date' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { last_used_before: 1.day.ago }
token_ids = json_response.map { |token| token['id'] }
expect(token_ids).to match_array([expired_token.id])
end
it 'returns tokens last used after specified date' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { last_used_after: 1.day.ago }
expect(json_response).to be_empty
end
end
context 'when filtering by expiration dates' do
it 'returns tokens that expire before specified date' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { expires_before: 1.day.ago }
token_ids = json_response.map { |token| token['id'] }
expect(token_ids).to match_array([expired_token.id])
end
it 'returns tokens that expire after specified date' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { expires_after: 1.day.ago }
token_ids = json_response.map { |token| token['id'] }
expect(token_ids).to match_array(all_access_tokens.pluck(:id).reject { |n| n == expired_token.id })
end
end
context 'when searching by name' do
it 'returns tokens matching the search term' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { search: 'a_test_1' }
token_ids = json_response.map { |token| token['id'] }
expect(token_ids).to match_array([expired_token.id])
end
end
context 'when sorting' do
it 'sorts tokens by last_used_desc when specified' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { sort: 'name_desc' }
token_ids = json_response.map { |token| token['id'] }
expect(token_ids.last).to eq(expired_token.id)
end
it 'sorts tokens by last_used_asc when specified' do
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { sort: 'name_asc' }
token_ids = json_response.map { |token| token['id'] }
expect(token_ids.first).to eq(expired_token.id)
end
end
end
context "when the user does not have valid permissions" do
let_it_be(:user) { user_non_priviledged }
let_it_be(:user) { user_non_privileged }
let_it_be(:project_bot) { create(:user, :project_bot, bot_namespace: namespace) }
let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) }
let_it_be(:resource_id) { resource.id }
@ -266,7 +359,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
end
context "when the user does not have valid permissions" do
let_it_be(:user) { user_non_priviledged }
let_it_be(:user) { user_non_privileged }
it "returns 401" do
get_token
@ -340,7 +433,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
end
context "when the user does not have valid permissions" do
let_it_be(:user) { user_non_priviledged }
let_it_be(:user) { user_non_privileged }
it "does not delete the token, and returns 400", :aggregate_failures do
delete_token
@ -483,7 +576,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
let_it_be(:resource_id) { resource.id }
context "when the user role is too low" do
let_it_be(:user) { user_non_priviledged }
let_it_be(:user) { user_non_privileged }
it "does not create the token, and returns the permission error" do
create_token
@ -687,7 +780,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
before_all do
resource.add_maintainer(user)
other_resource.add_maintainer(user)
resource.add_developer(user_non_priviledged)
resource.add_developer(user_non_privileged)
end
it_behaves_like 'resource access token API', 'project'
@ -703,7 +796,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
before_all do
resource.add_owner(user)
other_resource.add_owner(user)
resource.add_maintainer(user_non_priviledged)
resource.add_maintainer(user_non_privileged)
end
it_behaves_like 'resource access token API', 'group'

View File

@ -0,0 +1,83 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Protection::CheckDeleteRuleExistenceService, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project) }
let_it_be(:unauthorized_user) { create(:user) }
let_it_be(:project_developer) { create(:user, developer_of: project) }
let_it_be(:project_maintainer) { create(:user, maintainer_of: project) }
let_it_be(:project_owner) { project.owner }
let_it_be(:instance_admin) { create(:admin) }
let_it_be(:container_protection_rule_npm) do
create(:package_protection_rule,
project: project,
package_type: :npm,
package_name_pattern: "@#{project.full_path}*",
minimum_access_level_for_delete: :owner)
end
let_it_be(:container_protection_rule_pypi) do
create(:package_protection_rule,
project: project,
package_type: :pypi,
package_name_pattern: "#{project.full_path}*",
minimum_access_level_for_delete: :admin)
end
let(:params) { { package_name: package_name, package_type: package_type } }
let(:service) { described_class.new(project: project, current_user: current_user, params: params) }
subject(:service_response) { service.execute }
shared_examples 'a service response for protection rule exists' do
it_behaves_like 'returning a success service response'
it { is_expected.to have_attributes(payload: { protection_rule_exists?: true }) }
end
shared_examples 'a service response for protection rule does not exist' do
it_behaves_like 'returning a success service response'
it { is_expected.to have_attributes(payload: { protection_rule_exists?: false }) }
end
shared_examples 'an error service response for unauthorized actor' do
it_behaves_like 'returning an error service response', message: 'Unauthorized'
it { is_expected.to have_attributes reason: :unauthorized }
end
shared_examples 'an error service response for invalid package type' do
it_behaves_like 'returning an error service response', message: 'Invalid package type'
it { is_expected.to have_attributes reason: :invalid_package_type }
end
describe '#execute', :enable_admin_mode do
# rubocop:disable Layout/LineLength -- Avoid formatting in favor of one-line table syntax
where(:package_name, :package_type, :current_user, :expected_shared_example) do
lazy { "@#{project.full_path}" } | :npm | ref(:project_developer) | 'an error service response for unauthorized actor'
lazy { "@#{project.full_path}" } | :npm | ref(:project_maintainer) | 'a service response for protection rule exists'
lazy { "@#{project.full_path}" } | :npm | ref(:project_owner) | 'a service response for protection rule does not exist'
lazy { "@#{project.full_path}" } | :npm | ref(:instance_admin) | 'a service response for protection rule does not exist'
lazy { "@other-scope/#{project.full_path}" } | :npm | ref(:project_maintainer) | 'a service response for protection rule does not exist'
lazy { "@other-scope/#{project.full_path}" } | :npm | ref(:project_owner) | 'a service response for protection rule does not exist'
lazy { project.full_path } | :pypi | ref(:project_maintainer) | 'a service response for protection rule exists'
lazy { project.full_path } | :pypi | ref(:project_owner) | 'a service response for protection rule exists'
lazy { project.full_path } | :pypi | ref(:instance_admin) | 'a service response for protection rule does not exist'
# Edge cases
lazy { "@#{project.full_path}" } | :npm | ref(:unauthorized_user) | 'an error service response for unauthorized actor'
lazy { "@#{project.full_path}" } | :npm | nil | 'an error service response for unauthorized actor'
lazy { "@#{project.full_path}" } | :no_type | nil | 'an error service response for invalid package type'
lazy { "@#{project.full_path}" } | nil | ref(:project_owner) | 'an error service response for invalid package type'
nil | :npm | ref(:project_owner) | 'a service response for protection rule does not exist'
nil | nil | ref(:project_owner) | 'an error service response for invalid package type'
end
# rubocop:enable Layout/LineLength
with_them do
it_behaves_like params[:expected_shared_example]
end
end
end

View File

@ -43,6 +43,20 @@ RSpec.shared_examples 'an access token GET API with access token params' do
)
end
context 'when filtering by revoked' do
it 'returns not-revoked tokens when revoked is false' do
get api_request, params: { revoked: false }
expect_paginated_array_response_contain_exactly(*all_token_ids.excluding(revoked_token1.id, revoked_token2.id))
end
it 'returns revoked tokens when revoked is true' do
get api_request, params: { revoked: true }
expect_paginated_array_response_contain_exactly(revoked_token1.id, revoked_token2.id)
end
end
context 'when filtering by state' do
it 'returns active tokens when state is active' do
get api_request, params: { state: 'active' }
@ -68,20 +82,6 @@ RSpec.shared_examples 'an access token GET API with access token params' do
end
end
context 'when filtering by revoked' do
it 'returns not-revoked tokens when revoked is false' do
get api_request, params: { revoked: false }
expect_paginated_array_response_contain_exactly(*all_token_ids.excluding(revoked_token1.id, revoked_token2.id))
end
it 'returns revoked tokens when revoked is true' do
get api_request, params: { revoked: true }
expect_paginated_array_response_contain_exactly(revoked_token1.id, revoked_token2.id)
end
end
context 'when filtering by created dates' do
it 'returns tokens created before specified date' do
get api_request, params: { created_before: 1.day.ago }
@ -110,6 +110,20 @@ RSpec.shared_examples 'an access token GET API with access token params' do
end
end
context 'when filtering by expiration dates' do
it 'returns tokens that expire before specified date' do
get api_request, params: { expires_before: 1.year.ago + 1.day }
expect_paginated_array_response_contain_exactly(expired_token1.id, expired_token2.id)
end
it 'returns tokens that expire after specified date' do
get api_request, params: { expires_after: 1.year.ago, expires_before: 1.week.ago }
expect_paginated_array_response_contain_exactly(expired_token1.id, expired_token2.id)
end
end
context 'when searching by name' do
it 'returns tokens matching the search term' do
get api_request, params: { search: 'test' }

View File

@ -1441,10 +1441,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.123.0.tgz#1fa3b1a709755ff7c8ef67e18c0442101655ebf0"
integrity sha512-yjVn+utOTIKk8d9JlvGo6EgJ4TQ+CKpe3RddflAqtsQqQuL/2MlVdtaUePybxYzWIaumFuh5LouQ6BrWyw1niQ==
"@gitlab/ui@110.0.0":
version "110.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-110.0.0.tgz#3a1fd77513063d338b8a4676453e53321f58230b"
integrity sha512-AYiPQTQW3Hh+3j0eZr1WvuzDVNHEssxgrO66jbQO79WmSr88IXO13kICAbs0T1bvmS2vZx8VyP/TQ0ow3UJMHQ==
"@gitlab/ui@110.1.0":
version "110.1.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-110.1.0.tgz#5a38aafb92d67b589318c39f72b5ba622fa89b83"
integrity sha512-tCezdqWgSNKuksfvVfm8TWBSIbkuK0jhCoffFFKl3HzBf9FWnCqS5+XEHLU3nPttZBTi5T761BTNqqHx8SZUAg==
dependencies:
"@floating-ui/dom" "1.4.3"
echarts "^5.3.2"