diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-08 18:11:24 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-08 18:11:24 +0000 |
commit | 70b375c29f67bdc8bd7e8ade1d5355444106482d (patch) | |
tree | 7e4fdec178464a016953a9aecfd349441edb9f44 | |
parent | 3de2ce7c6b536d63ea2f93239022eb51fa9241c1 (diff) | |
download | gitlab-ce-70b375c29f67bdc8bd7e8ade1d5355444106482d.tar.gz |
Add latest changes from gitlab-org/gitlab@master
79 files changed, 1817 insertions, 554 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 7810021a883..8e3a753b1d8 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -1049,7 +1049,6 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab /app/services/audit_event_service.rb @gitlab-org/manage/compliance /app/services/concerns/audit_event_save_type.rb @gitlab-org/manage/compliance /app/views/profiles/audit_log.html.haml @gitlab-org/manage/compliance -/config/feature_flags/development/custom_headers_streaming_audit_events_ui.yml @gitlab-org/manage/compliance /data/deprecations/14-3-repository-push-audit-events.yml @gitlab-org/manage/compliance /data/removals/15_0/removal_manage_repository_push_audit_event.yml @gitlab-org/manage/compliance /db/docs/audit_events.yml @gitlab-org/manage/compliance diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index 8c48e803ad3..631fe7fef30 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -88,10 +88,8 @@ download-knapsack-report: - .bundle-base - .review:rules:review-qa-reliable stage: prepare - variables: - QA_KNAPSACK_REPORTS: review-qa-reliable,review-qa-all script: - - bundle exec rake "knapsack:download" + - bundle exec rake "knapsack:download[qa]" allow_failure: true artifacts: paths: diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md index cdc33b8aacb..4932fbd7f26 100644 --- a/.gitlab/merge_request_templates/Documentation.md +++ b/.gitlab/merge_request_templates/Documentation.md @@ -43,5 +43,5 @@ Documentation-related MRs should be reviewed by a Technical Writer for a non-blo - If the content still needs to be edited for topic types, you can create a follow-up issue with the ~"docs-technical-debt" label. - [ ] Review by assigned maintainer, who can always request/require the reviews above. Maintainer's review can occur before or after a technical writer review. -/label ~documentation ~"type::maintenance" ~"docs::improvement" +/label ~documentation ~"type::maintenance" ~"docs::improvement" ~"maintenance::refactor" /assign me diff --git a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue index ecb39f214ec..8ee7132bb25 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue @@ -1,7 +1,7 @@ <script> import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlSearchBoxByType } from '@gitlab/ui'; -import { mapGetters } from 'vuex'; import { __, sprintf } from '~/locale'; +import { convertEnvironmentScope } from '../utils'; export default { name: 'CiEnvironmentsDropdown', @@ -12,7 +12,11 @@ export default { GlSearchBoxByType, }, props: { - value: { + environments: { + type: Array, + required: true, + }, + selectedEnvironmentScope: { type: String, required: false, default: '', @@ -24,31 +28,36 @@ export default { }; }, computed: { - ...mapGetters(['joinedEnvironments']), composedCreateButtonLabel() { return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm }); }, - shouldRenderCreateButton() { - return this.searchTerm && !this.joinedEnvironments.includes(this.searchTerm); - }, - filteredResults() { + filteredEnvironments() { const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); - return this.joinedEnvironments.filter((resultString) => + return this.environments.filter((resultString) => resultString.toLowerCase().includes(lowerCasedSearchTerm), ); }, + shouldRenderCreateButton() { + return this.searchTerm && !this.environments.includes(this.searchTerm); + }, + environmentScopeLabel() { + return convertEnvironmentScope(this.selectedEnvironmentScope); + }, }, methods: { selectEnvironment(selected) { - this.$emit('selectEnvironment', selected); - this.searchTerm = ''; + this.$emit('select-environment', selected); + this.clearSearch(); }, - createClicked() { - this.$emit('createClicked', this.searchTerm); - this.searchTerm = ''; + convertEnvironmentScopeValue(scope) { + return convertEnvironmentScope(scope); + }, + createEnvironmentScope() { + this.$emit('create-environment-scope', this.searchTerm); + this.selectEnvironment(this.searchTerm); }, isSelected(env) { - return this.value === env; + return this.selectedEnvironmentScope === env; }, clearSearch() { this.searchTerm = ''; @@ -57,23 +66,23 @@ export default { }; </script> <template> - <gl-dropdown :text="value" @show="clearSearch"> + <gl-dropdown :text="environmentScopeLabel" @show="clearSearch"> <gl-search-box-by-type v-model.trim="searchTerm" data-testid="ci-environment-search" /> <gl-dropdown-item - v-for="environment in filteredResults" + v-for="environment in filteredEnvironments" :key="environment" :is-checked="isSelected(environment)" is-check-item @click="selectEnvironment(environment)" > - {{ environment }} + {{ convertEnvironmentScopeValue(environment) }} </gl-dropdown-item> - <gl-dropdown-item v-if="!filteredResults.length" ref="noMatchingResults">{{ + <gl-dropdown-item v-if="!filteredEnvironments.length" ref="noMatchingResults">{{ __('No matching results') }}</gl-dropdown-item> <template v-if="shouldRenderCreateButton"> <gl-dropdown-divider /> - <gl-dropdown-item data-testid="create-wildcard-button" @click="createClicked"> + <gl-dropdown-item data-testid="create-wildcard-button" @click="createEnvironmentScope"> {{ composedCreateButtonLabel }} </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue index 557a8d6b5ba..dc57f3fe4ce 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue @@ -14,22 +14,26 @@ import { GlModal, GlSprintf, } from '@gitlab/ui'; -import { mapActions, mapState } from 'vuex'; import { getCookie, setCookie } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; import Tracking from '~/tracking'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { mapComputed } from '~/vuex_shared/bindings'; + import { + allEnvironments, AWS_TOKEN_CONSTANTS, ADD_CI_VARIABLE_MODAL_ID, AWS_TIP_DISMISSED_COOKIE_NAME, AWS_TIP_MESSAGE, CONTAINS_VARIABLE_REFERENCE_MESSAGE, + defaultVariableState, ENVIRONMENT_SCOPE_LINK_TITLE, EVENT_LABEL, EVENT_ACTION, + EDIT_VARIABLE_ACTION, + VARIABLE_ACTIONS, + variableOptions, } from '../constants'; + import CiEnvironmentsDropdown from './ci_environments_dropdown.vue'; import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens'; @@ -58,66 +62,75 @@ export default { GlModal, GlSprintf, }, - mixins: [glFeatureFlagsMixin(), trackingMixin], + mixins: [trackingMixin], + inject: [ + 'awsLogoSvgPath', + 'awsTipCommandsLink', + 'awsTipDeployLink', + 'awsTipLearnLink', + 'containsVariableReferenceLink', + 'environmentScopeLink', + 'isProtectedByDefault', + 'maskedEnvironmentVariablesLink', + 'maskableRegex', + 'protectedEnvironmentVariablesLink', + ], + props: { + areScopedVariablesAvailable: { + type: Boolean, + required: false, + default: false, + }, + environments: { + type: Array, + required: false, + default: () => [], + }, + mode: { + type: String, + required: true, + validator(val) { + return VARIABLE_ACTIONS.includes(val); + }, + }, + selectedVariable: { + type: Object, + required: false, + default: () => {}, + }, + }, data() { return { isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true', + typeOptions: variableOptions, validationErrorEventProperty: '', + variable: { ...defaultVariableState, ...this.selectedVariable }, }; }, computed: { - ...mapState([ - 'projectId', - 'environments', - 'typeOptions', - 'variable', - 'variableBeingEdited', - 'isGroup', - 'maskableRegex', - 'selectedEnvironment', - 'isProtectedByDefault', - 'awsLogoSvgPath', - 'awsTipDeployLink', - 'awsTipCommandsLink', - 'awsTipLearnLink', - 'containsVariableReferenceLink', - 'protectedEnvironmentVariablesLink', - 'maskedEnvironmentVariablesLink', - 'environmentScopeLink', - ]), - ...mapComputed( - [ - { key: 'key', updateFn: 'updateVariableKey' }, - { key: 'secret_value', updateFn: 'updateVariableValue' }, - { key: 'variable_type', updateFn: 'updateVariableType' }, - { key: 'environment_scope', updateFn: 'setEnvironmentScope' }, - { key: 'protected_variable', updateFn: 'updateVariableProtected' }, - { key: 'masked', updateFn: 'updateVariableMasked' }, - ], - false, - 'variable', - ), - isTipVisible() { - return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key); - }, - canSubmit() { - return ( - this.variableValidationState && - this.variable.key !== '' && - this.variable.secret_value !== '' - ); - }, canMask() { const regex = RegExp(this.maskableRegex); - return regex.test(this.variable.secret_value); + return regex.test(this.variable.value); + }, + canSubmit() { + return this.variableValidationState && this.variable.key !== '' && this.variable.value !== ''; }, containsVariableReference() { const regex = /\$/; - return regex.test(this.variable.secret_value); + return regex.test(this.variable.value); }, displayMaskedError() { return !this.canMask && this.variable.masked; }, + isEditing() { + return this.mode === EDIT_VARIABLE_ACTION; + }, + isTipVisible() { + return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key); + }, + maskedFeedback() { + return this.displayMaskedError ? __('This variable can not be masked.') : ''; + }, maskedState() { if (this.displayMaskedError) { return false; @@ -125,10 +138,7 @@ export default { return true; }, modalActionText() { - return this.variableBeingEdited ? __('Update variable') : __('Add variable'); - }, - maskedFeedback() { - return this.displayMaskedError ? __('This variable can not be masked.') : ''; + return this.isEditing ? __('Update variable') : __('Add variable'); }, tokenValidationFeedback() { const tokenSpecificFeedback = this.$options.tokens?.[this.variable.key]?.invalidMessage; @@ -141,19 +151,16 @@ export default { const validator = this.$options.tokens?.[this.variable.key]?.validation; if (validator) { - return validator(this.variable.secret_value); + return validator(this.variable.value); } return true; }, - scopedVariablesAvailable() { - return !this.isGroup || this.glFeatures.groupScopedCiVariables; - }, variableValidationFeedback() { return `${this.tokenValidationFeedback} ${this.maskedFeedback}`; }, variableValidationState() { - return this.variable.secret_value === '' || (this.tokenValidationState && this.maskedState); + return this.variable.value === '' || (this.tokenValidationState && this.maskedState); }, }, watch: { @@ -165,19 +172,18 @@ export default { }, }, methods: { - ...mapActions([ - 'addVariable', - 'updateVariable', - 'resetEditing', - 'displayInputValue', - 'clearModal', - 'deleteVariable', - 'setEnvironmentScope', - 'addWildCardScope', - 'resetSelectedEnvironment', - 'setSelectedEnvironment', - 'setVariableProtected', - ]), + addVariable() { + this.$emit('add-variable', this.variable); + }, + createEnvironmentScope(env) { + this.$emit('create-environment-scope', env); + }, + deleteVariable() { + this.$emit('delete-variable', this.variable); + }, + updateVariable() { + this.$emit('update-variable', this.variable); + }, dismissTip() { setCookie(AWS_TIP_DISMISSED_COOKIE_NAME, 'true', { expires: 90 }); this.isTipDismissed = true; @@ -190,16 +196,22 @@ export default { this.$refs.modal.hide(); }, resetModalHandler() { - if (this.variableBeingEdited) { - this.resetEditing(); - } - - this.clearModal(); - this.resetSelectedEnvironment(); + this.resetVariableData(); this.resetValidationErrorEvents(); + + this.$emit('hideModal'); + }, + resetVariableData() { + this.variable = { ...defaultVariableState }; + }, + setEnvironmentScope(scope) { + this.variable = { ...this.variable, environmentScope: scope }; + }, + setVariableProtected() { + this.variable = { ...this.variable, protected: true }; }, updateOrAddVariable() { - if (this.variableBeingEdited) { + if (this.isEditing) { this.updateVariable(); } else { this.addVariable(); @@ -207,7 +219,7 @@ export default { this.hideModal(); }, setVariableProtectedByDefault() { - if (this.isProtectedByDefault && !this.variableBeingEdited) { + if (this.isProtectedByDefault && !this.isEditing) { this.setVariableProtected(); } }, @@ -220,11 +232,11 @@ export default { }, getTrackingErrorProperty() { let property; - if (this.variable.secret_value?.length && !property) { + if (this.variable.value?.length && !property) { if (this.displayMaskedError && this.maskableRegex?.length) { const supportedChars = this.maskableRegex.replace('^', '').replace(/{(\d,)}\$/, ''); const regex = new RegExp(supportedChars, 'g'); - property = this.variable.secret_value.replace(regex, ''); + property = this.variable.value.replace(regex, ''); } if (this.containsVariableReference) { property = '$'; @@ -237,6 +249,7 @@ export default { this.validationErrorEventProperty = ''; }, }, + defaultScope: allEnvironments.text, }; </script> @@ -252,7 +265,7 @@ export default { > <form> <gl-form-combobox - v-model="key" + v-model="variable.key" :token-list="$options.tokenList" :label-text="__('Key')" data-qa-selector="ci_variable_key_field" @@ -267,7 +280,7 @@ export default { <gl-form-textarea id="ci-variable-value" ref="valueField" - v-model="secret_value" + v-model="variable.value" :state="variableValidationState" rows="3" max-rows="6" @@ -278,7 +291,11 @@ export default { <div class="d-flex"> <gl-form-group :label="__('Type')" label-for="ci-variable-type" class="w-50 gl-mr-5"> - <gl-form-select id="ci-variable-type" v-model="variable_type" :options="typeOptions" /> + <gl-form-select + id="ci-variable-type" + v-model="variable.variableType" + :options="typeOptions" + /> </gl-form-group> <gl-form-group label-for="ci-variable-env" class="w-50" data-testid="environment-scope"> @@ -294,22 +311,24 @@ export default { </gl-link> </template> <ci-environments-dropdown - v-if="scopedVariablesAvailable" - class="w-100" - :value="environment_scope" - @selectEnvironment="setEnvironmentScope" - @createClicked="addWildCardScope" + v-if="areScopedVariablesAvailable" + class="gl-w-full" + :selected-environment-scope="variable.environmentScope" + :environments="environments" + @select-environment="setEnvironmentScope" + @create-environment-scope="createEnvironmentScope" /> - <gl-form-input v-else v-model="environment_scope" class="w-100" readonly /> + <gl-form-input v-else :value="$options.defaultScope" class="gl-w-full" readonly /> </gl-form-group> </div> <gl-form-group :label="__('Flags')" label-for="ci-variable-flags"> <gl-form-checkbox - v-model="protected_variable" - class="mb-0" + v-model="variable.protected" + class="gl-mb-0" data-testid="ci-variable-protected-checkbox" + :data-is-protected-checked="variable.protected" > {{ __('Protect variable') }} <gl-link target="_blank" :href="protectedEnvironmentVariablesLink"> @@ -322,7 +341,7 @@ export default { <gl-form-checkbox ref="masked-ci-variable" - v-model="masked" + v-model="variable.masked" data-testid="ci-variable-masked-checkbox" > {{ __('Mask variable') }} @@ -403,7 +422,7 @@ export default { <template #modal-footer> <gl-button @click="hideModal">{{ __('Cancel') }}</gl-button> <gl-button - v-if="variableBeingEdited" + v-if="isEditing" ref="deleteCiVariable" variant="danger" category="secondary" diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue index 7dcc5ce42d7..cebb7eb85ac 100644 --- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue +++ b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue @@ -30,7 +30,7 @@ import { EVENT_LABEL, EVENT_ACTION, } from '../constants'; -import CiEnvironmentsDropdown from './ci_environments_dropdown.vue'; +import LegacyCiEnvironmentsDropdown from './legacy_ci_environments_dropdown.vue'; import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens'; const trackingMixin = Tracking.mixin({ label: EVENT_LABEL }); @@ -43,7 +43,7 @@ export default { containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE, environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE, components: { - CiEnvironmentsDropdown, + LegacyCiEnvironmentsDropdown, GlAlert, GlButton, GlCollapse, @@ -293,7 +293,7 @@ export default { <gl-icon name="question" :size="12" /> </gl-link> </template> - <ci-environments-dropdown + <legacy-ci-environments-dropdown v-if="scopedVariablesAvailable" class="w-100" :value="environment_scope" diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js index fa55b4d9e77..e42a728a44e 100644 --- a/app/assets/javascripts/ci_variable_list/constants.js +++ b/app/assets/javascripts/ci_variable_list/constants.js @@ -2,16 +2,49 @@ import { __ } from '~/locale'; export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable'; +// This const will be deprecated once we remove VueX from the section export const displayText = { variableText: __('Variable'), fileText: __('File'), allEnvironmentsText: __('All (default)'), }; +export const variableTypes = { + variableType: 'ENV_VAR', + fileType: 'FILE', +}; + +// Once REST is removed, we won't need `types` export const types = { variableType: 'env_var', fileType: 'file', - allEnvironmentsType: '*', +}; + +export const allEnvironments = { + type: '*', + text: __('All (default)'), +}; + +// Once REST is removed, we won't need `types` key +export const variableText = { + [types.variableType]: __('Variable'), + [types.fileType]: __('File'), + [variableTypes.variableType]: __('Variable'), + [variableTypes.fileType]: __('File'), +}; + +export const variableOptions = [ + { value: types.variableType, text: variableText[types.variableType] }, + { value: types.fileType, text: variableText[types.fileType] }, +]; + +export const defaultVariableState = { + environmentScope: allEnvironments.type, + key: '', + masked: false, + protected: false, + value: '', + variableType: types.variableType, }; export const AWS_TIP_DISMISSED_COOKIE_NAME = 'ci_variable_list_constants_aws_tip_dismissed'; @@ -33,3 +66,20 @@ export const CONTAINS_VARIABLE_REFERENCE_MESSAGE = __( ); export const ENVIRONMENT_SCOPE_LINK_TITLE = __('Learn more'); + +export const ADD_VARIABLE_ACTION = 'ADD_VARIABLE'; +export const EDIT_VARIABLE_ACTION = 'EDIT_VARIABLE'; +export const VARIABLE_ACTIONS = [ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION]; + +export const GRAPHQL_PROJECT_TYPE = 'Project'; +export const GRAPHQL_GROUP_TYPE = 'Group'; + +export const ADD_MUTATION_ACTION = 'add'; +export const UPDATE_MUTATION_ACTION = 'update'; +export const DELETE_MUTATION_ACTION = 'delete'; + +export const environmentFetchErrorText = __( + 'There was an error fetching the environments information.', +); +export const genericMutationErrorText = __('Something went wrong on our end. Please try again.'); +export const variableFetchErrorText = __('There was an error fetching the variables.'); diff --git a/app/assets/javascripts/ci_variable_list/store/utils.js b/app/assets/javascripts/ci_variable_list/store/utils.js index d9ca460a8e1..f46a671ae7b 100644 --- a/app/assets/javascripts/ci_variable_list/store/utils.js +++ b/app/assets/javascripts/ci_variable_list/store/utils.js @@ -1,5 +1,5 @@ import { cloneDeep } from 'lodash'; -import { displayText, types } from '../constants'; +import { displayText, types, allEnvironments } from '../constants'; const variableTypeHandler = (type) => type === displayText.variableText ? types.variableType : types.fileType; @@ -15,7 +15,7 @@ export const prepareDataForDisplay = (variables) => { } variableCopy.secret_value = variableCopy.value; - if (variableCopy.environment_scope === types.allEnvironmentsType) { + if (variableCopy.environment_scope === allEnvironments.type) { variableCopy.environment_scope = displayText.allEnvironmentsText; } variableCopy.protected_variable = variableCopy.protected; @@ -31,7 +31,7 @@ export const prepareDataForApi = (variable, destroy = false) => { variableCopy.masked = variableCopy.masked.toString(); variableCopy.variable_type = variableTypeHandler(variableCopy.variable_type); if (variableCopy.environment_scope === displayText.allEnvironmentsText) { - variableCopy.environment_scope = types.allEnvironmentsType; + variableCopy.environment_scope = allEnvironments.type; } if (destroy) { diff --git a/app/assets/javascripts/ci_variable_list/utils.js b/app/assets/javascripts/ci_variable_list/utils.js new file mode 100644 index 00000000000..009e56469d1 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/utils.js @@ -0,0 +1,45 @@ +import { uniq } from 'lodash'; +import { allEnvironments } from './constants'; + +/** + * This function takes aa list of variable and environments + * and create a new Array that concatenate the environment list + * with the environment scopes find in the variable list. This is + * useful for variable settings so that we can render a list of all + * environment scopes available based on both the list of envs and what + * is found under each variable. + * @param {Array} variables + * @param {Array} environments + * @returns {Array} - Array of environments + */ + +export const createJoinedEnvironments = (variables = [], environments = []) => { + const scopesFromVariables = variables.map((variable) => variable.environmentScope); + return uniq(environments.concat(scopesFromVariables)).sort(); +}; + +/** + * This function job is to convert the * wildcard to text when applicable + * in the UI. It uses a constants to compare the incoming value to that + * of the * and then apply the corresponding label if applicable. If there + * is no scope, then we return the default value as well. + * @param {String} scope + * @returns {String} - Converted value if applicable + */ + +export const convertEnvironmentScope = (environmentScope = '') => { + if (environmentScope === allEnvironments.type || !environmentScope) { + return allEnvironments.text; + } + + return environmentScope; +}; + +/** + * Gives us an array of all the environments by name + * @param {Array} nodes + * @return {Array<String>} - Array of environments strings + */ +export const mapEnvironmentNames = (nodes = []) => { + return nodes.map((env) => env.name); +}; diff --git a/app/assets/javascripts/labels/labels_select.js b/app/assets/javascripts/labels/labels_select.js index 9d8ee165df2..3e5396c5bd8 100644 --- a/app/assets/javascripts/labels/labels_select.js +++ b/app/assets/javascripts/labels/labels_select.js @@ -438,7 +438,7 @@ export default class LabelsSelect { [ '<% if (isScopedLabel(label) && enableScopedLabels) { %>', "<span class='font-weight-bold scoped-label-tooltip-title'>Scoped label</span>", - '<br />', + '<br>', '<%= escapeStr(label.description) %>', '<% } else { %>', '<%= escapeStr(label.description) %>', diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index fe84660422b..424ea3b61c5 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import { debounce } from 'lodash'; import DEFAULT_PROJECT_TEMPLATES from 'any_else_ce/projects/default_project_templates'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; +import Tracking from '~/tracking'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '../lib/utils/constants'; import { ENTER_KEY } from '../lib/utils/keys'; import axios from '../lib/utils/axios_utils'; @@ -109,8 +110,31 @@ const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => { ); }; + const projectPathValueListener = () => { + // eslint-disable-next-line no-param-reassign + $projectPathInput.oldInputValue = $projectPathInput.value; + }; + + const projectPathTrackListener = () => { + if ($projectPathInput.oldInputValue === $projectPathInput.value) { + // no change made to the input + return; + } + + const trackEvent = 'user_input_path_slug'; + const trackCategory = undefined; // will be default set in event method + + Tracking.event(trackCategory, trackEvent, { + label: 'new_project_form', + }); + }; + $projectPathInput.removeEventListener('keyup', projectPathInputListener); $projectPathInput.addEventListener('keyup', projectPathInputListener); + $projectPathInput.removeEventListener('focus', projectPathValueListener); + $projectPathInput.addEventListener('focus', projectPathValueListener); + $projectPathInput.removeEventListener('blur', projectPathTrackListener); + $projectPathInput.addEventListener('blur', projectPathTrackListener); $projectPathInput.removeEventListener('change', projectPathInputListener); $projectPathInput.addEventListener('change', projectPathInputListener); diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 5a655c92e46..5843e13c7cd 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -7,10 +7,14 @@ class SearchController < ApplicationController include ProductAnalyticsTracking include SearchRateLimitable - RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show, :autocomplete].freeze + RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show, :autocomplete, :aggregations].freeze track_event :show, name: 'i_search_total', destinations: [:redis_hll, :snowplow] + def self.search_rate_limited_endpoints + %i[show count autocomplete] + end + around_action :allow_gitaly_ref_name_caching before_action :block_anonymous_global_searches, :check_scope_global_search_enabled, except: :opensearch @@ -19,7 +23,7 @@ class SearchController < ApplicationController search_term_present = params[:search].present? || params[:term].present? search_term_present && !params[:project_id].present? end - before_action :check_search_rate_limit!, only: [:show, :count, :autocomplete] + before_action :check_search_rate_limit!, only: search_rate_limited_endpoints rescue_from ActiveRecord::QueryCanceled, with: :render_timeout @@ -32,8 +36,6 @@ class SearchController < ApplicationController @project = search_service.project @group = search_service.group - return if params[:search].blank? - return unless search_term_valid? return if check_single_commit_result? @@ -53,7 +55,6 @@ class SearchController < ApplicationController @search_results = @search_service.search_results @search_objects = @search_service.search_objects @search_highlight = @search_service.search_highlight - @aggregations = @search_service.search_aggregations end increment_search_counters @@ -99,6 +100,8 @@ class SearchController < ApplicationController end def search_term_valid? + return false if params[:search].blank? + unless search_service.valid_query_length? flash[:alert] = t('errors.messages.search_chars_too_long', count: Gitlab::Search::Params::SEARCH_CHAR_LIMIT) return false @@ -151,7 +154,7 @@ class SearchController < ApplicationController payload[:metadata]['meta.search.filters.state'] = params[:state] payload[:metadata]['meta.search.force_search_results'] = params[:force_search_results] payload[:metadata]['meta.search.project_ids'] = params[:project_ids] - payload[:metadata]['meta.search.language'] = params[:language] + payload[:metadata]['meta.search.filters.language'] = params[:language] payload[:metadata]['meta.search.type'] = @search_type if @search_type.present? payload[:metadata]['meta.search.level'] = @search_level if @search_level.present? payload[:metadata][:global_search_duration_s] = @global_search_duration_s if @global_search_duration_s.present? @@ -207,7 +210,7 @@ class SearchController < ApplicationController case action_name.to_sym when :count render json: {}, status: :request_timeout - when :autocomplete + when :autocomplete, :aggregations render json: [], status: :request_timeout else render status: :request_timeout diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 3c7d358bde9..98cd831d6f1 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -1,6 +1,7 @@ - visibility_level = selected_visibility_level(@project, params.dig(:project, :visibility_level)) - ci_cd_only = local_assigns.fetch(:ci_cd_only, false) - hide_init_with_readme = local_assigns.fetch(:hide_init_with_readme, false) +- include_description = local_assigns.fetch(:include_description, true) - track_label = local_assigns.fetch(:track_label, 'blank_project') .row{ id: project_name_id } @@ -44,10 +45,19 @@ - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/profile/index', anchor: 'add-details-to-your-profile-with-a-readme') } = html_escape(_('%{project_path} is a project that you can use to add a README to your GitLab profile. Create a public project and initialize the repository with a README to get started. %{help_link_start}Learn more.%{help_link_end}')) % { project_path: "<strong>#{current_user.username} / #{current_user.username}</strong>".html_safe, help_link_start: help_link_start, help_link_end: '</a>'.html_safe } -.form-group - = f.label :description, class: 'label-bold' do - = s_('ProjectsNew|Project description %{tag_start}(optional)%{tag_end}').html_safe % { tag_start: '<span>'.html_safe, tag_end: '</span>'.html_safe } - = f.text_area :description, placeholder: s_('ProjectsNew|Description format'), class: "form-control gl-form-input", rows: 3, maxlength: 250, data: { qa_selector: 'project_description', track_label: "#{track_label}", track_action: "activate_form_input", track_property: "project_description", track_value: "" } +- if include_description + .form-group + = f.label :description, class: 'label-bold' do + = s_('ProjectsNew|Project description %{tag_start}(optional)%{tag_end}').html_safe % { tag_start: '<span>'.html_safe, tag_end: '</span>'.html_safe } + = f.text_area :description, + placeholder: s_('ProjectsNew|Description format'), + class: "form-control gl-form-input", + rows: 3, + maxlength: 250, + data: { qa_selector: 'project_description', + track_label: track_label, + track_action: "activate_form_input", + track_property: "project_description" } - unless Gitlab::CurrentSettings.current_application_settings.hide_third_party_offers? || !Gitlab.com? .js-deployment-target-select diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 07c38d9845c..56581fe7b18 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -13,7 +13,7 @@ .row{ 'v-cloak': true } #blank-project-pane.tab-pane.active = gitlab_ui_form_for @project, html: { class: 'new_project gl-mt-3' } do |f| - = render 'new_project_fields', f: f, project_name_id: "blank-project-name" + = render 'new_project_fields', f: f, project_name_id: "blank-project-name", include_description: false #create-from-template-pane.tab-pane = render Pajamas::CardComponent.new(card_options: { class: 'gl-my-5' }) do |c| diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml index 0ab99c83fd0..dccf61c6ec5 100644 --- a/app/views/projects/pages/_use.html.haml +++ b/app/views/projects/pages/_use.html.haml @@ -1,10 +1,9 @@ - unless @project.pages_deployed? - .card.border-info - .card-header.bg-info.text-white + = render Pajamas::CardComponent.new(card_options: { class: 'gl-border-blue-500' }, header_options: { class: 'gl-bg-blue-500 gl-text-white' }) do |c| + - c.header do = s_('GitLabPages|Configure pages') - .card-body - %p.gl-mb-0 - - docs_link_start = "<a href='#{help_page_path('user/project/pages/index')}' target='_blank' rel='noopener noreferrer' data-track-action='click_link' data-track-label='pages_docs_link'>".html_safe - - samples_link_start = "<a href='https://gitlab.com/pages' target='_blank' rel='noopener noreferrer' data-track-action='click_link' data-track-label='pages_samples_link'>".html_safe - - link_end = '</a>'.html_safe - = s_('GitLabPages|Your Pages site is not configured yet. See the %{docs_link_start}GitLab Pages documentation%{link_end} to learn how to upload your static site and have GitLab serve it. You can also take some inspiration from the %{samples_link_start}sample Pages projects%{link_end}.').html_safe % { docs_link_start: docs_link_start, samples_link_start: samples_link_start, link_end: link_end } + - c.body do + - docs_link_start = "<a href='#{help_page_path('user/project/pages/index')}' target='_blank' rel='noopener noreferrer' data-track-action='click_link' data-track-label='pages_docs_link'>".html_safe + - samples_link_start = "<a href='https://gitlab.com/pages' target='_blank' rel='noopener noreferrer' data-track-action='click_link' data-track-label='pages_samples_link'>".html_safe + - link_end = '</a>'.html_safe + = s_('GitLabPages|Your Pages site is not configured yet. See the %{docs_link_start}GitLab Pages documentation%{link_end} to learn how to upload your static site and have GitLab serve it. You can also take some inspiration from the %{samples_link_start}sample Pages projects%{link_end}.').html_safe % { docs_link_start: docs_link_start, samples_link_start: samples_link_start, link_end: link_end } diff --git a/app/views/projects/project_templates/_template.html.haml b/app/views/projects/project_templates/_template.html.haml index 5e4b1397dd3..d0fdd3a729a 100644 --- a/app/views/projects/project_templates/_template.html.haml +++ b/app/views/projects/project_templates/_template.html.haml @@ -10,7 +10,8 @@ .controls.d-flex.align-items-center %a.btn.gl-button.btn-default.gl-mr-3{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "template_preview", track_property: template.name, track_action: "click_button", track_value: "" } } = _("Preview") - %label.btn.gl-button.btn-confirm.template-button.choose-template.gl-mb-0{ for: template.name } + %label.btn.gl-button.btn-confirm.template-button.choose-template.gl-mb-0{ for: template.name, + 'data-testid': "use_template_#{template.name}" } %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "template_use", track_property: template.name, track_action: "click_button", track_value: "" } } %span{ data: { qa_selector: 'use_template_button' } } = _("Use template") diff --git a/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb b/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb index 8155b910677..0ec0a1b58d2 100644 --- a/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb +++ b/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb @@ -15,32 +15,34 @@ module Gitlab # client - An instance of Gitlab::GithubImport::Client. # project - An instance of Project. def import(client, project) - importer = ::Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter - return skip_to_next_stage(project, importer) if feature_disabled?(project) + importer = importer_class(project) + return skip_to_next_stage(project) if importer.nil? - start_importer(project, importer, client) + info(project.id, message: "starting importer", importer: importer.name) + waiter = importer.new(project, client).execute + move_to_next_stage(project, { waiter.key => waiter.jobs_remaining }) end private - def start_importer(project, importer, client) - info(project.id, message: "starting importer", importer: importer.name) - waiter = importer.new(project, client).execute - move_to_next_stage(project, waiter.key => waiter.jobs_remaining) + def importer_class(project) + if Feature.enabled?(:github_importer_single_endpoint_issue_events_import, project.group, type: :ops) + ::Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter + elsif Feature.enabled?(:github_importer_issue_events_import, project.group, type: :ops) + ::Gitlab::GithubImport::Importer::IssueEventsImporter + else + nil + end end - def skip_to_next_stage(project, importer) - info(project.id, message: "skipping importer", importer: importer.name) + def skip_to_next_stage(project) + info(project.id, message: "skipping importer", importer: "IssueEventsImporter") move_to_next_stage(project) end def move_to_next_stage(project, waiters = {}) AdvanceStageWorker.perform_async(project.id, waiters, :notes) end - - def feature_disabled?(project) - Feature.disabled?(:github_importer_issue_events_import, project.group, type: :ops) - end end end end diff --git a/config/feature_flags/ops/github_importer_issue_events_import.yml b/config/feature_flags/ops/github_importer_issue_events_import.yml index c4710858a0a..58660ceb287 100644 --- a/config/feature_flags/ops/github_importer_issue_events_import.yml +++ b/config/feature_flags/ops/github_importer_issue_events_import.yml @@ -2,7 +2,7 @@ name: github_importer_issue_events_import introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89134 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/365977 -milestone: '15.2' +milestone: '15.3' type: ops group: group::import default_enabled: false diff --git a/config/feature_flags/development/custom_headers_streaming_audit_events_ui.yml b/config/feature_flags/ops/github_importer_single_endpoint_issue_events_import.yml index 710a4f55130..88e9db6721f 100644 --- a/config/feature_flags/development/custom_headers_streaming_audit_events_ui.yml +++ b/config/feature_flags/ops/github_importer_single_endpoint_issue_events_import.yml @@ -1,8 +1,8 @@ --- -name: custom_headers_streaming_audit_events_ui -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90135 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/365259 -milestone: '15.2' -type: development -group: group::compliance +name: github_importer_single_endpoint_issue_events_import +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89134 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/365977 +milestone: '15.3' +type: ops +group: group::import default_enabled: false diff --git a/config/routes.rb b/config/routes.rb index dd3095f0a8d..859443be77c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -88,6 +88,10 @@ InitializerConnections.with_disabled_database_connections do get 'search/count' => 'search#count', as: :search_count get 'search/opensearch' => 'search#opensearch', as: :search_opensearch + Gitlab.ee do + get 'search/aggregations' => 'search#aggregations', as: :search_aggregations + end + # JSON Web Token get 'jwt/auth' => 'jwt#auth' diff --git a/db/migrate/20220723120039_add_author_id_to_vulnerability_state_transitions.rb b/db/migrate/20220723120039_add_author_id_to_vulnerability_state_transitions.rb new file mode 100644 index 00000000000..4b16ba2bb82 --- /dev/null +++ b/db/migrate/20220723120039_add_author_id_to_vulnerability_state_transitions.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddAuthorIdToVulnerabilityStateTransitions < Gitlab::Database::Migration[2.0] + def change + add_column :vulnerability_state_transitions, :author_id, :bigint + end +end diff --git a/db/post_migrate/20220726182310_add_user_fk_to_vulnerability_state_transitions.rb b/db/post_migrate/20220726182310_add_user_fk_to_vulnerability_state_transitions.rb new file mode 100644 index 00000000000..ad0bf6141b2 --- /dev/null +++ b/db/post_migrate/20220726182310_add_user_fk_to_vulnerability_state_transitions.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddUserFkToVulnerabilityStateTransitions < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + INDEX_NAME = 'index_vulnerability_state_transitions_on_author_id' + + def up + add_concurrent_index :vulnerability_state_transitions, :author_id, name: INDEX_NAME + add_concurrent_foreign_key :vulnerability_state_transitions, :users, column: :author_id, on_delete: :nullify + end + + def down + with_lock_retries do + remove_foreign_key :vulnerability_state_transitions, column: :author_id + end + + remove_concurrent_index_by_name :vulnerability_state_transitions, INDEX_NAME + end +end diff --git a/db/schema_migrations/20220723120039 b/db/schema_migrations/20220723120039 new file mode 100644 index 00000000000..54be61091e3 --- /dev/null +++ b/db/schema_migrations/20220723120039 @@ -0,0 +1 @@ +1a6a488243a8fa564f07301028477d64ca290b4ec636cfaab4816dab8bf3dd3f
\ No newline at end of file diff --git a/db/schema_migrations/20220726182310 b/db/schema_migrations/20220726182310 new file mode 100644 index 00000000000..fd5d282f525 --- /dev/null +++ b/db/schema_migrations/20220726182310 @@ -0,0 +1 @@ +318684106f2976e285b6aaa1a73363badeec083a180a68973ba9d51dd89886c0
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index d99366fd015..5b6e41a5315 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -22547,7 +22547,8 @@ CREATE TABLE vulnerability_state_transitions ( to_state smallint NOT NULL, from_state smallint NOT NULL, created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL + updated_at timestamp with time zone NOT NULL, + author_id bigint ); CREATE SEQUENCE vulnerability_state_transitions_id_seq @@ -30326,6 +30327,8 @@ CREATE UNIQUE INDEX index_vulnerability_scanners_on_project_id_and_external_id O CREATE INDEX index_vulnerability_state_transitions_id_and_vulnerability_id ON vulnerability_state_transitions USING btree (vulnerability_id, id); +CREATE INDEX index_vulnerability_state_transitions_on_author_id ON vulnerability_state_transitions USING btree (author_id); + CREATE INDEX index_vulnerability_statistics_on_latest_pipeline_id ON vulnerability_statistics USING btree (latest_pipeline_id); CREATE INDEX index_vulnerability_statistics_on_letter_grade ON vulnerability_statistics USING btree (letter_grade); @@ -32654,6 +32657,9 @@ ALTER TABLE ONLY gitlab_subscriptions ALTER TABLE ONLY merge_requests ADD CONSTRAINT fk_e719a85f8a FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE ONLY vulnerability_state_transitions + ADD CONSTRAINT fk_e719dc63df FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL; + ALTER TABLE ONLY issue_links ADD CONSTRAINT fk_e71bb44f1f FOREIGN KEY (target_id) REFERENCES issues(id) ON DELETE CASCADE; diff --git a/doc/administration/audit_event_streaming.md b/doc/administration/audit_event_streaming.md index e3928e31962..d51fc0b2097 100644 --- a/doc/administration/audit_event_streaming.md +++ b/doc/administration/audit_event_streaming.md @@ -6,39 +6,53 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Audit event streaming **(ULTIMATE)** -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/332747) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `ff_external_audit_events_namespace`. Disabled by default. -> - [Enabled on GitLab.com and by default on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/338939) in GitLab 14.7. -> - [Feature flag `ff_external_audit_events_namespace`](https://gitlab.com/gitlab-org/gitlab/-/issues/349588) removed in GitLab 14.8. +> - API [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/332747) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `ff_external_audit_events_namespace`. Disabled by default. +> - API [Enabled on GitLab.com and by default on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/338939) in GitLab 14.7. +> - API [Feature flag `ff_external_audit_events_namespace`](https://gitlab.com/gitlab-org/gitlab/-/issues/349588) removed in GitLab 14.8. +> - UI [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336411) in GitLab 14.9. > - [Subgroup events recording](https://gitlab.com/gitlab-org/gitlab/-/issues/366878) fixed in GitLab 15.2. +> - Custom HTTP headers API [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/361216) in GitLab 15.1 [with a flag](feature_flags.md) named `streaming_audit_event_headers`. Disabled by default. +> - Custom HTTP headers API [enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/362941) in GitLab 15.2. +> - Custom HTTP headers API [made generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/366524) in GitLab 15.3. [Feature flag `streaming_audit_event_headers`](https://gitlab.com/gitlab-org/gitlab/-/issues/362941) removed. +> - Custom HTTP headers UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/361630) in GitLab 15.2 [with a flag](feature_flags.md) named `custom_headers_streaming_audit_events_ui`. Disabled by default. +> - Custom HTTP headers UI [made generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/365259) in GitLab 15.3. [Feature flag `custom_headers_streaming_audit_events_ui`](https://gitlab.com/gitlab-org/gitlab/-/issues/365259) removed. -Users can set an HTTP endpoint for a top-level group to receive all audit events about the group, its subgroups, and +Users can set a streaming destination for a top-level group to receive all audit events about the group, its subgroups, and projects as structured JSON. Top-level group owners can manage their audit logs in third-party systems. Any service that can receive -structured JSON data can be used as the endpoint. +structured JSON data can be used as the streaming destination. + +Each streaming destination can have up to 20 custom HTTP headers included with each streamed event. NOTE: GitLab can stream a single event more than once to the same destination. Use the `id` key in the payload to deduplicate incoming data. -## Add a new event streaming destination +## Add a new streaming destination WARNING: -Event streaming destinations receive **all** audit event data, which could include sensitive information. Make sure you trust the destination endpoint. +Streaming destinations receive **all** audit event data, which could include sensitive information. Make sure you trust the streaming destination. ### Use the GitLab UI -Users with at least the Owner role for a group can add event streaming destinations for it: +Users with the Owner role for a group can add streaming destinations for it: 1. On the top bar, select **Menu > Groups** and find your group. 1. On the left sidebar, select **Security & Compliance > Audit events**. 1. On the main area, select **Streams** tab. - When the destination list is empty, select **Add stream** to show the section for adding destinations. - - When the destination list is not empty, select **{plus}** to show the section for adding destinations. -1. Enter the destination URL to add and select **Add**. + - When the destination list is not empty, select **Add stream** (**{plus}**) to show the section for adding destinations. +1. Enter the destination URL to add. +1. Optional. Locate the **Custom HTTP headers** table. +1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the **Active** checkbox, see the + [relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/361925). +1. Enter as many name and value pairs as required. When you enter a unique name and a value for a header, a new row in the table automatically appears. You can add up to + 20 headers per streaming destination. +1. After all headers have been filled out, select **Add** to add the new streaming destination. ### Use the API -To enable event streaming and add a destination, users with at least the Owner role for a group must use the +To enable streaming and add a destination, users with the Owner role for a group must use the `externalAuditEventDestinationCreate` mutation in the GraphQL API. ```graphql @@ -46,6 +60,7 @@ mutation { externalAuditEventDestinationCreate(input: { destinationUrl: "https://mydomain.io/endpoint/ingest", groupPath: "my-group" } ) { errors externalAuditEventDestination { + id destinationUrl verificationToken group { @@ -61,23 +76,35 @@ Event streaming is enabled if: - The returned `errors` object is empty. - The API responds with `200 OK`. +Group owners can add an HTTP header using the GraphQL `auditEventsStreamingHeadersCreate` mutation. You can retrieve the destination ID +by [listing all the streaming destinations](#use-the-api-1) for the group or from the mutation above. + +```graphql +mutation { + auditEventsStreamingHeadersCreate(input: { destinationId: "gid://gitlab/AuditEvents::ExternalAuditEventDestination/24601", key: "foo", value: "bar" }) { + errors + } +} +``` + +The header is created if the returned `errors` object is empty. + ## List streaming destinations -Users with at least the Owner role for a group can list event streaming destinations. +Users with the Owner role for a group can list streaming destinations. ### Use the GitLab UI -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336411) in GitLab 14.9. - -Users with at least the Owner role for a group can list event streaming destinations: +To list the streaming destinations: 1. On the top bar, select **Menu > Groups** and find your group. 1. On the left sidebar, select **Security & Compliance > Audit events**. 1. On the main area, select **Streams** tab. +1. To the right of the item, select **Edit** (**{pencil}**) to see all the custom HTTP headers. ### Use the API -Users with at least the Owner role for a group can view a list of event streaming destinations at any time using the +Users with the Owner role for a group can view a list of streaming destinations at any time using the `externalAuditEventDestinations` query type. ```graphql @@ -89,37 +116,48 @@ query { destinationUrl verificationToken id + headers { + nodes { + key + value + id + } + } } } } } ``` -If the resulting list is empty, then audit event streaming is not enabled for that group. +If the resulting list is empty, then audit streaming is not enabled for that group. -## Delete streaming destinations +You need the ID values returned by this query for the update and delete mutations. -Users with at least the Owner role for a group can delete event streaming destinations using the -`deleteAuditEventDestinations` mutation type. +## Update streaming destinations -When the last destination is successfully deleted, event streaming is disabled for the group. +Users with the Owner role for a group can update streaming destinations. ### Use the GitLab UI -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336411) in GitLab 14.9. - -Users with at least the Owner role for a group can delete event streaming destinations: +To update a streaming destinations custom HTTP headers: 1. On the top bar, select **Menu > Groups** and find your group. 1. On the left sidebar, select **Security & Compliance > Audit events**. 1. On the main area, select **Streams** tab. -1. Select **{remove}** at the right side of each item. +1. To the right of the item, select **Edit** (**{pencil}**). +1. Locate the **Custom HTTP headers** table. +1. Locate the header that you wish to update. +1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the **Active** checkbox, see the + [relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/361925). +1. Enter as many name and value pairs as required. When you enter a unique name and a value for a header, a new row in the table automatically appears. You can add up to + 20 headers per streaming destination. +1. Select **Save** to update the streaming destination. ### Use the API -Delete an event streaming destination by specifying an ID. Get the required ID by -[listing the details](audit_event_streaming.md#use-the-api-1) of event -streaming destinations. +Users with the Owner role for a group can update streaming destinations custom HTTP headers using the +`auditEventsStreamingHeadersUpdate` mutation type. You can retrieve the custom HTTP headers ID +by [listing all the custom HTTP headers](#use-the-api-1) for the group. ```graphql mutation { @@ -129,112 +167,71 @@ mutation { } ``` -Destination is deleted if: +Streaming destination is updated if: - The returned `errors` object is empty. - The API responds with `200 OK`. -## Custom HTTP headers - -> - API [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/361216) in GitLab 15.1 [with a flag](feature_flags.md) named `streaming_audit_event_headers`. Disabled by default. -> - API [enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/362941) in GitLab 15.2. -> - UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/361630) in GitLab 15.2 [with a flag](feature_flags.md) named `custom_headers_streaming_audit_events_ui`. Disabled by default. -> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/366524) in GitLab 15.3. - -Each streaming destination can have up to 20 custom HTTP headers included with each streamed event. - -### Adding custom HTTP headers - -Add custom HTTP headers with the API or GitLab UI. - -#### Use the API - -Group owners can add a HTTP header using the GraphQL `auditEventsStreamingHeadersCreate` mutation. You can retrieve the destination ID -by [listing the external audit destinations](#list-streaming-destinations) on the group. +Group owners can remove an HTTP header using the GraphQL `auditEventsStreamingHeadersDestroy` mutation. You can retrieve the header ID +by [listing all the custom HTTP headers](#use-the-api-1) for the group. ```graphql mutation { - auditEventsStreamingHeadersCreate(input: { destinationId: "gid://gitlab/AuditEvents::ExternalAuditEventDestination/24601", key: "foo", value: "bar" }) { + auditEventsStreamingHeadersDestroy(input: { headerId: "gid://gitlab/AuditEvents::Streaming::Header/1" }) { errors } } ``` -The header is created if the returned `errors` object is empty. +The header is deleted if the returned `errors` object is empty. -#### Use the GitLab UI +## Delete streaming destinations -FLAG: -On self-managed GitLab, by default the UI for this feature is not available. To make it available per group, ask an administrator to -[enable the feature flag](../administration/feature_flags.md) named `custom_headers_streaming_audit_events_ui`. On GitLab.com, the UI for this feature is -not available. The UI for this feature is not ready for production use. +Users with the Owner role for a group can delete streaming destinations. + +When the last destination is successfully deleted, streaming is disabled for the group. -Users with at least the Owner role for a group can add event streaming destinations and custom HTTP headers for it: +### Use the GitLab UI + +To delete a streaming destination: 1. On the top bar, select **Menu > Groups** and find your group. 1. On the left sidebar, select **Security & Compliance > Audit events**. -1. On the main area, select **Streams** tab. - - When the destination list is empty, select **Add stream** to show the section for adding destinations. - - When the destination list is not empty, select **{plus}** to show the section for adding destinations. -1. Enter the destination URL to add. -1. Locate the **Custom HTTP headers** table. -1. In the **Header** column, add the header's name. -1. In the **Value** column, add the header's value. -1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the **Active** checkbox, see the - [relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/361925). -1. Enter as many name and value pairs as required. When you enter a unique name and a value for a header, a new row in the table automatically appears. You can add up to - 20 headers per endpoint. -1. After all headers have been filled out, select **Add** to add the new endpoint. - -### Updating custom HTTP headers +1. On the main area, select the **Streams** tab. +1. To the right of the item, select **Delete** (**{remove}**). -Add custom HTTP headers with the API or GitLab UI. +To delete only the custom HTTP headers for a streaming destination: -#### Use the API +1. On the top bar, select **Menu > Groups** and find your group. +1. On the left sidebar, select **Security & Compliance > Audit events**. +1. On the main area, select the **Streams** tab. +1. To the right of the item, **Edit** (**{pencil}**). +1. Locate the **Custom HTTP headers** table. +1. Locate the header that you wish to remove. +1. To the right of the header, select **Delete** (**{remove}**). +1. Select **Save** to update the streaming destination. -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/361964) in GitLab 15.2. +### Use the API -Group owners can update a HTTP header using the GraphQL `auditEventsStreamingHeadersCreate` mutation. +Users with the Owner role for a group can delete streaming destinations using the +`externalAuditEventDestinationDestroy` mutation type. You can retrieve the destinations ID +by [listing all the streaming destinations](#use-the-api-1) for the group. ```graphql mutation { - auditEventsStreamingHeadersUpdate(input: { headerId: "gid://gitlab/AuditEvents::Streaming::Header/24255", key: "new-foo", value: "new-bar" }) { + externalAuditEventDestinationDestroy(input: { id: destination }) { errors } } ``` -#### Use the GitLab UI - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91986) in GitLab 15.3 [with a flag](feature_flags.md) named `custom_headers_streaming_audit_events_ui`. Disabled by default. - -FLAG: -On self-managed GitLab, by default the UI for this feature is not available. To make it available per group, ask an administrator to -[enable the feature flag](../administration/feature_flags.md) named `custom_headers_streaming_audit_events_ui`. On GitLab.com, the UI for this feature is -not available. The UI for this feature is not ready for production use. - -Users with at least the Owner role for a group can add event streaming destinations and custom HTTP headers for it: - -1. On the top bar, select **Menu > Groups** and find your group. -1. On the left sidebar, select **Security & Compliance > Audit events**. -1. On the main area, select **Streams** tab. -1. Select **{pencil}** at the right side of an item. -1. Locate the **Custom HTTP headers** table. -1. Locate the header that you wish to update. -1. In the **Header** column, you can change the header's name. -1. In the **Value** column, you can change the header's value. -1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the **Active** checkbox, see the - [relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/361925). -1. Select **Save** to update the endpoint. - -### Deleting custom HTTP headers - -Deleting custom HTTP headers with the API or GitLab UI. +Streaming destination is deleted if: -#### Use the API +- The returned `errors` object is empty. +- The API responds with `200 OK`. -Group owners can remove a HTTP header using the GraphQL `auditEventsStreamingHeadersDestroy` mutation. You can retrieve the header ID -by [listing all the custom headers](#list-all-custom-headers) on the group. +Group owners can remove an HTTP header using the GraphQL `auditEventsStreamingHeadersDestroy` mutation. You can retrieve the header ID +by [listing all the custom HTTP headers](#use-the-api-1) for the group. ```graphql mutation { @@ -246,70 +243,6 @@ mutation { The header is deleted if the returned `errors` object is empty. -#### Use the GitLab UI - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91986) in GitLab 15.3 [with a flag](feature_flags.md) named `custom_headers_streaming_audit_events_ui`. Disabled by default. - -FLAG: -On self-managed GitLab, by default the UI for this feature is not available. To make it available per group, ask an administrator to -[enable the feature flag](../administration/feature_flags.md) named `custom_headers_streaming_audit_events_ui`. On GitLab.com, the UI for this feature is -not available. The UI for this feature is not ready for production use. - -Users with at least the Owner role for a group can delete event streaming destinations: - -1. On the top bar, select **Menu > Groups** and find your group. -1. On the left sidebar, select **Security & Compliance > Audit events**. -1. On the main area, select **Streams** tab. -1. Select **{pencil}** at the right side of an item. -1. Locate the **Custom HTTP headers** table. -1. Locate the header that you wish to remove. -1. Select **{remove}** at the right side of the header. -1. Select **Save** to update the endpoint. - -### List all custom headers - -List all custom HTTP headers with the API or GitLab UI. - -#### Use the API - -You can list all custom headers for a top-level group as well as their value and ID using the GraphQL `externalAuditEventDestinations` query. The ID -value returned by this query is what you need to pass to the `deletion` mutation. - -```graphql -query { - group(fullPath: "your-group") { - id - externalAuditEventDestinations { - nodes { - destinationUrl - id - headers { - nodes { - key - value - id - } - } - } - } - } -} -``` - -#### Use the GitLab UI - -FLAG: -On self-managed GitLab, by default the UI for this feature is not available. To make it available per group, ask an administrator to -[enable the feature flag](../administration/feature_flags.md) named `custom_headers_streaming_audit_events_ui`. On GitLab.com, the UI for this feature is -not available. The UI for this feature is not ready for production use. - -Users with at least the Owner role for a group can add event streaming destinations and custom HTTP headers for it: - -1. On the top bar, select **Menu > Groups** and find your group. -1. On the left sidebar, select **Security & Compliance > Audit events**. -1. On the main area, select **Streams** tab. -1. Select **{pencil}** at the right side of an item. - ## Verify event authenticity > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345424) in GitLab 14.8. @@ -324,11 +257,11 @@ the destination's value when [listing streaming destinations](#list-streaming-de > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/360814) in GitLab 15.2. -Users with at least the Owner role for a group can list event streaming destinations and see the verification tokens: +Users with the Owner role for a group can list streaming destinations and see the verification tokens: 1. On the top bar, select **Menu > Groups** and find your group. 1. On the left sidebar, select **Security & Compliance > Audit events**. -1. On the main area, select **Streams**. +1. On the main area, select the **Streams**. 1. View the verification token on the right side of each item. ## Audit event streaming on Git operations @@ -350,7 +283,7 @@ Streaming audit events can be sent when signed-in users push or pull a project's Audit events are not captured for users that are not signed in. For example, when downloading a public project. -To configure streaming audit events for Git operations, see [Add a new event streaming destination](#add-a-new-event-streaming-destination). +To configure streaming audit events for Git operations, see [Add a new streaming destination](#add-a-new-streaming-destination). ### Headers @@ -432,7 +365,7 @@ Push: } ``` -#### Example payloads for SSH events with Deploy Key +### Example payloads for SSH events with Deploy Key > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363876) in GitLab 15.3. @@ -534,7 +467,7 @@ Push: } ``` -#### Example payloads for HTTP and HTTPS events with Deploy Token +### Example payloads for HTTP and HTTPS events with Deploy Token Fetch: diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index 3282bb5195d..0aa766aa0d4 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -272,7 +272,7 @@ Don't see the event you want in any of the epics linked above? You can either: - Use the **Audit Event Proposal** issue template to [create an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Audit%20Event%20Proposal) to request it. -- [Add it yourself](../development/audit_event_guide/). +- [Add it yourself](../development/audit_event_guide/index.md). ### Removed events diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 16851e5e2b5..a2def8a9f64 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -390,8 +390,8 @@ Some basic Ruby runtime metrics are available: ## Redis metrics These client metrics are meant to complement Redis server metrics. -These metrics are broken down per [Redis -instance](https://docs.gitlab.com/omnibus/settings/redis.html#running-with-multiple-redis-instances). +These metrics are broken down per +[Redis instance](https://docs.gitlab.com/omnibus/settings/redis.html#running-with-multiple-redis-instances). These metrics all have a `storage` label which indicates the Redis instance (`cache`, `shared_state`, and so on). diff --git a/doc/development/distributed_tracing.md b/doc/development/distributed_tracing.md index 116071cdfd9..f49d024095d 100644 --- a/doc/development/distributed_tracing.md +++ b/doc/development/distributed_tracing.md @@ -73,14 +73,14 @@ In this example, we have the following hypothetical values: - `driver`: the driver such a Jaeger. - `param_name`, `param_value`: these are driver specific configuration values. Configuration - parameters for Jaeger are documented [further on in this - document](#2-configure-the-gitlab_tracing-environment-variable) they should be URL encoded. + parameters for Jaeger are documented [further on in this document](#2-configure-the-gitlab_tracing-environment-variable) + they should be URL encoded. Multiple values should be separated by `&` characters like a URL. ## Using Jaeger in the GitLab Development Kit -The first tracing implementation that GitLab supports is Jaeger, and the [GitLab Development -Kit](https://gitlab.com/gitlab-org/gitlab-development-kit/) supports distributed tracing with +The first tracing implementation that GitLab supports is Jaeger, and the +[GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit/) supports distributed tracing with Jaeger out-of-the-box. The easiest way to access tracing from a GDK environment is through the @@ -116,8 +116,8 @@ Jaeger has many configuration options, but is very easy to start in an "all-in-o memory for trace storage (and is therefore non-persistent). The main advantage of "all-in-one" mode being ease of use. -For more detailed configuration options, refer to the [Jaeger -documentation](https://www.jaegertracing.io/docs/1.9/getting-started/). +For more detailed configuration options, refer to the +[Jaeger documentation](https://www.jaegertracing.io/docs/1.9/getting-started/). #### Using Docker @@ -201,8 +201,8 @@ If `GITLAB_TRACING` is not configured correctly, this issue is logged: ``` By default, GitLab ships with the Jaeger tracer, but other tracers can be included at compile time. -Details of how this can be done are included in the [LabKit tracing -documentation](https://pkg.go.dev/gitlab.com/gitlab-org/labkit/tracing). +Details of how this can be done are included in the +[LabKit tracing documentation](https://pkg.go.dev/gitlab.com/gitlab-org/labkit/tracing). If no log messages about tracing are emitted, the `GITLAB_TRACING` environment variable is likely not set. diff --git a/doc/development/logging.md b/doc/development/logging.md index 7f013edee79..f1fa7f4c8c9 100644 --- a/doc/development/logging.md +++ b/doc/development/logging.md @@ -44,8 +44,7 @@ These logs suffer from a number of problems: Note that currently on GitLab.com, any messages in `production.log` aren't indexed by Elasticsearch due to the sheer volume and noise. They do end up in Google Stackdriver, but it is still harder to search for -logs there. See the [GitLab.com logging -documentation](https://gitlab.com/gitlab-com/runbooks/-/tree/master/docs/logging) +logs there. See the [GitLab.com logging documentation](https://gitlab.com/gitlab-com/runbooks/-/tree/master/docs/logging) for more details. ## Use structured (JSON) logging @@ -386,18 +385,18 @@ end ## Additional steps with new log files 1. Consider log retention settings. By default, Omnibus rotates any - logs in `/var/log/gitlab/gitlab-rails/*.log` every hour and [keep at - most 30 compressed files](https://docs.gitlab.com/omnibus/settings/logs.html#logrotate). + logs in `/var/log/gitlab/gitlab-rails/*.log` every hour and + [keep at most 30 compressed files](https://docs.gitlab.com/omnibus/settings/logs.html#logrotate). On GitLab.com, that setting is only 6 compressed files. These settings should suffice for most users, but you may need to tweak them in [Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab). -1. If you add a new file, submit an issue to the [production - tracker](https://gitlab.com/gitlab-com/gl-infra/production/-/issues) or +1. If you add a new file, submit an issue to the + [production tracker](https://gitlab.com/gitlab-com/gl-infra/production/-/issues) or a merge request to the [`gitlab_fluentd`](https://gitlab.com/gitlab-cookbooks/gitlab_fluentd) project. See [this example](https://gitlab.com/gitlab-cookbooks/gitlab_fluentd/-/merge_requests/51/diffs). -1. Be sure to update the [GitLab CE/EE documentation](../administration/logs/index.md) and the [GitLab.com - runbooks](https://gitlab.com/gitlab-com/runbooks/blob/master/docs/logging/README.md). +1. Be sure to update the [GitLab CE/EE documentation](../administration/logs/index.md) and the + [GitLab.com runbooks](https://gitlab.com/gitlab-com/runbooks/blob/master/docs/logging/README.md). ## Control logging visibility diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md index b85cab1cf47..6044049b260 100644 --- a/doc/user/usage_quotas.md +++ b/doc/user/usage_quotas.md @@ -39,16 +39,19 @@ To prevent exceeding the namespace storage quota, you can: Starting October 19, 2022, a storage limit will be enforced on all GitLab Free namespaces. We will start with a large limit enforcement and eventually reduce it to 5 GB. +Impacted users are notified via email and in-app notifications will begin 2022-08-22. +Only GitLab SaaS users are impacted - the limits are not applicable to self-managed users. + The following table describes the enforcement schedule, which is subject to change. -| Target enforcement date | Limit | Expected Impact | Status | -| ------ | ------ | ------ | ------ | -| October 19, 2022 | 45,000 GB | LOW | Pending (**{hourglass}**)| -| October 20, 2022 | 7,500 GB | LOW | Pending (**{hourglass}**)| -| October 24, 2022 | 500 GB | MEDIUM | Pending (**{hourglass}**)| -| October 27, 2022 | 75 GB | MEDIUM HIGH | Pending (**{hourglass}**)| -| November 2, 2022 | 10 GB | HIGH | Pending (**{hourglass}**)| -| November 9, 2022 | 5 GB | VERY HIGH | Pending (**{hourglass}**)| +| Target enforcement date | Limit | Status | +| ----------------------- | ----- | ------ | +| October 19, 2022 | 45,000 GB | Pending (**{hourglass}**)| +| October 20, 2022 | 7,500 GB | Pending (**{hourglass}**)| +| October 24, 2022 | 500 GB | Pending (**{hourglass}**)| +| October 27, 2022 | 75 GB | Pending (**{hourglass}**)| +| November 2, 2022 | 10 GB | Pending (**{hourglass}**)| +| November 9, 2022 | 5 GB | Pending (**{hourglass}**)| Namespaces that reach the enforced limit will have their projects locked. To unlock your project, you will have to [manage its storage](#manage-your-storage-usage). diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml index 074f2f0f78e..a4baca12f59 100644 --- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.30.0' + DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.33.0' TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com' .dast-auto-deploy: diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml index 2478b14348a..d4e1539ae39 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - AUTO_DEPLOY_IMAGE_VERSION: 'v2.30.0' + AUTO_DEPLOY_IMAGE_VERSION: 'v2.33.0' TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com' .auto-deploy: diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml index 37ba6c5cb47..591ef49d146 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - AUTO_DEPLOY_IMAGE_VERSION: 'v2.30.0' + AUTO_DEPLOY_IMAGE_VERSION: 'v2.33.0' TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com' .auto-deploy: diff --git a/lib/gitlab/github_import/importer/events/base_importer.rb b/lib/gitlab/github_import/importer/events/base_importer.rb index d9e16a7c91e..9ab1d916d33 100644 --- a/lib/gitlab/github_import/importer/events/base_importer.rb +++ b/lib/gitlab/github_import/importer/events/base_importer.rb @@ -7,10 +7,10 @@ module Gitlab # Base class for importing issue events during project import from GitHub class BaseImporter # project - An instance of `Project`. - # user_finder - An instance of `Gitlab::GithubImport::UserFinder`. - def initialize(project, user_finder) + # client - An instance of `Gitlab::GithubImport::Client`. + def initialize(project, client) @project = project - @user_finder = user_finder + @user_finder = UserFinder.new(project, client) end # issue_event - An instance of `Gitlab::GithubImport::Representation::IssueEvent`. @@ -25,6 +25,10 @@ module Gitlab def author_id(issue_event, author_key: :actor) user_finder.author_id_for(issue_event, author_key: author_key).first end + + def issuable_db_id(object) + IssuableFinder.new(project, object).database_id + end end end end diff --git a/lib/gitlab/github_import/importer/events/changed_assignee.rb b/lib/gitlab/github_import/importer/events/changed_assignee.rb index 6930ea0ef8d..c8f6335e4a8 100644 --- a/lib/gitlab/github_import/importer/events/changed_assignee.rb +++ b/lib/gitlab/github_import/importer/events/changed_assignee.rb @@ -20,7 +20,7 @@ module Gitlab Note.create!( system: true, noteable_type: Issue.name, - noteable_id: issue_event.issue_db_id, + noteable_id: issuable_db_id(issue_event), project: project, author_id: assigner_id, note: note_body, diff --git a/lib/gitlab/github_import/importer/events/changed_label.rb b/lib/gitlab/github_import/importer/events/changed_label.rb index c3350251b2d..818a9202745 100644 --- a/lib/gitlab/github_import/importer/events/changed_label.rb +++ b/lib/gitlab/github_import/importer/events/changed_label.rb @@ -13,7 +13,7 @@ module Gitlab def create_event(issue_event) ResourceLabelEvent.create!( - issue_id: issue_event.issue_db_id, + issue_id: issuable_db_id(issue_event), user_id: author_id(issue_event), label_id: label_finder.id_for(issue_event.label_title), action: action(issue_event.event), diff --git a/lib/gitlab/github_import/importer/events/changed_milestone.rb b/lib/gitlab/github_import/importer/events/changed_milestone.rb index a3e4d2efb73..3164c041dc3 100644 --- a/lib/gitlab/github_import/importer/events/changed_milestone.rb +++ b/lib/gitlab/github_import/importer/events/changed_milestone.rb @@ -18,7 +18,7 @@ module Gitlab def create_event(issue_event) ResourceMilestoneEvent.create!( - issue_id: issue_event.issue_db_id, + issue_id: issuable_db_id(issue_event), user_id: author_id(issue_event), created_at: issue_event.created_at, milestone_id: project.milestones.find_by_title(issue_event.milestone_title)&.id, diff --git a/lib/gitlab/github_import/importer/events/closed.rb b/lib/gitlab/github_import/importer/events/closed.rb index 1db326328f6..ca8730d0f27 100644 --- a/lib/gitlab/github_import/importer/events/closed.rb +++ b/lib/gitlab/github_import/importer/events/closed.rb @@ -18,7 +18,7 @@ module Gitlab author_id: author_id(issue_event), action: 'closed', target_type: Issue.name, - target_id: issue_event.issue_db_id, + target_id: issuable_db_id(issue_event), created_at: issue_event.created_at, updated_at: issue_event.created_at ) @@ -27,7 +27,7 @@ module Gitlab def create_state_event(issue_event) ResourceStateEvent.create!( user_id: author_id(issue_event), - issue_id: issue_event.issue_db_id, + issue_id: issuable_db_id(issue_event), source_commit: issue_event.commit_id, state: 'closed', close_after_error_tracking_resolve: false, diff --git a/lib/gitlab/github_import/importer/events/cross_referenced.rb b/lib/gitlab/github_import/importer/events/cross_referenced.rb index 9a3a2536b7c..89fc1bdeb09 100644 --- a/lib/gitlab/github_import/importer/events/cross_referenced.rb +++ b/lib/gitlab/github_import/importer/events/cross_referenced.rb @@ -34,7 +34,7 @@ module Gitlab Note.create!( system: true, noteable_type: Issue.name, - noteable_id: issue_event.issue_db_id, + noteable_id: issuable_db_id(issue_event), project: project, author_id: user_id, note: note_body, @@ -66,7 +66,7 @@ module Gitlab iid: number, issuable_type: record_class.name ) - Gitlab::GithubImport::IssuableFinder.new(project, mentioned_in_adapter).database_id + issuable_db_id(mentioned_in_adapter) end def cross_reference_note_content(gfm_reference) diff --git a/lib/gitlab/github_import/importer/events/renamed.rb b/lib/gitlab/github_import/importer/events/renamed.rb index 44f05eb4af8..96d112b04c6 100644 --- a/lib/gitlab/github_import/importer/events/renamed.rb +++ b/lib/gitlab/github_import/importer/events/renamed.rb @@ -13,7 +13,7 @@ module Gitlab def note_params(issue_event) { - noteable_id: issue_event.issue_db_id, + noteable_id: issuable_db_id(issue_event), noteable_type: Issue.name, project_id: project.id, author_id: author_id(issue_event), diff --git a/lib/gitlab/github_import/importer/events/reopened.rb b/lib/gitlab/github_import/importer/events/reopened.rb index 923fee51394..b75344bf817 100644 --- a/lib/gitlab/github_import/importer/events/reopened.rb +++ b/lib/gitlab/github_import/importer/events/reopened.rb @@ -18,7 +18,7 @@ module Gitlab author_id: author_id(issue_event), action: 'reopened', target_type: Issue.name, - target_id: issue_event.issue_db_id, + target_id: issuable_db_id(issue_event), created_at: issue_event.created_at, updated_at: issue_event.created_at ) @@ -27,7 +27,7 @@ module Gitlab def create_state_event(issue_event) ResourceStateEvent.create!( user_id: author_id(issue_event), - issue_id: issue_event.issue_db_id, + issue_id: issuable_db_id(issue_event), state: 'reopened', created_at: issue_event.created_at ) diff --git a/lib/gitlab/github_import/importer/issue_event_importer.rb b/lib/gitlab/github_import/importer/issue_event_importer.rb index fb52c18853c..ef456e56ee1 100644 --- a/lib/gitlab/github_import/importer/issue_event_importer.rb +++ b/lib/gitlab/github_import/importer/issue_event_importer.rb @@ -4,7 +4,7 @@ module Gitlab module GithubImport module Importer class IssueEventImporter - attr_reader :issue_event, :project, :client, :user_finder + attr_reader :issue_event, :project, :client # issue_event - An instance of `Gitlab::GithubImport::Representation::IssueEvent`. # project - An instance of `Project`. @@ -13,29 +13,16 @@ module Gitlab @issue_event = issue_event @project = project @client = client - @user_finder = UserFinder.new(project, client) end + # TODO: Add MergeRequest events support + # https://gitlab.com/groups/gitlab-org/-/epics/7673 def execute - event_importer = case issue_event.event - when 'closed' - Gitlab::GithubImport::Importer::Events::Closed - when 'reopened' - Gitlab::GithubImport::Importer::Events::Reopened - when 'labeled', 'unlabeled' - Gitlab::GithubImport::Importer::Events::ChangedLabel - when 'renamed' - Gitlab::GithubImport::Importer::Events::Renamed - when 'milestoned', 'demilestoned' - Gitlab::GithubImport::Importer::Events::ChangedMilestone - when 'cross-referenced' - Gitlab::GithubImport::Importer::Events::CrossReferenced - when 'assigned', 'unassigned' - Gitlab::GithubImport::Importer::Events::ChangedAssignee - end + return if issue_event.issuable_type == 'MergeRequest' - if event_importer - event_importer.new(project, user_finder).execute(issue_event) + importer = event_importer_class(issue_event) + if importer + importer.new(project, client).execute(issue_event) else Gitlab::GithubImport::Logger.debug( message: 'UNSUPPORTED_EVENT_TYPE', @@ -43,6 +30,27 @@ module Gitlab ) end end + + private + + def event_importer_class(issue_event) + case issue_event.event + when 'closed' + Gitlab::GithubImport::Importer::Events::Closed + when 'reopened' + Gitlab::GithubImport::Importer::Events::Reopened + when 'labeled', 'unlabeled' + Gitlab::GithubImport::Importer::Events::ChangedLabel + when 'renamed' + Gitlab::GithubImport::Importer::Events::Renamed + when 'milestoned', 'demilestoned' + Gitlab::GithubImport::Importer::Events::ChangedMilestone + when 'cross-referenced' + Gitlab::GithubImport::Importer::Events::CrossReferenced + when 'assigned', 'unassigned' + Gitlab::GithubImport::Importer::Events::ChangedAssignee + end + end end end end diff --git a/lib/gitlab/github_import/importer/issue_events_importer.rb b/lib/gitlab/github_import/importer/issue_events_importer.rb new file mode 100644 index 00000000000..71dd99f91f9 --- /dev/null +++ b/lib/gitlab/github_import/importer/issue_events_importer.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + module Importer + class IssueEventsImporter + include ParallelScheduling + + def importer_class + IssueEventImporter + end + + def representation_class + Representation::IssueEvent + end + + def sidekiq_worker_class + ImportIssueEventWorker + end + + def object_type + :issue_event + end + + def collection_method + :repository_issue_events + end + + def id_for_already_imported_cache(event) + event.id + end + end + end + end +end diff --git a/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer.rb b/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer.rb index 45bbc25e637..8e4015acbbc 100644 --- a/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer.rb +++ b/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer.rb @@ -18,13 +18,16 @@ module Gitlab { project: project.id, collection: collection_method } end + # In single endpoint there is no issue info to which associated related + # To make it possible to identify issue in separated worker we need to patch + # Sawyer instances here with issue number def each_associated(parent_record, associated) compose_associated_id!(parent_record, associated) return if already_imported?(associated) Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :fetched) - associated.issue_db_id = parent_record.id + associated.issue = { 'number' => parent_record.iid } yield(associated) mark_as_imported(associated) diff --git a/lib/gitlab/github_import/issuable_finder.rb b/lib/gitlab/github_import/issuable_finder.rb index da205ebd345..e7a1b7b3368 100644 --- a/lib/gitlab/github_import/issuable_finder.rb +++ b/lib/gitlab/github_import/issuable_finder.rb @@ -69,6 +69,8 @@ module Gitlab object.noteable_id elsif object.respond_to?(:iid) object.iid + elsif object.respond_to?(:issuable_id) + object.issuable_id else raise( TypeError, diff --git a/lib/gitlab/github_import/representation/issue_event.rb b/lib/gitlab/github_import/representation/issue_event.rb index 3561628bdde..b01887f50ee 100644 --- a/lib/gitlab/github_import/representation/issue_event.rb +++ b/lib/gitlab/github_import/representation/issue_event.rb @@ -10,8 +10,7 @@ module Gitlab attr_reader :attributes expose_attribute :id, :actor, :event, :commit_id, :label_title, :old_title, :new_title, - :milestone_title, :source, :assignee, :assigner, :created_at - expose_attribute :issue_db_id # set in SingleEndpointIssueEventsImporter#each_associated + :milestone_title, :issue, :source, :assignee, :assigner, :created_at # attributes - A Hash containing the event details. The keys of this # Hash (and any nested hashes) must be symbols. @@ -23,6 +22,14 @@ module Gitlab { id: id } end + def issuable_type + issue && issue[:pull_request].present? ? 'MergeRequest' : 'Issue' + end + + def issuable_id + issue && issue[:number] + end + class << self # Builds an event from a GitHub API response. # @@ -37,10 +44,10 @@ module Gitlab old_title: event.rename && event.rename[:from], new_title: event.rename && event.rename[:to], milestone_title: event.milestone && event.milestone[:title], + issue: event.issue&.to_h&.symbolize_keys, source: event.source, assignee: user_representation(event.assignee), assigner: user_representation(event.assigner), - issue_db_id: event.issue_db_id, created_at: event.created_at ) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ca4949b0166..d5f1ec3bd2f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5629,7 +5629,7 @@ msgstr "" msgid "Automatically resolved" msgstr "" -msgid "Automatically update this project's branches and tags from the upstream repository every hour." +msgid "Automatically update this project's branches and tags from the upstream repository." msgstr "" msgid "Autosave|Note" @@ -19493,6 +19493,9 @@ msgstr "" msgid "How do I use file templates?" msgstr "" +msgid "How does pull mirroring work?" +msgstr "" + msgid "How many seconds an IP counts toward the IP address limit." msgstr "" @@ -34913,6 +34916,9 @@ msgstr "" msgid "SecurityOrchestration|Failed to load cluster agents." msgstr "" +msgid "SecurityOrchestration|Failed to load vulnerability scanners." +msgstr "" + msgid "SecurityOrchestration|If any scanner finds a newly detected critical vulnerability in an open merge request targeting the master branch, then require two approvals from any member of App security." msgstr "" diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 501c43d6bd0..53104a51398 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -13,7 +13,8 @@ module QA :github_personal_access_token, :github_repository_path, :gitlab_repository_path, - :personal_namespace + :personal_namespace, + :description attr_reader :repository_storage @@ -21,7 +22,6 @@ module QA :name, :path, :add_name_uuid, - :description, :runners_token, :visibility, :template_name, @@ -108,7 +108,7 @@ module QA end new_page.choose_name(@name) - new_page.add_description(@description) + new_page.add_description(@description) if @description new_page.set_visibility(@visibility) new_page.disable_initialize_with_sast new_page.disable_initialize_with_readme unless @initialize_with_readme diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb index 0063ce2613a..2d4ec6d5f72 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb @@ -10,7 +10,6 @@ module QA expect(project_page).to have_content( /Project \S?#{project_name}\S+ was successfully created/ ) - expect(project_page).to have_content('create awesome project test') expect(project_page).to have_content('The repository for this project is empty') end end @@ -26,7 +25,7 @@ module QA let(:project) do Resource::Project.fabricate_via_browser_ui! do |project| project.name = project_name - project.description = 'create awesome project test' + project.description = nil end end @@ -38,8 +37,8 @@ module QA let(:project) do Resource::Project.fabricate_via_browser_ui! do |project| project.name = project_name - project.description = 'create awesome project test' project.personal_namespace = Runtime::User.username + project.description = nil end end diff --git a/qa/qa/support/parallel_pipeline_jobs.rb b/qa/qa/support/parallel_pipeline_jobs.rb new file mode 100644 index 00000000000..c37958267de --- /dev/null +++ b/qa/qa/support/parallel_pipeline_jobs.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module QA + module Support + # Helper utility to fetch parallel job names in a given pipelines stage + # + class ParallelPipelineJobs + include Support::API + + PARALLEL_JOB_NAME_PATTERN = %r{^\S+ \d+/\d+$}.freeze + + def initialize(stage_name:, project_id:, pipeline_id:, access_token:) + @stage_name = stage_name + @access_token = access_token + @project_id = project_id || raise("project_id must be provided") + @pipeline_id = pipeline_id || raise("pipeline_id must be provided") + end + + # Fetch parallel job names in given stage + # + # Default to arguments available on CI + # + # @param [String] stage_name + # @param [Integer] project_id + # @param [Integer] pipeline_id + # @param [String] access_token + # @return [Array] + def self.fetch( + stage_name:, + access_token:, + project_id: ENV["CI_PROJECT_ID"], + pipeline_id: ENV["CI_PIPELINE_ID"] + ) + new( + stage_name: stage_name, + project_id: project_id, + pipeline_id: pipeline_id, + access_token: access_token + ).parallel_jobs + end + + # Parallel job list + # + # @return [Array<String>] + def parallel_jobs + api_get("projects/#{project_id}/pipelines/#{pipeline_id}/jobs?per_page=100") + .select { |job| job[:stage] == stage_name && job[:name].match?(PARALLEL_JOB_NAME_PATTERN) } + .map { |job| job[:name].gsub(%r{ \d+/\d+}, "") } + .uniq + end + + private + + attr_reader :stage_name, :access_token, :project_id, :pipeline_id + + # Api get request + # + # @param [String] path + # @param [Hash] payload + # @return [Hash, Array] + def api_get(path) + response = get("#{api_url}/#{path}", { headers: { "PRIVATE-TOKEN" => access_token } }) + parse_body(response) + end + + # Gitlab api url + # + # @return [String] + def api_url + @api_url ||= ENV['CI_API_V4_URL'] || "https://gitlab.com/api/v4" + end + end + end +end diff --git a/qa/tasks/knapsack.rake b/qa/tasks/knapsack.rake index fe9a9c4586f..c1225964aef 100644 --- a/qa/tasks/knapsack.rake +++ b/qa/tasks/knapsack.rake @@ -1,6 +1,5 @@ # frozen_string_literal: true -# rubocop:disable Rails/RakeEnvironment namespace :knapsack do desc "Run tests with knapsack runner" task :rspec, [:rspec_args] do |_, args| @@ -16,11 +15,26 @@ namespace :knapsack do exit QA::Specs::KnapsackRunner.run(rspec_args) end - desc "Download latest knapsack report or multiple reports passed via QA_KNAPSACK_REPORTS env variable" - task :download do - next QA::Support::KnapsackReport.download_report unless ENV["QA_KNAPSACK_REPORTS"] + desc "Download latest knapsack reports for parallel jobs" + task :download, [:stage_name] do |_, args| + test_stage_name = args[:stage_name] - ENV["QA_KNAPSACK_REPORTS"].split(",").each do |report_name| + # QA_KNAPSACK_REPORTS remains for changes to be backwards compatible + # TODO: remove and only use automated detection once changes are merged + unless ENV["QA_KNAPSACK_REPORTS"] || test_stage_name + QA::Runtime::Logger.warn("Missing QA_KNAPSACK_REPORTS environment variable or test stage name for autodetection") + next + end + + reports = if test_stage_name + QA::Support::ParallelPipelineJobs + .fetch(stage_name: test_stage_name, access_token: ENV["QA_GITLAB_CI_TOKEN"]) + .map { |job| job.tr(":", "-") } + else + ENV["QA_KNAPSACK_REPORTS"].split(",") + end + + reports.each do |report_name| QA::Support::KnapsackReport.new(report_name).download_report rescue StandardError => e QA::Runtime::Logger.error(e) @@ -37,4 +51,3 @@ namespace :knapsack do QA::Tools::LongRunningSpecReporter.execute end end -# rubocop:enable Rails/RakeEnvironment diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index e03a8b7d321..14b198dbefe 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -12,47 +12,6 @@ RSpec.describe SearchController do sign_in(user) end - shared_examples_for 'when the user cannot read cross project' do |action, params| - before do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?) - .with(user, :read_cross_project, :global) { false } - end - - it 'blocks access without a project_id' do - get action, params: params - - expect(response).to have_gitlab_http_status(:forbidden) - end - - it 'allows access with a project_id' do - get action, params: params.merge(project_id: create(:project, :public).id) - - expect(response).to have_gitlab_http_status(:ok) - end - end - - shared_examples_for 'with external authorization service enabled' do |action, params| - let(:project) { create(:project, namespace: user.namespace) } - let(:note) { create(:note_on_issue, project: project) } - - before do - enable_external_authorization_service_check - end - - it 'renders a 403 when no project is given' do - get action, params: params - - expect(response).to have_gitlab_http_status(:forbidden) - end - - it 'renders a 200 when a project was set' do - get action, params: params.merge(project_id: project.id) - - expect(response).to have_gitlab_http_status(:ok) - end - end - shared_examples_for 'support for active record query timeouts' do |action, params, method_to_stub, format| before do allow_next_instance_of(SearchService) do |service| @@ -133,10 +92,11 @@ RSpec.describe SearchController do { chars_under_limit: (('a' * (term_char_limit - 1) + ' ') * (term_limit - 1))[0, char_limit], chars_over_limit: (('a' * (term_char_limit - 1) + ' ') * (term_limit - 1))[0, char_limit + 1], - terms_under_limit: ('abc ' * (term_limit - 1)), + terms_under_limit: ('abc ' * (term_limit - 1)), terms_over_limit: ('abc ' * (term_limit + 1)), term_length_over_limit: ('a' * (term_char_limit + 1)), - term_length_under_limit: ('a' * (term_char_limit - 1)) + term_length_under_limit: ('a' * (term_char_limit - 1)), + blank: '' } end @@ -147,6 +107,7 @@ RSpec.describe SearchController do :terms_over_limit | :set_terms_flash :term_length_under_limit | :not_to_set_flash :term_length_over_limit | :not_to_set_flash # abuse, so do nothing. + :blank | :not_to_set_flash end with_them do @@ -417,7 +378,7 @@ RSpec.describe SearchController do expect(payload[:metadata]['meta.search.project_ids']).to eq(%w(456 789)) expect(payload[:metadata]['meta.search.type']).to eq('basic') expect(payload[:metadata]['meta.search.level']).to eq('global') - expect(payload[:metadata]['meta.search.language']).to eq('ruby') + expect(payload[:metadata]['meta.search.filters.language']).to eq('ruby') end get :show, params: { diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 9d2d1454d77..f45025d079a 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -3,16 +3,44 @@ require 'spec_helper' RSpec.describe 'New project', :js do - include Select2Helper include Spec::Support::Helpers::Features::TopNavSpecHelpers context 'as a user' do - let(:user) { create(:user) } + let_it_be(:user) { create(:user) } before do sign_in(user) end + it 'shows the project description field when it should' do + description_label = 'Project description (optional)' + + visit new_project_path + click_link 'Create blank project' + + page.within('#blank-project-pane') do + expect(page).not_to have_content(description_label) + end + + visit new_project_path + click_link 'Import project' + + page.within('#import-project-pane') do + click_button 'Repository by URL' + + expect(page).to have_content(description_label) + end + + visit new_project_path + click_link 'Create from template' + + page.within('#create-from-template-pane') do + find("[data-testid='use_template_#{Gitlab::ProjectTemplate.localized_templates_table.first.name}']").click + + expect(page).to have_content(description_label) + end + end + it 'shows a message if multiple levels are restricted' do Gitlab::CurrentSettings.update!( restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL] diff --git a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js new file mode 100644 index 00000000000..e9966576cab --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js @@ -0,0 +1,139 @@ +import { GlDropdown, GlDropdownItem, GlIcon, GlSearchBoxByType } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { allEnvironments } from '~/ci_variable_list/constants'; +import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue'; + +describe('Ci environments dropdown', () => { + let wrapper; + + const envs = ['dev', 'prod', 'staging']; + const defaultProps = { environments: envs, selectedEnvironmentScope: '' }; + + const findDropdownText = () => wrapper.findComponent(GlDropdown).text(); + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index); + const findActiveIconByIndex = (index) => findDropdownItemByIndex(index).findComponent(GlIcon); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + + const createComponent = ({ props = {}, searchTerm = '' } = {}) => { + wrapper = mount(CiEnvironmentsDropdown, { + propsData: { + ...defaultProps, + ...props, + }, + }); + + findSearchBox().vm.$emit('input', searchTerm); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('No environments found', () => { + beforeEach(() => { + createComponent({ searchTerm: 'stable' }); + }); + + it('renders create button with search term if environments do not contain search term', () => { + expect(findAllDropdownItems()).toHaveLength(2); + expect(findDropdownItemByIndex(1).text()).toBe('Create wildcard: stable'); + }); + + it('renders empty results message', () => { + expect(findDropdownItemByIndex(0).text()).toBe('No matching results'); + }); + }); + + describe('Search term is empty', () => { + beforeEach(() => { + createComponent({ props: { environments: envs } }); + }); + + it('renders all environments when search term is empty', () => { + expect(findAllDropdownItems()).toHaveLength(3); + expect(findDropdownItemByIndex(0).text()).toBe(envs[0]); + expect(findDropdownItemByIndex(1).text()).toBe(envs[1]); + expect(findDropdownItemByIndex(2).text()).toBe(envs[2]); + }); + + it('should not display active checkmark on the inactive stage', () => { + expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true); + }); + }); + + describe('when `*` is the value of selectedEnvironmentScope props', () => { + const wildcardScope = '*'; + + beforeEach(() => { + createComponent({ props: { selectedEnvironmentScope: wildcardScope } }); + }); + + it('shows the `All environments` text and not the wildcard', () => { + expect(findDropdownText()).toContain(allEnvironments.text); + expect(findDropdownText()).not.toContain(wildcardScope); + }); + }); + + describe('Environments found', () => { + const currentEnv = envs[2]; + + beforeEach(async () => { + createComponent({ searchTerm: currentEnv }); + await nextTick(); + }); + + it('renders only the environment searched for', () => { + expect(findAllDropdownItems()).toHaveLength(1); + expect(findDropdownItemByIndex(0).text()).toBe(currentEnv); + }); + + it('should not display create button', () => { + const environments = findAllDropdownItems().filter((env) => env.text().startsWith('Create')); + expect(environments).toHaveLength(0); + expect(findAllDropdownItems()).toHaveLength(1); + }); + + it('should not display empty results message', () => { + expect(wrapper.findComponent({ ref: 'noMatchingResults' }).exists()).toBe(false); + }); + + it('should clear the search term when showing the dropdown', () => { + wrapper.findComponent(GlDropdown).trigger('click'); + + expect(findSearchBox().text()).toBe(''); + }); + + describe('Custom events', () => { + describe('when clicking on an environment', () => { + const itemIndex = 0; + + beforeEach(() => { + createComponent(); + }); + + it('should emit `select-environment` if an environment is clicked', async () => { + await nextTick(); + + await findDropdownItemByIndex(itemIndex).vm.$emit('click'); + + expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]); + }); + }); + + describe('when creating a new environment from a search term', () => { + const search = 'new-env'; + beforeEach(() => { + createComponent({ searchTerm: search }); + }); + + it('should emit createClicked if an environment is clicked', async () => { + await nextTick(); + findDropdownItemByIndex(1).vm.$emit('click'); + expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]); + }); + }); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js new file mode 100644 index 00000000000..51b902d97dc --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js @@ -0,0 +1,380 @@ +import { GlButton, GlFormInput } from '@gitlab/ui'; +import { mockTracking } from 'helpers/tracking_helper'; +import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; +import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue'; +import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue'; +import { + ADD_VARIABLE_ACTION, + AWS_ACCESS_KEY_ID, + EDIT_VARIABLE_ACTION, + EVENT_LABEL, + EVENT_ACTION, + ENVIRONMENT_SCOPE_LINK_TITLE, +} from '~/ci_variable_list/constants'; +import { mockVariablesWithScopes } from '../mocks'; +import ModalStub from '../stubs'; + +describe('Ci variable modal', () => { + let wrapper; + let trackingSpy; + + const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$'; + + const defaultProvide = { + awsLogoSvgPath: '/logo', + awsTipCommandsLink: '/tips', + awsTipDeployLink: '/deploy', + awsTipLearnLink: '/learn-link', + containsVariableReferenceLink: '/reference', + environmentScopeLink: '/help/environments', + isProtectedByDefault: false, + maskedEnvironmentVariablesLink: '/variables-link', + maskableRegex, + protectedEnvironmentVariablesLink: '/protected-link', + }; + + const defaultProps = { + areScopedVariablesAvailable: true, + environments: [], + mode: ADD_VARIABLE_ACTION, + selectedVariable: {}, + }; + + const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => { + wrapper = mountFn(CiVariableModal, { + attachTo: document.body, + provide: { ...defaultProvide, ...provide }, + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + GlModal: ModalStub, + }, + }); + }; + + const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown); + const findReferenceWarning = () => wrapper.findByTestId('contains-variable-reference'); + const findModal = () => wrapper.find(ModalStub); + const findAWSTip = () => wrapper.findByTestId('aws-guidance-tip'); + const findAddorUpdateButton = () => wrapper.findByTestId('ciUpdateOrAddVariableBtn'); + const deleteVariableButton = () => + findModal() + .findAll(GlButton) + .wrappers.find((button) => button.props('variant') === 'danger'); + const findProtectedVariableCheckbox = () => + wrapper.findByTestId('ci-variable-protected-checkbox'); + const findMaskedVariableCheckbox = () => wrapper.findByTestId('ci-variable-masked-checkbox'); + const findValueField = () => wrapper.find('#ci-variable-value'); + const findEnvScopeLink = () => wrapper.findByTestId('environment-scope-link'); + const findEnvScopeInput = () => wrapper.findByTestId('environment-scope').find(GlFormInput); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Adding a variable', () => { + describe('when no key/value pair are present', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows the submit button as disabled ', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy(); + }); + }); + + describe('when a key/value pair is present', () => { + beforeEach(() => { + createComponent({ props: { selectedVariable: mockVariablesWithScopes[0] } }); + }); + + it('shows the submit button as enabled ', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); + }); + }); + + describe('events', () => { + const [currentVariable] = mockVariablesWithScopes; + + beforeEach(() => { + createComponent({ props: { selectedVariable: currentVariable } }); + jest.spyOn(wrapper.vm, '$emit'); + }); + + it('Dispatches `add-variable` action on submit', () => { + findAddorUpdateButton().vm.$emit('click'); + expect(wrapper.emitted('add-variable')).toEqual([[currentVariable]]); + }); + + it('Dispatches the `hideModal` event when dismissing', () => { + findModal().vm.$emit('hidden'); + expect(wrapper.emitted('hideModal')).toEqual([[]]); + }); + }); + }); + + describe('when protected by default', () => { + describe('when adding a new variable', () => { + beforeEach(() => { + createComponent({ provide: { isProtectedByDefault: true } }); + findModal().vm.$emit('shown'); + }); + + it('updates the protected value to true', () => { + expect( + findProtectedVariableCheckbox().attributes('data-is-protected-checked'), + ).toBeTruthy(); + }); + }); + + describe('when editing a variable', () => { + beforeEach(() => { + createComponent({ + provide: { isProtectedByDefault: false }, + props: { + selectedVariable: {}, + mode: EDIT_VARIABLE_ACTION, + }, + }); + findModal().vm.$emit('shown'); + }); + + it('keeps the value as false', async () => { + expect( + findProtectedVariableCheckbox().attributes('data-is-protected-checked'), + ).toBeUndefined(); + }); + }); + }); + + describe('Adding a new non-AWS variable', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + createComponent({ mountFn: mountExtended, props: { selectedVariable: variable } }); + }); + + it('does not show AWS guidance tip', () => { + const tip = findAWSTip(); + expect(tip.exists()).toBe(true); + expect(tip.isVisible()).toBe(false); + }); + }); + + describe('Adding a new AWS variable', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + const AWSKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: 'AKIAIOSFODNN7EXAMPLEjdhy', + }; + createComponent({ mountFn: mountExtended, props: { selectedVariable: AWSKeyVariable } }); + }); + + it('shows AWS guidance tip', () => { + const tip = findAWSTip(); + expect(tip.exists()).toBe(true); + expect(tip.isVisible()).toBe(true); + }); + }); + + describe('Reference warning when adding a variable', () => { + describe('with a $ character', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + const variableWithDollarSign = { + ...variable, + value: 'valueWith$', + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: variableWithDollarSign }, + }); + }); + + it(`renders the variable reference warning`, () => { + expect(findReferenceWarning().exists()).toBe(true); + }); + }); + + describe('without a $ character', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: variable }, + }); + }); + + it(`does not render the variable reference warning`, () => { + expect(findReferenceWarning().exists()).toBe(false); + }); + }); + }); + + describe('Editing a variable', () => { + const [variable] = mockVariablesWithScopes; + + beforeEach(() => { + createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } }); + jest.spyOn(wrapper.vm, '$emit'); + }); + + it('button text is Update variable when updating', () => { + expect(findAddorUpdateButton().text()).toBe('Update variable'); + }); + + it('Update variable button dispatches updateVariable with correct variable', () => { + findAddorUpdateButton().vm.$emit('click'); + expect(wrapper.emitted('update-variable')).toEqual([[variable]]); + }); + + it('Propagates the `hideModal` event', () => { + findModal().vm.$emit('hidden'); + expect(wrapper.emitted('hideModal')).toEqual([[]]); + }); + + it('dispatches `delete-variable` with correct variable to delete', () => { + deleteVariableButton().vm.$emit('click'); + expect(wrapper.emitted('delete-variable')).toEqual([[variable]]); + }); + }); + + describe('Environment scope', () => { + describe('when feature is available', () => { + it('renders the environment dropdown', () => { + createComponent({ + mountFn: mountExtended, + props: { + areScopedVariablesAvailable: true, + }, + }); + + expect(findCiEnvironmentsDropdown().exists()).toBe(true); + expect(findCiEnvironmentsDropdown().isVisible()).toBe(true); + }); + + it('renders a link to documentation on scopes', () => { + createComponent({ mountFn: mountExtended }); + + const link = findEnvScopeLink(); + + expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE); + expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink); + }); + }); + + describe('when feature is not available', () => { + it('disables the dropdown', () => { + createComponent({ + mountFn: mountExtended, + props: { + areScopedVariablesAvailable: false, + }, + }); + + expect(findCiEnvironmentsDropdown().exists()).toBe(false); + expect(findEnvScopeInput().attributes('readonly')).toBe('readonly'); + }); + }); + }); + + describe('Validations', () => { + const maskError = 'This variable can not be masked.'; + + describe('when the mask state is invalid', () => { + beforeEach(async () => { + const [variable] = mockVariablesWithScopes; + const invalidMaskVariable = { + ...variable, + value: 'd:;', + masked: false, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: invalidMaskVariable }, + }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + await findMaskedVariableCheckbox().trigger('click'); + }); + + it('disables the submit button', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy(); + }); + + it('shows the correct error text', () => { + expect(findModal().text()).toContain(maskError); + }); + + it('sends the correct tracking event', () => { + expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, { + label: EVENT_LABEL, + property: ';', + }); + }); + }); + + describe.each` + value | masked | eventSent | trackingErrorProperty + ${'secretValue'} | ${false} | ${0} | ${null} + ${'short'} | ${true} | ${0} | ${null} + ${'dollar$ign'} | ${false} | ${1} | ${'$'} + ${'dollar$ign'} | ${true} | ${1} | ${'$'} + ${'unsupported|char'} | ${true} | ${1} | ${'|'} + ${'unsupported|char'} | ${false} | ${0} | ${null} + `('Adding a new variable', ({ value, masked, eventSent, trackingErrorProperty }) => { + beforeEach(async () => { + const [variable] = mockVariablesWithScopes; + const invalidKeyVariable = { + ...variable, + value: '', + masked: false, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: invalidKeyVariable }, + }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + await findValueField().vm.$emit('input', value); + if (masked) { + await findMaskedVariableCheckbox().trigger('click'); + } + }); + + it(`${ + eventSent > 0 ? 'sends the correct' : 'does not send the' + } variable validation tracking event with ${value}`, () => { + expect(trackingSpy).toHaveBeenCalledTimes(eventSent); + + if (eventSent > 0) { + expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, { + label: EVENT_LABEL, + property: trackingErrorProperty, + }); + } + }); + }); + + describe('when masked variable has acceptable value', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + const validMaskandKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: '12345678', + masked: true, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: validMaskandKeyVariable }, + }); + }); + + it('does not disable the submit button', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); + }); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js new file mode 100644 index 00000000000..1ba50c74152 --- /dev/null +++ b/spec/frontend/ci_variable_list/mocks.js @@ -0,0 +1,116 @@ +import { variableTypes } from '~/ci_variable_list/constants'; + +export const devName = 'dev'; +export const prodName = 'prod'; + +export const mockVariables = [ + { + __typename: 'CiVariable', + id: 1, + key: 'my-var', + masked: false, + protected: true, + value: 'env_val', + variableType: variableTypes.variableType, + }, + { + __typename: 'CiVariable', + id: 2, + key: 'secret', + masked: true, + protected: false, + value: 'the_secret_value', + variableType: variableTypes.fileType, + }, +]; + +export const mockVariablesWithScopes = mockVariables.map((variable) => { + return { ...variable, environmentScope: '*' }; +}); + +const createDefaultVars = ({ withScope = true } = {}) => { + let base = mockVariables; + + if (withScope) { + base = mockVariablesWithScopes; + } + + return { + __typename: 'CiVariableConnection', + nodes: base, + }; +}; + +const defaultEnvs = { + __typename: 'EnvironmentConnection', + nodes: [ + { + __typename: 'Environment', + id: 1, + name: prodName, + }, + { + __typename: 'Environment', + id: 2, + name: devName, + }, + ], +}; + +export const mockEnvs = defaultEnvs.nodes; + +export const mockProjectEnvironments = { + data: { + project: { + __typename: 'Project', + id: 1, + environments: defaultEnvs, + }, + }, +}; + +export const mockProjectVariables = { + data: { + project: { + __typename: 'Project', + id: 1, + ciVariables: createDefaultVars(), + }, + }, +}; + +export const mockGroupEnvironments = { + data: { + group: { + __typename: 'Group', + id: 1, + environments: defaultEnvs, + }, + }, +}; + +export const mockGroupVariables = { + data: { + group: { + __typename: 'Group', + id: 1, + ciVariables: createDefaultVars(), + }, + }, +}; + +export const mockAdminVariables = { + data: { + ciVariables: createDefaultVars({ withScope: false }), + }, +}; + +export const newVariable = { + id: 3, + environmentScope: 'new', + key: 'AWS_RANDOM_THING', + masked: true, + protected: false, + value: 'devops', + variableType: variableTypes.variableType, +}; diff --git a/spec/frontend/ci_variable_list/utils_spec.js b/spec/frontend/ci_variable_list/utils_spec.js new file mode 100644 index 00000000000..1676e786515 --- /dev/null +++ b/spec/frontend/ci_variable_list/utils_spec.js @@ -0,0 +1,68 @@ +import { + createJoinedEnvironments, + convertEnvironmentScope, + mapEnvironmentNames, +} from '~/ci_variable_list/utils'; +import { allEnvironments } from '~/ci_variable_list/constants'; + +describe('utils', () => { + const environments = ['dev', 'prod']; + + describe('createJoinedEnvironments', () => { + it('returns only `environments` if `variables` argument is undefined', () => { + const variables = undefined; + + expect(createJoinedEnvironments(variables, environments)).toEqual(environments); + }); + + it('returns a list of environments and environment scopes taken from variables in alphabetical order', () => { + const envScope1 = 'new1'; + const envScope2 = 'new2'; + + const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }]; + + expect(createJoinedEnvironments(variables, environments)).toEqual([ + environments[0], + envScope1, + envScope2, + environments[1], + ]); + }); + + it('removes duplicate environments', () => { + const envScope1 = environments[0]; + const envScope2 = 'new2'; + + const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }]; + + expect(createJoinedEnvironments(variables, environments)).toEqual([ + environments[0], + envScope2, + environments[1], + ]); + }); + }); + + describe('convertEnvironmentScope', () => { + it('converts the * to the `All environments` text', () => { + expect(convertEnvironmentScope('*')).toBe(allEnvironments.text); + }); + + it('returns the environment as is if not the *', () => { + expect(convertEnvironmentScope('prod')).toBe('prod'); + }); + }); + + describe('mapEnvironmentNames', () => { + const envName = 'dev'; + const envName2 = 'prod'; + + const nodes = [ + { name: envName, otherProp: {} }, + { name: envName2, otherProp: {} }, + ]; + it('flatten a nodes array with only their names', () => { + expect(mapEnvironmentNames(nodes)).toEqual([envName, envName2]); + }); + }); +}); diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js index e1de2823726..ca552644258 100644 --- a/spec/frontend/content_editor/remark_markdown_processing_spec.js +++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js @@ -1170,7 +1170,7 @@ _world_. const trimmed = markdown.trim(); const document = await deserialize(trimmed); - expect(expectedDoc).not.toBeFalsy(); + expect(expectedDoc).not.toBe(false); expect(document.toJSON()).toEqual(expectedDoc.toJSON()); expect(serialize(document)).toEqual(expectedMarkdown ?? trimmed); }, diff --git a/spec/frontend/labels/labels_select_spec.js b/spec/frontend/labels/labels_select_spec.js index f6e280564cc..63f7c725bc7 100644 --- a/spec/frontend/labels/labels_select_spec.js +++ b/spec/frontend/labels/labels_select_spec.js @@ -101,6 +101,12 @@ describe('LabelsSelect', () => { expect($labelEl.find('a').attr('data-html')).toBe('true'); }); + it('generated label item template has correct title for tooltip', () => { + expect($labelEl.find('a').attr('title')).toBe( + "<span class='font-weight-bold scoped-label-tooltip-title'>Scoped label</span><br>Foobar", + ); + }); + it('generated label item template has correct label styles and classes', () => { expect($labelEl.find('span.gl-label-text').attr('style')).toBe( `background-color: ${label.color};`, diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 312e4f636c3..2c6b603197d 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -348,15 +348,13 @@ describe('URL utility', () => { describe('urlContainsSha', () => { it('returns true when there is a valid 40-character SHA1 hash in the URL', () => { shas.valid.forEach((sha) => { - expect( - urlUtils.urlContainsSha({ url: `http://urlstuff/${sha}/moreurlstuff` }), - ).toBeTruthy(); + expect(urlUtils.urlContainsSha({ url: `http://urlstuff/${sha}/moreurlstuff` })).toBe(true); }); }); it('returns false when there is not a valid 40-character SHA1 hash in the URL', () => { shas.invalid.forEach((str) => { - expect(urlUtils.urlContainsSha({ url: `http://urlstuff/${str}/moreurlstuff` })).toBeFalsy(); + expect(urlUtils.urlContainsSha({ url: `http://urlstuff/${str}/moreurlstuff` })).toBe(false); }); }); }); @@ -813,13 +811,13 @@ describe('URL utility', () => { }); it('should compare against the window location if no compare value is provided', () => { - expect(urlUtils.urlIsDifferent('different')).toBeTruthy(); - expect(urlUtils.urlIsDifferent(current)).toBeFalsy(); + expect(urlUtils.urlIsDifferent('different')).toBe(true); + expect(urlUtils.urlIsDifferent(current)).toBe(false); }); it('should use the provided compare value', () => { - expect(urlUtils.urlIsDifferent('different', current)).toBeTruthy(); - expect(urlUtils.urlIsDifferent(current, current)).toBeFalsy(); + expect(urlUtils.urlIsDifferent('different', current)).toBe(true); + expect(urlUtils.urlIsDifferent(current, current)).toBe(false); }); }); diff --git a/spec/frontend/projects/project_new_spec.js b/spec/frontend/projects/project_new_spec.js index 3034037fb1d..4fcecc3a307 100644 --- a/spec/frontend/projects/project_new_spec.js +++ b/spec/frontend/projects/project_new_spec.js @@ -1,6 +1,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { TEST_HOST } from 'helpers/test_constants'; import projectNew from '~/projects/project_new'; +import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper'; describe('New Project', () => { let $projectImportUrl; @@ -12,21 +13,27 @@ describe('New Project', () => { beforeEach(() => { setHTMLFixture(` - <div class='toggle-import-form'> - <div class='import-url-data'> - <div class="form-group"> - <input id="project_import_url" /> - </div> - <div id="import-url-auth-method"> - <div class="form-group"> - <input id="project-import-url-user" /> + <div class="tab-pane active"> + <div class='toggle-import-form'> + <form id="new_project"> + <div class='import-url-data'> + <div class="form-group"> + <input id="project_import_url" /> + </div> + <div id="import-url-auth-method"> + <div class="form-group"> + <input id="project-import-url-user" /> + </div> + <div class="form-group"> + <input id="project_import_url_password" /> + </div> + </div> + <input id="project_name" /> + <input id="project_path" /> </div> - <div class="form-group"> - <input id="project_import_url_password" /> - </div> - </div> - <input id="project_name" /> - <input id="project_path" /> + <div class="js-user-readme-repo"></div> + <button class="js-create-project-button"/> + </form> </div> </div> `); @@ -45,6 +52,38 @@ describe('New Project', () => { el.value = value; }; + describe('tracks manual path input', () => { + let trackingSpy; + + beforeEach(() => { + trackingSpy = mockTracking('_category_', undefined, jest.spyOn); + projectNew.bindEvents(); + $projectPath.oldInputValue = '_old_value_'; + }); + + afterEach(() => { + unmockTracking(); + }); + + it('tracks the event', () => { + $projectPath.value = '_new_value_'; + + triggerEvent($projectPath, 'blur'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'user_input_path_slug', { + label: 'new_project_form', + }); + }); + + it('does not track the event when there has been no change', () => { + $projectPath.value = '_old_value_'; + + triggerEvent($projectPath, 'blur'); + + expect(trackingSpy).not.toHaveBeenCalled(); + }); + }); + describe('deriveProjectPathFromUrl', () => { const dummyImportUrl = `${TEST_HOST}/dummy/import/url.git`; diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js index f0106914674..193a16bae8d 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js @@ -80,20 +80,20 @@ describe('MemoryUsage', () => { it('should have default data', () => { const data = MemoryUsage.data(); - expect(Array.isArray(data.memoryMetrics)).toBeTruthy(); + expect(Array.isArray(data.memoryMetrics)).toBe(true); expect(data.memoryMetrics.length).toBe(0); expect(typeof data.deploymentTime).toBe('number'); expect(data.deploymentTime).toBe(0); expect(typeof data.hasMetrics).toBe('boolean'); - expect(data.hasMetrics).toBeFalsy(); + expect(data.hasMetrics).toBe(false); expect(typeof data.loadFailed).toBe('boolean'); - expect(data.loadFailed).toBeFalsy(); + expect(data.loadFailed).toBe(false); expect(typeof data.loadingMetrics).toBe('boolean'); - expect(data.loadingMetrics).toBeTruthy(); + expect(data.loadingMetrics).toBe(true); expect(typeof data.backOffRequestCounter).toBe('number'); expect(data.backOffRequestCounter).toBe(0); @@ -144,7 +144,7 @@ describe('MemoryUsage', () => { vm.computeGraphData(metrics, deployment_time); const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm; - expect(hasMetrics).toBeTruthy(); + expect(hasMetrics).toBe(true); expect(memoryMetrics.length).toBeGreaterThan(0); expect(deploymentTime).toEqual(deployment_time); expect(memoryFrom).toEqual('9.13'); @@ -171,7 +171,7 @@ describe('MemoryUsage', () => { describe('template', () => { it('should render template elements correctly', () => { - expect(el.classList.contains('mr-memory-usage')).toBeTruthy(); + expect(el.classList.contains('mr-memory-usage')).toBe(true); expect(el.querySelector('.js-usage-info')).toBeDefined(); }); diff --git a/spec/lib/gitlab/github_import/importer/events/base_importer_spec.rb b/spec/lib/gitlab/github_import/importer/events/base_importer_spec.rb index f113ffcd0a7..41fe5fbdbbd 100644 --- a/spec/lib/gitlab/github_import/importer/events/base_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/base_importer_spec.rb @@ -4,10 +4,10 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::BaseImporter do let(:project) { instance_double('Project') } - let(:user_finder) { instance_double('Gitlab::GithubImport::UserFinder') } + let(:client) { instance_double('Gitlab::GithubImport::Client') } let(:issue_event) { instance_double('Gitlab::GithubImport::Representation::IssueEvent') } let(:importer_class) { Class.new(described_class) } - let(:importer_instance) { importer_class.new(project, user_finder) } + let(:importer_instance) { importer_class.new(project, client) } describe '#execute' do it { expect { importer_instance.execute(issue_event) }.to raise_error(NotImplementedError) } diff --git a/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb index a1918dd0da8..2f6f727dc38 100644 --- a/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb @@ -3,14 +3,13 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } let_it_be(:assignee) { create(:user) } let_it_be(:assigner) { create(:user) } let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } let(:issue) { create(:issue, project: project) } let(:issue_event) do @@ -22,7 +21,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do 'created_at' => '2022-04-26 18:30:53 UTC', 'assigner' => { 'id' => assigner.id, 'login' => assigner.username }, 'assignee' => { 'id' => assignee.id, 'login' => assignee.username }, - 'issue_db_id' => issue.id + 'issue' => { 'number' => issue.iid } ) end @@ -70,8 +69,13 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do describe '#execute' do before do - allow(user_finder).to receive(:find).with(assignee.id, assignee.username).and_return(assignee.id) - allow(user_finder).to receive(:find).with(assigner.id, assigner.username).and_return(assigner.id) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(assignee.id, assignee.username).and_return(assignee.id) + allow(finder).to receive(:find).with(assigner.id, assigner.username).and_return(assigner.id) + end end context 'when importing an assigned event' do diff --git a/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb index 98a8daf1653..e21672aa430 100644 --- a/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb @@ -3,13 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } let(:issue) { create(:issue, project: project) } let!(:label) { create(:label, project: project) } @@ -21,7 +20,8 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do 'commit_id' => nil, 'label_title' => label.title, 'issue_db_id' => issue.id, - 'created_at' => '2022-04-26 18:30:53 UTC' + 'created_at' => '2022-04-26 18:30:53 UTC', + 'issue' => { 'number' => issue.iid } ) end @@ -45,7 +45,12 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do before do allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(label.id) - allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end context 'when importing a labeled event' do diff --git a/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb index a5852c967df..2687627fc23 100644 --- a/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb @@ -3,13 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } let(:issue) { create(:issue, project: project) } let!(:milestone) { create(:milestone, project: project) } @@ -21,7 +20,8 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do 'commit_id' => nil, 'milestone_title' => milestone.title, 'issue_db_id' => issue.id, - 'created_at' => '2022-04-26 18:30:53 UTC' + 'created_at' => '2022-04-26 18:30:53 UTC', + 'issue' => { 'number' => issue.iid } ) end @@ -47,7 +47,12 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do describe '#execute' do before do allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(milestone.id) - allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end context 'when importing a milestoned event' do diff --git a/spec/lib/gitlab/github_import/importer/events/closed_spec.rb b/spec/lib/gitlab/github_import/importer/events/closed_spec.rb index 749c52a215e..9a49d80a8bb 100644 --- a/spec/lib/gitlab/github_import/importer/events/closed_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/closed_spec.rb @@ -3,13 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } let(:issue) { create(:issue, project: project) } let(:commit_id) { nil } @@ -22,7 +21,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do 'event' => 'closed', 'created_at' => '2022-04-26 18:30:53 UTC', 'commit_id' => commit_id, - 'issue_db_id' => issue.id + 'issue' => { 'number' => issue.iid } ) end @@ -48,7 +47,12 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do end before do - allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end it 'creates expected event and state event' do diff --git a/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb b/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb index fd2e564d1b1..68e001c7364 100644 --- a/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb @@ -3,14 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_gitlab_redis_cache do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } - let(:sawyer_stub) { Struct.new(:iid, :issuable_type, keyword_init: true) } let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } let(:issue_iid) { 999 } let(:issue) { create(:issue, project: project, iid: issue_iid) } @@ -32,7 +30,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g } }, 'created_at' => '2022-04-26 18:30:53 UTC', - 'issue_db_id' => issue.id + 'issue' => { 'number' => issue.iid } ) end @@ -41,7 +39,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g { system: true, noteable_type: Issue.name, - noteable_id: issue_event.issue_db_id, + noteable_id: issue.id, project_id: project.id, author_id: user.id, note: expected_note_body, @@ -53,10 +51,13 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g let(:expected_note_body) { "mentioned in issue ##{referenced_in.iid}" } before do - other_issue_resource = sawyer_stub.new(iid: referenced_in.iid, issuable_type: 'Issue') - Gitlab::GithubImport::IssuableFinder.new(project, other_issue_resource) - .cache_database_id(referenced_in.iid) - allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(referenced_in.iid) + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end it 'creates expected note' do @@ -75,11 +76,13 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g let(:expected_note_body) { "mentioned in merge request !#{referenced_in.iid}" } before do - other_issue_resource = - sawyer_stub.new(iid: referenced_in.iid, issuable_type: 'MergeRequest') - Gitlab::GithubImport::IssuableFinder.new(project, other_issue_resource) - .cache_database_id(referenced_in.iid) - allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(referenced_in.iid) + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end it 'creates expected note' do diff --git a/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb index 8acf82af40c..316ea798965 100644 --- a/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } @@ -11,7 +11,6 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do let(:issue) { create(:issue, project: project) } let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } let(:issue_event) do Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( 'id' => 6501124486, @@ -21,7 +20,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do 'created_at' => '2022-04-26 18:30:53 UTC', 'old_title' => 'old title', 'new_title' => 'new title', - 'issue_db_id' => issue.id + 'issue' => { 'number' => issue.iid } ) end @@ -48,7 +47,12 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do describe '#execute' do before do - allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end it 'creates expected note' do diff --git a/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb b/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb index 39b8809dfa4..2461dbb9701 100644 --- a/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb @@ -3,13 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_failures do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } let(:issue) { create(:issue, project: project) } let(:issue_event) do @@ -20,7 +19,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => 'reopened', 'created_at' => '2022-04-26 18:30:53 UTC', - 'issue_db_id' => issue.id + 'issue' => { 'number' => issue.iid } ) end @@ -45,7 +44,12 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail end before do - allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end it 'creates expected event and state event' do diff --git a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb index fee7c2708a4..33d5fbf13a0 100644 --- a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab specific_importer = double(importer_class.name) # rubocop:disable RSpec/VerifiedDoubles expect(importer_class) - .to receive(:new).with(project, anything) + .to receive(:new).with(project, client) .and_return(specific_importer) expect(specific_importer).to receive(:execute).with(issue_event) diff --git a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb new file mode 100644 index 00000000000..8d4c1b01e50 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::IssueEventsImporter do + subject(:importer) { described_class.new(project, client, parallel: parallel) } + + let(:project) { instance_double(Project, id: 4, import_source: 'foo/bar') } + let(:client) { instance_double(Gitlab::GithubImport::Client) } + + let(:parallel) { true } + let(:issue_event) do + struct = Struct.new( + :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone, + :source, :assignee, :assigner, :issue, :created_at, :performed_via_github_app, + keyword_init: true + ) + struct.new(id: rand(10), event: 'closed', created_at: '2022-04-26 18:30:53 UTC') + end + + describe '#parallel?' do + context 'when running in parallel mode' do + it { expect(importer).to be_parallel } + end + + context 'when running in sequential mode' do + let(:parallel) { false } + + it { expect(importer).not_to be_parallel } + end + end + + describe '#execute' do + context 'when running in parallel mode' do + it 'imports events in parallel' do + expect(importer).to receive(:parallel_import) + + importer.execute + end + end + + context 'when running in sequential mode' do + let(:parallel) { false } + + it 'imports notes in sequence' do + expect(importer).to receive(:sequential_import) + + importer.execute + end + end + end + + describe '#sequential_import' do + let(:parallel) { false } + + it 'imports each event in sequence' do + event_importer = instance_double(Gitlab::GithubImport::Importer::IssueEventImporter) + + allow(importer).to receive(:each_object_to_import).and_yield(issue_event) + + expect(Gitlab::GithubImport::Importer::IssueEventImporter) + .to receive(:new) + .with( + an_instance_of(Gitlab::GithubImport::Representation::IssueEvent), + project, + client + ) + .and_return(event_importer) + + expect(event_importer).to receive(:execute) + + importer.sequential_import + end + end + + describe '#parallel_import' do + it 'imports each note in parallel' do + allow(importer).to receive(:each_object_to_import).and_yield(issue_event) + + expect(Gitlab::GithubImport::ImportIssueEventWorker).to receive(:bulk_perform_in).with( + 1.second, [ + [project.id, an_instance_of(Hash), an_instance_of(String)] + ], batch_size: 1000, batch_delay: 1.minute + ) + + waiter = importer.parallel_import + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(1) + end + end + + describe '#importer_class' do + it { expect(importer.importer_class).to eq Gitlab::GithubImport::Importer::IssueEventImporter } + end + + describe '#representation_class' do + it { expect(importer.representation_class).to eq Gitlab::GithubImport::Representation::IssueEvent } + end + + describe '#sidekiq_worker_class' do + it { expect(importer.sidekiq_worker_class).to eq Gitlab::GithubImport::ImportIssueEventWorker } + end + + describe '#object_type' do + it { expect(importer.object_type).to eq :issue_event } + end + + describe '#collection_method' do + it { expect(importer.collection_method).to eq :repository_issue_events } + end + + describe '#id_for_already_imported_cache' do + it 'returns the ID of the given note' do + expect(importer.id_for_already_imported_cache(issue_event)).to eq(issue_event.id) + end + end + + describe '#collection_options' do + it { expect(importer.collection_options).to eq({}) } + end +end diff --git a/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb index 087faeffe02..bb1ee79ad93 100644 --- a/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb @@ -53,7 +53,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter describe '#each_object_to_import', :clean_gitlab_redis_cache do let(:issue_event) do - struct = Struct.new(:id, :event, :created_at, :issue_db_id, keyword_init: true) + struct = Struct.new(:id, :event, :created_at, :issue, keyword_init: true) struct.new(id: rand(10), event: 'closed', created_at: '2022-04-26 18:30:53 UTC') end @@ -81,7 +81,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter counter = 0 subject.each_object_to_import do |object| expect(object).to eq issue_event - expect(issue_event.issue_db_id).to eq issue.id + expect(issue_event.issue['number']).to eq issue.iid counter += 1 end expect(counter).to eq 1 diff --git a/spec/lib/gitlab/github_import/issuable_finder_spec.rb b/spec/lib/gitlab/github_import/issuable_finder_spec.rb index 3afd006109b..d550f15e8c5 100644 --- a/spec/lib/gitlab/github_import/issuable_finder_spec.rb +++ b/spec/lib/gitlab/github_import/issuable_finder_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::IssuableFinder, :clean_gitlab_redis_cache do let(:project) { double(:project, id: 4, group: nil) } let(:issue) do - double(:issue, issuable_type: MergeRequest, iid: 1) + double(:issue, issuable_type: MergeRequest, issuable_id: 1) end let(:finder) { described_class.new(project, issue) } diff --git a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb index cf796b55b14..d3a98035e73 100644 --- a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb +++ b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb @@ -25,8 +25,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do expect(issue_event.source).to eq({ type: 'issue', id: 123456 }) end - it 'includes the issue_db_id' do - expect(issue_event.issue_db_id).to eq(100500) + it 'includes the issue data' do + expect(issue_event.issue).to eq({ number: 2, pull_request: pull_request }) end context 'when actor data present' do @@ -119,6 +119,24 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do end end + describe '#issuable_id' do + it 'returns issuable_id' do + expect(issue_event.issuable_id).to eq(2) + end + end + + describe '#issuable_type' do + context 'when event related to issue' do + it { expect(issue_event.issuable_type).to eq('Issue') } + end + + context 'when event related to pull request' do + let(:pull_request) { { url: FFaker::Internet.http_url } } + + it { expect(issue_event.issuable_type).to eq('MergeRequest') } + end + end + describe '#github_identifiers' do it 'returns a hash with needed identifiers' do expect(issue_event.github_identifiers).to eq({ id: 6501124486 }) @@ -130,7 +148,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:response) do event_resource = Struct.new( :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone, - :source, :assignee, :assigner, :issue_db_id, :created_at, :performed_via_github_app, + :source, :assignee, :assigner, :issue, :created_at, :performed_via_github_app, keyword_init: true ) user_resource = Struct.new(:id, :login, keyword_init: true) @@ -149,7 +167,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do source: { type: 'issue', id: 123456 }, assignee: with_assignee ? user_resource.new(id: 5, login: 'tom') : nil, assigner: with_assignee ? user_resource.new(id: 6, login: 'jerry') : nil, - issue_db_id: 100500, + issue: { 'number' => 2, 'pull_request' => pull_request }, created_at: '2022-04-26 18:30:53 UTC', performed_via_github_app: nil ) @@ -160,6 +178,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_rename) { true } let(:with_milestone) { true } let(:with_assignee) { true } + let(:pull_request) { nil } it_behaves_like 'an IssueEvent' do let(:issue_event) { described_class.from_api_response(response) } @@ -185,7 +204,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do 'source' => { 'type' => 'issue', 'id' => 123456 }, 'assignee' => (with_assignee ? { 'id' => 5, 'login' => 'tom' } : nil), 'assigner' => (with_assignee ? { 'id' => 6, 'login' => 'jerry' } : nil), - "issue_db_id" => 100500, + 'issue' => { 'number' => 2, 'pull_request' => pull_request }, 'created_at' => '2022-04-26 18:30:53 UTC', 'performed_via_github_app' => nil } @@ -196,6 +215,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_rename) { true } let(:with_milestone) { true } let(:with_assignee) { true } + let(:pull_request) { nil } let(:issue_event) { described_class.from_json_hash(hash) } end diff --git a/spec/support/shared_examples/controllers/search_cross_project_authorization_shared_examples.rb b/spec/support/shared_examples/controllers/search_cross_project_authorization_shared_examples.rb new file mode 100644 index 00000000000..9421561aea4 --- /dev/null +++ b/spec/support/shared_examples/controllers/search_cross_project_authorization_shared_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'when the user cannot read cross project' do |action, params| + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(false) + end + + it 'blocks access without a project_id' do + get action, params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'allows access with a project_id' do + get action, params: params.merge(project_id: create(:project, :public).id) + + expect(response).to have_gitlab_http_status(:ok) + end +end diff --git a/spec/support/shared_examples/controllers/search_external_authorization_service_shared_examples.rb b/spec/support/shared_examples/controllers/search_external_authorization_service_shared_examples.rb new file mode 100644 index 00000000000..6b72988b3e6 --- /dev/null +++ b/spec/support/shared_examples/controllers/search_external_authorization_service_shared_examples.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'with external authorization service enabled' do |action, params| + include ExternalAuthorizationServiceHelpers + + let(:project) { create(:project, namespace: user.namespace) } + let(:note) { create(:note_on_issue, project: project) } + + before do + enable_external_authorization_service_check + end + + it 'renders a 403 when no project is given' do + get action, params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'renders a 200 when a project was set' do + get action, params: params.merge(project_id: project.id) + + expect(response).to have_gitlab_http_status(:ok) + end +end diff --git a/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb index b3c6a48767c..932152c0764 100644 --- a/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb +++ b/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb @@ -8,37 +8,66 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportIssueEventsWorker do let(:project) { create(:project) } let!(:group) { create(:group, projects: [project]) } let(:feature_flag_state) { [group] } + let(:single_endpoint_feature_flag_state) { [group] } describe '#import' do let(:importer) { instance_double('Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter') } let(:client) { instance_double('Gitlab::GithubImport::Client') } before do + stub_feature_flags(github_importer_single_endpoint_issue_events_import: single_endpoint_feature_flag_state) stub_feature_flags(github_importer_issue_events_import: feature_flag_state) end - it 'imports all the issue events' do - waiter = Gitlab::JobWaiter.new(2, '123') + context 'when single endpoint feature flag enabled' do + it 'imports all the issue events' do + waiter = Gitlab::JobWaiter.new(2, '123') - expect(Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter) - .to receive(:new) - .with(project, client) - .and_return(importer) + expect(Gitlab::GithubImport::Importer::IssueEventsImporter).not_to receive(:new) + expect(Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter) + .to receive(:new) + .with(project, client) + .and_return(importer) - expect(importer).to receive(:execute).and_return(waiter) + expect(importer).to receive(:execute).and_return(waiter) - expect(Gitlab::GithubImport::AdvanceStageWorker) - .to receive(:perform_async) - .with(project.id, { '123' => 2 }, :notes) + expect(Gitlab::GithubImport::AdvanceStageWorker) + .to receive(:perform_async) + .with(project.id, { '123' => 2 }, :notes) - worker.import(client, project) + worker.import(client, project) + end + end + + context 'when import issue events feature flag enabled' do + let(:single_endpoint_feature_flag_state) { false } + + it 'imports the issue events partly' do + waiter = Gitlab::JobWaiter.new(2, '123') + + expect(Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter).not_to receive(:new) + expect(Gitlab::GithubImport::Importer::IssueEventsImporter) + .to receive(:new) + .with(project, client) + .and_return(importer) + + expect(importer).to receive(:execute).and_return(waiter) + + expect(Gitlab::GithubImport::AdvanceStageWorker) + .to receive(:perform_async) + .with(project.id, { '123' => 2 }, :notes) + + worker.import(client, project) + end end - context 'when feature flag is disabled' do + context 'when feature flags are disabled' do let(:feature_flag_state) { false } + let(:single_endpoint_feature_flag_state) { false } it 'skips issue events import and calls next stage' do expect(Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter).not_to receive(:new) + expect(Gitlab::GithubImport::Importer::IssueEventsImporter).not_to receive(:new) expect(Gitlab::GithubImport::AdvanceStageWorker).to receive(:perform_async).with(project.id, {}, :notes) worker.import(client, project) |