Commit ad6848e7 authored by Alessio Caiazza's avatar Alessio Caiazza

Merge remote-tracking branch 'security/fix-master-merge-train'

parents f49eebc5 baa8dfaf
e4ff30e44b6ac21f33290bbe7a9cbbd42f98d4d1
e9860f7988a2c87638abf695d8613e3096312857
<script>
import { GlEmptyState, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
export default {
components: {
GlEmptyState,
GlSprintf,
GlLink,
GlButton,
},
inject: {
isAdmin: {
type: Boolean,
},
svgPath: {
type: String,
},
docsLink: {
type: String,
},
primaryButtonPath: {
type: String,
},
},
};
</script>
<template>
<gl-empty-state class="js-empty-state" :title="__('Usage ping is off')" :svg-path="svgPath">
<template #description>
<gl-sprintf
v-if="!isAdmin"
:message="
__(
'To view instance-level analytics, ask an admin to turn on %{docLinkStart}usage ping%{docLinkEnd}.',
)
"
>
<template #docLink="{content}">
<gl-link :href="docsLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
<template v-else
><p>
{{ __('Turn on usage ping to review instance-level analytics.') }}
</p>
<gl-button category="primary" variant="success" :href="primaryButtonPath">
{{ __('Turn on usage ping') }}</gl-button
>
</template>
</template>
</gl-empty-state>
</template>
......@@ -468,7 +468,7 @@ export default {
<div
:data-can-create-note="getNoteableData.current_user.can_create_note"
class="files d-flex"
class="files d-flex gl-mt-2"
>
<div
v-if="showTreeList"
......
......@@ -34,7 +34,6 @@ export const COUNT_OF_AVATARS_IN_GUTTER = 3;
export const LENGTH_OF_AVATAR_TOOLTIP = 17;
export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
export const MAX_LINES_TO_BE_RENDERED = 2000;
export const DIFF_FILE_SYMLINK_MODE = '120000';
export const DIFF_FILE_DELETED_MODE = '0';
......
......@@ -11,7 +11,6 @@ import {
OLD_LINE_TYPE,
MATCH_LINE_TYPE,
LINES_TO_BE_RENDERED_DIRECTLY,
MAX_LINES_TO_BE_RENDERED,
TREE_TYPE,
INLINE_DIFF_VIEW_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
......@@ -457,12 +456,10 @@ function getVisibleDiffLines(file) {
}
function finalizeDiffFile(file) {
const name = (file.viewer && file.viewer.name) || diffViewerModes.text;
const lines = getVisibleDiffLines(file);
Object.assign(file, {
renderIt: lines < LINES_TO_BE_RENDERED_DIRECTLY,
collapsed: name === diffViewerModes.text && lines > MAX_LINES_TO_BE_RENDERED,
isShowingFullFile: false,
isLoadingFullFile: false,
discussions: [],
......
import Vue from 'vue';
import UserCallout from '~/user_callout';
import UsagePingDisabled from '~/admin/dev_ops_score/components/usage_ping_disabled.vue';
document.addEventListener('DOMContentLoaded', () => new UserCallout());
document.addEventListener('DOMContentLoaded', () => {
// eslint-disable-next-line no-new
new UserCallout();
const emptyStateContainer = document.getElementById('js-devops-empty-state');
if (!emptyStateContainer) return false;
const { emptyStateSvgPath, enableUsagePingLink, docsLink, isAdmin } = emptyStateContainer.dataset;
return new Vue({
el: emptyStateContainer,
provide: {
isAdmin: Boolean(isAdmin),
svgPath: emptyStateSvgPath,
primaryButtonPath: enableUsagePingLink,
docsLink,
},
render(h) {
return h(UsagePingDisabled);
},
});
});
......@@ -1062,7 +1062,7 @@ table.code {
.diff-tree-list {
position: -webkit-sticky;
position: sticky;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 11px;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 15px;
top: $top-pos;
max-height: calc(100vh - #{$top-pos});
z-index: 202;
......
......@@ -445,7 +445,7 @@ class Issue < ApplicationRecord
super
rescue ActiveRecord::QueryCanceled => e
# Symptom of running out of space - schedule rebalancing
IssueRebalancingWorker.perform_async(id)
IssueRebalancingWorker.perform_async(nil, project_id)
raise e
end
......@@ -453,7 +453,7 @@ class Issue < ApplicationRecord
super
rescue ActiveRecord::QueryCanceled => e
# Symptom of running out of space - schedule rebalancing
IssueRebalancingWorker.perform_async(id)
IssueRebalancingWorker.perform_async(nil, project_id)
raise e
end
end
......
......@@ -29,7 +29,7 @@ module Issues
gates = [issue.project, issue.project.group].compact
return unless gates.any? { |gate| Feature.enabled?(:rebalance_issues, gate) }
IssueRebalancingWorker.perform_async(issue.id)
IssueRebalancingWorker.perform_async(nil, issue.project_id)
end
def create_assignee_note(issue, old_assignees)
......
.container.devops-empty
.col-sm-12.justify-content-center.text-center
= custom_icon('dev_ops_score_no_index')
%h4= _('Usage ping is not enabled')
- if !current_user.admin?
%p
- usage_ping_path = help_page_path('development/telemetry/usage_ping')
- usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
= s_('In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
- if current_user.admin?
%p
= _('Enable usage ping to get an overview of how you are using GitLab from a feature perspective.')
- if current_user.admin?
= link_to _('Enable usage ping'), metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'btn btn-primary'
......@@ -7,7 +7,7 @@
.gl-mt-3
- if !usage_ping_enabled
= render 'disabled'
#js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/telemetry/usage_ping') } }
- elsif @metric.blank?
= render 'no_data'
- else
......
......@@ -7,11 +7,14 @@ class IssueRebalancingWorker
urgency :low
feature_category :issue_tracking
def perform(issue_id)
issue = Issue.find(issue_id)
def perform(ignore = nil, project_id = nil)
return if project_id.nil?
project = Project.find(project_id)
issue = project.issues.first # All issues are equivalent as far as we are concerned
IssueRebalancingService.new(issue).execute
rescue ActiveRecord::RecordNotFound, IssueRebalancingService::TooManyIssues => e
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id)
Gitlab::ErrorTracking.log_exception(e, project_id: project_id)
end
end
---
title: Add index on merge_request_id to approval_merge_request_rules
merge_request: 40556
author:
type: other
---
title: Fix file file input top position cutoff
merge_request: 40634
author:
type: fixed
---
title: Migrate DevOps Score empty state into Vue component
merge_request: 40595
author:
type: changed
---
title: Fix auto-deploy-image external chart dependencies
merge_request: 40730
author:
type: fixed
---
title: Fix client usage of max line rendering
merge_request: 40741
author:
type: fixed
# frozen_string_literal: true
class AddIndexOnMergeRequestIdAndRuleTypeToApprovalMergeRequestRule < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME = "approval_mr_rule_index_merge_request_id"
def up
add_concurrent_index(
:approval_merge_request_rules,
:merge_request_id,
name: INDEX_NAME
)
end
def down
remove_concurrent_index_by_name :approval_merge_request_rules, INDEX_NAME
end
end
360c42f4d34c3b03e7a0375a0ff2776f066888f0a40131180bf301b876ea58db
\ No newline at end of file
......@@ -18945,6 +18945,8 @@ CREATE UNIQUE INDEX any_approver_merge_request_rule_type_unique_index ON public.
CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON public.approval_project_rules USING btree (project_id) WHERE (rule_type = 3);
CREATE INDEX approval_mr_rule_index_merge_request_id ON public.approval_merge_request_rules USING btree (merge_request_id);
CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE ((code_owner = true) AND (section IS NULL));
CREATE UNIQUE INDEX backup_labels_group_id_project_id_title_idx ON public.backup_labels USING btree (group_id, project_id, title);
......
......@@ -106,7 +106,7 @@ end
Using `any_instance` to stub a method (elasticsearch_indexing) that has been defined on a prepended module (EE::ApplicationSetting) is not supported.
```
### Alternative: `expect_next_instance_of` or `allow_next_instance_of`
### Alternative: `expect_next_instance_of`, `allow_next_instance_of`, `expect_next_found_instance_of` or `allow_next_found_instance_of`
Instead of writing:
......@@ -130,8 +130,21 @@ end
allow_next_instance_of(Project) do |project|
allow(project).to receive(:add_import_job)
end
# Do this:
expect_next_found_instance_of(Project) do |project|
expect(project).to receive(:add_import_job)
end
# Do this:
allow_next_found_instance_of(Project) do |project|
allow(project).to receive(:add_import_job)
end
```
_**Note:** Since Active Record is not calling the `.new` method on model classes to instantiate the objects,
you should use `expect_next_found_instance_of` or `allow_next_found_instance_of` mock helpers to setup mock on objects returned by Active Record query & finder methods._
If we also want to initialize the instance with some particular arguments, we
could also pass it like:
......
......@@ -203,15 +203,18 @@ export default {
/>
</li>
<li
v-if="item.labels.nodes.length"
class="gl-mr-3"
class="gl-mr-3 gl-display-flex gl-align-items-center"
:class="{ 'gl-opacity-5': !item.labels.nodes.length }"
:data-testid="$options.testIds.LABEL_DETAILS"
>
<span class="gl-display-flex gl-align-items-center"
><gl-icon name="label" class="gl-mr-1" /><span>{{
item.labels.nodes.length
}}</span></span
>
<gl-icon name="label" class="gl-mr-1" /><span>{{ item.labels.nodes.length }}</span>
</li>
<li
class="gl-mr-3 gl-display-flex gl-align-items-center"
:class="{ 'gl-opacity-5': !item.userNotesCount }"
:data-testid="$options.testIds.COMMENT_COUNT"
>
<gl-icon name="comments" class="gl-mr-2" /><span>{{ item.userNotesCount }}</span>
</li>
</ul>
</div>
......
......@@ -41,6 +41,7 @@ export const THROUGHPUT_TABLE_TEST_IDS = {
LINE_CHANGES: 'lineChangesCol',
ASSIGNEES: 'assigneesCol',
COMMITS: 'commitsCol',
COMMENT_COUNT: 'commentCount',
};
export const PIPELINE_STATUS_ICON_CLASSES = {
......
......@@ -34,6 +34,7 @@ query($fullPath: ID!, $startDate: Time!, $endDate: Time!, $limit: Int!) {
}
}
commitCount
userNotesCount
}
}
}
......
......@@ -13,6 +13,8 @@ function createMainApp() {
createIssueUrl: vulnerability.create_issue_url,
projectFingerprint: vulnerability.project_fingerprint,
vulnerabilityId: vulnerability.id,
issueTrackingHelpPath: vulnerability.issueTrackingHelpPath,
permissionsHelpPath: vulnerability.permissionsHelpPath,
},
render: h =>
......
......@@ -14,10 +14,3 @@ export const ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS = [
STEPS.yourGroup,
STEPS.yourProject,
];
export const ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS = [
STEPS.yourProfile,
STEPS.checkout,
STEPS.yourGroup,
STEPS.yourProject,
];
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import {
STEPS,
ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS,
ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS,
} from '../../constants';
import { STEPS, ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS } from '../../constants';
import ProgressBar from '../../components/progress_bar.vue';
export default () => {
......@@ -12,17 +7,11 @@ export default () => {
if (!el) return null;
const isInSubscriptionFlow = parseBoolean(el.dataset.isInSubscriptionFlow);
const steps = isInSubscriptionFlow
? ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS
: ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS;
return new Vue({
el,
render(createElement) {
return createElement(ProgressBar, {
props: { steps, currentStep: STEPS.yourProject },
props: { steps: ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS, currentStep: STEPS.yourProject },
});
},
});
......
......@@ -4,7 +4,6 @@ import {
STEPS,
SUBSCRIPTON_FLOW_STEPS,
ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS,
ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS,
} from '../constants';
import ProgressBar from '../components/progress_bar.vue';
......@@ -20,9 +19,7 @@ export default () => {
let steps;
if (isInSubscriptionFlow && isOnboardingIssuesExperimentEnabled) {
steps = ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS;
} else if (isInSubscriptionFlow) {
if (isInSubscriptionFlow) {
steps = SUBSCRIPTON_FLOW_STEPS;
} else if (isOnboardingIssuesExperimentEnabled) {
steps = ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS;
......
import Vue from 'vue';
import {
STEPS,
SUBSCRIPTON_FLOW_STEPS,
ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS,
} from 'ee/registrations/constants';
import { STEPS, SUBSCRIPTON_FLOW_STEPS } from 'ee/registrations/constants';
import ProgressBar from 'ee/registrations/components/progress_bar.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
export default () => {
const el = document.getElementById('progress-bar');
if (!el) return null;
const isOnboardingIssuesExperimentEnabled = parseBoolean(
el.dataset.isOnboardingIssuesExperimentEnabled,
);
const steps = isOnboardingIssuesExperimentEnabled
? ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS
: SUBSCRIPTON_FLOW_STEPS;
return new Vue({
el,
render(createElement) {
return createElement(ProgressBar, {
props: { steps, currentStep: STEPS.yourGroup },
props: { steps: SUBSCRIPTON_FLOW_STEPS, currentStep: STEPS.yourGroup },
});
},
});
......
<script>
import {
STEPS,
SUBSCRIPTON_FLOW_STEPS,
ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS,
} from 'ee/registrations/constants';
import { STEPS, SUBSCRIPTON_FLOW_STEPS } from 'ee/registrations/constants';
import { mapState } from 'vuex';
import ProgressBar from 'ee/registrations/components/progress_bar.vue';
import { s__ } from '~/locale';
......@@ -15,13 +11,9 @@ import ConfirmOrder from './checkout/confirm_order.vue';
export default {
components: { ProgressBar, SubscriptionDetails, BillingAddress, PaymentMethod, ConfirmOrder },
currentStep: STEPS.checkout,
steps: SUBSCRIPTON_FLOW_STEPS,
computed: {
...mapState(['isOnboardingIssuesExperimentEnabled', 'isNewUser']),
steps() {
return this.isOnboardingIssuesExperimentEnabled
? ONBOARDING_ISSUES_EXPERIMENT_AND_SUBSCRIPTION_FLOW_STEPS
: SUBSCRIPTON_FLOW_STEPS;
},
...mapState(['isNewUser']),
},
i18n: {
checkout: s__('Checkout|Checkout'),
......@@ -31,7 +23,7 @@ export default {
<template>
<div class="checkout d-flex flex-column justify-content-between w-100">
<div class="full-width">
<progress-bar v-if="isNewUser" :steps="steps" :current-step="$options.currentStep" />
<progress-bar v-if="isNewUser" :steps="$options.steps" :current-step="$options.currentStep" />
<div class="flash-container"></div>
<h2 class="mt-4 mb-3 mb-lg-5">{{ $options.i18n.checkout }}</h2>
<subscription-details />
......
......@@ -44,7 +44,6 @@ export default ({
setupForCompany,
fullName,
newUser,
onboardingIssuesExperimentEnabled,
groupData = '[]',
}) => {
const availablePlans = parsePlanData(planData);
......@@ -58,7 +57,6 @@ export default ({
availablePlans,
selectedPlan: determineSelectedPlan(planId, availablePlans),
isNewUser,
isOnboardingIssuesExperimentEnabled: parseBoolean(onboardingIssuesExperimentEnabled),
fullName,
groupData: groups,
selectedGroup: groupId,
......
......@@ -11,9 +11,19 @@ export default {
},
computed: {
message() {
return sprintf(s__('mrWidget|In the merge train at position %{mergeTrainPosition}'), {
mergeTrainPosition: this.mergeTrainIndex + 1,
});
const messageBeginningTrainPosition = s__(
'mrWidget|A new merge train has started and this merge request is the first of the queue.',
);
const messageAddedTrainPosition = sprintf(
s__(
'mrWidget|Added to the merge train. There are %{mergeTrainPosition} merge requests waiting to be merged',
),
{
mergeTrainPosition: this.mergeTrainIndex + 1,
},
);
return this.mergeTrainIndex === 0 ? messageBeginningTrainPosition : messageAddedTrainPosition;
},
},
};
......
<script>
import { sanitize } from 'dompurify';
import { GlFormTextarea, GlButton, GlLoadingIcon } from '@gitlab/ui';
import { GlFormTextarea, GlButton } from '@gitlab/ui';
export default {
components: { GlFormTextarea, GlButton, GlLoadingIcon },
components: { GlFormTextarea, GlButton },
props: {
initialComment: {
type: String,
......@@ -44,9 +44,9 @@ export default {
ref="saveButton"
variant="success"
:disabled="isSaveButtonDisabled"
:loading="isSaving"
@click="$emit('onSave', sanitizedComment)"
>
<gl-loading-icon v-if="isSaving" class="mr-1" />
{{ __('Save comment') }}
</gl-button>
<gl-button ref="cancelButton" class="ml-1" :disabled="isSaving" @click="$emit('onCancel')">
......
<script>
import axios from 'axios';
import { GlButton } from '@gitlab/ui';
import { GlButton, GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import RelatedIssuesStore from '~/related_issues/stores/related_issues_store';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import { issuableTypesMap, PathIdSeparator } from '~/related_issues/constants';
......@@ -15,6 +15,9 @@ export default {
components: {
RelatedIssuesBlock,
GlButton,
GlAlert,
GlSprintf,
GlLink,
},
props: {
endpoint: {
......@@ -45,6 +48,7 @@ export default {
isFetching: false,
isSubmitting: false,
isFormVisible: false,
errorCreatingIssue: false,
inputValue: '',
};
},
......@@ -69,6 +73,12 @@ export default {
reportType: {
type: String,
},
issueTrackingHelpPath: {
type: String,
},
permissionsHelpPath: {
type: String,
},
},
created() {
this.fetchRelatedIssues();
......@@ -76,6 +86,7 @@ export default {
methods: {
createIssue() {
this.isProcessingAction = true;
this.errorCreatingIssue = false;
return axios
.post(this.createIssueUrl, {
......@@ -95,9 +106,7 @@ export default {
})
.catch(() => {
this.isProcessingAction = false;
createFlash(
s__('VulnerabilityManagement|Something went wrong, could not create an issue.'),
);
this.errorCreatingIssue = true;
});
},
toggleFormVisibility() {
......@@ -204,44 +213,76 @@ export default {
autoCompleteSources: gl?.GfmAutoComplete?.dataSources,
issuableType: issuableTypesMap.ISSUE,
pathIdSeparator: PathIdSeparator.Issue,
i18n: {
relatedIssues: __('Related issues'),
createIssue: __('Create issue'),
createIssueErrorTitle: __('Could not create issue'),
createIssueErrorBody: s__(
'SecurityReports|Ensure that %{trackingStart}issue tracking%{trackingEnd} is enabled for this project and you have %{permissionsStart}permission to create new issues%{permissionsEnd}.',
),
},
};
</script>
<template>
<related-issues-block
:help-path="helpPath"
:is-fetching="isFetching"
:is-submitting="isSubmitting"
:related-issues="state.relatedIssues"
:can-admin="canModifyRelatedIssues"
:pending-references="state.pendingReferences"
:is-form-visible="isFormVisible"
:input-value="inputValue"
:auto-complete-sources="$options.autoCompleteSources"
:issuable-type="$options.issuableType"
:path-id-separator="$options.pathIdSeparator"
:show-categorized-issues="false"
@toggleAddRelatedIssuesForm="toggleFormVisibility"
@addIssuableFormInput="addPendingReferences"
@addIssuableFormBlur="processAllReferences"
@addIssuableFormSubmit="addRelatedIssue"
@addIssuableFormCancel="resetForm"
@pendingIssuableRemoveRequest="removePendingReference"
@relatedIssueRemoveRequest="removeRelatedIssue"
>
<template #headerText>
{{ __('Related issues') }}
</template>
<template v-if="!isIssueAlreadyCreated && !isFetching" #headerActions>
<gl-button
ref="createIssue"
variant="success"
category="secondary"
:loading="isProcessingAction"
@click="createIssue"
>
{{ __('Create issue') }}
</gl-button>
</template>
</related-issues-block>
<div>
<gl-alert
v-if="errorCreatingIssue"
variant="danger"
class="gl-mt-5"
@dismiss="errorCreatingIssue = false"
>
<p class="gl-font-weight-bold gl-mb-2">{{ $options.i18n.createIssueErrorTitle }}</p>
<p class="gl-mb-0">
<gl-sprintf :message="$options.i18n.createIssueErrorBody">
<template #tracking="{ content }">
<gl-link class="gl-display-inline-block" :href="issueTrackingHelpPath" target="_blank">
{{ content }}
</gl-link>
</template>
<template #permissions="{ content }">
<gl-link class="gl-display-inline-block" :href="permissionsHelpPath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
</gl-alert>
<related-issues-block
:help-path="helpPath"
:is-fetching="isFetching"
:is-submitting="isSubmitting"
:related-issues="state.relatedIssues"
:can-admin="canModifyRelatedIssues"
:pending-references="state.pendingReferences"
:is-form-visible="isFormVisible"
:input-value="inputValue"
:auto-complete-sources="$options.autoCompleteSources"
:issuable-type="$options.issuableType"
:path-id-separator="$options.pathIdSeparator"
:show-categorized-issues="false"
@toggleAddRelatedIssuesForm="toggleFormVisibility"
@addIssuableFormInput="addPendingReferences"
@addIssuableFormBlur="processAllReferences"
@addIssuableFormSubmit="addRelatedIssue"
@addIssuableFormCancel="resetForm"
@pendingIssuableRemoveRequest="removePendingReference"
@relatedIssueRemoveRequest="removeRelatedIssue"
>
<template #headerText>
{{ $options.i18n.relatedIssues }}
</template>
<template v-if="!isIssueAlreadyCreated && !isFetching" #headerActions>
<gl-button
ref="createIssue"
variant="success"
category="secondary"
:loading="isProcessingAction"
@click="createIssue"
>
{{ $options.i18n.createIssue }}
</gl-button>
</template>
</related-issues-block>
</div>
</template>
......@@ -11,7 +11,6 @@ module SubscriptionsHelper
plan_id: params[:plan_id],
namespace_id: params[:namespace_id],
new_user: new_user?.to_s,
onboarding_issues_experiment_enabled: experiment_enabled?(:onboarding_issues).to_s,
group_data: group_data.to_json
}
end
......
......@@ -18,7 +18,9 @@ module VulnerabilitiesHelper
vulnerability_feedback_help_path: help_page_path('user/application_security/index', anchor: 'interacting-with-the-vulnerabilities'),
related_issues_help_path: help_page_path('user/application_security/index', anchor: 'managing-related-issues-for-a-vulnerability'),
pipeline: vulnerability_pipeline_data(pipeline),
can_modify_related_issues: current_user.can?(:admin_vulnerability_issue_link, vulnerability)
can_modify_related_issues: current_user.can?(:admin_vulnerability_issue_link, vulnerability),
issue_tracking_help_path: help_page_path('user/project/settings', anchor: 'sharing-and-permissions'),
permissions_help_path: help_page_path('user/permissions', anchor: 'project-members-permissions')
}
result.merge(vulnerability_data(vulnerability), vulnerability_finding_data(vulnerability))
......
......@@ -4,7 +4,7 @@
.row.flex-grow-1.bg-gray-light
.d-flex.flex-column.align-items-center.w-100.p-3
.new-project.d-flex.flex-column.align-items-center.pt-5
#progress-bar{ data: { is_in_subscription_flow: in_subscription_flow?.to_s } }
#progress-bar
%h2.center= _('Create/import your first project')
%p
.center= html_escape(_('This project will live in your group %{strong_open}%{namespace}%{strong_close}. A project is where you house your files (repository), plan your work (issues), publish your documentation (wiki), and so much more.')) % { namespace: html_escape(@project.namespace.name), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
......
......@@ -11,7 +11,7 @@
%p= _('You have successfully purchased a %{plan} plan subscription for %{seats}. You’ll receive a receipt via email.') % { plan: plan_title, seats: number_of_users }
.edit-group.d-flex.flex-column.align-items-center.gl-pt-7
- if params[:new_user]
#progress-bar{ data: { is_onboarding_issues_experiment_enabled: experiment_enabled?(:onboarding_issues).to_s } }
#progress-bar
%h2.center= _('Create your group')
%p
%div= _('A group represents your organization in GitLab. Groups allow you to manage users and collaborate across multiple projects.')
......
---
title: Simplify progress bar steps logic
merge_request: 40390
author:
type: changed
---
title: Fix vulnerability save button spinner position
merge_request: 40781
author:
type: fixed
---
title: Improve error message when creating issue fails
merge_request: 40525
author:
type: changed
---
title: Change merge train position messaging
merge_request: 40777
author:
type: changed
......@@ -5,12 +5,10 @@ require 'spec_helper'
RSpec.describe 'New project screen', :js do
let_it_be(:user) { create(:user) }
let_it_be(:namespace) { create(:group) }
let(:in_subscription_flow) { false }
before do
gitlab_sign_in(user)
namespace.add_owner(user)
allow_any_instance_of(EE::RegistrationsHelper).to receive(:in_subscription_flow?).and_return(in_subscription_flow)
stub_experiment_for_user(onboarding_issues: true)
visit new_users_sign_up_project_path(namespace_id: namespace.id)
end
......@@ -21,10 +19,4 @@ RSpec.describe 'New project screen', :js do
expect(subject).to have_content('Create/import your first project')
expect(subject).to have_content('Your profile Your GitLab group Your first project')
end
context 'when in the subscription flow' do
let(:in_subscription_flow) { true }
it { is_expected.to have_content('Your profile Checkout Your GitLab group Your first project') }
end
end
......@@ -58,14 +58,5 @@ RSpec.describe 'Welcome screen', :js do
end
end
end
context 'when in the subscription flow and part of the onboarding issues experiment' do
let(:in_subscription_flow) { true }
let(:part_of_onboarding_issues_experiment) { true }
it 'shows the progress bar with the correct steps' do
expect(page).to have_content('Your profile Checkout Your GitLab group Your first project')
end
end
end
end
......@@ -7,13 +7,11 @@ RSpec.describe 'Welcome screen', :js do
let_it_be(:group) { create(:group) }
let(:params) { {} }
let(:part_of_onboarding_issues_experiment) { false }
describe 'on GitLab.com' do
before do
group.add_owner(user)
gitlab_sign_in(user)
stub_experiment_for_user(onboarding_issues: part_of_onboarding_issues_experiment)
stub_request(:get, 'https://customers.gitlab.com/gitlab_plans?plan=free')
.to_return(status: 200, body: '{}', headers: {})
......@@ -31,14 +29,6 @@ RSpec.describe 'Welcome screen', :js do
it 'shows the progress bar with the correct steps' do
expect(page).to have_content('Your profile Checkout Your GitLab group')
end
context 'when part of the onboarding issues experiment' do
let(:part_of_onboarding_issues_experiment) { true }
it 'shows the progress bar with the correct steps' do
expect(page).to have_content('Your profile Checkout Your GitLab group Your first project')
end
end
end
end
end
......@@ -140,16 +140,33 @@ describe('ThroughputTable', () => {
it('includes the correct title and IID', () => {
const { title, iid } = throughputTableData[0];
expect(findCol(TEST_IDS.MERGE_REQUEST_DETAILS).text()).toBe(`${title} !${iid}`);
expect(findCol(TEST_IDS.MERGE_REQUEST_DETAILS).text()).toContain(`${title} !${iid}`);
});
it('does not include any icons by default', () => {
const icon = findColSubComponent(TEST_IDS.MERGE_REQUEST_DETAILS, GlIcon);
it('includes an inactive label icon by default', () => {
const labels = findColSubItem(TEST_IDS.MERGE_REQUEST_DETAILS, TEST_IDS.LABEL_DETAILS);
const icon = labels.find(GlIcon);
expect(labels.text()).toBe('0');
expect(labels.classes()).toContain('gl-opacity-5');
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('label');
});
expect(icon.exists()).toBe(false);
it('includes an inactive comment icon by default', () => {
const commentCount = findColSubItem(
TEST_IDS.MERGE_REQUEST_DETAILS,
TEST_IDS.COMMENT_COUNT,
);
const icon = commentCount.find(GlIcon);
expect(commentCount.text()).toBe('0');
expect(commentCount.classes()).toContain('gl-opacity-5');
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('comments');
});
it('includes a label icon and count when available', async () => {
it('includes an active label icon and count when available', async () => {
additionalData({
labels: {
nodes: [{ title: 'Brinix' }],
......@@ -165,10 +182,30 @@ describe('ThroughputTable', () => {
const icon = labelDetails.find(GlIcon);
expect(labelDetails.text()).toBe('1');
expect(labelDetails.classes()).not.toContain('gl-opacity-5');
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('label');
});
it('includes an active comment icon and count when available', async () => {
additionalData({
userNotesCount: 2,
});
await wrapper.vm.$nextTick();
const commentCount = findColSubItem(
TEST_IDS.MERGE_REQUEST_DETAILS,
TEST_IDS.COMMENT_COUNT,
);
const icon = commentCount.find(GlIcon);
expect(commentCount.text()).toBe('2');
expect(commentCount.classes()).not.toContain('gl-opacity-5');
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('comments');
});
it('includes a pipeline icon and when available', async () => {
const iconName = 'status_canceled';
......
......@@ -83,5 +83,6 @@ export const throughputTableData = [
nodes: [],
},
commitCount: 1,
userNotesCount: 0,
},
];
......@@ -51,21 +51,6 @@ describe('Checkout', () => {
]);
});
describe('when part of the onboarding issues experiment', () => {
beforeEach(() => {
store.state.isOnboardingIssuesExperimentEnabled = true;
});
it('passes the steps', () => {
expect(findProgressBar().props('steps')).toEqual([
'Your profile',
'Checkout',
'Your GitLab group',
'Your first project',
]);
});
});
it('passes the current step', () => {
expect(findProgressBar().props('currentStep')).toEqual('Checkout');
});
......
......@@ -4,14 +4,11 @@ import MergeTrainPositionIndicator from 'ee/vue_merge_request_widget/components/
describe('MergeTrainPositionIndicator', () => {
let wrapper;
let vm;
const factory = propsData => {
wrapper = shallowMount(MergeTrainPositionIndicator, {
propsData,
});
({ vm } = wrapper);
};
afterEach(() => {
......@@ -19,19 +16,21 @@ describe('MergeTrainPositionIndicator', () => {
});
describe('computed', () => {
describe('message', () => {
it('should return the message with the correct position (i.e., index + 1)', () => {
describe('template', () => {
it('should render the correct message', () => {
factory({ mergeTrainIndex: 3 });
expect(vm.message).toBe('In the merge train at position 4');
expect(trimText(wrapper.text())).toBe(
'Added to the merge train. There are 4 merge requests waiting to be merged',
);
});
});
describe('template', () => {
it('should render the correct message', () => {
factory({ mergeTrainIndex: 3 });
it('should change the merge train message when the position is 1', () => {
factory({ mergeTrainIndex: 0 });
expect(trimText(wrapper.text())).toBe('In the merge train at position 4');
expect(trimText(wrapper.text())).toBe(
'A new merge train has started and this merge request is the first of the queue.',
);
});
});
});
......
......@@ -73,20 +73,19 @@ describe('History Comment Editor', () => {
expect(wrapper.emitted().onCancel).toHaveLength(1);
});
it('disables the save button when there is no text or only whitespace in the textarea', () => {
it('disables the save button when there is no text or only whitespace in the textarea', async () => {
createWrapper({ initialComment: 'some comment' });
textarea().vm.$emit('input', ' ');
await wrapper.vm.$nextTick();
return wrapper.vm.$nextTick().then(() => {
expect(saveButton().attributes('disabled')).toBeTruthy();
});
expect(saveButton().props('disabled')).toBe(true);
});
it('disables all elements when the isSaving prop is true', () => {
it('disables all elements when the comment is being saved', () => {
createWrapper({ isSaving: true });
expect(textarea().attributes('disabled')).toBeTruthy();
expect(saveButton().attributes('disabled')).toBeTruthy();
expect(cancelButton().attributes('disabled')).toBeTruthy();
expect(saveButton().props('loading')).toBe(true);
expect(cancelButton().props('disabled')).toBe(true);
});
});
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import RelatedIssues from 'ee/vulnerabilities/components/related_issues.vue';
......@@ -27,6 +28,8 @@ describe('Vulnerability related issues component', () => {
const vulnerabilityId = 5131;
const createIssueUrl = '/create/issue';
const projectFingerprint = 'project-fingerprint';
const issueTrackingHelpPath = '/help/issue/tracking';
const permissionsHelpPath = '/help/permissions';
const reportType = 'vulnerability';
const issue1 = { id: 3, vulnerabilityLinkId: 987 };
const issue2 = { id: 25, vulnerabilityLinkId: 876 };
......@@ -40,6 +43,8 @@ describe('Vulnerability related issues component', () => {
projectFingerprint,
createIssueUrl,
reportType,
issueTrackingHelpPath,
permissionsHelpPath,
},
...opts,
});
......@@ -54,6 +59,7 @@ describe('Vulnerability related issues component', () => {
const blockProp = prop => relatedIssuesBlock().props(prop);
const blockEmit = (eventName, data) => relatedIssuesBlock().vm.$emit(eventName, data);
const findCreateIssueButton = () => wrapper.find({ ref: 'createIssue' });
const findAlert = () => wrapper.find(GlAlert);
afterEach(() => {
wrapper.destroy();
......@@ -275,6 +281,13 @@ describe('Vulnerability related issues component', () => {
});
describe('when linked issue is not yet created', () => {
const failCreateIssueAction = async () => {
mockAxios.onPost(createIssueUrl).reply(500);
expect(findAlert().exists()).toBe(false);
findCreateIssueButton().vm.$emit('click');
await waitForPromises();
};
beforeEach(async () => {
mockAxios.onGet(propsData.endpoint).replyOnce(httpStatusCodes.OK, [issue1, issue2]);
createWrapper({}, { stubs: { RelatedIssuesBlock } });
......@@ -313,15 +326,17 @@ describe('Vulnerability related issues component', () => {
});
});
it('shows an error message when issue creation fails', () => {
mockAxios.onPost(createIssueUrl).reply(500);
findCreateIssueButton().vm.$emit('click');
return waitForPromises().then(() => {
expect(mockAxios.history.post).toHaveLength(1);
expect(createFlash).toHaveBeenCalledWith(
'Something went wrong, could not create an issue.',
);
});
it('shows an error message when issue creation fails', async () => {
await failCreateIssueAction();
expect(mockAxios.history.post).toHaveLength(1);
expect(findAlert().exists()).toBe(true);
});
it('dismisses the error message', async () => {
await failCreateIssueAction();
findAlert().vm.$emit('dismiss');
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(false);
});
});
});
......@@ -35,7 +35,6 @@ RSpec.describe SubscriptionsHelper do
before do
allow(helper).to receive(:params).and_return(plan_id: 'bronze_id', namespace_id: group.id.to_s)
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:experiment_enabled?).with(:onboarding_issues).and_return(false)
group.add_owner(user)
end
......
......@@ -11,7 +11,6 @@ RSpec.describe 'subscriptions/groups/edit' do
allow(view).to receive(:group_path).and_return('')
allow(view).to receive(:subscriptions_groups_path).and_return('')
allow(view).to receive(:current_user).and_return(User.new)
allow(view).to receive(:experiment_enabled?).with(:onboarding_issues).and_return(false)
end
let(:quantity) { '1' }
......
.dast-auto-deploy:
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.0"
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.2"
dast_environment_deploy:
extends: .dast-auto-deploy
......
.auto-deploy:
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.0"
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.2"
dependencies: []
include:
......
......@@ -7024,6 +7024,9 @@ msgstr ""
msgid "Could not create group"
msgstr ""
msgid "Could not create issue"
msgstr ""
msgid "Could not create project"
msgstr ""
......@@ -9284,9 +9287,6 @@ msgstr ""
msgid "Enable usage ping"
msgstr ""
msgid "Enable usage ping to get an overview of how you are using GitLab from a feature perspective."
msgstr ""
msgid "Enable/disable your service desk. %{link_start}Learn more about service desk%{link_end}."
msgstr ""
......@@ -13037,9 +13037,6 @@ msgstr ""
msgid "In %{time_to_now}"
msgstr ""
msgid "In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}."
msgstr ""
msgid "In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index."
msgstr ""
......@@ -21876,6 +21873,9 @@ msgstr ""
msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
msgid "SecurityReports|Ensure that %{trackingStart}issue tracking%{trackingEnd} is enabled for this project and you have %{permissionsStart}permission to create new issues%{permissionsEnd}."
msgstr ""
msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
......@@ -25977,6 +25977,9 @@ msgstr ""
msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file"
msgstr ""
msgid "To view instance-level analytics, ask an admin to turn on %{docLinkStart}usage ping%{docLinkEnd}."
msgstr ""
msgid "To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the months view, only epics in the past month, current month, and next 5 months are shown."
msgstr ""
......@@ -26286,6 +26289,9 @@ msgstr ""
msgid "Turn on usage ping"
msgstr ""
msgid "Turn on usage ping to review instance-level analytics."
msgstr ""
msgid "Twitter"
msgstr ""
......@@ -26763,7 +26769,7 @@ msgstr ""
msgid "Usage"
msgstr ""
msgid "Usage ping is not enabled"
msgid "Usage ping is off"
msgstr ""
msgid "Usage statistics"
......@@ -27550,9 +27556,6 @@ msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to unlink the issue. Please try again later."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not get user."
msgstr ""
......@@ -29537,9 +29540,15 @@ msgstr ""
msgid "mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd} %{addPipelineLinkStart}Add the .gitlab-ci.yml file%{addPipelineLinkEnd} to create one."
msgstr ""
msgid "mrWidget|A new merge train has started and this merge request is the first of the queue."
msgstr ""
msgid "mrWidget|Added to the merge train by"
msgstr ""
msgid "mrWidget|Added to the merge train. There are %{mergeTrainPosition} merge requests waiting to be merged"
msgstr ""
msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr ""
......@@ -29627,9 +29636,6 @@ msgstr ""
msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
msgstr ""
msgid "mrWidget|In the merge train at position %{mergeTrainPosition}"
msgstr ""
msgid "mrWidget|Jump to first unresolved thread"
msgstr ""
......
......@@ -22,10 +22,10 @@ RSpec.describe 'DevOps Report page' do
stub_application_setting(usage_ping_enabled: false)
end
it 'shows empty state' do
it 'shows empty state', :js do
visit admin_dev_ops_score_path
expect(page).to have_content('Usage ping is not enabled')
expect(page).to have_selector(".js-empty-state")
end
it 'hides the intro callout' do
......
......@@ -22,11 +22,11 @@ describe('IDE pipelines list', () => {
const defaultState = {
links: { ciHelpPagePath: TEST_HOST },
pipelinesEmptyStateSvgPath: TEST_HOST,
pipelines: {
stages: [],
failedStages: [],
isLoadingJobs: false,
},
};
const defaultPipelinesState = {
stages: [],
failedStages: [],
isLoadingJobs: false,
};
const fetchLatestPipelineMock = jest.fn();
......@@ -34,23 +34,20 @@ describe('IDE pipelines list', () => {
const failedStagesGetterMock = jest.fn().mockReturnValue([]);
const fakeProjectPath = 'alpha/beta';
const createComponent = (state = {}) => {
const { pipelines: pipelinesState, ...restOfState } = state;
const { defaultPipelines, ...defaultRestOfState } = defaultState;
const fakeStore = new Vuex.Store({
const createStore = (rootState, pipelinesState) => {
return new Vuex.Store({
getters: {
currentProject: () => ({ web_url: 'some/url ', path_with_namespace: fakeProjectPath }),
},
state: {
...defaultRestOfState,
...restOfState,
...defaultState,
...rootState,
},
modules: {
pipelines: {
namespaced: true,
state: {
...defaultPipelines,
...defaultPipelinesState,
...pipelinesState,
},
actions: {
......@@ -69,10 +66,12 @@ describe('IDE pipelines list', () => {
},
},
});
};
const createComponent = (state = {}, pipelinesState = {}) => {
wrapper = shallowMount(List, {
localVue,
store: fakeStore,
store: createStore(state, pipelinesState),
});
};
......@@ -94,31 +93,33 @@ describe('IDE pipelines list', () => {
describe('when loading', () => {
let defaultPipelinesLoadingState;
beforeAll(() => {
defaultPipelinesLoadingState = {
...defaultState.pipelines,
isLoadingPipeline: true,
};
});
it('does not render when pipeline has loaded before', () => {
createComponent({
pipelines: {
createComponent(
{},
{
...defaultPipelinesLoadingState,
hasLoadedPipeline: true,
},
});
);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
it('renders loading state', () => {
createComponent({
pipelines: {
createComponent(
{},
{
...defaultPipelinesLoadingState,
hasLoadedPipeline: false,
},
});
);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
......@@ -126,21 +127,22 @@ describe('IDE pipelines list', () => {
describe('when loaded', () => {
let defaultPipelinesLoadedState;
beforeAll(() => {
defaultPipelinesLoadedState = {
...defaultState.pipelines,
isLoadingPipeline: false,
hasLoadedPipeline: true,
};
});
it('renders empty state when no latestPipeline', () => {
createComponent({ pipelines: { ...defaultPipelinesLoadedState, latestPipeline: null } });
createComponent({}, { ...defaultPipelinesLoadedState, latestPipeline: null });
expect(wrapper.element).toMatchSnapshot();
});
describe('with latest pipeline loaded', () => {
let withLatestPipelineState;
beforeAll(() => {
withLatestPipelineState = {
...defaultPipelinesLoadedState,
......@@ -149,12 +151,12 @@ describe('IDE pipelines list', () => {
});
it('renders ci icon', () => {
createComponent({ pipelines: withLatestPipelineState });
createComponent({}, withLatestPipelineState);
expect(wrapper.find(CiIcon).exists()).toBe(true);
});
it('renders pipeline data', () => {
createComponent({ pipelines: withLatestPipelineState });
createComponent({}, withLatestPipelineState);
expect(wrapper.text()).toContain('#1');
});
......@@ -162,7 +164,7 @@ describe('IDE pipelines list', () => {
it('renders list of jobs', () => {
const stages = [];
const isLoadingJobs = true;
createComponent({ pipelines: { ...withLatestPipelineState, stages, isLoadingJobs } });
createComponent({}, { ...withLatestPipelineState, stages, isLoadingJobs });
const jobProps = wrapper
.findAll(Tab)
......@@ -177,7 +179,7 @@ describe('IDE pipelines list', () => {
const failedStages = [];
failedStagesGetterMock.mockReset().mockReturnValue(failedStages);
const isLoadingJobs = true;
createComponent({ pipelines: { ...withLatestPipelineState, isLoadingJobs } });
createComponent({}, { ...withLatestPipelineState, isLoadingJobs });
const jobProps = wrapper
.findAll(Tab)
......@@ -191,12 +193,13 @@ describe('IDE pipelines list', () => {
describe('with YAML error', () => {
it('renders YAML error', () => {
const yamlError = 'test yaml error';
createComponent({
pipelines: {
createComponent(
{},
{
...defaultPipelinesLoadedState,
latestPipeline: { ...pipelines[0], yamlError },
},
});
);
expect(wrapper.text()).toContain('Found errors in your .gitlab-ci.yml:');
expect(wrapper.text()).toContain(yamlError);
......
......@@ -1196,7 +1196,7 @@ RSpec.describe Issue do
it 'schedules rebalancing if we time-out when finding a gap' do
lhs = build_stubbed(:issue, relative_position: 99, project: project)
to_move = build(:issue, project: project)
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect { to_move.move_between(lhs, issue) }.to raise_error(ActiveRecord::QueryCanceled)
end
......@@ -1205,7 +1205,7 @@ RSpec.describe Issue do
describe '#find_next_gap_after' do
it 'schedules rebalancing if we time-out when finding a gap' do
allow(issue).to receive(:find_next_gap) { raise ActiveRecord::QueryCanceled }
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect { issue.move_sequence_after }.to raise_error(ActiveRecord::QueryCanceled)
end
......
......@@ -77,7 +77,7 @@ RSpec.describe Issues::CreateService do
it 'rebalances if needed' do
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).to receive(:perform_async).with(Integer)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
end
......@@ -86,7 +86,7 @@ RSpec.describe Issues::CreateService do
stub_feature_flags(rebalance_issues: false)
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).not_to receive(:perform_async).with(Integer)
expect(IssueRebalancingWorker).not_to receive(:perform_async)
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
end
......@@ -95,7 +95,7 @@ RSpec.describe Issues::CreateService do
stub_feature_flags(rebalance_issues: project)
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).to receive(:perform_async).with(Integer)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
end
......
......@@ -126,7 +126,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).not_to receive(:perform_async).with(issue.id)
expect(IssueRebalancingWorker).not_to receive(:perform_async)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
......@@ -142,7 +142,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
......@@ -156,7 +156,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
......@@ -170,7 +170,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).to receive(:perform_async).with(issue.id)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
......
......@@ -115,6 +115,7 @@ RSpec.configure do |config|
config.include StubExperiments
config.include StubGitlabCalls
config.include StubGitlabData
config.include NextFoundInstanceOf
config.include NextInstanceOf
config.include TestEnv
config.include Devise::Test::ControllerHelpers, type: :controller
......
# frozen_string_literal: true
module NextFoundInstanceOf
ERROR_MESSAGE = 'NextFoundInstanceOf mock helpers can only be used with ActiveRecord targets'
def expect_next_found_instance_of(klass)
check_if_active_record!(klass)
stub_allocate(expect(klass)) do |expectation|
yield(expectation)
end
end
def allow_next_found_instance_of(klass)
check_if_active_record!(klass)
stub_allocate(allow(klass)) do |allowance|
yield(allowance)
end
end
private
def check_if_active_record!(klass)
raise ArgumentError.new(ERROR_MESSAGE) unless klass < ActiveRecord::Base
end
def stub_allocate(target)
target.to receive(:allocate).and_wrap_original do |method|
method.call.tap { |allocation| yield(allocation) }
end
end
end
......@@ -10,23 +10,30 @@ RSpec.describe IssueRebalancingWorker do
service = double(execute: nil)
expect(IssueRebalancingService).to receive(:new).with(issue).and_return(service)
described_class.new.perform(issue.id)
described_class.new.perform(nil, issue.project_id)
end
it 'anticipates the inability to find the issue' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(ActiveRecord::RecordNotFound, include(issue_id: -1))
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(ActiveRecord::RecordNotFound, include(project_id: -1))
expect(IssueRebalancingService).not_to receive(:new)
described_class.new.perform(-1)
described_class.new.perform(nil, -1)
end
it 'anticipates there being too many issues' do
service = double
allow(service).to receive(:execute) { raise IssueRebalancingService::TooManyIssues }
expect(IssueRebalancingService).to receive(:new).with(issue).and_return(service)
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(IssueRebalancingService::TooManyIssues, include(issue_id: issue.id))
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(IssueRebalancingService::TooManyIssues, include(project_id: issue.project_id))
described_class.new.perform(issue.id)
described_class.new.perform(nil, issue.project_id)
end
it 'takes no action if the value is nil' do
expect(IssueRebalancingService).not_to receive(:new)
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
described_class.new.perform(nil, nil)
end
end
end
......@@ -54,13 +54,9 @@ RSpec.describe NewNoteWorker do
let(:note) { create(:note) }
before do
# TODO: `allow_next_instance_of` helper method is not working
# because ActiveRecord is directly calling `.allocate` on model
# classes and bypasses the `.new` method call.
# Fix the `allow_next_instance_of` helper and change these to mock
# the next instance of `Note` model class.
allow(Note).to receive(:find_by).with(id: note.id).and_return(note)
allow(note).to receive(:skip_notification?).and_return(true)
allow_next_found_instance_of(Note) do |note|
allow(note).to receive(:skip_notification?).and_return(true)
end
end
it 'does not create a new note notification' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment