diff options
89 files changed, 1186 insertions, 893 deletions
diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml index 9a9e918e83d..72f49123190 100644 --- a/.gitlab/ci/qa-common/main.gitlab-ci.yml +++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml @@ -6,7 +6,7 @@ workflow: include: - project: gitlab-org/quality/pipeline-common - ref: 6.3.0 + ref: 7.1.0 file: - /ci/base.gitlab-ci.yml - /ci/allure-report.yml diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml index 4e220f22800..fc48a6f92ec 100644 --- a/.rubocop_todo/gitlab/strong_memoize_attr.yml +++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml @@ -235,8 +235,6 @@ Gitlab/StrongMemoizeAttr: - 'app/services/packages/npm/create_tag_service.rb' - 'app/services/packages/nuget/metadata_extraction_service.rb' - 'app/services/packages/nuget/search_service.rb' - - 'app/services/packages/nuget/sync_metadatum_service.rb' - - 'app/services/packages/nuget/update_package_from_metadata_service.rb' - 'app/services/packages/pypi/create_package_service.rb' - 'app/services/packages/rpm/parse_package_service.rb' - 'app/services/packages/rubygems/dependency_resolver_service.rb' diff --git a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue index 4ec41381045..f0a41a5949e 100644 --- a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue +++ b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue @@ -60,7 +60,7 @@ export default { <hr aria-hidden="true" /> - <h2 class="gl-font-weight-normal gl-font-lg gl-my-5"> + <h2 class="gl-font-size-h2 gl-my-5"> {{ s__('Runners|Platform') }} </h2> <runner-platforms-radio-group v-model="platform" /> diff --git a/app/assets/javascripts/ci/runner/components/runner_create_form.vue b/app/assets/javascripts/ci/runner/components/runner_create_form.vue index d6db15a1996..040e42fa938 100644 --- a/app/assets/javascripts/ci/runner/components/runner_create_form.vue +++ b/app/assets/javascripts/ci/runner/components/runner_create_form.vue @@ -83,13 +83,13 @@ export default { if (errors?.length) { this.$emit('error', new Error(errors.join(' '))); + this.saving = false; } else { this.onSuccess(runner); } } catch (error) { captureException({ error, component: this.$options.name }); this.$emit('error', error); - } finally { this.saving = false; } }, @@ -103,7 +103,7 @@ export default { <gl-form @submit.prevent="onSubmit"> <runner-form-fields v-model="runner" /> - <div class="gl-display-flex"> + <div class="gl-display-flex gl-mt-6"> <gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="saving"> {{ s__('Runners|Create runner') }} </gl-button> diff --git a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue index 880d217e344..180c41e7ed6 100644 --- a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue +++ b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue @@ -1,5 +1,14 @@ <script> -import { GlFormGroup, GlFormCheckbox, GlFormInput, GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; +import { isEqual } from 'lodash'; +import { + GlFormGroup, + GlFormCheckbox, + GlFormInput, + GlIcon, + GlLink, + GlSprintf, + GlSkeletonLoader, +} from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants'; @@ -12,6 +21,7 @@ export default { GlIcon, GlLink, GlSprintf, + GlSkeletonLoader, RunnerMaintenanceNoteField: () => import('ee_component/ci/runner/components/runner_maintenance_note_field.vue'), }, @@ -21,12 +31,15 @@ export default { default: null, required: false, }, + loading: { + type: Boolean, + default: false, + required: false, + }, }, data() { return { - model: { - ...this.value, - }, + model: null, }; }, computed: { @@ -35,6 +48,15 @@ export default { }, }, watch: { + value: { + handler(newVal, oldVal) { + // update only when values change, avoids infinite loop + if (!isEqual(newVal, oldVal)) { + this.model = { ...newVal }; + } + }, + immediate: true, + }, model: { handler() { this.$emit('input', this.model); @@ -51,107 +73,115 @@ export default { </script> <template> <div> - <h2 class="gl-font-weight-normal gl-font-lg gl-my-5"> + <h2 class="gl-font-size-h2 gl-my-5"> {{ s__('Runners|Details') }} {{ __('(optional)') }} </h2> - <gl-form-group :label="s__('Runners|Runner description')" label-for="runner-description"> - <gl-form-input id="runner-description" v-model="model.description" name="description" /> - </gl-form-group> - <runner-maintenance-note-field v-model="model.maintenanceNote" class="gl-mt-5" /> + <gl-skeleton-loader v-if="loading" :lines="9" /> + <template v-else-if="model"> + <gl-form-group :label="s__('Runners|Runner description')" label-for="runner-description"> + <gl-form-input id="runner-description" v-model="model.description" name="description" /> + </gl-form-group> + <runner-maintenance-note-field v-model="model.maintenanceNote" class="gl-mt-5" /> + </template> <hr aria-hidden="true" /> - <h2 class="gl-font-weight-normal gl-font-lg gl-my-5"> + <h2 class="gl-font-size-h2 gl-my-5"> {{ s__('Runners|Configuration') }} {{ __('(optional)') }} </h2> - <div class="gl-mb-5"> - <gl-form-checkbox v-model="model.paused" name="paused"> - {{ __('Paused') }} - <template #help> - {{ s__('Runners|Stop the runner from accepting new jobs.') }} - </template> - </gl-form-checkbox> - - <gl-form-checkbox - v-model="model.accessLevel" - name="protected" - :value="$options.ACCESS_LEVEL_REF_PROTECTED" - :unchecked-value="$options.ACCESS_LEVEL_NOT_PROTECTED" - > - {{ __('Protected') }} - <template #help> - {{ s__('Runners|Use the runner on pipelines for protected branches only.') }} - </template> - </gl-form-checkbox> - - <gl-form-checkbox v-model="model.runUntagged" name="run-untagged"> - {{ __('Run untagged jobs') }} - <template #help> - {{ s__('Runners|Use the runner for jobs without tags in addition to tagged jobs.') }} - </template> - </gl-form-checkbox> - - <gl-form-checkbox v-if="canBeLockedToProject" v-model="model.locked" name="locked"> - {{ __('Lock to current projects') }} <gl-icon name="lock" /> - <template #help> - {{ - s__( - 'Runners|Use the runner for the currently assigned projects only. Only administrators can change the assigned projects.', - ) - }} - </template> - </gl-form-checkbox> - </div> + <gl-skeleton-loader v-if="loading" :lines="27" /> + <template v-else-if="model"> + <div class="gl-mb-5"> + <gl-form-checkbox v-model="model.paused" name="paused"> + {{ __('Paused') }} + <template #help> + {{ s__('Runners|Stop the runner from accepting new jobs.') }} + </template> + </gl-form-checkbox> - <gl-form-group :label="__('Tags')" label-for="runner-tags"> - <template #description> - <gl-sprintf - :message=" - s__('Runners|Multiple tags must be separated by a comma. For example, %{example}.') - " + <gl-form-checkbox + v-model="model.accessLevel" + name="protected" + :value="$options.ACCESS_LEVEL_REF_PROTECTED" + :unchecked-value="$options.ACCESS_LEVEL_NOT_PROTECTED" > - <template #example> - <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings --> - <code>macos, shared</code> + {{ __('Protected') }} + <template #help> + {{ s__('Runners|Use the runner on pipelines for protected branches only.') }} </template> - </gl-sprintf> - </template> - <template #label-description> - <gl-sprintf - :message=" - s__( - 'Runners|Add tags for the types of jobs the runner processes to ensure that the runner only runs jobs that you intend it to. %{helpLinkStart}Learn more.%{helpLinkEnd}', - ) - " - > - <template #helpLink="{ content }"> - <gl-link :href="$options.HELP_LABELS_PAGE_PATH" target="_blank">{{ content }}</gl-link> + </gl-form-checkbox> + + <gl-form-checkbox v-model="model.runUntagged" name="run-untagged"> + {{ __('Run untagged jobs') }} + <template #help> + {{ s__('Runners|Use the runner for jobs without tags in addition to tagged jobs.') }} </template> - </gl-sprintf> - </template> - <gl-form-input id="runner-tags" v-model="model.tagList" name="tags" /> - </gl-form-group> + </gl-form-checkbox> - <gl-form-group - :label="__('Maximum job timeout')" - :label-description=" - s__( - 'Runners|Maximum amount of time the runner can run before it terminates. If a project has a shorter job timeout period, the job timeout period of the instance runner is used instead.', - ) - " - label-for="runner-max-timeout" - :description="s__('Runners|Enter the number of seconds.')" - > - <gl-form-input - id="runner-max-timeout" - v-model.number="model.maximumTimeout" - name="max-timeout" - type="number" - /> - </gl-form-group> + <gl-form-checkbox v-if="canBeLockedToProject" v-model="model.locked" name="locked"> + {{ __('Lock to current projects') }} <gl-icon name="lock" /> + <template #help> + {{ + s__( + 'Runners|Use the runner for the currently assigned projects only. Only administrators can change the assigned projects.', + ) + }} + </template> + </gl-form-checkbox> + </div> + + <gl-form-group :label="__('Tags')" label-for="runner-tags"> + <template #description> + <gl-sprintf + :message=" + s__('Runners|Multiple tags must be separated by a comma. For example, %{example}.') + " + > + <template #example> + <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings --> + <code>macos, shared</code> + </template> + </gl-sprintf> + </template> + <template #label-description> + <gl-sprintf + :message=" + s__( + 'Runners|Add tags for the types of jobs the runner processes to ensure that the runner only runs jobs that you intend it to. %{helpLinkStart}Learn more.%{helpLinkEnd}', + ) + " + > + <template #helpLink="{ content }"> + <gl-link :href="$options.HELP_LABELS_PAGE_PATH" target="_blank">{{ + content + }}</gl-link> + </template> + </gl-sprintf> + </template> + <gl-form-input id="runner-tags" v-model="model.tagList" name="tags" /> + </gl-form-group> + + <gl-form-group + :label="__('Maximum job timeout')" + :label-description=" + s__( + 'Runners|Maximum amount of time the runner can run before it terminates. If a project has a shorter job timeout period, the job timeout period of the instance runner is used instead.', + ) + " + label-for="runner-max-timeout" + :description="s__('Runners|Enter the number of seconds.')" + > + <gl-form-input + id="runner-max-timeout" + v-model.number="model.maximumTimeout" + name="max-timeout" + type="number" + /> + </gl-form-group> + </template> </div> </template> diff --git a/app/assets/javascripts/ci/runner/components/runner_update_form.vue b/app/assets/javascripts/ci/runner/components/runner_update_form.vue index aebddc70646..bc044b609a3 100644 --- a/app/assets/javascripts/ci/runner/components/runner_update_form.vue +++ b/app/assets/javascripts/ci/runner/components/runner_update_form.vue @@ -1,23 +1,16 @@ <script> -import { - GlButton, - GlIcon, - GlForm, - GlFormCheckbox, - GlFormGroup, - GlFormInputGroup, - GlSkeletonLoader, - GlTooltipDirective, -} from '@gitlab/ui'; -import { - modelToUpdateMutationVariables, - runnerToModel, -} from 'ee_else_ce/ci/runner/runner_update_form_utils'; +import { GlButton, GlForm } from '@gitlab/ui'; +import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue'; import { createAlert, VARIANT_SUCCESS } from '~/alert'; import { visitUrl } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import { captureException } from '~/ci/runner/sentry_utils'; -import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants'; + +import { + modelToUpdateMutationVariables, + runnerToModel, +} from 'ee_else_ce/ci/runner/runner_update_form_utils'; +import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED } from '../constants'; import runnerUpdateMutation from '../graphql/edit/runner_update.mutation.graphql'; import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage'; @@ -25,20 +18,11 @@ export default { name: 'RunnerUpdateForm', components: { GlButton, - GlIcon, GlForm, - GlFormCheckbox, - GlFormGroup, - GlFormInputGroup, - GlSkeletonLoader, - RunnerMaintenanceNoteField: () => - import('ee_component/ci/runner/components/runner_maintenance_note_field.vue'), + RunnerFormFields, RunnerUpdateCostFactorFields: () => import('ee_component/ci/runner/components/runner_update_cost_factor_fields.vue'), }, - directives: { - GlTooltip: GlTooltipDirective, - }, props: { runner: { type: Object, @@ -59,19 +43,12 @@ export default { data() { return { saving: false, - model: runnerToModel(this.runner), + model: null, }; }, - computed: { - canBeLockedToProject() { - return this.runner?.runnerType === PROJECT_TYPE; - }, - }, watch: { - runner(newVal, oldVal) { - if (oldVal === null) { - this.model = runnerToModel(newVal); - } + runner(val) { + this.model = runnerToModel(val); }, }, methods: { @@ -114,94 +91,8 @@ export default { </script> <template> <gl-form @submit.prevent="onSubmit"> - <h4 class="gl-font-lg gl-my-5">{{ s__('Runners|Details') }}</h4> - - <gl-skeleton-loader v-if="loading" /> - - <template v-else> - <gl-form-group :label="__('Description')" data-testid="runner-field-description"> - <gl-form-input-group v-model="model.description" /> - </gl-form-group> - <runner-maintenance-note-field v-model="model.maintenanceNote" /> - </template> - - <hr /> - - <h4 class="gl-font-lg gl-my-5">{{ s__('Runners|Configuration') }}</h4> - - <template v-if="loading"> - <gl-skeleton-loader v-for="i in 3" :key="i" /> - </template> - <template v-else> - <div class="gl-mb-5"> - <gl-form-checkbox v-model="model.paused" data-testid="runner-field-paused"> - {{ __('Paused') }} - <template #help> - {{ s__('Runners|Stop the runner from accepting new jobs.') }} - </template> - </gl-form-checkbox> - - <gl-form-checkbox - v-model="model.accessLevel" - data-testid="runner-field-protected" - :value="$options.ACCESS_LEVEL_REF_PROTECTED" - :unchecked-value="$options.ACCESS_LEVEL_NOT_PROTECTED" - > - {{ __('Protected') }} - <template #help> - {{ s__('Runners|Use the runner on pipelines for protected branches only.') }} - </template> - </gl-form-checkbox> - - <gl-form-checkbox v-model="model.runUntagged" data-testid="runner-field-run-untagged"> - {{ __('Run untagged jobs') }} - <template #help> - {{ s__('Runners|Use the runner for jobs without tags, in addition to tagged jobs.') }} - </template> - </gl-form-checkbox> - - <gl-form-checkbox - v-if="canBeLockedToProject" - v-model="model.locked" - data-testid="runner-field-locked" - > - {{ __('Lock to current projects') }} <gl-icon name="lock" /> - <template #help> - {{ - s__( - 'Runners|Use the runner for the currently assigned projects only. Only administrators can change the assigned projects.', - ) - }} - </template> - </gl-form-checkbox> - </div> - - <gl-form-group - data-testid="runner-field-max-timeout" - :label="__('Maximum job timeout')" - :description=" - s__( - 'Runners|Enter the number of seconds. This timeout takes precedence over lower timeouts set for the project.', - ) - " - > - <gl-form-input-group v-model.number="model.maximumTimeout" type="number" /> - </gl-form-group> - - <gl-form-group - data-testid="runner-field-tags" - :label="__('Tags')" - :description=" - __( - 'You can set up jobs to only use runners with specific tags. Separate tags with commas.', - ) - " - > - <gl-form-input-group v-model="model.tagList" /> - </gl-form-group> - - <runner-update-cost-factor-fields v-model="model" /> - </template> + <runner-form-fields v-model="model" :loading="loading" /> + <runner-update-cost-factor-fields v-model="model" /> <div class="gl-mt-6"> <gl-button diff --git a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue index 5965330c4eb..2e1706ddae9 100644 --- a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue +++ b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue @@ -66,7 +66,7 @@ export default { <hr aria-hidden="true" /> - <h2 class="gl-font-weight-normal gl-font-lg gl-my-5"> + <h2 class="gl-font-size-h2 gl-my-5"> {{ s__('Runners|Platform') }} </h2> <runner-platforms-radio-group v-model="platform" /> diff --git a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue index 715b0c28148..51f5a9ce8d9 100644 --- a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue +++ b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue @@ -66,7 +66,7 @@ export default { <hr aria-hidden="true" /> - <h2 class="gl-font-weight-normal gl-font-lg gl-my-5"> + <h2 class="gl-font-size-h2 gl-my-5"> {{ s__('Runners|Platform') }} </h2> <runner-platforms-radio-group v-model="platform" /> diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index ea444b5c146..ab5f01227fb 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -108,27 +108,5 @@ export default class ContextualSidebar { const collapse = parseBoolean(getCookie('sidebar_collapsed')); this.toggleCollapsedSidebar(collapse, true); } - - const modalEl = document.querySelector('.js-invite-members-modal'); - if (modalEl) { - import( - /* webpackChunkName: 'initInviteMembersModal' */ '~/invite_members/init_invite_members_modal' - ) - .then(({ default: initInviteMembersModal }) => { - initInviteMembersModal(); - }) - .catch(() => {}); - - const inviteTriggers = document.querySelectorAll('.js-invite-members-trigger'); - if (inviteTriggers) { - import( - /* webpackChunkName: 'initInviteMembersTrigger' */ '~/invite_members/init_invite_members_trigger' - ) - .then(({ default: initInviteMembersTrigger }) => { - initInviteMembersTrigger(); - }) - .catch(() => {}); - } - } } } diff --git a/app/assets/javascripts/environments/components/kubernetes_agent_info.vue b/app/assets/javascripts/environments/components/kubernetes_agent_info.vue index 7660912f93a..03bde8d64ac 100644 --- a/app/assets/javascripts/environments/components/kubernetes_agent_info.vue +++ b/app/assets/javascripts/environments/components/kubernetes_agent_info.vue @@ -1,68 +1,37 @@ <script> -import { GlIcon, GlLink, GlSprintf, GlLoadingIcon, GlAlert } from '@gitlab/ui'; +import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getAgentLastContact, getAgentStatus } from '~/clusters_list/clusters_util'; import { AGENT_STATUSES } from '~/clusters_list/constants'; import { s__ } from '~/locale'; -import getK8sClusterAgentQuery from '../graphql/queries/k8s_cluster_agent.query.graphql'; export default { components: { GlIcon, GlLink, GlSprintf, - GlLoadingIcon, TimeAgoTooltip, - GlAlert, }, props: { - agentName: { - required: true, - type: String, - }, - agentId: { - required: true, - type: String, - }, - agentProjectPath: { - required: true, - type: String, - }, - }, - apollo: { clusterAgent: { - query: getK8sClusterAgentQuery, - variables() { - return { - agentName: this.agentName, - projectPath: this.agentProjectPath, - }; - }, - update: (data) => data?.project?.clusterAgent, - error() { - this.clusterAgent = null; - }, + required: true, + type: Object, }, }, - data() { - return { - clusterAgent: null, - }; - }, computed: { - isLoading() { - return this.$apollo.queries.clusterAgent.loading; - }, agentLastContact() { return getAgentLastContact(this.clusterAgent.tokens.nodes); }, agentStatus() { return getAgentStatus(this.agentLastContact); }, + agentId() { + return getIdFromGraphQLId(this.clusterAgent.id); + }, }, methods: {}, i18n: { - loadingError: s__('ClusterAgents|An error occurred while loading your agent'), agentId: s__('ClusterAgents|Agent ID #%{agentId}'), neverConnectedText: s__('ClusterAgents|Never'), }, @@ -70,8 +39,7 @@ export default { }; </script> <template> - <gl-loading-icon v-if="isLoading" inline /> - <div v-else-if="clusterAgent" class="gl-text-gray-900"> + <div class="gl-text-gray-900"> <gl-icon name="kubernetes-agent" class="gl-text-gray-500" /> <gl-link :href="clusterAgent.webPath" class="gl-mr-3"> <gl-sprintf :message="$options.i18n.agentId" @@ -92,8 +60,4 @@ export default { <span v-else>{{ $options.i18n.neverConnectedText }}</span> </span> </div> - - <gl-alert v-else variant="danger" :dismissible="false"> - {{ $options.i18n.loadingError }} - </gl-alert> </template> diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue index 1f15c4daa2f..a849adfc755 100644 --- a/app/assets/javascripts/environments/components/kubernetes_overview.vue +++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue @@ -2,7 +2,7 @@ import { GlCollapse, GlButton, GlAlert } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import csrf from '~/lib/utils/csrf'; -import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import KubernetesAgentInfo from './kubernetes_agent_info.vue'; import KubernetesPods from './kubernetes_pods.vue'; import KubernetesTabs from './kubernetes_tabs.vue'; @@ -18,17 +18,9 @@ export default { }, inject: ['kasTunnelUrl'], props: { - agentName: { + clusterAgent: { required: true, - type: String, - }, - agentId: { - required: true, - type: String, - }, - agentProjectPath: { - required: true, - type: String, + type: Object, }, namespace: { required: false, @@ -50,8 +42,7 @@ export default { return this.isVisible ? this.$options.i18n.collapse : this.$options.i18n.expand; }, gitlabAgentId() { - const id = isGid(this.agentId) ? getIdFromGraphQLId(this.agentId) : this.agentId; - return id.toString(); + return getIdFromGraphQLId(this.clusterAgent.id).toString(); }, k8sAccessConfiguration() { return { @@ -91,11 +82,7 @@ export default { </p> <gl-collapse :visible="isVisible" class="gl-md-pl-7 gl-md-pr-5 gl-mt-4"> <template v-if="isVisible"> - <kubernetes-agent-info - :agent-name="agentName" - :agent-id="agentId" - :agent-project-path="agentProjectPath" - class="gl-mb-5" /> + <kubernetes-agent-info :cluster-agent="clusterAgent" class="gl-mb-5" /> <gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mb-5"> {{ error }} diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue index 9ad31688329..72323c0e43e 100644 --- a/app/assets/javascripts/environments/components/new_environment_item.vue +++ b/app/assets/javascripts/environments/components/new_environment_item.vue @@ -13,6 +13,7 @@ import { truncate } from '~/lib/utils/text_utility'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import isLastDeployment from '../graphql/queries/is_last_deployment.query.graphql'; +import getEnvironmentClusterAgent from '../graphql/queries/environment_cluster_agent.query.graphql'; import ExternalUrl from './environment_external_url.vue'; import Actions from './environment_actions.vue'; import StopComponent from './environment_stop.vue'; @@ -51,7 +52,7 @@ export default { GlTooltip, }, mixins: [glFeatureFlagsMixin()], - inject: ['helpPagePath'], + inject: ['helpPagePath', 'projectPath'], props: { environment: { required: true, @@ -81,7 +82,7 @@ export default { tierTooltip: s__('Environment|Deployment tier'), }, data() { - return { visible: false }; + return { visible: false, clusterAgent: null }; }, computed: { icon() { @@ -163,23 +164,33 @@ export default { rolloutStatus() { return this.environment?.rolloutStatus; }, - agent() { - return this.environment?.agent || {}; - }, isKubernetesOverviewAvailable() { return this.glFeatures?.kasUserAccessProject; }, - hasRequiredAgentData() { - const { project, id, name } = this.agent || {}; - return project && id && name; - }, showKubernetesOverview() { - return this.isKubernetesOverviewAvailable && this.hasRequiredAgentData; + return Boolean(this.isKubernetesOverviewAvailable && this.clusterAgent); }, }, methods: { - toggleCollapse() { + toggleEnvironmentCollapse() { this.visible = !this.visible; + + if (this.visible) { + this.getClusterAgent(); + } + }, + getClusterAgent() { + if (!this.isKubernetesOverviewAvailable || this.clusterAgent) return; + + this.$apollo.addSmartQuery('environmentClusterAgent', { + variables() { + return { environmentName: this.environment.name, projectFullPath: this.projectPath }; + }, + query: getEnvironmentClusterAgent, + update(data) { + this.clusterAgent = data?.project?.environment?.clusterAgent; + }, + }); }, }, deploymentClasses: [ @@ -222,7 +233,7 @@ export default { :aria-label="label" size="small" category="secondary" - @click="toggleCollapse" + @click="toggleEnvironmentCollapse" /> <gl-link v-gl-tooltip @@ -359,10 +370,8 @@ export default { </div> <div v-if="showKubernetesOverview" :class="$options.kubernetesOverviewClasses"> <kubernetes-overview - :agent-project-path="agent.project" - :agent-name="agent.name" - :agent-id="agent.id" - :namespace="agent.kubernetesNamespace" + :cluster-agent="clusterAgent" + :namespace="environment.kubernetesNamespace" /> </div> <div v-if="rolloutStatus" :class="$options.deployBoardClasses"> diff --git a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql new file mode 100644 index 00000000000..760f1fba897 --- /dev/null +++ b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql @@ -0,0 +1,19 @@ +query getEnvironmentClusterAgent($projectFullPath: ID!, $environmentName: String) { + project(fullPath: $projectFullPath) { + id + environment(name: $environmentName) { + id + clusterAgent { + id + name + webPath + tokens { + nodes { + id + lastUsedAt + } + } + } + } + } +} diff --git a/app/assets/javascripts/environments/graphql/queries/k8s_cluster_agent.query.graphql b/app/assets/javascripts/environments/graphql/queries/k8s_cluster_agent.query.graphql deleted file mode 100644 index bd45d2dba2f..00000000000 --- a/app/assets/javascripts/environments/graphql/queries/k8s_cluster_agent.query.graphql +++ /dev/null @@ -1,15 +0,0 @@ -query getK8sClusterAgentQuery($projectPath: ID!, $agentName: String!) { - project(fullPath: $projectPath) { - id - clusterAgent(name: $agentName) { - id - webPath - tokens { - nodes { - id - lastUsedAt - } - } - } - } -} diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 63a1ba89fff..42682d9b79f 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -64,9 +64,31 @@ export function initScrollingTabs() { }); } -function initDeferred() { - initScrollingTabs(); +function initInviteMembers() { + const modalEl = document.querySelector('.js-invite-members-modal'); + if (!modalEl) return; + + import( + /* webpackChunkName: 'initInviteMembersModal' */ '~/invite_members/init_invite_members_modal' + ) + .then(({ default: initInviteMembersModal }) => { + initInviteMembersModal(); + }) + .catch(() => {}); + const inviteTriggers = document.querySelectorAll('.js-invite-members-trigger'); + if (!inviteTriggers) return; + + import( + /* webpackChunkName: 'initInviteMembersTrigger' */ '~/invite_members/init_invite_members_trigger' + ) + .then(({ default: initInviteMembersTrigger }) => { + initInviteMembersTrigger(); + }) + .catch(() => {}); +} + +function initWhatsNewComponent() { const appEl = document.getElementById('whats-new-app'); if (!appEl) return; @@ -84,6 +106,12 @@ function initDeferred() { }); } +function initDeferred() { + initScrollingTabs(); + initWhatsNewComponent(); + initInviteMembers(); +} + export default function initLayoutNav() { if (!gon.use_new_navigation) { const contextualSidebar = new ContextualSidebar(); diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js index 2e71eced66f..df6ca8eab96 100644 --- a/app/assets/javascripts/pages/groups/group_members/index.js +++ b/app/assets/javascripts/pages/groups/group_members/index.js @@ -1,8 +1,6 @@ import { groupMemberRequestFormatter } from '~/groups/members/utils'; import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger'; import initInviteGroupsModal from '~/invite_members/init_invite_groups_modal'; -import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; -import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import { s__ } from '~/locale'; import { initMembersApp } from '~/members'; import { MEMBER_TYPES, EE_APP_OPTIONS } from 'ee_else_ce/members/constants'; @@ -60,7 +58,5 @@ const APP_OPTIONS = { initMembersApp(document.querySelector('.js-group-members-list-app'), APP_OPTIONS); -initInviteMembersModal(); initInviteGroupsModal(); -initInviteMembersTrigger(); initInviteGroupTrigger(); diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js index dba65c7e791..5d9eafe5672 100644 --- a/app/assets/javascripts/pages/groups/shared/group_details.js +++ b/app/assets/javascripts/pages/groups/shared/group_details.js @@ -1,6 +1,5 @@ import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import initInviteMembersBanner from '~/groups/init_invite_members_banner'; -import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; import initNotificationsDropdown from '~/notifications'; import ProjectsList from '~/projects_list'; @@ -12,5 +11,4 @@ export default function initGroupDetails() { new ProjectsList(); // eslint-disable-line no-new initInviteMembersBanner(); - initInviteMembersModal(); } diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js index 79a4ed0f9c3..1e9111a3cc6 100644 --- a/app/assets/javascripts/pages/projects/project_members/index.js +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -1,9 +1,7 @@ import initImportProjectMembersTrigger from '~/invite_members/init_import_project_members_trigger'; import initImportProjectMembersModal from '~/invite_members/init_import_project_members_modal'; import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger'; -import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; import initInviteGroupsModal from '~/invite_members/init_invite_groups_modal'; -import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import { s__ } from '~/locale'; import { initMembersApp } from '~/members'; import { MEMBER_TYPES } from '~/members/constants'; @@ -11,9 +9,7 @@ import { groupLinkRequestFormatter } from '~/members/utils'; import { projectMemberRequestFormatter } from '~/projects/members/utils'; initImportProjectMembersModal(); -initInviteMembersModal(); initInviteGroupsModal(); -initInviteMembersTrigger(); initInviteGroupTrigger(); initImportProjectMembersTrigger(); diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 33d4090011f..e17f5255c54 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -1,7 +1,5 @@ import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; -import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; -import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import initClustersDeprecationAlert from '~/projects/clusters_deprecation_alert'; import leaveByUrl from '~/namespaces/leave_by_url'; import initVueNotificationsDropdown from '~/notifications'; @@ -42,8 +40,6 @@ initVueNotificationsDropdown(); new ShortcutsNavigation(); // eslint-disable-line no-new initUploadFileTrigger(); -initInviteMembersModal(); -initInviteMembersTrigger(); initClustersDeprecationAlert(); initTerraformNotification(); diff --git a/app/assets/javascripts/pages/projects/work_items/index.js b/app/assets/javascripts/pages/projects/work_items/index.js index 6eef2352e2c..11c257611f0 100644 --- a/app/assets/javascripts/pages/projects/work_items/index.js +++ b/app/assets/javascripts/pages/projects/work_items/index.js @@ -1,5 +1,3 @@ import { initWorkItemsRoot } from '~/work_items/index'; -import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; initWorkItemsRoot(); -initInviteMembersModal(); diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 74843bcc006..67e76b575e0 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -2,8 +2,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { TYPENAME_ISSUE, TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants'; import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; -import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; -import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import { TYPE_ISSUE, TYPE_MERGE_REQUEST, WORKSPACE_PROJECT } from '~/issues/constants'; import { gqlClient } from '~/issues/list/graphql'; import { @@ -805,8 +803,6 @@ const isAssigneesWidgetShown = (isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget; export function mountSidebar(mediator, store) { - initInviteMembersModal(); - initInviteMembersTrigger(); mountSidebarTodoWidget(); if (isAssigneesWidgetShown) { mountSidebarAssigneesWidget(); diff --git a/app/assets/stylesheets/notify_enhanced.scss b/app/assets/stylesheets/notify_enhanced.scss index b331d997a97..a3e02dabe0e 100644 --- a/app/assets/stylesheets/notify_enhanced.scss +++ b/app/assets/stylesheets/notify_enhanced.scss @@ -32,6 +32,10 @@ body { font-size: inherit; } +pre { + font-size: 14px; +} + .gl-mb-5 { @include gl-mb-5; } diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 1c988b9767f..bf8dea15c58 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -99,11 +99,11 @@ module ApplicationSettingsHelper checked_value: level, unchecked_value: nil ) do |c| - c.label do + c.with_label do visibility_level_icon(level) + content_tag(:span, label, { class: 'gl-ml-2' }) end - c.help_text do + c.with_help_text do restricted_visibility_levels_help_text.fetch(level) end end diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index ed8cca20241..3d0b899e867 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -37,7 +37,7 @@ module FormHelper dismissible: false, alert_options: { id: 'error_explanation', class: 'gl-mb-5' } ) do |c| - c.body do + c.with_body do tag.ul(class: 'gl-pl-5 gl-mb-0') do messages end diff --git a/app/models/analytics/cycle_analytics/value_stream.rb b/app/models/analytics/cycle_analytics/value_stream.rb index 59c68393d74..31e06075bcb 100644 --- a/app/models/analytics/cycle_analytics/value_stream.rb +++ b/app/models/analytics/cycle_analytics/value_stream.rb @@ -21,6 +21,7 @@ module Analytics scope :preload_associated_models, -> { includes(:namespace, stages: [:namespace, :end_event_label, :start_event_label]) } + scope :order_by_name_asc, -> { order(arel_table[:name].lower.asc) } after_save :ensure_aggregation_record_presence diff --git a/app/models/packages/nuget/metadatum.rb b/app/models/packages/nuget/metadatum.rb index 1db8c0eddbf..08276f87568 100644 --- a/app/models/packages/nuget/metadatum.rb +++ b/app/models/packages/nuget/metadatum.rb @@ -1,24 +1,22 @@ # frozen_string_literal: true class Packages::Nuget::Metadatum < ApplicationRecord + MAX_AUTHORS_LENGTH = 255 + MAX_DESCRIPTION_LENGTH = 4000 + belongs_to :package, -> { where(package_type: :nuget) }, inverse_of: :nuget_metadatum validates :package, presence: true validates :license_url, public_url: { allow_blank: true } validates :project_url, public_url: { allow_blank: true } validates :icon_url, public_url: { allow_blank: true } + validates :authors, presence: true, length: { maximum: MAX_AUTHORS_LENGTH } + validates :description, presence: true, length: { maximum: MAX_DESCRIPTION_LENGTH } - validate :ensure_at_least_one_field_supplied validate :ensure_nuget_package_type private - def ensure_at_least_one_field_supplied - return if license_url? || project_url? || icon_url? - - errors.add(:base, _('Nuget metadatum must have at least license_url, project_url or icon_url set')) - end - def ensure_nuget_package_type return if package&.nuget? diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 15f91cae86b..c70dc288710 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -163,6 +163,11 @@ class ProjectPolicy < BasePolicy condition(:service_desk_enabled) { @subject.service_desk_enabled? } with_scope :subject + condition(:model_experiments_enabled) do + Feature.enabled?(:ml_experiment_tracking, @subject) && @subject.feature_available?(:model_experiments, @user) + end + + with_scope :subject condition(:model_registry_enabled) { Feature.enabled?(:model_registry, @subject) } with_scope :subject @@ -223,6 +228,7 @@ class ProjectPolicy < BasePolicy feature_flags releases infrastructure + model_experiments ] features.each do |f| @@ -899,6 +905,10 @@ class ProjectPolicy < BasePolicy enable :read_model_registry end + rule { model_experiments_enabled }.policy do + enable :read_model_experiments + end + private def user_is_user? diff --git a/app/presenters/packages/nuget/presenter_helpers.rb b/app/presenters/packages/nuget/presenter_helpers.rb index 82ed80d8372..ea8558c54f4 100644 --- a/app/presenters/packages/nuget/presenter_helpers.rb +++ b/app/presenters/packages/nuget/presenter_helpers.rb @@ -5,7 +5,6 @@ module Packages module PresenterHelpers include ::API::Helpers::RelatedResourcesHelpers - BLANK_STRING = '' PACKAGE_DEPENDENCY_GROUP = 'PackageDependencyGroup' PACKAGE_DEPENDENCY = 'PackageDependency' @@ -45,14 +44,13 @@ module Packages def catalog_entry_for(package) { json_url: json_url_for(package), - authors: BLANK_STRING, dependency_groups: dependency_groups_for(package), package_name: package.name, package_version: package.version, archive_url: archive_url_for(package), - summary: BLANK_STRING, tags: tags_for(package), - metadatum: metadatum_for(package) + metadatum: metadatum_for(package), + published: package.created_at.iso8601 } end @@ -98,8 +96,8 @@ module Packages metadatum = package.nuget_metadatum return {} unless metadatum - metadatum.slice(:project_url, :license_url, :icon_url) - .compact + metadatum.slice(:authors, :description, :project_url, :license_url, :icon_url) + .compact end def base_path_for(package) diff --git a/app/presenters/packages/nuget/search_results_presenter.rb b/app/presenters/packages/nuget/search_results_presenter.rb index 311296d576c..45c2c5170ae 100644 --- a/app/presenters/packages/nuget/search_results_presenter.rb +++ b/app/presenters/packages/nuget/search_results_presenter.rb @@ -20,11 +20,9 @@ module Packages { type: 'Package', - authors: '', name: package_name, version: latest_version, versions: build_package_versions(packages), - summary: '', total_downloads: 0, verified: true, tags: tags_for(latest_package), @@ -48,7 +46,7 @@ module Packages def latest_version(packages) versions = packages.map(&:version).compact - VersionSorter.sort(versions).last # rubocop: disable Style/RedundantSort + VersionSorter.sort(versions).last end end end diff --git a/app/services/ci/runners/assign_runner_service.rb b/app/services/ci/runners/assign_runner_service.rb index 290f945cc72..4e7b08bdd7a 100644 --- a/app/services/ci/runners/assign_runner_service.rb +++ b/app/services/ci/runners/assign_runner_service.rb @@ -17,6 +17,10 @@ module Ci return ServiceResponse.error(message: 'user not allowed to assign runner', http_status: :forbidden) end + unless @user.can?(:register_project_runners, @project) + return ServiceResponse.error(message: 'user not allowed to add runners to project', http_status: :forbidden) + end + if @runner.assign_to(@project, @user) ServiceResponse.success else diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index efe42fb29d5..f982d66eb08 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -124,6 +124,10 @@ module Issues def update_project_counter_caches?(issue) super || issue.confidential_changed? end + + def log_audit_event(issue, user, event_type, message) + # defined in EE + end end end diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index e45033f2b91..f848a8db12a 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -28,6 +28,11 @@ module Issues event_service.close_issue(issue, current_user) create_note(issue, closed_via) if system_note + if current_user.project_bot? + log_audit_event(issue, current_user, "#{issue.issue_type}_closed_by_project_bot", + "Closed #{issue.issue_type.humanize(capitalize: false)} #{issue.title}") + end + closed_via = _("commit %{commit_id}") % { commit_id: closed_via.id } if closed_via.is_a?(Commit) notification_service.async.close_issue(issue, current_user, { closed_via: closed_via }) if notifications diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb index f4d229ecec7..d71ba4e3414 100644 --- a/app/services/issues/reopen_service.rb +++ b/app/services/issues/reopen_service.rb @@ -7,6 +7,12 @@ module Issues if issue.reopen event_service.reopen_issue(issue, current_user) + + if current_user.project_bot? + log_audit_event(issue, current_user, "#{issue.issue_type}_reopened_by_project_bot", + "Reopened #{issue.issue_type.humanize(capitalize: false)} #{issue.title}") + end + create_note(issue, 'reopened') notification_service.async.reopen_issue(issue, current_user) perform_incident_management_actions(issue) diff --git a/app/services/packages/nuget/metadata_extraction_service.rb b/app/services/packages/nuget/metadata_extraction_service.rb index 02086b2a282..3b92d977c79 100644 --- a/app/services/packages/nuget/metadata_extraction_service.rb +++ b/app/services/packages/nuget/metadata_extraction_service.rb @@ -7,18 +7,22 @@ module Packages ExtractionError = Class.new(StandardError) + ROOT_XPATH = '//xmlns:package/xmlns:metadata/xmlns' + XPATHS = { - package_name: '//xmlns:package/xmlns:metadata/xmlns:id', - package_version: '//xmlns:package/xmlns:metadata/xmlns:version', - license_url: '//xmlns:package/xmlns:metadata/xmlns:licenseUrl', - project_url: '//xmlns:package/xmlns:metadata/xmlns:projectUrl', - icon_url: '//xmlns:package/xmlns:metadata/xmlns:iconUrl' + package_name: "#{ROOT_XPATH}:id", + package_version: "#{ROOT_XPATH}:version", + authors: "#{ROOT_XPATH}:authors", + description: "#{ROOT_XPATH}:description", + license_url: "#{ROOT_XPATH}:licenseUrl", + project_url: "#{ROOT_XPATH}:projectUrl", + icon_url: "#{ROOT_XPATH}:iconUrl" }.freeze - XPATH_DEPENDENCIES = '//xmlns:package/xmlns:metadata/xmlns:dependencies/xmlns:dependency' - XPATH_DEPENDENCY_GROUPS = '//xmlns:package/xmlns:metadata/xmlns:dependencies/xmlns:group' - XPATH_TAGS = '//xmlns:package/xmlns:metadata/xmlns:tags' - XPATH_PACKAGE_TYPES = '//xmlns:package/xmlns:metadata/xmlns:packageTypes/xmlns:packageType' + XPATH_DEPENDENCIES = "#{ROOT_XPATH}:dependencies/xmlns:dependency".freeze + XPATH_DEPENDENCY_GROUPS = "#{ROOT_XPATH}:dependencies/xmlns:group".freeze + XPATH_TAGS = "#{ROOT_XPATH}:tags".freeze + XPATH_PACKAGE_TYPES = "#{ROOT_XPATH}:packageTypes/xmlns:packageType".freeze MAX_FILE_SIZE = 4.megabytes.freeze @@ -40,10 +44,6 @@ module Packages end end - def project - package_file.package.project - end - def valid_package_file? package_file && package_file.package&.nuget? && diff --git a/app/services/packages/nuget/sync_metadatum_service.rb b/app/services/packages/nuget/sync_metadatum_service.rb index ca9cc4d5b78..189b972c156 100644 --- a/app/services/packages/nuget/sync_metadatum_service.rb +++ b/app/services/packages/nuget/sync_metadatum_service.rb @@ -15,6 +15,8 @@ module Packages metadatum.destroy! if metadatum.persisted? else metadatum.update!( + authors: authors, + description: description, license_url: license_url, project_url: project_url, icon_url: icon_url @@ -24,26 +26,57 @@ module Packages private + attr_reader :package, :metadata + def metadatum - strong_memoize(:metadatum) do - @package.nuget_metadatum || @package.build_nuget_metadatum - end + package.nuget_metadatum || package.build_nuget_metadatum end + strong_memoize_attr :metadatum def blank_metadata? - project_url.blank? && license_url.blank? && icon_url.blank? + [authors, description, project_url, license_url, icon_url].all?(&:blank?) + end + + def authors + truncate_value(:authors, ::Packages::Nuget::Metadatum::MAX_AUTHORS_LENGTH) end + strong_memoize_attr :authors + + def description + truncate_value(:description, ::Packages::Nuget::Metadatum::MAX_DESCRIPTION_LENGTH) + end + strong_memoize_attr :description def project_url - @metadata[:project_url] + metadata[:project_url] end def license_url - @metadata[:license_url] + metadata[:license_url] end def icon_url - @metadata[:icon_url] + metadata[:icon_url] + end + + def truncate_value(field, max_length) + return unless metadata[field] + + if metadata[field].size > max_length + log_info("#{field.capitalize} is too long (maximum is #{max_length} characters)", field) + end + + metadata[field].truncate(max_length) + end + + def log_info(message, field) + Gitlab::AppLogger.info( + class: self.class.name, + message: message, + package_id: package.id, + project_id: package.project_id, + field => metadata[field] + ) end end end diff --git a/app/services/packages/nuget/update_package_from_metadata_service.rb b/app/services/packages/nuget/update_package_from_metadata_service.rb index 5456ad4cad7..7153a9035b8 100644 --- a/app/services/packages/nuget/update_package_from_metadata_service.rb +++ b/app/services/packages/nuget/update_package_from_metadata_service.rb @@ -17,7 +17,7 @@ module Packages end def execute - raise InvalidMetadataError, 'package name and/or package version not found in metadata' unless valid_metadata? + raise InvalidMetadataError, 'package name, version, authors and/or description not found in metadata' unless valid_metadata? try_obtain_lease do @package_file.transaction do @@ -55,17 +55,19 @@ module Packages return if symbol_package? ::Packages::Nuget::SyncMetadatumService - .new(package, metadata.slice(:project_url, :license_url, :icon_url)) + .new(package, metadata.slice(:authors, :description, :project_url, :license_url, :icon_url)) .execute + ::Packages::UpdateTagsService .new(package, package_tags) .execute + rescue StandardError => e raise InvalidMetadataError, e.message end def valid_metadata? - package_name.present? && package_version.present? + [package_name, package_version, package_authors, package_description].all?(&:present?) end def link_to_existing_package @@ -93,15 +95,14 @@ module Packages end def existing_package - strong_memoize(:existing_package) do - @package_file.project.packages - .nuget - .with_name(package_name) - .with_version(package_version) - .not_pending_destruction - .first - end + @package_file.project.packages + .nuget + .with_name(package_name) + .with_version(package_version) + .not_pending_destruction + .first end + strong_memoize_attr :existing_package def package_name metadata[:package_name] @@ -123,15 +124,22 @@ module Packages metadata.fetch(:package_types, []) end + def package_authors + metadata[:authors] + end + + def package_description + metadata[:description] + end + def symbol_package? package_types.include?(SYMBOL_PACKAGE_IDENTIFIER) end def metadata - strong_memoize(:metadata) do - ::Packages::Nuget::MetadataExtractionService.new(@package_file.id).execute - end + ::Packages::Nuget::MetadataExtractionService.new(@package_file.id).execute end + strong_memoize_attr :metadata def package_filename "#{package_name.downcase}.#{package_version.downcase}.#{symbol_package? ? 'snupkg' : 'nupkg'}" diff --git a/app/views/projects/mirrors/_branch_filter.html.haml b/app/views/projects/mirrors/_branch_filter.html.haml index 49b0f8c39c8..7d90906bfe8 100644 --- a/app/views/projects/mirrors/_branch_filter.html.haml +++ b/app/views/projects/mirrors/_branch_filter.html.haml @@ -2,8 +2,8 @@ = render Pajamas::CheckboxTagComponent.new(name: :only_protected_branches, checkbox_options: { class: 'js-mirror-protected' }, label_options: { class: 'gl-mb-0!' }) do |c| - = c.label do + - c.with_label do = _('Mirror only protected branches') - = c.help_text do + - c.with_help_text do = _('If enabled, only protected branches will be mirrored.') = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer' diff --git a/app/views/projects/mirrors/_mirror_repos_push.html.haml b/app/views/projects/mirrors/_mirror_repos_push.html.haml index 0947359a753..5b02d650989 100644 --- a/app/views/projects/mirrors/_mirror_repos_push.html.haml +++ b/app/views/projects/mirrors/_mirror_repos_push.html.haml @@ -12,8 +12,8 @@ = render Pajamas::CheckboxTagComponent.new(name: :keep_divergent_refs, checkbox_options: { class: 'js-mirror-keep-divergent-refs' }, label_options: { class: 'gl-mb-0!' }) do |c| - = c.label do + - c.with_label do = _('Keep divergent refs') - = c.help_text do + - c.with_help_text do - link_opening_tag = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe = html_escape(_('Do not force push over diverged refs. After the mirror is created, this setting can only be modified using the API. %{mirroring_docs_link_start}Learn more about this option%{link_closing_tag} and %{mirroring_api_docs_link_start}the API.%{link_closing_tag}')) % { mirroring_docs_link_start: link_opening_tag % {url: help_page_path('user/project/repository/mirror/push.md', anchor: 'keep-divergent-refs')}, mirroring_api_docs_link_start: link_opening_tag % {url: help_page_path('api/remote_mirrors')}, link_closing_tag: '</a>'.html_safe } diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index b31e8919832..c4630eec168 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -10,7 +10,7 @@ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, icon: 'search') .form-group{ class: 'gl-ml-5 gl-mb-n3!' } = render Pajamas::CheckboxTagComponent.new(name: :filter_ref, checked: @options[:filter_ref]) do |c| - = c.label do + - c.with_label do = _("Begin with the selected commit") - if @commit diff --git a/app/views/shared/_new_merge_request_checkbox.html.haml b/app/views/shared/_new_merge_request_checkbox.html.haml index 75289e2e6a5..fb3dfba2691 100644 --- a/app/views/shared/_new_merge_request_checkbox.html.haml +++ b/app/views/shared/_new_merge_request_checkbox.html.haml @@ -3,7 +3,7 @@ = render Pajamas::CheckboxTagComponent.new(name: 'create_merge_request', checked: true, checkbox_options: { class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" }) do |c| - = c.label do + - c.with_label do - translation_variables = { new_merge_request: "<strong>#{_('new merge request')}</strong>" } - translation = _('Start a %{new_merge_request} with these changes') % translation_variables #{ translation.html_safe } diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index 1dc24d205d0..1da0b82b634 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -7,10 +7,10 @@ - if @add_related_issue .form-group = render Pajamas::CheckboxTagComponent.new(name: :add_related_issue, value: @add_related_issue.iid, checked: true) do |c| - = c.label do + - c.with_label do - add_related_issue_link = link_to "\##{@add_related_issue.iid}", issue_path(@add_related_issue), class: ['has-tooltip'], title: @add_related_issue.title #{_('Relate to %{issuable_type} %{add_related_issue_link}').html_safe % { issuable_type: @add_related_issue.issue_type, add_related_issue_link: add_related_issue_link }} - = c.help_text do + - c.with_help_text do = _('Adds this %{issuable_type} as related to the %{issuable_type} it was created from') % { issuable_type: @add_related_issue.issue_type } - if issuable.respond_to?(:confidential) && can?(current_user, :set_confidentiality, issuable) diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb index 07699a50e36..0e7f11debd2 100644 --- a/app/workers/new_issue_worker.rb +++ b/app/workers/new_issue_worker.rb @@ -28,5 +28,15 @@ class NewIssueWorker # rubocop:disable Scalability/IdempotentWorker Issues::AfterCreateService .new(container: issuable.project, current_user: user) .execute(issuable) + + log_audit_event if user.project_bot? + end + + private + + def log_audit_event + # defined in EE end end + +NewIssueWorker.prepend_mod diff --git a/config/feature_flags/development/expired_storage_check.yml b/config/feature_flags/development/expired_storage_check.yml new file mode 100644 index 00000000000..271cc951f1f --- /dev/null +++ b/config/feature_flags/development/expired_storage_check.yml @@ -0,0 +1,8 @@ +--- +name: expired_storage_check +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121048 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/411919 +milestone: '16.1' +type: development +group: group::utilization +default_enabled: false diff --git a/config/feature_flags/development/gitlab_duo.yml b/config/feature_flags/development/gitlab_duo.yml new file mode 100644 index 00000000000..5fce9acf12f --- /dev/null +++ b/config/feature_flags/development/gitlab_duo.yml @@ -0,0 +1,8 @@ +--- +name: gitlab_duo +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122235 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/413688 +milestone: '16.1' +type: development +group: group::ai-enablement +default_enabled: false diff --git a/db/migrate/20230505115558_add_authors_and_description_to_nuget_metadatum.rb b/db/migrate/20230505115558_add_authors_and_description_to_nuget_metadatum.rb new file mode 100644 index 00000000000..366cd2151fd --- /dev/null +++ b/db/migrate/20230505115558_add_authors_and_description_to_nuget_metadatum.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AddAuthorsAndDescriptionToNugetMetadatum < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + with_lock_retries do + add_column :packages_nuget_metadata, :authors, :text, if_not_exists: true + add_column :packages_nuget_metadata, :description, :text, if_not_exists: true + end + + add_text_limit :packages_nuget_metadata, :authors, 255 + add_text_limit :packages_nuget_metadata, :description, 4000 + end + + def down + with_lock_retries do + remove_column :packages_nuget_metadata, :authors, if_exists: true + remove_column :packages_nuget_metadata, :description, if_exists: true + end + end +end diff --git a/db/schema_migrations/20230505115558 b/db/schema_migrations/20230505115558 new file mode 100644 index 00000000000..7906938db93 --- /dev/null +++ b/db/schema_migrations/20230505115558 @@ -0,0 +1 @@ +3e5c849215b0bac1a2e68bc815dc19583f4f5dbb7a205eceff6f5b7e80ed3246
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 0a510da3292..6a382cc533e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -19695,6 +19695,10 @@ CREATE TABLE packages_nuget_metadata ( license_url text, project_url text, icon_url text, + authors text, + description text, + CONSTRAINT check_d39a5fe9ee CHECK ((char_length(description) <= 4000)), + CONSTRAINT check_e2fc129ebd CHECK ((char_length(authors) <= 255)), CONSTRAINT packages_nuget_metadata_icon_url_constraint CHECK ((char_length(icon_url) <= 255)), CONSTRAINT packages_nuget_metadata_license_url_constraint CHECK ((char_length(license_url) <= 255)), CONSTRAINT packages_nuget_metadata_project_url_constraint CHECK ((char_length(project_url) <= 255)) diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index cf3a5e3a541..83076cab73a 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1038,6 +1038,7 @@ Input type: `AiActionInput` | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="mutationaiactionchat"></a>`chat` | [`AiChatInput`](#aichatinput) | Input for chat AI action. | | <a id="mutationaiactionclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | <a id="mutationaiactionexplaincode"></a>`explainCode` | [`AiExplainCodeInput`](#aiexplaincodeinput) | Input for explain_code AI action. | | <a id="mutationaiactionexplainvulnerability"></a>`explainVulnerability` | [`AiExplainVulnerabilityInput`](#aiexplainvulnerabilityinput) | Input for explain_vulnerability AI action. | @@ -27840,6 +27841,15 @@ be used as arguments). Only general use input types are listed here. For mutation input types, see the associated mutation type above. +### `AiChatInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="aichatinputcontent"></a>`content` | [`String!`](#string) | Content of the message. | +| <a id="aichatinputresourceid"></a>`resourceId` | [`AiModelID!`](#aimodelid) | Global ID of the resource to mutate. | + ### `AiExplainCodeInput` #### Arguments diff --git a/doc/api/packages/nuget.md b/doc/api/packages/nuget.md index a0761b56645..aa2b4586e9c 100644 --- a/doc/api/packages/nuget.md +++ b/doc/api/packages/nuget.md @@ -265,13 +265,14 @@ Example response: "packageContent": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/download/MyNuGetPkg/1.3.0.17/helloworld.1.3.0.17.nupkg", "catalogEntry": { "@id": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/metadata/MyNuGetPkg/1.3.0.17.json", - "authors": "", + "authors": "Author1, Author2", "dependencyGroups": [], "id": "MyNuGetPkg", "version": "1.3.0.17", "tags": "", "packageContent": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/download/MyNuGetPkg/1.3.0.17/helloworld.1.3.0.17.nupkg", - "summary": "" + "summary": "Summary of the package", + "published": "2023-05-08T17:23:25Z", } } ] @@ -307,13 +308,14 @@ Example response: "packageContent": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/download/MyNuGetPkg/1.3.0.17/helloworld.1.3.0.17.nupkg", "catalogEntry": { "@id": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/metadata/MyNuGetPkg/1.3.0.17.json", - "authors": "", + "authors": "Author1, Author2", "dependencyGroups": [], "id": "MyNuGetPkg", "version": "1.3.0.17", "tags": "", "packageContent": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/download/MyNuGetPkg/1.3.0.17/helloworld.1.3.0.17.nupkg", - "summary": "" + "summary": "Summary of the package", + "published": "2023-05-08T17:23:25Z", } } ``` @@ -347,10 +349,10 @@ Example response: "data": [ { "@type": "Package", - "authors": "", + "authors": "Author1, Author2", "id": "MyNuGetPkg", "title": "MyNuGetPkg", - "summary": "", + "summary": "Summary of the package", "totalDownloads": 0, "verified": true, "version": "1.3.0.17", diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 513659d0f68..33f51c20446 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -973,6 +973,8 @@ Under the hood, it works like this: ```ruby class SwapPrimaryKey < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + TABLE_NAME = :table_name PRIMARY_KEY = :table_name_pkey OLD_INDEX_NAME = :old_index_name diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md index a54b3fea53e..cee393df680 100644 --- a/doc/user/group/saml_sso/index.md +++ b/doc/user/group/saml_sso/index.md @@ -208,7 +208,7 @@ To change identity providers: To migrate users to a new email domain, tell users to: -1. Add their new email as the primary email to their accounts and verify it. +1. [Add their new email](../../profile/index.md#change-your-primary-email) as the primary email to their accounts and verify it. 1. Optional. Remove their old email from the account. If the **NameID** is configured with the email address, [change the **NameID** for users](#manage-user-saml-identity). diff --git a/lib/api/entities/nuget/metadatum.rb b/lib/api/entities/nuget/metadatum.rb index 256b916cb64..c316dfce740 100644 --- a/lib/api/entities/nuget/metadatum.rb +++ b/lib/api/entities/nuget/metadatum.rb @@ -4,6 +4,12 @@ module API module Entities module Nuget class Metadatum < Grape::Entity + expose :authors, documentation: { type: 'string', example: 'Authors' } do |metadatum| + metadatum[:authors] || '' + end + expose :description, as: :summary, documentation: { type: 'string', example: 'Description' } do |metadatum| + metadatum[:description] || '' + end expose :project_url, as: :projectUrl, expose_nil: false, documentation: { type: 'string', example: 'http://sandbox.com/project' } expose :license_url, as: :licenseUrl, expose_nil: false, documentation: { type: 'string', example: 'http://sandbox.com/license' } expose :icon_url, as: :iconUrl, expose_nil: false, documentation: { type: 'string', example: 'http://sandbox.com/icon' } diff --git a/lib/api/entities/nuget/package_metadata_catalog_entry.rb b/lib/api/entities/nuget/package_metadata_catalog_entry.rb index ce328c5a5ca..b6e768e5083 100644 --- a/lib/api/entities/nuget/package_metadata_catalog_entry.rb +++ b/lib/api/entities/nuget/package_metadata_catalog_entry.rb @@ -5,16 +5,15 @@ module API module Nuget class PackageMetadataCatalogEntry < Grape::Entity expose :json_url, as: :@id, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/1/packages/nuget/metadata/MyNuGetPkg/1.3.0.17.json' } - expose :authors, documentation: { type: 'string', example: 'Author' } expose :dependency_groups, as: :dependencyGroups, using: ::API::Entities::Nuget::DependencyGroup, documentation: { is_array: true, type: 'API::Entities::Nuget::DependencyGroup' } expose :package_name, as: :id, documentation: { type: 'string', example: 'MyNuGetPkg' } expose :package_version, as: :version, documentation: { type: 'string', example: '1.3.0.17' } expose :tags, documentation: { type: 'string', example: 'tag#1 tag#2' } expose :archive_url, as: :packageContent, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/1/packages/nuget/download/MyNuGetPkg/1.3.0.17/helloworld.1.3.0.17.nupkg' } - expose :summary, documentation: { type: 'string', example: 'Summary' } expose :metadatum, using: ::API::Entities::Nuget::Metadatum, merge: true, documentation: { type: 'API::Entities::Nuget::Metadatum' } + expose :published, documentation: { type: 'string', example: '2023-05-08T17:23:25Z' } end end end diff --git a/lib/api/entities/nuget/search_result.rb b/lib/api/entities/nuget/search_result.rb index bb3698de30b..303efa7718e 100644 --- a/lib/api/entities/nuget/search_result.rb +++ b/lib/api/entities/nuget/search_result.rb @@ -5,10 +5,8 @@ module API module Nuget class SearchResult < Grape::Entity expose :type, as: :@type, documentation: { type: 'string', example: 'Package' } - expose :authors, documentation: { type: 'string', example: 'Author' } expose :name, as: :id, documentation: { type: 'string', example: 'MyNuGetPkg' } expose :name, as: :title, documentation: { type: 'string', example: 'MyNuGetPkg' } - expose :summary, documentation: { type: 'string', example: 'Summary' } expose :total_downloads, as: :totalDownloads, documentation: { type: 'integer', example: 1 } expose :verified, documentation: { type: 'boolean' } expose :version, documentation: { type: 'string', example: '1.3.0.17' } diff --git a/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb b/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb index d15a0eaa44c..8e4373a9784 100644 --- a/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb +++ b/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb @@ -5,6 +5,7 @@ module Gitlab module QueryAnalyzers class PreventCrossDatabaseModification < Database::QueryAnalyzers::Base CrossDatabaseModificationAcrossUnsupportedTablesError = Class.new(QueryAnalyzerError) + QUERY_LIMIT = 10 # This method will allow cross database modifications within the block # Example: @@ -42,7 +43,8 @@ module Gitlab context.merge!({ transaction_depth_by_db: Hash.new { |h, k| h[k] = 0 }, modified_tables_by_db: Hash.new { |h, k| h[k] = Set.new }, - ignored_tables: [] + ignored_tables: [], + queries: [] }) end @@ -71,6 +73,7 @@ module Gitlab context[:transaction_depth_by_db][database] -= 1 if context[:transaction_depth_by_db][database] == 0 context[:modified_tables_by_db][database].clear + clear_queries # Attempt to troubleshoot https://gitlab.com/gitlab-org/gitlab/-/issues/351531 ::CrossDatabaseModification::TransactionStackTrackRecord.log_gitlab_transactions_stack(action: :end_of_transaction) @@ -104,6 +107,7 @@ module Gitlab # databases return if tables == ['schema_migrations'] + add_to_queries(sql) context[:modified_tables_by_db][database].merge(tables) all_tables = context[:modified_tables_by_db].values.flat_map(&:to_a) schemas = ::Gitlab::Database::GitlabSchema.table_schemas!(all_tables) @@ -111,15 +115,17 @@ module Gitlab schemas += ApplicationRecord.gitlab_transactions_stack if schemas.many? - message = "Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \ - "a transaction modifying the '#{all_tables.to_a.join(", ")}' tables. " \ - "Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions for details on how to resolve this exception." + messages = ["Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \ + "a transaction modifying the '#{all_tables.to_a.join(", ")}' tables. " \ + "Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions for details on how to resolve this exception."] - raise CrossDatabaseModificationAcrossUnsupportedTablesError, message + messages << cleaned_queries + + raise CrossDatabaseModificationAcrossUnsupportedTablesError, messages.join("\n\n") end rescue CrossDatabaseModificationAcrossUnsupportedTablesError => e ::Gitlab::ErrorTracking.track_exception(e, { gitlab_schemas: schemas, tables: all_tables, query: parsed.sql }) - raise if raise_exception? + raise if dev_or_test_env? end # rubocop:enable Metrics/AbcSize @@ -159,12 +165,28 @@ module Gitlab end end - # We only raise in tests for now otherwise some features will be broken - # in development. For now we've mostly only added allowlist based on - # spec names. Until we have allowed all the violations inline we don't - # want to raise in development. - def self.raise_exception? - Rails.env.test? + def self.dev_or_test_env? + Gitlab.dev_or_test_env? + end + + def self.clear_queries + return unless dev_or_test_env? + + context[:queries].clear + end + + def self.add_to_queries(sql) + return unless dev_or_test_env? + + context[:queries].push(sql) + end + + def self.cleaned_queries + return '' unless dev_or_test_env? + + context[:queries].last(QUERY_LIMIT).each_with_index.map do |sql, i| + "#{i}: #{sql}" + end.join("\n") end def self.in_transaction? diff --git a/lib/gitlab/project_authorizations.rb b/lib/gitlab/project_authorizations.rb index c770260a66e..eedea2f0997 100644 --- a/lib/gitlab/project_authorizations.rb +++ b/lib/gitlab/project_authorizations.rb @@ -120,7 +120,9 @@ module Gitlab .merge(user.group_members) .merge(GroupMember.active_state) - union = Namespace.from_union([shared_groups, member_groups_with_ancestors]) + union = Namespace + .select("namespaces.id, access_level") + .from_union([shared_groups, member_groups_with_ancestors]) Gitlab::SQL::CTE.new(:linear_namespaces_cte, union) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c08cf3fbb67..b0bd90ed3bf 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -31101,9 +31101,6 @@ msgstr "" msgid "Now, personalize your GitLab experience" msgstr "" -msgid "Nuget metadatum must have at least license_url, project_url or icon_url set" -msgstr "" - msgid "Number of Elasticsearch shards and replicas per index:" msgstr "" @@ -39255,9 +39252,6 @@ msgstr "" msgid "Runners|Enter the number of seconds." msgstr "" -msgid "Runners|Enter the number of seconds. This timeout takes precedence over lower timeouts set for the project." -msgstr "" - msgid "Runners|Environment" msgstr "" @@ -39787,9 +39781,6 @@ msgstr "" msgid "Runners|Use the runner for jobs without tags in addition to tagged jobs." msgstr "" -msgid "Runners|Use the runner for jobs without tags, in addition to tagged jobs." -msgstr "" - msgid "Runners|Use the runner for the currently assigned projects only. Only administrators can change the assigned projects." msgstr "" @@ -50381,9 +50372,6 @@ msgstr "" msgid "Vulnerability|Evidence:" msgstr "" -msgid "Vulnerability|Experiment" -msgstr "" - msgid "Vulnerability|Explain this vulnerability" msgstr "" @@ -50453,6 +50441,9 @@ msgstr "" msgid "Vulnerability|Request/Response" msgstr "" +msgid "Vulnerability|Response generated by AI" +msgstr "" + msgid "Vulnerability|Scanner Provider" msgstr "" @@ -50495,9 +50486,6 @@ msgstr "" msgid "Vulnerability|This is an experimental feature that uses AI to explain the vulnerability and provide recommendations. Use this feature with caution as we continue to iterate. Please provide your feedback and ideas in %{linkStart}this issue%{linkEnd}." msgstr "" -msgid "Vulnerability|This response is generated by AI." -msgstr "" - msgid "Vulnerability|Tool" msgstr "" diff --git a/package.json b/package.json index 536d49680b5..1d4bd4ec48c 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,8 @@ "@apollo/client": "^3.5.10", "@babel/core": "^7.18.5", "@babel/preset-env": "^7.18.2", - "@cubejs-client/core": "^0.33.0", - "@cubejs-client/vue": "^0.33.0", + "@cubejs-client/core": "^0.33.12", + "@cubejs-client/vue": "^0.33.12", "@gitlab/at.js": "1.5.7", "@gitlab/cluster-client": "^1.2.0", "@gitlab/favicon-overlay": "2.0.0", diff --git a/spec/components/previews/layouts/horizontal_section_component_preview.rb b/spec/components/previews/layouts/horizontal_section_component_preview.rb index cc7e8c8c2b1..7393020077f 100644 --- a/spec/components/previews/layouts/horizontal_section_component_preview.rb +++ b/spec/components/previews/layouts/horizontal_section_component_preview.rb @@ -13,9 +13,9 @@ module Layouts body: 'Settings fields here.' ) render(::Layouts::HorizontalSectionComponent.new(border: border, options: { class: 'gl-mb-6 gl-pb-3' })) do |c| - c.title { title } - c.description { description } - c.body { body } + c.with_title { title } + c.with_description { description } + c.with_body { body } end end end diff --git a/spec/components/previews/pajamas/banner_component_preview.rb b/spec/components/previews/pajamas/banner_component_preview.rb index 19f4f5243c0..db9bf2c51d6 100644 --- a/spec/components/previews/pajamas/banner_component_preview.rb +++ b/spec/components/previews/pajamas/banner_component_preview.rb @@ -32,7 +32,7 @@ module Pajamas # like rendering a partial that holds your button. def with_primary_action_slot render(Pajamas::BannerComponent.new) do |c| - c.primary_action do + c.with_primary_action do # You could also `render` another partial here. tag.button "I'm special", class: "btn btn-md btn-confirm gl-button" end @@ -44,7 +44,7 @@ module Pajamas # but for example, an inline SVG via `custom_icon`. def with_illustration_slot render(Pajamas::BannerComponent.new) do |c| - c.illustration do + c.with_illustration do '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="white" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-up"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>'.html_safe # rubocop:disable Layout/LineLength end content_tag :p, "This banner uses the illustration slot." diff --git a/spec/factories/packages/nuget/metadata.rb b/spec/factories/packages/nuget/metadata.rb index d2a2a666928..08a52997786 100644 --- a/spec/factories/packages/nuget/metadata.rb +++ b/spec/factories/packages/nuget/metadata.rb @@ -4,6 +4,8 @@ FactoryBot.define do factory :nuget_metadatum, class: 'Packages::Nuget::Metadatum' do package { association(:nuget_package) } + authors { 'Authors' } + description { 'Description' } license_url { 'http://www.gitlab.com' } project_url { 'http://www.gitlab.com' } icon_url { 'http://www.gitlab.com' } diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/package_metadata.json b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/package_metadata.json index 1244cbe474e..9c80f1621ad 100644 --- a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/package_metadata.json +++ b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/package_metadata.json @@ -1,26 +1,67 @@ { "type": "object", - "required": ["@id", "packageContent", "catalogEntry"], + "required": [ + "@id", + "packageContent", + "catalogEntry" + ], "properties": { - "@id": { "type": "string" }, - "packageContent": { "type": "string" }, + "@id": { + "type": "string" + }, + "packageContent": { + "type": "string" + }, "catalogEntry": { "type": "object", - "required": ["@id", "authors", "dependencyGroups", "id", "packageContent", "summary", "version"], + "required": [ + "@id", + "authors", + "dependencyGroups", + "id", + "packageContent", + "summary", + "version" + ], "properties": { - "@id": { "type": "string" }, - "authors": { "const": "" }, - "id": { "type": "string" }, - "packageContent": { "type": "string" }, - "summary": { "const": "" }, - "tags": { "type": "string" }, - "projectUrl": { "type": "string" }, - "licenseUrl": { "type": "string" }, - "iconUrl": { "type": "string" }, - "version": { "type": "string" }, + "@id": { + "type": "string" + }, + "authors": { + "type": "string" + }, + "id": { + "type": "string" + }, + "packageContent": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "tags": { + "type": "string" + }, + "projectUrl": { + "type": "string" + }, + "licenseUrl": { + "type": "string" + }, + "iconUrl": { + "type": "string" + }, + "version": { + "type": "string" + }, + "published": { + "type": "string" + }, "dependencyGroups": { "type": "array", - "items": { "$ref": "./dependency_group.json" } + "items": { + "$ref": "./dependency_group.json" + } } } } diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/packages_metadata.json b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/packages_metadata.json index 0fa59bc3bec..94b5ad48a1e 100644 --- a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/packages_metadata.json +++ b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/packages_metadata.json @@ -1,41 +1,96 @@ { "type": "object", - "required": ["count", "items"], + "required": [ + "count", + "items" + ], "properties": { "items": { "type": "array", "items": { "type": "object", - "required": ["lower", "upper", "count", "items"], + "required": [ + "lower", + "upper", + "count", + "items" + ], "properties": { - "lower": { "type": "string" }, - "upper": { "type": "string" }, - "count": { "type": "integer" }, + "lower": { + "type": "string" + }, + "upper": { + "type": "string" + }, + "count": { + "type": "integer" + }, "items": { "type": "array", "items": { "type": "object", - "required": ["@id", "packageContent", "catalogEntry"], + "required": [ + "@id", + "packageContent", + "catalogEntry" + ], "properties": { - "@id": { "type": "string" }, - "packageContent": { "type": "string" }, + "@id": { + "type": "string" + }, + "packageContent": { + "type": "string" + }, "catalogEntry": { "type": "object", - "required": ["@id", "authors", "dependencyGroups", "id", "packageContent", "summary", "version"], + "required": [ + "@id", + "authors", + "dependencyGroups", + "id", + "packageContent", + "summary", + "version" + ], "properties": { - "@id": { "type": "string" }, - "authors": { "const": "" }, - "id": { "type": "string" }, - "packageContent": { "type": "string" }, - "summary": { "const": "" }, - "tags": { "type": "string" }, - "projectUrl": { "type": "string" }, - "licenseUrl": { "type": "string" }, - "iconUrl": { "type": "string" }, - "version": { "type": "string" }, + "@id": { + "type": "string" + }, + "authors": { + "type": "string" + }, + "id": { + "type": "string" + }, + "packageContent": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "tags": { + "type": "string" + }, + "projectUrl": { + "type": "string" + }, + "licenseUrl": { + "type": "string" + }, + "iconUrl": { + "type": "string" + }, + "version": { + "type": "string" + }, + "published": { + "type": "string" + }, "dependencyGroups": { "type": "array", - "items": { "$ref": "./dependency_group.json" } + "items": { + "$ref": "./dependency_group.json" + } } } } @@ -47,7 +102,3 @@ } } } - - - - diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/search.json b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/search.json index 73d0927e24c..41ad7379d73 100644 --- a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/search.json +++ b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/search.json @@ -1,34 +1,83 @@ { "type": "object", - "required": ["totalHits", "data"], + "required": [ + "totalHits", + "data" + ], "properties": { - "totalHits": { "type": "integer" }, + "totalHits": { + "type": "integer" + }, "data": { "type": "array", "items": { "type": "object", - "required": ["@type", "authors", "id", "summary", "title", "totalDownloads", "verified", "versions"], + "required": [ + "@type", + "authors", + "id", + "summary", + "title", + "totalDownloads", + "verified", + "versions" + ], "properties": { - "@type": { "const": "Package" }, - "authors": { "const": "" }, - "id": { "type": "string" }, - "summary": { "const": "" }, - "title": { "type": "string" }, - "totalDownloads": { "const": 0 }, - "verified": { "const": true }, - "tags": { "type": "string" }, - "projectUrl": { "type": "string" }, - "licenseUrl": { "type": "string" }, - "iconUrl": { "type": "string" }, + "@type": { + "const": "Package" + }, + "authors": { + "type": "string" + }, + "id": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "title": { + "type": "string" + }, + "totalDownloads": { + "const": 0 + }, + "verified": { + "const": true + }, + "tags": { + "type": "string" + }, + "projectUrl": { + "type": "string" + }, + "licenseUrl": { + "type": "string" + }, + "iconUrl": { + "type": "string" + }, "versions": { "type": "array", "items": { "type": "object", - "required": ["@id", "version", "downloads"], + "required": [ + "@id", + "version", + "downloads" + ], "properties": { - "@id": { "type": "string" }, - "version": { "type": "string" }, - "downloads": { "const": 0 } + "@id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "downloads": { + "const": 0 + }, + "published": { + "type": "string" + } } } } diff --git a/spec/frontend/ci/runner/components/runner_create_form_spec.js b/spec/frontend/ci/runner/components/runner_create_form_spec.js index 243d23aeb38..f11667ee415 100644 --- a/spec/frontend/ci/runner/components/runner_create_form_spec.js +++ b/spec/frontend/ci/runner/components/runner_create_form_spec.js @@ -126,8 +126,8 @@ describe('RunnerCreateForm', () => { expect(wrapper.emitted('saved')[0]).toEqual([mockCreatedRunner]); }); - it('does not show a saving state', () => { - expect(findSubmitBtn().props('loading')).toBe(false); + it('maintains a saving state before navigating away', () => { + expect(findSubmitBtn().props('loading')).toBe(true); }); }); diff --git a/spec/frontend/ci/runner/components/runner_form_fields_spec.js b/spec/frontend/ci/runner/components/runner_form_fields_spec.js index 0e2f2aa2e91..98f170d8f18 100644 --- a/spec/frontend/ci/runner/components/runner_form_fields_spec.js +++ b/spec/frontend/ci/runner/components/runner_form_fields_spec.js @@ -1,4 +1,6 @@ import { nextTick } from 'vue'; +import { GlSkeletonLoader } from '@gitlab/ui'; +import { s__ } from '~/locale'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue'; import { @@ -8,46 +10,97 @@ import { } from '~/ci/runner/constants'; const mockDescription = 'My description'; +const mockNewDescription = 'My new description'; const mockMaxTimeout = 60; const mockTags = 'tag, tag2'; describe('RunnerFormFields', () => { let wrapper; + const findInputByLabel = (label) => wrapper.findByLabelText(label); const findInput = (name) => wrapper.find(`input[name="${name}"]`); - const createComponent = ({ runner } = {}) => { + const expectRendersFields = () => { + expect(wrapper.text()).toContain(s__('Runners|Details')); + expect(wrapper.text()).toContain(s__('Runners|Configuration')); + + expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(0); + expect(wrapper.findAll('input')).toHaveLength(6); + }; + + const createComponent = ({ ...props } = {}) => { wrapper = mountExtended(RunnerFormFields, { propsData: { - value: runner, + ...props, }, }); }; + describe('when runner is loading', () => { + beforeEach(() => { + createComponent({ loading: true }); + }); + + it('renders a loading frame', () => { + expect(wrapper.text()).toContain(s__('Runners|Details')); + expect(wrapper.text()).toContain(s__('Runners|Configuration')); + + expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(2); + expect(wrapper.findAll('input')).toHaveLength(0); + }); + + describe('and then is loaded', () => { + beforeEach(() => { + wrapper.setProps({ loading: false, value: { description: mockDescription } }); + }); + + it('renders fields', () => { + expectRendersFields(); + }); + }); + }); + + it('when runner is loaded, renders fields', () => { + createComponent({ + value: { description: mockDescription }, + }); + + expectRendersFields(); + }); + + it('when runner is updated with the same value, only emits when changed (avoids infinite loop)', async () => { + createComponent({ value: null, loading: true }); + await wrapper.setProps({ value: { description: mockDescription }, loading: false }); + await wrapper.setProps({ value: { description: mockDescription }, loading: false }); + + expect(wrapper.emitted('input')).toHaveLength(1); + }); + it('updates runner fields', async () => { - createComponent(); + createComponent({ + value: { description: mockDescription }, + }); expect(wrapper.emitted('input')).toBe(undefined); - findInput('description').setValue(mockDescription); + findInputByLabel(s__('Runners|Runner description')).setValue(mockNewDescription); findInput('max-timeout').setValue(mockMaxTimeout); - findInput('paused').setChecked(true); - findInput('protected').setChecked(true); - findInput('run-untagged').setChecked(true); findInput('tags').setValue(mockTags); await nextTick(); - expect(wrapper.emitted('input')[0][0]).toMatchObject({ - description: mockDescription, - maximumTimeout: mockMaxTimeout, - tagList: mockTags, - }); + expect(wrapper.emitted('input').at(-1)).toEqual([ + { + description: mockNewDescription, + maximumTimeout: mockMaxTimeout, + tagList: mockTags, + }, + ]); }); it('checks checkbox fields', async () => { createComponent({ - runner: { + value: { paused: false, accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: false, @@ -60,11 +113,13 @@ describe('RunnerFormFields', () => { await nextTick(); - expect(wrapper.emitted('input')[0][0]).toEqual({ - paused: true, - accessLevel: ACCESS_LEVEL_REF_PROTECTED, - runUntagged: true, - }); + expect(wrapper.emitted('input').at(-1)).toEqual([ + { + paused: true, + accessLevel: ACCESS_LEVEL_REF_PROTECTED, + runUntagged: true, + }, + ]); }); it('locked checkbox is not shown', () => { @@ -75,7 +130,7 @@ describe('RunnerFormFields', () => { it('when runner is of project type, locked checkbox can be checked', async () => { createComponent({ - runner: { + value: { runnerType: PROJECT_TYPE, locked: false, }, @@ -85,15 +140,17 @@ describe('RunnerFormFields', () => { await nextTick(); - expect(wrapper.emitted('input')[0][0]).toEqual({ - runnerType: PROJECT_TYPE, - locked: true, - }); + expect(wrapper.emitted('input').at(-1)).toEqual([ + { + runnerType: PROJECT_TYPE, + locked: true, + }, + ]); }); it('unchecks checkbox fields', async () => { createComponent({ - runner: { + value: { paused: true, accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true, @@ -106,10 +163,12 @@ describe('RunnerFormFields', () => { await nextTick(); - expect(wrapper.emitted('input')[0][0]).toEqual({ - paused: false, - accessLevel: ACCESS_LEVEL_NOT_PROTECTED, - runUntagged: false, - }); + expect(wrapper.emitted('input').at(-1)).toEqual([ + { + paused: false, + accessLevel: ACCESS_LEVEL_NOT_PROTECTED, + runUntagged: false, + }, + ]); }); }); diff --git a/spec/frontend/ci/runner/components/runner_update_form_spec.js b/spec/frontend/ci/runner/components/runner_update_form_spec.js index d1d4e38f47c..5851078a8d3 100644 --- a/spec/frontend/ci/runner/components/runner_update_form_spec.js +++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js @@ -1,20 +1,17 @@ -import Vue, { nextTick } from 'vue'; -import { GlForm, GlSkeletonLoader } from '@gitlab/ui'; +import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import { GlForm } from '@gitlab/ui'; import { __ } from '~/locale'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; +import { visitUrl } from '~/lib/utils/url_utility'; + import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert, VARIANT_SUCCESS } from '~/alert'; -import { visitUrl } from '~/lib/utils/url_utility'; + +import { runnerToModel } from 'ee_else_ce/ci/runner/runner_update_form_utils'; +import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue'; import RunnerUpdateForm from '~/ci/runner/components/runner_update_form.vue'; -import { - INSTANCE_TYPE, - GROUP_TYPE, - PROJECT_TYPE, - ACCESS_LEVEL_REF_PROTECTED, - ACCESS_LEVEL_NOT_PROTECTED, -} from '~/ci/runner/constants'; import runnerUpdateMutation from '~/ci/runner/graphql/edit/runner_update.mutation.graphql'; import { captureException } from '~/ci/runner/sentry_utils'; import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage'; @@ -38,16 +35,7 @@ describe('RunnerUpdateForm', () => { let runnerUpdateHandler; const findForm = () => wrapper.findComponent(GlForm); - const findPausedCheckbox = () => wrapper.findByTestId('runner-field-paused'); - const findProtectedCheckbox = () => wrapper.findByTestId('runner-field-protected'); - const findRunUntaggedCheckbox = () => wrapper.findByTestId('runner-field-run-untagged'); - const findLockedCheckbox = () => wrapper.findByTestId('runner-field-locked'); - const findFields = () => wrapper.findAll('[data-testid^="runner-field"'); - - const findDescriptionInput = () => wrapper.findByTestId('runner-field-description').find('input'); - const findMaxJobTimeoutInput = () => - wrapper.findByTestId('runner-field-max-timeout').find('input'); - const findTagsInput = () => wrapper.findByTestId('runner-field-tags').find('input'); + const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields); const findSubmit = () => wrapper.find('[type="submit"]'); const findSubmitDisabledAttr = () => findSubmit().attributes('disabled'); @@ -55,21 +43,10 @@ describe('RunnerUpdateForm', () => { const submitForm = () => findForm().trigger('submit'); const submitFormAndWait = () => submitForm().then(waitForPromises); - const getFieldsModel = () => ({ - paused: findPausedCheckbox().element.checked, - accessLevel: findProtectedCheckbox().element.checked - ? ACCESS_LEVEL_REF_PROTECTED - : ACCESS_LEVEL_NOT_PROTECTED, - runUntagged: findRunUntaggedCheckbox().element.checked, - locked: findLockedCheckbox().element?.checked || false, - maximumTimeout: findMaxJobTimeoutInput().element.value || null, - tagList: findTagsInput().element.value.split(',').filter(Boolean), - }); - const createComponent = ({ props } = {}) => { wrapper = mountExtended(RunnerUpdateForm, { propsData: { - runner: mockRunner, + runner: null, runnerPath: mockRunnerPath, ...props, }, @@ -106,141 +83,82 @@ describe('RunnerUpdateForm', () => { }, }); }); + }); + it('form has fields, submit and cancel buttons', () => { createComponent(); - }); - it('Form has a submit button', () => { + expect(findRunnerFormFields().exists()).toBe(true); expect(findSubmit().exists()).toBe(true); - }); - - it('Form fields match data', () => { - expect(mockRunner).toMatchObject(getFieldsModel()); - }); - - it('Form shows a cancel button', () => { - expect(runnerUpdateHandler).not.toHaveBeenCalled(); expect(findCancelBtn().attributes('href')).toBe(mockRunnerPath); }); - it('Form prevent multiple submissions', async () => { - await submitForm(); - - expect(findSubmitDisabledAttr()).toBe('disabled'); - }); - - it('Updates runner with no changes', async () => { - await submitFormAndWait(); - - // Some read-only fields are not submitted - const { __typename, shortSha, runnerType, createdAt, status, ...submitted } = mockRunner; - - expectToHaveSubmittedRunnerContaining(submitted); - }); - describe('When data is being loaded', () => { beforeEach(() => { createComponent({ props: { loading: true } }); }); - it('Form skeleton is shown', () => { - expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); - expect(findFields()).toHaveLength(0); + it('form has no runner', () => { + expect(findRunnerFormFields().props('value')).toBe(null); }); - it('Form cannot be submitted', () => { + it('form cannot be submitted', () => { expect(findSubmit().props('loading')).toBe(true); }); + }); + + describe('When runner has loaded', () => { + beforeEach(async () => { + createComponent({ props: { loading: true } }); - it('Form is updated when data loads', async () => { - wrapper.setProps({ + await wrapper.setProps({ loading: false, + runner: mockRunner, }); - - await nextTick(); - - expect(findFields()).not.toHaveLength(0); - expect(mockRunner).toMatchObject(getFieldsModel()); }); - }); - it.each` - runnerType | exists | outcome - ${INSTANCE_TYPE} | ${false} | ${'hidden'} - ${GROUP_TYPE} | ${false} | ${'hidden'} - ${PROJECT_TYPE} | ${true} | ${'shown'} - `(`When runner is $runnerType, locked field is $outcome`, ({ runnerType, exists }) => { - const runner = { ...mockRunner, runnerType }; - createComponent({ props: { runner } }); + it('shows runner fields', () => { + expect(findRunnerFormFields().props('value')).toEqual(runnerToModel(mockRunner)); + }); - expect(findLockedCheckbox().exists()).toBe(exists); - }); + it('form has not been submitted', () => { + expect(runnerUpdateHandler).not.toHaveBeenCalled(); + }); - describe('On submit, runner gets updated', () => { - it.each` - test | initialValue | findCheckbox | checked | submitted - ${'pauses'} | ${{ paused: false }} | ${findPausedCheckbox} | ${true} | ${{ paused: true }} - ${'activates'} | ${{ paused: true }} | ${findPausedCheckbox} | ${false} | ${{ paused: false }} - ${'unprotects'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }} | ${findProtectedCheckbox} | ${true} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }} - ${'protects'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }} | ${findProtectedCheckbox} | ${false} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }} - ${'"runs untagged jobs"'} | ${{ runUntagged: true }} | ${findRunUntaggedCheckbox} | ${false} | ${{ runUntagged: false }} - ${'"runs tagged jobs"'} | ${{ runUntagged: false }} | ${findRunUntaggedCheckbox} | ${true} | ${{ runUntagged: true }} - ${'locks'} | ${{ runnerType: PROJECT_TYPE, locked: true }} | ${findLockedCheckbox} | ${false} | ${{ locked: false }} - ${'unlocks'} | ${{ runnerType: PROJECT_TYPE, locked: false }} | ${findLockedCheckbox} | ${true} | ${{ locked: true }} - `('Checkbox $test runner', async ({ initialValue, findCheckbox, checked, submitted }) => { - const runner = { ...mockRunner, ...initialValue }; - createComponent({ props: { runner } }); - - await findCheckbox().setChecked(checked); - await submitFormAndWait(); + it('Form prevents multiple submissions', async () => { + await submitForm(); - expectToHaveSubmittedRunnerContaining({ - id: runner.id, - ...submitted, - }); + expect(findSubmitDisabledAttr()).toBe('disabled'); }); - it.each` - test | initialValue | findInput | value | submitted - ${'description'} | ${{ description: 'Desc. 1' }} | ${findDescriptionInput} | ${'Desc. 2'} | ${{ description: 'Desc. 2' }} - ${'max timeout'} | ${{ maximumTimeout: 36000 }} | ${findMaxJobTimeoutInput} | ${'40000'} | ${{ maximumTimeout: 40000 }} - ${'tags'} | ${{ tagList: ['tag1'] }} | ${findTagsInput} | ${'tag2, tag3'} | ${{ tagList: ['tag2', 'tag3'] }} - `("Field updates runner's $test", async ({ initialValue, findInput, value, submitted }) => { - const runner = { ...mockRunner, ...initialValue }; - createComponent({ props: { runner } }); - - await findInput().setValue(value); + it('Updates runner with no changes', async () => { await submitFormAndWait(); - expectToHaveSubmittedRunnerContaining({ - id: runner.id, - ...submitted, - }); + // Some read-only fields are not submitted + const { __typename, shortSha, runnerType, createdAt, status, ...submitted } = mockRunner; + + expectToHaveSubmittedRunnerContaining(submitted); }); - it.each` - value | submitted - ${''} | ${{ tagList: [] }} - ${'tag1, tag2'} | ${{ tagList: ['tag1', 'tag2'] }} - ${'with spaces'} | ${{ tagList: ['with spaces'] }} - ${'more ,,,,, commas'} | ${{ tagList: ['more', 'commas'] }} - `('Field updates runner\'s tags for "$value"', async ({ value, submitted }) => { - const runner = { ...mockRunner, tagList: ['tag1'] }; - createComponent({ props: { runner } }); - - await findTagsInput().setValue(value); + it('Updates runner with changes', async () => { + findRunnerFormFields().vm.$emit( + 'input', + runnerToModel({ ...mockRunner, description: 'A new description' }), + ); await submitFormAndWait(); - expectToHaveSubmittedRunnerContaining({ - id: runner.id, - ...submitted, - }); + expectToHaveSubmittedRunnerContaining({ description: 'A new description' }); }); }); describe('On error', () => { - beforeEach(() => { + beforeEach(async () => { createComponent(); + + await wrapper.setProps({ + loading: false, + runner: mockRunner, + }); }); it('On network error, error message is shown', async () => { diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js index 4716f807657..65c16697d44 100644 --- a/spec/frontend/environments/environment_folder_spec.js +++ b/spec/frontend/environments/environment_folder_spec.js @@ -35,7 +35,7 @@ describe('~/environments/components/environments_folder.vue', () => { ...propsData, }, stubs: { transition: stubTransition() }, - provide: { helpPagePath: '/help', projectId: '1' }, + provide: { helpPagePath: '/help', projectId: '1', projectPath: 'path/to/project' }, }); beforeEach(() => { diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js index addbf2c21dc..91268ade1e9 100644 --- a/spec/frontend/environments/graphql/mock_data.js +++ b/spec/frontend/environments/graphql/mock_data.js @@ -800,12 +800,14 @@ export const resolvedDeploymentDetails = { }; export const agent = { - project: 'agent-project', id: 'gid://gitlab/ClusterAgent/1', name: 'agent-name', - kubernetesNamespace: 'agent-namespace', + webPath: 'path/to/agent-page', + tokens: { nodes: [] }, }; +export const kubernetesNamespace = 'agent-namespace'; + const runningPod = { status: { phase: 'Running' } }; const pendingPod = { status: { phase: 'Pending' } }; const succeededPod = { status: { phase: 'Succeeded' } }; diff --git a/spec/frontend/environments/kubernetes_agent_info_spec.js b/spec/frontend/environments/kubernetes_agent_info_spec.js index b1795065281..9169b9284f4 100644 --- a/spec/frontend/environments/kubernetes_agent_info_spec.js +++ b/spec/frontend/environments/kubernetes_agent_info_spec.js @@ -1,26 +1,14 @@ import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import { GlIcon, GlLink, GlSprintf, GlLoadingIcon, GlAlert } from '@gitlab/ui'; +import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue'; import { AGENT_STATUSES, ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants'; -import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import getK8sClusterAgentQuery from '~/environments/graphql/queries/k8s_cluster_agent.query.graphql'; -Vue.use(VueApollo); - -const propsData = { - agentName: 'my-agent', - agentId: '1', - agentProjectPath: 'path/to/agent-config-project', -}; - -const mockClusterAgent = { - id: '1', - name: 'token-1', +const defaultClusterAgent = { + name: 'my-agent', + id: 'gid://gitlab/ClusterAgent/1', webPath: 'path/to/agent-page', }; @@ -29,27 +17,16 @@ const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNE describe('~/environments/components/kubernetes_agent_info.vue', () => { let wrapper; - let agentQueryResponse; - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findAgentLink = () => wrapper.findComponent(GlLink); const findAgentStatus = () => wrapper.findByTestId('agent-status'); const findAgentStatusIcon = () => findAgentStatus().findComponent(GlIcon); const findAgentLastUsedDate = () => wrapper.findByTestId('agent-last-used-date'); - const findAlert = () => wrapper.findComponent(GlAlert); - - const createWrapper = ({ tokens = [], queryResponse = null } = {}) => { - const clusterAgent = { ...mockClusterAgent, tokens: { nodes: tokens } }; - - agentQueryResponse = - queryResponse || - jest.fn().mockResolvedValue({ data: { project: { id: 'project-1', clusterAgent } } }); - const apolloProvider = createMockApollo([[getK8sClusterAgentQuery, agentQueryResponse]]); + const createWrapper = ({ tokens = [] } = {}) => { wrapper = extendedWrapper( shallowMount(KubernetesAgentInfo, { - apolloProvider, - propsData, + propsData: { clusterAgent: { ...defaultClusterAgent, tokens: { nodes: tokens } } }, stubs: { TimeAgoTooltip, GlSprintf }, }), ); @@ -60,28 +37,9 @@ describe('~/environments/components/kubernetes_agent_info.vue', () => { createWrapper(); }); - it('shows loading icon while fetching the agent details', async () => { - expect(findLoadingIcon().exists()).toBe(true); - await waitForPromises(); - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('sends expected params', async () => { - await waitForPromises(); - - const variables = { - agentName: propsData.agentName, - projectPath: propsData.agentProjectPath, - }; - - expect(agentQueryResponse).toHaveBeenCalledWith(variables); - }); - - it('renders the agent name with the link', async () => { - await waitForPromises(); - - expect(findAgentLink().attributes('href')).toBe(mockClusterAgent.webPath); - expect(findAgentLink().text()).toContain(mockClusterAgent.id); + it('renders the agent name with the link', () => { + expect(findAgentLink().attributes('href')).toBe(defaultClusterAgent.webPath); + expect(findAgentLink().text()).toContain('1'); }); }); @@ -110,15 +68,4 @@ describe('~/environments/components/kubernetes_agent_info.vue', () => { expect(findAgentLastUsedDate().text()).toBe(lastUsedText); }); }); - - describe('when the agent query has errored', () => { - beforeEach(() => { - createWrapper({ clusterAgent: null, queryResponse: jest.fn().mockRejectedValue() }); - return waitForPromises(); - }); - - it('displays an alert message', () => { - expect(findAlert().text()).toBe(KubernetesAgentInfo.i18n.loadingError); - }); - }); }); diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js index 394fd200edf..d4ba7323aaf 100644 --- a/spec/frontend/environments/kubernetes_overview_spec.js +++ b/spec/frontend/environments/kubernetes_overview_spec.js @@ -5,14 +5,12 @@ import KubernetesOverview from '~/environments/components/kubernetes_overview.vu import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue'; import KubernetesPods from '~/environments/components/kubernetes_pods.vue'; import KubernetesTabs from '~/environments/components/kubernetes_tabs.vue'; -import { agent } from './graphql/mock_data'; +import { agent, kubernetesNamespace } from './graphql/mock_data'; import { mockKasTunnelUrl } from './mock_data'; const propsData = { - agentId: agent.id, - agentName: agent.name, - agentProjectPath: agent.project, - namespace: agent.kubernetesNamespace, + clusterAgent: agent, + namespace: kubernetesNamespace, }; const provide = { @@ -91,23 +89,19 @@ describe('~/environments/components/kubernetes_overview.vue', () => { }); it('renders kubernetes agent info', () => { - expect(findAgentInfo().props()).toEqual({ - agentName: agent.name, - agentId: agent.id, - agentProjectPath: agent.project, - }); + expect(findAgentInfo().props('clusterAgent')).toEqual(agent); }); it('renders kubernetes pods', () => { expect(findKubernetesPods().props()).toEqual({ - namespace: agent.kubernetesNamespace, + namespace: kubernetesNamespace, configuration, }); }); it('renders kubernetes tabs', () => { expect(findKubernetesTabs().props()).toEqual({ - namespace: agent.kubernetesNamespace, + namespace: kubernetesNamespace, configuration, }); }); diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js index 51d66043d0a..02100046167 100644 --- a/spec/frontend/environments/new_environment_item_spec.js +++ b/spec/frontend/environments/new_environment_item_spec.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import { GlCollapse, GlIcon } from '@gitlab/ui'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import { stubTransition } from 'helpers/stub_transition'; import { formatDate, getTimeago } from '~/lib/utils/datetime_utility'; import { __, s__, sprintf } from '~/locale'; @@ -11,6 +12,7 @@ import EnvironmentActions from '~/environments/components/environment_actions.vu import Deployment from '~/environments/components/deployment.vue'; import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue'; import KubernetesOverview from '~/environments/components/kubernetes_overview.vue'; +import getEnvironmentClusterAgent from '~/environments/graphql/queries/environment_cluster_agent.query.graphql'; import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data'; import { mockKasTunnelUrl } from './mock_data'; @@ -18,9 +20,24 @@ Vue.use(VueApollo); describe('~/environments/components/new_environment_item.vue', () => { let wrapper; + let queryResponseHandler; - const createApolloProvider = () => { - return createMockApollo(); + const projectPath = '/1'; + + const createApolloProvider = (clusterAgent = null) => { + const response = { + data: { + project: { + id: '1', + environment: { + id: '1', + clusterAgent, + }, + }, + }, + }; + queryResponseHandler = jest.fn().mockResolvedValue(response); + return createMockApollo([[getEnvironmentClusterAgent, queryResponseHandler]]); }; const createWrapper = ({ propsData = {}, provideData = {}, apolloProvider } = {}) => @@ -30,7 +47,7 @@ describe('~/environments/components/new_environment_item.vue', () => { provide: { helpPagePath: '/help', projectId: '1', - projectPath: '/1', + projectPath, kasTunnelUrl: mockKasTunnelUrl, ...provideData, }, @@ -501,68 +518,69 @@ describe('~/environments/components/new_environment_item.vue', () => { }); describe('kubernetes overview', () => { - const environmentWithAgent = { - ...resolvedEnvironment, - agent, - }; - - it('should render if the feature flag is enabled and the environment has an agent object with the required data specified', () => { + it('should request agent data when the environment is visible if the feature flag is enabled', async () => { wrapper = createWrapper({ - propsData: { environment: environmentWithAgent }, + propsData: { environment: resolvedEnvironment }, provideData: { glFeatures: { kasUserAccessProject: true, }, }, - apolloProvider: createApolloProvider(), + apolloProvider: createApolloProvider(agent), }); - expandCollapsedSection(); + await expandCollapsedSection(); - expect(findKubernetesOverview().props()).toMatchObject({ - agentProjectPath: agent.project, - agentName: agent.name, - agentId: agent.id, - namespace: agent.kubernetesNamespace, + expect(queryResponseHandler).toHaveBeenCalledWith({ + environmentName: resolvedEnvironment.name, + projectFullPath: projectPath, }); }); - it('should not render if the feature flag is not enabled', () => { + it('should render if the feature flag is enabled and the environment has an agent associated', async () => { wrapper = createWrapper({ - propsData: { environment: environmentWithAgent }, - apolloProvider: createApolloProvider(), + propsData: { environment: resolvedEnvironment }, + provideData: { + glFeatures: { + kasUserAccessProject: true, + }, + }, + apolloProvider: createApolloProvider(agent), }); - expandCollapsedSection(); + await expandCollapsedSection(); + await waitForPromises(); - expect(findKubernetesOverview().exists()).toBe(false); + expect(findKubernetesOverview().props()).toMatchObject({ + clusterAgent: agent, + }); }); - it('should not render if the environment has no agent object', () => { + it('should not render if the feature flag is not enabled', async () => { wrapper = createWrapper({ - apolloProvider: createApolloProvider(), + propsData: { environment: resolvedEnvironment }, + apolloProvider: createApolloProvider(agent), }); - expandCollapsedSection(); + await expandCollapsedSection(); + expect(queryResponseHandler).not.toHaveBeenCalled(); expect(findKubernetesOverview().exists()).toBe(false); }); - it('should not render if the environment has an agent object without agent id specified', () => { - const environment = { - ...resolvedEnvironment, - agent: { - project: agent.project, - name: agent.name, - }, - }; - + it('should not render if the environment has no agent object', async () => { wrapper = createWrapper({ - propsData: { environment }, + propsData: { environment: resolvedEnvironment }, + provideData: { + glFeatures: { + kasUserAccessProject: true, + }, + }, apolloProvider: createApolloProvider(), }); - expandCollapsedSection(); + await expandCollapsedSection(); + await waitForPromises(); expect(findKubernetesOverview().exists()).toBe(false); }); diff --git a/spec/lib/api/entities/nuget/metadatum_spec.rb b/spec/lib/api/entities/nuget/metadatum_spec.rb index 210ff0abdd3..cb4e53a1960 100644 --- a/spec/lib/api/entities/nuget/metadatum_spec.rb +++ b/spec/lib/api/entities/nuget/metadatum_spec.rb @@ -2,9 +2,11 @@ require 'spec_helper' -RSpec.describe API::Entities::Nuget::Metadatum do +RSpec.describe API::Entities::Nuget::Metadatum, feature_category: :package_registry do let(:metadatum) do { + authors: 'Authors', + description: 'Description', project_url: 'http://sandbox.com/project', license_url: 'http://sandbox.com/license', icon_url: 'http://sandbox.com/icon' @@ -13,6 +15,8 @@ RSpec.describe API::Entities::Nuget::Metadatum do let(:expected) do { + 'authors': 'Authors', + 'summary': 'Description', 'projectUrl': 'http://sandbox.com/project', 'licenseUrl': 'http://sandbox.com/license', 'iconUrl': 'http://sandbox.com/icon' @@ -27,11 +31,27 @@ RSpec.describe API::Entities::Nuget::Metadatum do %i[project_url license_url icon_url].each do |optional_field| context "metadatum without #{optional_field}" do - let(:metadatum_without_a_field) { metadatum.except(optional_field) } - let(:expected_without_a_field) { expected.except(optional_field.to_s.camelize(:lower).to_sym) } - let(:entity) { described_class.new(metadatum_without_a_field) } + let(:metadatum) { super().merge(optional_field => nil) } - it { is_expected.to eq(expected_without_a_field) } + it { is_expected.not_to have_key(optional_field.to_s.camelize(:lower).to_sym) } + end + end + + describe 'authors' do + context 'with default value' do + let(:metadatum) { super().merge(authors: nil) } + + it { is_expected.to have_key(:authors) } + it { is_expected.to eq(expected.merge(authors: '')) } + end + end + + describe 'description' do + context 'with default value' do + let(:metadatum) { super().merge(description: nil) } + + it { is_expected.to have_key(:summary) } + it { is_expected.to eq(expected.merge(summary: '')) } end end end diff --git a/spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb b/spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb index c422b51bf3b..2fad42f907b 100644 --- a/spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb +++ b/spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb @@ -2,18 +2,19 @@ require 'spec_helper' -RSpec.describe API::Entities::Nuget::PackageMetadataCatalogEntry do +RSpec.describe API::Entities::Nuget::PackageMetadataCatalogEntry, feature_category: :package_registry do let(:entry) do { json_url: 'http://sandbox.com/json/package', - authors: 'Authors', dependency_groups: [], package_name: 'PackageTest', package_version: '1.2.3', tags: 'tag1 tag2 tag3', archive_url: 'http://sandbox.com/archive/package', - summary: 'Summary', + published: '2022-10-05T18:40:32.43+00:00', metadatum: { + authors: 'Authors', + description: 'Summary', project_url: 'http://sandbox.com/project', license_url: 'http://sandbox.com/license', icon_url: 'http://sandbox.com/icon' @@ -33,7 +34,8 @@ RSpec.describe API::Entities::Nuget::PackageMetadataCatalogEntry do 'summary': 'Summary', 'projectUrl': 'http://sandbox.com/project', 'licenseUrl': 'http://sandbox.com/license', - 'iconUrl': 'http://sandbox.com/icon' + 'iconUrl': 'http://sandbox.com/icon', + 'published': '2022-10-05T18:40:32.43+00:00' } end diff --git a/spec/lib/api/entities/nuget/search_result_spec.rb b/spec/lib/api/entities/nuget/search_result_spec.rb index a24cd44be9e..5edff28824f 100644 --- a/spec/lib/api/entities/nuget/search_result_spec.rb +++ b/spec/lib/api/entities/nuget/search_result_spec.rb @@ -2,11 +2,10 @@ require 'spec_helper' -RSpec.describe API::Entities::Nuget::SearchResult do +RSpec.describe API::Entities::Nuget::SearchResult, feature_category: :package_registry do let(:search_result) do { type: 'Package', - authors: 'Author', name: 'PackageTest', version: '1.2.3', versions: [ @@ -16,11 +15,12 @@ RSpec.describe API::Entities::Nuget::SearchResult do version: '1.2.3' } ], - summary: 'Summary', total_downloads: 100, verified: true, tags: 'tag1 tag2 tag3', metadatum: { + authors: 'Author', + description: 'Description', project_url: 'http://sandbox.com/project', license_url: 'http://sandbox.com/license', icon_url: 'http://sandbox.com/icon' @@ -34,7 +34,7 @@ RSpec.describe API::Entities::Nuget::SearchResult do 'authors': 'Author', 'id': 'PackageTest', 'title': 'PackageTest', - 'summary': 'Summary', + 'summary': 'Description', 'totalDownloads': 100, 'verified': true, 'version': '1.2.3', diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb index 02bd6b51463..3ccdb907cba 100644 --- a/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb +++ b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb @@ -57,13 +57,19 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio end end - shared_examples 'cross-database modification errors' do |model:| + shared_examples 'cross-database modification errors' do |model:, sql_log_contains:| let(:model) { model } context "within #{model} transaction" do it 'raises error' do model.transaction do - expect { run_queries }.to raise_error /Cross-database data modification/ + expect { run_queries }.to raise_error do |error| + expect(error.message).to include 'Cross-database data modification' + + sql_log_contains.each do |sql_query| + expect(error.message).to match sql_query + end + end end end end @@ -87,7 +93,8 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio include_examples 'successful examples', model: Ci::Pipeline - include_examples 'cross-database modification errors', model: Project + include_examples 'cross-database modification errors', model: Project, + sql_log_contains: [/UPDATE "ci_pipelines"/] end context 'when other data is modified' do @@ -98,7 +105,8 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio include_examples 'successful examples', model: Project - include_examples 'cross-database modification errors', model: Ci::Pipeline + include_examples 'cross-database modification errors', model: Ci::Pipeline, + sql_log_contains: [/UPDATE "projects"/] end context 'when both CI and other data is modified' do @@ -112,11 +120,8 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio end context 'when data modification happens in a transaction' do - it 'raises error' do - Project.transaction do - expect { run_queries }.to raise_error /Cross-database data modification/ - end - end + include_examples 'cross-database modification errors', model: Project, + sql_log_contains: [/UPDATE "projects"/, /UPDATE "ci_pipelines"/] context 'when ci_pipelines are ignored for cross modification' do it 'does not raise error' do @@ -131,11 +136,16 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio end context 'when data modification happens in nested transactions' do - it 'raises error' do + it 'raises error, with the generated sql queries included' do Project.transaction(requires_new: true) do project.touch Project.transaction(requires_new: true) do - expect { pipeline.touch }.to raise_error /Cross-database data modification/ + expect { pipeline.touch }.to raise_error do |error| + expect(error.message).to include('Cross-database data modification') + + expect(error.message).to match(/UPDATE "projects"/) + expect(error.message).to match(/UPDATE "ci_pipelines"/) + end end end end @@ -151,11 +161,8 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio Marginalia::Comment.prepend_comment = prepend_comment_was end - it 'raises error' do - Project.transaction do - expect { run_queries }.to raise_error /Cross-database data modification/ - end - end + include_examples 'cross-database modification errors', model: Project, + sql_log_contains: [/UPDATE "projects"/, /UPDATE "ci_pipelines"/] end end @@ -170,11 +177,8 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio end context 'when data modification happens in a transaction' do - it 'raises error' do - Project.transaction do - expect { run_queries }.to raise_error /Cross-database data modification/ - end - end + include_examples 'cross-database modification errors', model: Project, + sql_log_contains: [/UPDATE "projects"/, /SELECT "ci_pipelines"."id".*FOR UPDATE/] context 'when the modification is inside a factory save! call' do let(:runner) { create(:ci_runner, :project, projects: [build(:project)]) } @@ -194,7 +198,8 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio include_examples 'successful examples', model: Ci::Pipeline - include_examples 'cross-database modification errors', model: Project + include_examples 'cross-database modification errors', model: Project, + sql_log_contains: [/INSERT INTO "ci_variables"/] end describe '.allow_cross_database_modification_within_transaction' do diff --git a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb index 98fb154fb05..b8829cc794c 100644 --- a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb +++ b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb @@ -127,8 +127,8 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do form_builder.gitlab_ui_checkbox_component( :view_diffs_file_by_file ) do |c| - c.label { "Show one file at a time on merge request's Changes tab" } - c.help_text { 'Instead of all the files changed, show only one file at a time.' } + c.with_label { "Show one file at a time on merge request's Changes tab" } + c.with_help_text { 'Instead of all the files changed, show only one file at a time.' } end end @@ -208,8 +208,8 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do :access_level, :admin ) do |c| - c.label { "Admin" } - c.help_text { 'Administrators have access to all groups, projects, and users and can manage all features in this installation' } + c.with_label { "Admin" } + c.with_help_text { 'Administrators have access to all groups, projects, and users and can manage all features in this installation' } end end diff --git a/spec/migrations/20230505115558_add_authors_and_description_to_nuget_metadatum_spec.rb b/spec/migrations/20230505115558_add_authors_and_description_to_nuget_metadatum_spec.rb new file mode 100644 index 00000000000..11e8ec39476 --- /dev/null +++ b/spec/migrations/20230505115558_add_authors_and_description_to_nuget_metadatum_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe AddAuthorsAndDescriptionToNugetMetadatum, feature_category: :package_registry do + let(:metadatum) { table(:packages_nuget_metadata) } + + it 'correctly migrates up and down' do + reversible_migration do |migration| + migration.before -> { + expect(metadatum.column_names).not_to include('authors') + expect(metadatum.column_names).not_to include('description') + } + + migration.after -> { + metadatum.reset_column_information + + expect(metadatum.column_names).to include('authors') + expect(metadatum.column_names).to include('description') + } + end + end +end diff --git a/spec/models/analytics/cycle_analytics/value_stream_spec.rb b/spec/models/analytics/cycle_analytics/value_stream_spec.rb index e32fbef30ae..f290cf25ae6 100644 --- a/spec/models/analytics/cycle_analytics/value_stream_spec.rb +++ b/spec/models/analytics/cycle_analytics/value_stream_spec.rb @@ -27,6 +27,20 @@ RSpec.describe Analytics::CycleAnalytics::ValueStream, type: :model, feature_cat end end + describe 'scopes' do + let_it_be(:group) { create(:group) } + + describe '.order_by_name_asc' do + let_it_be(:stream1) { create(:cycle_analytics_value_stream, namespace: group, name: 'Bbb') } + let_it_be(:stream2) { create(:cycle_analytics_value_stream, namespace: group, name: 'aaa') } + let_it_be(:stream3) { create(:cycle_analytics_value_stream, namespace: group, name: 'Aaa') } + + it 'returns in case-insensitive alphabetical order' do + expect(described_class.order_by_name_asc).to eq [stream2, stream3, stream1] + end + end + end + describe 'ordering of stages' do let(:group) { create(:group) } let(:value_stream) do diff --git a/spec/models/packages/nuget/metadatum_spec.rb b/spec/models/packages/nuget/metadatum_spec.rb index c1bc5429500..6c652f78849 100644 --- a/spec/models/packages/nuget/metadatum_spec.rb +++ b/spec/models/packages/nuget/metadatum_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Packages::Nuget::Metadatum, type: :model do +RSpec.describe Packages::Nuget::Metadatum, type: :model, feature_category: :package_registry do describe 'relationships' do it { is_expected.to belong_to(:package).inverse_of(:nuget_metadatum) } end @@ -10,6 +10,11 @@ RSpec.describe Packages::Nuget::Metadatum, type: :model do describe 'validations' do it { is_expected.to validate_presence_of(:package) } + it { is_expected.to validate_presence_of(:authors) } + it { is_expected.to validate_length_of(:authors).is_at_most(described_class::MAX_AUTHORS_LENGTH) } + it { is_expected.to validate_presence_of(:description) } + it { is_expected.to validate_length_of(:description).is_at_most(described_class::MAX_DESCRIPTION_LENGTH) } + %i[license_url project_url icon_url].each do |url| describe "##{url}" do it { is_expected.to allow_value('http://sandbox.com').for(url) } @@ -18,17 +23,6 @@ RSpec.describe Packages::Nuget::Metadatum, type: :model do it { is_expected.not_to allow_value('sandbox.com').for(url) } end - describe '#ensure_at_least_one_field_supplied' do - subject { build(:nuget_metadatum) } - - it 'rejects unfilled metadatum' do - subject.attributes = { license_url: nil, project_url: nil, icon_url: nil } - - expect(subject).not_to be_valid - expect(subject.errors).to contain_exactly('Nuget metadatum must have at least license_url, project_url or icon_url set') - end - end - describe '#ensure_nuget_package_type' do subject { build(:nuget_metadatum) } diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index d07a4e9f207..ee8d811971a 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -3283,6 +3283,32 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do end end + describe ':read_model_experiments' do + using RSpec::Parameterized::TableSyntax + + where(:ff_ml_experiment_tracking, :current_user, :access_level, :allowed) do + false | ref(:owner) | Featurable::ENABLED | false + true | ref(:guest) | Featurable::ENABLED | true + true | ref(:guest) | Featurable::PRIVATE | true + true | ref(:guest) | Featurable::DISABLED | false + true | ref(:non_member) | Featurable::ENABLED | true + true | ref(:non_member) | Featurable::PRIVATE | false + true | ref(:non_member) | Featurable::DISABLED | false + end + with_them do + before do + stub_feature_flags(ml_experiment_tracking: ff_ml_experiment_tracking) + project.project_feature.update!(model_experiments_access_level: access_level) + end + + if params[:allowed] + it { is_expected.to be_allowed(:read_model_experiments) } + else + it { is_expected.not_to be_allowed(:read_model_experiments) } + end + end + end + private def project_subject(project_type) diff --git a/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb b/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb index 6c56763e719..616fb8e8e4e 100644 --- a/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb +++ b/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Packages::Nuget::PackageMetadataPresenter do +RSpec.describe Packages::Nuget::PackageMetadataPresenter, feature_category: :package_registry do include_context 'with expected presenters dependency groups' let_it_be(:package) { create(:nuget_package, :with_symbol_package, :with_metadatum) } @@ -44,13 +44,12 @@ RSpec.describe Packages::Nuget::PackageMetadataPresenter do expect(entry).to be_a Hash %i[json_url archive_url].each { |field| expect(entry[field]).not_to be_blank } - %i[authors summary].each { |field| expect(entry[field]).to be_blank } expect(entry[:dependency_groups]).to eq expected_dependency_groups(package.project_id, package.name, package.version) expect(entry[:package_name]).to eq package.name expect(entry[:package_version]).to eq package.version expect(entry[:tags].split(::Packages::Tag::NUGET_TAGS_SEPARATOR)).to contain_exactly('tag1', 'tag2') - %i[project_url license_url icon_url].each do |field| + %i[authors description project_url license_url icon_url].each do |field| expect(entry.dig(:metadatum, field)).to eq(package.nuget_metadatum.send(field)) end end diff --git a/spec/presenters/packages/nuget/search_results_presenter_spec.rb b/spec/presenters/packages/nuget/search_results_presenter_spec.rb index 745914c6c43..08c7d71599f 100644 --- a/spec/presenters/packages/nuget/search_results_presenter_spec.rb +++ b/spec/presenters/packages/nuget/search_results_presenter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Packages::Nuget::SearchResultsPresenter do +RSpec.describe Packages::Nuget::SearchResultsPresenter, feature_category: :package_registry do let_it_be(:project) { create(:project) } let_it_be(:package_a) { create(:nuget_package, :with_metadatum, project: project, name: 'DummyPackageA') } let_it_be(:tag1) { create(:packages_tag, package: package_a, name: 'tag1') } @@ -30,15 +30,12 @@ RSpec.describe Packages::Nuget::SearchResultsPresenter do expect_package_result(pkg_c, packages_c.first.name, packages_c.map(&:version)) end - # rubocop:disable Metrics/AbcSize def expect_package_result(package_json, name, versions, tags = [], with_metadatum: false) expect(package_json[:type]).to eq 'Package' - expect(package_json[:authors]).to be_blank expect(package_json[:name]).to eq(name) - expect(package_json[:summary]).to be_blank expect(package_json[:total_downloads]).to eq 0 - expect(package_json[:verified]).to be - expect(package_json[:version]).to eq VersionSorter.sort(versions).last # rubocop: disable Style/RedundantSort + expect(package_json[:verified]).to be_truthy + expect(package_json[:version]).to eq VersionSorter.sort(versions).last versions.zip(package_json[:versions]).each do |version, version_json| expect(version_json[:json_url]).to end_with("#{version}.json") expect(version_json[:downloads]).to eq 0 @@ -51,10 +48,9 @@ RSpec.describe Packages::Nuget::SearchResultsPresenter do expect(package_json[:tags]).to be_blank end - %i[project_url license_url icon_url].each do |field| + %i[authors description project_url license_url icon_url].each do |field| expect(package_json.dig(:metadatum, field)).to with_metadatum ? be_present : be_blank end end - # rubocop:enable Metrics/AbcSize end end diff --git a/spec/services/ci/runners/assign_runner_service_spec.rb b/spec/services/ci/runners/assign_runner_service_spec.rb index 92f6db2bdfb..00fbb5e2d26 100644 --- a/spec/services/ci/runners/assign_runner_service_spec.rb +++ b/spec/services/ci/runners/assign_runner_service_spec.rb @@ -3,10 +3,12 @@ require 'spec_helper' RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category: :runner_fleet do - subject(:execute) { described_class.new(runner, project, user).execute } + subject(:execute) { described_class.new(runner, new_project, user).execute } - let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) } - let_it_be(:project) { create(:project) } + let_it_be(:owner_group) { create(:group) } + let_it_be(:owner_project) { create(:project, group: owner_group) } + let_it_be(:new_project) { create(:project) } + let_it_be(:runner) { create(:ci_runner, :project, projects: [owner_project]) } context 'without user' do let(:user) { nil } @@ -30,11 +32,54 @@ RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category: end end + context 'with authorized user' do + let(:user) { create(:user) } + + context 'with user owning runner and being maintainer of new project' do + before do + owner_project.group.add_owner(user) + new_project.add_maintainer(user) + end + + it 'calls assign_to on runner and returns success response' do + expect(runner).to receive(:assign_to).with(new_project, user).once.and_call_original + + is_expected.to be_success + end + end + + context 'with user owning runner' do + before do + owner_project.add_maintainer(user) + end + + it 'does not call assign_to on runner and returns error message', :aggregate_failures do + expect(runner).not_to receive(:assign_to) + + is_expected.to be_error + expect(execute.message).to eq('user not allowed to add runners to project') + end + end + + context 'with user being maintainer of new project', :aggregate_failures do + before do + new_project.add_maintainer(user) + end + + it 'does not call assign_to on runner and returns error message' do + expect(runner).not_to receive(:assign_to) + + is_expected.to be_error + expect(execute.message).to eq('user not allowed to assign runner') + end + end + end + context 'with admin user', :enable_admin_mode do - let(:user) { create_default(:user, :admin) } + let(:user) { create(:user, :admin) } it 'calls assign_to on runner and returns success response' do - expect(runner).to receive(:assign_to).with(project, user).once.and_call_original + expect(runner).to receive(:assign_to).with(new_project, user).once.and_call_original is_expected.to be_success end diff --git a/spec/services/packages/nuget/metadata_extraction_service_spec.rb b/spec/services/packages/nuget/metadata_extraction_service_spec.rb index 9177a5379d9..8954b89971e 100644 --- a/spec/services/packages/nuget/metadata_extraction_service_spec.rb +++ b/spec/services/packages/nuget/metadata_extraction_service_spec.rb @@ -10,10 +10,16 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa describe '#execute' do subject { service.execute } + shared_examples 'raises an error' do |error_message| + it { expect { subject }.to raise_error(described_class::ExtractionError, error_message) } + end + context 'with valid package file id' do expected_metadata = { package_name: 'DummyProject.DummyPackage', package_version: '1.0.0', + authors: 'Test', + description: 'This is a dummy project', package_dependencies: [ { name: 'Newtonsoft.Json', @@ -72,15 +78,23 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa allow(service).to receive(:nuspec_file_content).and_return(fixture_file(nuspec_filepath)) end - it { expect(subject[:license_url]).to eq('https://opensource.org/licenses/MIT') } - it { expect(subject[:project_url]).to eq('https://gitlab.com/gitlab-org/gitlab') } - it { expect(subject[:icon_url]).to eq('https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png') } + it 'returns the correct metadata' do + expected_metadata = { + authors: 'Author Test', + description: 'Description Test', + license_url: 'https://opensource.org/licenses/MIT', + project_url: 'https://gitlab.com/gitlab-org/gitlab', + icon_url: 'https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png' + } + + expect(subject.slice(*expected_metadata.keys)).to eq(expected_metadata) + end end context 'with invalid package file id' do let(:package_file) { double('file', id: 555) } - it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'invalid package file') } + it_behaves_like 'raises an error', 'invalid package file' end context 'linked to a non nuget package' do @@ -88,7 +102,7 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa package_file.package.maven! end - it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'invalid package file') } + it_behaves_like 'raises an error', 'invalid package file' end context 'with a 0 byte package file id' do @@ -96,7 +110,7 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa allow_any_instance_of(Packages::PackageFileUploader).to receive(:size).and_return(0) end - it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'invalid package file') } + it_behaves_like 'raises an error', 'invalid package file' end context 'without the nuspec file' do @@ -104,7 +118,7 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa allow_any_instance_of(Zip::File).to receive(:glob).and_return([]) end - it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'nuspec file not found') } + it_behaves_like 'raises an error', 'nuspec file not found' end context 'with a too big nuspec file' do @@ -112,18 +126,17 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa allow_any_instance_of(Zip::File).to receive(:glob).and_return([double('file', size: 6.megabytes)]) end - it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'nuspec file too big') } + it_behaves_like 'raises an error', 'nuspec file too big' end context 'with a corrupted nupkg file with a wrong entry size' do let(:nupkg_fixture_path) { expand_fixture_path('packages/nuget/corrupted_package.nupkg') } - let(:expected_error) { "nuspec file has the wrong entry size: entry 'DummyProject.DummyPackage.nuspec' should be 255B, but is larger when inflated." } before do allow(Zip::File).to receive(:new).and_return(Zip::File.new(nupkg_fixture_path, false, false)) end - it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, expected_error) } + it_behaves_like 'raises an error', "nuspec file has the wrong entry size: entry 'DummyProject.DummyPackage.nuspec' should be 255B, but is larger when inflated." end end end diff --git a/spec/services/packages/nuget/sync_metadatum_service_spec.rb b/spec/services/packages/nuget/sync_metadatum_service_spec.rb index ae07f312fcc..bf352d134c0 100644 --- a/spec/services/packages/nuget/sync_metadatum_service_spec.rb +++ b/spec/services/packages/nuget/sync_metadatum_service_spec.rb @@ -6,6 +6,8 @@ RSpec.describe Packages::Nuget::SyncMetadatumService, feature_category: :package let_it_be(:package, reload: true) { create(:nuget_package) } let_it_be(:metadata) do { + authors: 'Package authors', + description: 'Package description', project_url: 'https://test.org/test', license_url: 'https://test.org/MIT', icon_url: 'https://test.org/icon.png' @@ -53,5 +55,39 @@ RSpec.describe Packages::Nuget::SyncMetadatumService, feature_category: :package end end end + + context 'with metadata containing only authors and description' do + let_it_be(:metadata) { { authors: 'Package authors 2', description: 'Package description 2' } } + + it 'updates the nuget metadatum' do + subject + + expect(nuget_metadatum.authors).to eq('Package authors 2') + expect(nuget_metadatum.description).to eq('Package description 2') + end + end + + context 'with too long metadata' do + let(:metadata) { super().merge(authors: 'a' * 260, description: 'a' * 4010) } + let(:max_authors_length) { ::Packages::Nuget::Metadatum::MAX_AUTHORS_LENGTH } + let(:max_description_length) { ::Packages::Nuget::Metadatum::MAX_DESCRIPTION_LENGTH } + + it 'truncates authors and description to the maximum length and logs its info' do + %i[authors description].each do |field| + expect(Gitlab::AppLogger).to receive(:info).with( + class: described_class.name, + package_id: package.id, + project_id: package.project_id, + message: "#{field.capitalize} is too long (maximum is #{send("max_#{field}_length")} characters)", + field => metadata[field] + ) + end + + subject + + expect(nuget_metadatum.authors.size).to eq(max_authors_length) + expect(nuget_metadatum.description.size).to eq(max_description_length) + end + end end end diff --git a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb index c35863030b0..aebd5457075 100644 --- a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb +++ b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb @@ -38,7 +38,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ shared_examples 'not updating the package if the lease is taken' do context 'without obtaining the exclusive lease' do let(:lease_key) { "packages:nuget:update_package_from_metadata_service:package:#{package_id}" } - let(:metadata) { { package_name: package_name, package_version: package_version } } + let(:metadata) { { package_name: package_name, package_version: package_version, authors: 'author1, author2', description: 'test description' } } let(:package_from_package_file) { package_file.package } before do @@ -66,12 +66,12 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ context 'with no existing package' do let(:package_id) { package.id } - it 'updates package and package file', :aggregate_failures do + it 'updates package and package file and creates metadatum', :aggregate_failures do expect { subject } .to not_change { ::Packages::Package.count } .and change { Packages::Dependency.count }.by(1) .and change { Packages::DependencyLink.count }.by(1) - .and change { ::Packages::Nuget::Metadatum.count }.by(0) + .and change { ::Packages::Nuget::Metadatum.count }.by(1) expect(package.reload.name).to eq(package_name) expect(package.version).to eq(package_version) @@ -98,7 +98,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ .and change { Packages::Dependency.count }.by(0) .and change { Packages::DependencyLink.count }.by(0) .and change { Packages::Nuget::DependencyLinkMetadatum.count }.by(0) - .and change { ::Packages::Nuget::Metadatum.count }.by(0) + .and change { ::Packages::Nuget::Metadatum.count }.by(1) expect(package_file.reload.file_name).to eq(package_file_name) expect(package_file.package).to eq(existing_package) end @@ -117,7 +117,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ .to not_change { ::Packages::Package.count } .and change { Packages::Dependency.count }.by(1) .and change { Packages::DependencyLink.count }.by(1) - .and change { ::Packages::Nuget::Metadatum.count }.by(0) + .and change { ::Packages::Nuget::Metadatum.count }.by(1) end end end @@ -158,6 +158,8 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ .and change { ::Packages::Nuget::Metadatum.count }.by(1) metadatum = package_file.reload.package.nuget_metadatum + expect(metadatum.authors).to eq('Author Test') + expect(metadatum.description).to eq('Description Test') expect(metadatum.license_url).to eq('https://opensource.org/licenses/MIT') expect(metadatum.project_url).to eq('https://gitlab.com/gitlab-org/gitlab') expect(metadatum.icon_url).to eq('https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png') @@ -172,7 +174,19 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ allow(service).to receive(:metadata).and_return(metadata) end - it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError + it_behaves_like 'raising an', described_class::InvalidMetadataError + end + + context 'without authors or description' do + %i[authors description].each do |property| + let(:metadata) { { package_name: package_name, package_version: package_version, license_url: 'http://localhost/', property => nil } } + + before do + allow(service).to receive(:metadata).and_return(metadata) + end + + it_behaves_like 'raising an', described_class::InvalidMetadataError + end end end @@ -222,13 +236,17 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ context 'with no existing package' do let(:package_id) { package.id } - it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError + it_behaves_like 'raising an', described_class::InvalidMetadataError end context 'with existing package' do let!(:existing_package) { create(:nuget_package, project: package.project, name: package_name, version: package_version) } let(:package_id) { existing_package.id } + before do + allow(service).to receive(:metadata).and_return(service.send(:metadata).merge(authors: 'Author Test')) + end + it 'link existing package and updates package file', :aggregate_failures do expect(service).to receive(:try_obtain_lease).and_call_original expect(::Packages::Nuget::SyncMetadatumService).not_to receive(:new) @@ -264,7 +282,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ allow(service).to receive(:package_name).and_return(invalid_name) end - it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError + it_behaves_like 'raising an', described_class::InvalidMetadataError end end end @@ -284,7 +302,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ allow(service).to receive(:package_version).and_return(invalid_version) end - it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError + it_behaves_like 'raising an', described_class::InvalidMetadataError end end end diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb index db685a4d0a1..94e5df8c7ae 100644 --- a/spec/support/shared_examples/features/runners_shared_examples.rb +++ b/spec/support/shared_examples/features/runners_shared_examples.rb @@ -207,7 +207,7 @@ RSpec.shared_examples 'submits edit runner form' do context 'when a runner is updated', :js do before do - find('[data-testid="runner-field-description"] input').set('new-runner-description') + fill_in s_('Runners|Runner description'), with: 'new-runner-description' click_on _('Save changes') wait_for_requests diff --git a/yarn.lock b/yarn.lock index 207e554e458..863c16e7ac2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1005,10 +1005,10 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.1.tgz#b6b8d81780b9a9f6459f4bfe9226ac6aefaefe87" integrity sha512-aG20vknL4/YjQF9BSV7ts4EWm/yrjagAN7OWBNmlbEOUiu0llj4OGrFoOKK3g2vey4/p2omKCoHrWtPxSwV3HA== -"@cubejs-client/core@^0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@cubejs-client/core/-/core-0.33.0.tgz#f6edd051e75cd104ae0e7ceae36244ee5d391485" - integrity sha512-481ENJFSYnq4cg2Z6VcTn0U6YKLSF4akMmLh8Pt8df2s8WTBXro0AamuINHYOhCfzmvPYl7e26x+wMYGG1ubMA== +"@cubejs-client/core@^0.33.12": + version "0.33.12" + resolved "https://registry.yarnpkg.com/@cubejs-client/core/-/core-0.33.12.tgz#135044493e450087ab10f035ef6eb280c9066bf1" + integrity sha512-BY4aNEIjRTL7Yj9K7pF+fETPTpyoistUVBhjxOO5Yc3HSoQWoupetonl33FEw1CIq4PczYtVQVLvmkMuGi8gPA== dependencies: "@babel/runtime" "^7.1.2" core-js "^3.6.5" @@ -1018,12 +1018,12 @@ url-search-params-polyfill "^7.0.0" uuid "^8.3.2" -"@cubejs-client/vue@^0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@cubejs-client/vue/-/vue-0.33.0.tgz#57ed9a5466de912f8176acad022d36e33d0f49fd" - integrity sha512-WPX2zQn5hYLwV0y3DaQy67AqOuRc0hA9taMFP9N6/cO3+N2DOg77ocRwgDt0/KmYRkaq4D37w87I9whAbocgtQ== +"@cubejs-client/vue@^0.33.12": + version "0.33.12" + resolved "https://registry.yarnpkg.com/@cubejs-client/vue/-/vue-0.33.12.tgz#9b1618aebc7a310a9535b4a1011f715d33dc3410" + integrity sha512-YwMtc4/AyWaXJKEbd7rEizc5xZ0t9VcM7B77aIEa+rbdMNZEurEBvnCx+gidOgk1nQ75Y0mfKk4TrKRCWg09+Q== dependencies: - "@cubejs-client/core" "^0.33.0" + "@cubejs-client/core" "^0.33.12" core-js "^3.6.5" ramda "^0.27.2" |