diff options
138 files changed, 1506 insertions, 881 deletions
diff --git a/.gitlab/issue_templates/Experiment Rollout.md b/.gitlab/issue_templates/Experiment Rollout.md index c5fdc739943..23d7f325b14 100644 --- a/.gitlab/issue_templates/Experiment Rollout.md +++ b/.gitlab/issue_templates/Experiment Rollout.md @@ -105,5 +105,16 @@ In this rollout issue, ensure the scoped `experiment::` label is kept accurate. /chatops run feature set <experiment-key> false ``` +## Experiment Successful Cleanup Concerns + +_Items to be considered if candidate experience is to become a permanent part of GitLab_ + +<!-- +Add a list of items raised during MR review or otherwise that may need further thought/condideration +before making it a permanent part of the product. + +Example: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70451#note_727246104 +--> + /label ~"feature flag" ~"devops::growth" ~"growth experiment" ~"experiment-rollout" ~Engineering ~"workflow::scheduling" ~"experiment::pending" /milestone %"Next 1-3 releases" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 46726a8fa07..238f07ac22c 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -14,7 +14,6 @@ import { import { escape } from 'lodash'; import { mapActions, mapGetters, mapState } from 'vuex'; import { IdState } from 'vendor/vue-virtual-scroller'; -import { diffViewerModes } from '~/ide/constants'; import { scrollToElement } from '~/lib/utils/common_utils'; import { truncateSha } from '~/lib/utils/text_utility'; import { __, s__, sprintf } from '~/locale'; @@ -181,7 +180,7 @@ export default { return this.diffFile.renamed_file; }, isModeChanged() { - return this.diffFile.viewer.name === diffViewerModes.mode_changed; + return this.diffFile.mode_changed; }, expandDiffToFullFileTitle() { if (this.diffFile.isShowingFullFile) { diff --git a/app/assets/javascripts/experimentation/utils.js b/app/assets/javascripts/experimentation/utils.js index 1567bf14d0f..9ae6f6b3fdd 100644 --- a/app/assets/javascripts/experimentation/utils.js +++ b/app/assets/javascripts/experimentation/utils.js @@ -3,7 +3,12 @@ import { get } from 'lodash'; import { DEFAULT_VARIANT, CANDIDATE_VARIANT, TRACKING_CONTEXT_SCHEMA } from './constants'; function getExperimentsData() { - return get(window, ['gon', 'experiment'], {}); + // Pull from deprecated window.gon.experiment + const experimentsFromGon = get(window, ['gon', 'experiment'], {}); + // Pull from preferred window.gl.experiments + const experimentsFromGl = get(window, ['gl', 'experiments'], {}); + + return { ...experimentsFromGon, ...experimentsFromGl }; } function convertExperimentDataToExperimentContext(experimentData) { diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue index 9b3cd04c4e2..c0504cbb645 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue @@ -1,5 +1,6 @@ <script> -import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; +import { GlAlert, GlLink, GlSprintf, GlEmptyState } from '@gitlab/ui'; +import { isEmpty } from 'lodash'; import { mapState, mapMutations } from 'vuex'; import { retrieveAlert } from '~/jira_connect/subscriptions/utils'; import { SET_ALERT } from '../store/mutation_types'; @@ -13,6 +14,7 @@ export default { GlAlert, GlLink, GlSprintf, + GlEmptyState, SubscriptionsList, AddNamespaceButton, SignInButton, @@ -21,12 +23,18 @@ export default { usersPath: { default: '', }, + subscriptions: { + default: [], + }, }, computed: { ...mapState(['alert']), shouldShowAlert() { return Boolean(this.alert?.message); }, + hasSubscriptions() { + return !isEmpty(this.subscriptions); + }, userSignedIn() { return Boolean(!this.usersPath); }, @@ -66,15 +74,44 @@ export default { </template> </gl-alert> - <h2 class="gl-text-center">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2> - - <div class="jira-connect-app-body gl-my-7 gl-px-5 gl-pb-4"> - <div class="gl-display-flex gl-justify-content-end"> - <sign-in-button v-if="!userSignedIn" :users-path="usersPath" /> - <add-namespace-button v-else /> - </div> + <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2> + <div class="jira-connect-app-body gl-mx-auto gl-px-5 gl-mb-7"> + <template v-if="hasSubscriptions"> + <div class="gl-display-flex gl-justify-content-end"> + <sign-in-button v-if="!userSignedIn" :users-path="usersPath" /> + <add-namespace-button v-else /> + </div> - <subscriptions-list /> + <subscriptions-list /> + </template> + <template v-else> + <div v-if="!userSignedIn" class="gl-text-center"> + <p class="gl-mb-7">{{ s__('JiraService|Sign in to GitLab.com to get started.') }}</p> + <sign-in-button class="gl-mb-7" :users-path="usersPath"> + {{ __('Sign in to GitLab') }} + </sign-in-button> + <p> + {{ + s__( + 'Integrations|Note: this integration only works with accounts on GitLab.com (SaaS).', + ) + }} + </p> + </div> + <gl-empty-state + v-else + :title="s__('Integrations|No linked namespaces')" + :description=" + s__( + 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.', + ) + " + > + <template #actions> + <add-namespace-button /> + </template> + </gl-empty-state> + </template> </div> </div> </template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue index 7062fb370ed..33126040c16 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui'; +import { GlButton, GlTable } from '@gitlab/ui'; import { isEmpty } from 'lodash'; import { mapMutations } from 'vuex'; import { removeSubscription } from '~/jira_connect/subscriptions/api'; @@ -12,7 +12,6 @@ import GroupItemName from './group_item_name.vue'; export default { components: { GlButton, - GlEmptyState, GlTable, GroupItemName, TimeagoTooltip, @@ -44,17 +43,15 @@ export default { }, ], i18n: { - emptyTitle: s__('Integrations|No linked namespaces'), - emptyDescription: s__( - 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.', - ), unlinkError: s__('Integrations|Failed to unlink namespace. Please try again.'), }, methods: { ...mapMutations({ setAlert: SET_ALERT, }), - isEmpty, + isUnlinkButtonDisabled(item) { + return !isEmpty(item); + }, isLoadingItem(item) { return this.loadingItem === item; }, @@ -81,29 +78,22 @@ export default { </script> <template> - <div> - <gl-empty-state - v-if="isEmpty(subscriptions)" - :title="$options.i18n.emptyTitle" - :description="$options.i18n.emptyDescription" - /> - <gl-table v-else :items="subscriptions" :fields="$options.fields"> - <template #cell(name)="{ item }"> - <group-item-name :group="item.group" /> - </template> - <template #cell(created_at)="{ item }"> - <timeago-tooltip :time="item.created_at" /> - </template> - <template #cell(actions)="{ item }"> - <gl-button - :class="unlinkBtnClass(item)" - category="secondary" - :loading="isLoadingItem(item)" - :disabled="!isEmpty(loadingItem)" - @click.prevent="onClick(item)" - >{{ __('Unlink') }}</gl-button - > - </template> - </gl-table> - </div> + <gl-table :items="subscriptions" :fields="$options.fields"> + <template #cell(name)="{ item }"> + <group-item-name :group="item.group" /> + </template> + <template #cell(created_at)="{ item }"> + <timeago-tooltip :time="item.created_at" /> + </template> + <template #cell(actions)="{ item }"> + <gl-button + :class="unlinkBtnClass(item)" + category="secondary" + :loading="isLoadingItem(item)" + :disabled="isUnlinkButtonDisabled(loadingItem)" + @click.prevent="onClick(item)" + >{{ __('Unlink') }}</gl-button + > + </template> + </gl-table> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index 790870ee4c6..fa4f8b76cb9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -10,8 +10,8 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import eventHub from '../../event_hub'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import getStateQuery from '../../queries/get_state.query.graphql'; -import workInProgressQuery from '../../queries/states/work_in_progress.query.graphql'; -import removeWipMutation from '../../queries/toggle_wip.mutation.graphql'; +import draftQuery from '../../queries/states/draft.query.graphql'; +import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql'; import StatusIcon from '../mr_widget_status_icon.vue'; export default { @@ -23,7 +23,7 @@ export default { mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], apollo: { userPermissions: { - query: workInProgressQuery, + query: draftQuery, skip() { return !this.glFeatures.mergeRequestWidgetGraphql; }, @@ -53,25 +53,25 @@ export default { }, }, methods: { - removeWipMutation() { + removeDraftMutation() { const { mergeRequestQueryVariables } = this; this.isMakingRequest = true; this.$apollo .mutate({ - mutation: removeWipMutation, + mutation: removeDraftMutation, variables: { ...mergeRequestQueryVariables, - wip: false, + draft: false, }, update( store, { data: { - mergeRequestSetWip: { + mergeRequestSetDraft: { errors, - mergeRequest: { mergeableDiscussionsState, workInProgress, title }, + mergeRequest: { mergeableDiscussionsState, draft, title }, }, }, }, @@ -91,7 +91,7 @@ export default { const data = produce(sourceData, (draftState) => { draftState.project.mergeRequest.mergeableDiscussionsState = mergeableDiscussionsState; - draftState.project.mergeRequest.workInProgress = workInProgress; + draftState.project.mergeRequest.draft = draft; draftState.project.mergeRequest.title = title; }); @@ -104,14 +104,14 @@ export default { optimisticResponse: { // eslint-disable-next-line @gitlab/require-i18n-strings __typename: 'Mutation', - mergeRequestSetWip: { + mergeRequestSetDraft: { __typename: 'MergeRequestSetWipPayload', errors: [], mergeRequest: { __typename: 'MergeRequest', mergeableDiscussionsState: true, title: this.mr.title, - workInProgress: false, + draft: false, }, }, }, @@ -119,7 +119,7 @@ export default { .then( ({ data: { - mergeRequestSetWip: { + mergeRequestSetDraft: { mergeRequest: { title }, }, }, @@ -137,9 +137,9 @@ export default { this.isMakingRequest = false; }); }, - handleRemoveWIP() { + handleRemoveDraft() { if (this.glFeatures.mergeRequestWidgetGraphql) { - this.removeWipMutation(); + this.removeDraftMutation(); } else { this.isMakingRequest = true; this.service @@ -178,8 +178,8 @@ export default { size="small" :disabled="isMakingRequest" :loading="isMakingRequest" - class="js-remove-wip gl-ml-3" - @click="handleRemoveWIP" + class="js-remove-draft gl-ml-3" + @click="handleRemoveDraft" > {{ s__('mrWidget|Mark as ready') }} </gl-button> diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql index 871aa880b36..bfb1517be81 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql @@ -23,7 +23,7 @@ query getState($projectPath: ID!, $iid: String!) { userPermissions { canMerge } - workInProgress + draft } } } diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql index 0983c28448e..0983c28448e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql new file mode 100644 index 00000000000..200fb1b7ca5 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql @@ -0,0 +1,10 @@ +mutation toggleDraftStatus($projectPath: ID!, $iid: String!, $draft: Boolean!) { + mergeRequestSetDraft(input: { projectPath: $projectPath, iid: $iid, draft: $draft }) { + mergeRequest { + mergeableDiscussionsState + title + draft + } + errors + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql deleted file mode 100644 index cfaa198d516..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql +++ /dev/null @@ -1,10 +0,0 @@ -mutation toggleWIPStatus($projectPath: ID!, $iid: String!, $wip: Boolean!) { - mergeRequestSetWip(input: { projectPath: $projectPath, iid: $iid, wip: $wip }) { - mergeRequest { - mergeableDiscussionsState - title - workInProgress - } - errors - } -} diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js index d52371032eb..2ae4f4da2f3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js @@ -17,8 +17,8 @@ export default function deviseState() { return stateKey.rebase; } else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) { return stateKey.pipelineFailed; - } else if (this.workInProgress) { - return stateKey.workInProgress; + } else if (this.draft) { + return stateKey.draft; } else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) { return stateKey.unresolvedDiscussions; } else if (this.isPipelineBlocked) { diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 6628225cd46..10a2907c81a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -164,7 +164,7 @@ export default class MergeRequestStore { this.projectArchived = data.project_archived; this.isSHAMismatch = this.sha !== data.diff_head_sha; this.shouldBeRebased = Boolean(data.should_be_rebased); - this.workInProgress = data.work_in_progress; + this.draft = data.draft; } const currentUser = data.current_user; @@ -207,7 +207,7 @@ export default class MergeRequestStore { this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled'; this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha; this.shouldBeRebased = mergeRequest.shouldBeRebased; - this.workInProgress = mergeRequest.workInProgress; + this.draft = mergeRequest.draft; this.mergeRequestState = mergeRequest.state; this.setState(); diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js index 4cb23407a74..9dfeaee905c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js @@ -4,7 +4,7 @@ export const stateToComponentMap = { merging: 'mr-widget-merging', conflicts: 'mr-widget-conflicts', missingBranch: 'mr-widget-missing-branch', - workInProgress: 'mr-widget-wip', + draft: 'mr-widget-wip', readyToMerge: 'mr-widget-ready-to-merge', nothingToMerge: 'mr-widget-nothing-to-merge', notAllowedToMerge: 'mr-widget-not-allowed', @@ -24,7 +24,7 @@ export const stateToComponentMap = { export const statesToShowHelpWidget = [ 'merging', 'conflicts', - 'workInProgress', + 'draft', 'readyToMerge', 'checking', 'unresolvedDiscussions', @@ -40,7 +40,7 @@ export const stateKey = { nothingToMerge: 'nothingToMerge', checking: 'checking', conflicts: 'conflicts', - workInProgress: 'workInProgress', + draft: 'draft', pipelineFailed: 'pipelineFailed', unresolvedDiscussions: 'unresolvedDiscussions', pipelineBlocked: 'pipelineBlocked', diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss index 4beb5edbe7b..9fe0490571e 100644 --- a/app/assets/stylesheets/page_bundles/jira_connect.scss +++ b/app/assets/stylesheets/page_bundles/jira_connect.scss @@ -42,8 +42,6 @@ $header-height: 40px; .jira-connect-app-body { max-width: 768px; - margin-left: auto; - margin-right: auto; } // needed for external_link diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb index 8707381bb68..fc930ffebbd 100644 --- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb +++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb @@ -113,8 +113,6 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy end def send_manifest(manifest, from_cache:) - # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536 - manifest.touch response.headers[DependencyProxy::Manifest::DIGEST_HEADER] = manifest.digest response.headers['Content-Length'] = manifest.size response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION diff --git a/app/graphql/mutations/merge_requests/set_wip.rb b/app/graphql/mutations/merge_requests/set_wip.rb deleted file mode 100644 index 9b6b67d4b4f..00000000000 --- a/app/graphql/mutations/merge_requests/set_wip.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module MergeRequests - class SetWip < Base - graphql_name 'MergeRequestSetWip' - - argument :wip, - GraphQL::Types::Boolean, - required: true, - description: <<~DESC - Whether or not to set the merge request as a draft. - DESC - - def resolve(project_path:, iid:, wip: nil) - merge_request = authorized_find!(project_path: project_path, iid: iid) - project = merge_request.project - - ::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: { wip_event: wip_event(merge_request, wip) }) - .execute(merge_request) - - { - merge_request: merge_request, - errors: errors_on_object(merge_request) - } - end - - private - - def wip_event(merge_request, wip) - wip ? 'wip' : 'unwip' - end - end - end -end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 004ac364487..a8a96b0ccef 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -53,9 +53,6 @@ module Types description: 'Indicates if the source branch is protected.' field :target_branch, GraphQL::Types::String, null: false, description: 'Target branch of the merge request.' - field :work_in_progress, GraphQL::Types::Boolean, method: :work_in_progress?, null: false, - deprecated: { reason: 'Use `draft`', milestone: '13.12' }, - description: 'Indicates if the merge request is a draft.' field :draft, GraphQL::Types::Boolean, method: :draft?, null: false, description: 'Indicates if the merge request is a draft.' field :merge_when_pipeline_succeeds, GraphQL::Types::Boolean, null: true, diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 7be995a40b2..5366283e55a 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -65,9 +65,6 @@ module Types mount_mutation Mutations::MergeRequests::SetLocked mount_mutation Mutations::MergeRequests::SetMilestone mount_mutation Mutations::MergeRequests::SetSubscription - mount_mutation Mutations::MergeRequests::SetWip, - calls_gitaly: true, - deprecated: { reason: 'Use mergeRequestSetDraft', milestone: '13.12' } mount_mutation Mutations::MergeRequests::SetDraft, calls_gitaly: true mount_mutation Mutations::MergeRequests::SetAssignees mount_mutation Mutations::MergeRequests::ReviewerRereview diff --git a/app/models/concerns/ttl_expirable.rb b/app/models/concerns/ttl_expirable.rb index 00abe0a06e6..6d89521255c 100644 --- a/app/models/concerns/ttl_expirable.rb +++ b/app/models/concerns/ttl_expirable.rb @@ -5,10 +5,11 @@ module TtlExpirable included do validates :status, presence: true + default_value_for :read_at, Time.zone.now enum status: { default: 0, expired: 1, processing: 2, error: 3 } - scope :updated_before, ->(number_of_days) { where("updated_at <= ?", Time.zone.now - number_of_days.days) } + scope :read_before, ->(number_of_days) { where("read_at <= ?", Time.zone.now - number_of_days.days) } scope :active, -> { where(status: :default) } scope :lock_next_by, ->(sort) do @@ -17,4 +18,8 @@ module TtlExpirable .lock('FOR UPDATE SKIP LOCKED') end end + + def read! + self.update(read_at: Time.zone.now) + end end diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 0c36e51120f..2775b520b2f 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -12,7 +12,8 @@ class GpgSignature < ApplicationRecord same_user_different_email: 2, other_user: 3, unverified_key: 4, - unknown_key: 5 + unknown_key: 5, + multiple_signatures: 6 } belongs_to :project diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index bd68311fcc8..5a74f8392ec 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -268,7 +268,6 @@ class MergeRequest < ApplicationRecord from_fork.where('source_project_id = ? OR target_project_id = ?', project.id, project.id) end scope :merged, -> { with_state(:merged) } - scope :closed_and_merged, -> { with_states(:closed, :merged) } scope :open_and_closed, -> { with_states(:opened, :closed) } scope :drafts, -> { where(draft: true) } scope :from_source_branches, ->(branches) { where(source_branch: branches) } diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 80abb733418..14a0c38dcf0 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -497,6 +497,10 @@ class Namespace < ApplicationRecord Feature.enabled?(:block_issue_repositioning, self, type: :ops, default_enabled: :yaml) end + def project_namespace_creation_enabled? + Feature.enabled?(:create_project_namespace_on_project_create, self, default_enabled: :yaml) + end + private def expire_child_caches diff --git a/app/models/namespaces/project_namespace.rb b/app/models/namespaces/project_namespace.rb index d1806c1c088..22ec550dee2 100644 --- a/app/models/namespaces/project_namespace.rb +++ b/app/models/namespaces/project_namespace.rb @@ -4,8 +4,6 @@ module Namespaces class ProjectNamespace < Namespace has_one :project, foreign_key: :project_namespace_id, inverse_of: :project_namespace - validates :project, presence: true - def self.sti_name 'Project' end diff --git a/app/models/packages/npm.rb b/app/models/packages/npm.rb index e49199d911c..9221187d92a 100644 --- a/app/models/packages/npm.rb +++ b/app/models/packages/npm.rb @@ -9,5 +9,9 @@ module Packages package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first end + + def self.table_name_prefix + 'packages_npm_' + end end end diff --git a/app/models/packages/npm/metadatum.rb b/app/models/packages/npm/metadatum.rb new file mode 100644 index 00000000000..7388c4bdbd2 --- /dev/null +++ b/app/models/packages/npm/metadatum.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Packages::Npm::Metadatum < ApplicationRecord + belongs_to :package, -> { where(package_type: :npm) }, inverse_of: :npm_metadatum + + validates :package, presence: true + # From https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object + validates :package_json, json_schema: { filename: "npm_package_json" } + validate :ensure_npm_package_type + validate :ensure_package_json_size + + private + + def ensure_npm_package_type + return if package&.npm? + + errors.add(:base, _('Package type must be NPM')) + end + + def ensure_package_json_size + return if package_json.to_s.size < 20000 + + errors.add(:package_json, _('structure is too large')) + end +end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index ff9e7ef4cf5..f25d0299524 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -39,6 +39,7 @@ class Packages::Package < ApplicationRecord has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum' has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum' has_one :rubygems_metadatum, inverse_of: :package, class_name: 'Packages::Rubygems::Metadatum' + has_one :npm_metadatum, inverse_of: :package, class_name: 'Packages::Npm::Metadatum' has_many :build_infos, inverse_of: :package has_many :pipelines, through: :build_infos, disable_joins: -> { disable_cross_joins_to_pipelines? } has_one :debian_publication, inverse_of: :package, class_name: 'Packages::Debian::Publication' @@ -126,6 +127,7 @@ class Packages::Package < ApplicationRecord .where(Packages::Composer::Metadatum.table_name => { target_sha: target }) end scope :preload_composer, -> { preload(:composer_metadatum) } + scope :preload_npm_metadatum, -> { preload(:npm_metadatum) } scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) } diff --git a/app/models/project.rb b/app/models/project.rb index f7f8ae1f77c..36f48b91221 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -98,7 +98,7 @@ class Project < ApplicationRecord before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? } before_save :ensure_runners_token - before_save :ensure_project_namespace_in_sync + before_validation :ensure_project_namespace_in_sync after_save :update_project_statistics, if: :saved_change_to_namespace_id? @@ -147,7 +147,7 @@ class Project < ApplicationRecord belongs_to :namespace # Sync deletion via DB Trigger to ensure we do not have # a project without a project_namespace (or vice-versa) - belongs_to :project_namespace, autosave: true, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id', inverse_of: :project + belongs_to :project_namespace, autosave: true, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id' alias_method :parent, :namespace alias_attribute :parent_id, :namespace_id @@ -476,6 +476,7 @@ class Project < ApplicationRecord validates :project_feature, presence: true validates :namespace, presence: true + validates :project_namespace, presence: true, if: -> { self.namespace && self.root_namespace.project_namespace_creation_enabled? } validates :name, uniqueness: { scope: :namespace_id } validates :import_url, public_url: { schemes: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS }, ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS }, @@ -2919,12 +2920,28 @@ class Project < ApplicationRecord end def ensure_project_namespace_in_sync - if changes.keys & [:name, :path, :namespace_id, :visibility_level] && project_namespace.present? - project_namespace.name = name - project_namespace.path = path - project_namespace.parent = namespace - project_namespace.visibility_level = visibility_level - end + # create project_namespace when project is created if create_project_namespace_on_project_create FF is enabled + build_project_namespace if project_namespace_creation_enabled? + + # regardless of create_project_namespace_on_project_create FF we need + # to keep project and project namespace in sync if there is one + sync_attributes(project_namespace) if sync_project_namespace? + end + + def project_namespace_creation_enabled? + new_record? && !project_namespace && self.namespace && self.root_namespace.project_namespace_creation_enabled? + end + + def sync_project_namespace? + (changes.keys & %w(name path namespace_id namespace visibility_level shared_runners_enabled)).any? && project_namespace.present? + end + + def sync_attributes(project_namespace) + project_namespace.name = name + project_namespace.path = path + project_namespace.parent = namespace + project_namespace.shared_runners_enabled = shared_runners_enabled + project_namespace.visibility_level = visibility_level end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 7b24ae35125..c7054b89255 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1091,6 +1091,13 @@ class Repository after_create true + rescue Gitlab::Git::Repository::RepositoryExists + # We do not want to call `#after_create` given that we didn't create the + # repo, but we obviously have a mismatch between what's in our exists cache + # and actual on-disk state as seen by Gitaly. Let's thus expire our caches. + expire_status_cache + + nil end def create_from_bundle(bundle_path) diff --git a/app/presenters/packages/npm/package_presenter.rb b/app/presenters/packages/npm/package_presenter.rb index b9595eb6647..9e3308c2573 100644 --- a/app/presenters/packages/npm/package_presenter.rb +++ b/app/presenters/packages/npm/package_presenter.rb @@ -5,26 +5,37 @@ module Packages class PackagePresenter include API::Helpers::RelatedResourcesHelpers + # Allowed fields are those defined in the abbreviated form + # defined here: https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object + # except: name, version, dist, dependencies and xDependencies. Those are generated by this presenter. + PACKAGE_JSON_ALLOWED_FIELDS = %w[deprecated bin directories dist engines _hasShrinkwrap].freeze + attr_reader :name, :packages - def initialize(name, packages) + def initialize(name, packages, include_metadata: false) @name = name @packages = packages + @include_metadata = include_metadata end def versions package_versions = {} packages.each_batch do |relation| - relation.including_dependency_links - .preload_files - .each do |package| - package_file = package.package_files.last + batched_packages = relation.including_dependency_links + .preload_files + + if @include_metadata + batched_packages = batched_packages.preload_npm_metadatum + end + + batched_packages.each do |package| + package_file = package.package_files.last - next unless package_file + next unless package_file - package_versions[package.version] = build_package_version(package, package_file) - end + package_versions[package.version] = build_package_version(package, package_file) + end end package_versions @@ -41,14 +52,14 @@ module Packages end def build_package_version(package, package_file) - { + abbreviated_package_json(package).merge( name: package.name, version: package.version, dist: { shasum: package_file.file_sha1, tarball: tarball_url(package, package_file) } - }.tap do |package_version| + ).tap do |package_version| package_version.merge!(build_package_dependencies(package)) end end @@ -79,6 +90,13 @@ module Packages Packages::Tag.for_packages(packages) .preload_package end + + def abbreviated_package_json(package) + return {} unless @include_metadata + + json = package.npm_metadatum&.package_json || {} + json.slice(*PACKAGE_JSON_ALLOWED_FIELDS) + end end end end diff --git a/app/services/dependency_proxy/find_or_create_blob_service.rb b/app/services/dependency_proxy/find_or_create_blob_service.rb index 0a6db6e3d34..1b43263a3ba 100644 --- a/app/services/dependency_proxy/find_or_create_blob_service.rb +++ b/app/services/dependency_proxy/find_or_create_blob_service.rb @@ -30,8 +30,7 @@ module DependencyProxy blob.save! end - # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536 - blob.touch if from_cache + blob.read! if from_cache success(blob: blob, from_cache: from_cache) end diff --git a/app/services/dependency_proxy/find_or_create_manifest_service.rb b/app/services/dependency_proxy/find_or_create_manifest_service.rb index 055980c431f..aeb62be9f3a 100644 --- a/app/services/dependency_proxy/find_or_create_manifest_service.rb +++ b/app/services/dependency_proxy/find_or_create_manifest_service.rb @@ -58,6 +58,8 @@ module DependencyProxy def respond(from_cache: true) if @manifest + @manifest.read! + success(manifest: @manifest, from_cache: from_cache) else error('Failed to download the manifest from the external registry', 503) diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb index 1d5d9c38432..959e105694f 100644 --- a/app/services/packages/npm/create_package_service.rb +++ b/app/services/packages/npm/create_package_service.rb @@ -21,6 +21,10 @@ module Packages ::Packages::CreateDependencyService.new(package, package_dependencies).execute ::Packages::Npm::CreateTagService.new(package, dist_tag).execute + if Feature.enabled?(:packages_npm_abbreviated_metadata, project) + package.create_npm_metadatum!(package_json: version_data) + end + package end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index ea20510f36e..b7ed9202b01 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -152,14 +152,12 @@ module Projects deleted_count = project.commit_statuses.delete_all - if deleted_count > 0 - Gitlab::AppLogger.info( - class: 'Projects::DestroyService', - project_id: project.id, - message: 'leftover commit statuses', - orphaned_commit_status_count: deleted_count - ) - end + Gitlab::AppLogger.info( + class: 'Projects::DestroyService', + project_id: project.id, + message: 'leftover commit statuses', + orphaned_commit_status_count: deleted_count + ) end # The project can have multiple webhooks with hundreds of thousands of web_hook_logs. diff --git a/app/validators/json_schemas/npm_package_json.json b/app/validators/json_schemas/npm_package_json.json new file mode 100644 index 00000000000..01bd874d214 --- /dev/null +++ b/app/validators/json_schemas/npm_package_json.json @@ -0,0 +1,26 @@ +{ + "description": "NPM package json metadata", + "type": "object", + "properties": { + "name": { "type": "string" }, + "version": { "type": "string" }, + "dist": { + "type": "object", + "properties": { + "tarball": { "type": "string" }, + "shasum": { "type": "string" } + }, + "additionalProperties": true, + "required": [ + "tarball", + "shasum" + ] + } + }, + "additionalProperties": true, + "required": [ + "name", + "version", + "dist" + ] +} diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml index 3f7f929f766..ef6479f8be2 100644 --- a/app/views/import/github/new.html.haml +++ b/app/views/import/github/new.html.haml @@ -23,7 +23,7 @@ = form_tag personal_access_token_import_github_path, method: :post do .form-group %label.label-bold= _('Personal Access Token') - = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('e.g. %{token}') % { token: '8d3f016698e...' }, data: { qa_selector: 'personal_access_token_field' } + = text_field_tag :personal_access_token, '', class: 'form-control gl-form-input', placeholder: _('e.g. %{token}') % { token: '8d3f016698e...' }, data: { qa_selector: 'personal_access_token_field' } %span.form-text.text-muted = import_github_personal_access_token_message diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml index cbe9a860210..be2be7288f8 100644 --- a/app/views/jira_connect/subscriptions/index.html.haml +++ b/app/views/jira_connect/subscriptions/index.html.haml @@ -9,20 +9,9 @@ = link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in' %main.jira-connect-app.gl-px-5.gl-pt-7.gl-mx-auto - - if current_user.blank? && @subscriptions.empty? - .jira-connect-app-body.gl-px-5.gl-text-center - %h2= s_('JiraService|GitLab for Jira Configuration') - %p= s_('JiraService|Sign in to GitLab.com to get started.') + .js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) } - .gl-mt-7 - = external_link _('Sign in to GitLab'), jira_connect_users_path, class: "btn gl-button btn-confirm js-jira-connect-sign-in" - - .gl-mt-7 - %p= s_('Integrations|Note: this integration only works with accounts on GitLab.com (SaaS).') - - else - .js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) } - - %p.jira-connect-app-body.gl-px-5.gl-mt-7.gl-font-base.gl-text-center + %p.jira-connect-app-body.gl-px-5.gl-font-base.gl-text-center.gl-mx-auto %strong= s_('Integrations|Browser limitations') - browser_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">' - firefox_link_start = browser_link_start.html_safe % { url: 'https://www.mozilla.org/en-US/firefox/' } diff --git a/app/views/layouts/_published_experiments.html.haml b/app/views/layouts/_published_experiments.html.haml new file mode 100644 index 00000000000..717e1e633d2 --- /dev/null +++ b/app/views/layouts/_published_experiments.html.haml @@ -0,0 +1,4 @@ += javascript_tag(nonce: content_security_policy_nonce) do + :plain + gl = window.gl || {}; + gl.experiments = #{raw ApplicationExperiment.published_experiments.reject { |name, data| data[:excluded] }.to_json}; diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 899bf65de48..26e3d9b3b92 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -16,4 +16,5 @@ = render 'layouts/img_loader' + = render 'layouts/published_experiments' = yield :scripts_body diff --git a/app/views/projects/commit/_multiple_signatures_signature_badge.html.haml b/app/views/projects/commit/_multiple_signatures_signature_badge.html.haml new file mode 100644 index 00000000000..74515438af2 --- /dev/null +++ b/app/views/projects/commit/_multiple_signatures_signature_badge.html.haml @@ -0,0 +1,6 @@ +- title = capture do + = html_escape(_('This commit was signed with %{strong_open}multiple%{strong_close} signatures.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe } + +- locals = { signature: signature, title: title, label: _('Unverified'), css_class: 'invalid', icon: 'status_notfound_borderless' } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 2fbaa5812c0..86cac7c8376 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -1,5 +1,4 @@ - add_page_startup_api_call discussions_path(@issue) -- add_page_startup_api_call notes_url - @gfm_form = true diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index c3b1d22a909..ae39bd60233 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -246,6 +246,15 @@ :weight: 1 :idempotent: true :tags: [] +- :name: cronjob:clusters_integrations_check_prometheus_health + :worker_name: Clusters::Integrations::CheckPrometheusHealthWorker + :feature_category: :incident_management + :has_external_dependencies: true + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: cronjob:container_expiration_policy :worker_name: ContainerExpirationPolicyWorker :feature_category: :container_registry @@ -1087,15 +1096,6 @@ :idempotent: :tags: - :needs_own_queue -- :name: incident_management:clusters_integrations_check_prometheus_health - :worker_name: Clusters::Integrations::CheckPrometheusHealthWorker - :feature_category: :incident_management - :has_external_dependencies: true - :urgency: :low - :resource_boundary: :unknown - :weight: 2 - :idempotent: true - :tags: [] - :name: incident_management:incident_management_add_severity_system_note :worker_name: IncidentManagement::AddSeveritySystemNoteWorker :feature_category: :incident_management diff --git a/app/workers/clusters/integrations/check_prometheus_health_worker.rb b/app/workers/clusters/integrations/check_prometheus_health_worker.rb index 0a583003e6f..0c0d86e975c 100644 --- a/app/workers/clusters/integrations/check_prometheus_health_worker.rb +++ b/app/workers/clusters/integrations/check_prometheus_health_worker.rb @@ -12,7 +12,6 @@ module Clusters include CronjobQueue # rubocop:enable Scalability/CronWorkerContext - queue_namespace :incident_management feature_category :incident_management urgency :low diff --git a/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb b/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb index fed469e6dc8..6a1de00ce80 100644 --- a/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb +++ b/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb @@ -13,9 +13,8 @@ module DependencyProxy def perform DependencyProxy::ImageTtlGroupPolicy.enabled.each do |policy| - # Technical Debt: change to read_before https://gitlab.com/gitlab-org/gitlab/-/issues/341536 - qualified_blobs = policy.group.dependency_proxy_blobs.active.updated_before(policy.ttl) - qualified_manifests = policy.group.dependency_proxy_manifests.active.updated_before(policy.ttl) + qualified_blobs = policy.group.dependency_proxy_blobs.active.read_before(policy.ttl) + qualified_manifests = policy.group.dependency_proxy_manifests.active.read_before(policy.ttl) enqueue_blob_cleanup_job if expire_artifacts(qualified_blobs, DependencyProxy::Blob) enqueue_manifest_cleanup_job if expire_artifacts(qualified_manifests, DependencyProxy::Manifest) diff --git a/config/feature_flags/development/create_project_namespace_on_project_create.yml b/config/feature_flags/development/create_project_namespace_on_project_create.yml new file mode 100644 index 00000000000..3fbf929ca2e --- /dev/null +++ b/config/feature_flags/development/create_project_namespace_on_project_create.yml @@ -0,0 +1,8 @@ +--- +name: create_project_namespace_on_project_create +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70972 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344954 +milestone: '14.5' +type: development +group: group::workspace +default_enabled: false diff --git a/config/feature_flags/development/rate_limiter_safe_increment.yml b/config/feature_flags/development/multiple_gpg_signatures.yml index 16e8f2fe7c7..3b9b8d0a465 100644 --- a/config/feature_flags/development/rate_limiter_safe_increment.yml +++ b/config/feature_flags/development/multiple_gpg_signatures.yml @@ -1,8 +1,8 @@ --- -name: rate_limiter_safe_increment -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73343 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285352 +name: multiple_gpg_signatures +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74095 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345261 milestone: '14.5' type: development -group: group::project management +group: group::source code default_enabled: false diff --git a/config/feature_flags/development/linear_group_plans_preloaded_ancestor_scopes.yml b/config/feature_flags/development/packages_npm_abbreviated_metadata.yml index d45b8d71a20..422051fa2fd 100644 --- a/config/feature_flags/development/linear_group_plans_preloaded_ancestor_scopes.yml +++ b/config/feature_flags/development/packages_npm_abbreviated_metadata.yml @@ -1,8 +1,8 @@ --- -name: linear_group_plans_preloaded_ancestor_scopes -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70685 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341349 -milestone: '14.4' +name: packages_npm_abbreviated_metadata +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73639 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344827 +milestone: '14.5' type: development -group: group::access +group: group::package default_enabled: false diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index debaa1e6a82..16e792487d3 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -591,6 +591,9 @@ Settings.cron_jobs['batched_background_migrations_worker']['job_class'] = 'Datab Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances'] ||= Settingslogic.new({}) Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances']['cron'] ||= '* 0/15 * * *' Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances']['job_class'] = 'Issues::RescheduleStuckIssueRebalancesWorker' +Settings.cron_jobs['clusters_integrations_check_prometheus_health_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['clusters_integrations_check_prometheus_health_worker']['cron'] ||= '0 * * * *' +Settings.cron_jobs['clusters_integrations_check_prometheus_health_worker']['job_class'] = 'Clusters::Integrations::CheckPrometheusHealthWorker' Gitlab.ee do Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= Settingslogic.new({}) diff --git a/config/initializers/postgres_partitioning.rb b/config/initializers/postgres_partitioning.rb index 883824cc16f..5af8cf52656 100644 --- a/config/initializers/postgres_partitioning.rb +++ b/config/initializers/postgres_partitioning.rb @@ -35,8 +35,4 @@ unless Gitlab.jh? ]) end -begin - Gitlab::Database::Partitioning.sync_partitions unless ENV['DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP'] -rescue ActiveRecord::ActiveRecordError, PG::Error - # ignore - happens when Rake tasks yet have to create a database, e.g. for testing -end +Gitlab::Database::Partitioning.sync_partitions_ignore_db_error diff --git a/data/deprecations/templates/example.yml b/data/deprecations/templates/example.yml index ebacb31645e..fb164247cb6 100644 --- a/data/deprecations/templates/example.yml +++ b/data/deprecations/templates/example.yml @@ -6,7 +6,7 @@ # and what types we expect those attribute values to be. # # For more information please refer to the handbook documentation here: -# {{LINK TBD}} +# https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations # # Please delete this line and above before submitting your merge request. diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 95d956fb402..fa19775a571 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -157,16 +157,17 @@ class Gitlab::Seeder::CycleAnalytics end def create_new_vsm_project + namespace = FactoryBot.create( + :group, + name: "Value Stream Management Group #{suffix}", + path: "vsmg-#{suffix}" + ) project = FactoryBot.create( :project, name: "Value Stream Management Project #{suffix}", path: "vsmp-#{suffix}", creator: admin, - namespace: FactoryBot.create( - :group, - name: "Value Stream Management Group #{suffix}", - path: "vsmg-#{suffix}" - ) + namespace: namespace ) project.create_repository diff --git a/db/migrate/20211028132247_create_packages_npm_metadata.rb b/db/migrate/20211028132247_create_packages_npm_metadata.rb new file mode 100644 index 00000000000..cbe5429fca1 --- /dev/null +++ b/db/migrate/20211028132247_create_packages_npm_metadata.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class CreatePackagesNpmMetadata < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + def up + with_lock_retries do + create_table :packages_npm_metadata, id: false do |t| + t.references :package, primary_key: true, default: nil, index: false, foreign_key: { to_table: :packages_packages, on_delete: :cascade }, type: :bigint + t.jsonb :package_json, default: {}, null: false + + t.check_constraint 'char_length(package_json::text) < 20000' + end + end + end + + def down + with_lock_retries do + drop_table :packages_npm_metadata + end + end +end diff --git a/db/migrate/20211105125756_add_read_at_to_dependency_proxy_manifests.rb b/db/migrate/20211105125756_add_read_at_to_dependency_proxy_manifests.rb new file mode 100644 index 00000000000..a594674f470 --- /dev/null +++ b/db/migrate/20211105125756_add_read_at_to_dependency_proxy_manifests.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddReadAtToDependencyProxyManifests < Gitlab::Database::Migration[1.0] + def change + add_column :dependency_proxy_manifests, :read_at, :datetime_with_timezone, null: false, default: -> { 'NOW()' } + end +end diff --git a/db/migrate/20211105125813_add_read_at_to_dependency_proxy_blobs.rb b/db/migrate/20211105125813_add_read_at_to_dependency_proxy_blobs.rb new file mode 100644 index 00000000000..1808a541498 --- /dev/null +++ b/db/migrate/20211105125813_add_read_at_to_dependency_proxy_blobs.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddReadAtToDependencyProxyBlobs < Gitlab::Database::Migration[1.0] + def change + add_column :dependency_proxy_blobs, :read_at, :datetime_with_timezone, null: false, default: -> { 'NOW()' } + end +end diff --git a/db/migrate/20211108203248_update_dependency_proxy_indexes_with_read_at.rb b/db/migrate/20211108203248_update_dependency_proxy_indexes_with_read_at.rb new file mode 100644 index 00000000000..ac0f0ddca17 --- /dev/null +++ b/db/migrate/20211108203248_update_dependency_proxy_indexes_with_read_at.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class UpdateDependencyProxyIndexesWithReadAt < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + NEW_BLOB_INDEX = 'index_dependency_proxy_blobs_on_group_id_status_read_at_id' + OLD_BLOB_INDEX = 'index_dependency_proxy_blobs_on_group_id_status_and_id' + + NEW_MANIFEST_INDEX = 'index_dependency_proxy_manifests_on_group_id_status_read_at_id' + OLD_MANIFEST_INDEX = 'index_dependency_proxy_manifests_on_group_id_status_and_id' + + def up + add_concurrent_index :dependency_proxy_blobs, [:group_id, :status, :read_at, :id], name: NEW_BLOB_INDEX + add_concurrent_index :dependency_proxy_manifests, [:group_id, :status, :read_at, :id], name: NEW_MANIFEST_INDEX + + remove_concurrent_index_by_name :dependency_proxy_blobs, OLD_BLOB_INDEX + remove_concurrent_index_by_name :dependency_proxy_manifests, OLD_MANIFEST_INDEX + end + + def down + add_concurrent_index :dependency_proxy_blobs, [:group_id, :status, :id], name: OLD_BLOB_INDEX + add_concurrent_index :dependency_proxy_manifests, [:group_id, :status, :id], name: OLD_MANIFEST_INDEX + + remove_concurrent_index_by_name :dependency_proxy_blobs, NEW_BLOB_INDEX + remove_concurrent_index_by_name :dependency_proxy_manifests, NEW_MANIFEST_INDEX + end +end diff --git a/db/schema_migrations/20211028132247 b/db/schema_migrations/20211028132247 new file mode 100644 index 00000000000..ab8fa3b55eb --- /dev/null +++ b/db/schema_migrations/20211028132247 @@ -0,0 +1 @@ +50a5c8af2cde1ae79d627f70d3b266488f76f76b481aefca8516db5360cfa843
\ No newline at end of file diff --git a/db/schema_migrations/20211105125756 b/db/schema_migrations/20211105125756 new file mode 100644 index 00000000000..842187d9ae2 --- /dev/null +++ b/db/schema_migrations/20211105125756 @@ -0,0 +1 @@ +13cf3d164d541df48b6d14d7cc1953113476ba8ea5975d7d0c5f84098e2e0e61
\ No newline at end of file diff --git a/db/schema_migrations/20211105125813 b/db/schema_migrations/20211105125813 new file mode 100644 index 00000000000..449c3d95d6a --- /dev/null +++ b/db/schema_migrations/20211105125813 @@ -0,0 +1 @@ +a48f62bed7e4c4a0e69acd3b340065317aff71602e696970276a4e443f1dcabf
\ No newline at end of file diff --git a/db/schema_migrations/20211108203248 b/db/schema_migrations/20211108203248 new file mode 100644 index 00000000000..4f8c570b627 --- /dev/null +++ b/db/schema_migrations/20211108203248 @@ -0,0 +1 @@ +a2556a3d8b21e59caa6cbf7f83d621fef391904d0c13c77c0e5da713a580b4c9
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index a109953a827..669d2549dd8 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -13237,7 +13237,8 @@ CREATE TABLE dependency_proxy_blobs ( file_store integer, file_name character varying NOT NULL, file text NOT NULL, - status smallint DEFAULT 0 NOT NULL + status smallint DEFAULT 0 NOT NULL, + read_at timestamp with time zone DEFAULT now() NOT NULL ); CREATE SEQUENCE dependency_proxy_blobs_id_seq @@ -13286,6 +13287,7 @@ CREATE TABLE dependency_proxy_manifests ( digest text NOT NULL, content_type text, status smallint DEFAULT 0 NOT NULL, + read_at timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT check_079b293a7b CHECK ((char_length(file) <= 255)), CONSTRAINT check_167a9a8a91 CHECK ((char_length(content_type) <= 255)), CONSTRAINT check_c579e3f586 CHECK ((char_length(file_name) <= 255)), @@ -17199,6 +17201,12 @@ CREATE SEQUENCE packages_maven_metadata_id_seq ALTER SEQUENCE packages_maven_metadata_id_seq OWNED BY packages_maven_metadata.id; +CREATE TABLE packages_npm_metadata ( + package_id bigint NOT NULL, + package_json jsonb DEFAULT '{}'::jsonb NOT NULL, + CONSTRAINT chk_rails_e5cbc301ae CHECK ((char_length((package_json)::text) < 20000)) +); + CREATE TABLE packages_nuget_dependency_link_metadata ( dependency_link_id bigint NOT NULL, target_framework text NOT NULL, @@ -23524,6 +23532,9 @@ ALTER TABLE ONLY packages_helm_file_metadata ALTER TABLE ONLY packages_maven_metadata ADD CONSTRAINT packages_maven_metadata_pkey PRIMARY KEY (id); +ALTER TABLE ONLY packages_npm_metadata + ADD CONSTRAINT packages_npm_metadata_pkey PRIMARY KEY (package_id); + ALTER TABLE ONLY packages_nuget_dependency_link_metadata ADD CONSTRAINT packages_nuget_dependency_link_metadata_pkey PRIMARY KEY (dependency_link_id); @@ -25637,13 +25648,13 @@ CREATE UNIQUE INDEX index_dep_prox_manifests_on_group_id_file_name_and_status ON CREATE INDEX index_dependency_proxy_blobs_on_group_id_and_file_name ON dependency_proxy_blobs USING btree (group_id, file_name); -CREATE INDEX index_dependency_proxy_blobs_on_group_id_status_and_id ON dependency_proxy_blobs USING btree (group_id, status, id); +CREATE INDEX index_dependency_proxy_blobs_on_group_id_status_read_at_id ON dependency_proxy_blobs USING btree (group_id, status, read_at, id); CREATE INDEX index_dependency_proxy_blobs_on_status ON dependency_proxy_blobs USING btree (status); CREATE INDEX index_dependency_proxy_group_settings_on_group_id ON dependency_proxy_group_settings USING btree (group_id); -CREATE INDEX index_dependency_proxy_manifests_on_group_id_status_and_id ON dependency_proxy_manifests USING btree (group_id, status, id); +CREATE INDEX index_dependency_proxy_manifests_on_group_id_status_read_at_id ON dependency_proxy_manifests USING btree (group_id, status, read_at, id); CREATE INDEX index_dependency_proxy_manifests_on_status ON dependency_proxy_manifests USING btree (status); @@ -30779,6 +30790,9 @@ ALTER TABLE ONLY atlassian_identities ALTER TABLE ONLY serverless_domain_cluster ADD CONSTRAINT fk_rails_c09009dee1 FOREIGN KEY (pages_domain_id) REFERENCES pages_domains(id) ON DELETE CASCADE; +ALTER TABLE ONLY packages_npm_metadata + ADD CONSTRAINT fk_rails_c0e5fce6f3 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE; + ALTER TABLE ONLY labels ADD CONSTRAINT fk_rails_c1ac5161d8 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 6e387cdec4a..41781fa8680 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -3509,31 +3509,6 @@ Input type: `MergeRequestSetSubscriptionInput` | <a id="mutationmergerequestsetsubscriptionerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationmergerequestsetsubscriptionmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. | -### `Mutation.mergeRequestSetWip` - -WARNING: -**Deprecated** in 13.12. -Use mergeRequestSetDraft. - -Input type: `MergeRequestSetWipInput` - -#### Arguments - -| Name | Type | Description | -| ---- | ---- | ----------- | -| <a id="mutationmergerequestsetwipclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | -| <a id="mutationmergerequestsetwipiid"></a>`iid` | [`String!`](#string) | IID of the merge request to mutate. | -| <a id="mutationmergerequestsetwipprojectpath"></a>`projectPath` | [`ID!`](#id) | Project the merge request to mutate is in. | -| <a id="mutationmergerequestsetwipwip"></a>`wip` | [`Boolean!`](#boolean) | Whether or not to set the merge request as a draft. | - -#### Fields - -| Name | Type | Description | -| ---- | ---- | ----------- | -| <a id="mutationmergerequestsetwipclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | -| <a id="mutationmergerequestsetwiperrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | -| <a id="mutationmergerequestsetwipmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. | - ### `Mutation.mergeRequestUpdate` Update attributes of a merge request. @@ -11583,7 +11558,6 @@ Maven metadata. | <a id="mergerequestusernotescount"></a>`userNotesCount` | [`Int`](#int) | User notes count of the merge request. | | <a id="mergerequestuserpermissions"></a>`userPermissions` | [`MergeRequestPermissions!`](#mergerequestpermissions) | Permissions for the current user on the resource. | | <a id="mergerequestweburl"></a>`webUrl` | [`String`](#string) | Web URL of the merge request. | -| <a id="mergerequestworkinprogress"></a>`workInProgress` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 13.12. Use `draft`. | #### Fields with arguments diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 9458fe650bc..2d0a24ea60b 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -365,8 +365,7 @@ include: '.gitlab-ci-production.yml' #### `include:file` -> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53903) in GitLab 11.7. -> - Including multiple files from the same project [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/26793) in GitLab 13.6. [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/271560) in GitLab 13.8. +> Including multiple files from the same project [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/26793) in GitLab 13.6. [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/271560) in GitLab 13.8. To include files from another private project on the same GitLab instance, use `include:file`. You can use `include:file` in combination with `include:project` only. @@ -451,8 +450,6 @@ include: #### `include:template` -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53445) in GitLab 11.7. - Use `include:template` to include [`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates). **Keyword type**: Global keyword. @@ -643,8 +640,7 @@ job2: **Additional details**: -You might need to use single quotes (`'`) or double quotes (`"`) when using -[special characters in `script`](script.md#use-special-characters-with-script). +- When you use [these special characters in `script`](script.md#use-special-characters-with-script), you must use single quotes (`'`) or double quotes (`"`) . **Related topics**: @@ -680,8 +676,8 @@ job: **Additional details**: -Scripts you specify in `before_script` are concatenated with any scripts you specify -in the main [`script`](#script). The combined scripts execute together in a single shell. +- Scripts you specify in `before_script` are concatenated with any scripts you specify + in the main [`script`](#script). The combined scripts execute together in a single shell. **Related topics**: @@ -799,7 +795,7 @@ job4: Use the `.pre` stage to make a job run at the start of a pipeline. `.pre` is always the first stage in a pipeline. User-defined stages execute after `.pre`. -You do not need to define `.pre` in [`stages`](#stages). +You do not have to define `.pre` in [`stages`](#stages). You must have a job in at least one stage other than `.pre` or `.post`. @@ -834,7 +830,7 @@ job2: Use the `.post` stage to make a job run at the end of a pipeline. `.post` is always the last stage in a pipeline. User-defined stages execute before `.post`. -You do not need to define `.post` in [`stages`](#stages). +You do not have to define `.post` in [`stages`](#stages). You must have a job in at least one stage other than `.pre` or `.post`. @@ -865,8 +861,6 @@ job2: ### `extends` -> Introduced in GitLab 11.3. - Use `extends` to reuse configuration sections. It's an alternative to [YAML anchors](yaml_specific_features.md#anchors) and is a little more flexible and readable. You can use `extends` to reuse configuration from [included configuration files](#use-extends-and-include-together). @@ -1356,7 +1350,7 @@ pipeline based on branch names or pipeline types. | `schedules` | For [scheduled pipelines](../pipelines/schedules.md). | | `tags` | When the Git reference for a pipeline is a tag. | | `triggers` | For pipelines created by using a [trigger token](../triggers/index.md#authentication-tokens). | - | `web` | For pipelines created by using **Run pipeline** button in the GitLab UI, from the project's **CI/CD > Pipelines** section. | + | `web` | For pipelines created by selecting **Run pipeline** in the GitLab UI, from the project's **CI/CD > Pipelines** section. | **Example of `only:refs` and `except:refs`**: @@ -1440,8 +1434,6 @@ deploy: #### `only:changes` / `except:changes` -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/19232) in GitLab 11.4. - Use the `changes` keyword with `only` to run a job, or with `except` to skip a job, when a Git push event modifies a file. @@ -2043,7 +2035,7 @@ Use `environment` to define the [environment](../environments/index.md) that a j **Possible inputs**: The name of the environment the job deploys to, in one of these formats: -- Plain text, including letters, digits, spaces and these characters: `-`, `_`, `/`, `$`, `{`, `}`. +- Plain text, including letters, digits, spaces, and these characters: `-`, `_`, `/`, `$`, `{`, `}`. - CI/CD variables, including predefined, secure, or variables defined in the `.gitlab-ci.yml` file. You can't use variables defined in a `script` section. @@ -2072,7 +2064,7 @@ Common environment names are `qa`, `staging`, and `production`, but you can use **Possible inputs**: The name of the environment the job deploys to, in one of these formats: -- Plain text, including letters, digits, spaces and these characters: `-`, `_`, `/`, `$`, `{`, `}`. +- Plain text, including letters, digits, spaces, and these characters: `-`, `_`, `/`, `$`, `{`, `}`. - CI/CD variables, including predefined, secure, or variables defined in the `.gitlab-ci.yml` file. You can't use variables defined in a `script` section. @@ -2124,7 +2116,7 @@ environment. **Additional details**: -See [`environment:action`](#environmentaction) for more details and an example. +- See [`environment:action`](#environmentaction) for more details and an example. #### `environment:action` @@ -2392,7 +2384,7 @@ cache-job: **Additional details**: -- If you use **Windows Batch** to run your shell scripts you need to replace +- If you use **Windows Batch** to run your shell scripts you must replace `$` with `%`. For example: `key: %CI_COMMIT_REF_SLUG%` - The `cache:key` value can't contain: @@ -2446,9 +2438,11 @@ these files changes, a new cache key is computed and a new cache is created. Any job runs that use the same `Gemfile.lock` and `package.json` with `cache:key:files` use the new cache, instead of rebuilding the dependencies. -**Additional details**: The cache `key` is a SHA computed from the most recent commits -that changed each listed file. If neither file is changed in any commits, the -fallback key is `default`. +**Additional details**: + +- The cache `key` is a SHA computed from the most recent commits +that changed each listed file. + If neither file is changed in any commits, the fallback key is `default`. ##### `cache:key:prefix` @@ -2485,8 +2479,9 @@ If a branch changes `Gemfile.lock`, that branch has a new SHA checksum for `cach A new cache key is generated, and a new cache is created for that key. If `Gemfile.lock` is not found, the prefix is added to `default`, so the key in the example would be `rspec-default`. -**Additional details**: If no file in `cache:key:files` is changed in any commits, -the prefix is added to the `default` key. +**Additional details**: + +- If no file in `cache:key:files` is changed in any commits, the prefix is added to the `default` key. #### `cache:untracked` @@ -2552,7 +2547,7 @@ This example stores the cache whether or not the job fails or succeeds. To change the upload and download behavior of a cache, use the `cache:policy` keyword. By default, the job downloads the cache when the job starts, and uploads changes -to the cache when the job ends. This is the `pull-push` policy (default). +to the cache when the job ends. This caching style is the `pull-push` policy (default). To set a job to only download the cache when the job starts, but never upload changes when the job finishes, use `cache:policy:pull`. @@ -2766,7 +2761,7 @@ time is not defined, it defaults to the To override the expiration date and protect artifacts from being automatically deleted: -- Use the **Keep** button on the job page. +- Select **Keep** on the job page. - [In GitLab 13.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/22761), set the value of `expire_in` to `never`. @@ -2871,7 +2866,7 @@ job: --- -If you use **Windows Batch** to run your shell scripts you need to replace +If you use **Windows Batch** to run your shell scripts you must replace `$` with `%`: ```yaml @@ -2882,7 +2877,7 @@ job: - binaries/ ``` -If you use **Windows PowerShell** to run your shell scripts you need to replace +If you use **Windows PowerShell** to run your shell scripts you must replace `$` with `$env:`: ```yaml @@ -2900,9 +2895,8 @@ link outside it. You can use Wildcards that use [glob](https://en.wikipedia.org/ patterns and: - In [GitLab Runner 13.0 and later](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2620), -[`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match). -- In GitLab Runner 12.10 and earlier, -[`filepath.Match`](https://pkg.go.dev/path/filepath#Match). + [`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match). +- In GitLab Runner 12.10 and earlier, [`filepath.Match`](https://pkg.go.dev/path/filepath#Match). To restrict which jobs a specific job fetches artifacts from, see [dependencies](#dependencies). @@ -2983,9 +2977,6 @@ artifacts: #### `artifacts:reports` -> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20390) in GitLab 11.2. -> - Requires GitLab Runner 11.2 and above. - Use [`artifacts:reports`](#artifactsreports) to: - Collect test reports, code quality reports, and security reports from jobs. @@ -3041,9 +3032,7 @@ requests and the pipeline view. It's also used to provide data for security dash ##### `artifacts:reports:browser_performance` **(PREMIUM)** -> - Introduced in GitLab 11.5. -> - Requires GitLab Runner 11.5 and above. -> - [Name changed](https://gitlab.com/gitlab-org/gitlab/-/issues/225914) from `artifacts:reports:performance` in GitLab 14.0. +> [Name changed](https://gitlab.com/gitlab-org/gitlab/-/issues/225914) from `artifacts:reports:performance` in GitLab 14.0. The `browser_performance` report collects [Browser Performance Testing metrics](../../user/project/merge_requests/browser_performance_testing.md) as artifacts. @@ -3064,8 +3053,7 @@ dashboards. ##### `artifacts:reports:cobertura` -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3708) in GitLab 12.9. -> - Requires [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 and above. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3708) in GitLab 12.9. The `cobertura` report collects [Cobertura coverage XML files](../../user/project/merge_requests/test_coverage_visualization.md). The collected Cobertura coverage reports upload to GitLab as an artifact @@ -3076,9 +3064,7 @@ third party ports for other languages like JavaScript, Python, Ruby, and so on. ##### `artifacts:reports:codequality` -> - Introduced in GitLab 11.5. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) to GitLab Free in 13.2. -> - Requires GitLab Runner 11.5 and above. +> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) to GitLab Free in 13.2. The `codequality` report collects [Code Quality issues](../../user/project/merge_requests/code_quality.md) as artifacts. @@ -3087,9 +3073,6 @@ The collected Code Quality report uploads to GitLab as an artifact and is summar ##### `artifacts:reports:container_scanning` **(ULTIMATE)** -> - Introduced in GitLab 11.5. -> - Requires GitLab Runner 11.5 and above. - The `container_scanning` report collects [Container Scanning vulnerabilities](../../user/application_security/container_scanning/index.md) as artifacts. @@ -3110,9 +3093,6 @@ requests and the pipeline view. It's also used to provide data for security dash ##### `artifacts:reports:dast` **(ULTIMATE)** -> - Introduced in GitLab 11.5. -> - Requires GitLab Runner 11.5 and above. - The `dast` report collects [DAST vulnerabilities](../../user/application_security/dast/index.md) as artifacts. @@ -3121,9 +3101,6 @@ dashboards. ##### `artifacts:reports:dependency_scanning` **(ULTIMATE)** -> - Introduced in GitLab 11.5. -> - Requires GitLab Runner 11.5 and above. - The `dependency_scanning` report collects [Dependency Scanning vulnerabilities](../../user/application_security/dependency_scanning/index.md) as artifacts. @@ -3132,15 +3109,14 @@ dashboards. ##### `artifacts:reports:dotenv` -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17066) in GitLab 12.9. -> - Requires GitLab Runner 11.5 and later. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17066) in GitLab 12.9. The `dotenv` report collects a set of environment variables as artifacts. The collected variables are registered as runtime-created variables of the job, which is useful to [set dynamic environment URLs after a job finishes](../environments/index.md#set-dynamic-environment-urls-after-a-job-finishes). -There are a couple of exceptions to the [original dotenv rules](https://github.com/motdotla/dotenv#rules): +The exceptions to the [original dotenv rules](https://github.com/motdotla/dotenv#rules) are: - The variable key can contain only letters, digits, and underscores (`_`). - The maximum size of the `.env` file is 5 KB. @@ -3154,9 +3130,6 @@ There are a couple of exceptions to the [original dotenv rules](https://github.c ##### `artifacts:reports:junit` -> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20390) in GitLab 11.2. -> - Requires GitLab Runner 11.2 and above. - The `junit` report collects [JUnit report format XML files](https://www.ibm.com/docs/en/adfz/developer-for-zos/14.1.0?topic=formats-junit-xml-format) as artifacts. Although JUnit was originally developed in Java, there are many third party ports for other @@ -3179,21 +3152,20 @@ rspec: The collected Unit test reports upload to GitLab as an artifact and display in merge requests. If the JUnit tool you use exports to multiple XML files, specify -multiple test report paths within a single job to +multiple test report paths in a single job to concatenate them into a single file. Use a filename pattern (`junit: rspec-*.xml`), an array of filenames (`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`), or a combination thereof (`junit: [rspec.xml, test-results/TEST-*.xml]`). ##### `artifacts:reports:license_scanning` **(ULTIMATE)** -> - Introduced in GitLab 12.8. -> - Requires GitLab Runner 11.5 and above. +> Introduced in GitLab 12.8. The `license_scanning` report collects [Licenses](../../user/compliance/license_compliance/index.md) as artifacts. -The License Compliance report uploads to GitLab as an artifact and displays automatically in merge requests and the pipeline view, and provide data for security -dashboards. +The License Compliance report uploads to GitLab as an artifact and displays automatically +in merge requests and the pipeline view. The report provides data for security dashboards. ##### `artifacts:reports:load_performance` **(PREMIUM)** @@ -3208,8 +3180,6 @@ shown in merge requests automatically. ##### `artifacts:reports:metrics` **(PREMIUM)** -> Introduced in GitLab 11.10. - The `metrics` report collects [Metrics](../metrics_reports.md) as artifacts. @@ -3218,7 +3188,6 @@ The collected Metrics report uploads to GitLab as an artifact and displays in me ##### `artifacts:reports:requirements` **(ULTIMATE)** > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2859) in GitLab 13.1. -> - Requires GitLab Runner 11.5 and above. The `requirements` report collects `requirements.json` files as artifacts. @@ -3228,9 +3197,7 @@ marked as Satisfied. ##### `artifacts:reports:sast` -> - Introduced in GitLab 11.5. > - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/2098) from GitLab Ultimate to GitLab Free in 13.3. -> - Requires GitLab Runner 11.5 and above. The `sast` report collects [SAST vulnerabilities](../../user/application_security/sast/index.md) as artifacts. @@ -3302,7 +3269,7 @@ failure. 1. `on_success` (default): Upload artifacts only when the job succeeds. 1. `on_failure`: Upload artifacts only when the job fails. -1. `always`: Always upload artifacts. Useful, for example, when +1. `always`: Always upload artifacts. For example, when [uploading artifacts](../unit_test_reports.md#viewing-junit-screenshots-on-gitlab) required to troubleshoot failing tests. @@ -3394,8 +3361,6 @@ to select a specific site profile and scanner profile. ### `retry` -> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3515) in GitLab 11.5, you can control which failures to retry on. - Use `retry` to configure how many times a job is retried if it fails. If not defined, defaults to `0` and jobs do not retry. @@ -3513,8 +3478,6 @@ test: ### `parallel` -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/21480) in GitLab 11.5. - Use `parallel` to run a job multiple times in parallel in a single pipeline. Multiple runners must exist, or a single runner must be configured to run multiple jobs concurrently. @@ -3987,7 +3950,7 @@ This keyword must be used with `secrets:vault`. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/28321) in GitLab 13.4 and GitLab Runner 13.4. -Use `secrets:vault` to specify secrets provided by a [Hashicorp Vault](https://www.vaultproject.io/). +Use `secrets:vault` to specify secrets provided by a [HashiCorp Vault](https://www.vaultproject.io/). **Keyword type**: Job keyword. You can use it only as part of a job. diff --git a/doc/development/experiment_guide/gitlab_experiment.md b/doc/development/experiment_guide/gitlab_experiment.md index 1cd3fefb7cf..af4512dcde0 100644 --- a/doc/development/experiment_guide/gitlab_experiment.md +++ b/doc/development/experiment_guide/gitlab_experiment.md @@ -92,7 +92,7 @@ end ``` When this code executes, the experiment is run, a variant is assigned, and (if within a -controller or view) a `window.gon.experiment.pill_color` object will be available in the +controller or view) a `window.gl.experiments.pill_color` object will be available in the client layer, with details like: - The assigned variant. @@ -522,14 +522,14 @@ shared example: [tracks assignment and records the subject](https://gitlab.com/g This is in flux as of GitLab 13.10, and can't be documented just yet. -Any experiment that's been run in the request lifecycle surfaces in `window.gon.experiment`, +Any experiment that's been run in the request lifecycle surfaces in and `window.gl.experiments`, and matches [this schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-0) so you can use it when resolving some concepts around experimentation in the client layer. ### Use experiments in Vue With the `gitlab-experiment` component, you can define slots that match the name of the -variants pushed to `window.gon.experiment`. For example, if we alter the `pill_color` +variants pushed to `window.gl.experiments`. For example, if we alter the `pill_color` experiment to just use the default variants of `control` and `candidate` like so: ```ruby @@ -587,7 +587,51 @@ For example, the Vue component for the previously-defined `pill_color` experimen ``` NOTE: -When there is no experiment data in the `window.gon.experiment` object for the given experiment name, the `control` slot will be used, if it exists. +When there is no experiment data in the `window.gl.experiments` object for the given experiment name, the `control` slot will be used, if it exists. + +## Test with Jest + +### Stub Helpers + +You can stub experiments using the `stubExperiments` helper defined in `spec/frontend/__helpers__/experimentation_helper.js`. + +```javascript +import { stubExperiments } from 'helpers/experimentation_helper'; +import { getExperimentData } from '~/experimentation/utils'; + +describe('when my_experiment is enabled', () => { + beforeEach(() => { + stubExperiments({ my_experiment: 'candidate' }); + }); + + it('sets the correct data', () => { + expect(getExperimentData('my_experiment')).toEqual({ experiment: 'my_experiment', variant: 'candidate' }); + }); +}); +``` + +NOTE: +This method of stubbing in Jest specs will not automatically un-stub itself at the end of the test. We merge our stubbed experiment in with all the other global data in `window.gl`. If you need to remove the stubbed experiment(s) after your test or ensure a clean global object before your test, you'll need to manage the global object directly yourself: + +```javascript +desribe('tests that care about global state', () => { + const originalObjects = []; + + beforeEach(() => { + // For backwards compatibility for now, we're using both window.gon & window.gl + originalObjects.push(window.gon, window.gl); + }); + + afterEach(() => { + [window.gon, window.gl] = originalObjects; + }); + + it('stubs experiment in fresh global state', () => { + stubExperiment({ my_experiment: 'candidate' }); + // ... + }); +}) +``` ## Notes on feature flags diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index d249d36f159..dd2d3d8bf32 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -588,8 +588,6 @@ class like so: ```ruby class MyMigration < Gitlab::Database::Migration[1.0] - include Gitlab::Database::MigrationHelpers - disable_ddl_transaction! INDEX_NAME = 'index_name' @@ -633,8 +631,6 @@ be used with a name option. For example: ```ruby class MyMigration < Gitlab::Database::Migration[1.0] - include Gitlab::Database::MigrationHelpers - INDEX_NAME = 'index_name' def up diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md index 433c00571be..454be0e52f2 100644 --- a/doc/user/packages/npm_registry/index.md +++ b/doc/user/packages/npm_registry/index.md @@ -208,6 +208,15 @@ Then, you can run `npm publish` either locally or by using GitLab CI/CD. - **GitLab CI/CD:** Set an `NPM_TOKEN` [CI/CD variable](../../../ci/variables/index.md) under your project's **Settings > CI/CD > Variables**. +## Working with private registries + +When working with private repositories, you may want to configure additional settings to ensure a secure communication channel: + +```shell +# Force npm to always require authentication when accessing the registry, even for GET requests. +npm config set always-auth true +``` + ## Package naming convention When you use the [instance-level endpoint](#use-the-gitlab-endpoint-for-npm-packages), only the packages with names in the format of `@scope/package-name` are available. @@ -363,6 +372,10 @@ This rule has a different impact depending on the package name: This aligns with npmjs.org's behavior. However, npmjs.org does not ever let you publish the same version more than once, even if it has been deleted. +## `package.json` limitations + +You can't publish a package if its `package.json` file exceeds 20,000 characters. + ## Install a package npm packages are commonly-installed by using the `npm` or `yarn` commands @@ -427,22 +440,29 @@ and use your organization's URL. The name is case-sensitive and must match the n //gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken= "<your_token>" ``` -### npm dependencies metadata +### npm metadata > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11867) in GitLab Premium 12.6. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Free in 13.3. - -In GitLab 12.6 and later, packages published to the Package Registry expose the following attributes to the npm client: - -- name -- version -- dist-tags -- dependencies - - dependencies - - devDependencies - - bundleDependencies - - peerDependencies - - deprecated +> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/330929) in GitLab 14.5. + +The GitLab Package Registry exposes the following attributes to the npm client. +These are similar to the [abbreviated metadata format](https://github.com/npm/registry/blob/9e368cf6aaca608da5b2c378c0d53f475298b916/docs/responses/package-metadata.md#abbreviated-metadata-format): + +- `name` +- `versions` + - `name` + - `version` + - `deprecated` + - `dependencies` + - `devDependencies` + - `bundleDependencies` + - `peerDependencies` + - `bin` + - `directories` + - `dist` + - `engines` + - `_hasShrinkwrap` ## Add npm distribution tags @@ -579,6 +599,10 @@ root namespace and therefore cannot be published again using the same name. This is also true even if the prior published package shares the same name, but not the version. +#### Package JSON file is too large + +Make sure that your `package.json` file does not [exceed `20,000` characters](#packagejson-limitations). + ### `npm publish` returns `npm ERR! 500 Internal Server Error - PUT` This is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/238950) in GitLab diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md index eaab1933b79..d155f91e40b 100644 --- a/doc/user/project/integrations/custom_issue_tracker.md +++ b/doc/user/project/integrations/custom_issue_tracker.md @@ -4,31 +4,40 @@ group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Custom issue tracker service **(FREE)** +# Custom issue tracker **(FREE)** -Use a custom issue tracker that is not in the integration list. +You can integrate an [external issue tracker](../../../integration/external-issue-tracker.md) +with GitLab. If your preferred issue tracker is not listed in the +[integrations list](../../../integration/external-issue-tracker.md#integration), +you can enable a custom issue tracker. + +After you enable the custom issue tracker, a link to the issue tracker displays +on the left sidebar in your project. + +![Custom issue tracker link](img/custom_issue_tracker_v14_5.png) + +## Enable a custom issue tracker To enable a custom issue tracker in a project: -1. Go to the [Integrations page](overview.md#accessing-integrations). +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. 1. Select **Custom issue tracker**. 1. Select the checkbox under **Enable integration**. 1. Fill in the required fields: - **Project URL**: The URL to view all the issues in the custom issue tracker. - **Issue URL**: The URL to view an issue in the custom issue tracker. The URL must contain `:id`. - GitLab replaces `:id` with the issue number (for example, - `https://customissuetracker.com/project-name/:id`, which becomes `https://customissuetracker.com/project-name/123`). + GitLab replaces `:id` with the issue number (for example, + `https://customissuetracker.com/project-name/:id`, which becomes + `https://customissuetracker.com/project-name/123`). - **New issue URL**: <!-- The line below was originally added in January 2018: https://gitlab.com/gitlab-org/gitlab/-/commit/778b231f3a5dd42ebe195d4719a26bf675093350 --> - **This URL is not used and removal is planned in a future release.** - Enter any URL here. - For more information, see [issue 327503](https://gitlab.com/gitlab-org/gitlab/-/issues/327503). - -1. Select **Save changes** or optionally select **Test settings**. + **This URL is not used and an [issue exists](https://gitlab.com/gitlab-org/gitlab/-/issues/327503) to remove it.** + Enter any URL. -After you configure and enable the custom issue tracker service, a link appears on the GitLab -project pages. This link takes you to the custom issue tracker. +1. Optional. Select **Test settings**. +1. Select **Save changes**. ## Reference issues in a custom issue tracker diff --git a/doc/user/project/integrations/img/custom_issue_tracker_v14_5.png b/doc/user/project/integrations/img/custom_issue_tracker_v14_5.png Binary files differnew file mode 100644 index 00000000000..e316a2acc39 --- /dev/null +++ b/doc/user/project/integrations/img/custom_issue_tracker_v14_5.png diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md index 5fe3e5be4c0..510fcd61cb6 100644 --- a/doc/user/project/service_desk.md +++ b/doc/user/project/service_desk.md @@ -207,7 +207,7 @@ To configure a custom mailbox for Service Desk with IMAP, add the following snip service_desk_email: enabled: true address: "project_contact+%{key}@example.com" - user: "project_support@example.com" + user: "project_contact@example.com" password: "[REDACTED]" host: "imap.gmail.com" port: 993 @@ -224,7 +224,7 @@ To configure a custom mailbox for Service Desk with IMAP, add the following snip ```ruby gitlab_rails['service_desk_email_enabled'] = true gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@gmail.com" - gitlab_rails['service_desk_email_email'] = "project_support@gmail.com" + gitlab_rails['service_desk_email_email'] = "project_contact@gmail.com" gitlab_rails['service_desk_email_password'] = "[REDACTED]" gitlab_rails['service_desk_email_mailbox_name'] = "inbox" gitlab_rails['service_desk_email_idle_timeout'] = 60 diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb index d6e006df976..c62947722a9 100644 --- a/lib/api/concerns/packages/npm_endpoints.rb +++ b/lib/api/concerns/packages/npm_endpoints.rb @@ -121,7 +121,9 @@ module API not_found!('Packages') if packages.empty? - present ::Packages::Npm::PackagePresenter.new(package_name, packages), + include_metadata = Feature.enabled?(:packages_npm_abbreviated_metadata, project) + + present ::Packages::Npm::PackagePresenter.new(package_name, packages, include_metadata: include_metadata), with: ::API::Entities::NpmPackage end end diff --git a/lib/bulk_imports/ndjson_pipeline.rb b/lib/bulk_imports/ndjson_pipeline.rb index f01ce22a46d..6cc29d63919 100644 --- a/lib/bulk_imports/ndjson_pipeline.rb +++ b/lib/bulk_imports/ndjson_pipeline.rb @@ -13,7 +13,7 @@ module BulkImports relation_hash, relation_index = data relation_definition = import_export_config.top_relation_tree(relation) - deep_transform_relation!(relation_hash, relation, relation_definition) do |key, hash| + relation_object = deep_transform_relation!(relation_hash, relation, relation_definition) do |key, hash| relation_factory.create( relation_index: relation_index, relation_sym: key.to_sym, @@ -25,6 +25,9 @@ module BulkImports excluded_keys: import_export_config.relation_excluded_keys(key) ) end + + relation_object.assign_attributes(portable_class_sym => portable) + relation_object end def load(_, object) @@ -94,6 +97,10 @@ module BulkImports def members_mapper @members_mapper ||= BulkImports::UsersMapper.new(context: context) end + + def portable_class_sym + portable.class.to_s.downcase.to_sym + end end end end diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb index ef8fd9504f7..3db2f1295f9 100644 --- a/lib/gitlab/application_rate_limiter.rb +++ b/lib/gitlab/application_rate_limiter.rb @@ -79,28 +79,6 @@ module Gitlab increment(key, options[:scope]) > threshold_value end - # Increments the given cache key and increments the value by 1 with the - # expiration interval defined in `.rate_limits`. - # - # @param key [Symbol] Key attribute registered in `.rate_limits` - # @option scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project) - # - # @return [Integer] incremented value - def increment(key, scope) - return safe_increment(key, scope) if Feature.enabled?(:rate_limiter_safe_increment, default_enabled: :yaml) - - value = 0 - interval_value = interval(key) - - ::Gitlab::Redis::RateLimiting.with do |redis| - cache_key = action_key(key, scope) - value = redis.incr(cache_key) - redis.expire(cache_key, interval_value) if value == 1 - end - - value - end - # Increments a cache key that is based on the current time and interval. # So that when time passes to the next interval, the key changes and the count starts again from 0. # @@ -110,7 +88,7 @@ module Gitlab # @option scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project) # # @return [Integer] incremented value - def safe_increment(key, scope) + def increment(key, scope) interval_value = interval(key) period_key, time_elapsed_in_period = Time.now.to_i.divmod(interval_value) diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml index dca5326f270..c0ba2c58a41 100644 --- a/lib/gitlab/database/gitlab_schemas.yml +++ b/lib/gitlab/database/gitlab_schemas.yml @@ -355,6 +355,7 @@ packages_dependency_links: :gitlab_main packages_events: :gitlab_main packages_helm_file_metadata: :gitlab_main packages_maven_metadata: :gitlab_main +packages_npm_metadata: :gitlab_main packages_nuget_dependency_link_metadata: :gitlab_main packages_nuget_metadata: :gitlab_main packages_package_file_build_infos: :gitlab_main diff --git a/lib/gitlab/database/partitioning.rb b/lib/gitlab/database/partitioning.rb index 866477c46da..1343354715a 100644 --- a/lib/gitlab/database/partitioning.rb +++ b/lib/gitlab/database/partitioning.rb @@ -31,6 +31,12 @@ module Gitlab registered_tables.merge(tables) end + def sync_partitions_ignore_db_error + sync_partitions unless ENV['DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP'] + rescue ActiveRecord::ActiveRecordError, PG::Error + # ignore - happens when Rake tasks yet have to create a database, e.g. for testing + end + def sync_partitions(models_to_sync = registered_for_sync) Gitlab::AppLogger.info(message: 'Syncing dynamic postgres partitions') diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index da1a3efd562..5afdcc0bd4c 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -20,6 +20,7 @@ module Gitlab EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000' NoRepository = Class.new(::Gitlab::Git::BaseError) + RepositoryExists = Class.new(::Gitlab::Git::BaseError) InvalidRepository = Class.new(::Gitlab::Git::BaseError) InvalidBlobName = Class.new(::Gitlab::Git::BaseError) InvalidRef = Class.new(::Gitlab::Git::BaseError) @@ -101,6 +102,8 @@ module Gitlab def create_repository wrapped_gitaly_errors do gitaly_repository_client.create_repository + rescue GRPC::AlreadyExists => e + raise RepositoryExists, e.message end end diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 1abbd6dc45b..9a6317e2b76 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -48,7 +48,7 @@ module Gitlab if gpg_key Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key) - clear_memoization(:verified_signature) + clear_memoization(:gpg_signatures) end yield gpg_key @@ -56,16 +56,7 @@ module Gitlab end def verified_signature - strong_memoize(:verified_signature) { gpgme_signature } - end - - def gpgme_signature - GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature| - # Return the first signature for now: https://gitlab.com/gitlab-org/gitlab-foss/issues/54932 - break verified_signature - end - rescue GPGME::Error - nil + gpg_signatures.first end def create_cached_signature! @@ -77,6 +68,24 @@ module Gitlab end end + def gpg_signatures + strong_memoize(:gpg_signatures) do + signatures = [] + + GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature| + signatures << verified_signature + end + + signatures + rescue GPGME::Error + [] + end + end + + def multiple_signatures? + gpg_signatures.size > 1 + end + def attributes(gpg_key) user_infos = user_infos(gpg_key) verification_status = verification_status(gpg_key) @@ -93,6 +102,7 @@ module Gitlab end def verification_status(gpg_key) + return :multiple_signatures if multiple_signatures? && Feature.enabled?(:multiple_gpg_signatures, @commit.project, default_enabled: :yaml) return :unknown_key unless gpg_key return :unverified_key unless gpg_key.verified? return :unverified unless verified_signature&.valid? diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bb95f06cec1..f24c6060c8f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -23326,6 +23326,9 @@ msgstr "" msgid "No forks are available to you." msgstr "" +msgid "No group provided" +msgstr "" + msgid "No grouping" msgstr "" @@ -23377,6 +23380,9 @@ msgstr "" msgid "No members found" msgstr "" +msgid "No memberships found" +msgstr "" + msgid "No merge requests found" msgstr "" @@ -24442,6 +24448,9 @@ msgstr "" msgid "Package type must be Maven" msgstr "" +msgid "Package type must be NPM" +msgstr "" + msgid "Package type must be NuGet" msgstr "" @@ -35112,6 +35121,9 @@ msgstr "" msgid "This commit is part of merge request %{link_to_merge_request}. Comments created here will be created in the context of that merge request." msgstr "" +msgid "This commit was signed with %{strong_open}multiple%{strong_close} signatures." +msgstr "" + msgid "This commit was signed with a %{strong_open}verified%{strong_close} signature and the committer email is verified to belong to the same user." msgstr "" @@ -39464,6 +39476,9 @@ msgstr "" msgid "You do not have permission to access dora metrics." msgstr "" +msgid "You do not have permission to approve a member" +msgstr "" + msgid "You do not have permission to leave this %{namespaceType}." msgstr "" @@ -41767,6 +41782,9 @@ msgstr "" msgid "starts on %{timebox_start_date}" msgstr "" +msgid "structure is too large" +msgstr "" + msgid "stuck" msgstr "" diff --git a/qa/chemlab-library-gitlab.gemspec b/qa/chemlab-library-gitlab.gemspec index 34a55ba8927..9af4a650d98 100644 --- a/qa/chemlab-library-gitlab.gemspec +++ b/qa/chemlab-library-gitlab.gemspec @@ -19,4 +19,5 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.add_runtime_dependency 'chemlab', '~> 0.9' + spec.add_runtime_dependency 'zeitwerk', '~> 2.4' end diff --git a/qa/lib/gitlab.rb b/qa/lib/gitlab.rb index 4418e51facb..8c33071633d 100644 --- a/qa/lib/gitlab.rb +++ b/qa/lib/gitlab.rb @@ -1,31 +1,14 @@ # frozen_string_literal: true require 'chemlab/library' +require 'zeitwerk' + +loader = Zeitwerk::Loader.new +loader.push_dir(__dir__) +loader.ignore("#{__dir__}/gitlab/**/*.stub.rb") # ignore page stubs +loader.setup # Chemlab Page Libraries for GitLab module Gitlab include Chemlab::Library - - module Page - module Main - autoload :Login, 'gitlab/page/main/login' - autoload :SignUp, 'gitlab/page/main/sign_up' - end - - module Subscriptions - autoload :New, 'gitlab/page/subscriptions/new' - end - - module Admin - autoload :Dashboard, 'gitlab/page/admin/dashboard' - autoload :Subscription, 'gitlab/page/admin/subscription' - end - - module Group - module Settings - autoload :Billing, 'gitlab/page/group/settings/billing' - autoload :UsageQuotas, 'gitlab/page/group/settings/usage_quotas' - end - end - end end diff --git a/qa/lib/gitlab/page/main/welcome.rb b/qa/lib/gitlab/page/main/welcome.rb new file mode 100644 index 00000000000..a2df1da61c9 --- /dev/null +++ b/qa/lib/gitlab/page/main/welcome.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Main + class Welcome < Chemlab::Page + path '/users/sign_up/welcome' + + button :get_started_button + end + end + end +end diff --git a/qa/lib/gitlab/page/main/welcome.stub.rb b/qa/lib/gitlab/page/main/welcome.stub.rb new file mode 100644 index 00000000000..a10e697bcbf --- /dev/null +++ b/qa/lib/gitlab/page/main/welcome.stub.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Main + module Welcome + # @note Defined as +button :get_started_button+ + # Clicks +get_started_button+ + def get_started_button + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::Welcome.perform do |welcome| + # expect(welcome.get_started_button_element).to exist + # end + # @return [Watir::Button] The raw +Button+ element + def get_started_button_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::Welcome.perform do |welcome| + # expect(welcome).to be_get_started_button + # end + # @return [Boolean] true if the +get_started_button+ element is present on the page + def get_started_button? + # This is a stub, used for indexing. The method is dynamically generated. + end + end + end + end +end diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index f0aa351bee0..cf528b414c0 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -62,7 +62,7 @@ RSpec.describe Dashboard::TodosController do create(:issue, project: project, assignees: [user]) group_2 = create(:group) group_2.add_owner(user) - project_2 = create(:project) + project_2 = create(:project, namespace: user.namespace) project_2.add_developer(user) merge_request_2 = create(:merge_request, source_project: project_2) create(:todo, project: project, author: author, user: user, target: merge_request_2) diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index 3d7636b1f30..5b1c6777523 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -86,10 +86,11 @@ RSpec.describe Projects::MergeRequests::DiffsController do let(:project) { create(:project, :repository) } let(:user) { create(:user) } + let(:maintainer) { true } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } before do - project.add_maintainer(user) + project.add_maintainer(user) if maintainer sign_in(user) end @@ -383,8 +384,9 @@ RSpec.describe Projects::MergeRequests::DiffsController do end context 'when the user cannot view the merge request' do + let(:maintainer) { false } + before do - project.team.truncate diff_for_path(old_path: existing_path, new_path: existing_path) end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 438fc2f2106..e2fc867db44 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -10,7 +10,8 @@ RSpec.describe Projects::MergeRequestsController do let_it_be_with_reload(:project_public_with_private_builds) { create(:project, :repository, :public, :builds_private) } let(:user) { project.owner } - let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } + let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: merge_request_source_project, allow_maintainer_to_push: false) } + let(:merge_request_source_project) { project } before do sign_in(user) @@ -2073,8 +2074,6 @@ RSpec.describe Projects::MergeRequestsController do end describe 'POST #rebase' do - let(:viewer) { user } - def post_rebase post :rebase, params: { namespace_id: project.namespace, project_id: project, id: merge_request } end @@ -2085,7 +2084,7 @@ RSpec.describe Projects::MergeRequestsController do context 'successfully' do it 'enqeues a RebaseWorker' do - expect_rebase_worker_for(viewer) + expect_rebase_worker_for(user) post_rebase @@ -2108,17 +2107,17 @@ RSpec.describe Projects::MergeRequestsController do context 'with a forked project' do let(:forked_project) { fork_project(project, fork_owner, repository: true) } let(:fork_owner) { create(:user) } + let(:merge_request_source_project) { forked_project } - before do - project.add_developer(fork_owner) + context 'user cannot push to source branch' do + before do + project.add_developer(fork_owner) - merge_request.update!(source_project: forked_project) - forked_project.add_reporter(user) - end + forked_project.add_reporter(user) + end - context 'user cannot push to source branch' do it 'returns 404' do - expect_rebase_worker_for(viewer).never + expect_rebase_worker_for(user).never post_rebase diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index a25c597edb2..baf500c2b57 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -499,13 +499,12 @@ RSpec.describe RegistrationsController do expect(User.last.name).to eq("#{base_user_params[:first_name]} #{base_user_params[:last_name]}") end - it 'sets the username and caller_id in the context' do + it 'sets the caller_id in the context' do expect(controller).to receive(:create).and_wrap_original do |m, *args| m.call(*args) expect(Gitlab::ApplicationContext.current) - .to include('meta.user' => base_user_params[:username], - 'meta.caller_id' => 'RegistrationsController#create') + .to include('meta.caller_id' => 'RegistrationsController#create') end subject diff --git a/spec/factories/namespaces/project_namespaces.rb b/spec/factories/namespaces/project_namespaces.rb index ca9fc5f8768..6bf17088741 100644 --- a/spec/factories/namespaces/project_namespaces.rb +++ b/spec/factories/namespaces/project_namespaces.rb @@ -2,7 +2,7 @@ FactoryBot.define do factory :project_namespace, class: 'Namespaces::ProjectNamespace' do - project + association :project, factory: :project, strategy: :build parent { project.namespace } visibility_level { project.visibility_level } name { project.name } diff --git a/spec/factories/packages/npm/metadata.rb b/spec/factories/packages/npm/metadata.rb new file mode 100644 index 00000000000..c8acaa10199 --- /dev/null +++ b/spec/factories/packages/npm/metadata.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :npm_metadatum, class: 'Packages::Npm::Metadatum' do + package { association(:npm_package) } + + package_json do + { + 'name': package.name, + 'version': package.version, + 'dist': { + 'tarball': 'http://localhost/tarball.tgz', + 'shasum': '1234567890' + } + } + end + end +end diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb index 70b53f23508..610a80eb12c 100644 --- a/spec/features/signed_commits_spec.rb +++ b/spec/features/signed_commits_spec.rb @@ -115,6 +115,19 @@ RSpec.describe 'GPG signed commits' do end end + it 'unverified signature: commit contains multiple GPG signatures' do + user_1_key + + visit project_commit_path(project, GpgHelpers::MULTIPLE_SIGNATURES_SHA) + wait_for_all_requests + + page.find('.gpg-status-box', text: 'Unverified').click + + within '.popover' do + expect(page).to have_content "This commit was signed with multiple signatures." + end + end + it 'verified and the gpg user has a gitlab profile' do user_1_key @@ -169,7 +182,7 @@ RSpec.describe 'GPG signed commits' do page.find('.gpg-status-box', text: 'Unverified').click within '.popover' do - expect(page).to have_content 'This commit was signed with an unverified signature' + expect(page).to have_content 'This commit was signed with multiple signatures.' end end end diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json b/spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json index 3e74dc0a1c2..64969d71250 100644 --- a/spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json +++ b/spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json @@ -36,11 +36,11 @@ ".{1,}": { "type": "string" } } }, - "deprecated": { - "type": "object", - "patternProperties": { - ".{1,}": { "type": "string" } - } - } + "deprecated": { "type": "string"}, + "bin": { "type": "string" }, + "directories": { "type": "array" }, + "engines": { "type": "object" }, + "_hasShrinkwrap": { "type": "boolean" }, + "additionalProperties": true } } diff --git a/spec/fixtures/packages/npm/payload.json b/spec/fixtures/packages/npm/payload.json index 664aa636001..5ecb013b9bf 100644 --- a/spec/fixtures/packages/npm/payload.json +++ b/spec/fixtures/packages/npm/payload.json @@ -14,7 +14,8 @@ "express":"^4.16.4" }, "dist":{ - "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f" + "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f", + "tarball":"http://localhost/npm/package.tgz" } } }, diff --git a/spec/fixtures/packages/npm/payload_with_duplicated_packages.json b/spec/fixtures/packages/npm/payload_with_duplicated_packages.json index a6ea8760bd5..bc4a7b3f55a 100644 --- a/spec/fixtures/packages/npm/payload_with_duplicated_packages.json +++ b/spec/fixtures/packages/npm/payload_with_duplicated_packages.json @@ -28,7 +28,8 @@ "express":"^4.16.4" }, "dist":{ - "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f" + "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f", + "tarball":"http://localhost/npm/package.tgz" } } }, diff --git a/spec/frontend/__helpers__/experimentation_helper.js b/spec/frontend/__helpers__/experimentation_helper.js index 7a2ef61216a..e0156226acc 100644 --- a/spec/frontend/__helpers__/experimentation_helper.js +++ b/spec/frontend/__helpers__/experimentation_helper.js @@ -1,5 +1,6 @@ import { merge } from 'lodash'; +// This helper is for specs that use `gitlab/experimentation` module export function withGonExperiment(experimentKey, value = true) { let origGon; @@ -12,16 +13,26 @@ export function withGonExperiment(experimentKey, value = true) { window.gon = origGon; }); } -// This helper is for specs that use `gitlab-experiment` utilities, which have a different schema that gets pushed via Gon compared to `Experimentation Module` -export function assignGitlabExperiment(experimentKey, variant) { - let origGon; - beforeEach(() => { - origGon = window.gon; - window.gon = { experiment: { [experimentKey]: { variant } } }; - }); +// The following helper is for specs that use `gitlab-experiment` utilities, +// which have a different schema that gets pushed to the frontend compared to +// the `Experimentation` Module. +// +// Usage: stubExperiments({ experiment_feature_flag_name: 'variant_name', ... }) +export function stubExperiments(experiments = {}) { + // Deprecated + window.gon = window.gon || {}; + window.gon.experiment = window.gon.experiment || {}; + // Preferred + window.gl = window.gl || {}; + window.gl.experiments = window.gl.experiemnts || {}; - afterEach(() => { - window.gon = origGon; + Object.entries(experiments).forEach(([name, variant]) => { + const experimentData = { experiment: name, variant }; + + // Deprecated + window.gon.experiment[name] = experimentData; + // Preferred + window.gl.experiments[name] = experimentData; }); } diff --git a/spec/frontend/boards/components/new_board_button_spec.js b/spec/frontend/boards/components/new_board_button_spec.js index e66c31953f6..075fe225ec2 100644 --- a/spec/frontend/boards/components/new_board_button_spec.js +++ b/spec/frontend/boards/components/new_board_button_spec.js @@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'; import { GlButton } from '@gitlab/ui'; import NewBoardButton from '~/boards/components/new_board_button.vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import { assignGitlabExperiment } from 'helpers/experimentation_helper'; +import { stubExperiments } from 'helpers/experimentation_helper'; import eventHub from '~/boards/eventhub'; const FEATURE = 'prominent_create_board_btn'; @@ -28,7 +28,9 @@ describe('NewBoardButton', () => { }); describe('control variant', () => { - assignGitlabExperiment(FEATURE, 'control'); + beforeAll(() => { + stubExperiments({ [FEATURE]: 'control' }); + }); it('renders nothing', () => { wrapper = createComponent(); @@ -38,7 +40,9 @@ describe('NewBoardButton', () => { }); describe('candidate variant', () => { - assignGitlabExperiment(FEATURE, 'candidate'); + beforeAll(() => { + stubExperiments({ [FEATURE]: 'candidate' }); + }); it('renders New board button when `candidate` variant', () => { wrapper = createComponent(); diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js index e39b761b54d..342b4bfcc50 100644 --- a/spec/frontend/diffs/components/diff_file_header_spec.js +++ b/spec/frontend/diffs/components/diff_file_header_spec.js @@ -241,18 +241,19 @@ describe('DiffFileHeader component', () => { }); describe('for any file', () => { - const otherModes = Object.keys(diffViewerModes).filter((m) => m !== 'mode_changed'); + const allModes = Object.keys(diffViewerModes).map((m) => [m]); - it('for mode_changed file mode displays mode changes', () => { + it.each(allModes)('for %s file mode displays mode changes', (mode) => { createComponent({ props: { diffFile: { ...diffFile, + mode_changed: true, a_mode: 'old-mode', b_mode: 'new-mode', viewer: { ...diffFile.viewer, - name: diffViewerModes.mode_changed, + name: diffViewerModes[mode], }, }, }, @@ -260,13 +261,14 @@ describe('DiffFileHeader component', () => { expect(findModeChangedLine().text()).toMatch(/old-mode.+new-mode/); }); - it.each(otherModes.map((m) => [m]))( + it.each(allModes.filter((m) => m[0] !== 'mode_changed'))( 'for %s file mode does not display mode changes', (mode) => { createComponent({ props: { diffFile: { ...diffFile, + mode_changed: false, a_mode: 'old-mode', b_mode: 'new-mode', viewer: { diff --git a/spec/frontend/experimentation/utils_spec.js b/spec/frontend/experimentation/utils_spec.js index e9b7d445473..df24828ceb9 100644 --- a/spec/frontend/experimentation/utils_spec.js +++ b/spec/frontend/experimentation/utils_spec.js @@ -1,4 +1,4 @@ -import { assignGitlabExperiment } from 'helpers/experimentation_helper'; +import { stubExperiments } from 'helpers/experimentation_helper'; import { DEFAULT_VARIANT, CANDIDATE_VARIANT, @@ -7,15 +7,45 @@ import { import * as experimentUtils from '~/experimentation/utils'; describe('experiment Utilities', () => { - const TEST_KEY = 'abc'; + const ABC_KEY = 'abc'; + const DEF_KEY = 'def'; + + let origGon; + let origGl; + + beforeEach(() => { + origGon = window.gon; + origGl = window.gl; + window.gon.experiment = {}; + window.gl.experiments = {}; + }); + + afterEach(() => { + window.gon = origGon; + window.gl = origGl; + }); describe('getExperimentData', () => { + const ABC_DATA = '_abc_data_'; + const ABC_DATA2 = '_updated_abc_data_'; + const DEF_DATA = '_def_data_'; + describe.each` - gon | input | output - ${[TEST_KEY, '_data_']} | ${[TEST_KEY]} | ${{ variant: '_data_' }} - ${[]} | ${[TEST_KEY]} | ${undefined} - `('with input=$input and gon=$gon', ({ gon, input, output }) => { - assignGitlabExperiment(...gon); + gonData | glData | input | output + ${[ABC_KEY, ABC_DATA]} | ${[]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }} + ${[]} | ${[ABC_KEY, ABC_DATA]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }} + ${[ABC_KEY, ABC_DATA]} | ${[DEF_KEY, DEF_DATA]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }} + ${[ABC_KEY, ABC_DATA]} | ${[DEF_KEY, DEF_DATA]} | ${[DEF_KEY]} | ${{ experiment: DEF_KEY, variant: DEF_DATA }} + ${[ABC_KEY, ABC_DATA]} | ${[ABC_KEY, ABC_DATA2]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA2 }} + ${[]} | ${[]} | ${[ABC_KEY]} | ${undefined} + `('with input=$input, gon=$gonData, & gl=$glData', ({ gonData, glData, input, output }) => { + beforeEach(() => { + const [gonKey, gonVariant] = gonData; + const [glKey, glVariant] = glData; + + if (gonKey) window.gon.experiment[gonKey] = { experiment: gonKey, variant: gonVariant }; + if (glKey) window.gl.experiments[glKey] = { experiment: glKey, variant: glVariant }; + }); it(`returns ${output}`, () => { expect(experimentUtils.getExperimentData(...input)).toEqual(output); @@ -25,66 +55,47 @@ describe('experiment Utilities', () => { describe('getAllExperimentContexts', () => { const schema = TRACKING_CONTEXT_SCHEMA; - let origGon; - - beforeEach(() => { - origGon = window.gon; - }); - - afterEach(() => { - window.gon = origGon; - }); it('collects all of the experiment contexts into a single array', () => { - const experiments = [ - { experiment: 'abc', variant: 'candidate' }, - { experiment: 'def', variant: 'control' }, - { experiment: 'ghi', variant: 'blue' }, - ]; - window.gon = { - experiment: experiments.reduce((collector, { experiment, variant }) => { - return { ...collector, [experiment]: { experiment, variant } }; - }, {}), - }; + const experiments = { [ABC_KEY]: 'candidate', [DEF_KEY]: 'control', ghi: 'blue' }; + + stubExperiments(experiments); expect(experimentUtils.getAllExperimentContexts()).toEqual( - experiments.map((data) => ({ schema, data })), + Object.entries(experiments).map(([experiment, variant]) => ({ + schema, + data: { experiment, variant }, + })), ); }); it('returns an empty array if there are no experiments', () => { - window.gon.experiment = {}; - expect(experimentUtils.getAllExperimentContexts()).toEqual([]); }); - - it('includes all additional experiment data', () => { - const experiment = 'experimentWithCustomData'; - const data = { experiment, variant: 'control', color: 'blue', style: 'rounded' }; - window.gon.experiment[experiment] = data; - - expect(experimentUtils.getAllExperimentContexts()).toContainEqual({ schema, data }); - }); }); describe('isExperimentVariant', () => { describe.each` - gon | input | output - ${[TEST_KEY, DEFAULT_VARIANT]} | ${[TEST_KEY, DEFAULT_VARIANT]} | ${true} - ${[TEST_KEY, '_variant_name']} | ${[TEST_KEY, '_variant_name']} | ${true} - ${[TEST_KEY, '_variant_name']} | ${[TEST_KEY, '_bogus_name']} | ${false} - ${[TEST_KEY, '_variant_name']} | ${['boguskey', '_variant_name']} | ${false} - ${[]} | ${[TEST_KEY, '_variant_name']} | ${false} - `('with input=$input and gon=$gon', ({ gon, input, output }) => { - assignGitlabExperiment(...gon); - - it(`returns ${output}`, () => { - expect(experimentUtils.isExperimentVariant(...input)).toEqual(output); - }); - }); + experiment | variant | input | output + ${ABC_KEY} | ${DEFAULT_VARIANT} | ${[ABC_KEY, DEFAULT_VARIANT]} | ${true} + ${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_variant_name']} | ${true} + ${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_bogus_name']} | ${false} + ${ABC_KEY} | ${'_variant_name'} | ${['boguskey', '_variant_name']} | ${false} + ${undefined} | ${undefined} | ${[ABC_KEY, '_variant_name']} | ${false} + `( + 'with input=$input, experiment=$experiment, variant=$variant', + ({ experiment, variant, input, output }) => { + it(`returns ${output}`, () => { + if (experiment) stubExperiments({ [experiment]: variant }); + + expect(experimentUtils.isExperimentVariant(...input)).toEqual(output); + }); + }, + ); }); describe('experiment', () => { + const experiment = 'marley'; const useSpy = jest.fn(); const controlSpy = jest.fn(); const trySpy = jest.fn(); @@ -98,49 +109,62 @@ describe('experiment Utilities', () => { }; describe('when there is no experiment data', () => { - it('calls control variant', () => { - experimentUtils.experiment('marley', variants); + it('calls the use variant', () => { + experimentUtils.experiment(experiment, variants); expect(useSpy).toHaveBeenCalled(); }); + + describe("when 'control' is provided instead of 'use'", () => { + it('calls the control variant', () => { + experimentUtils.experiment(experiment, { control: controlSpy }); + expect(controlSpy).toHaveBeenCalled(); + }); + }); }); describe('when experiment variant is "control"', () => { - assignGitlabExperiment('marley', DEFAULT_VARIANT); + beforeEach(() => { + stubExperiments({ [experiment]: DEFAULT_VARIANT }); + }); - it('calls the control variant', () => { - experimentUtils.experiment('marley', variants); + it('calls the use variant', () => { + experimentUtils.experiment(experiment, variants); expect(useSpy).toHaveBeenCalled(); }); describe("when 'control' is provided instead of 'use'", () => { it('calls the control variant', () => { - experimentUtils.experiment('marley', { control: controlSpy }); + experimentUtils.experiment(experiment, { control: controlSpy }); expect(controlSpy).toHaveBeenCalled(); }); }); }); describe('when experiment variant is "candidate"', () => { - assignGitlabExperiment('marley', CANDIDATE_VARIANT); + beforeEach(() => { + stubExperiments({ [experiment]: CANDIDATE_VARIANT }); + }); - it('calls the candidate variant', () => { - experimentUtils.experiment('marley', variants); + it('calls the try variant', () => { + experimentUtils.experiment(experiment, variants); expect(trySpy).toHaveBeenCalled(); }); describe("when 'candidate' is provided instead of 'try'", () => { - it('calls the control variant', () => { - experimentUtils.experiment('marley', { candidate: candidateSpy }); + it('calls the candidate variant', () => { + experimentUtils.experiment(experiment, { candidate: candidateSpy }); expect(candidateSpy).toHaveBeenCalled(); }); }); }); describe('when experiment variant is "get_up_stand_up"', () => { - assignGitlabExperiment('marley', 'get_up_stand_up'); + beforeEach(() => { + stubExperiments({ [experiment]: 'get_up_stand_up' }); + }); it('calls the get-up-stand-up variant', () => { - experimentUtils.experiment('marley', variants); + experimentUtils.experiment(experiment, variants); expect(getUpStandUpSpy).toHaveBeenCalled(); }); }); @@ -148,14 +172,17 @@ describe('experiment Utilities', () => { describe('getExperimentVariant', () => { it.each` - gon | input | output - ${{ experiment: { [TEST_KEY]: { variant: DEFAULT_VARIANT } } }} | ${[TEST_KEY]} | ${DEFAULT_VARIANT} - ${{ experiment: { [TEST_KEY]: { variant: CANDIDATE_VARIANT } } }} | ${[TEST_KEY]} | ${CANDIDATE_VARIANT} - ${{}} | ${[TEST_KEY]} | ${DEFAULT_VARIANT} - `('with input=$input and gon=$gon, returns $output', ({ gon, input, output }) => { - window.gon = gon; - - expect(experimentUtils.getExperimentVariant(...input)).toEqual(output); - }); + experiment | variant | input | output + ${ABC_KEY} | ${DEFAULT_VARIANT} | ${ABC_KEY} | ${DEFAULT_VARIANT} + ${ABC_KEY} | ${CANDIDATE_VARIANT} | ${ABC_KEY} | ${CANDIDATE_VARIANT} + ${undefined} | ${undefined} | ${ABC_KEY} | ${DEFAULT_VARIANT} + `( + 'with input=$input, experiment=$experiment, & variant=$variant; returns $output', + ({ experiment, variant, input, output }) => { + stubExperiments({ [experiment]: variant }); + + expect(experimentUtils.getExperimentVariant(input)).toEqual(output); + }, + ); }); }); diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js index b63236dec82..8e464968453 100644 --- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js @@ -1,12 +1,14 @@ -import { GlAlert, GlLink } from '@gitlab/ui'; +import { GlAlert, GlLink, GlEmptyState } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue'; import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue'; import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue'; +import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue'; import createStore from '~/jira_connect/subscriptions/store'; import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types'; import { __ } from '~/locale'; +import { mockSubscription } from '../mock_data'; jest.mock('~/jira_connect/subscriptions/utils', () => ({ retrieveAlert: jest.fn().mockReturnValue({ message: 'error message' }), @@ -20,6 +22,8 @@ describe('JiraConnectApp', () => { const findAlertLink = () => findAlert().findComponent(GlLink); const findSignInButton = () => wrapper.findComponent(SignInButton); const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton); + const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList); + const findEmptyState = () => wrapper.findComponent(GlEmptyState); const createComponent = ({ provide, mountFn = shallowMount } = {}) => { store = createStore(); @@ -36,91 +40,114 @@ describe('JiraConnectApp', () => { describe('template', () => { describe.each` - scenario | usersPath | expectSignInButton | expectNamespaceButton - ${'user is not signed in'} | ${'/users'} | ${true} | ${false} - ${'user is signed in'} | ${undefined} | ${false} | ${true} - `('when $scenario', ({ usersPath, expectSignInButton, expectNamespaceButton }) => { - beforeEach(() => { - createComponent({ - provide: { - usersPath, - }, + scenario | usersPath | subscriptions | expectSignInButton | expectEmptyState | expectNamespaceButton | expectSubscriptionsList + ${'user is not signed in with subscriptions'} | ${'/users'} | ${[mockSubscription]} | ${true} | ${false} | ${false} | ${true} + ${'user is not signed in without subscriptions'} | ${'/users'} | ${undefined} | ${true} | ${false} | ${false} | ${false} + ${'user is signed in with subscriptions'} | ${undefined} | ${[mockSubscription]} | ${false} | ${false} | ${true} | ${true} + ${'user is signed in without subscriptions'} | ${undefined} | ${undefined} | ${false} | ${true} | ${false} | ${false} + `( + 'when $scenario', + ({ + usersPath, + expectSignInButton, + subscriptions, + expectEmptyState, + expectNamespaceButton, + expectSubscriptionsList, + }) => { + beforeEach(() => { + createComponent({ + provide: { + usersPath, + subscriptions, + }, + }); }); - }); - it('renders sign in button as expected', () => { - expect(findSignInButton().exists()).toBe(expectSignInButton); - }); + it(`${expectSignInButton ? 'renders' : 'does not render'} sign in button`, () => { + expect(findSignInButton().exists()).toBe(expectSignInButton); + }); - it('renders "Add Namespace" button as expected', () => { - expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton); - }); - }); + it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => { + expect(findEmptyState().exists()).toBe(expectEmptyState); + }); - describe('alert', () => { - it.each` - message | variant | alertShouldRender - ${'Test error'} | ${'danger'} | ${true} - ${'Test notice'} | ${'info'} | ${true} - ${''} | ${undefined} | ${false} - ${undefined} | ${undefined} | ${false} - `( - 'renders correct alert when message is `$message` and variant is `$variant`', - async ({ message, alertShouldRender, variant }) => { - createComponent(); - - store.commit(SET_ALERT, { message, variant }); - await wrapper.vm.$nextTick(); - - const alert = findAlert(); - - expect(alert.exists()).toBe(alertShouldRender); - if (alertShouldRender) { - expect(alert.isVisible()).toBe(alertShouldRender); - expect(alert.html()).toContain(message); - expect(alert.props('variant')).toBe(variant); - expect(findAlertLink().exists()).toBe(false); - } - }, - ); - - it('hides alert on @dismiss event', async () => { + it(`${ + expectNamespaceButton ? 'renders' : 'does not render' + } button to add namespace`, () => { + expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton); + }); + + it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => { + expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList); + }); + }, + ); + }); + + describe('alert', () => { + it.each` + message | variant | alertShouldRender + ${'Test error'} | ${'danger'} | ${true} + ${'Test notice'} | ${'info'} | ${true} + ${''} | ${undefined} | ${false} + ${undefined} | ${undefined} | ${false} + `( + 'renders correct alert when message is `$message` and variant is `$variant`', + async ({ message, alertShouldRender, variant }) => { createComponent(); - store.commit(SET_ALERT, { message: 'test message' }); + store.commit(SET_ALERT, { message, variant }); await wrapper.vm.$nextTick(); - findAlert().vm.$emit('dismiss'); - await wrapper.vm.$nextTick(); + const alert = findAlert(); - expect(findAlert().exists()).toBe(false); - }); + expect(alert.exists()).toBe(alertShouldRender); + if (alertShouldRender) { + expect(alert.isVisible()).toBe(alertShouldRender); + expect(alert.html()).toContain(message); + expect(alert.props('variant')).toBe(variant); + expect(findAlertLink().exists()).toBe(false); + } + }, + ); - it('renders link when `linkUrl` is set', async () => { - createComponent({ mountFn: mount }); + it('hides alert on @dismiss event', async () => { + createComponent(); - store.commit(SET_ALERT, { - message: __('test message %{linkStart}test link%{linkEnd}'), - linkUrl: 'https://gitlab.com', - }); - await wrapper.vm.$nextTick(); + store.commit(SET_ALERT, { message: 'test message' }); + await wrapper.vm.$nextTick(); + + findAlert().vm.$emit('dismiss'); + await wrapper.vm.$nextTick(); + + expect(findAlert().exists()).toBe(false); + }); - const alertLink = findAlertLink(); + it('renders link when `linkUrl` is set', async () => { + createComponent({ mountFn: mount }); - expect(alertLink.exists()).toBe(true); - expect(alertLink.text()).toContain('test link'); - expect(alertLink.attributes('href')).toBe('https://gitlab.com'); + store.commit(SET_ALERT, { + message: __('test message %{linkStart}test link%{linkEnd}'), + linkUrl: 'https://gitlab.com', }); + await wrapper.vm.$nextTick(); - describe('when alert is set in localStoage', () => { - it('renders alert on mount', () => { - createComponent(); + const alertLink = findAlertLink(); - const alert = findAlert(); + expect(alertLink.exists()).toBe(true); + expect(alertLink.text()).toContain('test link'); + expect(alertLink.attributes('href')).toBe('https://gitlab.com'); + }); - expect(alert.exists()).toBe(true); - expect(alert.html()).toContain('error message'); - }); + describe('when alert is set in localStoage', () => { + it('renders alert on mount', () => { + createComponent(); + + const alert = findAlert(); + + expect(alert.exists()).toBe(true); + expect(alert.html()).toContain('error message'); }); }); }); diff --git a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js index 32b43765843..4e4a2b58600 100644 --- a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js @@ -1,12 +1,15 @@ -import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui'; -import { mount, shallowMount } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; import waitForPromises from 'helpers/wait_for_promises'; import * as JiraConnectApi from '~/jira_connect/subscriptions/api'; +import GroupItemName from '~/jira_connect/subscriptions/components/group_item_name.vue'; + import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue'; import createStore from '~/jira_connect/subscriptions/store'; import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types'; import { reloadPage } from '~/jira_connect/subscriptions/utils'; +import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { mockSubscription } from '../mock_data'; jest.mock('~/jira_connect/subscriptions/utils'); @@ -15,11 +18,13 @@ describe('SubscriptionsList', () => { let wrapper; let store; - const createComponent = ({ mountFn = shallowMount, provide = {} } = {}) => { + const createComponent = () => { store = createStore(); - wrapper = mountFn(SubscriptionsList, { - provide, + wrapper = mount(SubscriptionsList, { + provide: { + subscriptions: [mockSubscription], + }, store, }); }; @@ -28,28 +33,28 @@ describe('SubscriptionsList', () => { wrapper.destroy(); }); - const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); - const findGlTable = () => wrapper.findComponent(GlTable); - const findUnlinkButton = () => findGlTable().findComponent(GlButton); + const findUnlinkButton = () => wrapper.findComponent(GlButton); const clickUnlinkButton = () => findUnlinkButton().trigger('click'); describe('template', () => { - it('renders GlEmptyState when subscriptions is empty', () => { + beforeEach(() => { createComponent(); + }); + + it('renders "name" cell correctly', () => { + const groupItemNames = wrapper.findAllComponents(GroupItemName); + expect(groupItemNames.wrappers).toHaveLength(1); - expect(findGlEmptyState().exists()).toBe(true); - expect(findGlTable().exists()).toBe(false); + const item = groupItemNames.at(0); + expect(item.props('group')).toBe(mockSubscription.group); }); - it('renders GlTable when subscriptions are present', () => { - createComponent({ - provide: { - subscriptions: [mockSubscription], - }, - }); + it('renders "created at" cell correctly', () => { + const timeAgoTooltips = wrapper.findAllComponents(TimeagoTooltip); + expect(timeAgoTooltips.wrappers).toHaveLength(1); - expect(findGlEmptyState().exists()).toBe(false); - expect(findGlTable().exists()).toBe(true); + const item = timeAgoTooltips.at(0); + expect(item.props('time')).toBe(mockSubscription.created_at); }); }); @@ -57,12 +62,7 @@ describe('SubscriptionsList', () => { let removeSubscriptionSpy; beforeEach(() => { - createComponent({ - mountFn: mount, - provide: { - subscriptions: [mockSubscription], - }, - }); + createComponent(); removeSubscriptionSpy = jest.spyOn(JiraConnectApi, 'removeSubscription').mockResolvedValue(); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js index be15e4df66d..0fb0d5b0b68 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js @@ -46,7 +46,7 @@ describe('Wip', () => { is_new_mr_data: true, }; - describe('handleRemoveWIP', () => { + describe('handleRemoveDraft', () => { it('should make a request to service and handle response', (done) => { const vm = createComponent(); @@ -59,7 +59,7 @@ describe('Wip', () => { }), ); - vm.handleRemoveWIP(); + vm.handleRemoveDraft(); setImmediate(() => { expect(vm.isMakingRequest).toBeTruthy(); expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); @@ -84,7 +84,7 @@ describe('Wip', () => { expect(el.innerText).toContain('This merge request is still a draft.'); expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy(); expect(el.querySelector('button').innerText).toContain('Merge'); - expect(el.querySelector('.js-remove-wip').innerText.replace(/\s\s+/g, ' ')).toContain( + expect(el.querySelector('.js-remove-draft').innerText.replace(/\s\s+/g, ' ')).toContain( 'Mark as ready', ); }); @@ -93,7 +93,7 @@ describe('Wip', () => { vm.mr.removeWIPPath = ''; Vue.nextTick(() => { - expect(el.querySelector('.js-remove-wip')).toEqual(null); + expect(el.querySelector('.js-remove-draft')).toEqual(null); done(); }); }); diff --git a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js index 4d0e5e71f27..fc760f5c5be 100644 --- a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js +++ b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js @@ -15,7 +15,7 @@ describe('getStateKey', () => { branchMissing: false, commitsCount: 2, hasConflicts: false, - workInProgress: false, + draft: false, }; const bound = getStateKey.bind(context); @@ -49,9 +49,9 @@ describe('getStateKey', () => { expect(bound()).toEqual('unresolvedDiscussions'); - context.workInProgress = true; + context.draft = true; - expect(bound()).toEqual('workInProgress'); + expect(bound()).toEqual('draft'); context.onlyAllowMergeIfPipelineSucceeds = true; context.isPipelineFailed = true; @@ -99,7 +99,7 @@ describe('getStateKey', () => { branchMissing: false, commitsCount: 2, hasConflicts: false, - workInProgress: false, + draft: false, }; const bound = getStateKey.bind(context); diff --git a/spec/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/graphql/mutations/merge_requests/set_wip_spec.rb deleted file mode 100644 index fae9c4f7fe0..00000000000 --- a/spec/graphql/mutations/merge_requests/set_wip_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Mutations::MergeRequests::SetWip do - let(:merge_request) { create(:merge_request) } - let(:user) { create(:user) } - - subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } - - specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) } - - describe '#resolve' do - let(:wip) { true } - let(:mutated_merge_request) { subject[:merge_request] } - - subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, wip: wip) } - - it_behaves_like 'permission level for merge request mutation is correctly verified' - - context 'when the user can update the merge request' do - before do - merge_request.project.add_developer(user) - end - - it 'returns the merge request as a wip' do - expect(mutated_merge_request).to eq(merge_request) - expect(mutated_merge_request).to be_work_in_progress - expect(subject[:errors]).to be_empty - end - - it 'returns errors merge request could not be updated' do - # Make the merge request invalid - merge_request.allow_broken = true - merge_request.update!(source_project: nil) - - expect(subject[:errors]).not_to be_empty - end - - context 'when passing wip as false' do - let(:wip) { false } - - it 'removes `wip` from the title' do - merge_request.update!(title: "WIP: working on it") - - expect(mutated_merge_request).not_to be_work_in_progress - end - - it 'does not do anything if the title did not start with wip' do - expect(mutated_merge_request).not_to be_work_in_progress - end - end - end - end -end diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb index bc3ccb0d9ba..b17b7c32289 100644 --- a/spec/graphql/types/merge_request_type_spec.rb +++ b/spec/graphql/types/merge_request_type_spec.rb @@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do notes discussions user_permissions id iid title title_html description description_html state created_at updated_at source_project target_project project project_id source_project_id target_project_id source_branch - target_branch work_in_progress draft merge_when_pipeline_succeeds diff_head_sha + target_branch draft merge_when_pipeline_succeeds diff_head_sha merge_commit_sha user_notes_count user_discussions_count should_remove_source_branch diff_refs diff_stats diff_stats_summary force_remove_source_branch diff --git a/spec/graphql/types/mutation_type_spec.rb b/spec/graphql/types/mutation_type_spec.rb index c1a5c93c85b..95d835c88cf 100644 --- a/spec/graphql/types/mutation_type_spec.rb +++ b/spec/graphql/types/mutation_type_spec.rb @@ -3,14 +3,6 @@ require 'spec_helper' RSpec.describe Types::MutationType do - it 'is expected to have the deprecated MergeRequestSetWip' do - field = get_field('MergeRequestSetWip') - - expect(field).to be_present - expect(field.deprecation_reason).to be_present - expect(field.resolver).to eq(Mutations::MergeRequests::SetWip) - end - it 'is expected to have the MergeRequestSetDraft' do expect(described_class).to have_graphql_mutation(Mutations::MergeRequests::SetDraft) end diff --git a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb index 7d156c2c3df..c5197fb29d9 100644 --- a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb +++ b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb @@ -111,6 +111,7 @@ RSpec.describe BulkImports::NdjsonPipeline do context = double(portable: group, current_user: user, import_export_config: config, bulk_import: import_double, entity: entity_double) allow(subject).to receive(:import_export_config).and_return(config) allow(subject).to receive(:context).and_return(context) + relation_object = double expect(Gitlab::ImportExport::Group::RelationFactory) .to receive(:create) @@ -124,6 +125,8 @@ RSpec.describe BulkImports::NdjsonPipeline do user: user, excluded_keys: nil ) + .and_return(relation_object) + expect(relation_object).to receive(:assign_attributes).with(group: group) subject.transform(context, data) end diff --git a/spec/lib/bulk_imports/projects/stage_spec.rb b/spec/lib/bulk_imports/projects/stage_spec.rb index 685bf223f9c..7c07c4f7b3d 100644 --- a/spec/lib/bulk_imports/projects/stage_spec.rb +++ b/spec/lib/bulk_imports/projects/stage_spec.rb @@ -27,7 +27,8 @@ RSpec.describe BulkImports::Projects::Stage do describe '#pipelines' do it 'list all the pipelines with their stage number, ordered by stage' do - expect(subject.pipelines).to eq(pipelines) + expect(subject.pipelines & pipelines).to eq(pipelines) + expect(subject.pipelines.last.last).to eq(BulkImports::Common::Pipelines::EntityFinisher) end end end diff --git a/spec/lib/gitlab/application_rate_limiter_spec.rb b/spec/lib/gitlab/application_rate_limiter_spec.rb index 0ce7ae8d3aa..c74bcf8d678 100644 --- a/spec/lib/gitlab/application_rate_limiter_spec.rb +++ b/spec/lib/gitlab/application_rate_limiter_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::ApplicationRateLimiter do subject { described_class } - describe '.throttled?' do + describe '.throttled?', :clean_gitlab_redis_rate_limiting do let(:rate_limits) do { test_action: { @@ -56,136 +56,51 @@ RSpec.describe Gitlab::ApplicationRateLimiter do end end - context 'when rate_limiter_safe_increment is disabled' do - let(:redis) { double('redis') } - let(:key) { rate_limits.keys[0] } + shared_examples 'throttles based on key and scope' do + let(:start_time) { Time.current.beginning_of_hour } - before do - allow(Gitlab::Redis::RateLimiting).to receive(:with).and_yield(redis) - - stub_feature_flags(rate_limiter_safe_increment: false) - end - - shared_examples 'action rate limiter' do - it 'increases the throttle count and sets the expiration time' do - expect(redis).to receive(:incr).with(cache_key).and_return(1) - expect(redis).to receive(:expire).with(cache_key, 120) - - expect(subject.throttled?(key, scope: scope)).to be_falsy + it 'returns true when threshold is exceeded' do + travel_to(start_time) do + expect(subject.throttled?(:test_action, scope: scope)).to eq(false) end - it 'returns true if the key is throttled' do - expect(redis).to receive(:incr).with(cache_key).and_return(2) - expect(redis).not_to receive(:expire) + travel_to(start_time + 1.minute) do + expect(subject.throttled?(:test_action, scope: scope)).to eq(true) - expect(subject.throttled?(key, scope: scope)).to be_truthy - end - - context 'when throttling is disabled' do - it 'returns false and does not set expiration time' do - expect(redis).not_to receive(:incr) - expect(redis).not_to receive(:expire) - - expect(subject.throttled?(key, scope: scope, threshold: 0)).to be_falsy - end + # Assert that it does not affect other actions or scope + expect(subject.throttled?(:another_action, scope: scope)).to eq(false) + expect(subject.throttled?(:test_action, scope: [user])).to eq(false) end end - context 'when the key is an array of only ActiveRecord models' do - let(:scope) { [user, project] } + it 'returns false when interval has elapsed' do + travel_to(start_time) do + expect(subject.throttled?(:test_action, scope: scope)).to eq(false) - let(:cache_key) do - "application_rate_limiter:test_action:user:#{user.id}:project:#{project.id}" + # another_action has a threshold of 3 so we simulate 2 requests + expect(subject.throttled?(:another_action, scope: scope)).to eq(false) + expect(subject.throttled?(:another_action, scope: scope)).to eq(false) end - it_behaves_like 'action rate limiter' - - context 'when a scope attribute is nil' do - let(:scope) { [user, nil] } - - let(:cache_key) do - "application_rate_limiter:test_action:user:#{user.id}" - end - - it_behaves_like 'action rate limiter' - end - end - - context 'when the key is a combination of ActiveRecord models and strings' do - let(:project) { create(:project, :public, :repository) } - let(:commit) { project.repository.commit } - let(:path) { 'app/controllers/groups_controller.rb' } - let(:scope) { [project, commit, path] } - - let(:cache_key) do - "application_rate_limiter:test_action:project:#{project.id}:commit:#{commit.sha}:#{path}" - end - - it_behaves_like 'action rate limiter' - - context 'when a scope attribute is nil' do - let(:scope) { [project, commit, nil] } - - let(:cache_key) do - "application_rate_limiter:test_action:project:#{project.id}:commit:#{commit.sha}" - end + travel_to(start_time + 2.minutes) do + expect(subject.throttled?(:test_action, scope: scope)).to eq(false) - it_behaves_like 'action rate limiter' + # Assert that another_action has its own interval that hasn't elapsed + expect(subject.throttled?(:another_action, scope: scope)).to eq(true) end end end - context 'when rate_limiter_safe_increment is enabled', :clean_gitlab_redis_rate_limiting do - before do - stub_feature_flags(rate_limiter_safe_increment: true) - end - - shared_examples 'throttles based on key and scope' do - let(:start_time) { Time.current.beginning_of_hour } - - it 'returns true when threshold is exceeded' do - travel_to(start_time) do - expect(subject.throttled?(:test_action, scope: scope)).to eq(false) - end - - travel_to(start_time + 1.minute) do - expect(subject.throttled?(:test_action, scope: scope)).to eq(true) - - # Assert that it does not affect other actions or scope - expect(subject.throttled?(:another_action, scope: scope)).to eq(false) - expect(subject.throttled?(:test_action, scope: [user])).to eq(false) - end - end - - it 'returns false when interval has elapsed' do - travel_to(start_time) do - expect(subject.throttled?(:test_action, scope: scope)).to eq(false) - - # another_action has a threshold of 3 so we simulate 2 requests - expect(subject.throttled?(:another_action, scope: scope)).to eq(false) - expect(subject.throttled?(:another_action, scope: scope)).to eq(false) - end - - travel_to(start_time + 2.minutes) do - expect(subject.throttled?(:test_action, scope: scope)).to eq(false) - - # Assert that another_action has its own interval that hasn't elapsed - expect(subject.throttled?(:another_action, scope: scope)).to eq(true) - end - end - end - - context 'when using ActiveRecord models as scope' do - let(:scope) { [user, project] } + context 'when using ActiveRecord models as scope' do + let(:scope) { [user, project] } - it_behaves_like 'throttles based on key and scope' - end + it_behaves_like 'throttles based on key and scope' + end - context 'when using ActiveRecord models and strings as scope' do - let(:scope) { [project, 'app/controllers/groups_controller.rb'] } + context 'when using ActiveRecord models and strings as scope' do + let(:scope) { [project, 'app/controllers/groups_controller.rb'] } - it_behaves_like 'throttles based on key and scope' - end + it_behaves_like 'throttles based on key and scope' end end diff --git a/spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb b/spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb index e746451b1b9..f9628849dbf 100644 --- a/spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb @@ -111,11 +111,11 @@ RSpec.describe Gitlab::BackgroundMigration::PopulatePersonalSnippetStatistics do if with_repo allow(snippet).to receive(:disk_path).and_return(disk_path(snippet)) + raw_repository(snippet).create_repository + TestEnv.copy_repo(snippet, bare_repo: TestEnv.factory_repo_path_bare, refs: TestEnv::BRANCH_SHA) - - raw_repository(snippet).create_repository end end end diff --git a/spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb b/spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb index 897f5e81372..7884e0d97c0 100644 --- a/spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb @@ -183,11 +183,11 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateProjectSnippetStatistics do if with_repo allow(snippet).to receive(:disk_path).and_return(disk_path(snippet)) + raw_repository(snippet).create_repository + TestEnv.copy_repo(snippet, bare_repo: TestEnv.factory_repo_path_bare, refs: TestEnv::BRANCH_SHA) - - raw_repository(snippet).create_repository end end end diff --git a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb index cdcc862c376..9d49db1f018 100644 --- a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb +++ b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb @@ -38,7 +38,8 @@ RSpec.describe Gitlab::Database::Count::ReltuplesCountStrategy do it 'returns nil counts for inherited tables' do models.each { |model| expect(model).not_to receive(:count) } - expect(subject).to eq({ Namespace => 3 }) + # 3 Namespaces as parents for each Project and 3 ProjectNamespaces(for each Project) + expect(subject).to eq({ Namespace => 6 }) end end diff --git a/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb index c2028f8c238..2f261aebf02 100644 --- a/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb +++ b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb @@ -47,7 +47,8 @@ RSpec.describe Gitlab::Database::Count::TablesampleCountStrategy do result = subject expect(result[Project]).to eq(3) expect(result[Group]).to eq(1) - expect(result[Namespace]).to eq(4) + # 1-Group, 3 namespaces for each project and 3 project namespaces for each project + expect(result[Namespace]).to eq(7) end end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index 55102554508..20d5972bd88 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -136,7 +136,7 @@ RSpec.describe Gitlab::Gpg::Commit do it 'returns a valid signature' do verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true) allow(GPGME::Crypto).to receive(:new).and_return(crypto) - allow(crypto).to receive(:verify).and_return(verified_signature) + allow(crypto).to receive(:verify).and_yield(verified_signature) signature = described_class.new(commit).signature @@ -178,7 +178,7 @@ RSpec.describe Gitlab::Gpg::Commit do keyid = GpgHelpers::User1.fingerprint.last(16) verified_signature = double('verified-signature', fingerprint: keyid, valid?: true) allow(GPGME::Crypto).to receive(:new).and_return(crypto) - allow(crypto).to receive(:verify).and_return(verified_signature) + allow(crypto).to receive(:verify).and_yield(verified_signature) signature = described_class.new(commit).signature @@ -194,6 +194,71 @@ RSpec.describe Gitlab::Gpg::Commit do end end + context 'commit with multiple signatures' do + let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first } + + let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) } + + let!(:gpg_key) do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + let!(:crypto) { instance_double(GPGME::Crypto) } + + before do + fake_signature = [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) + .with(Gitlab::Git::Repository, commit_sha) + .and_return(fake_signature) + end + + it 'returns an invalid signatures error' do + verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true) + allow(GPGME::Crypto).to receive(:new).and_return(crypto) + allow(crypto).to receive(:verify).and_yield(verified_signature).and_yield(verified_signature) + + signature = described_class.new(commit).signature + + expect(signature).to have_attributes( + commit_sha: commit_sha, + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + gpg_key_user_name: GpgHelpers::User1.names.first, + gpg_key_user_email: GpgHelpers::User1.emails.first, + verification_status: 'multiple_signatures' + ) + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(multiple_gpg_signatures: false) + end + + it 'returns an valid signature' do + verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true) + allow(GPGME::Crypto).to receive(:new).and_return(crypto) + allow(crypto).to receive(:verify).and_yield(verified_signature).and_yield(verified_signature) + + signature = described_class.new(commit).signature + + expect(signature).to have_attributes( + commit_sha: commit_sha, + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + gpg_key_user_name: GpgHelpers::User1.names.first, + gpg_key_user_email: GpgHelpers::User1.emails.first, + verification_status: 'verified' + ) + end + end + end + context 'commit signed with a subkey' do let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User3.emails.first } diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index 2f38ed58727..f0ba0f0459d 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -425,6 +425,9 @@ RSpec.describe Gitlab::PathRegex do it { is_expected.not_to match('gitlab.org/') } it { is_expected.not_to match('/gitlab.org') } it { is_expected.not_to match('gitlab git') } + it { is_expected.not_to match('gitlab?') } + it { is_expected.to match('gitlab.org-') } + it { is_expected.to match('gitlab.org_') } end describe '.project_path_format_regex' do @@ -437,6 +440,14 @@ RSpec.describe Gitlab::PathRegex do it { is_expected.not_to match('?gitlab') } it { is_expected.not_to match('git lab') } it { is_expected.not_to match('gitlab.git') } + it { is_expected.not_to match('gitlab?') } + it { is_expected.not_to match('gitlab git') } + it { is_expected.to match('gitlab.org') } + it { is_expected.to match('gitlab.org-') } + it { is_expected.to match('gitlab.org_') } + it { is_expected.to match('gitlab.org.') } + it { is_expected.not_to match('gitlab.org/') } + it { is_expected.not_to match('/gitlab.org') } end context 'repository routes' do diff --git a/spec/models/concerns/loaded_in_group_list_spec.rb b/spec/models/concerns/loaded_in_group_list_spec.rb index 94a9a8cde12..d38e842c666 100644 --- a/spec/models/concerns/loaded_in_group_list_spec.rb +++ b/spec/models/concerns/loaded_in_group_list_spec.rb @@ -25,7 +25,7 @@ RSpec.describe LoadedInGroupList do context 'with project namespaces' do let_it_be(:group1) { create(:group, parent: parent) } let_it_be(:group2) { create(:group, parent: parent) } - let_it_be(:project_namespace) { create(:project_namespace, project: project) } + let_it_be(:project_namespace) { project.project_namespace } it 'does not include project_namespaces in the count of subgroups' do expect(found_group.preloaded_subgroup_count).to eq(3) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index c326b2d6fcd..2467ef541b2 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -566,7 +566,7 @@ RSpec.describe Group do context 'when project namespace exists in the group' do let!(:project) { create(:project, group: group) } - let!(:project_namespace) { create(:project_namespace, project: project) } + let!(:project_namespace) { project.project_namespace } it 'filters out project namespace' do expect(group.descendants.find_by_id(project_namespace.id)).to be_nil diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 46a22f64591..8f5860c799c 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -32,9 +32,10 @@ RSpec.describe Namespace do describe '#children' do let_it_be(:group) { create(:group) } let_it_be(:subgroup) { create(:group, parent: group) } - let_it_be(:project_namespace) { create(:project_namespace, parent: group) } + let_it_be(:project_with_namespace) { create(:project, namespace: group) } it 'excludes project namespaces' do + expect(project_with_namespace.project_namespace.parent).to eq(group) expect(group.children).to match_array([subgroup]) end end @@ -239,7 +240,9 @@ RSpec.describe Namespace do let(:namespace) { build(:project_namespace) } it 'allows to update path to single char' do - namespace = create(:project_namespace) + project = create(:project) + namespace = project.project_namespace + namespace.update!(path: 'j') expect(namespace).to be_valid @@ -342,9 +345,13 @@ RSpec.describe Namespace do describe '.without_project_namespaces' do let_it_be(:user_namespace) { create(:user_namespace) } - let_it_be(:project_namespace) { create(:project_namespace) } + let_it_be(:project) { create(:project) } + let_it_be(:project_namespace) { project.project_namespace } it 'excludes project namespaces' do + expect(project_namespace).not_to be_nil + expect(project_namespace.parent).not_to be_nil + expect(described_class.all).to include(project_namespace) expect(described_class.without_project_namespaces).to match_array([namespace, namespace1, namespace2, namespace1sub, namespace2sub, user_namespace, project_namespace.parent]) end end @@ -562,7 +569,7 @@ RSpec.describe Namespace do context 'with project namespaces' do let_it_be(:project) { create(:project, namespace: parent_group, path: 'some-new-path') } - let_it_be(:project_namespace) { create(:project_namespace, project: project) } + let_it_be(:project_namespace) { project.project_namespace } it 'does not return project namespace' do search_result = described_class.search('path') diff --git a/spec/models/namespaces/project_namespace_spec.rb b/spec/models/namespaces/project_namespace_spec.rb index f38e8aa85d0..4416c49f1bf 100644 --- a/spec/models/namespaces/project_namespace_spec.rb +++ b/spec/models/namespaces/project_namespace_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Namespaces::ProjectNamespace, type: :model do # using delete rather than destroy due to `delete` skipping AR hooks/callbacks # so it's ensured to work at the DB level. Uses ON DELETE CASCADE on foreign key let_it_be(:project) { create(:project) } - let_it_be(:project_namespace) { create(:project_namespace, project: project) } + let_it_be(:project_namespace) { project.project_namespace } it 'also deletes the associated project' do project_namespace.delete diff --git a/spec/models/packages/npm/metadatum_spec.rb b/spec/models/packages/npm/metadatum_spec.rb new file mode 100644 index 00000000000..ff8cce5310e --- /dev/null +++ b/spec/models/packages/npm/metadatum_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Npm::Metadatum, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:package).inverse_of(:npm_metadatum) } + end + + describe 'validations' do + describe 'package', :aggregate_failures do + it { is_expected.to validate_presence_of(:package) } + + it 'ensure npm package type' do + metadatum = build(:npm_metadatum) + + metadatum.package = build(:nuget_package) + + expect(metadatum).not_to be_valid + expect(metadatum.errors).to contain_exactly('Package type must be NPM') + end + end + + describe 'package_json', :aggregate_failures do + let(:valid_json) { { 'name' => 'foo', 'version' => 'v1.0', 'dist' => { 'tarball' => 'x', 'shasum' => 'x' } } } + + it { is_expected.to allow_value(valid_json).for(:package_json) } + it { is_expected.to allow_value(valid_json.merge('extra-field': { 'foo': 'bar' })).for(:package_json) } + it { is_expected.to allow_value(with_dist { |dist| dist.merge('extra-field': 'x') }).for(:package_json) } + + %w[name version dist].each do |field| + it { is_expected.not_to allow_value(valid_json.except(field)).for(:package_json) } + end + + %w[tarball shasum].each do |field| + it { is_expected.not_to allow_value(with_dist { |dist| dist.except(field) }).for(:package_json) } + end + + it { is_expected.not_to allow_value({}).for(:package_json) } + + it { is_expected.not_to allow_value(test: 'test' * 10000).for(:package_json) } + + def with_dist + valid_json.tap do |h| + h['dist'] = yield(h['dist']) + end + end + end + end +end diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index 12a2a2a94a6..3fca139fc12 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -20,6 +20,7 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.to have_one(:debian_distribution).through(:debian_publication).source(:distribution).inverse_of(:packages).class_name('Packages::Debian::ProjectDistribution') } it { is_expected.to have_one(:nuget_metadatum).inverse_of(:package) } it { is_expected.to have_one(:rubygems_metadatum).inverse_of(:package) } + it { is_expected.to have_one(:npm_metadatum).inverse_of(:package) } end describe '.with_debian_codename' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 4b3a6c731d5..109504ae1d8 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Project, factory_default: :keep do describe 'associations' do it { is_expected.to belong_to(:group) } it { is_expected.to belong_to(:namespace) } - it { is_expected.to belong_to(:project_namespace).class_name('Namespaces::ProjectNamespace').with_foreign_key('project_namespace_id').inverse_of(:project) } + it { is_expected.to belong_to(:project_namespace).class_name('Namespaces::ProjectNamespace').with_foreign_key('project_namespace_id') } it { is_expected.to belong_to(:creator).class_name('User') } it { is_expected.to belong_to(:pool_repository) } it { is_expected.to have_many(:users) } @@ -191,7 +191,7 @@ RSpec.describe Project, factory_default: :keep do # using delete rather than destroy due to `delete` skipping AR hooks/callbacks # so it's ensured to work at the DB level. Uses AFTER DELETE trigger. let_it_be(:project) { create(:project) } - let_it_be(:project_namespace) { create(:project_namespace, project: project) } + let_it_be(:project_namespace) { project.project_namespace } it 'also deletes the associated ProjectNamespace' do project.delete @@ -233,6 +233,58 @@ RSpec.describe Project, factory_default: :keep do expect(project.project_setting).to be_an_instance_of(ProjectSetting) expect(project.project_setting).to be_new_record end + + context 'with project namespaces' do + it 'automatically creates a project namespace' do + project = build(:project, path: 'hopefully-valid-path1') + project.save! + + expect(project).to be_persisted + expect(project.project_namespace).to be_persisted + expect(project.project_namespace).to be_in_sync_with_project(project) + end + + context 'with FF disabled' do + before do + stub_feature_flags(create_project_namespace_on_project_create: false) + end + + it 'does not create a project namespace' do + project = build(:project, path: 'hopefully-valid-path2') + project.save! + + expect(project).to be_persisted + expect(project.project_namespace).to be_nil + end + end + end + end + + context 'updating a project' do + context 'with project namespaces' do + it 'keeps project namespace in sync with project' do + project = create(:project) + project.update!(path: 'hopefully-valid-path1') + + expect(project).to be_persisted + expect(project.project_namespace).to be_persisted + expect(project.project_namespace).to be_in_sync_with_project(project) + end + + context 'with FF disabled' do + before do + stub_feature_flags(create_project_namespace_on_project_create: false) + end + + it 'does not create a project namespace when project is updated' do + project = create(:project) + project.update!(path: 'hopefully-valid-path1') + + expect(project).to be_persisted + expect(project.project_namespace).to be_nil + end + end + end end context 'updating cd_cd_settings' do @@ -322,6 +374,18 @@ RSpec.describe Project, factory_default: :keep do create(:project) end + context 'validates project namespace creation' do + it 'does not create project namespace if project is not created' do + project = build(:project, path: 'tree') + + project.valid? + + expect(project).not_to be_valid + expect(project).to be_new_record + expect(project.project_namespace).to be_new_record + end + end + context 'repository storages inclusion' do let(:project2) { build(:project, repository_storage: 'missing') } diff --git a/spec/policies/namespaces/project_namespace_policy_spec.rb b/spec/policies/namespaces/project_namespace_policy_spec.rb index 22f3ccec1f8..5bb38deb498 100644 --- a/spec/policies/namespaces/project_namespace_policy_spec.rb +++ b/spec/policies/namespaces/project_namespace_policy_spec.rb @@ -4,7 +4,8 @@ require 'spec_helper' RSpec.describe NamespacePolicy do let_it_be(:parent) { create(:namespace) } - let_it_be(:namespace) { create(:project_namespace, parent: parent) } + let_it_be(:project) { create(:project, namespace: parent) } + let_it_be(:namespace) { project.project_namespace } let(:permissions) do [:owner_access, :create_projects, :admin_namespace, :read_namespace, diff --git a/spec/presenters/packages/npm/package_presenter_spec.rb b/spec/presenters/packages/npm/package_presenter_spec.rb index 65f69d4056b..49046492ab4 100644 --- a/spec/presenters/packages/npm/package_presenter_spec.rb +++ b/spec/presenters/packages/npm/package_presenter_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe ::Packages::Npm::PackagePresenter do + using RSpec::Parameterized::TableSyntax + let_it_be(:project) { create(:project) } let_it_be(:package_name) { "@#{project.root_namespace.path}/test" } let_it_be(:package1) { create(:npm_package, version: '2.0.4', project: project, name: package_name) } @@ -13,42 +15,88 @@ RSpec.describe ::Packages::Npm::PackagePresenter do let(:presenter) { described_class.new(package_name, packages) } describe '#versions' do - subject { presenter.versions } + let_it_be('package_json') do + { + 'name': package_name, + 'version': '2.0.4', + 'deprecated': 'warning!', + 'bin': './cli.js', + 'directories': ['lib'], + 'engines': { 'npm': '^7.5.6' }, + '_hasShrinkwrap': false, + 'dist': { + 'tarball': 'http://localhost/tarball.tgz', + 'shasum': '1234567890' + }, + 'custom_field': 'foo_bar' + } + end - context 'for packages without dependencies' do - it { is_expected.to be_a(Hash) } - it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') } - it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') } + let(:presenter) { described_class.new(package_name, packages, include_metadata: include_metadata) } - ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| - it { expect(subject.dig(package1.version, dependency_type)).to be nil } - it { expect(subject.dig(package2.version, dependency_type)).to be nil } - end + subject { presenter.versions } - it 'avoids N+1 database queries' do - check_n_plus_one(:versions) do - create_list(:npm_package, 5, project: project, name: package_name) + where(:has_dependencies, :has_metadatum, :include_metadata) do + true | true | true + false | true | true + true | false | true + false | false | true + + # TODO : to remove along with packages_npm_abbreviated_metadata + # See https://gitlab.com/gitlab-org/gitlab/-/issues/344827 + true | true | false + false | true | false + true | false | false + false | false | false + end + + with_them do + if params[:has_dependencies] + ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| + let_it_be("package_dependency_link_for_#{dependency_type}") { create(:packages_dependency_link, package: package1, dependency_type: dependency_type) } end end - end - context 'for packages with dependencies' do - ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| - let_it_be("package_dependency_link_for_#{dependency_type}") { create(:packages_dependency_link, package: package1, dependency_type: dependency_type) } + if params[:has_metadatum] + let_it_be('package_metadatadum') { create(:npm_metadatum, package: package1, package_json: package_json) } end it { is_expected.to be_a(Hash) } it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') } it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') } - ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| - it { expect(subject.dig(package1.version, dependency_type.to_s)).to be_any } + it { expect(subject[package1.version]['custom_field']).to be_blank } + + context 'dependencies' do + ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| + if params[:has_dependencies] + it { expect(subject.dig(package1.version, dependency_type.to_s)).to be_any } + else + it { expect(subject.dig(package1.version, dependency_type)).to be nil } + end + + it { expect(subject.dig(package2.version, dependency_type)).to be nil } + end + end + + context 'metadatum' do + ::Packages::Npm::PackagePresenter::PACKAGE_JSON_ALLOWED_FIELDS.each do |metadata_field| + if params[:has_metadatum] && params[:include_metadata] + it { expect(subject.dig(package1.version, metadata_field)).not_to be nil } + else + it { expect(subject.dig(package1.version, metadata_field)).to be nil } + end + + it { expect(subject.dig(package2.version, metadata_field)).to be nil } + end end it 'avoids N+1 database queries' do check_n_plus_one(:versions) do create_list(:npm_package, 5, project: project, name: package_name).each do |npm_package| - ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| - create(:packages_dependency_link, package: npm_package, dependency_type: dependency_type) + if has_dependencies + ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| + create(:packages_dependency_link, package: npm_package, dependency_type: dependency_type) + end end end end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb index 2143abd3031..bea2365eaa6 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb @@ -8,14 +8,14 @@ RSpec.describe 'Setting Draft status of a merge request' do let(:current_user) { create(:user) } let(:merge_request) { create(:merge_request) } let(:project) { merge_request.project } - let(:input) { { wip: true } } + let(:input) { { draft: true } } let(:mutation) do variables = { project_path: project.full_path, iid: merge_request.iid.to_s } - graphql_mutation(:merge_request_set_wip, variables.merge(input), + graphql_mutation(:merge_request_set_draft, variables.merge(input), <<-QL.strip_heredoc clientMutationId errors @@ -28,7 +28,7 @@ RSpec.describe 'Setting Draft status of a merge request' do end def mutation_response - graphql_mutation_response(:merge_request_set_wip) + graphql_mutation_response(:merge_request_set_draft) end before do @@ -58,7 +58,7 @@ RSpec.describe 'Setting Draft status of a merge request' do end context 'when passing Draft false as input' do - let(:input) { { wip: false } } + let(:input) { { draft: false } } it 'does not do anything if the merge reqeust was not marked draft' do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index 43bbc6f3ca8..01dbf523071 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -8,7 +8,7 @@ RSpec.describe API::Namespaces do let_it_be(:group1) { create(:group, name: 'group.one') } let_it_be(:group2) { create(:group, :nested) } let_it_be(:project) { create(:project, namespace: group2, name: group2.name, path: group2.path) } - let_it_be(:project_namespace) { create(:project_namespace, project: project) } + let_it_be(:project_namespace) { project.project_namespace } describe "GET /namespaces" do context "when unauthenticated" do diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index 0d04c2cad5b..7c3f1890095 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -180,6 +180,7 @@ RSpec.describe API::NpmProjectPackages do .to change { project.packages.count }.by(1) .and change { Packages::PackageFile.count }.by(1) .and change { Packages::Tag.count }.by(1) + .and change { Packages::Npm::Metadatum.count }.by(1) expect(response).to have_gitlab_http_status(:ok) end @@ -317,6 +318,25 @@ RSpec.describe API::NpmProjectPackages do end end end + + context 'with a too large metadata structure' do + let(:package_name) { "@#{group.path}/my_package_name" } + let(:params) do + upload_params(package_name: package_name, package_version: '1.2.3').tap do |h| + h['versions']['1.2.3']['test'] = 'test' * 10000 + end + end + + it_behaves_like 'not a package tracking event' + + it 'returns an error' do + expect { upload_package_with_token } + .not_to change { project.packages.count } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.body).to include('Validation failed: Package json structure is too large') + end + end end def upload_package(package_name, params = {}) diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 0c9e125cc90..097d374640c 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -47,7 +47,7 @@ RSpec.describe API::ProjectImport do it 'executes a limited number of queries' do control_count = ActiveRecord::QueryRecorder.new { subject }.count - expect(control_count).to be <= 100 + expect(control_count).to be <= 101 end it 'schedules an import using a namespace' do diff --git a/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb b/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb index 20b0546effa..5f7afdf699a 100644 --- a/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb +++ b/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb @@ -39,7 +39,7 @@ RSpec.describe DependencyProxy::FindOrCreateBlobService do let(:blob_sha) { blob.file_name.sub('.gz', '') } it 'uses cached blob instead of downloading one' do - expect { subject }.to change { blob.reload.updated_at } + expect { subject }.to change { blob.reload.read_at } expect(subject[:status]).to eq(:success) expect(subject[:blob]).to be_a(DependencyProxy::Blob) diff --git a/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb b/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb index 407ee42d345..ef608c9b113 100644 --- a/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb +++ b/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb @@ -84,7 +84,7 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do shared_examples 'using the cached manifest' do it 'uses cached manifest instead of downloading one', :aggregate_failures do - subject + expect { subject }.to change { dependency_proxy_manifest.reload.read_at } expect(subject[:status]).to eq(:success) expect(subject[:manifest]).to be_a(DependencyProxy::Manifest) diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index 21aa213a8ff..6712dccd249 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -185,9 +185,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do context 'when projects have project namespaces' do let_it_be(:project1) { create(:project, :private, namespace: group) } - let_it_be(:project_namespace1) { create(:project_namespace, project: project1) } let_it_be(:project2) { create(:project, :private, namespace: group) } - let_it_be(:project_namespace2) { create(:project_namespace, project: project2) } it_behaves_like 'project namespace path is in sync with project path' do let(:group_full_path) { "#{group.path}" } @@ -250,33 +248,42 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do let_it_be(:membership) { create(:group_member, :owner, group: new_parent_group, user: user) } let_it_be(:project) { create(:project, path: 'foo', namespace: new_parent_group) } - before do - group.update_attribute(:path, 'foo') - end - - it 'returns false' do - expect(transfer_service.execute(new_parent_group)).to be_falsy - end - it 'adds an error on group' do - transfer_service.execute(new_parent_group) - expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken') + expect(transfer_service.execute(new_parent_group)).to be_falsy + expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup or a project with the same path.') end - context 'when projects have project namespaces' do - let!(:project_namespace) { create(:project_namespace, project: project) } - + # currently when a project is created it gets a corresponding project namespace + # so we test the case where a project without a project namespace is transferred + # for backward compatibility + context 'without project namespace' do before do - transfer_service.execute(new_parent_group) + project_namespace = project.project_namespace + project.update_column(:project_namespace_id, nil) + project_namespace.delete end - it_behaves_like 'project namespace path is in sync with project path' do - let(:group_full_path) { "#{new_parent_group.full_path}" } - let(:projects_with_project_namespace) { [project] } + it 'adds an error on group' do + expect(project.reload.project_namespace).to be_nil + expect(transfer_service.execute(new_parent_group)).to be_falsy + expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken') end end end + context 'when projects have project namespaces' do + let_it_be(:project) { create(:project, path: 'foo', namespace: new_parent_group) } + + before do + transfer_service.execute(new_parent_group) + end + + it_behaves_like 'project namespace path is in sync with project path' do + let(:group_full_path) { "#{new_parent_group.full_path}" } + let(:projects_with_project_namespace) { [project] } + end + end + context 'when the group is allowed to be transferred' do let_it_be(:new_parent_group, reload: true) { create(:group, :public) } let_it_be(:new_parent_group_integration) { create(:integrations_slack, group: new_parent_group, project: nil, webhook: 'http://new-group.slack.com') } @@ -445,8 +452,6 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do context 'when transferring a group with project descendants' do let!(:project1) { create(:project, :repository, :private, namespace: group) } let!(:project2) { create(:project, :repository, :internal, namespace: group) } - let!(:project_namespace1) { create(:project_namespace, project: project1) } - let!(:project_namespace2) { create(:project_namespace, project: project2) } before do TestEnv.clean_test_path @@ -483,8 +488,6 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do let!(:project1) { create(:project, :repository, :public, namespace: group) } let!(:project2) { create(:project, :repository, :public, namespace: group) } let!(:new_parent_group) { create(:group, :private) } - let!(:project_namespace1) { create(:project_namespace, project: project1) } - let!(:project_namespace2) { create(:project_namespace, project: project2) } it 'updates projects visibility to match the new parent' do group.projects.each do |project| @@ -504,8 +507,6 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do let!(:project2) { create(:project, :repository, :internal, namespace: group) } let!(:subgroup1) { create(:group, :private, parent: group) } let!(:subgroup2) { create(:group, :internal, parent: group) } - let!(:project_namespace1) { create(:project_namespace, project: project1) } - let!(:project_namespace2) { create(:project_namespace, project: project2) } before do TestEnv.clean_test_path diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb index ba5729eaf59..9598355a640 100644 --- a/spec/services/packages/npm/create_package_service_spec.rb +++ b/spec/services/packages/npm/create_package_service_spec.rb @@ -16,6 +16,7 @@ RSpec.describe Packages::Npm::CreatePackageService do let(:override) { {} } let(:package_name) { "@#{namespace.path}/my-app" } + let(:version_data) { params.dig('versions', '1.0.1') } subject { described_class.new(project, user, params).execute } @@ -25,6 +26,7 @@ RSpec.describe Packages::Npm::CreatePackageService do .to change { Packages::Package.count }.by(1) .and change { Packages::Package.npm.count }.by(1) .and change { Packages::Tag.count }.by(1) + .and change { Packages::Npm::Metadatum.count }.by(1) end it_behaves_like 'assigns the package creator' do @@ -40,6 +42,8 @@ RSpec.describe Packages::Npm::CreatePackageService do expect(package.version).to eq(version) end + it { expect(subject.npm_metadatum.package_json).to eq(version_data) } + it { expect(subject.name).to eq(package_name) } it { expect(subject.version).to eq(version) } @@ -54,6 +58,31 @@ RSpec.describe Packages::Npm::CreatePackageService do expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1) end end + + context 'with a too large metadata structure' do + before do + params[:versions][version][:test] = 'test' * 10000 + end + + it 'does not create the package' do + expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Package json structure is too large') + .and not_change { Packages::Package.count } + .and not_change { Packages::Package.npm.count } + .and not_change { Packages::Tag.count } + .and not_change { Packages::Npm::Metadatum.count } + end + end + + context 'with packages_npm_abbreviated_metadata disabled' do + before do + stub_feature_flags(packages_npm_abbreviated_metadata: false) + end + + it 'creates a package without metadatum' do + expect { subject } + .not_to change { Packages::Npm::Metadatum.count } + end + end end describe '#execute' do diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index bce65d19aae..d903523840c 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -49,6 +49,7 @@ RSpec.describe Projects::CreateService, '#execute' do it 'keeps them as specified' do expect(project.name).to eq('one') expect(project.path).to eq('two') + expect(project.project_namespace).to be_in_sync_with_project(project) end end @@ -58,6 +59,7 @@ RSpec.describe Projects::CreateService, '#execute' do it 'sets name == path' do expect(project.path).to eq('one.two_three-four') expect(project.name).to eq(project.path) + expect(project.project_namespace).to be_in_sync_with_project(project) end end @@ -67,6 +69,7 @@ RSpec.describe Projects::CreateService, '#execute' do it 'sets path == name' do expect(project.name).to eq('one.two_three-four') expect(project.path).to eq(project.name) + expect(project.project_namespace).to be_in_sync_with_project(project) end end @@ -78,6 +81,7 @@ RSpec.describe Projects::CreateService, '#execute' do it 'parameterizes the name' do expect(project.name).to eq('one.two_three-four and five') expect(project.path).to eq('one-two_three-four-and-five') + expect(project.project_namespace).to be_in_sync_with_project(project) end end end @@ -111,13 +115,14 @@ RSpec.describe Projects::CreateService, '#execute' do end context 'user namespace' do - it do + it 'creates a project in user namespace' do project = create_project(user, opts) expect(project).to be_valid expect(project.owner).to eq(user) expect(project.team.maintainers).to include(user) expect(project.namespace).to eq(user.namespace) + expect(project.project_namespace).to be_in_sync_with_project(project) end end @@ -151,6 +156,7 @@ RSpec.describe Projects::CreateService, '#execute' do expect(project.owner).to eq(user) expect(project.team.maintainers).to contain_exactly(user) expect(project.namespace).to eq(user.namespace) + expect(project.project_namespace).to be_in_sync_with_project(project) end end @@ -160,6 +166,7 @@ RSpec.describe Projects::CreateService, '#execute' do project = create_project(admin, opts) expect(project).not_to be_persisted + expect(project.project_namespace).to be_in_sync_with_project(project) end end end @@ -183,6 +190,7 @@ RSpec.describe Projects::CreateService, '#execute' do expect(project.namespace).to eq(group) expect(project.team.owners).to include(user) expect(user.authorized_projects).to include(project) + expect(project.project_namespace).to be_in_sync_with_project(project) end end @@ -339,6 +347,7 @@ RSpec.describe Projects::CreateService, '#execute' do end imported_project + expect(imported_project.project_namespace).to be_in_sync_with_project(imported_project) end it 'stores import data and URL' do @@ -406,6 +415,7 @@ RSpec.describe Projects::CreateService, '#execute' do expect(project.visibility_level).to eq(project_level) expect(project).to be_saved expect(project).to be_valid + expect(project.project_namespace).to be_in_sync_with_project(project) end end end @@ -424,6 +434,7 @@ RSpec.describe Projects::CreateService, '#execute' do expect(project.errors.messages[:visibility_level].first).to( match('restricted by your GitLab administrator') ) + expect(project.project_namespace).to be_in_sync_with_project(project) end it 'does not allow a restricted visibility level for admins when admin mode is disabled' do @@ -493,6 +504,7 @@ RSpec.describe Projects::CreateService, '#execute' do expect(project).to be_valid expect(project.owner).to eq(user) expect(project.namespace).to eq(user.namespace) + expect(project.project_namespace).to be_in_sync_with_project(project) end context 'when another repository already exists on disk' do @@ -522,6 +534,7 @@ RSpec.describe Projects::CreateService, '#execute' do expect(project).to respond_to(:errors) expect(project.errors.messages).to have_key(:base) expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk') + expect(project.project_namespace).to be_in_sync_with_project(project) end it 'does not allow to import project when path matches existing repository on disk' do @@ -531,6 +544,7 @@ RSpec.describe Projects::CreateService, '#execute' do expect(project).to respond_to(:errors) expect(project.errors.messages).to have_key(:base) expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk') + expect(project.project_namespace).to be_in_sync_with_project(project) end end @@ -555,6 +569,7 @@ RSpec.describe Projects::CreateService, '#execute' do expect(project).to respond_to(:errors) expect(project.errors.messages).to have_key(:base) expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk') + expect(project.project_namespace).to be_in_sync_with_project(project) end end end @@ -870,6 +885,7 @@ RSpec.describe Projects::CreateService, '#execute' do expect(project).to be_valid expect(project.shared_runners_enabled).to eq(expected_result_for_project) + expect(project.project_namespace).to be_in_sync_with_project(project) end end end @@ -890,6 +906,7 @@ RSpec.describe Projects::CreateService, '#execute' do expect(project).to be_valid expect(project.shared_runners_enabled).to eq(expected_result_for_project) + expect(project.project_namespace).to be_in_sync_with_project(project) end end end @@ -907,6 +924,7 @@ RSpec.describe Projects::CreateService, '#execute' do expect(project.persisted?).to eq(false) expect(project).to be_invalid expect(project.errors[:shared_runners_enabled]).to include('cannot be enabled because parent group does not allow it') + expect(project.project_namespace).to be_in_sync_with_project(project) end end end @@ -926,6 +944,7 @@ RSpec.describe Projects::CreateService, '#execute' do expect(project).to be_valid expect(project.shared_runners_enabled).to eq(expected_result) + expect(project.project_namespace).to be_in_sync_with_project(project) end end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index b539b01066e..2164580a158 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -66,8 +66,6 @@ RSpec.describe Projects::TransferService do end context 'when project has an associated project namespace' do - let!(:project_namespace) { create(:project_namespace, project: project) } - it 'keeps project namespace in sync with project' do transfer_result = execute_transfer @@ -272,8 +270,6 @@ RSpec.describe Projects::TransferService do end context 'when project has an associated project namespace' do - let!(:project_namespace) { create(:project_namespace, project: project) } - it 'keeps project namespace in sync with project' do attempt_project_transfer @@ -294,8 +290,6 @@ RSpec.describe Projects::TransferService do end context 'when project has an associated project namespace' do - let!(:project_namespace) { create(:project_namespace, project: project) } - it 'keeps project namespace in sync with project' do transfer_result = execute_transfer diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6a0d9ea0669..47aea2c1c37 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -197,6 +197,14 @@ RSpec.configure do |config| if ENV['CI'] || ENV['RETRIES'] # This includes the first try, i.e. tests will be run 4 times before failing. config.default_retry_count = ENV.fetch('RETRIES', 3).to_i + 1 + + # Do not retry controller tests because rspec-retry cannot properly + # reset the controller which may contain data from last attempt. See + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73360 + config.around(:each, type: :controller) do |example| + example.run_with_retry(retry: 1) + end + config.exceptions_to_hard_fail = [DeprecationToolkitEnv::DeprecationBehaviors::SelectiveRaise::RaiseDisallowedDeprecation] end diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb index 813c6176317..81e669aab57 100644 --- a/spec/support/helpers/gpg_helpers.rb +++ b/spec/support/helpers/gpg_helpers.rb @@ -4,6 +4,7 @@ module GpgHelpers SIGNED_COMMIT_SHA = '8a852d50dda17cc8fd1408d2fd0c5b0f24c76ca4' SIGNED_AND_AUTHORED_SHA = '3c1d9a0266cb0c62d926f4a6c649beed561846f5' DIFFERING_EMAIL_SHA = 'a17a9f66543673edf0a3d1c6b93bdda3fe600f32' + MULTIPLE_SIGNATURES_SHA = 'c7794c14268d67ad8a2d5f066d706539afc75a96' module User1 extend self diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 3b89820be07..ae5687c1e2d 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -9,7 +9,7 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { - 'signed-commits' => '6101e87', + 'signed-commits' => 'c7794c1', 'not-merged-branch' => 'b83d6e3', 'branch-merged' => '498214d', 'empty-branch' => '7efb185', diff --git a/spec/support/matchers/project_namespace_matcher.rb b/spec/support/matchers/project_namespace_matcher.rb new file mode 100644 index 00000000000..95aa5429679 --- /dev/null +++ b/spec/support/matchers/project_namespace_matcher.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +RSpec::Matchers.define :be_in_sync_with_project do |project| + match do |project_namespace| + # if project is not persisted make sure we do not have a persisted project_namespace for it + break false if project.new_record? && project_namespace&.persisted? + # don't really care if project is not in sync if the project was never persisted. + break true if project.new_record? && !project_namespace.present? + + project_namespace.present? && + project.name == project_namespace.name && + project.path == project_namespace.path && + project.namespace == project_namespace.parent && + project.visibility_level == project_namespace.visibility_level && + project.shared_runners_enabled == project_namespace.shared_runners_enabled + end + + failure_message_when_negated do |project_namespace| + if project.new_record? && project_namespace&.persisted? + "expected that a non persisted project #{project} does not have a persisted project namespace #{project_namespace}" + else + <<-MSG + expected that the project's attributes name, path, namespace_id, visibility_level, shared_runners_enabled + are in sync with the corresponding project namespace attributes + MSG + end + end +end diff --git a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb index a4e0d6c871e..2d08de297a3 100644 --- a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb @@ -11,18 +11,18 @@ RSpec.shared_examples 'ttl_expirable' do it { is_expected.to validate_presence_of(:status) } end - describe '.updated_before' do + describe '.read_before' do # rubocop:disable Rails/SaveBang let_it_be_with_reload(:item1) { create(class_symbol) } let_it_be(:item2) { create(class_symbol) } # rubocop:enable Rails/SaveBang before do - item1.update_column(:updated_at, 1.month.ago) + item1.update_column(:read_at, 1.month.ago) end it 'returns items with created at older than the supplied number of days' do - expect(described_class.updated_before(10)).to contain_exactly(item1) + expect(described_class.read_before(10)).to contain_exactly(item1) end end @@ -48,4 +48,13 @@ RSpec.shared_examples 'ttl_expirable' do expect(described_class.lock_next_by(:created_at)).to contain_exactly(item3) end end + + describe '#read', :freeze_time do + let_it_be(:old_read_at) { 1.day.ago } + let_it_be(:item1) { create(class_symbol, read_at: old_read_at) } + + it 'updates read_at' do + expect { item1.read! }.to change { item1.reload.read_at } + end + end end diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb index 0e60476f10a..ac6a843663f 100644 --- a/spec/support/shared_examples/namespaces/traversal_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_examples.rb @@ -23,7 +23,7 @@ RSpec.shared_examples 'namespace traversal' do let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } let_it_be(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] } let_it_be(:project) { create(:project, group: nested_group) } - let_it_be(:project_namespace) { create(:project_namespace, project: project) } + let_it_be(:project_namespace) { project.project_namespace } describe '#root_ancestor' do it 'returns the correct root ancestor' do diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb index 2af7b616659..19677e92001 100644 --- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb @@ -8,6 +8,8 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) } let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) } + let_it_be(:package_metadatum) { create(:npm_metadatum, package: package) } + let(:headers) { {} } subject { get(url, headers: headers) } @@ -39,6 +41,19 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| # query count can slightly change between the examples so we're using a custom threshold expect { get(url, headers: headers) }.not_to exceed_query_limit(control).with_threshold(4) end + + context 'with packages_npm_abbreviated_metadata disabled' do + before do + stub_feature_flags(packages_npm_abbreviated_metadata: false) + end + + it 'calls the presenter without including metadata' do + expect(::Packages::Npm::PackagePresenter) + .to receive(:new).with(anything, anything, include_metadata: false).and_call_original + + subject + end + end end shared_examples 'reject metadata request' do |status:| diff --git a/spec/views/jira_connect/subscriptions/index.html.haml_spec.rb b/spec/views/jira_connect/subscriptions/index.html.haml_spec.rb index dcc36c93327..0a4d283a983 100644 --- a/spec/views/jira_connect/subscriptions/index.html.haml_spec.rb +++ b/spec/views/jira_connect/subscriptions/index.html.haml_spec.rb @@ -7,7 +7,7 @@ RSpec.describe 'jira_connect/subscriptions/index.html.haml' do before do allow(view).to receive(:current_user).and_return(user) - assign(:subscriptions, []) + assign(:subscriptions, create_list(:jira_connect_subscription, 1)) end context 'when the user is signed in' do diff --git a/spec/views/layouts/_published_experiments.html.haml_spec.rb b/spec/views/layouts/_published_experiments.html.haml_spec.rb new file mode 100644 index 00000000000..d1ade8ddd6e --- /dev/null +++ b/spec/views/layouts/_published_experiments.html.haml_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'layouts/_published_experiments', :experiment do + before do + stub_const('TestControlExperiment', ApplicationExperiment) + stub_const('TestCandidateExperiment', ApplicationExperiment) + stub_const('TestExcludedExperiment', ApplicationExperiment) + + TestControlExperiment.new('test_control').tap do |e| + e.variant(:control) + e.publish + end + TestCandidateExperiment.new('test_candidate').tap do |e| + e.variant(:candidate) + e.publish + end + TestExcludedExperiment.new('test_excluded').tap do |e| + e.exclude! + e.publish + end + + render + end + + it 'renders out data for all non-excluded, published experiments' do + output = rendered + + expect(output).to include('gl.experiments = {') + expect(output).to match(/"test_control":\{[^}]*"variant":"control"/) + expect(output).to match(/"test_candidate":\{[^}]*"variant":"candidate"/) + expect(output).not_to include('"test_excluded"') + end +end diff --git a/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb b/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb index d3234f4c212..ae0cb097ebf 100644 --- a/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb +++ b/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb @@ -12,8 +12,8 @@ RSpec.describe DependencyProxy::ImageTtlGroupPolicyWorker do subject { worker.perform } context 'when there are images to expire' do - let_it_be_with_reload(:old_blob) { create(:dependency_proxy_blob, group: group, updated_at: 1.year.ago) } - let_it_be_with_reload(:old_manifest) { create(:dependency_proxy_manifest, group: group, updated_at: 1.year.ago) } + let_it_be_with_reload(:old_blob) { create(:dependency_proxy_blob, group: group, read_at: 1.year.ago) } + let_it_be_with_reload(:old_manifest) { create(:dependency_proxy_manifest, group: group, read_at: 1.year.ago) } let_it_be_with_reload(:new_blob) { create(:dependency_proxy_blob, group: group) } let_it_be_with_reload(:new_manifest) { create(:dependency_proxy_manifest, group: group) } |