Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
20b517258a
commit
c8512eb431
|
@ -151,10 +151,10 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.filterParams['not[iteration_id]']) {
|
||||
if (this.filterParams['not[iterationId]']) {
|
||||
filteredSearchValue.push({
|
||||
type: 'iteration_id',
|
||||
value: { data: this.filterParams['not[iteration_id]'], operator: '!=' },
|
||||
type: 'iteration',
|
||||
value: { data: this.filterParams['not[iterationId]'], operator: '!=' },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
import { transformBoardConfig } from 'ee_else_ce/boards/boards_util';
|
||||
import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
|
||||
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
|
||||
import { updateHistory } from '~/lib/utils/url_utility';
|
||||
import FilteredSearchContainer from '../filtered_search/container';
|
||||
import vuexstore from './stores';
|
||||
|
||||
export default class FilteredSearchBoards extends FilteredSearchManager {
|
||||
constructor(store, updateUrl = false, cantEdit = []) {
|
||||
super({
|
||||
page: 'boards',
|
||||
isGroupDecendent: true,
|
||||
stateFiltersSelector: '.issues-state-filters',
|
||||
isGroup: IS_EE,
|
||||
useDefaultState: false,
|
||||
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
|
||||
});
|
||||
|
||||
this.store = store;
|
||||
this.updateUrl = updateUrl;
|
||||
|
||||
// Issue boards is slightly different, we handle all the requests async
|
||||
// instead or reloading the page, we just re-fire the list ajax requests
|
||||
this.isHandledAsync = true;
|
||||
this.cantEdit = cantEdit.filter((i) => typeof i === 'string');
|
||||
this.cantEditWithValue = cantEdit.filter((i) => typeof i === 'object');
|
||||
|
||||
if (vuexstore.state.boardConfig) {
|
||||
const boardConfigPath = transformBoardConfig(vuexstore.state.boardConfig);
|
||||
// TODO Refactor: https://gitlab.com/gitlab-org/gitlab/-/issues/329274
|
||||
// here we are using "window.location.search" as a temporary store
|
||||
// only to unpack the params and do another validation inside
|
||||
// 'performSearch' and 'setFilter' vuex actions.
|
||||
if (boardConfigPath !== '') {
|
||||
const filterPath = window.location.search ? `${window.location.search}&` : '?';
|
||||
updateHistory({
|
||||
url: `${filterPath}${transformBoardConfig(vuexstore.state.boardConfig)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateObject(path) {
|
||||
const groupByParam = new URLSearchParams(window.location.search).get('group_by');
|
||||
this.store.path = `${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`;
|
||||
|
||||
updateHistory({
|
||||
url: `?${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`,
|
||||
});
|
||||
vuexstore.dispatch('performSearch');
|
||||
}
|
||||
|
||||
removeTokens() {
|
||||
const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token');
|
||||
|
||||
// Remove all the tokens as they will be replaced by the search manager
|
||||
[].forEach.call(tokens, (el) => {
|
||||
el.parentNode.removeChild(el);
|
||||
});
|
||||
|
||||
this.filteredSearchInput.value = '';
|
||||
}
|
||||
|
||||
updateTokens() {
|
||||
this.removeTokens();
|
||||
|
||||
this.loadSearchParamsFromURL();
|
||||
|
||||
// Get the placeholder back if search is empty
|
||||
this.filteredSearchInput.dispatchEvent(new Event('input'));
|
||||
}
|
||||
|
||||
canEdit(tokenName, tokenValue) {
|
||||
if (this.cantEdit.includes(tokenName)) return false;
|
||||
return (
|
||||
this.cantEditWithValue.findIndex(
|
||||
(token) => token.name === tokenName && token.value === tokenValue,
|
||||
) === -1
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,5 +10,6 @@ export const gqlClient = createDefaultClient(
|
|||
return object.__typename === 'BoardList' ? object.iid : defaultDataIdFromObject(object);
|
||||
},
|
||||
},
|
||||
batchMax: 2,
|
||||
},
|
||||
);
|
||||
|
|
|
@ -8,8 +8,6 @@ import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_t
|
|||
import BoardApp from '~/boards/components/board_app.vue';
|
||||
import '~/boards/filters/due_date_filters';
|
||||
import { issuableTypes } from '~/boards/constants';
|
||||
import eventHub from '~/boards/eventhub';
|
||||
import FilteredSearchBoards from '~/boards/filtered_search_boards';
|
||||
import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards';
|
||||
import store from '~/boards/stores';
|
||||
import toggleFocusMode from '~/boards/toggle_focus';
|
||||
|
@ -50,17 +48,6 @@ function mountBoardApp(el) {
|
|||
},
|
||||
});
|
||||
|
||||
if (!gon?.features?.issueBoardsFilteredSearch) {
|
||||
// Warning: FilteredSearchBoards has an implicit dependency on the Vuex state 'boardConfig'
|
||||
// Improve this situation in the future.
|
||||
const filterManager = new FilteredSearchBoards({ path: '' }, true, []);
|
||||
filterManager.setup();
|
||||
|
||||
eventHub.$on('updateTokens', () => {
|
||||
filterManager.updateTokens();
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
|
@ -110,10 +97,14 @@ export default () => {
|
|||
}
|
||||
});
|
||||
|
||||
if (gon?.features?.issueBoardsFilteredSearch) {
|
||||
const { releasesFetchPath } = $boardApp.dataset;
|
||||
initBoardsFilteredSearch(apolloProvider, isLoggedIn(), releasesFetchPath);
|
||||
}
|
||||
const { releasesFetchPath, epicFeatureAvailable, iterationFeatureAvailable } = $boardApp.dataset;
|
||||
initBoardsFilteredSearch(
|
||||
apolloProvider,
|
||||
isLoggedIn(),
|
||||
releasesFetchPath,
|
||||
parseBoolean(epicFeatureAvailable),
|
||||
parseBoolean(iterationFeatureAvailable),
|
||||
);
|
||||
|
||||
mountBoardApp($boardApp);
|
||||
|
||||
|
|
|
@ -4,7 +4,13 @@ import store from '~/boards/stores';
|
|||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { queryToObject } from '~/lib/utils/url_utility';
|
||||
|
||||
export default (apolloProvider, isSignedIn, releasesFetchPath) => {
|
||||
export default (
|
||||
apolloProvider,
|
||||
isSignedIn,
|
||||
releasesFetchPath,
|
||||
epicFeatureAvailable,
|
||||
iterationFeatureAvailable,
|
||||
) => {
|
||||
const el = document.getElementById('js-issue-board-filtered-search');
|
||||
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
|
||||
|
||||
|
@ -23,6 +29,8 @@ export default (apolloProvider, isSignedIn, releasesFetchPath) => {
|
|||
initialFilterParams,
|
||||
isSignedIn,
|
||||
releasesFetchPath,
|
||||
epicFeatureAvailable,
|
||||
iterationFeatureAvailable,
|
||||
},
|
||||
store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094
|
||||
apolloProvider,
|
||||
|
|
|
@ -10,6 +10,7 @@ import ISetter from '~/filtered_search/droplab/plugins/input_setter';
|
|||
import createFlash from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
|
||||
// Todo: Remove this when fixing issue in input_setter plugin
|
||||
const InputSetter = { ...ISetter };
|
||||
|
@ -171,12 +172,21 @@ export default class CreateMergeRequestDropdown {
|
|||
this.isCreatingMergeRequest = true;
|
||||
|
||||
return this.createBranch().then(() => {
|
||||
window.location.href = canCreateConfidentialMergeRequest()
|
||||
let path = canCreateConfidentialMergeRequest()
|
||||
? this.createMrPath.replace(
|
||||
this.projectPath,
|
||||
confidentialMergeRequestState.selectedProject.pathWithNamespace,
|
||||
)
|
||||
: this.createMrPath;
|
||||
path = mergeUrlParams(
|
||||
{
|
||||
'merge_request[target_branch]': this.refInput.value,
|
||||
'merge_request[source_branch]': this.branchInput.value,
|
||||
},
|
||||
path,
|
||||
);
|
||||
|
||||
window.location.href = path;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import createFlash from '~/flash';
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import { __ } from '~/locale';
|
||||
import AccessDropdown from '~/projects/settings/access_dropdown';
|
||||
import { initToggle } from '~/toggles';
|
||||
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
|
||||
|
||||
export default class ProtectedBranchEdit {
|
||||
|
@ -14,8 +15,6 @@ export default class ProtectedBranchEdit {
|
|||
this.$wrap = options.$wrap;
|
||||
this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
|
||||
this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
|
||||
this.$forcePushToggle = this.$wrap.find('.js-force-push-toggle');
|
||||
this.$codeOwnerToggle = this.$wrap.find('.js-code-owner-toggle');
|
||||
|
||||
this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest(
|
||||
`.${ACCESS_LEVELS.MERGE}-container`,
|
||||
|
@ -25,38 +24,45 @@ export default class ProtectedBranchEdit {
|
|||
);
|
||||
|
||||
this.buildDropdowns();
|
||||
this.bindEvents();
|
||||
this.initToggles();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.$forcePushToggle.on('click', this.onForcePushToggleClick.bind(this));
|
||||
initToggles() {
|
||||
const wrap = this.$wrap.get(0);
|
||||
|
||||
const forcePushToggle = initToggle(wrap.querySelector('.js-force-push-toggle'));
|
||||
forcePushToggle.$on('change', (value) => {
|
||||
forcePushToggle.isLoading = true;
|
||||
forcePushToggle.disabled = true;
|
||||
this.updateProtectedBranch(
|
||||
{
|
||||
allow_force_push: value,
|
||||
},
|
||||
() => {
|
||||
forcePushToggle.isLoading = false;
|
||||
forcePushToggle.disabled = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (this.hasLicense) {
|
||||
this.$codeOwnerToggle.on('click', this.onCodeOwnerToggleClick.bind(this));
|
||||
const codeOwnerToggle = initToggle(wrap.querySelector('.js-code-owner-toggle'));
|
||||
codeOwnerToggle.$on('change', (value) => {
|
||||
codeOwnerToggle.isLoading = true;
|
||||
codeOwnerToggle.disabled = true;
|
||||
this.updateProtectedBranch(
|
||||
{
|
||||
code_owner_approval_required: value,
|
||||
},
|
||||
() => {
|
||||
codeOwnerToggle.isLoading = false;
|
||||
codeOwnerToggle.disabled = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onForcePushToggleClick() {
|
||||
this.$forcePushToggle.toggleClass('is-checked');
|
||||
this.$forcePushToggle.prop('disabled', true);
|
||||
|
||||
const formData = {
|
||||
allow_force_push: this.$forcePushToggle.hasClass('is-checked'),
|
||||
};
|
||||
|
||||
this.updateProtectedBranch(formData, () => this.$forcePushToggle.prop('disabled', false));
|
||||
}
|
||||
|
||||
onCodeOwnerToggleClick() {
|
||||
this.$codeOwnerToggle.toggleClass('is-checked');
|
||||
this.$codeOwnerToggle.prop('disabled', true);
|
||||
|
||||
const formData = {
|
||||
code_owner_approval_required: this.$codeOwnerToggle.hasClass('is-checked'),
|
||||
};
|
||||
|
||||
this.updateProtectedBranch(formData, () => this.$codeOwnerToggle.prop('disabled', false));
|
||||
}
|
||||
|
||||
updateProtectedBranch(formData, callback) {
|
||||
axios
|
||||
.patch(this.$wrap.data('url'), {
|
||||
|
|
|
@ -7,7 +7,6 @@ class Groups::BoardsController < Groups::ApplicationController
|
|||
|
||||
before_action :assign_endpoint_vars
|
||||
before_action do
|
||||
push_frontend_feature_flag(:issue_boards_filtered_search, group, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:iteration_cadences, group, default_enabled: :yaml)
|
||||
experiment(:prominent_create_board_btn, subject: current_user) do |e|
|
||||
|
|
|
@ -7,7 +7,6 @@ class Projects::BoardsController < Projects::ApplicationController
|
|||
before_action :check_issues_available!
|
||||
before_action :assign_endpoint_vars
|
||||
before_action do
|
||||
push_frontend_feature_flag(:issue_boards_filtered_search, project&.group, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml)
|
||||
experiment(:prominent_create_board_btn, subject: current_user) do |e|
|
||||
|
|
|
@ -56,7 +56,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
|
|||
end
|
||||
|
||||
def replace_path
|
||||
url_helpers.project_create_blob_path(project, ref_qualified_path)
|
||||
url_helpers.project_update_blob_path(project, ref_qualified_path)
|
||||
end
|
||||
|
||||
def pipeline_editor_path
|
||||
|
|
|
@ -24,4 +24,4 @@
|
|||
= _('Never')
|
||||
|
||||
%td
|
||||
= link_to _('Remove'), profile_chat_name_path(chat_name), method: :delete, class: 'gl-button btn btn-danger float-right', data: { confirm: _('Are you sure you want to revoke this nickname?') }
|
||||
= link_to _('Remove'), profile_chat_name_path(chat_name), method: :delete, class: 'gl-button btn btn-danger float-right', aria: { label: _('Remove') }, data: { confirm: _('Are you sure you want to remove this nickname?'), confirm_btn_variant: 'danger' }
|
||||
|
|
|
@ -5,10 +5,6 @@
|
|||
- placeholder = local_assigns[:placeholder] || _('Search or filter results...')
|
||||
- block_css_class = type != :productivity_analytics ? 'row-content-block second-block' : ''
|
||||
- is_epic_board = board&.to_type == "EpicBoard"
|
||||
- if @group.present?
|
||||
- ff_resource = @group
|
||||
- else
|
||||
- ff_resource = board&.resource_parent&.group
|
||||
|
||||
- if is_epic_board
|
||||
- user_can_admin_list = can?(current_user, :admin_epic_board_list, board.resource_parent)
|
||||
|
@ -31,7 +27,7 @@
|
|||
= check_box_tag checkbox_id, nil, false, class: "check-all-issues left"
|
||||
- if is_epic_board
|
||||
#js-board-filtered-search{ data: { full_path: @group&.full_path } }
|
||||
- elsif Feature.enabled?(:issue_boards_filtered_search, ff_resource, default_enabled: :yaml) && board
|
||||
- elsif board
|
||||
#js-issue-board-filtered-search
|
||||
- else
|
||||
.issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row
|
||||
|
|
|
@ -34,4 +34,8 @@
|
|||
= _('Members of %{group} can also push to this branch: %{branch}') % { group: (group_push_access_levels.size > 1 ? 'these groups' : 'this group'), branch: group_push_access_levels.map(&:humanize).to_sentence }
|
||||
|
||||
%td
|
||||
= render "shared/buttons/project_feature_toggle", is_checked: protected_branch.allow_force_push, label: s_("ProtectedBranch|Toggle allowed to force push"), class_list: "js-force-push-toggle project-feature-toggle", data: { qa_selector: 'force_push_toggle_button', qa_branch_name: protected_branch.name }
|
||||
= render "shared/gl_toggle",
|
||||
classes: 'js-force-push-toggle',
|
||||
label: s_("ProtectedBranch|Toggle allowed to force push"),
|
||||
is_checked: protected_branch.allow_force_push,
|
||||
label_position: 'hidden'
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: issue_boards_filtered_search
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61752
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331649
|
||||
milestone: '14.1'
|
||||
name: exit_registration_verification
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80286
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352397
|
||||
milestone: '14.8'
|
||||
type: development
|
||||
group: group::product planning
|
||||
default_enabled: true
|
||||
group: group::activation
|
||||
default_enabled: false
|
|
@ -69,7 +69,10 @@ Rails.application.routes.draw do
|
|||
resources :groups, only: [:new, :create]
|
||||
resources :projects, only: [:new, :create]
|
||||
resources :groups_projects, only: [:new, :create] do
|
||||
post :import, on: :collection
|
||||
collection do
|
||||
post :import
|
||||
put :exit
|
||||
end
|
||||
end
|
||||
draw :verification
|
||||
end
|
||||
|
|
|
@ -40,11 +40,16 @@ const CACHE_PATH = process.env.WEBPACK_CACHE_PATH || path.join(ROOT_PATH, 'tmp/c
|
|||
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
|
||||
const IS_DEV_SERVER = process.env.WEBPACK_SERVE === 'true';
|
||||
|
||||
const { DEV_SERVER_HOST, DEV_SERVER_PUBLIC_ADDR } = process.env;
|
||||
const {
|
||||
DEV_SERVER_HOST,
|
||||
DEV_SERVER_PUBLIC_ADDR,
|
||||
DEV_SERVER_TYPE,
|
||||
DEV_SERVER_SSL_KEY,
|
||||
DEV_SERVER_SSL_CERT,
|
||||
} = process.env;
|
||||
const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10);
|
||||
const DEV_SERVER_ALLOWED_HOSTS =
|
||||
process.env.DEV_SERVER_ALLOWED_HOSTS && process.env.DEV_SERVER_ALLOWED_HOSTS.split(',');
|
||||
const DEV_SERVER_HTTPS = process.env.DEV_SERVER_HTTPS && process.env.DEV_SERVER_HTTPS !== 'false';
|
||||
const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false';
|
||||
const INCREMENTAL_COMPILER_ENABLED =
|
||||
IS_DEV_SERVER &&
|
||||
|
@ -709,7 +714,6 @@ module.exports = {
|
|||
},
|
||||
host: DEV_SERVER_HOST || 'localhost',
|
||||
port: DEV_SERVER_PORT || 3808,
|
||||
https: DEV_SERVER_HTTPS,
|
||||
hot: DEV_SERVER_LIVERELOAD,
|
||||
// The following settings are mainly needed for HMR support in gitpod.
|
||||
// Per default only local hosts are allowed, but here we could
|
||||
|
@ -720,6 +724,13 @@ module.exports = {
|
|||
client: {
|
||||
...(DEV_SERVER_PUBLIC_ADDR ? { webSocketURL: DEV_SERVER_PUBLIC_ADDR } : {}),
|
||||
},
|
||||
server: {
|
||||
type: DEV_SERVER_TYPE || 'http',
|
||||
options: {
|
||||
key: DEV_SERVER_SSL_KEY,
|
||||
cert: DEV_SERVER_SSL_CERT,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
devtool: NO_SOURCEMAPS ? false : devtool,
|
||||
|
|
|
@ -6,6 +6,41 @@ module Gitlab
|
|||
module Security
|
||||
module Validators
|
||||
class SchemaValidator
|
||||
# https://docs.gitlab.com/ee/update/deprecations.html#147
|
||||
SUPPORTED_VERSIONS = {
|
||||
cluster_image_scanning: %w[14.0.4 14.0.5 14.0.6 14.1.0],
|
||||
container_scanning: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0],
|
||||
coverage_fuzzing: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0],
|
||||
dast: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0],
|
||||
api_fuzzing: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0],
|
||||
dependency_scanning: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0],
|
||||
sast: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0],
|
||||
secret_detection: %w[14.0.0 14.0.1 14.0.2 14.0.3 14.0.4 14.0.5 14.0.6 14.1.0]
|
||||
}.freeze
|
||||
|
||||
# https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/tags
|
||||
PREVIOUS_RELEASES = %w[10.0.0 12.0.0 12.1.0 13.0.0
|
||||
13.1.0 2.3.0-rc1 2.3.0-rc1 2.3.1-rc1 2.3.2-rc1 2.3.3-rc1
|
||||
2.4.0-rc1 3.0.0 3.0.0-rc1 3.1.0-rc1 4.0.0-rc1 5.0.0-rc1
|
||||
5.0.1-rc1 6.0.0-rc1 6.0.1-rc1 6.1.0-rc1 7.0.0-rc1 7.0.1-rc1
|
||||
8.0.0-rc1 8.0.1-rc1 8.1.0-rc1 9.0.0-rc1].freeze
|
||||
|
||||
# These come from https://app.periscopedata.com/app/gitlab/895813/Secure-Scan-metrics?widget=12248944&udv=1385516
|
||||
KNOWN_VERSIONS_TO_DEPRECATE = %w[0.1 1.0 1.0.0 1.2 1.3 10.0.0 12.1.0 13.1.0 2.0 2.1 2.1.0 2.3 2.3.0 2.4 3.0 3.0.0 3.0.6 3.13.2 V2.7.0].freeze
|
||||
|
||||
VERSIONS_TO_DEPRECATE_IN_15_0 = (PREVIOUS_RELEASES + KNOWN_VERSIONS_TO_DEPRECATE).freeze
|
||||
|
||||
DEPRECATED_VERSIONS = {
|
||||
cluster_image_scanning: VERSIONS_TO_DEPRECATE_IN_15_0,
|
||||
container_scanning: VERSIONS_TO_DEPRECATE_IN_15_0,
|
||||
coverage_fuzzing: VERSIONS_TO_DEPRECATE_IN_15_0,
|
||||
dast: VERSIONS_TO_DEPRECATE_IN_15_0,
|
||||
api_fuzzing: VERSIONS_TO_DEPRECATE_IN_15_0,
|
||||
dependency_scanning: VERSIONS_TO_DEPRECATE_IN_15_0,
|
||||
sast: VERSIONS_TO_DEPRECATE_IN_15_0,
|
||||
secret_detection: VERSIONS_TO_DEPRECATE_IN_15_0
|
||||
}.freeze
|
||||
|
||||
class Schema
|
||||
def root_path
|
||||
File.join(__dir__, 'schemas')
|
||||
|
|
|
@ -4788,6 +4788,9 @@ msgstr ""
|
|||
msgid "Are you sure you want to remove this list?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to remove this nickname?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to reset the health check token?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -4800,9 +4803,6 @@ msgstr ""
|
|||
msgid "Are you sure you want to revoke this %{type}? This action cannot be undone."
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to revoke this nickname?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to revoke this personal access token? This action cannot be undone."
|
||||
msgstr ""
|
||||
|
||||
|
@ -14670,6 +14670,9 @@ msgstr ""
|
|||
msgid "Existing sign in methods may be removed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Exit."
|
||||
msgstr ""
|
||||
|
||||
msgid "Expand"
|
||||
msgstr ""
|
||||
|
||||
|
@ -18275,6 +18278,9 @@ msgstr ""
|
|||
msgid "IdentityVerification|Verify your identity"
|
||||
msgstr ""
|
||||
|
||||
msgid "IdentityVerification|You can always verify your account at a later time to create a group."
|
||||
msgstr ""
|
||||
|
||||
msgid "If any indexed field exceeds this limit, it is truncated to this number of characters. The rest of the content is neither indexed nor searchable. This does not apply to repository and wiki indexing. For unlimited characters, set this to 0."
|
||||
msgstr ""
|
||||
|
||||
|
@ -32418,9 +32424,6 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|Invalid policy type"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Latest scan"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Latest scan run against %{agent}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -32430,6 +32433,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|New policy"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|No description"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|No rules defined - policy will not run."
|
||||
msgstr ""
|
||||
|
||||
|
@ -32442,6 +32448,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|Policies"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Policy Type"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Policy cannot be enabled for non-existing branches (%{branches})"
|
||||
msgstr ""
|
||||
|
||||
|
@ -32586,9 +32595,6 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|users with ids"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|view results"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|vulnerabilities"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -120,7 +120,7 @@
|
|||
"dateformat": "^5.0.1",
|
||||
"deckar01-task_list": "^2.3.1",
|
||||
"diff": "^3.4.0",
|
||||
"dompurify": "^2.3.5",
|
||||
"dompurify": "^2.3.6",
|
||||
"dropzone": "^4.2.0",
|
||||
"editorconfig": "^0.15.3",
|
||||
"emoji-regex": "^10.0.0",
|
||||
|
|
|
@ -64,4 +64,9 @@ desc "Deletes resources created during E2E test runs"
|
|||
task :delete_test_resources, :file_pattern do |t, args|
|
||||
QA::Tools::DeleteTestResources.new(args[:file_pattern]).run
|
||||
end
|
||||
|
||||
desc "Deletes test users"
|
||||
task :delete_test_users, [:delete_before, :dry_run, :exclude_users] do |t, args|
|
||||
QA::Tools::DeleteTestUsers.new(args).run
|
||||
end
|
||||
# rubocop:enable Rails/RakeEnvironment
|
||||
|
|
|
@ -87,20 +87,21 @@ module QA
|
|||
end
|
||||
|
||||
def update(url)
|
||||
row_index = find_repository_row_index url
|
||||
row_index = find_repository_row_index(url)
|
||||
|
||||
within_element_by_index(:mirrored_repository_row, row_index) do
|
||||
# When a repository is first mirrored, the update process might
|
||||
# already be started, so the button is already "clicked"
|
||||
click_element :update_now_button unless has_element? :updating_button
|
||||
end
|
||||
end
|
||||
|
||||
# Wait a few seconds for the sync to occur and then refresh the page
|
||||
# so that 'last update' shows 'just now' or a period in seconds
|
||||
sleep 5
|
||||
def verify_update(url)
|
||||
refresh
|
||||
|
||||
wait_until(max_duration: 180, sleep_interval: 1) do
|
||||
row_index = find_repository_row_index(url)
|
||||
|
||||
wait_until(sleep_interval: 1) do
|
||||
within_element_by_index(:mirrored_repository_row, row_index) do
|
||||
last_update = find_element(:mirror_last_update_at_cell, wait: 0)
|
||||
last_update.has_text?('just now') || last_update.has_text?('seconds')
|
||||
|
|
|
@ -165,6 +165,14 @@ module QA
|
|||
def transform_api_resource(api_resource)
|
||||
api_resource
|
||||
end
|
||||
|
||||
# Get api request url
|
||||
#
|
||||
# @param [String] path
|
||||
# @return [String]
|
||||
def request_url(path, **opts)
|
||||
Runtime::API::Request.new(api_client, path, **opts).url
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ module QA
|
|||
class DeployKey < Base
|
||||
attr_accessor :title, :key
|
||||
|
||||
attribute :id
|
||||
|
||||
attribute :md5_fingerprint do
|
||||
Page::Project::Settings::Repository.perform do |setting|
|
||||
setting.expand_deploy_keys do |key|
|
||||
|
@ -34,6 +36,46 @@ module QA
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fabricate_via_api!
|
||||
resource_web_url(api_get)
|
||||
rescue ResourceNotFoundError
|
||||
super
|
||||
end
|
||||
|
||||
def resource_web_url(resource)
|
||||
super
|
||||
rescue ResourceURLMissingError
|
||||
# this particular resource does not expose a web_url property
|
||||
end
|
||||
|
||||
def api_get_path
|
||||
"/projects/#{project.id}/deploy_keys/#{find_id}"
|
||||
end
|
||||
|
||||
def api_post_path
|
||||
"/projects/#{project.id}/deploy_keys"
|
||||
end
|
||||
|
||||
def api_post_body
|
||||
{
|
||||
key: key,
|
||||
title: title
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_id
|
||||
id
|
||||
rescue NoValueError
|
||||
found_key = auto_paginated_response(request_url("/projects/#{project.id}/deploy_keys", per_page: '100'))
|
||||
.find { |keys| keys[:key].strip == @key.strip }
|
||||
|
||||
return found_key.fetch(:id) if found_key
|
||||
|
||||
raise ResourceNotFoundError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -407,14 +407,6 @@ module QA
|
|||
Git::Location.new(api_resource[:http_url_to_repo])
|
||||
api_resource
|
||||
end
|
||||
|
||||
# Get api request url
|
||||
#
|
||||
# @param [String] path
|
||||
# @return [String]
|
||||
def request_url(path, **opts)
|
||||
Runtime::API::Request.new(api_client, path, **opts).url
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ module QA
|
|||
deploy_key_title = 'deploy key title'
|
||||
deploy_key_value = key.public_key
|
||||
|
||||
deploy_key = Resource::DeployKey.fabricate! do |resource|
|
||||
deploy_key = Resource::DeployKey.fabricate_via_browser_ui! do |resource|
|
||||
resource.title = deploy_key_title
|
||||
resource.key = deploy_key_value
|
||||
end
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This script deletes users with a username starting with "qa-user-"
|
||||
# - Specify `delete_before` to delete only keys that were created before the given date (default: yesterday)
|
||||
# - If `dry_run` is true the script will list the users to be deleted by username, but it won't delete them
|
||||
# - Specify `exclude_users` as a comma-separated list of usernames to not delete.
|
||||
#
|
||||
# Required environment variables: GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS
|
||||
# - GITLAB_QA_ACCESS_TOKEN must have admin API access
|
||||
|
||||
module QA
|
||||
module Tools
|
||||
class DeleteTestUsers
|
||||
include Support::API
|
||||
|
||||
ITEMS_PER_PAGE = '100'
|
||||
EXCLUDE_USERS = %w[qa-user-abc123].freeze
|
||||
FALSY_VALUES = %w[false no 0].freeze
|
||||
|
||||
def initialize(delete_before: (Date.today - 1).to_s, dry_run: 'false', exclude_users: nil)
|
||||
raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS']
|
||||
raise ArgumentError, "Please provide GITLAB_QA_ACCESS_TOKEN" unless ENV['GITLAB_QA_ACCESS_TOKEN']
|
||||
|
||||
@api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN'])
|
||||
@dry_run = !FALSY_VALUES.include?(dry_run.to_s.downcase)
|
||||
@delete_before = Date.parse(delete_before)
|
||||
@page_no = '1'
|
||||
@exclude_users = Array(exclude_users.to_s.split(',')) + EXCLUDE_USERS
|
||||
end
|
||||
|
||||
def run
|
||||
puts "Deleting users with a username starting with 'qa-user-' created before #{@delete_before}..."
|
||||
|
||||
while page_no.present?
|
||||
users = fetch_test_users
|
||||
|
||||
delete_test_users(users) if users.present?
|
||||
end
|
||||
|
||||
puts "\nDone"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :dry_run, :page_no
|
||||
alias_method :dry_run?, :dry_run
|
||||
|
||||
def fetch_test_users
|
||||
puts "Fetching QA test users from page #{page_no}..."
|
||||
|
||||
response = get Runtime::API::Request.new(@api_client, "/users", page: page_no, per_page: ITEMS_PER_PAGE).url
|
||||
|
||||
# When we reach the last page, the x-next-page header is a blank string
|
||||
@page_no = response.headers[:x_next_page].to_s
|
||||
|
||||
JSON.parse(response.body).select do |user|
|
||||
user['username'].start_with?('qa-user-', 'test-user-') \
|
||||
&& (user['name'] == 'QA Tests' || user['name'].start_with?('QA User')) \
|
||||
&& !@exclude_users.include?(user['username']) \
|
||||
&& Date.parse(user.fetch('created_at', Date.today.to_s)) < @delete_before
|
||||
end
|
||||
end
|
||||
|
||||
def delete_test_users(users)
|
||||
usernames = users.map { |user| user['username'] }.join(', ')
|
||||
if dry_run?
|
||||
puts "Dry run: found users with usernames #{usernames}"
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
puts "Deleting #{users.length} users with usernames #{usernames}..."
|
||||
users.each do |user|
|
||||
delete_response = delete Runtime::API::Request.new(@api_client, "/users/#{user['id']}", hard_delete: 'true').url
|
||||
dot_or_f = delete_response.code == 204 ? "\e[32m.\e[0m" : "\e[31mF\e[0m"
|
||||
print dot_or_f
|
||||
end
|
||||
print "\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -79,7 +79,7 @@ module QA
|
|||
else
|
||||
default
|
||||
end
|
||||
rescue QA::Resource::Base::NoValueError
|
||||
rescue QA::Resource::Base::NoValueError, QA::Resource::Errors::ResourceNotFoundError
|
||||
default
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,8 +22,6 @@ RSpec.describe 'Issue board filters', :js do
|
|||
let(:filter_submit) { find('.gl-search-box-by-click-search-button') }
|
||||
|
||||
before do
|
||||
stub_feature_flags(issue_boards_filtered_search: true)
|
||||
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
|
|
|
@ -13,6 +13,10 @@ RSpec.describe 'Project issue boards', :js do
|
|||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:user2) { create(:user) }
|
||||
|
||||
let(:filtered_search) { find('[data-testid="issue-board-filtered-search"]') }
|
||||
let(:filter_input) { find('.gl-filtered-search-term-input') }
|
||||
let(:filter_submit) { find('.gl-search-box-by-click-search-button') }
|
||||
|
||||
context 'signed in user' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
|
@ -90,8 +94,7 @@ RSpec.describe 'Project issue boards', :js do
|
|||
end
|
||||
|
||||
it 'search closed list' do
|
||||
find('.filtered-search').set(issue8.title)
|
||||
find('.filtered-search').native.send_keys(:enter)
|
||||
set_filter_and_search_by_token_value(issue8.title)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
|
@ -101,8 +104,7 @@ RSpec.describe 'Project issue boards', :js do
|
|||
end
|
||||
|
||||
it 'search list' do
|
||||
find('.filtered-search').set(issue5.title)
|
||||
find('.filtered-search').native.send_keys(:enter)
|
||||
set_filter_and_search_by_token_value(issue5.title)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
|
@ -111,26 +113,6 @@ RSpec.describe 'Project issue boards', :js do
|
|||
expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 0)
|
||||
end
|
||||
|
||||
context 'search list negation queries' do
|
||||
before do
|
||||
visit_project_board_path_without_query_limit(project, board)
|
||||
end
|
||||
|
||||
it 'does not have the != option' do
|
||||
find('.filtered-search').set('label:')
|
||||
|
||||
wait_for_requests
|
||||
within('#js-dropdown-operator') do
|
||||
tokens = all(:css, 'li.filter-dropdown-item')
|
||||
expect(tokens.count).to eq(2)
|
||||
button = tokens[0].find('button')
|
||||
expect(button).to have_content('=')
|
||||
button = tokens[1].find('button')
|
||||
expect(button).to have_content('!=')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows user to delete board' do
|
||||
remove_list
|
||||
|
||||
|
@ -309,8 +291,8 @@ RSpec.describe 'Project issue boards', :js do
|
|||
context 'filtering' do
|
||||
it 'filters by author' do
|
||||
set_filter("author", user2.username)
|
||||
click_filter_link(user2.username)
|
||||
submit_filter
|
||||
click_on user2.username
|
||||
filter_submit.click
|
||||
|
||||
wait_for_requests
|
||||
wait_for_board_cards(2, 1)
|
||||
|
@ -319,8 +301,8 @@ RSpec.describe 'Project issue boards', :js do
|
|||
|
||||
it 'filters by assignee' do
|
||||
set_filter("assignee", user.username)
|
||||
click_filter_link(user.username)
|
||||
submit_filter
|
||||
click_on user.username
|
||||
filter_submit.click
|
||||
|
||||
wait_for_requests
|
||||
|
||||
|
@ -330,8 +312,8 @@ RSpec.describe 'Project issue boards', :js do
|
|||
|
||||
it 'filters by milestone' do
|
||||
set_filter("milestone", "\"#{milestone.title}")
|
||||
click_filter_link(milestone.title)
|
||||
submit_filter
|
||||
click_on milestone.title
|
||||
filter_submit.click
|
||||
|
||||
wait_for_requests
|
||||
wait_for_board_cards(2, 1)
|
||||
|
@ -341,8 +323,8 @@ RSpec.describe 'Project issue boards', :js do
|
|||
|
||||
it 'filters by label' do
|
||||
set_filter("label", testing.title)
|
||||
click_filter_link(testing.title)
|
||||
submit_filter
|
||||
click_on testing.title
|
||||
filter_submit.click
|
||||
|
||||
wait_for_requests
|
||||
wait_for_board_cards(2, 1)
|
||||
|
@ -351,8 +333,10 @@ RSpec.describe 'Project issue boards', :js do
|
|||
|
||||
it 'filters by label with encoded character' do
|
||||
set_filter("label", a_plus.title)
|
||||
click_filter_link(a_plus.title)
|
||||
submit_filter
|
||||
# This one is a char encoding issue like the & issue
|
||||
click_on a_plus.title
|
||||
filter_submit.click
|
||||
wait_for_requests
|
||||
|
||||
wait_for_board_cards(1, 1)
|
||||
wait_for_empty_boards((2..4))
|
||||
|
@ -360,8 +344,8 @@ RSpec.describe 'Project issue boards', :js do
|
|||
|
||||
it 'filters by label with space after reload', :quarantine do
|
||||
set_filter("label", "\"#{accepting.title}")
|
||||
click_filter_link(accepting.title)
|
||||
submit_filter
|
||||
click_on accepting.title
|
||||
filter_submit.click
|
||||
|
||||
# Test after reload
|
||||
page.evaluate_script 'window.location.reload()'
|
||||
|
@ -384,13 +368,13 @@ RSpec.describe 'Project issue boards', :js do
|
|||
it 'removes filtered labels' do
|
||||
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
|
||||
set_filter("label", testing.title)
|
||||
click_filter_link(testing.title)
|
||||
submit_filter
|
||||
click_on testing.title
|
||||
filter_submit.click
|
||||
|
||||
wait_for_board_cards(2, 1)
|
||||
|
||||
find('.clear-search').click
|
||||
submit_filter
|
||||
find('[data-testid="filtered-search-clear-button"]').click
|
||||
filter_submit.click
|
||||
end
|
||||
|
||||
wait_for_board_cards(2, 8)
|
||||
|
@ -400,9 +384,9 @@ RSpec.describe 'Project issue boards', :js do
|
|||
create_list(:labeled_issue, 30, project: project, labels: [planning, testing])
|
||||
|
||||
set_filter("label", testing.title)
|
||||
click_filter_link(testing.title)
|
||||
click_on testing.title
|
||||
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
|
||||
submit_filter
|
||||
filter_submit.click
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
@ -442,10 +426,10 @@ RSpec.describe 'Project issue boards', :js do
|
|||
|
||||
it 'filters by multiple labels', :quarantine do
|
||||
set_filter("label", testing.title)
|
||||
click_filter_link(testing.title)
|
||||
click_on testing.title
|
||||
|
||||
set_filter("label", bug.title)
|
||||
click_filter_link(bug.title)
|
||||
click_on bug.title
|
||||
|
||||
submit_filter
|
||||
|
||||
|
@ -463,7 +447,7 @@ RSpec.describe 'Project issue boards', :js do
|
|||
wait_for_requests
|
||||
end
|
||||
|
||||
page.within('.tokens-container') do
|
||||
page.within('.gl-filtered-search-token') do
|
||||
expect(page).to have_content(bug.title)
|
||||
end
|
||||
|
||||
|
@ -561,19 +545,26 @@ RSpec.describe 'Project issue boards', :js do
|
|||
end
|
||||
end
|
||||
|
||||
def set_filter_and_search_by_token_value(value)
|
||||
filter_input.click
|
||||
filter_input.set(value)
|
||||
filter_submit.click
|
||||
end
|
||||
|
||||
def set_filter(type, text)
|
||||
find('.filtered-search').native.send_keys("#{type}:=#{text}")
|
||||
filter_input.click
|
||||
filter_input.native.send_keys("#{type}:=#{text}")
|
||||
end
|
||||
|
||||
def submit_filter
|
||||
find('.filtered-search').native.send_keys(:enter)
|
||||
filter_input.native.send_keys(:enter)
|
||||
end
|
||||
|
||||
def click_filter_link(link_text)
|
||||
page.within('.filtered-search-box') do
|
||||
page.within(filtered_search) do
|
||||
expect(page).to have_button(link_text)
|
||||
|
||||
click_button(link_text)
|
||||
click_on link_text
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -179,38 +179,6 @@ RSpec.describe 'Labels Hierarchy', :js do
|
|||
|
||||
it_behaves_like 'assigning labels from sidebar'
|
||||
end
|
||||
|
||||
context 'on project board issue sidebar' do
|
||||
let(:board) { create(:board, project: project_1) }
|
||||
|
||||
before do
|
||||
project_1.add_developer(user)
|
||||
|
||||
visit project_board_path(project_1, board)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
find('.board-card').click
|
||||
end
|
||||
|
||||
it_behaves_like 'assigning labels from sidebar'
|
||||
end
|
||||
|
||||
context 'on group board issue sidebar' do
|
||||
let(:board) { create(:board, group: parent) }
|
||||
|
||||
before do
|
||||
parent.add_developer(user)
|
||||
|
||||
visit group_board_path(parent, board)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
find('.board-card').click
|
||||
end
|
||||
|
||||
it_behaves_like 'assigning labels from sidebar'
|
||||
end
|
||||
end
|
||||
|
||||
context 'issuable filtering' do
|
||||
|
@ -242,29 +210,5 @@ RSpec.describe 'Labels Hierarchy', :js do
|
|||
|
||||
it_behaves_like 'filtering by ancestor labels for groups'
|
||||
end
|
||||
|
||||
context 'on project boards filter' do
|
||||
let(:board) { create(:board, project: project_1) }
|
||||
|
||||
before do
|
||||
project_1.add_developer(user)
|
||||
|
||||
visit project_board_path(project_1, board)
|
||||
end
|
||||
|
||||
it_behaves_like 'filtering by ancestor labels for projects', true
|
||||
end
|
||||
|
||||
context 'on group boards filter' do
|
||||
let(:board) { create(:board, group: parent) }
|
||||
|
||||
before do
|
||||
parent.add_developer(user)
|
||||
|
||||
visit group_board_path(parent, board)
|
||||
end
|
||||
|
||||
it_behaves_like 'filtering by ancestor labels for groups', true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,59 +8,101 @@ import ProtectedBranchEdit from '~/protected_branches/protected_branch_edit';
|
|||
jest.mock('~/flash');
|
||||
|
||||
const TEST_URL = `${TEST_HOST}/url`;
|
||||
const FORCE_PUSH_TOGGLE_TESTID = 'force-push-toggle';
|
||||
const CODE_OWNER_TOGGLE_TESTID = 'code-owner-toggle';
|
||||
const IS_CHECKED_CLASS = 'is-checked';
|
||||
const IS_DISABLED_CLASS = 'is-disabled';
|
||||
const IS_LOADING_SELECTOR = '.toggle-loading';
|
||||
|
||||
describe('ProtectedBranchEdit', () => {
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
setFixtures(`<div id="wrap" data-url="${TEST_URL}">
|
||||
<button class="js-force-push-toggle">Toggle</button>
|
||||
</div>`);
|
||||
|
||||
jest.spyOn(ProtectedBranchEdit.prototype, 'buildDropdowns').mockImplementation();
|
||||
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
const findForcePushesToggle = () => document.querySelector('.js-force-push-toggle');
|
||||
const findForcePushToggle = () =>
|
||||
document.querySelector(`div[data-testid="${FORCE_PUSH_TOGGLE_TESTID}"] button`);
|
||||
const findCodeOwnerToggle = () =>
|
||||
document.querySelector(`div[data-testid="${CODE_OWNER_TOGGLE_TESTID}"] button`);
|
||||
|
||||
const create = ({ isChecked = false }) => {
|
||||
if (isChecked) {
|
||||
findForcePushesToggle().classList.add(IS_CHECKED_CLASS);
|
||||
}
|
||||
const create = ({
|
||||
forcePushToggleChecked = false,
|
||||
codeOwnerToggleChecked = false,
|
||||
hasLicense = true,
|
||||
} = {}) => {
|
||||
setFixtures(`<div id="wrap" data-url="${TEST_URL}">
|
||||
<span
|
||||
class="js-force-push-toggle"
|
||||
data-label="Toggle allowed to force push"
|
||||
data-is-checked="${forcePushToggleChecked}"
|
||||
data-testid="${FORCE_PUSH_TOGGLE_TESTID}"></span>
|
||||
<span
|
||||
class="js-code-owner-toggle"
|
||||
data-label="Toggle code owner approval"
|
||||
data-is-checked="${codeOwnerToggleChecked}"
|
||||
data-testid="${CODE_OWNER_TOGGLE_TESTID}"></span>
|
||||
</div>`);
|
||||
|
||||
return new ProtectedBranchEdit({ $wrap: $('#wrap'), hasLicense: false });
|
||||
return new ProtectedBranchEdit({ $wrap: $('#wrap'), hasLicense });
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('when unchecked toggle button', () => {
|
||||
describe('when license supports code owner approvals', () => {
|
||||
beforeEach(() => {
|
||||
create();
|
||||
});
|
||||
|
||||
it('instantiates the code owner toggle', () => {
|
||||
expect(findCodeOwnerToggle()).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when license does not support code owner approvals', () => {
|
||||
beforeEach(() => {
|
||||
create({ hasLicense: false });
|
||||
});
|
||||
|
||||
it('does not instantiate the code owner toggle', () => {
|
||||
expect(findCodeOwnerToggle()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
description | checkedOption | patchParam | finder
|
||||
${'force push'} | ${'forcePushToggleChecked'} | ${'allow_force_push'} | ${findForcePushToggle}
|
||||
${'code owner'} | ${'codeOwnerToggleChecked'} | ${'code_owner_approval_required'} | ${findCodeOwnerToggle}
|
||||
`('when unchecked $description toggle button', ({ checkedOption, patchParam, finder }) => {
|
||||
let toggle;
|
||||
|
||||
beforeEach(() => {
|
||||
create({ isChecked: false });
|
||||
create({ [checkedOption]: false });
|
||||
|
||||
toggle = findForcePushesToggle();
|
||||
toggle = finder();
|
||||
});
|
||||
|
||||
it('is not changed', () => {
|
||||
expect(toggle).not.toHaveClass(IS_CHECKED_CLASS);
|
||||
expect(toggle).not.toBeDisabled();
|
||||
expect(toggle.querySelector(IS_LOADING_SELECTOR)).toBe(null);
|
||||
expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
|
||||
});
|
||||
|
||||
describe('when clicked', () => {
|
||||
beforeEach(() => {
|
||||
mock.onPatch(TEST_URL, { protected_branch: { allow_force_push: true } }).replyOnce(200, {});
|
||||
mock.onPatch(TEST_URL, { protected_branch: { [patchParam]: true } }).replyOnce(200, {});
|
||||
|
||||
toggle.click();
|
||||
});
|
||||
|
||||
it('checks and disables button', () => {
|
||||
expect(toggle).toHaveClass(IS_CHECKED_CLASS);
|
||||
expect(toggle).toBeDisabled();
|
||||
expect(toggle.querySelector(IS_LOADING_SELECTOR)).not.toBe(null);
|
||||
expect(toggle).toHaveClass(IS_DISABLED_CLASS);
|
||||
});
|
||||
|
||||
it('sends update to BE', () =>
|
||||
|
@ -68,7 +110,8 @@ describe('ProtectedBranchEdit', () => {
|
|||
// Args are asserted in the `.onPatch` call
|
||||
expect(mock.history.patch).toHaveLength(1);
|
||||
|
||||
expect(toggle).not.toBeDisabled();
|
||||
expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
|
||||
expect(toggle.querySelector(IS_LOADING_SELECTOR)).toBe(null);
|
||||
expect(createFlash).not.toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -3,6 +3,50 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
|
||||
describe 'SUPPORTED_VERSIONS' do
|
||||
schema_path = Rails.root.join("lib", "gitlab", "ci", "parsers", "security", "validators", "schemas")
|
||||
|
||||
it 'matches DEPRECATED_VERSIONS keys' do
|
||||
expect(described_class::SUPPORTED_VERSIONS.keys).to eq(described_class::DEPRECATED_VERSIONS.keys)
|
||||
end
|
||||
|
||||
context 'files under schema path are explicitly listed' do
|
||||
# We only care about the part that comes before report-format.json
|
||||
# https://rubular.com/r/N8Juz7r8hYDYgD
|
||||
filename_regex = /(?<report_type>[-\w]*)\-report-format.json/
|
||||
|
||||
versions = Dir.glob(File.join(schema_path, "*", File::SEPARATOR)).map { |path| path.split("/").last }
|
||||
|
||||
versions.each do |version|
|
||||
files = Dir[schema_path.join(version, "*.json")]
|
||||
|
||||
files.each do |file|
|
||||
matches = filename_regex.match(file)
|
||||
report_type = matches[:report_type].tr("-", "_").to_sym
|
||||
|
||||
it "#{report_type} #{version}" do
|
||||
expect(described_class::SUPPORTED_VERSIONS[report_type]).to include(version)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'every SUPPORTED_VERSION has a corresponding JSON file' do
|
||||
described_class::SUPPORTED_VERSIONS.each_key do |report_type|
|
||||
# api_fuzzing is covered by DAST schema
|
||||
next if report_type == :api_fuzzing
|
||||
|
||||
described_class::SUPPORTED_VERSIONS[report_type].each do |version|
|
||||
it "#{report_type} #{version} schema file is present" do
|
||||
filename = "#{report_type.to_s.tr("_", "-")}-report-format.json"
|
||||
full_path = schema_path.join(version, filename)
|
||||
expect(File.file?(full_path)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:report_type, :expected_errors, :valid_data) do
|
||||
|
|
|
@ -28,7 +28,7 @@ RSpec.describe BlobPresenter do
|
|||
end
|
||||
|
||||
describe '#replace_path' do
|
||||
it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/create/#{blob.commit_id}/#{blob.path}") }
|
||||
it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/update/#{blob.commit_id}/#{blob.path}") }
|
||||
end
|
||||
|
||||
describe '#can_current_user_push_to_branch' do
|
||||
|
|
|
@ -304,8 +304,6 @@ RSpec.configure do |config|
|
|||
# As we're ready to change `master` usages to `main`, let's enable it
|
||||
stub_feature_flags(main_branch_over_master: false)
|
||||
|
||||
stub_feature_flags(issue_boards_filtered_search: false)
|
||||
|
||||
# Disable issue respositioning to avoid heavy load on database when importing big projects.
|
||||
# This is only turned on when app is handling heavy project imports.
|
||||
# Can be removed when we find a better way to deal with the problem.
|
||||
|
|
|
@ -4953,10 +4953,10 @@ dompurify@2.3.4:
|
|||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.4.tgz#1cf5cf0105ccb4debdf6db162525bd41e6ddacc6"
|
||||
integrity sha512-6BVcgOAVFXjI0JTjEvZy901Rghm+7fDQOrNIcxB4+gdhj6Kwp6T9VBhBY/AbagKHJocRkDYGd6wvI+p4/10xtQ==
|
||||
|
||||
dompurify@^2.3.5:
|
||||
version "2.3.5"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.5.tgz#c83ed5a3ae5ce23e52efe654ea052ffb358dd7e3"
|
||||
integrity sha512-kD+f8qEaa42+mjdOpKeztu9Mfx5bv9gVLO6K9jRx4uGvh6Wv06Srn4jr1wPNY2OOUGGSKHNFN+A8MA3v0E0QAQ==
|
||||
dompurify@^2.3.5, dompurify@^2.3.6:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.6.tgz#2e019d7d7617aacac07cbbe3d88ae3ad354cf875"
|
||||
integrity sha512-OFP2u/3T1R5CEgWCEONuJ1a5+MFKnOYpkywpUSxv/dj1LeBT1erK+JwM7zK0ROy2BRhqVCf0LRw/kHqKuMkVGg==
|
||||
|
||||
domutils@^2.5.2, domutils@^2.6.0:
|
||||
version "2.6.0"
|
||||
|
|
Loading…
Reference in New Issue