Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
33c421eb90
commit
403df26fe3
|
|
@ -629,7 +629,7 @@ lib/gitlab/checks/**
|
|||
/doc/administration/operations/gitlab_sshd.md @brendan777
|
||||
/doc/administration/operations/moving_repositories.md @eread
|
||||
/doc/administration/package_information/ @axil
|
||||
/doc/administration/packages/ @marcel.amirault
|
||||
/doc/administration/packages/ @lyspin
|
||||
/doc/administration/packages/index.md @phillipwells
|
||||
/doc/administration/pages/ @msedlakjakubowski
|
||||
/doc/administration/polling.md @axil
|
||||
|
|
@ -712,11 +712,11 @@ lib/gitlab/checks/**
|
|||
/doc/api/cluster_agents.md @phillipwells
|
||||
/doc/api/code_suggestions.md @jglassman1
|
||||
/doc/api/commits.md @brendan777
|
||||
/doc/api/container_registry.md @marcel.amirault
|
||||
/doc/api/container_registry.md @lyspin
|
||||
/doc/api/custom_attributes.md @msedlakjakubowski
|
||||
/doc/api/dependencies.md @rdickenson @phillipwells
|
||||
/doc/api/dependency_list_export.md @rdickenson
|
||||
/doc/api/dependency_proxy.md @marcel.amirault
|
||||
/doc/api/dependency_proxy.md @lyspin
|
||||
/doc/api/deploy_keys.md @phillipwells
|
||||
/doc/api/deploy_tokens.md @phillipwells
|
||||
/doc/api/deployments.md @phillipwells
|
||||
|
|
@ -822,7 +822,7 @@ lib/gitlab/checks/**
|
|||
/doc/api/project_aliases.md @brendan777
|
||||
/doc/api/project_badges.md @brendan777
|
||||
/doc/api/project_clusters.md @phillipwells
|
||||
/doc/api/project_container_registry_protection_rules.md @marcel.amirault
|
||||
/doc/api/project_container_registry_protection_rules.md @lyspin
|
||||
/doc/api/project_forks.md @emily.sahlani
|
||||
/doc/api/project_import_export.md @ashrafkhamis
|
||||
/doc/api/project_job_token_scopes.md @marcel.amirault
|
||||
|
|
@ -903,9 +903,8 @@ lib/gitlab/checks/**
|
|||
/doc/ci/examples/ @lyspin
|
||||
/doc/ci/examples/deployment/ @phillipwells
|
||||
/doc/ci/examples/semantic-release.md @phillipwells
|
||||
/doc/ci/gitlab_google_cloud_integration/ @marcel.amirault
|
||||
/doc/ci/gitlab_google_cloud_integration/ @lyspin
|
||||
/doc/ci/index.md @lyspin
|
||||
/doc/ci/interactive_web_terminal/ @rsarangadharan
|
||||
/doc/ci/jobs/ @marcel.amirault
|
||||
/doc/ci/jobs/job_artifacts.md @lyspin
|
||||
/doc/ci/jobs/job_artifacts_troubleshooting.md @lyspin
|
||||
|
|
@ -1054,6 +1053,7 @@ lib/gitlab/checks/**
|
|||
/doc/user/application_security/secret_detection/ @rdickenson
|
||||
/doc/user/application_security/security_dashboard/ @rdickenson
|
||||
/doc/user/application_security/vulnerabilities/ @rdickenson
|
||||
/doc/user/application_security/vulnerabilities/risk_assessment_data.md @rdickenson @phillipwells
|
||||
/doc/user/application_security/vulnerability_report/ @rdickenson
|
||||
/doc/user/asciidoc.md @brendan777
|
||||
/doc/user/clusters/ @phillipwells
|
||||
|
|
@ -1104,9 +1104,9 @@ lib/gitlab/checks/**
|
|||
/doc/user/operations_dashboard/ @phillipwells
|
||||
/doc/user/organization/ @emily.sahlani
|
||||
/doc/user/packages/ @phillipwells
|
||||
/doc/user/packages/container_registry/ @marcel.amirault
|
||||
/doc/user/packages/dependency_proxy/ @marcel.amirault
|
||||
/doc/user/packages/harbor_container_registry/ @marcel.amirault
|
||||
/doc/user/packages/container_registry/ @lyspin
|
||||
/doc/user/packages/dependency_proxy/ @lyspin
|
||||
/doc/user/packages/harbor_container_registry/ @lyspin
|
||||
/doc/user/profile/account/ @jglassman1
|
||||
/doc/user/profile/achievements.md @emily.sahlani
|
||||
/doc/user/profile/comment_templates.md @aqualls
|
||||
|
|
@ -1137,8 +1137,8 @@ lib/gitlab/checks/**
|
|||
/doc/user/project/integrations/beyond_identity.md @brendan777
|
||||
/doc/user/project/integrations/git_guardian.md @brendan777
|
||||
/doc/user/project/integrations/github.md @lyspin
|
||||
/doc/user/project/integrations/google_artifact_management.md @marcel.amirault
|
||||
/doc/user/project/integrations/harbor.md @marcel.amirault
|
||||
/doc/user/project/integrations/google_artifact_management.md @lyspin
|
||||
/doc/user/project/integrations/harbor.md @lyspin
|
||||
/doc/user/project/integrations/matrix.md @sselhorn
|
||||
/doc/user/project/issue_board.md @msedlakjakubowski
|
||||
/doc/user/project/issues/ @msedlakjakubowski
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
start-release-environments-security-pipeline:
|
||||
allow_failure: false
|
||||
allow_failure: true
|
||||
extends:
|
||||
- .release-environments:rules:start-release-environments-security-pipeline
|
||||
stage: release-environments
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -687,7 +687,7 @@ gem 'lockbox', '~> 1.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
|
|||
gem 'valid_email', '~> 0.1' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
|
||||
# JSON
|
||||
gem 'jsonb_accessor', '~> 1.3.10' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'jsonb_accessor', '~> 1.4' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'json', '~> 2.7.2', feature_category: :shared
|
||||
gem 'json_schemer', '~> 2.3.0', feature_category: :shared
|
||||
gem 'oj', '~> 3.13.21' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
|
|
|
|||
|
|
@ -348,8 +348,8 @@
|
|||
{"name":"json","version":"2.7.3","platform":"ruby","checksum":"7cc3ccbfc5f83025f5524776ae8581e911772944169666988406679251932bdf"},
|
||||
{"name":"json-jwt","version":"1.16.6","platform":"ruby","checksum":"ab451f9cd8743cecc4137f4170806046c1d8a6d4ee6e8570e0b5c958409b266c"},
|
||||
{"name":"json_schemer","version":"2.3.0","platform":"ruby","checksum":"9f1fa173b859ca520f15e9e8d08b0892ffca80b78dd8221feb3e360ff4cdeb35"},
|
||||
{"name":"jsonb_accessor","version":"1.3.10","platform":"java","checksum":"6630ac69dac46457b03e1352178ed3e2d7ba2d8edb99f2e9b64a0e60cda9ed26"},
|
||||
{"name":"jsonb_accessor","version":"1.3.10","platform":"ruby","checksum":"670f80a257ae39e3be9233c6a8ef3b03517e06687affe510dfe61237454c58e0"},
|
||||
{"name":"jsonb_accessor","version":"1.4","platform":"java","checksum":"2c5590d33d89c7b929d5cf38ae3d2c52658bf6f84f03b06ede5c88e9d76f3451"},
|
||||
{"name":"jsonb_accessor","version":"1.4","platform":"ruby","checksum":"010e087cb843e76b6a624d68af918a8efd6b0ff2ae25b6f0acebdfea45f776ab"},
|
||||
{"name":"jsonpath","version":"1.1.2","platform":"ruby","checksum":"6804124c244d04418218acb85b15c7caa79c592d7d6970195300428458946d3a"},
|
||||
{"name":"jwt","version":"2.9.3","platform":"ruby","checksum":"55fd07ccdd64c622d36859748f2290fb9c119ce30b482867504e9f12654d6a65"},
|
||||
{"name":"kaminari","version":"1.2.2","platform":"ruby","checksum":"c4076ff9adccc6109408333f87b5c4abbda5e39dc464bd4c66d06d9f73442a3e"},
|
||||
|
|
|
|||
|
|
@ -1047,9 +1047,9 @@ GEM
|
|||
hana (~> 1.3)
|
||||
regexp_parser (~> 2.0)
|
||||
simpleidn (~> 0.2)
|
||||
jsonb_accessor (1.3.10)
|
||||
activerecord (>= 5.0)
|
||||
activesupport (>= 5.0)
|
||||
jsonb_accessor (1.4)
|
||||
activerecord (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
pg (>= 0.18.1)
|
||||
jsonpath (1.1.2)
|
||||
multi_json
|
||||
|
|
@ -2143,7 +2143,7 @@ DEPENDENCIES
|
|||
js_regex (~> 3.8)
|
||||
json (~> 2.7.2)
|
||||
json_schemer (~> 2.3.0)
|
||||
jsonb_accessor (~> 1.3.10)
|
||||
jsonb_accessor (~> 1.4)
|
||||
jwt (~> 2.9.3)
|
||||
kaminari (~> 1.2.2)
|
||||
knapsack (~> 4.0.0)
|
||||
|
|
|
|||
|
|
@ -352,8 +352,8 @@
|
|||
{"name":"json","version":"2.7.3","platform":"ruby","checksum":"7cc3ccbfc5f83025f5524776ae8581e911772944169666988406679251932bdf"},
|
||||
{"name":"json-jwt","version":"1.16.6","platform":"ruby","checksum":"ab451f9cd8743cecc4137f4170806046c1d8a6d4ee6e8570e0b5c958409b266c"},
|
||||
{"name":"json_schemer","version":"2.3.0","platform":"ruby","checksum":"9f1fa173b859ca520f15e9e8d08b0892ffca80b78dd8221feb3e360ff4cdeb35"},
|
||||
{"name":"jsonb_accessor","version":"1.3.10","platform":"java","checksum":"6630ac69dac46457b03e1352178ed3e2d7ba2d8edb99f2e9b64a0e60cda9ed26"},
|
||||
{"name":"jsonb_accessor","version":"1.3.10","platform":"ruby","checksum":"670f80a257ae39e3be9233c6a8ef3b03517e06687affe510dfe61237454c58e0"},
|
||||
{"name":"jsonb_accessor","version":"1.4","platform":"java","checksum":"2c5590d33d89c7b929d5cf38ae3d2c52658bf6f84f03b06ede5c88e9d76f3451"},
|
||||
{"name":"jsonb_accessor","version":"1.4","platform":"ruby","checksum":"010e087cb843e76b6a624d68af918a8efd6b0ff2ae25b6f0acebdfea45f776ab"},
|
||||
{"name":"jsonpath","version":"1.1.2","platform":"ruby","checksum":"6804124c244d04418218acb85b15c7caa79c592d7d6970195300428458946d3a"},
|
||||
{"name":"jwt","version":"2.9.3","platform":"ruby","checksum":"55fd07ccdd64c622d36859748f2290fb9c119ce30b482867504e9f12654d6a65"},
|
||||
{"name":"kaminari","version":"1.2.2","platform":"ruby","checksum":"c4076ff9adccc6109408333f87b5c4abbda5e39dc464bd4c66d06d9f73442a3e"},
|
||||
|
|
|
|||
|
|
@ -1061,9 +1061,9 @@ GEM
|
|||
hana (~> 1.3)
|
||||
regexp_parser (~> 2.0)
|
||||
simpleidn (~> 0.2)
|
||||
jsonb_accessor (1.3.10)
|
||||
activerecord (>= 5.0)
|
||||
activesupport (>= 5.0)
|
||||
jsonb_accessor (1.4)
|
||||
activerecord (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
pg (>= 0.18.1)
|
||||
jsonpath (1.1.2)
|
||||
multi_json
|
||||
|
|
@ -2170,7 +2170,7 @@ DEPENDENCIES
|
|||
js_regex (~> 3.8)
|
||||
json (~> 2.7.2)
|
||||
json_schemer (~> 2.3.0)
|
||||
jsonb_accessor (~> 1.3.10)
|
||||
jsonb_accessor (~> 1.4)
|
||||
jwt (~> 2.9.3)
|
||||
kaminari (~> 1.2.2)
|
||||
knapsack (~> 4.0.0)
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export default {
|
|||
</template>
|
||||
<template #cell(author)="{ item: { author } }">
|
||||
<gl-avatar-link
|
||||
v-if="author"
|
||||
:href="author.webUrl"
|
||||
:title="author.name"
|
||||
class="js-user-link !gl-text-subtle"
|
||||
|
|
|
|||
|
|
@ -161,6 +161,8 @@ export default {
|
|||
}
|
||||
},
|
||||
allowDuplicateExceptions(item) {
|
||||
if (item.format === NUGET_PACKAGE_FORMAT) return item.duplicatesAllowed;
|
||||
|
||||
// We're also enabling the duplicate exceptions input when duplicates are allowed
|
||||
// But the change is behind the packagesAllowDuplicateExceptions feature flag
|
||||
return !this.glFeatures.packagesAllowDuplicateExceptions && item.duplicatesAllowed;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ export const DESIGN_SINGLE_ARCHIVE_ERROR = s__(
|
|||
const DESIGN_UPLOAD_SKIPPED_MESSAGE = s__('DesignManagement|Upload skipped. %{reason}');
|
||||
const MAX_SKIPPED_FILES_LISTINGS = 5;
|
||||
|
||||
export const UPDATE_DESCRIPTION_ERROR = s__(
|
||||
'DesignManagement|Could not update description. Please try again.',
|
||||
);
|
||||
|
||||
export const UPLOAD_DESIGN_ERROR_MESSAGE = s__(
|
||||
'DesignManagement|Error uploading a new design. Please try again.',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,39 +1,292 @@
|
|||
<script>
|
||||
import { GlButton, GlFormGroup, GlAlert, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
|
||||
import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '~/vue_shared/constants';
|
||||
import { renderGFM } from '~/behaviors/markdown/render_gfm';
|
||||
import { toggleMarkCheckboxes } from '~/behaviors/markdown/utils';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import updateDesignDescriptionMutation from '../graphql/update_design_description.mutation.graphql';
|
||||
import { UPDATE_DESCRIPTION_ERROR } from '../constants';
|
||||
|
||||
const isCheckbox = (target) => target?.classList.contains('task-list-item-checkbox');
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
descriptionLabel: s__('DesignManagement|Description'),
|
||||
components: {
|
||||
MarkdownEditor,
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlFormGroup,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
i18n: {
|
||||
editDescription: s__('DesignManagement|Edit description'),
|
||||
descriptionLabel: s__('DesignManagement|Description'),
|
||||
addDescriptionLabel: s__('DesignManagement|Add a description'),
|
||||
cancelEdit: s__('DesignManagement|Are you sure you want to cancel editing the description?'),
|
||||
primaryModalButton: s__('DesignManagement|Discard changes'),
|
||||
cancelModalButton: s__('DesignManagement|Continue editing'),
|
||||
},
|
||||
formFieldProps: {
|
||||
id: 'design-description',
|
||||
name: 'design-description',
|
||||
placeholder: s__('DesignManagement|Write a comment or drag your files here…'),
|
||||
'aria-label': s__('DesignManagement|Design description'),
|
||||
},
|
||||
markdownDocsPath: helpPagePath('user/markdown'),
|
||||
quickActionsDocsPath: helpPagePath('user/project/quick_actions'),
|
||||
props: {
|
||||
design: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
designVariables: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
markdownPreviewPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
descriptionText: this.design.description || '',
|
||||
showEditor: false,
|
||||
isSubmitting: false,
|
||||
errorMessage: '',
|
||||
autosaveKey: `WorkItem/${getIdFromGraphQLId(this.design.issue.id)}/Design/${getIdFromGraphQLId(
|
||||
this.design.id,
|
||||
)}`,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canUpdate() {
|
||||
return this.design.issue?.userPermissions?.updateDesign && !this.showEditor;
|
||||
},
|
||||
showAddDescriptionButton() {
|
||||
return this.design.issue?.userPermissions?.updateDesign && !this.design.descriptionHtml;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'design.descriptionHtml': {
|
||||
handler(newDescriptionHtml, oldDescriptionHtml) {
|
||||
if (newDescriptionHtml !== oldDescriptionHtml) {
|
||||
this.renderGFM();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
startEditing() {
|
||||
this.showEditor = true;
|
||||
},
|
||||
closeForm() {
|
||||
// If description text is empty on cancel,
|
||||
// restore the old description
|
||||
if (!this.descriptionText) {
|
||||
this.descriptionText = this.design.description;
|
||||
}
|
||||
this.showEditor = false;
|
||||
},
|
||||
async cancelDescription() {
|
||||
if (this.descriptionText && this.descriptionText !== this.design.description) {
|
||||
const confirmed = await confirmAction(this.$options.i18n.cancelEdit, {
|
||||
primaryBtnText: this.$options.i18n.primaryModalButton,
|
||||
cancelBtnText: this.$options.i18n.cancelModalButton,
|
||||
primaryBtnVariant: 'danger',
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update markdown's placeholder text based on if
|
||||
// an existing description was being edited or
|
||||
// a totally new description was being added
|
||||
if (!this.design.descriptionHtml) {
|
||||
this.descriptionText = '';
|
||||
} else {
|
||||
this.descriptionText = this.design.description;
|
||||
}
|
||||
|
||||
markdownEditorEventHub.$emit(CLEAR_AUTOSAVE_ENTRY_EVENT, this.autosaveKey);
|
||||
this.showEditor = false;
|
||||
this.enableCheckboxes();
|
||||
} else {
|
||||
this.descriptionText = this.design.description;
|
||||
this.showEditor = false;
|
||||
}
|
||||
},
|
||||
enableCheckboxes() {
|
||||
if (this.canUpdate) {
|
||||
const checkboxes = this.$el.querySelectorAll('.task-list-item-checkbox');
|
||||
|
||||
// enable boxes, disabled by default in markdown
|
||||
checkboxes.forEach((checkbox) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
checkbox.disabled = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
async renderGFM() {
|
||||
await this.$nextTick();
|
||||
renderGFM(this.$refs['gfm-content']);
|
||||
this.enableCheckboxes();
|
||||
},
|
||||
setDescriptionText(newText) {
|
||||
// Do not update when cmd+enter is executed
|
||||
if (!this.isSubmitting) {
|
||||
this.descriptionText = newText;
|
||||
}
|
||||
},
|
||||
async updateDesignDescription() {
|
||||
this.isSubmitting = true;
|
||||
|
||||
if (this.$refs.markdownEditor) {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
trackSavedUsingEditor(this.$refs.markdownEditor.isContentEditorActive, 'Design');
|
||||
}
|
||||
|
||||
try {
|
||||
const descriptionInput = { description: this.descriptionText, id: this.design.id };
|
||||
|
||||
await this.$apollo.mutate({
|
||||
mutation: updateDesignDescriptionMutation,
|
||||
variables: {
|
||||
input: descriptionInput,
|
||||
},
|
||||
});
|
||||
|
||||
this.closeForm();
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
this.errorMessage = UPDATE_DESCRIPTION_ERROR;
|
||||
} finally {
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
},
|
||||
toggleCheckboxes(event) {
|
||||
const { target } = event;
|
||||
|
||||
if (isCheckbox(target)) {
|
||||
target.disabled = true;
|
||||
|
||||
const { sourcepos } = target.parentElement.dataset;
|
||||
|
||||
if (!sourcepos) return;
|
||||
|
||||
// Toggle checkboxes based on user input
|
||||
this.descriptionText = toggleMarkCheckboxes({
|
||||
rawMarkdown: this.descriptionText,
|
||||
checkboxChecked: target.checked,
|
||||
sourcepos,
|
||||
});
|
||||
|
||||
// Update the desciption text using mutation
|
||||
this.updateDesignDescription();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="design-description-container">
|
||||
<div class="design-description-view">
|
||||
<div class="design-description-header gl-mb-2 gl-flex gl-justify-between">
|
||||
<h3 class="gl-m-0 gl-text-lg !gl-leading-20">
|
||||
{{ $options.i18n.descriptionLabel }}
|
||||
</h3>
|
||||
<gl-form-group
|
||||
v-if="showEditor"
|
||||
class="design-description-form common-note-form"
|
||||
:label="$options.i18n.descriptionLabel"
|
||||
label-class="!gl-leading-20 !gl-text-lg"
|
||||
>
|
||||
<div v-if="errorMessage" class="gl-pb-3">
|
||||
<gl-alert variant="danger" @dismiss="errorMessage = null">
|
||||
{{ errorMessage }}
|
||||
</gl-alert>
|
||||
</div>
|
||||
<div v-if="design.descriptionHtml" class="design-description js-task-list-container">
|
||||
<div
|
||||
ref="gfm-content"
|
||||
v-safe-html="design.descriptionHtml"
|
||||
class="md gl-mb-4"
|
||||
data-testid="design-description-content"
|
||||
></div>
|
||||
<markdown-editor
|
||||
ref="markdownEditor"
|
||||
:value="descriptionText"
|
||||
:render-markdown-path="markdownPreviewPath"
|
||||
:markdown-docs-path="$options.markdownDocsPath"
|
||||
:form-field-props="$options.formFieldProps"
|
||||
:quick-actions-docs-path="$options.quickActionsDocsPath"
|
||||
:autosave-key="autosaveKey"
|
||||
enable-autocomplete
|
||||
:supports-quick-actions="false"
|
||||
autofocus
|
||||
@input="setDescriptionText"
|
||||
@keydown.meta.enter="updateDesignDescription"
|
||||
@keydown.ctrl.enter="updateDesignDescription"
|
||||
@keydown.esc.stop="cancelDescription"
|
||||
/>
|
||||
<div class="gl-mt-3 gl-flex">
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
:loading="isSubmitting"
|
||||
data-testid="save-description"
|
||||
@click="updateDesignDescription"
|
||||
>{{ s__('DesignManagement|Save changes') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
category="tertiary"
|
||||
class="gl-ml-3"
|
||||
data-testid="cancel"
|
||||
@click="cancelDescription"
|
||||
>{{ s__('DesignManagement|Cancel') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</gl-form-group>
|
||||
<div v-else class="design-description-view">
|
||||
<gl-button
|
||||
v-if="showAddDescriptionButton"
|
||||
class="gl-mb-5 gl-ml-auto"
|
||||
size="small"
|
||||
data-testid="add-design-description"
|
||||
:aria-label="$options.i18n.addDescriptionLabel"
|
||||
@click="startEditing"
|
||||
>
|
||||
{{ $options.i18n.addDescriptionLabel }}
|
||||
</gl-button>
|
||||
<template v-else>
|
||||
<div class="design-description-header gl-mb-2 gl-flex gl-justify-between">
|
||||
<h3 class="gl-m-0 gl-text-lg !gl-leading-20">
|
||||
{{ $options.i18n.descriptionLabel }}
|
||||
</h3>
|
||||
<gl-button
|
||||
v-if="canUpdate"
|
||||
v-gl-tooltip
|
||||
class="gl-ml-auto"
|
||||
size="small"
|
||||
category="tertiary"
|
||||
:aria-label="$options.i18n.editDescription"
|
||||
:title="$options.i18n.editDescription"
|
||||
data-testid="edit-description"
|
||||
icon="pencil"
|
||||
@click="startEditing"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="design.descriptionHtml" class="design-description js-task-list-container">
|
||||
<div
|
||||
ref="gfm-content"
|
||||
v-safe-html="design.descriptionHtml"
|
||||
class="md gl-mb-4"
|
||||
data-testid="design-description-content"
|
||||
@change="toggleCheckboxes"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -131,6 +131,9 @@ export default {
|
|||
design() {
|
||||
return this.localDesign || {};
|
||||
},
|
||||
markdownPreviewPath() {
|
||||
return `/${this.fullPath}/-/preview_markdown?target_type=Issue`;
|
||||
},
|
||||
designVariables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
|
|
@ -330,8 +333,10 @@ export default {
|
|||
</div>
|
||||
<design-sidebar
|
||||
:design="design"
|
||||
:design-variables="designVariables"
|
||||
:is-loading="isLoading"
|
||||
:is-open="isSidebarOpen"
|
||||
:markdown-preview-path="markdownPreviewPath"
|
||||
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
|
||||
@toggleResolvedComments="toggleResolvedComments"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
designVariables: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
|
@ -37,6 +41,10 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
markdownPreviewPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -44,8 +52,13 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
showDescription() {
|
||||
return !this.isLoading && Boolean(this.design.descriptionHtml);
|
||||
showDescriptionForm() {
|
||||
// user either has permission to add or update description,
|
||||
// or the existing description should be shown read-only.
|
||||
return (
|
||||
!this.isLoading &&
|
||||
(this.design.issue?.userPermissions?.updateDesign || Boolean(this.design.descriptionHtml))
|
||||
);
|
||||
},
|
||||
discussions() {
|
||||
return this.design?.discussions ? extractDiscussions(this.design.discussions) : [];
|
||||
|
|
@ -99,7 +112,13 @@ export default {
|
|||
<design-disclosure :open="isOpen">
|
||||
<template #default>
|
||||
<div class="image-notes gl-h-full gl-pt-0" @click.self="handleSidebarClick">
|
||||
<design-description v-if="showDescription" :design="design" class="gl-border-b gl-my-5" />
|
||||
<design-description
|
||||
v-if="showDescriptionForm"
|
||||
:design="design"
|
||||
:design-variables="designVariables"
|
||||
:markdown-preview-path="markdownPreviewPath"
|
||||
class="gl-border-b gl-my-5"
|
||||
/>
|
||||
<div v-if="isLoading" class="gl-my-5">
|
||||
<gl-skeleton-loader />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#import "./fragments/design_file.fragment.graphql"
|
||||
#import "./fragments/discussion_resolved_status.fragment.graphql"
|
||||
#import "~/graphql_shared/fragments/author.fragment.graphql"
|
||||
#import "./fragments/design_note.fragment.graphql"
|
||||
#import "./fragments/version.fragment.graphql"
|
||||
|
||||
|
|
@ -34,6 +35,21 @@ query getDesignDetails(
|
|||
}
|
||||
}
|
||||
}
|
||||
issue {
|
||||
id
|
||||
title
|
||||
webPath
|
||||
webUrl
|
||||
participants {
|
||||
nodes {
|
||||
...Author
|
||||
}
|
||||
}
|
||||
userPermissions {
|
||||
createDesign
|
||||
updateDesign
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
versions {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#import "./fragments/design_file.fragment.graphql"
|
||||
#import "./fragments/discussion_resolved_status.fragment.graphql"
|
||||
#import "~/graphql_shared/fragments/author.fragment.graphql"
|
||||
#import "./fragments/design_note.fragment.graphql"
|
||||
|
||||
query getLocalDesignQuery($atVersion: DesignManagementVersionID, $filenames: [String!]) {
|
||||
|
|
@ -17,5 +18,20 @@ query getLocalDesignQuery($atVersion: DesignManagementVersionID, $filenames: [St
|
|||
}
|
||||
}
|
||||
}
|
||||
issue {
|
||||
id
|
||||
title
|
||||
webPath
|
||||
webUrl
|
||||
participants {
|
||||
nodes {
|
||||
...Author
|
||||
}
|
||||
}
|
||||
userPermissions {
|
||||
createDesign
|
||||
updateDesign
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
mutation updateWorkItemDesignDescriptionMutation($input: DesignManagementUpdateInput!) {
|
||||
designManagementUpdate(input: $input) {
|
||||
errors
|
||||
design {
|
||||
id
|
||||
image
|
||||
description
|
||||
descriptionHtml
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -235,8 +235,8 @@ class Namespace < ApplicationRecord
|
|||
.where(project_statistics[:namespace_id].eq(arel_table[:id]))
|
||||
.lateral(subquery.name)
|
||||
|
||||
select(arel_table[Arel.star], subquery[Arel.star])
|
||||
.from([arel.as('namespaces'), statistics])
|
||||
model.select(arel_table[Arel.star], subquery[Arel.star])
|
||||
.from([arel.as(arel_table.name), statistics])
|
||||
end
|
||||
|
||||
scope :with_jira_installation, ->(installation_id) do
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ module Packages
|
|||
ExtractionError = Class.new(StandardError)
|
||||
|
||||
def execute
|
||||
return ServiceResponse.success if Namespace::PackageSetting.duplicates_allowed?(existing_package)
|
||||
return ServiceResponse.success unless target_package_is_duplicate?
|
||||
return ServiceResponse.success if package_settings_allow_duplicates? || !target_package_is_duplicate?
|
||||
|
||||
ServiceResponse.error(
|
||||
message: 'A package with the same name and version already exists',
|
||||
|
|
@ -21,12 +20,21 @@ module Packages
|
|||
|
||||
private
|
||||
|
||||
def package_settings_allow_duplicates?
|
||||
package_settings.nuget_duplicates_allowed? || package_settings.class.duplicates_allowed?(existing_package)
|
||||
end
|
||||
|
||||
def target_package_is_duplicate?
|
||||
existing_package.name.casecmp(metadata[:package_name]) == 0 &&
|
||||
(existing_package.version.casecmp(metadata[:package_version]) == 0 ||
|
||||
existing_package.normalized_nuget_version&.casecmp(metadata[:package_version]) == 0)
|
||||
end
|
||||
|
||||
def package_settings
|
||||
project.namespace.package_settings
|
||||
end
|
||||
strong_memoize_attr :package_settings
|
||||
|
||||
def existing_package
|
||||
::Packages::Nuget::PackageFinder
|
||||
.new(
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@
|
|||
"glm_source": {
|
||||
"description": "GitLab link management value for source on the link the user clicks to start onboarding",
|
||||
"type": "string"
|
||||
},
|
||||
"joining_project": {
|
||||
"description": "Setting to understand if a user is joining a project or not during onboarding",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: api_request_access_with_scope
|
||||
description: A susbset of API requests authenticated by a token with an audited scope
|
||||
introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/499461
|
||||
introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172548
|
||||
feature_category: duo_workflow
|
||||
milestone: '17.7'
|
||||
saved_to_database: true
|
||||
scope: [User]
|
||||
streamed: true
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
description: Tracks when a CI build fails pipeline fails. Includes information about build_id, build_artifact_types, exit_code, failure_reason and project_id
|
||||
action: failed
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
product_group: pipeline_execution
|
||||
milestone: '15.7'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92637
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
description: Tracks when an issue is promoted to an epic
|
||||
action: promote
|
||||
identifiers:
|
||||
- project
|
||||
- namespace
|
||||
- user
|
||||
product_group: product_planning
|
||||
milestone: '14.0'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167137
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
additional_properties:
|
||||
property:
|
||||
description: Always 'issue_id'
|
||||
value:
|
||||
description: Issue id
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
description: Tracks when a manifest is pulled from the package registry
|
||||
action: pull_manifest
|
||||
identifiers:
|
||||
- namespace
|
||||
- user
|
||||
product_group: container_registry
|
||||
milestone: '14.2'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167137
|
||||
distributions:
|
||||
- ce
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
description: Tracks when user is reported for abuse
|
||||
action: select_abuse_category
|
||||
identifiers:
|
||||
- user
|
||||
product_group: authorization
|
||||
milestone: '15.9'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167137
|
||||
distributions:
|
||||
- ce
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
additional_properties:
|
||||
property:
|
||||
description: The abuse category that was reported
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: enforce_ssh_certificates_via_settings
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136498
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426235
|
||||
milestone: '16.7'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: false
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: ssh_certificates_rest_endpoints
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130866
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/424501
|
||||
milestone: '16.4'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: false
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
name: expand_nested_variables_in_job_rules_exists_and_changes
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327780
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/166779
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/483115
|
||||
milestone: '17.6'
|
||||
group: group::pipeline authoring
|
||||
name: api_audit_requests_with_scope
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/499461
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172548
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/505974
|
||||
milestone: '17.7'
|
||||
group: group::duo workflow
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -195,6 +195,9 @@ country_access_logs:
|
|||
column: user_id
|
||||
on_delete: async_delete
|
||||
dast_pre_scan_verifications:
|
||||
- table: ci_pipelines
|
||||
column: ci_pipeline_id
|
||||
on_delete: async_delete
|
||||
- table: p_ci_pipelines
|
||||
column: ci_pipeline_id
|
||||
on_delete: async_delete
|
||||
|
|
@ -213,6 +216,9 @@ dast_profiles:
|
|||
column: project_id
|
||||
on_delete: async_delete
|
||||
dast_profiles_pipelines:
|
||||
- table: ci_pipelines
|
||||
column: ci_pipeline_id
|
||||
on_delete: async_delete
|
||||
- table: p_ci_pipelines
|
||||
column: ci_pipeline_id
|
||||
on_delete: async_delete
|
||||
|
|
@ -266,6 +272,9 @@ dependency_list_export_parts:
|
|||
column: organization_id
|
||||
on_delete: async_delete
|
||||
dependency_list_exports:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_delete
|
||||
- table: p_ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_delete
|
||||
|
|
@ -323,6 +332,9 @@ members:
|
|||
column: user_id
|
||||
on_delete: async_delete
|
||||
merge_request_metrics:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
- table: p_ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
|
|
@ -337,10 +349,16 @@ merge_request_requested_changes:
|
|||
column: user_id
|
||||
on_delete: async_delete
|
||||
merge_requests:
|
||||
- table: ci_pipelines
|
||||
column: head_pipeline_id
|
||||
on_delete: async_nullify
|
||||
- table: p_ci_pipelines
|
||||
column: head_pipeline_id
|
||||
on_delete: async_nullify
|
||||
merge_trains:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
- table: p_ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
|
|
@ -434,6 +452,9 @@ p_ci_stages:
|
|||
column: project_id
|
||||
on_delete: async_delete
|
||||
packages_build_infos:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
- table: p_ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
|
|
@ -444,6 +465,9 @@ packages_npm_metadata_caches:
|
|||
target_column: status
|
||||
target_value: 2
|
||||
packages_package_file_build_infos:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
- table: p_ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
|
|
@ -508,6 +532,9 @@ sbom_components:
|
|||
column: organization_id
|
||||
on_delete: async_delete
|
||||
sbom_occurrences:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
- table: p_ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
|
|
@ -610,6 +637,9 @@ vulnerability_external_issue_links:
|
|||
column: author_id
|
||||
on_delete: async_delete
|
||||
vulnerability_feedback:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
- table: p_ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
|
|
@ -671,6 +701,12 @@ vulnerability_namespace_historical_statistics:
|
|||
column: namespace_id
|
||||
on_delete: async_delete
|
||||
vulnerability_occurrences:
|
||||
- table: ci_pipelines
|
||||
column: initial_pipeline_id
|
||||
on_delete: async_nullify
|
||||
- table: ci_pipelines
|
||||
column: latest_pipeline_id
|
||||
on_delete: async_nullify
|
||||
- table: p_ci_pipelines
|
||||
column: initial_pipeline_id
|
||||
on_delete: async_nullify
|
||||
|
|
@ -707,6 +743,9 @@ vulnerability_state_transitions:
|
|||
column: author_id
|
||||
on_delete: async_nullify
|
||||
vulnerability_statistics:
|
||||
- table: ci_pipelines
|
||||
column: latest_pipeline_id
|
||||
on_delete: async_nullify
|
||||
- table: p_ci_pipelines
|
||||
column: latest_pipeline_id
|
||||
on_delete: async_nullify
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
migration_job_name: BackfillDetectedAtFromCreatedAtColumn
|
||||
description: backfills the vulnerabilities table with nil detected_at with values from created_at.
|
||||
feature_category: vulnerability_management
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/170022
|
||||
milestone: '17.6'
|
||||
queued_migration_version: 20241021063020
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class QueueBackfillDetectedAtFromCreatedAtColumn < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.6'
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_sec
|
||||
|
||||
MIGRATION = "BackfillDetectedAtFromCreatedAtColumn"
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 1000
|
||||
SUB_BATCH_SIZE = 100
|
||||
|
||||
def up
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:vulnerabilities,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
delete_batched_background_migration(MIGRATION, :vulnerabilities, :id, [])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
6031eb946f78909f5934e1476f6fc4458cde1a900488b9b9d9e290d3fdeab659
|
||||
|
|
@ -12,9 +12,7 @@ DETAILS:
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421915) in GitLab 16.4 [with a flag](../user/feature_flags.md) named `ssh_certificates_rest_endpoints`. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/424501) in GitLab 16.9.
|
||||
|
||||
FLAG:
|
||||
On GitLab.com, this feature is available. On GitLab Dedicated, this feature is not available.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/424501) in GitLab 17.7. Feature flag `ssh_certificates_rest_endpoints` removed.
|
||||
|
||||
Use this API to create, read and delete SSH certificates for a group.
|
||||
Only top-level groups can store SSH certificates.
|
||||
|
|
|
|||
|
|
@ -261,6 +261,12 @@ Audit event types belong to the following product categories.
|
|||
| [`cluster_agent_token_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112036) | Triggered when a user creates a cluster agent token | **{check-circle}** Yes | GitLab [15.10](https://gitlab.com/gitlab-org/gitlab/-/issues/382133) | Project |
|
||||
| [`cluster_agent_token_revoked`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112036) | Triggered when a user revokes a cluster agent token | **{check-circle}** Yes | GitLab [15.10](https://gitlab.com/gitlab-org/gitlab/-/issues/382133) | Project |
|
||||
|
||||
### Duo workflow
|
||||
|
||||
| Name | Description | Saved to database | Streamed | Introduced in | Scope |
|
||||
|:------------|:------------|:------------------|:---------|:--------------|:--------------|
|
||||
| [`api_request_access_with_scope`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172548) | A susbset of API requests authenticated by a token with an audited scope | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.7](https://gitlab.com/gitlab-org/gitlab/-/issues/499461) | User |
|
||||
|
||||
### Dynamic application security testing
|
||||
|
||||
| Name | Description | Saved to database | Introduced in | Scope |
|
||||
|
|
|
|||
|
|
@ -233,6 +233,10 @@ If you encounter issues:
|
|||
1. Search for the setting **GitLab: Debug** and enable it.
|
||||
1. Examine the [Duo Workflow Service production LangSmith trace](https://smith.langchain.com/o/477de7ad-583e-47b6-a1c4-c4a0300e7aca/projects/p/5409132b-2cf3-4df8-9f14-70204f90ed9b?timeModel=%7B%22duration%22%3A%227d%22%7D&tab=0).
|
||||
|
||||
## Audit log
|
||||
|
||||
Audit event is created for each API request done by Duo Workflow. View these events on the [instance audit events](../../administration/audit_event_reports.md#instance-audit-events) page.
|
||||
|
||||
## Give feedback
|
||||
|
||||
Duo Workflow is an experiment and your feedback is crucial. To report issues or suggest improvements,
|
||||
|
|
|
|||
|
|
@ -21,10 +21,7 @@ To temporarily grant Git access to your projects, you can use SSH certificates.
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421915) in GitLab 16.4 [with a flag](../feature_flags.md) named `ssh_certificates_rest_endpoints`. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/424501) in GitLab 16.9.
|
||||
|
||||
FLAG:
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
For more information, see the history.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/424501) in GitLab 17.7. Feature flag `ssh_certificates_rest_endpoints` removed.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
|
|
@ -69,6 +66,7 @@ The user certificates can only be used to access the projects in the top-level g
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421915) in GitLab 16.7 [with a flag](../feature_flags.md) named `enforce_ssh_certificates_via_settings`. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/426235) in GitLab 16.9.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/488635) in GitLab 17.7. Feature flag `enforce_ssh_certificates_via_settings` removed.
|
||||
|
||||
FLAG:
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ module API
|
|||
API_RESPONSE_STATUS_CODE = 'gitlab.api.response_status_code'
|
||||
INTEGER_ID_REGEX = /^-?\d+$/
|
||||
|
||||
# ai_workflows scope is used by Duo Workflow which is an AI automation tool, requests authenticated by token with
|
||||
# this scope are audited to keep track of all actions done by Duo Workflow.
|
||||
TOKEN_SCOPES_TO_AUDIT = [:ai_workflows].freeze
|
||||
|
||||
def logger
|
||||
API.logger
|
||||
end
|
||||
|
|
@ -89,6 +93,7 @@ module API
|
|||
|
||||
if @current_user
|
||||
load_balancer_stick_request(::ApplicationRecord, :user, @current_user.id)
|
||||
audit_request_with_token_scope(@current_user)
|
||||
end
|
||||
|
||||
@current_user
|
||||
|
|
@ -816,6 +821,29 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
def audit_request_with_token_scope(user)
|
||||
return unless Feature.enabled?(:api_audit_requests_with_scope, user)
|
||||
|
||||
token_info = request.env[::Gitlab::Auth::AuthFinders::API_TOKEN_ENV]
|
||||
return unless token_info
|
||||
return unless TOKEN_SCOPES_TO_AUDIT.intersect?(Array.wrap(token_info[:token_scopes]))
|
||||
|
||||
context = {
|
||||
name: 'api_request_access_with_scope',
|
||||
author: user,
|
||||
scope: user,
|
||||
target: ::Gitlab::Audit::NullTarget.new,
|
||||
message: "API request with token scopes #{token_info[:token_scopes]} - #{request.request_method} #{request.path}",
|
||||
additional_details: {
|
||||
request: request.path,
|
||||
method: request.request_method,
|
||||
token_scopes: token_info[:token_scopes]
|
||||
}
|
||||
}
|
||||
|
||||
::Gitlab::Audit::Auditor.audit(context)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
|
|
|
|||
|
|
@ -45,28 +45,31 @@ module BulkImports
|
|||
|
||||
return unless object
|
||||
|
||||
if object.new_record?
|
||||
saver = Gitlab::ImportExport::Base::RelationObjectSaver.new(
|
||||
relation_object: object,
|
||||
relation_key: relation,
|
||||
relation_definition: relation_definition,
|
||||
importable: portable
|
||||
)
|
||||
begin
|
||||
if object.new_record?
|
||||
saver = Gitlab::ImportExport::Base::RelationObjectSaver.new(
|
||||
relation_object: object,
|
||||
relation_key: relation,
|
||||
relation_definition: relation_definition,
|
||||
importable: portable
|
||||
)
|
||||
|
||||
saver.execute
|
||||
saver.execute
|
||||
|
||||
capture_invalid_subrelations(saver.invalid_subrelations)
|
||||
else
|
||||
if object.invalid?
|
||||
Gitlab::Import::Errors.merge_nested_errors(object)
|
||||
capture_invalid_subrelations(saver.invalid_subrelations)
|
||||
else
|
||||
if object.invalid?
|
||||
Gitlab::Import::Errors.merge_nested_errors(object)
|
||||
|
||||
raise(ActiveRecord::RecordInvalid, object)
|
||||
raise(ActiveRecord::RecordInvalid, object)
|
||||
end
|
||||
|
||||
object.save!
|
||||
end
|
||||
|
||||
object.save!
|
||||
ensure
|
||||
push_placeholder_references(original_users_map) if context.importer_user_mapping_enabled?
|
||||
end
|
||||
|
||||
push_placeholder_references(original_users_map) if context.importer_user_mapping_enabled?
|
||||
end
|
||||
|
||||
def deep_transform_relation!(relation_hash, relation_key, relation_definition, &block)
|
||||
|
|
|
|||
|
|
@ -221,7 +221,11 @@ module Gitlab
|
|||
private
|
||||
|
||||
def save_current_token_in_env
|
||||
request.env[API_TOKEN_ENV] = { token_id: access_token.id, token_type: access_token.class.to_s }
|
||||
request.env[API_TOKEN_ENV] = {
|
||||
token_id: access_token.id,
|
||||
token_type: access_token.class.to_s,
|
||||
token_scopes: access_token.scopes.map(&:to_sym)
|
||||
}
|
||||
end
|
||||
|
||||
def save_auth_failure_in_application_context(access_token, cause, requested_scopes)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
class BackfillDetectedAtFromCreatedAtColumn < BatchedMigrationJob
|
||||
operation_name :backfill_vulnerabilities_detected_at
|
||||
feature_category :vulnerability_management
|
||||
|
||||
def perform
|
||||
each_sub_batch do |sub_batch|
|
||||
sub_batch.where(detected_at: nil).update_all('detected_at = created_at')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -17,7 +17,7 @@ module Gitlab
|
|||
return true unless modified_paths
|
||||
return false if modified_paths.empty?
|
||||
|
||||
expanded_globs = expand_globs(context, pipeline).uniq
|
||||
expanded_globs = expand_globs(context).uniq
|
||||
return false if expanded_globs.empty?
|
||||
|
||||
cache_key = [
|
||||
|
|
@ -43,17 +43,11 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def expand_globs(context, pipeline)
|
||||
def expand_globs(context)
|
||||
return paths unless context
|
||||
|
||||
if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline.project)
|
||||
paths.map do |glob|
|
||||
expand_value_nested(glob, context)
|
||||
end
|
||||
else
|
||||
paths.map do |glob|
|
||||
expand_value(glob, context)
|
||||
end
|
||||
paths.map do |glob|
|
||||
expand_value_nested(glob, context)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -76,22 +70,13 @@ module Gitlab
|
|||
def find_compare_to_sha(pipeline, context)
|
||||
return unless @globs.include?(:compare_to)
|
||||
|
||||
compare_to = if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline.project)
|
||||
expand_value_nested(@globs[:compare_to], context)
|
||||
else
|
||||
expand_value(@globs[:compare_to], context)
|
||||
end
|
||||
|
||||
compare_to = expand_value_nested(@globs[:compare_to], context)
|
||||
commit = pipeline.project.commit(compare_to)
|
||||
raise Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref' unless commit
|
||||
|
||||
commit.sha
|
||||
end
|
||||
|
||||
def expand_value(value, context)
|
||||
ExpandVariables.expand_existing(value, -> { context.variables_hash })
|
||||
end
|
||||
|
||||
def expand_value_nested(value, context)
|
||||
ExpandVariables.expand_existing(value, -> { context.variables_hash_expanded })
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ module Gitlab
|
|||
@ref = clause[:ref]
|
||||
end
|
||||
|
||||
def satisfied_by?(pipeline, context)
|
||||
def satisfied_by?(_pipeline, context)
|
||||
# Return early to avoid redundant Gitaly calls
|
||||
return false unless @globs.any?
|
||||
|
||||
context = change_context(context, pipeline) if @project_path
|
||||
context = change_context(context) if @project_path
|
||||
|
||||
expanded_globs = expand_globs(context, pipeline)
|
||||
expanded_globs = expand_globs(context)
|
||||
top_level_only = expanded_globs.all?(&method(:top_level_glob?))
|
||||
|
||||
paths = worktree_paths(context, top_level_only)
|
||||
|
|
@ -42,15 +42,9 @@ module Gitlab
|
|||
grouped.values_at(:exact, :extension, :pattern).map { |globs| Array(globs) }
|
||||
end
|
||||
|
||||
def expand_globs(context, pipeline)
|
||||
if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline&.project)
|
||||
@globs.map do |glob|
|
||||
expand_value_nested(glob, context)
|
||||
end
|
||||
else
|
||||
@globs.map do |glob|
|
||||
expand_value(glob, context)
|
||||
end
|
||||
def expand_globs(context)
|
||||
@globs.map do |glob|
|
||||
expand_value_nested(glob, context)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -127,10 +121,10 @@ module Gitlab
|
|||
glob.delete_prefix(WILDCARD_NESTED_PATTERN)
|
||||
end
|
||||
|
||||
def change_context(old_context, pipeline)
|
||||
def change_context(old_context)
|
||||
user = find_context_user(old_context)
|
||||
new_project = find_context_project(user, old_context, pipeline)
|
||||
new_sha = find_context_sha(new_project, old_context, pipeline)
|
||||
new_project = find_context_project(user, old_context)
|
||||
new_sha = find_context_sha(new_project, old_context)
|
||||
|
||||
Gitlab::Ci::Config::External::Context.new(
|
||||
project: new_project,
|
||||
|
|
@ -144,13 +138,8 @@ module Gitlab
|
|||
context.is_a?(Gitlab::Ci::Config::External::Context) ? context.user : context.pipeline.user
|
||||
end
|
||||
|
||||
def find_context_project(user, context, pipeline)
|
||||
full_path = if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline.project)
|
||||
expand_value_nested(@project_path, context)
|
||||
else
|
||||
expand_value(@project_path, context)
|
||||
end
|
||||
|
||||
def find_context_project(user, context)
|
||||
full_path = expand_value_nested(@project_path, context)
|
||||
project = Project.find_by_full_path(full_path)
|
||||
|
||||
unless project
|
||||
|
|
@ -167,16 +156,10 @@ module Gitlab
|
|||
project
|
||||
end
|
||||
|
||||
def find_context_sha(project, context, pipeline)
|
||||
def find_context_sha(project, context)
|
||||
return project.commit&.sha unless @ref
|
||||
|
||||
ref = if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes,
|
||||
pipeline.project)
|
||||
expand_value_nested(@ref, context)
|
||||
else
|
||||
expand_value(@ref, context)
|
||||
end
|
||||
|
||||
ref = expand_value_nested(@ref, context)
|
||||
commit = project.commit(ref)
|
||||
|
||||
unless commit
|
||||
|
|
@ -198,10 +181,6 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def expand_value(value, context)
|
||||
ExpandVariables.expand_existing(value, -> { context.variables_hash })
|
||||
end
|
||||
|
||||
def expand_value_nested(value, context)
|
||||
ExpandVariables.expand_existing(value, -> { context.variables_hash_expanded })
|
||||
end
|
||||
|
|
|
|||
|
|
@ -295,6 +295,16 @@ module Gitlab
|
|||
def being_watched_cache_key
|
||||
"gitlab:ci:trace:#{job.id}:watched"
|
||||
end
|
||||
|
||||
# Like `set` it writes the whole trace but doesn't hide secrets!
|
||||
# This is solely used in spec factories to avoid calling unstubbed
|
||||
# ApplicationSetting(ci_job_token_signing_key) attribute outside
|
||||
# spec examples (like in `let_it_be`).
|
||||
def unsafe_set(data)
|
||||
write('w+b') do |stream|
|
||||
stream.set(data.dup)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -163,7 +163,6 @@ module Gitlab
|
|||
{ service: 'gitaly.RepositoryService', method: 'RepositorySize' },
|
||||
{ service: 'gitaly.RepositoryService', method: 'SearchFilesByContent' },
|
||||
{ service: 'gitaly.RepositoryService', method: 'SearchFilesByName' },
|
||||
{ service: 'gitaly.ServerService', method: 'ClockSynced' },
|
||||
{ service: 'gitaly.ServerService', method: 'DiskStatistics' },
|
||||
{ service: 'gitaly.ServerService', method: 'ReadinessCheck' },
|
||||
{ service: 'gitaly.ServerService', method: 'ServerInfo' },
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ namespace :tw do
|
|||
CodeOwnerRule.new('Code Review', '@aqualls'),
|
||||
CodeOwnerRule.new('Compliance', '@eread'),
|
||||
CodeOwnerRule.new('Composition Analysis', '@rdickenson @phillipwells'),
|
||||
CodeOwnerRule.new('Container Registry', '@marcel.amirault'),
|
||||
CodeOwnerRule.new('Container Registry', '@lyspin'),
|
||||
CodeOwnerRule.new('Contributor Experience', '@eread'),
|
||||
CodeOwnerRule.new('Custom Models', '@jglassman1'),
|
||||
# CodeOwnerRule.new('Database', ''),
|
||||
|
|
|
|||
|
|
@ -19307,6 +19307,9 @@ msgstr ""
|
|||
msgid "DesignManagement|Are you sure you want to cancel creating this comment?"
|
||||
msgstr ""
|
||||
|
||||
msgid "DesignManagement|Are you sure you want to cancel editing the description?"
|
||||
msgstr ""
|
||||
|
||||
msgid "DesignManagement|Are you sure you want to cancel editing this comment?"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -262,7 +262,12 @@ FactoryBot.define do
|
|||
|
||||
trait :trace_live do
|
||||
after(:create) do |build, evaluator|
|
||||
Gitlab::ExclusiveLease.skipping_transaction_check { build.trace.set('BUILD TRACE') }
|
||||
Gitlab::ExclusiveLease.skipping_transaction_check do
|
||||
# We can skip calling `Ci::Build#hide_secrets` because this content is safe.
|
||||
# This allows not to call into potentialy unstubbed ApplicationSetting in specs.
|
||||
# For example: `ci_job_token_signing_key` when in `let_it_be` context.
|
||||
build.trace.send(:unsafe_set, 'BUILD TRACE')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ RSpec.describe 'Upload a nuget package', :api, :js, feature_category: :package_r
|
|||
|
||||
let(:api_path) { "/projects/#{project.id}/packages/nuget/" }
|
||||
let(:url) { capybara_url(api(api_path)) }
|
||||
let(:file) { File.open(expand_fixture_path('packages/nuget/package.nupkg')) }
|
||||
let(:file) { fixture_file_upload('spec/fixtures/packages/nuget/package.nupkg') }
|
||||
|
||||
subject do
|
||||
HTTParty.put(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import ModelVersionActionsDropdown from '~/ml/model_registry/components/model_ve
|
|||
import { createAlert } from '~/alert';
|
||||
import {
|
||||
modelVersionWithCandidateAndAuthor,
|
||||
modelVersionWithCandidateAndNullAuthor,
|
||||
deleteModelVersionResponses,
|
||||
} from '../graphql_mock_data';
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ describe('ModelVersionsTable', () => {
|
|||
deleteResolver = jest.fn().mockResolvedValue(deleteModelVersionResponses.success),
|
||||
canWriteModelRegistry = true,
|
||||
mountFn = mountExtended,
|
||||
tableItems = items,
|
||||
} = {}) => {
|
||||
const requestHandlers = [[deleteModelVersionMutation, deleteResolver]];
|
||||
apolloProvider = createMockApollo(requestHandlers);
|
||||
|
|
@ -45,7 +47,7 @@ describe('ModelVersionsTable', () => {
|
|||
canWriteModelRegistry,
|
||||
},
|
||||
propsData: {
|
||||
items,
|
||||
items: tableItems,
|
||||
canWriteModelRegistry,
|
||||
},
|
||||
apolloProvider,
|
||||
|
|
@ -108,6 +110,12 @@ describe('ModelVersionsTable', () => {
|
|||
expect(avatarLink.text()).toContain(items[0].author.name);
|
||||
});
|
||||
|
||||
it('renders the author information correctly for items with no author', () => {
|
||||
createWrapper({ tableItems: [modelVersionWithCandidateAndNullAuthor] });
|
||||
const avatarLink = findTableRows().at(0).findComponent(GlAvatarLink);
|
||||
expect(avatarLink.exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders actions dropdown if canWriteModelRegistry is true', () => {
|
||||
createWrapper({}, true);
|
||||
expect(findActionsDropdown().exists()).toBe(true);
|
||||
|
|
|
|||
|
|
@ -146,6 +146,11 @@ export const modelVersionWithCandidateAndAuthor = {
|
|||
},
|
||||
};
|
||||
|
||||
export const modelVersionWithCandidateAndNullAuthor = {
|
||||
...modelVersionWithCandidateAndAuthor,
|
||||
author: null,
|
||||
};
|
||||
|
||||
export const graphqlCandidates = [
|
||||
{
|
||||
id: 'gid://gitlab/Ml::Candidate/1',
|
||||
|
|
|
|||
|
|
@ -299,26 +299,6 @@ describe('Packages Settings', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('with packagesAllowDuplicateExceptions FF enabled', () => {
|
||||
it('renders ExceptionsInput and assigns duplication allowness and exception props', () => {
|
||||
mountComponent({
|
||||
mountFn: mountExtended,
|
||||
features: { packagesAllowDuplicateExceptions: true },
|
||||
});
|
||||
|
||||
const { nugetDuplicateExceptionRegex } = packageSettings;
|
||||
|
||||
expect(findNugetDuplicatedSettingsExceptionsInput().props()).toMatchObject({
|
||||
duplicatesAllowed: false,
|
||||
duplicateExceptionRegex: nugetDuplicateExceptionRegex,
|
||||
duplicateExceptionRegexError: '',
|
||||
loading: false,
|
||||
name: 'nugetDuplicateExceptionRegex',
|
||||
id: 'nuget-duplicated-settings-regex-input',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('on update event calls the mutation', () => {
|
||||
const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
|
||||
mountComponent({ mountFn: mountExtended, mutationResolver });
|
||||
|
|
|
|||
|
|
@ -1,25 +1,291 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
import updateDesignDescriptionMutation from '~/work_items/components/design_management/graphql/update_design_description.mutation.graphql';
|
||||
import DesignDescription from '~/work_items/components/design_management/design_preview/design_description.vue';
|
||||
import mockDesign from './mock_design';
|
||||
import { renderGFM } from '~/behaviors/markdown/render_gfm';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import { UPDATE_DESCRIPTION_ERROR } from '~/work_items/components/design_management/constants';
|
||||
import { mockUpdateDesignDescriptionResponse, designDescriptionFactory } from '../mock_data';
|
||||
|
||||
jest.mock('~/behaviors/markdown/render_gfm');
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('DesignDescription', () => {
|
||||
let wrapper;
|
||||
|
||||
function createComponent() {
|
||||
const mockDesignVariables = {
|
||||
fullPath: '',
|
||||
iid: '1',
|
||||
filenames: ['test.jpg'],
|
||||
atVersion: null,
|
||||
};
|
||||
const formFieldProps = {
|
||||
id: 'design-description',
|
||||
name: 'design-description',
|
||||
placeholder: 'Write a comment or drag your files here…',
|
||||
'aria-label': 'Design description',
|
||||
};
|
||||
const updatedDescription = 'New test description';
|
||||
|
||||
const mockDesign = designDescriptionFactory();
|
||||
|
||||
const findDesignContent = () => wrapper.findByTestId('design-description-content');
|
||||
const findEditDescriptionButton = () => wrapper.findByTestId('edit-description');
|
||||
const findSaveDescriptionButton = () => wrapper.findByTestId('save-description');
|
||||
const findCancelDescriptionButton = () => wrapper.findByTestId('cancel');
|
||||
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
|
||||
const findCheckboxAtIndex = (index) => wrapper.findAll('input[type="checkbox"]').at(index);
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findAddDesignDescriptionButton = () => wrapper.findByTestId('add-design-description');
|
||||
|
||||
const updateDescriptionMutationSuccessHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockUpdateDesignDescriptionResponse);
|
||||
const mockDesignUpdateResponseHandler = jest.fn().mockResolvedValue(
|
||||
mockUpdateDesignDescriptionResponse({
|
||||
description: updatedDescription,
|
||||
descriptionHtml: `<p data-sourcepos="1:1-1:16" dir="auto">${updatedDescription}</p>`,
|
||||
}),
|
||||
);
|
||||
const updateDescriptionMutationError = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error(UPDATE_DESCRIPTION_ERROR));
|
||||
|
||||
const createComponent = ({
|
||||
design = mockDesign,
|
||||
descriptionText = '',
|
||||
isSubmitting = false,
|
||||
designUpdateMutationHandler = updateDescriptionMutationSuccessHandler,
|
||||
} = {}) => {
|
||||
wrapper = shallowMountExtended(DesignDescription, {
|
||||
apolloProvider: createMockApollo([
|
||||
[updateDesignDescriptionMutation, designUpdateMutationHandler],
|
||||
]),
|
||||
propsData: {
|
||||
design: mockDesign,
|
||||
design,
|
||||
designVariables: mockDesignVariables,
|
||||
markdownPreviewPath: '/gitlab-org/gitlab-test/preview_markdown?target_type=Issue',
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formFieldProps,
|
||||
descriptionText,
|
||||
isSubmitting,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders design description', () => {
|
||||
expect(wrapper.findByTestId('design-description-content').text()).toBe(
|
||||
mockDesign.descriptionHtml,
|
||||
);
|
||||
expect(wrapper.findByTestId('design-description-content').text()).toBe(mockDesign.description);
|
||||
});
|
||||
|
||||
describe('user has no updateDesign permission', () => {
|
||||
it('renders description content without edit button', () => {
|
||||
createComponent({
|
||||
design: designDescriptionFactory({
|
||||
updateDesign: false,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(findDesignContent().text()).toEqual('Description test');
|
||||
expect(findEditDescriptionButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('user has updateDesign permission', () => {
|
||||
it('renders description content with the edit button', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findDesignContent().text()).toEqual('Description test');
|
||||
expect(findEditDescriptionButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders save button when editor is open', async () => {
|
||||
createComponent();
|
||||
|
||||
await findEditDescriptionButton().vm.$emit('click');
|
||||
|
||||
expect(findSaveDescriptionButton().exists()).toBe(true);
|
||||
expect(findSaveDescriptionButton().attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('renders add a description button when there is no description', () => {
|
||||
createComponent({
|
||||
design: designDescriptionFactory({
|
||||
description: '',
|
||||
descriptionHtml: '',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(findMarkdownEditor().exists()).toBe(false);
|
||||
expect(findAddDesignDescriptionButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders description form when add a description button is clicked', async () => {
|
||||
createComponent({
|
||||
design: designDescriptionFactory({
|
||||
description: '',
|
||||
descriptionHtml: '',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(findAddDesignDescriptionButton().exists()).toBe(true);
|
||||
expect(findMarkdownEditor().exists()).toBe(false);
|
||||
|
||||
await findAddDesignDescriptionButton().vm.$emit('click');
|
||||
|
||||
expect(findMarkdownEditor().exists()).toBe(true);
|
||||
expect(findAddDesignDescriptionButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('resets description text if empty when form is closed', async () => {
|
||||
createComponent();
|
||||
|
||||
await findEditDescriptionButton().vm.$emit('click');
|
||||
|
||||
findMarkdownEditor().vm.$emit('input', '');
|
||||
|
||||
await findCancelDescriptionButton().vm.$emit('click');
|
||||
|
||||
expect(findDesignContent().text()).toEqual('Description test');
|
||||
});
|
||||
|
||||
it('triggers mutation when form is submitted and hides the form', async () => {
|
||||
const trackingSpy = mockTracking(undefined, null, jest.spyOn);
|
||||
createComponent({
|
||||
designUpdateMutationHandler: mockDesignUpdateResponseHandler,
|
||||
});
|
||||
|
||||
await findEditDescriptionButton().vm.$emit('click');
|
||||
|
||||
findMarkdownEditor().vm.$emit('input', updatedDescription);
|
||||
findSaveDescriptionButton().vm.$emit('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(mockDesignUpdateResponseHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
description: updatedDescription,
|
||||
id: 'gid:/gitlab/Design/1',
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findMarkdownEditor().exists()).toBe(false);
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'save_markdown', {
|
||||
label: 'markdown_editor',
|
||||
property: 'Design',
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error message when mutation fails', async () => {
|
||||
createComponent({
|
||||
descriptionText: updatedDescription,
|
||||
designUpdateMutationHandler: updateDescriptionMutationError,
|
||||
});
|
||||
|
||||
await findEditDescriptionButton().vm.$emit('click');
|
||||
findMarkdownEditor().vm.$emit('input', updatedDescription);
|
||||
findSaveDescriptionButton().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
expect(findAlert().text()).toBe(UPDATE_DESCRIPTION_ERROR);
|
||||
});
|
||||
|
||||
describe('content has checkboxes', () => {
|
||||
const mockCheckboxDescription = '- [x] todo 1\n- [ ] todo 2';
|
||||
const mockCheckboxDescriptionHtml = `<ul dir="auto" class="task-list" data-sourcepos="1:1-4:0">
|
||||
<li class="task-list-item" data-sourcepos="1:1-2:15">
|
||||
<input checked="" class="task-list-item-checkbox" type="checkbox"> todo 1</li>
|
||||
<li class="task-list-item" data-sourcepos="2:1-2:15">
|
||||
<input class="task-list-item-checkbox" type="checkbox"> todo 2</li>
|
||||
</ul>`;
|
||||
const checkboxDesignDescription = designDescriptionFactory({
|
||||
updateDesign: true,
|
||||
description: mockCheckboxDescription,
|
||||
descriptionHtml: mockCheckboxDescriptionHtml,
|
||||
});
|
||||
const mockCheckedDescriptionUpdateResponseHandler = jest.fn().mockResolvedValue(
|
||||
mockUpdateDesignDescriptionResponse({
|
||||
description: '- [x] todo 1\n- [x] todo 2',
|
||||
descriptionHtml: `<ul dir="auto" class="task-list" data-sourcepos="1:1-4:0">
|
||||
<li class="task-list-item" data-sourcepos="1:1-2:15">
|
||||
<input checked="" class="task-list-item-checkbox" type="checkbox"> todo 1</li>
|
||||
<li class="task-list-item" data-sourcepos="2:1-2:15">
|
||||
<input class="task-list-item-checkbox" type="checkbox"> todo 2</li>
|
||||
</ul>`,
|
||||
}),
|
||||
);
|
||||
const mockUnCheckedDescriptionUpdateResponseHandler = jest.fn().mockResolvedValue(
|
||||
mockUpdateDesignDescriptionResponse({
|
||||
description: '- [ ] todo 1\n- [ ] todo 2',
|
||||
descriptionHtml: `<ul dir="auto" class="task-list" data-sourcepos="1:1-4:0">
|
||||
<li class="task-list-item" data-sourcepos="1:1-2:15">
|
||||
<input class="task-list-item-checkbox" type="checkbox"> todo 1</li>
|
||||
<li class="task-list-item" data-sourcepos="2:1-2:15">
|
||||
<input class="task-list-item-checkbox" type="checkbox"> todo 2</li>
|
||||
</ul>`,
|
||||
}),
|
||||
);
|
||||
|
||||
it.each`
|
||||
assertionName | mockUpdateCheckboxesResponseHandler | checkboxIndex | checked | expectedDesignDescription
|
||||
${'checked'} | ${mockCheckedDescriptionUpdateResponseHandler} | ${1} | ${true} | ${'- [x] todo 1\n- [x] todo 2'}
|
||||
${'unchecked'} | ${mockUnCheckedDescriptionUpdateResponseHandler} | ${0} | ${false} | ${'- [ ] todo 1\n- [ ] todo 2'}
|
||||
`(
|
||||
'updates the store object when checkbox is $assertionName',
|
||||
async ({
|
||||
mockUpdateCheckboxesResponseHandler,
|
||||
checkboxIndex,
|
||||
checked,
|
||||
expectedDesignDescription,
|
||||
}) => {
|
||||
createComponent({
|
||||
design: checkboxDesignDescription,
|
||||
descriptionText: mockCheckboxDescription,
|
||||
designUpdateMutationHandler: mockUpdateCheckboxesResponseHandler,
|
||||
});
|
||||
|
||||
findCheckboxAtIndex(checkboxIndex).setChecked(checked);
|
||||
|
||||
expect(mockUpdateCheckboxesResponseHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
description: expectedDesignDescription,
|
||||
id: 'gid:/gitlab/Design/1',
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(renderGFM).toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('disables checkbox while updating', () => {
|
||||
createComponent({
|
||||
design: checkboxDesignDescription,
|
||||
descriptionText: mockCheckboxDescription,
|
||||
});
|
||||
|
||||
findCheckboxAtIndex(1).setChecked();
|
||||
|
||||
expect(findCheckboxAtIndex(1).attributes().disabled).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,6 +40,13 @@ describe('DesignSidebar', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const mockDesignVariables = {
|
||||
fullPath: 'gitlab-org/gitlab-shell',
|
||||
iid: '1',
|
||||
filenames: ['image_name.png'],
|
||||
atVersion: null,
|
||||
};
|
||||
|
||||
function createComponent({ design = mockDesign, isLoading = false, isLoggedIn = true } = {}) {
|
||||
if (isLoggedIn) {
|
||||
window.gon.current_user_id = 1;
|
||||
|
|
@ -49,8 +56,10 @@ describe('DesignSidebar', () => {
|
|||
propsData: {
|
||||
design,
|
||||
isLoading,
|
||||
designVariables: mockDesignVariables,
|
||||
isOpen: true,
|
||||
resolvedDiscussionsExpanded: false,
|
||||
markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
|
||||
},
|
||||
mocks: {
|
||||
$route,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,32 @@ export const mockDesign = {
|
|||
nodes: [],
|
||||
__typename: 'DiscussionConnection',
|
||||
},
|
||||
issue: {
|
||||
id: 'gid:/gitlab/Issue/1',
|
||||
title: 'My precious issue',
|
||||
webPath: 'full-issue-path',
|
||||
webUrl: 'full-issue-url',
|
||||
participants: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'Administrator',
|
||||
id: 'gid://gitlab/User/1"',
|
||||
username: 'root',
|
||||
webUrl: 'link-to-author',
|
||||
webPath: '/root',
|
||||
avatarUrl: 'link-to-avatar',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
],
|
||||
__typename: 'UserCoreConnection',
|
||||
},
|
||||
userPermissions: {
|
||||
createDesign: true,
|
||||
updateDesign: true,
|
||||
__typename: 'IssuePermissions',
|
||||
},
|
||||
__typename: 'Issue',
|
||||
},
|
||||
__typename: 'Design',
|
||||
};
|
||||
|
||||
|
|
@ -84,6 +110,53 @@ export const designCollectionResponse = (mockDesigns = [mockDesign]) => ({
|
|||
},
|
||||
});
|
||||
|
||||
export const designDescriptionFactory = ({
|
||||
updateDesign = true,
|
||||
description = 'Description test',
|
||||
descriptionHtml = '<p data-sourcepos="1:1-1:16" dir="auto">Description test</p>',
|
||||
} = {}) => ({
|
||||
id: 'gid:/gitlab/Design/1',
|
||||
iid: 1,
|
||||
filename: 'test.jpg',
|
||||
fullPath: 'full-design-path',
|
||||
image: 'test.jpg',
|
||||
description,
|
||||
descriptionHtml,
|
||||
updatedAt: '01-01-2019',
|
||||
updatedBy: {
|
||||
name: 'test',
|
||||
},
|
||||
issue: {
|
||||
id: 'gid:/gitlab/Issue/1',
|
||||
title: 'My precious issue',
|
||||
webPath: 'full-issue-path',
|
||||
webUrl: 'full-issue-url',
|
||||
participants: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'link-to-author',
|
||||
avatarUrl: 'link-to-avatar',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
],
|
||||
__typename: 'UserCoreConnection',
|
||||
},
|
||||
userPermissions: {
|
||||
updateDesign,
|
||||
awardEmoji: true,
|
||||
__typename: 'IssuePermissions',
|
||||
},
|
||||
__typename: 'Issue',
|
||||
},
|
||||
discussions: {
|
||||
nodes: [],
|
||||
__typename: 'DiscussionConnection',
|
||||
},
|
||||
__typename: 'Design',
|
||||
});
|
||||
|
||||
export const allDesignsArchivedResponse = () => ({
|
||||
data: {
|
||||
workItem: {
|
||||
|
|
@ -134,6 +207,18 @@ export const getDesignResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const mockUpdateDesignDescriptionResponse = (options) => {
|
||||
return {
|
||||
data: {
|
||||
designManagementUpdate: {
|
||||
errors: [],
|
||||
design: designDescriptionFactory(options),
|
||||
},
|
||||
__typename: 'DesignManagementUpdatePayload',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const mockArchiveDesignMutationResponse = {
|
||||
data: {
|
||||
designManagementDelete: {
|
||||
|
|
|
|||
|
|
@ -458,6 +458,61 @@ RSpec.describe BulkImports::NdjsonPipeline, feature_category: :importers do
|
|||
subject.load(nil, [merge_request, original_users_map])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an exception is raised when saving a nested object' do
|
||||
it 'still pushes a placeholder reference for the persisted objects' do
|
||||
merge_request = build(:merge_request,
|
||||
source_project: project,
|
||||
target_project: project,
|
||||
author: source_user_1.mapped_user
|
||||
)
|
||||
|
||||
note = build(:note,
|
||||
project: project,
|
||||
author: source_user_1.mapped_user
|
||||
)
|
||||
|
||||
diff_note = build(:diff_note_on_merge_request,
|
||||
project: project,
|
||||
author: source_user_1.mapped_user
|
||||
)
|
||||
allow(diff_note).to receive(:save).and_raise(DiffNote::NoteDiffFileCreationError)
|
||||
|
||||
merge_request.notes << note
|
||||
merge_request.notes << diff_note
|
||||
|
||||
original_users_map = {}.compare_by_identity
|
||||
original_users_map[merge_request] = {
|
||||
'author_id' => source_user_1.source_user_identifier
|
||||
}
|
||||
original_users_map[note] = {
|
||||
'author_id' => source_user_1.source_user_identifier
|
||||
}
|
||||
original_users_map[diff_note] = {
|
||||
'author_id' => source_user_1.source_user_identifier
|
||||
}
|
||||
|
||||
expect(Import::PlaceholderReferences::PushService).to receive(:from_record).with(
|
||||
import_source: ::Import::SOURCE_DIRECT_TRANSFER,
|
||||
import_uid: context.bulk_import_id,
|
||||
record: merge_request,
|
||||
user_reference_column: :author_id,
|
||||
source_user: source_user_1
|
||||
).and_call_original
|
||||
|
||||
expect(Import::PlaceholderReferences::PushService).to receive(:from_record).with(
|
||||
import_source: ::Import::SOURCE_DIRECT_TRANSFER,
|
||||
import_uid: context.bulk_import_id,
|
||||
record: note,
|
||||
user_reference_column: :author_id,
|
||||
source_user: source_user_1
|
||||
).and_call_original
|
||||
|
||||
expect { subject.load(nil, [merge_request, original_users_map]) }.to raise_error(
|
||||
DiffNote::NoteDiffFileCreationError
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ RSpec.describe ::Ci::JobToken::Jwt::Decode, feature_category: :continuous_integr
|
|||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(ci_job_token_jwt: true)
|
||||
allow(::Ci::JobToken::Jwt::Encode).to receive(:key).and_return(key)
|
||||
allow(described_class).to receive(:key).and_return(key)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1043,10 +1043,6 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
|
|||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(ci_job_token_jwt: true)
|
||||
end
|
||||
|
||||
context 'for running build' do
|
||||
let!(:build) { create(:ci_build, :running, user: user) }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillDetectedAtFromCreatedAtColumn, :migration, schema: 20241021063020, feature_category: :vulnerability_management do
|
||||
let(:migration) do
|
||||
described_class.new(
|
||||
start_id: vulnerability_without_detected_at.id,
|
||||
end_id: vulnerability_without_detected_at.id,
|
||||
batch_table: :vulnerabilities,
|
||||
batch_column: batch_column,
|
||||
sub_batch_size: sub_batch_size,
|
||||
pause_ms: pause_ms,
|
||||
connection: ApplicationRecord.connection
|
||||
)
|
||||
end
|
||||
|
||||
let(:batch_column) { :id }
|
||||
let(:sub_batch_size) { 1_000 }
|
||||
let(:pause_ms) { 0 }
|
||||
|
||||
let(:projects_table) { table(:projects) }
|
||||
let(:namespaces_table) { table(:namespaces) }
|
||||
let(:vulnerabilities_table) { table(:vulnerabilities) }
|
||||
let(:vulnerability_identifiers_table) { table(:vulnerability_identifiers) }
|
||||
let(:vulnerability_occurrences_table) { table(:vulnerability_occurrences) }
|
||||
let(:scanners_table) { table(:vulnerability_scanners) }
|
||||
|
||||
let(:namespace) { namespaces_table.create!(name: 'test', path: 'test') }
|
||||
let(:project) do
|
||||
projects_table.create!(
|
||||
namespace_id: namespace.id,
|
||||
name: 'test',
|
||||
path: 'test',
|
||||
project_namespace_id: namespace.id
|
||||
)
|
||||
end
|
||||
|
||||
# Add a scanner (required for vulnerability_occurrences)
|
||||
let(:scanner) do
|
||||
scanners_table.create!(
|
||||
project_id: project.id,
|
||||
external_id: 'test_scanner',
|
||||
name: 'test_scanner'
|
||||
)
|
||||
end
|
||||
|
||||
# Create vulnerability identifier (required for vulnerability_occurrences)
|
||||
let(:identifier) do
|
||||
vulnerability_identifiers_table.create!(
|
||||
project_id: project.id,
|
||||
fingerprint: 'test-identifier',
|
||||
external_type: 'test_type',
|
||||
external_id: 'test_id',
|
||||
name: 'test_name'
|
||||
)
|
||||
end
|
||||
|
||||
# Create vulnerability occurrences (findings)
|
||||
let!(:finding1) do
|
||||
vulnerability_occurrences_table.create!(
|
||||
project_id: project.id,
|
||||
scanner_id: scanner.id,
|
||||
primary_identifier_id: identifier.id,
|
||||
project_fingerprint: SecureRandom.hex(20),
|
||||
location_fingerprint: SecureRandom.hex(20),
|
||||
name: 'Test finding 1',
|
||||
severity: 5,
|
||||
confidence: 5,
|
||||
report_type: 1,
|
||||
uuid: SecureRandom.uuid,
|
||||
created_at: 2.days.ago,
|
||||
metadata_version: 'something',
|
||||
raw_metadata: {}
|
||||
)
|
||||
end
|
||||
|
||||
let!(:finding2) do
|
||||
vulnerability_occurrences_table.create!(
|
||||
project_id: project.id,
|
||||
scanner_id: scanner.id,
|
||||
primary_identifier_id: identifier.id,
|
||||
project_fingerprint: SecureRandom.hex(20),
|
||||
location_fingerprint: SecureRandom.hex(20),
|
||||
name: 'Test finding 2',
|
||||
severity: 5,
|
||||
confidence: 5,
|
||||
report_type: 1,
|
||||
uuid: SecureRandom.uuid,
|
||||
created_at: 4.days.ago,
|
||||
metadata_version: 'something',
|
||||
raw_metadata: {}
|
||||
)
|
||||
end
|
||||
|
||||
let!(:vulnerability_without_detected_at) do
|
||||
vulnerabilities_table.create!(
|
||||
project_id: project.id,
|
||||
author_id: 1,
|
||||
title: 'Test vulnerability 1',
|
||||
severity: 5,
|
||||
confidence: 5,
|
||||
report_type: 1,
|
||||
state: 1,
|
||||
finding_id: finding1.id,
|
||||
created_at: 2.days.ago
|
||||
)
|
||||
end
|
||||
|
||||
let!(:vulnerability_with_detected_at) do
|
||||
vulnerabilities_table.create!(
|
||||
project_id: project.id,
|
||||
author_id: 1,
|
||||
title: 'Test vulnerability 2',
|
||||
severity: 5,
|
||||
confidence: 5,
|
||||
report_type: 1,
|
||||
state: 1,
|
||||
finding_id: finding2.id,
|
||||
created_at: 4.days.ago,
|
||||
detected_at: 3.days.ago
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
# detected_at default value is NOW(), so update it to NULL
|
||||
vulnerability_without_detected_at.update_column :detected_at, nil
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'backfills detected_at with created_at for vulnerabilities with nil detected_at' do
|
||||
expect(vulnerabilities_table.where(detected_at: nil).count).to eq 1
|
||||
|
||||
migration.perform
|
||||
|
||||
expect(vulnerabilities_table.where(detected_at: nil).count).to eq 0
|
||||
end
|
||||
|
||||
it 'does not modify vulnerabilities that already have detected_at' do
|
||||
expect { migration.perform }.not_to change {
|
||||
vulnerabilities_table.find(vulnerability_with_detected_at.id).detected_at
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -177,15 +177,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes, feature_category
|
|||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
|
||||
allow(context).to receive(:variables_hash).and_return(variables_hash)
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -293,15 +284,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes, feature_category
|
|||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
|
||||
allow(context).to receive(:variables_hash).and_return(variables_hash)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'when the variable is nested' do
|
||||
let(:context) { instance_double(Gitlab::Ci::Build::Context::Base) }
|
||||
let(:variables_hash) do
|
||||
|
|
@ -322,19 +304,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes, feature_category
|
|||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
|
||||
allow(context).to receive(:variables_hash).and_return(variables_hash)
|
||||
end
|
||||
|
||||
it 'raises ParseError' do
|
||||
expect { satisfied_by }.to raise_error(
|
||||
::Gitlab::Ci::Build::Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -72,14 +72,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
|
|||
let(:globs) { ['$NESTED_FULL_PATH_VALID'] }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -130,14 +122,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
|
|||
let(:globs) { ['$FILE_TXT'] }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project path is invalid' do
|
||||
|
|
@ -159,19 +143,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
|
|||
"rules:exists:project `invalid/path/subdir` is not a valid project path"
|
||||
)
|
||||
end
|
||||
|
||||
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { satisfied_by? }.to raise_error(
|
||||
Gitlab::Ci::Build::Rules::Rule::Clause::ParseError,
|
||||
"rules:exists:project `invalid/path/subdir` is not a valid project path"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project path contains a masked variable' do
|
||||
|
|
@ -202,14 +173,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
|
|||
let(:ref) { '$NEW_BRANCH' }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the ref is invalid' do
|
||||
|
|
@ -232,20 +195,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
|
|||
"in project `#{other_project.full_path}`"
|
||||
)
|
||||
end
|
||||
|
||||
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { satisfied_by? }.to raise_error(
|
||||
Gitlab::Ci::Build::Rules::Rule::Clause::ParseError,
|
||||
"rules:exists:ref `invalid/ref/new_branch` is not a valid ref " \
|
||||
"in project `#{other_project.full_path}`"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the ref contains a masked variable' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe QueueBackfillDetectedAtFromCreatedAtColumn, migration: :gitlab_sec, feature_category: :vulnerability_management do
|
||||
let!(:batched_migration) { described_class::MIGRATION }
|
||||
|
||||
it 'schedules a new batched migration' do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).to have_scheduled_batched_migration(
|
||||
gitlab_schema: :gitlab_sec,
|
||||
table_name: :vulnerabilities,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6125,7 +6125,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
|
|||
let(:token) { 'my-token' }
|
||||
|
||||
before do
|
||||
stub_feature_flags(ci_job_token_jwt: true)
|
||||
allow_next_instance_of(::Ci::JobToken::Jwt::Encode, build) do |encode|
|
||||
allow(encode).to receive(:jwt).and_return(token)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ RSpec.describe UserDetail, feature_category: :system_access do
|
|||
let(:registration_type) { 'free' }
|
||||
let(:glm_source) { 'glm_source' }
|
||||
let(:glm_content) { 'glm_content' }
|
||||
let(:joining_project) { true }
|
||||
let(:onboarding_status) do
|
||||
{
|
||||
step_url: step_url,
|
||||
|
|
@ -25,7 +26,8 @@ RSpec.describe UserDetail, feature_category: :system_access do
|
|||
initial_registration_type: registration_type,
|
||||
registration_type: registration_type,
|
||||
glm_source: glm_source,
|
||||
glm_content: glm_content
|
||||
glm_content: glm_content,
|
||||
joining_project: joining_project
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -127,6 +129,22 @@ RSpec.describe UserDetail, feature_category: :system_access do
|
|||
end
|
||||
end
|
||||
|
||||
context 'for joining_project' do
|
||||
let(:onboarding_status) do
|
||||
{
|
||||
joining_project: joining_project
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to allow_value(onboarding_status).for(:onboarding_status) }
|
||||
|
||||
context "when 'joining_project' is invalid" do
|
||||
let(:joining_project) { 'true' }
|
||||
|
||||
it { is_expected.not_to allow_value(onboarding_status).for(:onboarding_status) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no data' do
|
||||
let(:onboarding_status) { {} }
|
||||
|
||||
|
|
|
|||
|
|
@ -516,4 +516,79 @@ RSpec.describe API::API, feature_category: :system_access do
|
|||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'audit logging of requests with a specific token scope' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:token) { create(:oauth_access_token, user: user, scopes: [:ai_workflows]) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
let_it_be(:path) { "/projects/#{issue.project.id}/issues/#{issue.iid}" }
|
||||
|
||||
before_all do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
shared_examples 'audited request' do
|
||||
it 'adds audit log' do
|
||||
expect(::Gitlab::Audit::Auditor).to receive(:audit).with(hash_including({
|
||||
name: 'api_request_access_with_scope',
|
||||
message: "API request with token scopes [:ai_workflows] - GET /api/v4#{path}"
|
||||
})).and_call_original
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(status)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'not audited request' do
|
||||
it "doesn't add audit log" do
|
||||
expect(::Gitlab::Audit::Auditor).not_to receive(:audit)
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when endpoint allows token with ai_workflow scope' do
|
||||
subject { get api(path, oauth_access_token: token) }
|
||||
|
||||
context 'when token with ai_workflows scope is used' do
|
||||
let(:status) { :ok }
|
||||
|
||||
it_behaves_like 'audited request'
|
||||
|
||||
context 'when request fails' do
|
||||
let_it_be(:path) { "/projects/#{issue.project.id}/issues/#{non_existing_record_id}" }
|
||||
let(:status) { :not_found }
|
||||
|
||||
it_behaves_like 'audited request'
|
||||
end
|
||||
|
||||
context 'when api_audit_requests_with_scope flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(api_audit_requests_with_scope: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'not audited request'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when token with ai_workflows scope is not used' do
|
||||
let_it_be(:token) { create(:oauth_access_token, user: user, scopes: [:api]) }
|
||||
let(:status) { :ok }
|
||||
|
||||
it_behaves_like 'not audited request'
|
||||
end
|
||||
end
|
||||
|
||||
context "when endpoint doesn't allow token with ai_workflow scope" do
|
||||
subject { delete api(path, oauth_access_token: token) }
|
||||
|
||||
let(:status) { :forbidden }
|
||||
|
||||
it_behaves_like 'not audited request'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -83,6 +83,23 @@ RSpec.describe API::Groups, :with_current_organization, feature_category: :group
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'includes statistics when all_available is false' do
|
||||
let(:params) { { all_available: false, statistics: true } }
|
||||
|
||||
before do
|
||||
group1.add_developer(admin)
|
||||
create(:group, parent: group1)
|
||||
end
|
||||
|
||||
it 'returns the statistics', :aggregate_failures do
|
||||
get api(api_endpoint, admin, admin_mode: true), params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.first).to include('statistics')
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /groups" do
|
||||
shared_examples 'groups list N+1' do
|
||||
it 'avoids N+1 queries', :use_sql_query_cache do
|
||||
|
|
@ -351,6 +368,10 @@ RSpec.describe API::Groups, :with_current_organization, feature_category: :group
|
|||
|
||||
expect(json_response[0]["statistics"].keys).to match_array(stat_keys)
|
||||
end
|
||||
|
||||
it_behaves_like 'includes statistics when all_available is false' do
|
||||
let(:api_endpoint) { "/groups" }
|
||||
end
|
||||
end
|
||||
|
||||
context "when using skip_groups in request" do
|
||||
|
|
@ -2507,6 +2528,10 @@ RSpec.describe API::Groups, :with_current_organization, feature_category: :group
|
|||
expect(json_response).to be_an Array
|
||||
expect(json_response.first).to include('statistics')
|
||||
end
|
||||
|
||||
it_behaves_like 'includes statistics when all_available is false' do
|
||||
let(:api_endpoint) { "/groups/#{group1.id}/subgroups" }
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'skips searching in full path' do
|
||||
|
|
@ -2646,6 +2671,10 @@ RSpec.describe API::Groups, :with_current_organization, feature_category: :group
|
|||
expect(json_response).to be_an Array
|
||||
expect(json_response.first).to include('statistics')
|
||||
end
|
||||
|
||||
it_behaves_like 'includes statistics when all_available is false' do
|
||||
let(:api_endpoint) { "/groups/#{group1.id}/descendant_groups" }
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'skips searching in full path' do
|
||||
|
|
|
|||
|
|
@ -249,17 +249,6 @@ RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_compositio
|
|||
expect(pipeline).to be_persisted
|
||||
expect(build_names).to contain_exactly('job1', 'job2', 'job4')
|
||||
end
|
||||
|
||||
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
|
||||
end
|
||||
|
||||
it 'creates all relevant jobs' do
|
||||
expect(pipeline).to be_persisted
|
||||
expect(build_names).to contain_exactly('job1', 'job2')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -906,16 +895,6 @@ RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_compositio
|
|||
it 'creates both jobs' do
|
||||
expect(build_names).to contain_exactly('job1', 'job2')
|
||||
end
|
||||
|
||||
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
|
||||
end
|
||||
|
||||
it 'does not create job1' do
|
||||
expect(build_names).to contain_exactly('job2')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ RSpec.describe Packages::Nuget::CheckDuplicatesService, feature_category: :packa
|
|||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:file_name) { 'package.nupkg' }
|
||||
let_it_be_with_reload(:package_settings) { create(:namespace_package_setting, :group, namespace: project.namespace) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
|
|
@ -44,57 +43,13 @@ RSpec.describe Packages::Nuget::CheckDuplicatesService, feature_category: :packa
|
|||
|
||||
context 'with nuget_duplicate_exception_regex' do
|
||||
before do
|
||||
package_settings.update!(nuget_duplicate_exception_regex: ".*#{existing_package.name.last(3)}.*")
|
||||
package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*")
|
||||
end
|
||||
|
||||
it_behaves_like 'returning success'
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'when package file is in object storage' do
|
||||
let(:params) { super().merge(remote_url: 'https://example.com') }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(::Packages::Nuget::ExtractRemoteMetadataFileService) do |instance|
|
||||
allow(instance).to receive(:execute)
|
||||
.and_return(ServiceResponse.success(payload: Nokogiri::XML::Document.new))
|
||||
end
|
||||
allow_next_instance_of(::Packages::Nuget::ExtractMetadataContentService) do |instance|
|
||||
allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata))
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'handling duplicates disallowed when package exists'
|
||||
|
||||
context 'when ExtractRemoteMetadataFileService raises ExtractionError' do
|
||||
before do
|
||||
allow_next_instance_of(::Packages::Nuget::ExtractRemoteMetadataFileService) do |instance|
|
||||
allow(instance).to receive(:execute).and_raise(
|
||||
::Packages::Nuget::ExtractRemoteMetadataFileService::ExtractionError, 'nuspec file not found'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'returning error', reason: :bad_request, message: 'nuspec file not found'
|
||||
end
|
||||
|
||||
context 'when version is normalized' do
|
||||
let(:metadata) { super().merge(package_version: '1.7.15') }
|
||||
|
||||
it_behaves_like 'handling duplicates disallowed when package exists'
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'when package file is on disk' do
|
||||
before do
|
||||
allow_next_instance_of(::Packages::Nuget::MetadataExtractionService) do |instance|
|
||||
allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata))
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'handling duplicates disallowed when package exists'
|
||||
end
|
||||
|
||||
context 'with existing package' do
|
||||
let_it_be(:existing_package) { create(:nuget_package, :with_metadatum, project: project, version: '1.7.15.0') }
|
||||
let_it_be(:metadata) do
|
||||
|
|
@ -106,67 +61,63 @@ RSpec.describe Packages::Nuget::CheckDuplicatesService, feature_category: :packa
|
|||
}
|
||||
end
|
||||
|
||||
before do
|
||||
allow_next_instance_of(::Packages::Nuget::MetadataExtractionService) do |instance|
|
||||
allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when nuget duplicates are allowed' do
|
||||
before do
|
||||
package_settings.update!(nuget_duplicates_allowed: true)
|
||||
allow_next_instance_of(Namespace::PackageSetting) do |instance|
|
||||
allow(instance).to receive(:nuget_duplicates_allowed?).and_return(true)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'returning success'
|
||||
|
||||
context 'when the package name matches the exception regex' do
|
||||
before do
|
||||
package_settings.update!(nuget_duplicate_exception_regex: existing_package.name)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning error', reason: :conflict,
|
||||
message: 'A package with the same name and version already exists'
|
||||
end
|
||||
|
||||
context 'when the package version matches the exception regex' do
|
||||
before do
|
||||
package_settings.update!(nuget_duplicate_exception_regex: existing_package.version)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning error', reason: :conflict,
|
||||
message: 'A package with the same name and version already exists'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when nuget duplicates are not allowed' do
|
||||
before do
|
||||
package_settings.update!(nuget_duplicates_allowed: false)
|
||||
let!(:package_settings) do
|
||||
create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'when package file is in object storage'
|
||||
it_behaves_like 'when package file is on disk'
|
||||
end
|
||||
context 'when package file is in object storage' do
|
||||
let(:params) { super().merge(remote_url: 'https://example.com') }
|
||||
|
||||
context 'when packages_allow_duplicate_exceptions is disabled' do
|
||||
before do
|
||||
stub_feature_flags(packages_allow_duplicate_exceptions: false)
|
||||
end
|
||||
|
||||
context 'when nuget duplicates are allowed' do
|
||||
before do
|
||||
package_settings.update!(nuget_duplicates_allowed: true)
|
||||
allow_next_instance_of(::Packages::Nuget::ExtractRemoteMetadataFileService) do |instance|
|
||||
allow(instance).to receive(:execute)
|
||||
.and_return(ServiceResponse.success(payload: Nokogiri::XML::Document.new))
|
||||
end
|
||||
allow_next_instance_of(::Packages::Nuget::ExtractMetadataContentService) do |instance|
|
||||
allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata))
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'returning success'
|
||||
end
|
||||
it_behaves_like 'handling duplicates disallowed when package exists'
|
||||
|
||||
context 'when nuget duplicates are not allowed' do
|
||||
before do
|
||||
package_settings.update!(nuget_duplicates_allowed: false)
|
||||
context 'when ExtractRemoteMetadataFileService raises ExtractionError' do
|
||||
before do
|
||||
allow_next_instance_of(::Packages::Nuget::ExtractRemoteMetadataFileService) do |instance|
|
||||
allow(instance).to receive(:execute).and_raise(
|
||||
::Packages::Nuget::ExtractRemoteMetadataFileService::ExtractionError, 'nuspec file not found'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'returning error', reason: :bad_request, message: 'nuspec file not found'
|
||||
end
|
||||
|
||||
it_behaves_like 'when package file is in object storage'
|
||||
it_behaves_like 'when package file is on disk'
|
||||
context 'when version is normalized' do
|
||||
let(:metadata) { super().merge(package_version: '1.7.15') }
|
||||
|
||||
it_behaves_like 'handling duplicates disallowed when package exists'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when package file is on disk' do
|
||||
before do
|
||||
allow_next_instance_of(::Packages::Nuget::MetadataExtractionService) do |instance|
|
||||
allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata))
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'handling duplicates disallowed when package exists'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -183,42 +134,20 @@ RSpec.describe Packages::Nuget::CheckDuplicatesService, feature_category: :packa
|
|||
end
|
||||
|
||||
context 'when nuget duplicates are allowed' do
|
||||
before do
|
||||
package_settings.nuget_duplicates_allowed = true
|
||||
let_it_be(:package_settings) do
|
||||
create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning success'
|
||||
end
|
||||
|
||||
context 'when nuget duplicates are not allowed' do
|
||||
before do
|
||||
package_settings.nuget_duplicates_allowed = false
|
||||
let_it_be(:package_settings) do
|
||||
create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning success'
|
||||
end
|
||||
|
||||
context 'when packages_allow_duplicate_exceptions is disabled' do
|
||||
before do
|
||||
stub_feature_flags(packages_allow_duplicate_exceptions: false)
|
||||
end
|
||||
|
||||
context 'when nuget duplicates are allowed' do
|
||||
before do
|
||||
package_settings.nuget_duplicates_allowed = true
|
||||
end
|
||||
|
||||
it_behaves_like 'returning success'
|
||||
end
|
||||
|
||||
context 'when nuget duplicates are not allowed' do
|
||||
before do
|
||||
package_settings.nuget_duplicates_allowed = false
|
||||
end
|
||||
|
||||
it_behaves_like 'returning success'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -356,10 +356,6 @@ RSpec.configure do |config|
|
|||
# Our test suite is setup to test plain text editor by default with separate tests just
|
||||
# for the rich text editor. Switch the flag off to continue testing the same way as before
|
||||
stub_feature_flags(rich_text_editor_as_default: false)
|
||||
|
||||
# TODO: temporary measure to unblock master
|
||||
# issue: https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/issues/9437
|
||||
stub_feature_flags(ci_job_token_jwt: false)
|
||||
else
|
||||
unstub_all_feature_flags
|
||||
end
|
||||
|
|
|
|||
|
|
@ -652,12 +652,6 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
|
|||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow_next_instance_of(::Packages::Nuget::CheckDuplicatesService) do |instance|
|
||||
allow(instance).to receive(:execute).and_return(ServiceResponse.success)
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.to have_request_urgency(:low) }
|
||||
|
||||
context 'with valid project' do
|
||||
|
|
@ -764,86 +758,30 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
|
|||
it_behaves_like 'returning response status', :forbidden
|
||||
end
|
||||
|
||||
context 'when handling duplicates' do
|
||||
context 'when package duplicates are not allowed' do
|
||||
let(:params) { { package: temp_file(file_name, content: File.open(expand_fixture_path('packages/nuget/package.nupkg'))) } }
|
||||
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
|
||||
let_it_be(:existing_package) { create(:nuget_package, project: project) }
|
||||
let_it_be(:metadata) { { package_name: existing_package.name, package_version: existing_package.version } }
|
||||
let_it_be(:package_settings) do
|
||||
create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: false)
|
||||
end
|
||||
|
||||
before do
|
||||
allow_next_instance_of(::Packages::Nuget::MetadataExtractionService) do |instance|
|
||||
allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata))
|
||||
end
|
||||
allow_next_instance_of(::Packages::Nuget::CheckDuplicatesService) do |instance|
|
||||
allow(instance).to receive(:execute).and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
context 'when package duplicates are not allowed' do
|
||||
let_it_be(:package_settings) do
|
||||
create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: false)
|
||||
end
|
||||
it_behaves_like 'returning response status', :conflict unless symbol_package
|
||||
it_behaves_like 'returning response status', :created if symbol_package
|
||||
|
||||
it_behaves_like 'returning response status', :conflict unless symbol_package
|
||||
it_behaves_like 'returning response status', :created if symbol_package
|
||||
|
||||
context 'when exception_regex is set' do
|
||||
before do
|
||||
package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*")
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', :created
|
||||
end
|
||||
|
||||
context 'with packages_allow_duplicate_exceptions disabled' do
|
||||
before do
|
||||
stub_feature_flags(packages_allow_duplicate_exceptions: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', :conflict unless symbol_package
|
||||
it_behaves_like 'returning response status', :created if symbol_package
|
||||
|
||||
context 'when exception_regex is set' do
|
||||
before do
|
||||
package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*")
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', :created
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when package duplicates are allowed' do
|
||||
let_it_be(:package_settings) do
|
||||
create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: true)
|
||||
context 'when exception_regex is set' do
|
||||
before do
|
||||
package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*")
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', :created
|
||||
|
||||
context 'when exception_regex is set' do
|
||||
before do
|
||||
package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*")
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', :conflict unless symbol_package
|
||||
it_behaves_like 'returning response status', :created if symbol_package
|
||||
end
|
||||
|
||||
context 'with packages_allow_duplicate_exceptions disabled' do
|
||||
before do
|
||||
stub_feature_flags(packages_allow_duplicate_exceptions: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', :created
|
||||
|
||||
context 'when exception_regex is set' do
|
||||
before do
|
||||
package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*")
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', :created
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue