Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6aa920eeb4
commit
fd27e4f95b
|
@ -1 +1 @@
|
|||
14.8.1
|
||||
14.9.0
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
|
||||
import * as lowlight from 'lowlight';
|
||||
import { lowlight } from 'lowlight/lib/all';
|
||||
|
||||
const extractLanguage = (element) => element.getAttribute('lang');
|
||||
|
||||
|
|
|
@ -28,8 +28,12 @@ export const overridesTabTitle = s__('Integrations|Projects using custom setting
|
|||
|
||||
export const integrationFormSections = {
|
||||
CONNECTION: 'connection',
|
||||
JIRA_TRIGGER: 'jira_trigger',
|
||||
JIRA_ISSUES: 'jira_issues',
|
||||
};
|
||||
|
||||
export const integrationFormSectionComponents = {
|
||||
[integrationFormSections.CONNECTION]: 'IntegrationSectionConnection',
|
||||
[integrationFormSections.JIRA_TRIGGER]: 'IntegrationSectionJiraTrigger',
|
||||
[integrationFormSections.JIRA_ISSUES]: 'IntegrationSectionJiraIssues',
|
||||
};
|
||||
|
|
|
@ -39,6 +39,14 @@ export default {
|
|||
import(
|
||||
/* webpackChunkName: 'integrationSectionConnection' */ '~/integrations/edit/components/sections/connection.vue'
|
||||
),
|
||||
IntegrationSectionJiraIssues: () =>
|
||||
import(
|
||||
/* webpackChunkName: 'integrationSectionJiraIssues' */ '~/integrations/edit/components/sections/jira_issues.vue'
|
||||
),
|
||||
IntegrationSectionJiraTrigger: () =>
|
||||
import(
|
||||
/* webpackChunkName: 'integrationSectionJiraTrigger' */ '~/integrations/edit/components/sections/jira_trigger.vue'
|
||||
),
|
||||
GlButton,
|
||||
GlForm,
|
||||
},
|
||||
|
@ -47,6 +55,11 @@ export default {
|
|||
SafeHtml,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
provide() {
|
||||
return {
|
||||
hasSections: this.hasSections,
|
||||
};
|
||||
},
|
||||
inject: {
|
||||
helpHtml: {
|
||||
default: '',
|
||||
|
@ -208,9 +221,9 @@ export default {
|
|||
|
||||
<template v-if="hasSections">
|
||||
<div
|
||||
v-for="section in customState.sections"
|
||||
v-for="(section, index) in customState.sections"
|
||||
:key="section.type"
|
||||
class="gl-border-b gl-mb-5"
|
||||
:class="{ 'gl-border-b gl-pb-3 gl-mb-6': index !== customState.sections.length - 1 }"
|
||||
data-testid="integration-section"
|
||||
>
|
||||
<div class="row">
|
||||
|
@ -225,6 +238,7 @@ export default {
|
|||
:fields="fieldsForSection(section)"
|
||||
:is-validated="isValidated"
|
||||
@toggle-integration-active="onToggleIntegrationState"
|
||||
@request-jira-issue-types="onRequestJiraIssueTypes"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -244,13 +258,13 @@ export default {
|
|||
@toggle-integration-active="onToggleIntegrationState"
|
||||
/>
|
||||
<jira-trigger-fields
|
||||
v-if="isJira"
|
||||
v-if="isJira && !hasSections"
|
||||
:key="`${currentKey}-jira-trigger-fields`"
|
||||
v-bind="propsSource.triggerFieldsProps"
|
||||
:is-validated="isValidated"
|
||||
/>
|
||||
<trigger-fields
|
||||
v-else-if="propsSource.triggerEvents.length"
|
||||
v-else-if="propsSource.triggerEvents.length && !hasSections"
|
||||
:key="`${currentKey}-trigger-fields`"
|
||||
:events="propsSource.triggerEvents"
|
||||
:type="propsSource.type"
|
||||
|
@ -262,15 +276,18 @@ export default {
|
|||
:is-validated="isValidated"
|
||||
/>
|
||||
<jira-issues-fields
|
||||
v-if="isJira && !isInstanceOrGroupLevel"
|
||||
v-if="isJira && !isInstanceOrGroupLevel && !hasSections"
|
||||
:key="`${currentKey}-jira-issues-fields`"
|
||||
v-bind="propsSource.jiraIssuesProps"
|
||||
:is-validated="isValidated"
|
||||
@request-jira-issue-types="onRequestJiraIssueTypes"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isEditable" class="row">
|
||||
<div :class="hasSections ? 'col' : 'col-lg-8 offset-lg-4'">
|
||||
<div
|
||||
v-if="isEditable"
|
||||
class="footer-block row-content-block gl-display-flex gl-justify-content-space-between"
|
||||
>
|
||||
<div>
|
||||
|
|
|
@ -16,6 +16,11 @@ export default {
|
|||
JiraIssueCreationVulnerabilities: () =>
|
||||
import('ee_component/integrations/edit/components/jira_issue_creation_vulnerabilities.vue'),
|
||||
},
|
||||
inject: {
|
||||
hasSections: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
showJiraIssuesIntegration: {
|
||||
type: Boolean,
|
||||
|
@ -101,9 +106,12 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<gl-form-group :label="$options.i18n.sectionTitle" label-for="jira-issue-settings">
|
||||
<gl-form-group
|
||||
:label="hasSections ? null : $options.i18n.sectionTitle"
|
||||
label-for="jira-issue-settings"
|
||||
>
|
||||
<div id="jira-issue-settings">
|
||||
<p>
|
||||
<p v-if="!hasSections">
|
||||
{{ $options.i18n.sectionDescription }}
|
||||
</p>
|
||||
<template v-if="showJiraIssuesIntegration">
|
||||
|
|
|
@ -62,6 +62,11 @@ export default {
|
|||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
inject: {
|
||||
hasSections: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
initialTriggerCommit: {
|
||||
type: Boolean,
|
||||
|
@ -134,12 +139,14 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<gl-form-group
|
||||
:label="__('Trigger')"
|
||||
:label="hasSections ? null : __('Trigger')"
|
||||
label-for="service[trigger]"
|
||||
:description="
|
||||
s__(
|
||||
'JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link and comment (if enabled) will be created.',
|
||||
)
|
||||
hasSections
|
||||
? null
|
||||
: s__(
|
||||
'JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link and comment (if enabled) will be created.',
|
||||
)
|
||||
"
|
||||
>
|
||||
<input name="service[commit_events]" type="hidden" :value="triggerCommit || false" />
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import JiraIssuesFields from '../jira_issues_fields.vue';
|
||||
|
||||
export default {
|
||||
name: 'IntegrationSectionJiraIssues',
|
||||
components: {
|
||||
JiraIssuesFields,
|
||||
},
|
||||
props: {
|
||||
isValidated: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentKey', 'propsSource']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<jira-issues-fields
|
||||
:key="`${currentKey}-jira-issues-fields`"
|
||||
v-bind="propsSource.jiraIssuesProps"
|
||||
:is-validated="isValidated"
|
||||
@request-jira-issue-types="$emit('request-jira-issue-types')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,32 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import JiraTriggerFields from '../jira_trigger_fields.vue';
|
||||
|
||||
export default {
|
||||
name: 'IntegrationSectionJiraTrigger',
|
||||
components: {
|
||||
JiraTriggerFields,
|
||||
},
|
||||
props: {
|
||||
isValidated: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentKey', 'propsSource']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<jira-trigger-fields
|
||||
:key="`${currentKey}-jira-trigger-fields`"
|
||||
v-bind="propsSource.triggerFieldsProps"
|
||||
:is-validated="isValidated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -174,6 +174,8 @@ export default {
|
|||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
this.isRetrying = false;
|
||||
|
||||
this.reportFailure(POST_FAILURE);
|
||||
} else {
|
||||
await this.$apollo.queries.pipeline.refetch();
|
||||
|
@ -182,6 +184,8 @@ export default {
|
|||
}
|
||||
}
|
||||
} catch {
|
||||
this.isRetrying = false;
|
||||
|
||||
this.reportFailure(POST_FAILURE);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -86,7 +86,7 @@ export default {
|
|||
);
|
||||
},
|
||||
statusIconName() {
|
||||
if (this.hasFetchError) return EXTENSION_ICONS.error;
|
||||
if (this.hasFetchError) return EXTENSION_ICONS.failed;
|
||||
if (this.isLoadingSummary) return null;
|
||||
|
||||
return this.statusIcon(this.collapsedData);
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { __, n__, s__, sprintf } from '~/locale';
|
||||
|
||||
const digitText = (bold = false) => (bold ? '%{strong_start}%d%{strong_end}' : '%d');
|
||||
const noText = (bold = false) => (bold ? '%{strong_start}no%{strong_end}' : 'no');
|
||||
|
||||
export const TESTS_FAILED_STATUS = 'failed';
|
||||
export const ERROR_STATUS = 'error';
|
||||
|
||||
export const i18n = {
|
||||
label: s__('Reports|Test summary'),
|
||||
loading: s__('Reports|Test summary results are loading'),
|
||||
error: s__('Reports|Test summary failed to load results'),
|
||||
fullReport: s__('Reports|Full report'),
|
||||
|
||||
noChanges: (bold) => s__(`Reports|${noText(bold)} changed test results`),
|
||||
resultsString: (combinedString, resolvedString) =>
|
||||
sprintf(s__('Reports|%{combinedString} and %{resolvedString}'), {
|
||||
combinedString,
|
||||
resolvedString,
|
||||
}),
|
||||
|
||||
summaryText: (name, resultsString) =>
|
||||
sprintf(__('%{name}: %{resultsString}'), { name, resultsString }),
|
||||
|
||||
failedClause: (failed, bold) =>
|
||||
n__(`${digitText(bold)} failed`, `${digitText(bold)} failed`, failed),
|
||||
erroredClause: (errored, bold) =>
|
||||
n__(`${digitText(bold)} error`, `${digitText(bold)} errors`, errored),
|
||||
resolvedClause: (resolved, bold) =>
|
||||
n__(`${digitText(bold)} fixed test result`, `${digitText(bold)} fixed test results`, resolved),
|
||||
totalClause: (total, bold) =>
|
||||
n__(`${digitText(bold)} total test`, `${digitText(bold)} total tests`, total),
|
||||
|
||||
reportError: s__('Reports|An error occurred while loading report'),
|
||||
reportErrorWithName: (name) =>
|
||||
sprintf(s__('Reports|An error occurred while loading %{name} results'), { name }),
|
||||
headReportParsingError: s__('Reports|Head report parsing error:'),
|
||||
baseReportParsingError: s__('Reports|Base report parsing error:'),
|
||||
};
|
|
@ -0,0 +1,82 @@
|
|||
import { uniqueId } from 'lodash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { EXTENSION_ICONS } from '../../constants';
|
||||
import { summaryTextBuilder, reportTextBuilder, reportSubTextBuilder } from './utils';
|
||||
import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
|
||||
|
||||
export default {
|
||||
name: 'WidgetTestSummary',
|
||||
enablePolling: true,
|
||||
i18n,
|
||||
expandEvent: 'i_testing_summary_widget_total',
|
||||
props: ['testResultsPath', 'headBlobPath', 'pipeline'],
|
||||
computed: {
|
||||
summary(data) {
|
||||
if (data.parsingInProgress) {
|
||||
return this.$options.i18n.loading;
|
||||
}
|
||||
if (data.hasSuiteError) {
|
||||
return this.$options.i18n.error;
|
||||
}
|
||||
return summaryTextBuilder(this.$options.i18n.label, data.summary);
|
||||
},
|
||||
statusIcon(data) {
|
||||
if (data.parsingInProgress) {
|
||||
return null;
|
||||
}
|
||||
if (data.status === TESTS_FAILED_STATUS) {
|
||||
return EXTENSION_ICONS.warning;
|
||||
}
|
||||
if (data.hasSuiteError) {
|
||||
return EXTENSION_ICONS.failed;
|
||||
}
|
||||
return EXTENSION_ICONS.success;
|
||||
},
|
||||
tertiaryButtons() {
|
||||
return [
|
||||
{
|
||||
text: this.$options.i18n.fullReport,
|
||||
href: `${this.pipeline.path}/test_report`,
|
||||
target: '_blank',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchCollapsedData() {
|
||||
return axios.get(this.testResultsPath).then(({ data = {}, status }) => {
|
||||
return {
|
||||
data: {
|
||||
hasSuiteError: data.suites?.some((suite) => suite.status === ERROR_STATUS),
|
||||
parsingInProgress: status === 204,
|
||||
...data,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
fetchFullData() {
|
||||
return Promise.resolve(this.prepareReports());
|
||||
},
|
||||
suiteIcon(suite) {
|
||||
if (suite.status === ERROR_STATUS) {
|
||||
return EXTENSION_ICONS.error;
|
||||
}
|
||||
if (suite.status === TESTS_FAILED_STATUS) {
|
||||
return EXTENSION_ICONS.failed;
|
||||
}
|
||||
return EXTENSION_ICONS.success;
|
||||
},
|
||||
prepareReports() {
|
||||
return this.collapsedData.suites.map((suite) => {
|
||||
return {
|
||||
id: uniqueId('suite-'),
|
||||
text: reportTextBuilder(suite),
|
||||
subtext: reportSubTextBuilder(suite),
|
||||
icon: {
|
||||
name: this.suiteIcon(suite),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
import { i18n } from './constants';
|
||||
|
||||
const textBuilder = (results, boldNumbers = false) => {
|
||||
const { failed, errored, resolved, total } = results;
|
||||
|
||||
const failedOrErrored = (failed || 0) + (errored || 0);
|
||||
const failedString = failed ? i18n.failedClause(failed, boldNumbers) : null;
|
||||
const erroredString = errored ? i18n.erroredClause(errored, boldNumbers) : null;
|
||||
const combinedString =
|
||||
failed && errored ? `${failedString}, ${erroredString}` : failedString || erroredString;
|
||||
const resolvedString = resolved ? i18n.resolvedClause(resolved, boldNumbers) : null;
|
||||
const totalString = total ? i18n.totalClause(total, boldNumbers) : null;
|
||||
|
||||
let resultsString = i18n.noChanges(boldNumbers);
|
||||
|
||||
if (failedOrErrored) {
|
||||
if (resolved) {
|
||||
resultsString = i18n.resultsString(combinedString, resolvedString);
|
||||
} else {
|
||||
resultsString = combinedString;
|
||||
}
|
||||
} else if (resolved) {
|
||||
resultsString = resolvedString;
|
||||
}
|
||||
|
||||
return `${resultsString}, ${totalString}`;
|
||||
};
|
||||
|
||||
export const summaryTextBuilder = (name = '', results = {}) => {
|
||||
const resultsString = textBuilder(results, true);
|
||||
return i18n.summaryText(name, resultsString);
|
||||
};
|
||||
|
||||
export const reportTextBuilder = ({ name = '', summary = {}, status }) => {
|
||||
if (!name) {
|
||||
return i18n.reportError;
|
||||
}
|
||||
if (status === 'error') {
|
||||
return i18n.reportErrorWithName(name);
|
||||
}
|
||||
|
||||
const resultsString = textBuilder(summary);
|
||||
return i18n.summaryText(name, resultsString);
|
||||
};
|
||||
|
||||
export const reportSubTextBuilder = ({ suite_errors }) => {
|
||||
const errors = [];
|
||||
if (suite_errors?.head) {
|
||||
errors.push(`${i18n.headReportParsingError} ${suite_errors.head}`);
|
||||
}
|
||||
if (suite_errors?.base) {
|
||||
errors.push(`${i18n.baseReportParsingError} ${suite_errors.base}`);
|
||||
}
|
||||
return errors.join('<br />');
|
||||
};
|
|
@ -46,6 +46,7 @@ import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variab
|
|||
import getStateQuery from './queries/get_state.query.graphql';
|
||||
import terraformExtension from './extensions/terraform';
|
||||
import accessibilityExtension from './extensions/accessibility';
|
||||
import testReportExtension from './extensions/test_report';
|
||||
|
||||
export default {
|
||||
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
|
||||
|
@ -190,6 +191,9 @@ export default {
|
|||
shouldRenderTerraformPlans() {
|
||||
return Boolean(this.mr?.terraformReportsPath);
|
||||
},
|
||||
shouldRenderTestReport() {
|
||||
return Boolean(this.mr?.testResultsPath);
|
||||
},
|
||||
mergeError() {
|
||||
let { mergeError } = this.mr;
|
||||
|
||||
|
@ -246,6 +250,11 @@ export default {
|
|||
this.registerAccessibilityExtension();
|
||||
}
|
||||
},
|
||||
shouldRenderTestReport(newVal) {
|
||||
if (newVal) {
|
||||
this.registerTestReportExtension();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
MRWidgetService.fetchInitialData()
|
||||
|
@ -491,6 +500,11 @@ export default {
|
|||
registerExtension(accessibilityExtension);
|
||||
}
|
||||
},
|
||||
registerTestReportExtension() {
|
||||
if (this.shouldRenderTestReport && this.shouldShowExtension) {
|
||||
registerExtension(testReportExtension);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -563,7 +577,7 @@ export default {
|
|||
/>
|
||||
|
||||
<grouped-test-reports-app
|
||||
v-if="mr.testResultsPath"
|
||||
v-if="mr.testResultsPath && !shouldShowExtension"
|
||||
class="js-reports-container"
|
||||
:endpoint="mr.testResultsPath"
|
||||
:head-blob-path="mr.headBlobPath"
|
||||
|
|
|
@ -17,10 +17,6 @@ module IssuableActions
|
|||
def show
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@show_crm_contacts = issuable.is_a?(Issue) && # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
can?(current_user, :read_crm_contact, issuable.project.group) &&
|
||||
CustomerRelations::Contact.exists_for_group?(issuable.project.group)
|
||||
|
||||
@issuable_sidebar = serializer.represent(issuable, serializer: 'sidebar') # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
render 'show'
|
||||
end
|
||||
|
|
|
@ -82,6 +82,10 @@ class Groups::ApplicationController < ApplicationController
|
|||
def has_project_list?
|
||||
false
|
||||
end
|
||||
|
||||
def validate_root_group!
|
||||
render_404 unless group.root?
|
||||
end
|
||||
end
|
||||
|
||||
Groups::ApplicationController.prepend_mod_with('Groups::ApplicationController')
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class Groups::Crm::ContactsController < Groups::ApplicationController
|
||||
feature_category :team_planning
|
||||
|
||||
before_action :validate_root_group!
|
||||
before_action :authorize_read_crm_contact!
|
||||
|
||||
def new
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class Groups::Crm::OrganizationsController < Groups::ApplicationController
|
||||
feature_category :team_planning
|
||||
|
||||
before_action :validate_root_group!
|
||||
before_action :authorize_read_crm_organization!
|
||||
|
||||
def new
|
||||
|
|
|
@ -161,14 +161,20 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def retry
|
||||
::Ci::RetryPipelineWorker.perform_async(pipeline.id, current_user.id) # rubocop:disable CodeReuse/Worker
|
||||
# Check for access before execution to allow for async execution while still returning access results
|
||||
access_response = ::Ci::RetryPipelineService.new(@project, current_user).check_access(pipeline)
|
||||
|
||||
if access_response.error?
|
||||
response = { json: { errors: [access_response.message] }, status: access_response.http_status }
|
||||
else
|
||||
response = { json: {}, status: :no_content }
|
||||
::Ci::RetryPipelineWorker.perform_async(pipeline.id, current_user.id) # rubocop:disable CodeReuse/Worker
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_back_or_default default: project_pipelines_path(project)
|
||||
format.json do
|
||||
render response
|
||||
end
|
||||
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -17,10 +17,11 @@ module Mutations
|
|||
pipeline = authorized_find!(id: id)
|
||||
project = pipeline.project
|
||||
|
||||
::Ci::RetryPipelineService.new(project, current_user).execute(pipeline)
|
||||
service_response = ::Ci::RetryPipelineService.new(project, current_user).execute(pipeline)
|
||||
|
||||
{
|
||||
pipeline: pipeline,
|
||||
errors: errors_on_object(pipeline)
|
||||
errors: errors_on_object(pipeline) + service_response.errors
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Groups
|
||||
module CrmSettingsHelper
|
||||
def crm_feature_flag_enabled?(group)
|
||||
def crm_feature_available?(group)
|
||||
Feature.enabled?(:customer_relations, group)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,8 +23,9 @@ class CustomerRelations::Contact < ApplicationRecord
|
|||
validates :last_name, presence: true, length: { maximum: 255 }
|
||||
validates :email, length: { maximum: 255 }
|
||||
validates :description, length: { maximum: 1024 }
|
||||
validates :email, uniqueness: { scope: :group_id }
|
||||
validate :validate_email_format
|
||||
validate :unique_email_for_group_hierarchy
|
||||
validate :validate_root_group
|
||||
|
||||
def self.reference_prefix
|
||||
'[contact:'
|
||||
|
@ -41,14 +42,13 @@ class CustomerRelations::Contact < ApplicationRecord
|
|||
def self.find_ids_by_emails(group, emails)
|
||||
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
|
||||
|
||||
where(group_id: group.self_and_ancestor_ids, email: emails)
|
||||
.pluck(:id)
|
||||
where(group: group, email: emails).pluck(:id)
|
||||
end
|
||||
|
||||
def self.exists_for_group?(group)
|
||||
return false unless group
|
||||
|
||||
exists?(group_id: group.self_and_ancestor_ids)
|
||||
exists?(group: group)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -59,13 +59,9 @@ class CustomerRelations::Contact < ApplicationRecord
|
|||
self.errors.add(:email, I18n.t(:invalid, scope: 'valid_email.validations.email')) unless ValidateEmail.valid?(self.email)
|
||||
end
|
||||
|
||||
def unique_email_for_group_hierarchy
|
||||
return unless group
|
||||
return unless email
|
||||
def validate_root_group
|
||||
return if group&.root?
|
||||
|
||||
duplicate_email_exists = CustomerRelations::Contact
|
||||
.where(group_id: group.self_and_hierarchy.pluck(:id), email: email)
|
||||
.where.not(id: id).exists?
|
||||
self.errors.add(:email, _('contact with same email already exists in group hierarchy')) if duplicate_email_exists
|
||||
self.errors.add(:base, _('contacts can only be added to root groups'))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ class CustomerRelations::IssueContact < ApplicationRecord
|
|||
belongs_to :issue, optional: false, inverse_of: :customer_relations_contacts
|
||||
belongs_to :contact, optional: false, inverse_of: :issue_contacts
|
||||
|
||||
validate :contact_belongs_to_issue_group_or_ancestor
|
||||
validate :contact_belongs_to_root_group
|
||||
|
||||
def self.find_contact_ids_by_emails(issue_id, emails)
|
||||
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
|
||||
|
@ -24,11 +24,11 @@ class CustomerRelations::IssueContact < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def contact_belongs_to_issue_group_or_ancestor
|
||||
def contact_belongs_to_root_group
|
||||
return unless contact&.group_id
|
||||
return unless issue&.project&.namespace_id
|
||||
return if issue.project.group&.self_and_ancestor_ids&.include?(contact.group_id)
|
||||
return if issue.project.root_ancestor&.id == contact.group_id
|
||||
|
||||
errors.add(:base, _('The contact does not belong to the issue group or an ancestor'))
|
||||
errors.add(:base, _("The contact does not belong to the issue group's root ancestor"))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,9 +19,18 @@ class CustomerRelations::Organization < ApplicationRecord
|
|||
validates :name, uniqueness: { case_sensitive: false, scope: [:group_id] }
|
||||
validates :name, length: { maximum: 255 }
|
||||
validates :description, length: { maximum: 1024 }
|
||||
validate :validate_root_group
|
||||
|
||||
def self.find_by_name(group_id, name)
|
||||
where(group: group_id)
|
||||
.where('LOWER(name) = LOWER(?)', name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_root_group
|
||||
return if group&.root?
|
||||
|
||||
self.errors.add(:base, _('organizations can only be added to root groups'))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,6 +15,9 @@ module Integrations
|
|||
ATLASSIAN_REFERRER_GITLAB_COM = { atlOrigin: 'eyJpIjoiY2QyZTJiZDRkNGZhNGZlMWI3NzRkNTBmZmVlNzNiZTkiLCJwIjoianN3LWdpdGxhYi1pbnQifQ' }.freeze
|
||||
ATLASSIAN_REFERRER_SELF_MANAGED = { atlOrigin: 'eyJpIjoiYjM0MTA4MzUyYTYxNDVkY2IwMzVjOGQ3ZWQ3NzMwM2QiLCJwIjoianN3LWdpdGxhYlNNLWludCJ9' }.freeze
|
||||
|
||||
SECTION_TYPE_JIRA_TRIGGER = 'jira_trigger'
|
||||
SECTION_TYPE_JIRA_ISSUES = 'jira_issues'
|
||||
|
||||
validates :url, public_url: true, presence: true, if: :activated?
|
||||
validates :api_url, public_url: true, allow_blank: true
|
||||
validates :username, presence: true, if: :activated?
|
||||
|
@ -157,13 +160,31 @@ module Integrations
|
|||
end
|
||||
|
||||
def sections
|
||||
[
|
||||
jira_issues_link_start = '<a href="%{url}">'.html_safe % { url: help_page_url('integration/jira/issues.html') }
|
||||
|
||||
sections = [
|
||||
{
|
||||
type: SECTION_TYPE_CONNECTION,
|
||||
title: s_('Integrations|Connection details'),
|
||||
description: help
|
||||
},
|
||||
{
|
||||
type: SECTION_TYPE_JIRA_TRIGGER,
|
||||
title: _('Trigger'),
|
||||
description: s_('JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link and comment (if enabled) will be created.')
|
||||
}
|
||||
].freeze
|
||||
]
|
||||
|
||||
# Jira issues is currently only configurable on the project level.
|
||||
if project_level?
|
||||
sections.push({
|
||||
type: SECTION_TYPE_JIRA_ISSUES,
|
||||
title: _('Issues'),
|
||||
description: s_('JiraService|Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of your Jira issues. %{jira_issues_link_start}Learn more.%{link_end}') % { jira_issues_link_start: jira_issues_link_start, link_end: '</a>'.html_safe }
|
||||
})
|
||||
end
|
||||
|
||||
sections
|
||||
end
|
||||
|
||||
def web_url(path = nil, **params)
|
||||
|
|
|
@ -13,7 +13,7 @@ class IssuePolicy < IssuablePolicy
|
|||
end
|
||||
|
||||
desc "User can read contacts belonging to the issue group"
|
||||
condition(:can_read_crm_contacts, scope: :subject) { @user.can?(:read_crm_contact, @subject.project.group) }
|
||||
condition(:can_read_crm_contacts, scope: :subject) { @user.can?(:read_crm_contact, @subject.project.root_ancestor) }
|
||||
|
||||
desc "Issue is confidential"
|
||||
condition(:confidential, scope: :subject) { @subject.confidential? }
|
||||
|
|
|
@ -10,6 +10,11 @@ class IssueSidebarBasicEntity < IssuableSidebarBasicEntity
|
|||
can?(current_user, :update_escalation_status, issue.project)
|
||||
end
|
||||
end
|
||||
|
||||
expose :show_crm_contacts do |issuable|
|
||||
current_user&.can?(:read_crm_contact, issuable.project.root_ancestor) &&
|
||||
CustomerRelations::Contact.exists_for_group?(issuable.project.root_ancestor)
|
||||
end
|
||||
end
|
||||
|
||||
IssueSidebarBasicEntity.prepend_mod_with('IssueSidebarBasicEntity')
|
||||
|
|
|
@ -65,7 +65,7 @@ module Ci
|
|||
|
||||
def check_access!(build)
|
||||
unless can?(current_user, :update_build, build)
|
||||
raise Gitlab::Access::AccessDeniedError
|
||||
raise Gitlab::Access::AccessDeniedError, '403 Forbidden'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,9 +5,8 @@ module Ci
|
|||
include Gitlab::OptimisticLocking
|
||||
|
||||
def execute(pipeline)
|
||||
unless can?(current_user, :update_pipeline, pipeline)
|
||||
raise Gitlab::Access::AccessDeniedError
|
||||
end
|
||||
access_response = check_access(pipeline)
|
||||
return access_response if access_response.error?
|
||||
|
||||
pipeline.ensure_scheduling_type!
|
||||
|
||||
|
@ -30,6 +29,18 @@ module Ci
|
|||
Ci::ProcessPipelineService
|
||||
.new(pipeline)
|
||||
.execute
|
||||
|
||||
ServiceResponse.success
|
||||
rescue Gitlab::Access::AccessDeniedError => e
|
||||
ServiceResponse.error(message: e.message, http_status: :forbidden)
|
||||
end
|
||||
|
||||
def check_access(pipeline)
|
||||
if can?(current_user, :update_pipeline, pipeline)
|
||||
ServiceResponse.success
|
||||
else
|
||||
ServiceResponse.error(message: '403 Forbidden', http_status: :forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -52,7 +52,7 @@ module Issues
|
|||
end
|
||||
|
||||
def add_by_email
|
||||
contact_ids = ::CustomerRelations::Contact.find_ids_by_emails(project_group, emails(:add_emails))
|
||||
contact_ids = ::CustomerRelations::Contact.find_ids_by_emails(project_group.root_ancestor, emails(:add_emails))
|
||||
add_by_id(contact_ids)
|
||||
end
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
= render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group
|
||||
= render 'groups/settings/membership', f: f, group: @group
|
||||
|
||||
- if crm_feature_flag_enabled?(@group)
|
||||
- if crm_feature_available?(@group)
|
||||
%h5= _('Customer relations')
|
||||
.form-group.gl-mb-3
|
||||
= f.gitlab_ui_checkbox_component :crm_enabled,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
- if @errors.present?
|
||||
.gl-alert.gl-alert-danger.gl-mb-5
|
||||
.gl-alert-container
|
||||
= sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
|
||||
.gl-alert-content
|
||||
.gl-alert-body
|
||||
- @errors.each do |error|
|
||||
= error
|
||||
= render 'shared/global_alert',
|
||||
variant: :danger,
|
||||
dismissible: false,
|
||||
alert_class: 'gl-mb-5' do
|
||||
.gl-alert-body
|
||||
- @errors.each do |error|
|
||||
= error
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
.block{ class: 'gl-pt-0! gl-collapse-empty', data: { qa_selector: 'iteration_container', testid: 'iteration_container' } }<
|
||||
= render_if_exists 'shared/issuable/iteration_select', can_edit: can_edit_issuable.to_s, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
|
||||
|
||||
- if @show_crm_contacts
|
||||
- if issuable_sidebar[:show_crm_contacts]
|
||||
.block.contact
|
||||
#js-issue-crm-contacts{ data: { issue_id: issuable_sidebar[:id] } }
|
||||
|
||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348786
|
|||
milestone: '14.6'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354955
|
|||
milestone: '14.9'
|
||||
type: development
|
||||
group: group::pipeline insights
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: vulnerability_reads_table
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76220
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348151
|
||||
milestone: '14.9'
|
||||
type: development
|
||||
group: group::threat insights
|
||||
default_enabled: false
|
|
@ -6,15 +6,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Customer relations management (CRM) **(FREE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.6 [with a flag](../../administration/feature_flags.md) named `customer_relations`. Disabled by default.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `customer_relations`.
|
||||
On GitLab.com, this feature is not available.
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.6 [with a flag](../../administration/feature_flags.md) named `customer_relations`. Disabled by default.
|
||||
> - In GitLab 14.8 and later, you can [create contacts and organizations only in root groups](https://gitlab.com/gitlab-org/gitlab/-/issues/350634).
|
||||
|
||||
With customer relations management (CRM) you can create a record of contacts
|
||||
(individuals) and organizations (companies) and relate them to issues.
|
||||
|
||||
Contacts and organizations can only be created for root groups.
|
||||
|
||||
You can use contacts and organizations to tie work to customers for billing and reporting purposes.
|
||||
To read more about what is planned for the future, see [issue 2256](https://gitlab.com/gitlab-org/gitlab/-/issues/2256).
|
||||
|
||||
|
|
|
@ -130,7 +130,7 @@ module.exports = (path, options = {}) => {
|
|||
'^.+\\.(md|zip|png)$': 'jest-raw-loader',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(@gitlab/ui|@gitlab/favicon-overlay|bootstrap-vue|three|monaco-editor|monaco-yaml|fast-mersenne-twister|prosemirror-markdown|dateformat)/)',
|
||||
'node_modules/(?!(@gitlab/ui|@gitlab/favicon-overlay|bootstrap-vue|three|monaco-editor|monaco-yaml|fast-mersenne-twister|prosemirror-markdown|dateformat|lowlight|fault)/)',
|
||||
],
|
||||
timers: 'fake',
|
||||
testEnvironment: '<rootDir>/spec/frontend/environment.js',
|
||||
|
|
|
@ -223,9 +223,13 @@ module API
|
|||
post ':id/pipelines/:pipeline_id/retry', feature_category: :continuous_integration do
|
||||
authorize! :update_pipeline, pipeline
|
||||
|
||||
pipeline.retry_failed(current_user)
|
||||
response = pipeline.retry_failed(current_user)
|
||||
|
||||
present pipeline, with: Entities::Ci::Pipeline
|
||||
if response.success?
|
||||
present pipeline, with: Entities::Ci::Pipeline
|
||||
else
|
||||
render_api_error!(response.errors.join(', '), response.http_status)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Cancel all builds in the pipeline' do
|
||||
|
|
|
@ -291,7 +291,7 @@ module Gitlab
|
|||
types Issue
|
||||
condition do
|
||||
current_user.can?(:set_issue_crm_contacts, quick_action_target) &&
|
||||
CustomerRelations::Contact.exists_for_group?(quick_action_target.project.group)
|
||||
CustomerRelations::Contact.exists_for_group?(quick_action_target.project.root_ancestor)
|
||||
end
|
||||
execution_message do
|
||||
_('One or more contacts were successfully added.')
|
||||
|
@ -306,7 +306,7 @@ module Gitlab
|
|||
types Issue
|
||||
condition do
|
||||
current_user.can?(:set_issue_crm_contacts, quick_action_target) &&
|
||||
CustomerRelations::Contact.exists_for_group?(quick_action_target.project.group)
|
||||
CustomerRelations::Contact.exists_for_group?(quick_action_target.project.root_ancestor)
|
||||
end
|
||||
execution_message do
|
||||
_('One or more contacts were successfully removed.')
|
||||
|
|
|
@ -24,6 +24,8 @@ module Sidebars
|
|||
|
||||
override :render?
|
||||
def render?
|
||||
return false unless context.group.root?
|
||||
|
||||
can_read_contact? || can_read_organization?
|
||||
end
|
||||
|
||||
|
|
|
@ -847,6 +847,9 @@ msgstr ""
|
|||
msgid "%{name}, confirm your email address now!"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{name}: %{resultsString}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{no_of_days} day"
|
||||
msgid_plural "%{no_of_days} days"
|
||||
msgstr[0] ""
|
||||
|
@ -21059,6 +21062,9 @@ msgstr ""
|
|||
msgid "JiraService|Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of your Jira issues."
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of your Jira issues. %{jira_issues_link_start}Learn more.%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|You must configure Jira before enabling this integration. %{jira_doc_link_start}Learn more.%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30941,6 +30947,9 @@ msgstr ""
|
|||
msgid "Reports|Filename"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Full report"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Head report parsing error:"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30983,9 +30992,15 @@ msgstr ""
|
|||
msgid "Reports|Test summary failed loading results"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Test summary failed to load results"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Test summary results are being parsed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Test summary results are loading"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Tool"
|
||||
msgstr ""
|
||||
|
||||
|
@ -36665,7 +36680,7 @@ msgstr ""
|
|||
msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
|
||||
msgstr ""
|
||||
|
||||
msgid "The contact does not belong to the issue group or an ancestor"
|
||||
msgid "The contact does not belong to the issue group's root ancestor"
|
||||
msgstr ""
|
||||
|
||||
msgid "The content editor may change the markdown formatting style of the document, which may not match your original markdown style."
|
||||
|
@ -43453,7 +43468,7 @@ msgstr ""
|
|||
msgid "compliance violation has already been recorded"
|
||||
msgstr ""
|
||||
|
||||
msgid "contact with same email already exists in group hierarchy"
|
||||
msgid "contacts can only be added to root groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "container registry images"
|
||||
|
@ -44363,6 +44378,9 @@ msgstr ""
|
|||
msgid "or"
|
||||
msgstr ""
|
||||
|
||||
msgid "organizations can only be added to root groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "other card matches"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -141,7 +141,7 @@
|
|||
"jszip-utils": "^0.0.2",
|
||||
"katex": "^0.13.2",
|
||||
"lodash": "^4.17.20",
|
||||
"lowlight": "^1.20.0",
|
||||
"lowlight": "^2.5.0",
|
||||
"marked": "^0.3.12",
|
||||
"mathjax": "3",
|
||||
"mermaid": "^8.13.10",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Manage', :requires_admin do
|
||||
RSpec.describe 'Manage', :reliable, :requires_admin do
|
||||
describe 'Gitlab migration' do
|
||||
let(:import_wait_duration) { { max_duration: 300, sleep_interval: 2 } }
|
||||
let(:admin_api_client) { Runtime::API::Client.as_admin }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Manage', :github, :requires_admin do
|
||||
RSpec.describe 'Manage', :reliable, :github, :requires_admin do
|
||||
describe 'Project import' do
|
||||
let(:github_repo) { 'gitlab-qa-github/import-test' }
|
||||
let(:api_client) { Runtime::API::Client.as_admin }
|
||||
|
|
|
@ -932,6 +932,33 @@ RSpec.describe Projects::PipelinesController do
|
|||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when access denied' do
|
||||
it 'returns an error' do
|
||||
sign_in(create(:user))
|
||||
|
||||
post_retry
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when service returns an error' do
|
||||
before do
|
||||
service_response = ServiceResponse.error(message: 'some error', http_status: 404)
|
||||
allow_next_instance_of(::Ci::RetryPipelineService) do |service|
|
||||
allow(service).to receive(:check_access).and_return(service_response)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not retry' do
|
||||
post_retry
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response.body).to include('some error')
|
||||
expect(::Ci::RetryPipelineWorker).not_to have_received(:perform_async).with(pipeline.id, user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST cancel.json' do
|
||||
|
|
|
@ -21,7 +21,7 @@ FactoryBot.define do
|
|||
|
||||
trait :for_issue do
|
||||
issue { raise ArgumentError, '`issue` is manadatory' }
|
||||
contact { association(:contact, group: issue.project.group) }
|
||||
contact { association(:contact, group: issue.project.root_ancestor) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -60,6 +60,18 @@ RSpec.describe 'Group navbar' do
|
|||
it_behaves_like 'verified navigation bar'
|
||||
end
|
||||
|
||||
context 'when customer_relations feature and flag is enabled but subgroup' do
|
||||
let(:group) { create(:group, :crm_enabled, parent: create(:group)) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(customer_relations: true)
|
||||
|
||||
visit group_path(group)
|
||||
end
|
||||
|
||||
it_behaves_like 'verified navigation bar'
|
||||
end
|
||||
|
||||
context 'when dependency proxy is available' do
|
||||
before do
|
||||
stub_config(dependency_proxy: { enabled: true })
|
||||
|
|
|
@ -435,6 +435,29 @@ describe('IntegrationForm', () => {
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe('when IntegrationSectionConnection emits `request-jira-issue-types` event', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(document, 'querySelector').mockReturnValue(document.createElement('form'));
|
||||
|
||||
createComponent({
|
||||
provide: {
|
||||
glFeatures: { integrationFormSections: true },
|
||||
},
|
||||
customStateProps: {
|
||||
sections: [mockSectionConnection],
|
||||
testPath: '/test',
|
||||
},
|
||||
mountFn: mountExtended,
|
||||
});
|
||||
|
||||
findConnectionSectionComponent().vm.$emit('request-jira-issue-types');
|
||||
});
|
||||
|
||||
it('dispatches `requestJiraIssueTypes` action', () => {
|
||||
expect(dispatch).toHaveBeenCalledWith('requestJiraIssueTypes', expect.any(FormData));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ActiveCheckbox', () => {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import IntegrationSectionJiraIssue from '~/integrations/edit/components/sections/jira_issues.vue';
|
||||
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
|
||||
import { createStore } from '~/integrations/edit/store';
|
||||
|
||||
import { mockIntegrationProps } from '../../mock_data';
|
||||
|
||||
describe('IntegrationSectionJiraIssue', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = () => {
|
||||
const store = createStore({
|
||||
customState: { ...mockIntegrationProps },
|
||||
});
|
||||
wrapper = shallowMount(IntegrationSectionJiraIssue, {
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findJiraIssuesFields = () => wrapper.findComponent(JiraIssuesFields);
|
||||
|
||||
describe('template', () => {
|
||||
it('renders JiraIssuesFields', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findJiraIssuesFields().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import IntegrationSectionJiraTrigger from '~/integrations/edit/components/sections/jira_trigger.vue';
|
||||
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
|
||||
import { createStore } from '~/integrations/edit/store';
|
||||
|
||||
import { mockIntegrationProps } from '../../mock_data';
|
||||
|
||||
describe('IntegrationSectionJiraTrigger', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = () => {
|
||||
const store = createStore({
|
||||
customState: { ...mockIntegrationProps },
|
||||
});
|
||||
wrapper = shallowMount(IntegrationSectionJiraTrigger, {
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findJiraTriggerFields = () => wrapper.findComponent(JiraTriggerFields);
|
||||
|
||||
describe('template', () => {
|
||||
it('renders JiraTriggerFields', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findJiraTriggerFields().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,7 @@
|
|||
import { GlModal, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import HeaderComponent from '~/pipelines/components/header_component.vue';
|
||||
import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
|
||||
import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
|
||||
|
@ -17,6 +19,7 @@ import {
|
|||
describe('Pipeline details header', () => {
|
||||
let wrapper;
|
||||
let glModalDirective;
|
||||
let mutate = jest.fn();
|
||||
|
||||
const findDeleteModal = () => wrapper.find(GlModal);
|
||||
const findRetryButton = () => wrapper.find('[data-testid="retryPipeline"]');
|
||||
|
@ -44,7 +47,7 @@ describe('Pipeline details header', () => {
|
|||
startPolling: jest.fn(),
|
||||
},
|
||||
},
|
||||
mutate: jest.fn(),
|
||||
mutate,
|
||||
};
|
||||
|
||||
return shallowMount(HeaderComponent, {
|
||||
|
@ -120,6 +123,26 @@ describe('Pipeline details header', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Retry action failed', () => {
|
||||
beforeEach(() => {
|
||||
mutate = jest.fn().mockRejectedValue('error');
|
||||
|
||||
wrapper = createComponent(mockCancelledPipelineHeader);
|
||||
});
|
||||
|
||||
it('retry button loading state should reset on error', async () => {
|
||||
findRetryButton().vm.$emit('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findRetryButton().props('loading')).toBe(true);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findRetryButton().props('loading')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cancel action', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent(mockRunningPipelineHeader);
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import testReportExtension from '~/vue_merge_request_widget/extensions/test_report';
|
||||
import { i18n } from '~/vue_merge_request_widget/extensions/test_report/constants';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
|
||||
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
|
||||
import httpStatusCodes from '~/lib/utils/http_status';
|
||||
|
||||
import { failedReport } from '../../../reports/mock_data/mock_data';
|
||||
import mixedResultsTestReports from '../../../reports/mock_data/new_and_fixed_failures_report.json';
|
||||
import newErrorsTestReports from '../../../reports/mock_data/new_errors_report.json';
|
||||
import newFailedTestReports from '../../../reports/mock_data/new_failures_report.json';
|
||||
import successTestReports from '../../../reports/mock_data/no_failures_report.json';
|
||||
import resolvedFailures from '../../../reports/mock_data/resolved_failures.json';
|
||||
|
||||
const reportWithParsingErrors = failedReport;
|
||||
reportWithParsingErrors.suites[0].suite_errors = {
|
||||
head: 'JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
|
||||
base: 'JUnit data parsing failed: string not matched',
|
||||
};
|
||||
|
||||
describe('Test report extension', () => {
|
||||
let wrapper;
|
||||
let mock;
|
||||
|
||||
registerExtension(testReportExtension);
|
||||
|
||||
const endpoint = '/root/repo/-/merge_requests/4/test_reports.json';
|
||||
|
||||
const mockApi = (statusCode, data = mixedResultsTestReports) => {
|
||||
mock.onGet(endpoint).reply(statusCode, data);
|
||||
};
|
||||
|
||||
const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
|
||||
const findTertiaryButton = () => wrapper.find(GlButton);
|
||||
const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = mountExtended(extensionsContainer, {
|
||||
propsData: {
|
||||
mr: {
|
||||
testResultsPath: endpoint,
|
||||
headBlobPath: 'head/blob/path',
|
||||
pipeline: { path: 'pipeline/path' },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createExpandedWidgetWithData = async (data = mixedResultsTestReports) => {
|
||||
mockApi(httpStatusCodes.OK, data);
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
findToggleCollapsedButton().trigger('click');
|
||||
await waitForPromises();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('summary', () => {
|
||||
it('displays loading text', () => {
|
||||
mockApi(httpStatusCodes.OK);
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.text()).toContain(i18n.loading);
|
||||
});
|
||||
|
||||
it('displays failed loading text', async () => {
|
||||
mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.text()).toContain(i18n.error);
|
||||
});
|
||||
|
||||
it.each`
|
||||
description | mockData | expectedResult
|
||||
${'mixed test results'} | ${mixedResultsTestReports} | ${'Test summary: 2 failed and 2 fixed test results, 11 total tests'}
|
||||
${'unchanged test results'} | ${successTestReports} | ${'Test summary: no changed test results, 11 total tests'}
|
||||
${'tests with errors'} | ${newErrorsTestReports} | ${'Test summary: 2 errors, 11 total tests'}
|
||||
${'failed test results'} | ${newFailedTestReports} | ${'Test summary: 2 failed, 11 total tests'}
|
||||
${'resolved failures'} | ${resolvedFailures} | ${'Test summary: 4 fixed test results, 11 total tests'}
|
||||
`('displays summary text for $description', async ({ mockData, expectedResult }) => {
|
||||
mockApi(httpStatusCodes.OK, mockData);
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.text()).toContain(expectedResult);
|
||||
});
|
||||
|
||||
it('displays a link to the full report', async () => {
|
||||
mockApi(httpStatusCodes.OK);
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findTertiaryButton().text()).toBe('Full report');
|
||||
expect(findTertiaryButton().attributes('href')).toBe('pipeline/path/test_report');
|
||||
});
|
||||
|
||||
it('shows an error when a suite has a parsing error', async () => {
|
||||
mockApi(httpStatusCodes.OK, reportWithParsingErrors);
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.text()).toContain(i18n.error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expanded data', () => {
|
||||
it('displays summary for each suite', async () => {
|
||||
await createExpandedWidgetWithData();
|
||||
|
||||
expect(trimText(findAllExtensionListItems().at(0).text())).toBe(
|
||||
'rspec:pg: 1 failed and 2 fixed test results, 8 total tests',
|
||||
);
|
||||
expect(trimText(findAllExtensionListItems().at(1).text())).toBe(
|
||||
'java ant: 1 failed, 3 total tests',
|
||||
);
|
||||
});
|
||||
|
||||
it('displays suite parsing errors', async () => {
|
||||
await createExpandedWidgetWithData(reportWithParsingErrors);
|
||||
|
||||
const suiteText = trimText(findAllExtensionListItems().at(0).text());
|
||||
|
||||
expect(suiteText).toContain(
|
||||
'Head report parsing error: JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
|
||||
);
|
||||
expect(suiteText).toContain(
|
||||
'Base report parsing error: JUnit data parsing failed: string not matched',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1025,7 +1025,7 @@ describe('MrWidgetOptions', () => {
|
|||
it('captures sentry error and displays error when poll has failed', () => {
|
||||
expect(captureException).toHaveBeenCalledTimes(1);
|
||||
expect(captureException).toHaveBeenCalledWith(new Error('Fetch error'));
|
||||
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('error');
|
||||
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1036,7 +1036,7 @@ describe('MrWidgetOptions', () => {
|
|||
const itHandlesTheException = () => {
|
||||
expect(captureException).toHaveBeenCalledTimes(1);
|
||||
expect(captureException).toHaveBeenCalledWith(new Error('Fetch error'));
|
||||
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('error');
|
||||
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -3,23 +3,45 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Groups::CrmSettingsHelper do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:root_group) { create(:group) }
|
||||
|
||||
describe '#crm_feature_flag_enabled?' do
|
||||
describe '#crm_feature_available?' do
|
||||
subject do
|
||||
helper.crm_feature_flag_enabled?(group)
|
||||
helper.crm_feature_available?(group)
|
||||
end
|
||||
|
||||
context 'when feature flag is enabled' do
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
context 'in root group' do
|
||||
let(:group) { root_group }
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(customer_relations: false)
|
||||
context 'when feature flag is enabled' do
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsy }
|
||||
context 'when feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(customer_relations: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsy }
|
||||
end
|
||||
end
|
||||
|
||||
context 'in subgroup' do
|
||||
let_it_be(:subgroup) { create(:group, parent: root_group) }
|
||||
|
||||
let(:group) { subgroup }
|
||||
|
||||
context 'when feature flag is enabled' do
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(customer_relations: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsy }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -610,6 +610,7 @@ project:
|
|||
- sync_events
|
||||
- secure_files
|
||||
- security_trainings
|
||||
- vulnerability_reads
|
||||
award_emoji:
|
||||
- awardable
|
||||
- user
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe CustomerRelations::Contact, type: :model do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:group) }
|
||||
it { is_expected.to belong_to(:organization).optional }
|
||||
|
@ -23,6 +25,8 @@ RSpec.describe CustomerRelations::Contact, type: :model do
|
|||
it { is_expected.to validate_length_of(:email).is_at_most(255) }
|
||||
it { is_expected.to validate_length_of(:description).is_at_most(1024) }
|
||||
|
||||
it { is_expected.to validate_uniqueness_of(:email).scoped_to(:group_id) }
|
||||
|
||||
it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :email
|
||||
end
|
||||
|
||||
|
@ -38,33 +42,15 @@ RSpec.describe CustomerRelations::Contact, type: :model do
|
|||
it { expect(described_class.reference_postfix).to eq(']') }
|
||||
end
|
||||
|
||||
describe '#unique_email_for_group_hierarchy' do
|
||||
let_it_be(:parent) { create(:group) }
|
||||
let_it_be(:group) { create(:group, parent: parent) }
|
||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||
|
||||
let_it_be(:existing_contact) { create(:contact, group: group) }
|
||||
|
||||
context 'with unique email for group hierarchy' do
|
||||
describe '#root_group' do
|
||||
context 'when root group' do
|
||||
subject { build(:contact, group: group) }
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'with duplicate email in group' do
|
||||
subject { build(:contact, email: existing_contact.email, group: group) }
|
||||
|
||||
it { is_expected.to be_invalid }
|
||||
end
|
||||
|
||||
context 'with duplicate email in parent group' do
|
||||
subject { build(:contact, email: existing_contact.email, group: subgroup) }
|
||||
|
||||
it { is_expected.to be_invalid }
|
||||
end
|
||||
|
||||
context 'with duplicate email in subgroup' do
|
||||
subject { build(:contact, email: existing_contact.email, group: parent) }
|
||||
context 'when subgroup' do
|
||||
subject { build(:contact, group: create(:group, parent: group)) }
|
||||
|
||||
it { is_expected.to be_invalid }
|
||||
end
|
||||
|
@ -82,7 +68,6 @@ RSpec.describe CustomerRelations::Contact, type: :model do
|
|||
end
|
||||
|
||||
describe '#self.find_ids_by_emails' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:group_contacts) { create_list(:contact, 2, group: group) }
|
||||
let_it_be(:other_contacts) { create_list(:contact, 2) }
|
||||
|
||||
|
@ -92,13 +77,6 @@ RSpec.describe CustomerRelations::Contact, type: :model do
|
|||
expect(contact_ids).to match_array(group_contacts.pluck(:id))
|
||||
end
|
||||
|
||||
it 'returns ids of contacts from parent group' do
|
||||
subgroup = create(:group, parent: group)
|
||||
contact_ids = described_class.find_ids_by_emails(subgroup, group_contacts.pluck(:email))
|
||||
|
||||
expect(contact_ids).to match_array(group_contacts.pluck(:id))
|
||||
end
|
||||
|
||||
it 'does not return ids of contacts from other groups' do
|
||||
contact_ids = described_class.find_ids_by_emails(group, other_contacts.pluck(:email))
|
||||
|
||||
|
@ -112,28 +90,17 @@ RSpec.describe CustomerRelations::Contact, type: :model do
|
|||
end
|
||||
|
||||
describe '#self.exists_for_group?' do
|
||||
let(:group) { create(:group) }
|
||||
let(:subgroup) { create(:group, parent: group) }
|
||||
|
||||
context 'with no contacts in group or parent' do
|
||||
context 'with no contacts in group' do
|
||||
it 'returns false' do
|
||||
expect(described_class.exists_for_group?(subgroup)).to be_falsey
|
||||
expect(described_class.exists_for_group?(group)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'with contacts in group' do
|
||||
it 'returns true' do
|
||||
create(:contact, group: subgroup)
|
||||
|
||||
expect(described_class.exists_for_group?(subgroup)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'with contacts in parent' do
|
||||
it 'returns true' do
|
||||
create(:contact, group: group)
|
||||
|
||||
expect(described_class.exists_for_group?(subgroup)).to be_truthy
|
||||
expect(described_class.exists_for_group?(group)).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,8 @@ RSpec.describe CustomerRelations::IssueContact do
|
|||
let_it_be(:issue_contact, reload: true) { create(:issue_customer_relations_contact) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||
let_it_be(:project) { create(:project, group: subgroup) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:subgroup_project) { create(:project, group: subgroup) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
|
||||
subject { issue_contact }
|
||||
|
@ -27,33 +28,36 @@ RSpec.describe CustomerRelations::IssueContact do
|
|||
let(:for_issue) { build(:issue_customer_relations_contact, :for_issue, issue: issue) }
|
||||
let(:for_contact) { build(:issue_customer_relations_contact, :for_contact, contact: contact) }
|
||||
|
||||
it 'uses objects from the same group', :aggregate_failures do
|
||||
expect(stubbed.contact.group).to eq(stubbed.issue.project.group)
|
||||
expect(built.contact.group).to eq(built.issue.project.group)
|
||||
expect(created.contact.group).to eq(created.issue.project.group)
|
||||
context 'for root groups' do
|
||||
it 'uses objects from the same group', :aggregate_failures do
|
||||
expect(stubbed.contact.group).to eq(stubbed.issue.project.group)
|
||||
expect(built.contact.group).to eq(built.issue.project.group)
|
||||
expect(created.contact.group).to eq(created.issue.project.group)
|
||||
end
|
||||
end
|
||||
|
||||
it 'builds using the same group', :aggregate_failures do
|
||||
expect(for_issue.contact.group).to eq(subgroup)
|
||||
expect(for_contact.issue.project.group).to eq(group)
|
||||
context 'for subgroups' do
|
||||
it 'builds using the root ancestor' do
|
||||
expect(for_issue.contact.group).to eq(group)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validation' do
|
||||
it 'fails when the contact group does not belong to the issue group or ancestors' do
|
||||
it 'fails when the contact group is unrelated to the issue group' do
|
||||
built = build(:issue_customer_relations_contact, issue: create(:issue), contact: create(:contact))
|
||||
|
||||
expect(built).not_to be_valid
|
||||
end
|
||||
|
||||
it 'succeeds when the contact group is the same as the issue group' do
|
||||
built = build(:issue_customer_relations_contact, issue: create(:issue, project: project), contact: create(:contact, group: subgroup))
|
||||
it 'succeeds when the contact belongs to a root group and is the same as the issue group' do
|
||||
built = build(:issue_customer_relations_contact, issue: create(:issue, project: project), contact: create(:contact, group: group))
|
||||
|
||||
expect(built).to be_valid
|
||||
end
|
||||
|
||||
it 'succeeds when the contact group is an ancestor of the issue group' do
|
||||
built = build(:issue_customer_relations_contact, issue: create(:issue, project: project), contact: create(:contact, group: group))
|
||||
it 'succeeds when the contact belongs to a root group and it is an ancestor of the issue group' do
|
||||
built = build(:issue_customer_relations_contact, issue: create(:issue, project: subgroup_project), contact: create(:contact, group: group))
|
||||
|
||||
expect(built).to be_valid
|
||||
end
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe CustomerRelations::Organization, type: :model do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:group).with_foreign_key('group_id') }
|
||||
end
|
||||
|
@ -17,6 +19,20 @@ RSpec.describe CustomerRelations::Organization, type: :model do
|
|||
it { is_expected.to validate_length_of(:description).is_at_most(1024) }
|
||||
end
|
||||
|
||||
describe '#root_group' do
|
||||
context 'when root group' do
|
||||
subject { build(:organization, group: group) }
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'when subgroup' do
|
||||
subject { build(:organization, group: create(:group, parent: group)) }
|
||||
|
||||
it { is_expected.to be_invalid }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#name' do
|
||||
it 'strips name' do
|
||||
organization = described_class.new(name: ' GitLab ')
|
||||
|
@ -27,7 +43,6 @@ RSpec.describe CustomerRelations::Organization, type: :model do
|
|||
end
|
||||
|
||||
describe '#find_by_name' do
|
||||
let!(:group) { create(:group) }
|
||||
let!(:organiztion1) { create(:organization, group: group, name: 'Test') }
|
||||
let!(:organiztion2) { create(:organization, group: create(:group), name: 'Test') }
|
||||
|
||||
|
|
|
@ -2950,7 +2950,14 @@ RSpec.describe Group do
|
|||
|
||||
expect(group.crm_enabled?).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns true where crm_settings.state is enabled for subgroup' do
|
||||
subgroup = create(:group, :crm_enabled, parent: group)
|
||||
|
||||
expect(subgroup.crm_enabled?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '.get_ids_by_ids_or_paths' do
|
||||
let(:group_path) { 'group_path' }
|
||||
let!(:group) { create(:group, path: group_path) }
|
||||
|
|
|
@ -109,6 +109,32 @@ RSpec.describe Integrations::Jira do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#sections' do
|
||||
let(:integration) { create(:jira_integration) }
|
||||
|
||||
subject(:sections) { integration.sections.map { |s| s[:type] } }
|
||||
|
||||
context 'when project_level? is true' do
|
||||
before do
|
||||
allow(integration).to receive(:project_level?).and_return(true)
|
||||
end
|
||||
|
||||
it 'includes SECTION_TYPE_JIRA_ISSUES' do
|
||||
expect(sections).to include(described_class::SECTION_TYPE_JIRA_ISSUES)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project_level? is false' do
|
||||
before do
|
||||
allow(integration).to receive(:project_level?).and_return(false)
|
||||
end
|
||||
|
||||
it 'does not include SECTION_TYPE_JIRA_ISSUES' do
|
||||
expect(sections).not_to include(described_class::SECTION_TYPE_JIRA_ISSUES)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.reference_pattern' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
|
|
|
@ -1167,7 +1167,6 @@ RSpec.describe Issue do
|
|||
end
|
||||
|
||||
describe '#check_for_spam?' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
let_it_be(:support_bot) { ::User.support_bot }
|
||||
|
||||
where(:support_bot?, :visibility_level, :confidential, :new_attributes, :check_for_spam?) do
|
||||
|
|
|
@ -396,4 +396,36 @@ RSpec.describe IssuePolicy do
|
|||
expect(policies).to be_allowed(:read_issue_iid)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'set_issue_crm_contacts' do
|
||||
let(:user) { create(:user) }
|
||||
let(:subgroup) { create(:group, :crm_enabled, parent: create(:group, :crm_enabled)) }
|
||||
let(:project) { create(:project, group: subgroup) }
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
let(:policies) { described_class.new(user, issue) }
|
||||
|
||||
context 'when project reporter' do
|
||||
it 'is disallowed' do
|
||||
project.add_reporter(user)
|
||||
|
||||
expect(policies).to be_disallowed(:set_issue_crm_contacts)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subgroup reporter' do
|
||||
it 'is allowed' do
|
||||
subgroup.add_reporter(user)
|
||||
|
||||
expect(policies).to be_disallowed(:set_issue_crm_contacts)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when root group reporter' do
|
||||
it 'is allowed' do
|
||||
subgroup.parent.add_reporter(user)
|
||||
|
||||
expect(policies).to be_allowed(:set_issue_crm_contacts)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1075,6 +1075,23 @@ RSpec.describe API::Ci::Pipelines do
|
|||
expect(json_response['id']).to be nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'handles errors' do
|
||||
before do
|
||||
service_response = ServiceResponse.error(http_status: 403, message: 'hello world')
|
||||
allow_next_instance_of(::Ci::RetryPipelineService) do |service|
|
||||
allow(service).to receive(:check_access).and_return(service_response)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns error' do
|
||||
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
expect(json_response['message']).to eq 'hello world'
|
||||
expect(json_response['id']).to be nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do
|
||||
|
|
|
@ -9,12 +9,10 @@ RSpec.describe 'Setting issues crm contacts' do
|
|||
let_it_be(:group) { create(:group, :crm_enabled) }
|
||||
let_it_be(:subgroup) { create(:group, :crm_enabled, parent: group) }
|
||||
let_it_be(:project) { create(:project, group: subgroup) }
|
||||
let_it_be(:group_contacts) { create_list(:contact, 4, group: group) }
|
||||
let_it_be(:subgroup_contacts) { create_list(:contact, 4, group: subgroup) }
|
||||
let_it_be(:contacts) { create_list(:contact, 4, group: group) }
|
||||
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
let(:operation_mode) { Types::MutationOperationModeEnum.default_mode }
|
||||
let(:contacts) { subgroup_contacts }
|
||||
let(:initial_contacts) { contacts[0..1] }
|
||||
let(:mutation_contacts) { contacts[1..2] }
|
||||
let(:contact_ids) { contact_global_ids(mutation_contacts) }
|
||||
|
@ -116,15 +114,7 @@ RSpec.describe 'Setting issues crm contacts' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with issue group contacts' do
|
||||
let(:contacts) { subgroup_contacts }
|
||||
|
||||
it_behaves_like 'successful mutation'
|
||||
end
|
||||
|
||||
context 'with issue ancestor group contacts' do
|
||||
it_behaves_like 'successful mutation'
|
||||
end
|
||||
it_behaves_like 'successful mutation'
|
||||
|
||||
context 'when the contact does not exist' do
|
||||
let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] }
|
||||
|
|
|
@ -49,6 +49,12 @@ RSpec.describe Groups::Crm::ContactsController do
|
|||
|
||||
it_behaves_like 'response with 404 status'
|
||||
end
|
||||
|
||||
context 'when subgroup' do
|
||||
let(:group) { create(:group, :private, :crm_enabled, parent: create(:group)) }
|
||||
|
||||
it_behaves_like 'response with 404 status'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unauthorized user' do
|
||||
|
|
|
@ -49,6 +49,12 @@ RSpec.describe Groups::Crm::OrganizationsController do
|
|||
|
||||
it_behaves_like 'response with 404 status'
|
||||
end
|
||||
|
||||
context 'when subgroup' do
|
||||
let(:group) { create(:group, :private, :crm_enabled, parent: create(:group)) }
|
||||
|
||||
it_behaves_like 'response with 404 status'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unauthorized user' do
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe IssueSidebarBasicEntity do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:group) { create(:group, :crm_enabled) }
|
||||
let_it_be(:project) { create(:project, :repository, group: group) }
|
||||
let_it_be(:user) { create(:user, developer_projects: [project]) }
|
||||
let_it_be(:issue) { create(:issue, project: project, assignees: [user]) }
|
||||
let_it_be_with_reload(:issue) { create(:issue, project: project, assignees: [user]) }
|
||||
|
||||
let(:serializer) { IssueSerializer.new(current_user: user, project: project) }
|
||||
|
||||
|
@ -71,4 +72,27 @@ RSpec.describe IssueSidebarBasicEntity do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'show_crm_contacts' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:is_reporter, :contacts_exist_for_group, :expected) do
|
||||
false | false | false
|
||||
false | true | false
|
||||
true | false | false
|
||||
true | true | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'sets proper boolean value for show_crm_contacts' do
|
||||
allow(CustomerRelations::Contact).to receive(:exists_for_group?).with(group).and_return(contacts_exist_for_group)
|
||||
|
||||
if is_reporter
|
||||
project.root_ancestor.add_reporter(user)
|
||||
end
|
||||
|
||||
expect(entity[:show_crm_contacts]).to be(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -137,7 +137,7 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the last stage was skipepd' do
|
||||
context 'when the last stage was skipped' do
|
||||
before do
|
||||
create_build('build 1', :success, 0)
|
||||
create_build('test 2', :failed, 1)
|
||||
|
@ -336,12 +336,32 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
|
|||
expect(pipeline.reload).to be_running
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not allowed to retry build' do
|
||||
before do
|
||||
build = create(:ci_build, pipeline: pipeline, status: :failed)
|
||||
allow_next_instance_of(Ci::RetryBuildService) do |service|
|
||||
allow(service).to receive(:can?).with(user, :update_build, build).and_return(false)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns an error' do
|
||||
response = service.execute(pipeline)
|
||||
|
||||
expect(response.http_status).to eq(:forbidden)
|
||||
expect(response.errors).to include('403 Forbidden')
|
||||
expect(pipeline.reload).not_to be_running
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not allowed to retry pipeline' do
|
||||
it 'raises an error' do
|
||||
expect { service.execute(pipeline) }
|
||||
.to raise_error Gitlab::Access::AccessDeniedError
|
||||
it 'returns an error' do
|
||||
response = service.execute(pipeline)
|
||||
|
||||
expect(response.http_status).to eq(:forbidden)
|
||||
expect(response.errors).to include('403 Forbidden')
|
||||
expect(pipeline.reload).not_to be_running
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -359,9 +379,12 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
|
|||
create_build('verify', :canceled, 1)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { service.execute(pipeline) }
|
||||
.to raise_error Gitlab::Access::AccessDeniedError
|
||||
it 'returns an error' do
|
||||
response = service.execute(pipeline)
|
||||
|
||||
expect(response.http_status).to eq(:forbidden)
|
||||
expect(response.errors).to include('403 Forbidden')
|
||||
expect(pipeline.reload).not_to be_running
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -372,9 +395,12 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
|
|||
create_build('verify', :canceled, 2)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { service.execute(pipeline) }
|
||||
.to raise_error Gitlab::Access::AccessDeniedError
|
||||
it 'returns an error' do
|
||||
response = service.execute(pipeline)
|
||||
|
||||
expect(response.http_status).to eq(:forbidden)
|
||||
expect(response.errors).to include('403 Forbidden')
|
||||
expect(pipeline.reload).not_to be_running
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe Issues::SetCrmContactsService do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group, :crm_enabled) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:project) { create(:project, group: create(:group, parent: group)) }
|
||||
let_it_be(:contacts) { create_list(:contact, 4, group: group) }
|
||||
let_it_be(:issue, reload: true) { create(:issue, project: project) }
|
||||
let_it_be(:issue_contact_1) do
|
||||
|
|
|
@ -203,7 +203,7 @@ RSpec.shared_context 'group navbar structure' do
|
|||
nav_sub_items: []
|
||||
},
|
||||
{
|
||||
nav_item: _('Group information'),
|
||||
nav_item: group.root? ? _('Group information') : _('Subgroup information'),
|
||||
nav_sub_items: [
|
||||
_('Activity'),
|
||||
_('Labels'),
|
||||
|
|
|
@ -11,7 +11,7 @@ RSpec.describe 'shared/issuable/_sidebar.html.haml' do
|
|||
end
|
||||
|
||||
context 'project in a group' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:group) { create(:group, :crm_enabled) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
let_it_be(:incident) { create(:incident, project: project) }
|
||||
|
@ -35,5 +35,34 @@ RSpec.describe 'shared/issuable/_sidebar.html.haml' do
|
|||
expect(rendered).not_to have_css('[data-testid="escalation_status_container"]')
|
||||
end
|
||||
end
|
||||
|
||||
context 'crm contacts widget' do
|
||||
let(:issuable) { issue }
|
||||
|
||||
context 'without permission' do
|
||||
it 'is expected not to be shown' do
|
||||
create(:contact, group: group)
|
||||
|
||||
expect(rendered).not_to have_css('#js-issue-crm-contacts')
|
||||
end
|
||||
end
|
||||
|
||||
context 'without contacts' do
|
||||
it 'is expected not to be shown' do
|
||||
group.add_developer(user)
|
||||
|
||||
expect(rendered).not_to have_css('#js-issue-crm-contacts')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with permission and contacts' do
|
||||
it 'is expected to be shown' do
|
||||
create(:contact, group: group)
|
||||
group.add_developer(user)
|
||||
|
||||
expect(rendered).to have_css('#js-issue-crm-contacts')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
36
yarn.lock
36
yarn.lock
|
@ -1896,6 +1896,13 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/hast@^2.0.0":
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"
|
||||
integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==
|
||||
dependencies:
|
||||
"@types/unist" "*"
|
||||
|
||||
"@types/http-proxy@^1.17.8":
|
||||
version "1.17.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.8.tgz#968c66903e7e42b483608030ee85800f22d03f55"
|
||||
|
@ -2119,6 +2126,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/ungap__global-this/-/ungap__global-this-0.3.1.tgz#18ce9f657da556037a29d50604335614ce703f4c"
|
||||
integrity sha512-+/DsiV4CxXl6ZWefwHZDXSe1Slitz21tom38qPCaG0DYCS1NnDPIQDTKcmQ/tvK/edJUKkmuIDBJbmKDiB0r/g==
|
||||
|
||||
"@types/unist@*":
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
||||
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
|
||||
|
||||
"@types/websocket@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.4.tgz#1dc497280d8049a5450854dd698ee7e6ea9e60b8"
|
||||
|
@ -5768,6 +5780,13 @@ fault@^1.0.0:
|
|||
dependencies:
|
||||
format "^0.2.0"
|
||||
|
||||
fault@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c"
|
||||
integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==
|
||||
dependencies:
|
||||
format "^0.2.0"
|
||||
|
||||
faye-websocket@^0.11.3:
|
||||
version "0.11.3"
|
||||
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e"
|
||||
|
@ -6397,10 +6416,10 @@ highlight.js@^10.6.0, highlight.js@~10.7.0:
|
|||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360"
|
||||
integrity sha512-oFLl873u4usRM9K63j4ME9u3etNF0PLiJhSQ8rdfuL51Wn3zkD6drf9ZW0dOzjnZI22YYG24z30JcmfCZjMgYg==
|
||||
|
||||
highlight.js@^11.3.1:
|
||||
version "11.3.1"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.3.1.tgz#813078ef3aa519c61700f84fe9047231c5dc3291"
|
||||
integrity sha512-PUhCRnPjLtiLHZAQ5A/Dt5F8cWZeMyj9KRsACsWT+OD6OP0x6dp5OmT5jdx0JgEyPxPZZIPQpRN2TciUT7occw==
|
||||
highlight.js@^11.3.1, highlight.js@~11.4.0:
|
||||
version "11.4.0"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.4.0.tgz#34ceadd49e1596ee5aba3d99346cdfd4845ee05a"
|
||||
integrity sha512-nawlpCBCSASs7EdvZOYOYVkJpGmAOKMYZgZtUqSRqodZE0GRVcFKwo1RcpeOemqh9hyttTdd5wDBwHkuSyUfnA==
|
||||
|
||||
hmac-drbg@^1.0.1:
|
||||
version "1.0.1"
|
||||
|
@ -8104,6 +8123,15 @@ lowlight@^1.20.0:
|
|||
fault "^1.0.0"
|
||||
highlight.js "~10.7.0"
|
||||
|
||||
lowlight@^2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-2.5.0.tgz#723a39fc0d9b911731a395b320519cbb0790ab14"
|
||||
integrity sha512-OXGUch9JZu4q5r4Ir6QlUp5pBXMxS7NHaclhRiUlxNRcOSK0gtXZcVrsGP4eM7bv0/KDHg/TXQagx/X35EULsA==
|
||||
dependencies:
|
||||
"@types/hast" "^2.0.0"
|
||||
fault "^2.0.0"
|
||||
highlight.js "~11.4.0"
|
||||
|
||||
lru-cache@^4.1.2, lru-cache@^4.1.5:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
|
||||
|
|
Loading…
Reference in New Issue