Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c0496e1078
commit
871b886a17
|
|
@ -2,55 +2,9 @@
|
|||
# Cop supports --autocorrect.
|
||||
RSpec/DescribedClass:
|
||||
Exclude:
|
||||
- 'ee/spec/models/concerns/elastic/merge_request_spec.rb'
|
||||
- 'ee/spec/models/concerns/elastic/note_spec.rb'
|
||||
- 'ee/spec/models/concerns/elastic/project_spec.rb'
|
||||
- 'ee/spec/models/concerns/elastic/repository_spec.rb'
|
||||
- 'ee/spec/models/dast_scanner_profile_spec.rb'
|
||||
- 'ee/spec/models/dast_site_profile_spec.rb'
|
||||
- 'ee/spec/models/ee/ci/job_artifact_spec.rb'
|
||||
- 'ee/spec/models/ee/ci/pending_build_spec.rb'
|
||||
- 'ee/spec/models/ee/ci/runner_spec.rb'
|
||||
- 'ee/spec/models/ee/gpg_key_spec.rb'
|
||||
- 'ee/spec/models/ee/group_spec.rb'
|
||||
- 'ee/spec/models/ee/project_spec.rb'
|
||||
- 'ee/spec/models/ee/vulnerability_spec.rb'
|
||||
- 'ee/spec/models/epic_issue_spec.rb'
|
||||
- 'ee/spec/models/epic_spec.rb'
|
||||
- 'ee/spec/models/geo/container_repository_registry_spec.rb'
|
||||
- 'ee/spec/models/geo/design_registry_spec.rb'
|
||||
- 'ee/spec/models/geo/package_file_registry_spec.rb'
|
||||
- 'ee/spec/models/geo/project_registry_spec.rb'
|
||||
- 'ee/spec/models/geo/secondary_usage_data_spec.rb'
|
||||
- 'ee/spec/models/issuable_metric_image_spec.rb'
|
||||
- 'ee/spec/models/issue_spec.rb'
|
||||
- 'ee/spec/models/iteration_spec.rb'
|
||||
- 'ee/spec/models/license_spec.rb'
|
||||
- 'ee/spec/models/project_import_state_spec.rb'
|
||||
- 'ee/spec/models/release_highlight_spec.rb'
|
||||
- 'ee/spec/models/requirements_management/test_report_spec.rb'
|
||||
- 'ee/spec/models/resource_weight_event_spec.rb'
|
||||
- 'ee/spec/models/uploads/local_spec.rb'
|
||||
- 'ee/spec/models/vulnerabilities/flag_spec.rb'
|
||||
- 'ee/spec/services/arkose/blocked_users_report_service_spec.rb'
|
||||
- 'ee/spec/services/ee/resource_events/synthetic_weight_notes_builder_service_spec.rb'
|
||||
- 'ee/spec/services/ee/users/reject_service_spec.rb'
|
||||
- 'ee/spec/services/security/ingestion/tasks/update_vulnerability_uuids_spec.rb'
|
||||
- 'ee/spec/services/users/captcha_challenge_service_spec.rb'
|
||||
- 'ee/spec/workers/concerns/elastic/indexing_control_spec.rb'
|
||||
- 'ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb'
|
||||
- 'ee/spec/workers/geo/verification_state_backfill_worker_spec.rb'
|
||||
- 'qa/spec/service/docker_run/base_spec.rb'
|
||||
- 'qa/spec/support/loglinking_spec.rb'
|
||||
- 'qa/spec/support/page_error_checker_spec.rb'
|
||||
- 'spec/config/settings_spec.rb'
|
||||
- 'spec/controllers/repositories/git_http_controller_spec.rb'
|
||||
- 'spec/experiments/application_experiment_spec.rb'
|
||||
- 'spec/frontend/fixtures/timezones.rb'
|
||||
- 'spec/graphql/gitlab_schema_spec.rb'
|
||||
- 'spec/graphql/graphql_triggers_spec.rb'
|
||||
- 'spec/graphql/types/global_id_type_spec.rb'
|
||||
- 'spec/initializers/google_api_client_spec.rb'
|
||||
- 'spec/lib/feature_spec.rb'
|
||||
- 'spec/lib/gitlab/ci/variables/collection/item_spec.rb'
|
||||
- 'spec/lib/gitlab/git/repository_spec.rb'
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
<script>
|
||||
import { s__ } from '~/locale';
|
||||
import ReportItem from '~/ci/reports/components/report_item.vue';
|
||||
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ReportItem,
|
||||
SmartVirtualList,
|
||||
},
|
||||
props: {
|
||||
component: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
nestedLevel: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
validator: (value) => [0, 1, 2].includes(value),
|
||||
},
|
||||
resolvedIssues: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
unresolvedIssues: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
resolvedHeading: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: s__('ciReport|Fixed'),
|
||||
},
|
||||
unresolvedHeading: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: s__('ciReport|New'),
|
||||
},
|
||||
},
|
||||
groups: ['unresolved', 'resolved'],
|
||||
typicalReportItemHeight: 32,
|
||||
maxShownReportItems: 20,
|
||||
computed: {
|
||||
groups() {
|
||||
return this.$options.groups
|
||||
.map((group) => ({
|
||||
name: group,
|
||||
issues: this[`${group}Issues`],
|
||||
heading: this[`${group}Heading`],
|
||||
}))
|
||||
.filter(({ issues }) => issues.length > 0);
|
||||
},
|
||||
listLength() {
|
||||
// every group has a header which is rendered as a list item
|
||||
const groupsCount = this.groups.length;
|
||||
const issuesCount = this.groups.reduce(
|
||||
(totalIssues, { issues }) => totalIssues + issues.length,
|
||||
0,
|
||||
);
|
||||
|
||||
return groupsCount + issuesCount;
|
||||
},
|
||||
listClasses() {
|
||||
return {
|
||||
'gl-pl-9': this.nestedLevel === 1,
|
||||
'gl-pl-11-5': this.nestedLevel === 2,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<smart-virtual-list
|
||||
:length="listLength"
|
||||
:remain="$options.maxShownReportItems"
|
||||
:size="$options.typicalReportItemHeight"
|
||||
:class="listClasses"
|
||||
class="report-block-container"
|
||||
wtag="ul"
|
||||
wclass="report-block-list"
|
||||
>
|
||||
<template v-for="(group, groupIndex) in groups">
|
||||
<h2
|
||||
:key="group.name"
|
||||
:data-testid="`${group.name}Heading`"
|
||||
:class="[groupIndex > 0 ? 'mt-2' : 'mt-0']"
|
||||
class="h5 mb-1"
|
||||
>
|
||||
{{ group.heading }}
|
||||
</h2>
|
||||
<report-item
|
||||
v-for="(issue, issueIndex) in group.issues"
|
||||
:key="`${group.name}-${issue.name}-${group.name}-${issueIndex}`"
|
||||
:issue="issue"
|
||||
:show-report-section-status-icon="false"
|
||||
:component="component"
|
||||
status="none"
|
||||
/>
|
||||
</template>
|
||||
</smart-virtual-list>
|
||||
</template>
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon.vue';
|
||||
import HelpPopover from '~/vue_shared/components/help_popover.vue';
|
||||
import { ICON_WARNING } from '../constants';
|
||||
|
||||
/**
|
||||
* Renders the summary row for each report
|
||||
*
|
||||
* Used both in MR widget and Pipeline's view for:
|
||||
* - Unit tests reports
|
||||
* - Security reports
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'ReportSummaryRow',
|
||||
components: {
|
||||
CiIcon,
|
||||
HelpPopover,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
nestedSummary: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
summary: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
statusIcon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
popoverOptions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconStatus() {
|
||||
return {
|
||||
group: this.statusIcon,
|
||||
icon: `status_${this.statusIcon}`,
|
||||
};
|
||||
},
|
||||
rowClasses() {
|
||||
if (!this.nestedSummary) {
|
||||
return ['gl-px-5'];
|
||||
}
|
||||
return ['gl-pl-9', 'gl-pr-5', { 'gl-bg-gray-10': this.statusIcon === ICON_WARNING }];
|
||||
},
|
||||
statusIconSize() {
|
||||
if (!this.nestedSummary) {
|
||||
return 24;
|
||||
}
|
||||
return 16;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="gl-border-t-solid gl-border-t-gray-100 gl-border-t-1 gl-py-3 gl-display-flex gl-align-items-center"
|
||||
:class="rowClasses"
|
||||
>
|
||||
<div class="gl-mr-3">
|
||||
<gl-loading-icon
|
||||
v-if="statusIcon === 'loading'"
|
||||
css-class="report-block-list-loading-icon"
|
||||
size="lg"
|
||||
/>
|
||||
<ci-icon v-else :status="iconStatus" :size="statusIconSize" data-testid="summary-row-icon" />
|
||||
</div>
|
||||
<div class="report-block-list-issue-description">
|
||||
<div class="report-block-list-issue-description-text" data-testid="summary-row-description">
|
||||
<slot name="summary">{{ summary }}</slot
|
||||
><span v-if="popoverOptions" class="text-nowrap"
|
||||
> <help-popover v-if="popoverOptions" :options="popoverOptions" class="align-top" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */"
|
||||
class="text-right flex-fill d-flex justify-content-end flex-column flex-sm-row"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -7,8 +7,6 @@ export const STATUS_SUCCESS = 'success';
|
|||
export const STATUS_NEUTRAL = 'neutral';
|
||||
export const STATUS_NOT_FOUND = 'not_found';
|
||||
|
||||
export const ICON_WARNING = 'warning';
|
||||
|
||||
export const status = {
|
||||
LOADING,
|
||||
ERROR,
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
<script>
|
||||
import { reportTypeToSecurityReportTypeEnum } from 'ee_else_ce/vue_shared/security_reports/constants';
|
||||
import { createAlert } from '~/alert';
|
||||
import { s__ } from '~/locale';
|
||||
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
|
||||
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
|
||||
import { extractSecurityReportArtifactsFromMergeRequest } from '~/vue_shared/security_reports/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SecurityReportDownloadDropdown,
|
||||
},
|
||||
props: {
|
||||
reportTypes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator: (reportType) => {
|
||||
return reportType.every((report) => reportTypeToSecurityReportTypeEnum[report]);
|
||||
},
|
||||
},
|
||||
targetProjectFullPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
mrIid: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
injectedArtifacts: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reportArtifacts: [],
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
reportArtifacts: {
|
||||
query: securityReportMergeRequestDownloadPathsQuery,
|
||||
variables() {
|
||||
return {
|
||||
projectPath: this.targetProjectFullPath,
|
||||
iid: String(this.mrIid),
|
||||
reportTypes: this.reportTypes.map(
|
||||
(reportType) => reportTypeToSecurityReportTypeEnum[reportType],
|
||||
),
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return extractSecurityReportArtifactsFromMergeRequest(this.reportTypes, data);
|
||||
},
|
||||
error(error) {
|
||||
this.showError(error);
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isLoadingReportArtifacts() {
|
||||
return this.$apollo.queries.reportArtifacts.loading;
|
||||
},
|
||||
mergedReportArtifacts() {
|
||||
return [...this.reportArtifacts, ...this.injectedArtifacts];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showError(error) {
|
||||
createAlert({
|
||||
message: this.$options.i18n.apiError,
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
apiError: s__(
|
||||
'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
|
||||
),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<security-report-download-dropdown
|
||||
:title="s__('SecurityReports|Download results')"
|
||||
:artifacts="mergedReportArtifacts"
|
||||
:loading="isLoadingReportArtifacts"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
export const SEVERITY_CLASS_NAME_MAP = {
|
||||
critical: 'gl-text-red-800',
|
||||
high: 'gl-text-red-600',
|
||||
medium: 'gl-text-orange-400',
|
||||
low: 'gl-text-orange-300',
|
||||
info: 'gl-text-blue-400',
|
||||
unknown: 'gl-text-gray-400',
|
||||
};
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<script>
|
||||
import { GlButton, GlIcon, GlLink, GlPopover } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlPopover,
|
||||
},
|
||||
props: {
|
||||
helpPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
discoverProjectSecurityPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
securityReportsHelp: s__('SecurityReports|Security reports help page link'),
|
||||
upgradeToManageVulnerabilities: s__('SecurityReports|Upgrade to manage vulnerabilities'),
|
||||
upgradeToInteract: s__(
|
||||
'SecurityReports|Upgrade to interact, track and shift left with vulnerability management features in the UI.',
|
||||
),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-if="discoverProjectSecurityPath">
|
||||
<gl-button
|
||||
ref="discoverProjectSecurity"
|
||||
icon="question-o"
|
||||
category="tertiary"
|
||||
:aria-label="$options.i18n.upgradeToManageVulnerabilities"
|
||||
/>
|
||||
|
||||
<gl-popover
|
||||
:target="() => $refs.discoverProjectSecurity.$el"
|
||||
:title="$options.i18n.upgradeToManageVulnerabilities"
|
||||
placement="top"
|
||||
triggers="click blur"
|
||||
>
|
||||
{{ $options.i18n.upgradeToInteract }}
|
||||
<gl-link :href="discoverProjectSecurityPath" target="_blank" class="gl-font-sm">{{
|
||||
__('Learn more')
|
||||
}}</gl-link>
|
||||
</gl-popover>
|
||||
</span>
|
||||
|
||||
<gl-link v-else target="_blank" :href="helpPath" :aria-label="$options.i18n.securityReportsHelp">
|
||||
<gl-icon name="question-o" />
|
||||
</gl-link>
|
||||
</template>
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
<script>
|
||||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { SEVERITY_CLASS_NAME_MAP } from './constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlSprintf,
|
||||
},
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
shouldShowCountMessage() {
|
||||
return !this.message.status && Boolean(this.message.countMessage);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getSeverityClass(severity) {
|
||||
return SEVERITY_CLASS_NAME_MAP[severity];
|
||||
},
|
||||
},
|
||||
slotNames: ['critical', 'high', 'other'],
|
||||
spacingClasses: {
|
||||
critical: 'gl-pl-4',
|
||||
high: 'gl-px-2',
|
||||
other: 'gl-px-2',
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<gl-sprintf :message="message.message">
|
||||
<template #total="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<span v-if="shouldShowCountMessage" class="gl-font-sm">
|
||||
<gl-sprintf :message="message.countMessage">
|
||||
<template v-for="slotName in $options.slotNames" #[slotName]="{ content }">
|
||||
<span :key="slotName">
|
||||
<strong
|
||||
v-if="message[slotName] > 0"
|
||||
:class="[getSeverityClass(slotName), $options.spacingClasses[slotName]]"
|
||||
>
|
||||
{{ content }}
|
||||
</strong>
|
||||
<span v-else :class="$options.spacingClasses[slotName]">
|
||||
{{ content }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -28,7 +28,6 @@ export const REPORT_TYPE_CLUSTER_IMAGE_SCANNING = 'cluster_image_scanning';
|
|||
export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing';
|
||||
export const REPORT_TYPE_CORPUS_MANAGEMENT = 'corpus_management';
|
||||
export const REPORT_TYPE_API_FUZZING = 'api_fuzzing';
|
||||
export const REPORT_TYPE_MANUALLY_ADDED = 'generic';
|
||||
|
||||
/**
|
||||
* SecurityReportTypeEnum values for use with GraphQL.
|
||||
|
|
|
|||
|
|
@ -1,238 +0,0 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { createAlert } from '~/alert';
|
||||
import { s__ } from '~/locale';
|
||||
import ReportSection from '~/ci/reports/components/report_section.vue';
|
||||
import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/ci/reports/constants';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import HelpIcon from './components/help_icon.vue';
|
||||
import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue';
|
||||
import SecuritySummary from './components/security_summary.vue';
|
||||
import {
|
||||
REPORT_TYPE_SAST,
|
||||
REPORT_TYPE_SECRET_DETECTION,
|
||||
reportTypeToSecurityReportTypeEnum,
|
||||
} from './constants';
|
||||
import securityReportMergeRequestDownloadPathsQuery from './graphql/queries/security_report_merge_request_download_paths.query.graphql';
|
||||
import store from './store';
|
||||
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants';
|
||||
import { extractSecurityReportArtifactsFromMergeRequest } from './utils';
|
||||
|
||||
export default {
|
||||
store,
|
||||
components: {
|
||||
ReportSection,
|
||||
HelpIcon,
|
||||
SecurityReportDownloadDropdown,
|
||||
SecuritySummary,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
pipelineId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
securityReportsDocsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
discoverProjectSecurityPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
sastComparisonPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
secretDetectionComparisonPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
targetProjectFullPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
mrIid: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
canDiscoverProjectSecurity: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
availableSecurityReports: [],
|
||||
canShowCounts: false,
|
||||
|
||||
// When core_security_mr_widget_counts is not enabled, the
|
||||
// error state is shown even when successfully loaded, since success
|
||||
// state suggests that the security scans detected no security problems,
|
||||
// which is not necessarily the case. A future iteration will actually
|
||||
// check whether problems were found and display the appropriate status.
|
||||
status: ERROR,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
reportArtifacts: {
|
||||
query: securityReportMergeRequestDownloadPathsQuery,
|
||||
variables() {
|
||||
return {
|
||||
projectPath: this.targetProjectFullPath,
|
||||
iid: String(this.mrIid),
|
||||
reportTypes: this.$options.reportTypes.map(
|
||||
(reportType) => reportTypeToSecurityReportTypeEnum[reportType],
|
||||
),
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return extractSecurityReportArtifactsFromMergeRequest(this.$options.reportTypes, data);
|
||||
},
|
||||
error(error) {
|
||||
this.showError(error);
|
||||
},
|
||||
result({ loading, data }) {
|
||||
if (loading || !data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Query has completed, so populate the availableSecurityReports.
|
||||
this.onCheckingAvailableSecurityReports(
|
||||
this.reportArtifacts.map(({ reportType }) => reportType),
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['groupedSummaryText', 'summaryStatus']),
|
||||
hasSecurityReports() {
|
||||
return this.availableSecurityReports.length > 0;
|
||||
},
|
||||
hasSastReports() {
|
||||
return this.availableSecurityReports.includes(REPORT_TYPE_SAST);
|
||||
},
|
||||
hasSecretDetectionReports() {
|
||||
return this.availableSecurityReports.includes(REPORT_TYPE_SECRET_DETECTION);
|
||||
},
|
||||
isLoadingReportArtifacts() {
|
||||
return this.$apollo.queries.reportArtifacts.loading;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(MODULE_SAST, {
|
||||
setSastDiffEndpoint: 'setDiffEndpoint',
|
||||
fetchSastDiff: 'fetchDiff',
|
||||
}),
|
||||
...mapActions(MODULE_SECRET_DETECTION, {
|
||||
setSecretDetectionDiffEndpoint: 'setDiffEndpoint',
|
||||
fetchSecretDetectionDiff: 'fetchDiff',
|
||||
}),
|
||||
fetchCounts() {
|
||||
if (!this.glFeatures.coreSecurityMrWidgetCounts) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.sastComparisonPath && this.hasSastReports) {
|
||||
this.setSastDiffEndpoint(this.sastComparisonPath);
|
||||
this.fetchSastDiff();
|
||||
this.canShowCounts = true;
|
||||
}
|
||||
|
||||
if (this.secretDetectionComparisonPath && this.hasSecretDetectionReports) {
|
||||
this.setSecretDetectionDiffEndpoint(this.secretDetectionComparisonPath);
|
||||
this.fetchSecretDetectionDiff();
|
||||
this.canShowCounts = true;
|
||||
}
|
||||
},
|
||||
onCheckingAvailableSecurityReports(availableSecurityReports) {
|
||||
this.availableSecurityReports = availableSecurityReports;
|
||||
this.fetchCounts();
|
||||
},
|
||||
showError(error) {
|
||||
createAlert({
|
||||
message: this.$options.i18n.apiError,
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
},
|
||||
},
|
||||
reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION],
|
||||
i18n: {
|
||||
apiError: s__(
|
||||
'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
|
||||
),
|
||||
scansHaveRun: s__('SecurityReports|Security scans have run'),
|
||||
},
|
||||
summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR],
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<report-section
|
||||
v-if="canShowCounts"
|
||||
:status="summaryStatus"
|
||||
:has-issues="false"
|
||||
class="mr-widget-border-top mr-report"
|
||||
data-testid="security-mr-widget"
|
||||
track-action="users_expanding_secure_security_report"
|
||||
>
|
||||
<template v-for="slot in $options.summarySlots" #[slot]>
|
||||
<span :key="slot">
|
||||
<security-summary :message="groupedSummaryText" />
|
||||
|
||||
<help-icon
|
||||
class="gl-ml-3"
|
||||
:help-path="securityReportsDocsPath"
|
||||
:discover-project-security-path="discoverProjectSecurityPath"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #action-buttons>
|
||||
<security-report-download-dropdown
|
||||
:text="s__('SecurityReports|Download results')"
|
||||
:artifacts="reportArtifacts"
|
||||
:loading="isLoadingReportArtifacts"
|
||||
/>
|
||||
</template>
|
||||
</report-section>
|
||||
|
||||
<!-- TODO: Remove this section when removing core_security_mr_widget_counts
|
||||
feature flag. See https://gitlab.com/gitlab-org/gitlab/-/issues/284097 -->
|
||||
<report-section
|
||||
v-else-if="hasSecurityReports"
|
||||
:status="status"
|
||||
:has-issues="false"
|
||||
class="mr-widget-border-top mr-report"
|
||||
data-testid="security-mr-widget"
|
||||
track-action="users_expanding_secure_security_report"
|
||||
>
|
||||
<template #error>
|
||||
{{ $options.i18n.scansHaveRun }}
|
||||
|
||||
<help-icon
|
||||
class="gl-ml-3"
|
||||
:help-path="securityReportsDocsPath"
|
||||
:discover-project-security-path="discoverProjectSecurityPath"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #action-buttons>
|
||||
<security-report-download-dropdown
|
||||
:text="s__('SecurityReports|Download results')"
|
||||
:artifacts="reportArtifacts"
|
||||
:loading="isLoadingReportArtifacts"
|
||||
/>
|
||||
</template>
|
||||
</report-section>
|
||||
</template>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
/**
|
||||
* Vuex module names corresponding to security scan types. These are similar to
|
||||
* the snake_case report types from the backend, but should not be considered
|
||||
* to be equivalent.
|
||||
*/
|
||||
export const MODULE_SAST = 'sast';
|
||||
export const MODULE_SECRET_DETECTION = 'secretDetection';
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import { s__, sprintf } from '~/locale';
|
||||
import { LOADING, ERROR, SUCCESS } from '~/ci/reports/constants';
|
||||
import { TRANSLATION_IS_LOADING } from './messages';
|
||||
import { countVulnerabilities, groupedTextBuilder } from './utils';
|
||||
|
||||
export const summaryCounts = (state) =>
|
||||
countVulnerabilities(
|
||||
state.reportTypes.reduce((acc, reportType) => {
|
||||
acc.push(...state[reportType].newIssues);
|
||||
return acc;
|
||||
}, []),
|
||||
);
|
||||
|
||||
export const groupedSummaryText = (state, getters) => {
|
||||
const reportType = s__('ciReport|Security scanning');
|
||||
let status = '';
|
||||
|
||||
// All reports are loading
|
||||
if (getters.areAllReportsLoading) {
|
||||
return { message: sprintf(TRANSLATION_IS_LOADING, { reportType }) };
|
||||
}
|
||||
|
||||
// All reports returned error
|
||||
if (getters.allReportsHaveError) {
|
||||
return { message: s__('ciReport|Security scanning failed loading any results') };
|
||||
}
|
||||
|
||||
if (getters.areReportsLoading && getters.anyReportHasError) {
|
||||
status = s__('ciReport|is loading, errors when loading results');
|
||||
} else if (getters.areReportsLoading && !getters.anyReportHasError) {
|
||||
status = s__('ciReport|is loading');
|
||||
} else if (!getters.areReportsLoading && getters.anyReportHasError) {
|
||||
status = s__('ciReport|: Loading resulted in an error');
|
||||
}
|
||||
|
||||
const { critical, high, other } = getters.summaryCounts;
|
||||
|
||||
return groupedTextBuilder({ reportType, status, critical, high, other });
|
||||
};
|
||||
|
||||
export const summaryStatus = (state, getters) => {
|
||||
if (getters.areReportsLoading) {
|
||||
return LOADING;
|
||||
}
|
||||
|
||||
if (getters.anyReportHasError || getters.anyReportHasIssues) {
|
||||
return ERROR;
|
||||
}
|
||||
|
||||
return SUCCESS;
|
||||
};
|
||||
|
||||
export const areReportsLoading = (state) =>
|
||||
state.reportTypes.some((reportType) => state[reportType].isLoading);
|
||||
|
||||
export const areAllReportsLoading = (state) =>
|
||||
state.reportTypes.every((reportType) => state[reportType].isLoading);
|
||||
|
||||
export const allReportsHaveError = (state) =>
|
||||
state.reportTypes.every((reportType) => state[reportType].hasError);
|
||||
|
||||
export const anyReportHasError = (state) =>
|
||||
state.reportTypes.some((reportType) => state[reportType].hasError);
|
||||
|
||||
export const anyReportHasIssues = (state) =>
|
||||
state.reportTypes.some((reportType) => state[reportType].newIssues.length > 0);
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import Vuex from 'vuex';
|
||||
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
|
||||
import * as getters from './getters';
|
||||
import sast from './modules/sast';
|
||||
import secretDetection from './modules/secret_detection';
|
||||
import state from './state';
|
||||
|
||||
export default () =>
|
||||
new Vuex.Store({
|
||||
modules: {
|
||||
[MODULE_SAST]: sast,
|
||||
[MODULE_SECRET_DETECTION]: secretDetection,
|
||||
},
|
||||
getters,
|
||||
state,
|
||||
});
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import { s__ } from '~/locale';
|
||||
|
||||
export const TRANSLATION_IS_LOADING = s__('ciReport|%{reportType} is loading');
|
||||
export const TRANSLATION_HAS_ERROR = s__('ciReport|%{reportType}: Loading resulted in an error');
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
|
||||
import { fetchDiffData } from '../../utils';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export const setDiffEndpoint = ({ commit }, path) => commit(types.SET_DIFF_ENDPOINT, path);
|
||||
|
||||
export const requestDiff = ({ commit }) => commit(types.REQUEST_DIFF);
|
||||
|
||||
export const receiveDiffSuccess = ({ commit }, response) =>
|
||||
commit(types.RECEIVE_DIFF_SUCCESS, response);
|
||||
|
||||
export const receiveDiffError = ({ commit }, response) =>
|
||||
commit(types.RECEIVE_DIFF_ERROR, response);
|
||||
|
||||
export const fetchDiff = ({ state, rootState, dispatch }) => {
|
||||
dispatch('requestDiff');
|
||||
|
||||
return fetchDiffData(rootState, state.paths.diffEndpoint, REPORT_TYPE_SAST)
|
||||
.then((data) => {
|
||||
dispatch('receiveDiffSuccess', data);
|
||||
return data;
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch('receiveDiffError');
|
||||
});
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import * as actions from './actions';
|
||||
import mutations from './mutations';
|
||||
import state from './state';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
actions,
|
||||
};
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export const RECEIVE_DIFF_SUCCESS = 'RECEIVE_DIFF_SUCCESS';
|
||||
export const RECEIVE_DIFF_ERROR = 'RECEIVE_DIFF_ERROR';
|
||||
export const REQUEST_DIFF = 'REQUEST_DIFF';
|
||||
export const SET_DIFF_ENDPOINT = 'SET_DIFF_ENDPOINT';
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import { parseDiff } from '../../utils';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_DIFF_ENDPOINT](state, path) {
|
||||
Vue.set(state.paths, 'diffEndpoint', path);
|
||||
},
|
||||
|
||||
[types.REQUEST_DIFF](state) {
|
||||
state.isLoading = true;
|
||||
},
|
||||
|
||||
[types.RECEIVE_DIFF_SUCCESS](state, { diff, enrichData }) {
|
||||
const { added, fixed, existing } = parseDiff(diff, enrichData);
|
||||
const baseReportOutofDate = diff.base_report_out_of_date || false;
|
||||
const hasBaseReport = Boolean(diff.base_report_created_at);
|
||||
|
||||
state.isLoading = false;
|
||||
state.newIssues = added;
|
||||
state.resolvedIssues = fixed;
|
||||
state.allIssues = existing;
|
||||
state.baseReportOutofDate = baseReportOutofDate;
|
||||
state.hasBaseReport = hasBaseReport;
|
||||
},
|
||||
|
||||
[types.RECEIVE_DIFF_ERROR](state) {
|
||||
state.isLoading = false;
|
||||
state.hasError = true;
|
||||
},
|
||||
};
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
export default () => ({
|
||||
paths: {
|
||||
diffEndpoint: null,
|
||||
},
|
||||
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
|
||||
newIssues: [],
|
||||
resolvedIssues: [],
|
||||
allIssues: [],
|
||||
baseReportOutofDate: false,
|
||||
hasBaseReport: false,
|
||||
});
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { REPORT_TYPE_SECRET_DETECTION } from '~/vue_shared/security_reports/constants';
|
||||
import { fetchDiffData } from '../../utils';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export const setDiffEndpoint = ({ commit }, path) => commit(types.SET_DIFF_ENDPOINT, path);
|
||||
|
||||
export const requestDiff = ({ commit }) => commit(types.REQUEST_DIFF);
|
||||
|
||||
export const receiveDiffSuccess = ({ commit }, response) =>
|
||||
commit(types.RECEIVE_DIFF_SUCCESS, response);
|
||||
|
||||
export const receiveDiffError = ({ commit }, response) =>
|
||||
commit(types.RECEIVE_DIFF_ERROR, response);
|
||||
|
||||
export const fetchDiff = ({ state, rootState, dispatch }) => {
|
||||
dispatch('requestDiff');
|
||||
|
||||
return fetchDiffData(rootState, state.paths.diffEndpoint, REPORT_TYPE_SECRET_DETECTION)
|
||||
.then((data) => {
|
||||
dispatch('receiveDiffSuccess', data);
|
||||
return data;
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch('receiveDiffError');
|
||||
});
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import * as actions from './actions';
|
||||
import mutations from './mutations';
|
||||
import state from './state';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
actions,
|
||||
};
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export const RECEIVE_DIFF_SUCCESS = 'RECEIVE_DIFF_SUCCESS';
|
||||
export const RECEIVE_DIFF_ERROR = 'RECEIVE_DIFF_ERROR';
|
||||
export const REQUEST_DIFF = 'REQUEST_DIFF';
|
||||
export const SET_DIFF_ENDPOINT = 'SET_DIFF_ENDPOINT';
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { parseDiff } from '~/vue_shared/security_reports/store/utils';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_DIFF_ENDPOINT](state, path) {
|
||||
state.paths.diffEndpoint = path;
|
||||
},
|
||||
|
||||
[types.REQUEST_DIFF](state) {
|
||||
state.isLoading = true;
|
||||
},
|
||||
|
||||
[types.RECEIVE_DIFF_SUCCESS](state, { diff, enrichData }) {
|
||||
const { added, fixed, existing } = parseDiff(diff, enrichData);
|
||||
const baseReportOutofDate = diff.base_report_out_of_date || false;
|
||||
const hasBaseReport = Boolean(diff.base_report_created_at);
|
||||
|
||||
state.isLoading = false;
|
||||
state.newIssues = added;
|
||||
state.resolvedIssues = fixed;
|
||||
state.allIssues = existing;
|
||||
state.baseReportOutofDate = baseReportOutofDate;
|
||||
state.hasBaseReport = hasBaseReport;
|
||||
},
|
||||
|
||||
[types.RECEIVE_DIFF_ERROR](state) {
|
||||
state.isLoading = false;
|
||||
state.hasError = true;
|
||||
},
|
||||
};
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
export default () => ({
|
||||
paths: {
|
||||
diffEndpoint: null,
|
||||
},
|
||||
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
|
||||
newIssues: [],
|
||||
resolvedIssues: [],
|
||||
allIssues: [],
|
||||
baseReportOutofDate: false,
|
||||
hasBaseReport: false,
|
||||
});
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
|
||||
|
||||
export default () => ({
|
||||
reportTypes: [MODULE_SAST, MODULE_SECRET_DETECTION],
|
||||
});
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import pollUntilComplete from '~/lib/utils/poll_until_complete';
|
||||
import { __, n__, sprintf } from '~/locale';
|
||||
import { CRITICAL, HIGH } from '~/vulnerabilities/constants';
|
||||
import {
|
||||
FEEDBACK_TYPE_DISMISSAL,
|
||||
FEEDBACK_TYPE_ISSUE,
|
||||
FEEDBACK_TYPE_MERGE_REQUEST,
|
||||
} from '../constants';
|
||||
|
||||
export const fetchDiffData = (state, endpoint, category) => {
|
||||
const requests = [pollUntilComplete(endpoint)];
|
||||
|
||||
if (state.canReadVulnerabilityFeedback) {
|
||||
requests.push(axios.get(state.vulnerabilityFeedbackPath, { params: { category } }));
|
||||
}
|
||||
|
||||
return Promise.all(requests).then(([diffResponse, enrichResponse]) => ({
|
||||
diff: diffResponse.data,
|
||||
enrichData: enrichResponse?.data ?? [],
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns given vulnerability enriched with the corresponding
|
||||
* feedback (`dismissal` or `issue` type)
|
||||
* @param {Object} vulnerabilityObject
|
||||
* @param {Array} feedbackList
|
||||
*/
|
||||
export const enrichVulnerabilityWithFeedback = (vulnerabilityObject, feedbackList = []) => {
|
||||
const vulnerability = { ...vulnerabilityObject };
|
||||
// Some records may have a null `uuid`, we need to fallback to using `project_fingerprint` in those cases. Once all entries have been fixed, we can remove the fallback.
|
||||
// related epic: https://gitlab.com/groups/gitlab-org/-/epics/2791
|
||||
feedbackList
|
||||
.filter((fb) =>
|
||||
fb.finding_uuid
|
||||
? fb.finding_uuid === vulnerability.uuid
|
||||
: fb.project_fingerprint === vulnerability.project_fingerprint,
|
||||
)
|
||||
.forEach((feedback) => {
|
||||
if (feedback.feedback_type === FEEDBACK_TYPE_DISMISSAL) {
|
||||
vulnerability.isDismissed = true;
|
||||
vulnerability.dismissalFeedback = feedback;
|
||||
} else if (feedback.feedback_type === FEEDBACK_TYPE_ISSUE && feedback.issue_iid) {
|
||||
vulnerability.hasIssue = true;
|
||||
vulnerability.issue_feedback = feedback;
|
||||
} else if (
|
||||
feedback.feedback_type === FEEDBACK_TYPE_MERGE_REQUEST &&
|
||||
feedback.merge_request_iid
|
||||
) {
|
||||
vulnerability.hasMergeRequest = true;
|
||||
vulnerability.merge_request_feedback = feedback;
|
||||
}
|
||||
});
|
||||
|
||||
return vulnerability;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the added, fixed, and existing vulnerabilities from the API report.
|
||||
*
|
||||
* @param {Object} diff The original reports.
|
||||
* @param {Object} enrichData Feedback data to add to the reports.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export const parseDiff = (diff, enrichData) => {
|
||||
const enrichVulnerability = (vulnerability) => ({
|
||||
...enrichVulnerabilityWithFeedback(vulnerability, enrichData),
|
||||
category: vulnerability.report_type,
|
||||
title: vulnerability.message || vulnerability.name,
|
||||
});
|
||||
|
||||
return {
|
||||
added: diff.added ? diff.added.map(enrichVulnerability) : [],
|
||||
fixed: diff.fixed ? diff.fixed.map(enrichVulnerability) : [],
|
||||
existing: diff.existing ? diff.existing.map(enrichVulnerability) : [],
|
||||
};
|
||||
};
|
||||
|
||||
const createCountMessage = ({ critical, high, other, total }) => {
|
||||
const otherMessage = n__('%d Other', '%d Others', other);
|
||||
const countMessage = __(
|
||||
'%{criticalStart}%{critical} Critical%{criticalEnd} %{highStart}%{high} High%{highEnd} and %{otherStart}%{otherMessage}%{otherEnd}',
|
||||
);
|
||||
return total ? sprintf(countMessage, { critical, high, otherMessage }) : '';
|
||||
};
|
||||
|
||||
const createStatusMessage = ({ reportType, status, total }) => {
|
||||
const vulnMessage = n__('vulnerability', 'vulnerabilities', total);
|
||||
let message;
|
||||
if (status) {
|
||||
message = __('%{reportType} %{status}');
|
||||
} else if (!total) {
|
||||
message = __('%{reportType} detected no new vulnerabilities.');
|
||||
} else {
|
||||
message = __(
|
||||
'%{reportType} detected %{totalStart}%{total}%{totalEnd} potential %{vulnMessage}',
|
||||
);
|
||||
}
|
||||
return sprintf(message, { reportType, status, total, vulnMessage });
|
||||
};
|
||||
|
||||
/**
|
||||
* Counts vulnerabilities.
|
||||
* Returns the amount of critical, high, and other vulnerabilities.
|
||||
*
|
||||
* @param {Array} vulnerabilities The raw vulnerabilities to parse
|
||||
* @returns {{critical: number, high: number, other: number}}
|
||||
*/
|
||||
export const countVulnerabilities = (vulnerabilities = []) =>
|
||||
vulnerabilities.reduce(
|
||||
(acc, { severity }) => {
|
||||
if (severity === CRITICAL) {
|
||||
acc.critical += 1;
|
||||
} else if (severity === HIGH) {
|
||||
acc.high += 1;
|
||||
} else {
|
||||
acc.other += 1;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ critical: 0, high: 0, other: 0 },
|
||||
);
|
||||
|
||||
/**
|
||||
* Takes an object of options and returns the object with an externalized string representing
|
||||
* the critical, high, and other severity vulnerabilities for a given report.
|
||||
*
|
||||
* The resulting string _may_ still contain sprintf-style placeholders. These
|
||||
* are left in place so they can be replaced with markup, via the
|
||||
* SecuritySummary component.
|
||||
* @param {{reportType: string, status: string, critical: number, high: number, other: number}} options
|
||||
* @returns {Object} the parameters with an externalized string
|
||||
*/
|
||||
export const groupedTextBuilder = ({
|
||||
reportType = '',
|
||||
status = '',
|
||||
critical = 0,
|
||||
high = 0,
|
||||
other = 0,
|
||||
} = {}) => {
|
||||
const total = critical + high + other;
|
||||
|
||||
return {
|
||||
countMessage: createCountMessage({ critical, high, other, total }),
|
||||
message: createStatusMessage({ reportType, status, total }),
|
||||
critical,
|
||||
high,
|
||||
other,
|
||||
status,
|
||||
total,
|
||||
};
|
||||
};
|
||||
|
|
@ -39,13 +39,3 @@ export const extractSecurityReportArtifacts = (reportTypes, jobs) => {
|
|||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const extractSecurityReportArtifactsFromPipeline = (reportTypes, data) => {
|
||||
const jobs = data.project?.pipeline?.jobs?.nodes ?? [];
|
||||
return extractSecurityReportArtifacts(reportTypes, jobs);
|
||||
};
|
||||
|
||||
export const extractSecurityReportArtifactsFromMergeRequest = (reportTypes, data) => {
|
||||
const jobs = data.project?.mergeRequest?.headPipeline?.jobs?.nodes ?? [];
|
||||
return extractSecurityReportArtifacts(reportTypes, jobs);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
feature_category :continuous_integration, [:ci_cd, :reset_registration_token]
|
||||
urgency :low, [:ci_cd, :reset_registration_token]
|
||||
feature_category :service_ping, [:usage_data, :service_usage_data]
|
||||
feature_category :integrations, [:integrations]
|
||||
feature_category :integrations, [:integrations, :slack_app_manifest_share, :slack_app_manifest_download]
|
||||
feature_category :pages, [:lets_encrypt_terms_of_service]
|
||||
feature_category :error_tracking, [:reset_error_tracking_access_token]
|
||||
|
||||
|
|
@ -114,6 +114,14 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
redirect_to ::Gitlab::LetsEncrypt.terms_of_service_url
|
||||
end
|
||||
|
||||
def slack_app_manifest_share
|
||||
redirect_to Slack::Manifest.share_url
|
||||
end
|
||||
|
||||
def slack_app_manifest_download
|
||||
send_data Slack::Manifest.to_json, type: :json, disposition: 'attachment', filename: 'slack_manifest.json'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_application_setting
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ module Emails
|
|||
to: @recipient.notification_email_for(@project.group),
|
||||
subject: subject("#{@issue.title} (##{@issue.iid})"),
|
||||
'X-GitLab-NotificationReason' => reason,
|
||||
'X-GitLab-ConfidentialIssue' => confidentiality
|
||||
'X-GitLab-ConfidentialIssue' => confidentiality.to_s
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,7 +15,14 @@ module Emails
|
|||
|
||||
@issue = @note.noteable
|
||||
@target_url = project_issue_url(*note_target_url_options)
|
||||
mail_answer_note_thread(@issue, @note, note_thread_options(reason))
|
||||
mail_answer_note_thread(
|
||||
@issue,
|
||||
@note,
|
||||
note_thread_options(
|
||||
reason,
|
||||
confidentiality: @issue.confidential?
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def note_merge_request_email(recipient_id, note_id, reason = nil)
|
||||
|
|
@ -62,13 +69,15 @@ module Emails
|
|||
{ anchor: "note_#{@note.id}" }
|
||||
end
|
||||
|
||||
def note_thread_options(reason)
|
||||
def note_thread_options(reason, confidentiality: nil)
|
||||
{
|
||||
from: sender(@note.author_id),
|
||||
to: @recipient.notification_email_for(@project&.group || @group),
|
||||
subject: subject("#{@note.noteable.title} (#{@note.noteable.reference_link_text})"),
|
||||
'X-GitLab-NotificationReason' => reason
|
||||
}
|
||||
}.tap do |options|
|
||||
options['X-GitLab-ConfidentialIssue'] = confidentiality.to_s unless confidentiality.nil?
|
||||
end
|
||||
end
|
||||
|
||||
def setup_note_mail(note_id, recipient_id)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class Milestone < ApplicationRecord
|
|||
include IidRoutes
|
||||
include UpdatedAtFilterable
|
||||
include EachBatch
|
||||
include Spammable
|
||||
|
||||
prepend_mod_with('Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
|
||||
|
|
@ -62,6 +63,9 @@ class Milestone < ApplicationRecord
|
|||
validate :parent_type_check
|
||||
validate :uniqueness_of_title, if: :title_changed?
|
||||
|
||||
attr_spammable :title, spam_title: true
|
||||
attr_spammable :description, spam_description: true
|
||||
|
||||
state_machine :state, initial: :active do
|
||||
event :close do
|
||||
transition active: :closed
|
||||
|
|
@ -255,6 +259,10 @@ class Milestone < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def check_for_spam?(*)
|
||||
spammable_attribute_changed? && parent.public?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def timebox_format_reference(format = :iid)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class UserCustomAttribute < ApplicationRecord
|
|||
ARKOSE_RISK_BAND = 'arkose_risk_band'
|
||||
AUTO_BANNED_BY_ABUSE_REPORT_ID = 'auto_banned_by_abuse_report_id'
|
||||
ALLOW_POSSIBLE_SPAM = 'allow_possible_spam'
|
||||
IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt'
|
||||
|
||||
class << self
|
||||
def upsert_custom_attributes(custom_attributes)
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PrometheusAlertEntity < Grape::Entity
|
||||
include RequestAwareEntity
|
||||
|
||||
expose :id
|
||||
expose :title
|
||||
expose :query
|
||||
expose :threshold
|
||||
expose :runbook_url
|
||||
|
||||
expose :operator do |prometheus_alert|
|
||||
prometheus_alert.computed_operator
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
alias_method :prometheus_alert, :object
|
||||
|
||||
def can_read_prometheus_alerts?
|
||||
can?(request.current_user, :read_prometheus_alerts, prometheus_alert.project)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PrometheusAlertSerializer < BaseSerializer
|
||||
entity PrometheusAlertEntity
|
||||
end
|
||||
|
|
@ -5,11 +5,19 @@ module Milestones
|
|||
def execute
|
||||
milestone = parent.milestones.new(params)
|
||||
|
||||
before_create(milestone)
|
||||
|
||||
if milestone.save && milestone.project_milestone?
|
||||
event_service.open_milestone(milestone, current_user)
|
||||
end
|
||||
|
||||
milestone
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def before_create(milestone)
|
||||
milestone.check_for_spam(user: current_user, action: :create)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,11 +13,22 @@ module Milestones
|
|||
end
|
||||
|
||||
if params.present?
|
||||
milestone.update(params.except(:state_event))
|
||||
milestone.assign_attributes(params.except(:state_event))
|
||||
end
|
||||
|
||||
if milestone.changed?
|
||||
before_update(milestone)
|
||||
end
|
||||
|
||||
milestone.save
|
||||
milestone
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def before_update(milestone)
|
||||
milestone.check_for_spam(user: current_user, action: :update)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,71 @@
|
|||
- return unless Gitlab.dev_or_test_env? || Gitlab.com?
|
||||
- gitlab_com = Gitlab.com?
|
||||
|
||||
- return unless Feature.enabled?(:slack_app_self_managed) || gitlab_com
|
||||
|
||||
- expanded = integration_expanded?('slack_app_')
|
||||
|
||||
%section.settings.as-slack.no-animate#js-slack-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Slack application')
|
||||
= s_('Integrations|GitLab for Slack app')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Slack integration allows you to interact with GitLab via slash commands in a chat window.')
|
||||
= s_('SlackIntegration|Configure your GitLab for Slack app.')
|
||||
= link_to(_('Learn more.'), help_page_path('user/admin_area/settings/slack_app'), target: '_blank', rel: 'noopener noreferrer')
|
||||
|
||||
.settings-content
|
||||
- unless gitlab_com
|
||||
%h5
|
||||
= s_('SlackIntegration|Step 1: Create your GitLab for Slack app')
|
||||
%p
|
||||
= s_('SlackIntegration|You must do this step only once.')
|
||||
%p
|
||||
= link_to slack_app_manifest_share_admin_application_settings_path, class: 'btn btn-default gl-button' do
|
||||
= image_tag 'illustrations/slack_logo.svg', class: 'gl-w-9! gl-h-9! gl-my-n4! gl-ml-n4 gl-mr-n2!'
|
||||
%strong.gl-button-text
|
||||
= s_("SlackIntegration|Create Slack app")
|
||||
%hr
|
||||
%h5
|
||||
= s_('SlackIntegration|Step 2: Configure the app settings')
|
||||
%p
|
||||
- tag_pair_slack_apps = tag_pair(link_to('', 'https://api.slack.com/apps', target: '_blank', rel: 'noopener noreferrer'), :link_start, :link_end)
|
||||
- tag_pair_strong = tag_pair(tag.strong, :strong_open, :strong_close)
|
||||
= safe_format(s_('SlackIntegration|Copy the %{link_start}settings%{link_end} from %{strong_open}%{settings_heading}%{strong_close} in your GitLab for Slack app.'), tag_pair_slack_apps, tag_pair_strong, settings_heading: 'App Credentials')
|
||||
= link_to(_('Learn more.'), help_page_path('user/admin_area/settings/slack_app', anchor: 'configure-the-settings'), target: '_blank', rel: 'noopener noreferrer')
|
||||
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-slack-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_errors(@application_setting) if expanded
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
= f.gitlab_ui_checkbox_component :slack_app_enabled, s_('ApplicationSettings|Enable Slack application'),
|
||||
help_text: s_('ApplicationSettings|This option is only available on GitLab.com')
|
||||
= f.gitlab_ui_checkbox_component :slack_app_enabled, s_('ApplicationSettings|Enable GitLab for Slack app')
|
||||
.form-group
|
||||
= f.label :slack_app_id, s_('SlackIntegration|Client ID'), class: 'label-bold'
|
||||
= f.text_field :slack_app_id, class: 'form-control gl-form-input'
|
||||
.form-group
|
||||
= f.label :slack_app_secret, s_('SlackIntegration|Client secret'), class: 'label-bold'
|
||||
= f.text_field :slack_app_secret, class: 'form-control gl-form-input'
|
||||
.form-text.text-muted
|
||||
= s_('SlackIntegration|Used for authenticating OAuth requests from the GitLab for Slack app.')
|
||||
.form-group
|
||||
= f.label :slack_app_signing_secret, s_('SlackIntegration|Signing secret'), class: 'label-bold'
|
||||
= f.text_field :slack_app_signing_secret, class: 'form-control gl-form-input'
|
||||
.form-text.text-muted
|
||||
= s_('SlackIntegration|Used for authenticating API requests from the GitLab for Slack app.')
|
||||
.form-group
|
||||
= f.label :slack_app_verification_token, s_('SlackIntegration|Verification token'), class: 'label-bold'
|
||||
= f.text_field :slack_app_verification_token, class: 'form-control gl-form-input'
|
||||
|
||||
.form-text.text-muted
|
||||
= s_('SlackIntegration|Used only for authenticating slash commands from the GitLab for Slack app. This method of authentication is deprecated by Slack.')
|
||||
= f.submit _('Save changes'), pajamas_button: true
|
||||
|
||||
- unless gitlab_com
|
||||
%hr
|
||||
%h5
|
||||
= s_('SlackIntegration|Update your Slack app')
|
||||
%p
|
||||
= s_('SlackIntegration|When GitLab releases new features for the GitLab for Slack app, you might have to manually update your copy to use the new features.')
|
||||
= link_to(_('Learn more.'), help_page_path('user/admin_area/settings/slack_app', anchor: 'update-the-gitlab-for-slack-app'), target: '_blank', rel: 'noopener noreferrer')
|
||||
%p
|
||||
= render Pajamas::ButtonComponent.new(href: slack_app_manifest_download_admin_application_settings_path, icon: 'download') do
|
||||
= s_("SlackIntegration|Download latest manifest file")
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@
|
|||
= render 'admin/application_settings/plantuml'
|
||||
= render 'admin/application_settings/diagramsnet'
|
||||
= render 'admin/application_settings/sourcegraph'
|
||||
= render_if_exists 'admin/application_settings/slack'
|
||||
-# this partial is from JiHu, see details in https://jihulab.com/gitlab-cn/gitlab/-/merge_requests/417
|
||||
= render_if_exists 'admin/application_settings/dingtalk_integration'
|
||||
-# this partial is from JiHu, see details in https://jihulab.com/gitlab-cn/gitlab/-/merge_requests/640
|
||||
|
|
@ -109,4 +108,5 @@
|
|||
= render 'admin/application_settings/floc'
|
||||
= render_if_exists 'admin/application_settings/add_license'
|
||||
= render 'admin/application_settings/jira_connect'
|
||||
= render 'admin/application_settings/slack'
|
||||
= render_if_exists 'admin/application_settings/ai_access'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: dynamically_compute_deployment_approval
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120699
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/411370
|
||||
milestone: '16.2'
|
||||
type: development
|
||||
group: group::environments
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: key_set_optimizer_ignored_columns
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125462
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/417515
|
||||
milestone: '16.2'
|
||||
type: development
|
||||
group: group::project management
|
||||
default_enabled: false
|
||||
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416305
|
|||
milestone: '16.2'
|
||||
type: development
|
||||
group: group::environments
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: slack_app_self_managed
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124823
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416448
|
||||
milestone: '16.2'
|
||||
type: development
|
||||
group: group::import and integrate
|
||||
default_enabled: false
|
||||
|
|
@ -155,7 +155,8 @@ namespace :admin do
|
|||
put :clear_repository_check_states
|
||||
match :general, :integrations, :repository, :ci_cd, :reporting, :metrics_and_profiling, :network, :preferences, via: [:get, :patch]
|
||||
get :lets_encrypt_terms_of_service
|
||||
|
||||
get :slack_app_manifest_download, format: :json
|
||||
get :slack_app_manifest_share
|
||||
get :service_usage_data
|
||||
|
||||
resource :appearances, only: [:show, :create, :update], path: 'appearance', module: 'application_settings' do
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ The first row contains the headers, which are listed in the following table alon
|
|||
|
||||
Successful sign-in events are the only audit events available at all tiers. To see successful sign-in events:
|
||||
|
||||
1. Select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Edit profile > Authentication log**.
|
||||
|
||||
After upgrading to a paid tier, you can also see successful sign-in events on audit event pages.
|
||||
|
|
|
|||
|
|
@ -1215,7 +1215,7 @@ Input type: `AuditEventsStreamingDestinationEventsAddInput`
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationauditeventsstreamingdestinationeventsaddclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationauditeventsstreamingdestinationeventsadderrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationauditeventsstreamingdestinationeventsaddeventtypefilters"></a>`eventTypeFilters` | [`[String!]`](#string) | Event type filters present. |
|
||||
| <a id="mutationauditeventsstreamingdestinationeventsaddeventtypefilters"></a>`eventTypeFilters` | [`[String!]`](#string) | List of event type filters for the audit event external destination. |
|
||||
|
||||
### `Mutation.auditEventsStreamingDestinationEventsRemove`
|
||||
|
||||
|
|
@ -1236,6 +1236,26 @@ Input type: `AuditEventsStreamingDestinationEventsRemoveInput`
|
|||
| <a id="mutationauditeventsstreamingdestinationeventsremoveclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationauditeventsstreamingdestinationeventsremoveerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
|
||||
### `Mutation.auditEventsStreamingDestinationInstanceEventsAdd`
|
||||
|
||||
Input type: `AuditEventsStreamingDestinationInstanceEventsAddInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationauditeventsstreamingdestinationinstanceeventsaddclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationauditeventsstreamingdestinationinstanceeventsadddestinationid"></a>`destinationId` | [`AuditEventsInstanceExternalAuditEventDestinationID!`](#auditeventsinstanceexternalauditeventdestinationid) | Destination id. |
|
||||
| <a id="mutationauditeventsstreamingdestinationinstanceeventsaddeventtypefilters"></a>`eventTypeFilters` | [`[String!]!`](#string) | List of event type filters to add for streaming. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationauditeventsstreamingdestinationinstanceeventsaddclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationauditeventsstreamingdestinationinstanceeventsadderrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationauditeventsstreamingdestinationinstanceeventsaddeventtypefilters"></a>`eventTypeFilters` | [`[String!]`](#string) | List of event type filters for the audit event external destination. |
|
||||
|
||||
### `Mutation.auditEventsStreamingHeadersCreate`
|
||||
|
||||
Input type: `AuditEventsStreamingHeadersCreateInput`
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
status: proposed
|
||||
creation-date: "2023-03-07"
|
||||
authors: [ "@username" ]
|
||||
coach: "@username"
|
||||
approvers: [ "@product-manager", "@engineering-manager" ]
|
||||
authors: [ "@ajwalker" ]
|
||||
coach: [ "@ayufan" ]
|
||||
approvers: [ "@DarrenEastman", "@engineering-manager" ]
|
||||
owning-stage: "~devops::<stage>"
|
||||
participating-stages: []
|
||||
---
|
||||
|
|
|
|||
|
|
@ -240,9 +240,9 @@ To [Create a new group](../group/index.md#create-a-group) select **New group**.
|
|||
|
||||
[Topics](../project/working_with_projects.md#explore-topics) are used to categorize and find similar projects.
|
||||
|
||||
You can administer all topics in the GitLab instance from the Admin Area's Topics page.
|
||||
### View all topics
|
||||
|
||||
To access the Topics page:
|
||||
To view all topics in the GitLab instance:
|
||||
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **Admin Area**.
|
||||
|
|
@ -250,22 +250,71 @@ To access the Topics page:
|
|||
|
||||
For each topic, the page displays its name and the number of projects labeled with the topic.
|
||||
|
||||
To create a new topic, select **New topic**.
|
||||
### Search for topics
|
||||
|
||||
To edit a topic, select **Edit** in that topic's row.
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **Admin Area**.
|
||||
1. Select **Overview > Topics**.
|
||||
1. In the search box, enter your search criteria.
|
||||
The topic search is case-insensitive and applies partial matching.
|
||||
|
||||
To remove a topic, select **Remove** in that topic's row.
|
||||
### Create a topic
|
||||
|
||||
To remove a topic and move all assigned projects to another topic, select **Merge topics**.
|
||||
To create a topic:
|
||||
|
||||
To search for topics by name, enter your criteria in the search box. The topic search is case
|
||||
insensitive and applies partial matching.
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **Admin Area**.
|
||||
1. Select **Overview > Topics**.
|
||||
1. Select **New topic**.
|
||||
1. Enter the **Topic slug (name)** and **Topic title**.
|
||||
1. Optional. Enter a **Description** and add a **Topic avatar**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
The created topics are displayed on the **Explore topics** page.
|
||||
|
||||
NOTE:
|
||||
The assigned topics are visible only to everyone with access to the project,
|
||||
but everyone can see which topics exist on the GitLab instance.
|
||||
Do not include sensitive information in the name of a topic.
|
||||
|
||||
### Edit a topic
|
||||
|
||||
You can edit a topic's name, title, description, and avatar at any time.
|
||||
To edit a topic:
|
||||
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **Admin Area**.
|
||||
1. Select **Overview > Topics**.
|
||||
1. Select **Edit** in that topic's row.
|
||||
1. Edit the topic slug (name), title, description, or avatar.
|
||||
1. Select **Save changes**.
|
||||
|
||||
### Remove a topic
|
||||
|
||||
If you no longer need a topic, you can permanently remove it.
|
||||
To remove a topic:
|
||||
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **Admin Area**.
|
||||
1. Select **Overview > Topics**.
|
||||
1. To remove a topic, select **Remove** in that topic's row.
|
||||
|
||||
### Merge topics
|
||||
|
||||
You can move all projects assigned to a topic to another topic.
|
||||
The source topic is then permanently deleted.
|
||||
After a merged topic is deleted, you cannot restore it.
|
||||
|
||||
To merge topics:
|
||||
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **Admin Area**.
|
||||
1. Select **Overview > Topics**.
|
||||
1. Select **Merge topics**.
|
||||
1. From the **Source topic** dropdown list, select the topic you want to merge and remove.
|
||||
1. From the **Target topic** dropdown list, select the topic you want to merge the source topic into.
|
||||
1. Select **Merge**.
|
||||
|
||||
## Administering Gitaly servers
|
||||
|
||||
You can list all Gitaly servers in the GitLab instance from the Admin Area's **Gitaly Servers**
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ The **General** settings contain:
|
|||
Set max session time for web terminal.
|
||||
- [FLoC](floc.md) - Enable or disable
|
||||
[Federated Learning of Cohorts (FLoC)](https://en.wikipedia.org/wiki/Federated_Learning_of_Cohorts) tracking.
|
||||
- [GitLab for Slack app](slack_app.md) - Enable and configure the GitLab for Slack app.
|
||||
|
||||
### CI/CD
|
||||
|
||||
|
|
@ -91,10 +92,6 @@ The **Integrations** settings contain:
|
|||
to receive invite email bounce events from Mailgun, if it is your email provider.
|
||||
- [PlantUML](../../../administration/integration/plantuml.md) - Allow rendering of PlantUML
|
||||
diagrams in documents.
|
||||
- [Slack application](../../../user/project/integrations/gitlab_slack_application.md) -
|
||||
Slack integration allows you to interact with GitLab via slash commands in a chat window.
|
||||
This option is only available on GitLab.com, though it may be
|
||||
[available for self-managed instances in the future](https://gitlab.com/gitlab-org/gitlab/-/issues/28164).
|
||||
- [Customer experience improvement and third-party offers](third_party_offers.md) -
|
||||
Control the display of customer experience improvement content and third-party offers.
|
||||
- [Snowplow](../../../development/internal_analytics/snowplow/index.md) - Configure the Snowplow integration.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
---
|
||||
stage: Manage
|
||||
group: Import and Integrate
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# GitLab for Slack app administration **(FREE SELF)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/358872) for self-managed instances in GitLab 16.2 [with a flag](../../../administration/feature_flags.md) named `slack_app_self_managed`. 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 `slack_app_self_managed`. On GitLab.com, this feature is available.
|
||||
|
||||
This page contains information about configuring the GitLab for Slack app on self-managed instances. For user documentation, see [GitLab for Slack app](../../../user/project/integrations/gitlab_slack_application.md).
|
||||
|
||||
The GitLab for Slack app distributed through the Slack app directory only works with GitLab.com.
|
||||
On self-managed GitLab, you can create your own copy of the GitLab for Slack app from a [Slack app manifest file](https://api.slack.com/reference/manifests#creating_apps) and configure your instance.
|
||||
|
||||
The app is a private one-time copy installed in your Slack workspace only and not distributed through the Slack app directory. To have the [GitLab for Slack app](../../../user/project/integrations/gitlab_slack_application.md) on your self-managed instance, you must first enable the integration.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be at least a [workspace administrator](https://slack.com/help/articles/360018112273-Types-of-roles-in-Slack) in Slack.
|
||||
- You must be [signed in](https://slack.com/signin) to your Slack workspace.
|
||||
|
||||
## Create a GitLab for Slack app
|
||||
|
||||
To create a GitLab for Slack app:
|
||||
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **Admin Area**.
|
||||
1. On the left sidebar, select **Settings > General**.
|
||||
1. Expand **GitLab for Slack app**.
|
||||
1. Select **Create Slack app**.
|
||||
|
||||
You are then redirected to Slack for the next steps. In the modal that appears:
|
||||
|
||||
1. Select the Slack workspace to create the app in, then select **Next**.
|
||||
1. Slack displays a summary of the app for review. To view the complete manifest, select **Edit Configurations**. To go back to the review summary, select **Next**.
|
||||
1. Select **Create**.
|
||||
1. Close the modal by selecting **Got it**.
|
||||
1. Select **Install to Workspace**.
|
||||
|
||||
## Configure the settings
|
||||
|
||||
After you've [created a GitLab for Slack app](#create-a-gitlab-for-slack-app), you can configure the settings in GitLab:
|
||||
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **Admin Area**.
|
||||
1. On the left sidebar, select **Settings > General**.
|
||||
1. Expand **GitLab for Slack app**.
|
||||
1. Select the **Enable GitLab for Slack app** checkbox.
|
||||
1. Enter the details of your GitLab for Slack app:
|
||||
1. Go to [Slack API](https://api.slack.com/apps).
|
||||
1. Select **GitLab (\<your host name\>)**. You can search to find it.
|
||||
1. Scroll to **App Credentials**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
### Test your configuration
|
||||
|
||||
To test your GitLab for Slack app configuration:
|
||||
|
||||
1. Enter the `/gitlab help` slash command into a channel in your Slack workspace.
|
||||
1. Press <kbd>Enter</kbd>.
|
||||
|
||||
You should see a list of available Slash commands.
|
||||
|
||||
To use Slash commands for a project, configure the [GitLab for Slack app](../../../user/project/integrations/gitlab_slack_application.md) for the project.
|
||||
|
||||
## Update the GitLab for Slack app
|
||||
|
||||
When GitLab releases new features for the GitLab for Slack app, you might have to manually update your copy to use the new features.
|
||||
|
||||
To update your copy of the GitLab for Slack app:
|
||||
|
||||
- In GitLab:
|
||||
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **Admin Area**.
|
||||
1. On the left sidebar, select **Settings > General**.
|
||||
1. Expand **GitLab for Slack app**.
|
||||
1. Select **Download latest manifest file** to download `slack_manifest.json`.
|
||||
|
||||
- In Slack:
|
||||
|
||||
1. Go to [Slack API](https://api.slack.com/apps).
|
||||
1. Select **GitLab (\<your host name\>)**. You can search to find it.
|
||||
1. On the left sidebar, select **App Manifest**.
|
||||
1. Select the **JSON** tab to switch to a JSON view of the manifest.
|
||||
1. Copy the contents of the `slack_manifest.json` file you've downloaded from GitLab.
|
||||
1. Paste the contents into the JSON viewer to replace any existing contents.
|
||||
1. Select **Save Changes**.
|
||||
|
||||
## Connectivity requirements
|
||||
|
||||
To enable the GitLab for Slack app functionality, your network must allow inbound and outbound connections between GitLab and Slack.
|
||||
|
||||
- For [Slack notifications](../../../user/project/integrations/gitlab_slack_application.md#slack-notifications), the GitLab instance must be able to send requests to `https://slack.com`.
|
||||
- For [Slash commands](../../../user/project/integrations/gitlab_slack_application.md#slash-commands) and other features, the GitLab instance must be able to receive requests from `https://slack.com`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Slash commands return `/gitlab failed with the error "dispatch_failed"` in Slack
|
||||
|
||||
Slash commands might return `/gitlab failed with the error "dispatch_failed"` in Slack. To resolve this issue, ensure:
|
||||
|
||||
- The GitLab for Slack app is properly [configured](#configure-the-settings), and the **Enable GitLab for Slack app** checkbox is selected.
|
||||
- Your GitLab instance [allows requests to and from Slack](#connectivity-requirements).
|
||||
|
|
@ -250,7 +250,7 @@ Your primary email is used by default.
|
|||
|
||||
To change your commit email:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. In the **Commit email** dropdown list, select an email address.
|
||||
1. Select **Update profile settings**.
|
||||
|
|
@ -261,7 +261,7 @@ Your primary email is the default email address for your login, commit email, an
|
|||
|
||||
To change your primary email:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. In the **Email** field, enter your new email address.
|
||||
1. Select **Update profile settings**.
|
||||
|
|
@ -271,7 +271,7 @@ To change your primary email:
|
|||
|
||||
You can select one of your [configured email addresses](#add-emails-to-your-user-profile) to be displayed on your public profile:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. In the **Public email** field, select one of the available email addresses.
|
||||
1. Select **Update profile settings**.
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ anyone else.
|
|||
|
||||
To edit your notification settings:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Preferences**.
|
||||
1. On the left sidebar, select **Notifications**.
|
||||
1. Edit the desired global, group, or project notification settings.
|
||||
|
|
@ -99,7 +99,7 @@ You can select a notification level and email address for each group.
|
|||
|
||||
To select a notification level for a group, use either of these methods:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Preferences**.
|
||||
1. On the left sidebar, select **Notifications**.
|
||||
1. Locate the project in the **Groups** section.
|
||||
|
|
@ -118,7 +118,7 @@ Or:
|
|||
You can select an email address to receive notifications for each group you belong to.
|
||||
You can use group notifications, for example, if you work freelance, and want to keep email about clients' projects separate.
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Preferences**.
|
||||
1. On the left sidebar, select **Notifications**.
|
||||
1. Locate the project in the **Groups** section.
|
||||
|
|
@ -130,7 +130,7 @@ To help you stay up to date, you can select a notification level for each projec
|
|||
|
||||
To select a notification level for a project, use either of these methods:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Preferences**.
|
||||
1. On the left sidebar, select **Notifications**.
|
||||
1. Locate the project in the **Projects** section.
|
||||
|
|
@ -152,7 +152,7 @@ These emails are enabled by default.
|
|||
|
||||
To opt out:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Preferences**.
|
||||
1. On the left sidebar, select **Notifications**.
|
||||
1. Clear the **Receive product marketing emails** checkbox.
|
||||
|
|
@ -335,7 +335,7 @@ The participants are:
|
|||
|
||||
If you no longer wish to receive any email notifications:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Preferences**.
|
||||
1. On the left sidebar, select **Notifications**.
|
||||
1. Clear the **Receive product marketing emails** checkbox.
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ Use impersonation tokens to automate authentication as a specific user.
|
|||
|
||||
You can create as many personal access tokens as you like.
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. On the left sidebar, select **Access Tokens**.
|
||||
1. Enter a name and expiry date for the token.
|
||||
|
|
@ -79,7 +79,7 @@ for guidance on managing personal access tokens (for example, setting a short ex
|
|||
|
||||
At any time, you can revoke a personal access token.
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. On the left sidebar, select **Access Tokens**.
|
||||
1. In the **Active personal access tokens** area, next to the key, select **Revoke**.
|
||||
|
|
@ -96,7 +96,7 @@ Token usage information is updated every 10 minutes. GitLab considers a token us
|
|||
|
||||
To view the last time a token was used:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. On the left sidebar, select **Access Tokens**.
|
||||
1. In the **Active personal access tokens** area, next to the key, view the **Last Used** date.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ of GitLab to their liking.
|
|||
|
||||
To navigate to your profile's preferences:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Preferences**.
|
||||
|
||||
## Navigation theme
|
||||
|
|
|
|||
|
|
@ -4,53 +4,46 @@ group: Import and Integrate
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# GitLab for Slack app **(FREE SAAS)**
|
||||
# GitLab for Slack app **(FREE)**
|
||||
|
||||
NOTE:
|
||||
This feature is only configurable on GitLab.com.
|
||||
For self-managed GitLab instances, use
|
||||
[Slack slash commands](slack_slash_commands.md) and [Slack notifications](slack.md) instead.
|
||||
For more information about our plans to make this feature configurable for all GitLab installations,
|
||||
see [epic 1211](https://gitlab.com/groups/gitlab-org/-/epics/1211).
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/358872) for self-managed instances in GitLab 16.2 [with a flag](../../../administration/feature_flags.md) named `slack_app_self_managed`. Disabled by default.
|
||||
|
||||
Slack provides a native application that you can enable with your project's integrations on GitLab.com. GitLab
|
||||
links your Slack user with your GitLab user so that commands you run in Slack are run by the linked GitLab user on
|
||||
GitLab.com.
|
||||
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 `slack_app_self_managed`. On GitLab.com, this feature is available.
|
||||
|
||||
The GitLab for Slack app is a native Slack app that provides [slash commands](#slash-commands) and [notifications](#slack-notifications) in your Slack workspace. GitLab links your Slack user with your GitLab user so that commands
|
||||
you run in Slack are run by the linked GitLab user on GitLab.com.
|
||||
|
||||
## Installation
|
||||
|
||||
Prerequisite:
|
||||
|
||||
- You must have the [appropriate permissions to add apps to your Slack workspace](https://slack.com/help/articles/202035138-Add-apps-to-your-Slack-workspace).
|
||||
|
||||
In GitLab 15.0 and later, the GitLab for Slack app uses
|
||||
[granular permissions](https://medium.com/slack-developer-blog/more-precision-less-restrictions-a3550006f9c3).
|
||||
Although functionality has not changed, you should [reinstall the app](#update-the-gitlab-for-slack-app).
|
||||
|
||||
### Through the Slack App Directory
|
||||
### Through project integration settings
|
||||
|
||||
To enable the GitLab for Slack app for your workspace,
|
||||
install the [GitLab application](https://slack-platform.slack.com/apps/A676ADMV5-gitlab)
|
||||
from the [Slack App Directory](https://slack.com/apps).
|
||||
To install the GitLab for Slack app integration:
|
||||
|
||||
On the [GitLab for Slack app landing page](https://gitlab.com/-/profile/slack/edit),
|
||||
you can select a GitLab project to link with your Slack workspace.
|
||||
|
||||
### Through GitLab project settings
|
||||
|
||||
Alternatively, you can configure the GitLab for Slack app with your project's
|
||||
integration settings.
|
||||
|
||||
You must have the appropriate permissions for your Slack
|
||||
workspace to be able to install a new application. See
|
||||
[Add apps to your Slack workspace](https://slack.com/help/articles/202035138-Add-apps-to-your-Slack-workspace).
|
||||
|
||||
To enable the GitLab integration for your Slack workspace:
|
||||
|
||||
1. Go to your project's **Settings > Integration > GitLab for Slack app** (only
|
||||
visible on GitLab.com).
|
||||
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
|
||||
1. Select **Settings > Integrations**.
|
||||
1. Select **GitLab for Slack app**. On self-managed GitLab, an administrator must first [enable the integration](../../admin_area/settings/slack_app.md).
|
||||
1. Select **Install GitLab for Slack app**.
|
||||
1. Select **Allow** on Slack's confirmation screen.
|
||||
1. On the Slack confirmation page, select **Allow**.
|
||||
|
||||
To update the app in your Slack workspace to the latest version,
|
||||
you can also select **Reinstall GitLab for Slack app**.
|
||||
|
||||
### Through the Slack app directory **(FREE SAAS)**
|
||||
|
||||
On GitLab.com, you can install the GitLab for Slack app
|
||||
from the [Slack app directory](https://slack-platform.slack.com/apps/A676ADMV5-gitlab).
|
||||
On the [GitLab for Slack app page](https://gitlab.com/-/profile/slack/edit),
|
||||
select a GitLab project to link with your Slack workspace.
|
||||
|
||||
## Slash commands
|
||||
|
||||
You can use slash commands to run common GitLab operations. Replace `<project>` with a project full path.
|
||||
|
|
@ -91,9 +84,9 @@ The command returns an error if no matching action is found.
|
|||
|
||||
By default, slash commands expect a project full path. To create a shorter project alias in the GitLab for Slack app:
|
||||
|
||||
1. Go to your project's home page.
|
||||
1. Go to **Settings > Integrations** (only visible on GitLab.com).
|
||||
1. On the **Integrations** page, select **GitLab for Slack app**.
|
||||
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
|
||||
1. Select **Settings > Integrations**.
|
||||
1. Select **GitLab for Slack app**. On self-managed GitLab, an administrator must first [enable the integration](../../admin_area/settings/slack_app.md).
|
||||
1. The current **Project Alias**, if any, is displayed. To edit this value,
|
||||
select **Edit**.
|
||||
1. Enter your desired alias, and select **Save changes**.
|
||||
|
|
@ -174,6 +167,12 @@ The GitLab for Slack app is updated for all projects that use the integration.
|
|||
|
||||
Alternatively, you can [configure a new Slack integration](https://about.gitlab.com/solutions/slack/).
|
||||
|
||||
### GitLab for Slack app does not appear in the list of integrations
|
||||
|
||||
The GitLab for Slack app might not appear in the list of integrations. To have the GitLab for Slack app on your self-managed instance, an administrator must first [enable the integration](../../admin_area/settings/slack_app.md). On GitLab.com, the GitLab for Slack app is available by default.
|
||||
|
||||
The GitLab for Slack app is enabled at the project level only. Support for the app at the group and instance levels is proposed in [issue 391526](https://gitlab.com/gitlab-org/gitlab/-/issues/391526).
|
||||
|
||||
### Project or alias not found
|
||||
|
||||
Some Slack commands must have a project full path or alias and fail with the following error
|
||||
|
|
@ -187,7 +186,11 @@ As a workaround, ensure:
|
|||
|
||||
- The project full path is correct.
|
||||
- If using a [project alias](#create-a-project-alias-for-slash-commands), the alias is correct.
|
||||
- The GitLab for Slack app integration is [enabled for the project](#through-gitlab-project-settings).
|
||||
- The GitLab for Slack app integration is [enabled for the project](#through-project-integration-settings).
|
||||
|
||||
### Slash commands return `/gitlab failed with the error "dispatch_failed"` in Slack
|
||||
|
||||
Slash commands might return `/gitlab failed with the error "dispatch_failed"` in Slack. To resolve this issue, ensure an administrator has properly configured the [GitLab for Slack app settings](../../admin_area/settings/slack_app.md) on your self-managed instance.
|
||||
|
||||
### Notifications are not received to a channel
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ clear your browser's cookies or change this behavior again.
|
|||
|
||||
To view one file at a time for all of your merge requests:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Preferences**.
|
||||
1. Scroll to the **Behavior** section and select the **Show one file at a time on merge request's Changes tab** checkbox.
|
||||
1. Select **Save changes**.
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ Each individual user must also choose to enable Code Suggestions.
|
|||
Each user can enable Code Suggestions for themselves:
|
||||
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. On the left sidebar, select **Preferences**.
|
||||
1. Select **Preferences**.
|
||||
1. In the **Code Suggestions** section, select the **Enable Code Suggestions** checkbox.
|
||||
1. Select **Save changes**.
|
||||
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ If you don't already have a GPG key, create one:
|
|||
To add a GPG key to your user settings:
|
||||
|
||||
1. Sign in to GitLab.
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. Select **GPG Keys** (**{key}**).
|
||||
1. In **Key**, paste your _public_ key.
|
||||
|
|
@ -253,7 +253,7 @@ If a GPG key becomes compromised, revoke it. Revoking a key changes both future
|
|||
|
||||
To revoke a GPG key:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. Select **GPG Keys** (**{key}**).
|
||||
1. Select **Revoke** next to the GPG key you want to delete.
|
||||
|
|
@ -268,7 +268,7 @@ When you remove a GPG key from your GitLab account:
|
|||
|
||||
To remove a GPG key from your account:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. Select **GPG Keys** (**{key}**).
|
||||
1. Select **Remove** (**{remove}**) next to the GPG key you want to delete.
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ If an SSH key becomes compromised, revoke it. Revoking a key changes both future
|
|||
|
||||
To revoke an SSH key:
|
||||
|
||||
1. In the upper-right corner, select your avatar.
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. On the left sidebar, select (**{key}**) **SSH Keys**.
|
||||
1. Select **Revoke** next to the SSH key you want to delete.
|
||||
|
|
|
|||
|
|
@ -9,40 +9,46 @@ info: "To determine the technical writer assigned to the Stage/Group associated
|
|||
Most work in GitLab is done in a [project](../../user/project/index.md). Files and
|
||||
code are saved in projects, and most features are in the scope of projects.
|
||||
|
||||
## View projects
|
||||
## View all projects for the instance
|
||||
|
||||
To view all your projects:
|
||||
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **View all your projects**.
|
||||
|
||||
To browse all projects you can access:
|
||||
To view all projects for the GitLab instance:
|
||||
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **Explore**.
|
||||
|
||||
### Who can view the Projects page
|
||||
On the left sidebar, **Projects** is selected. On the right, the list shows
|
||||
all projects for the instance.
|
||||
|
||||
When you select a project, the project landing page shows the project contents.
|
||||
If you are not authenticated, then the list shows public projects only.
|
||||
|
||||
For public projects, and members of internal and private projects
|
||||
with [permissions to view the project's code](../permissions.md#project-members-permissions),
|
||||
the project landing page shows:
|
||||
## View projects you are a member of
|
||||
|
||||
- A [`README` or index file](repository/index.md#readme-and-index-files).
|
||||
- A list of directories in the project's repository.
|
||||
To view projects you are a member of:
|
||||
|
||||
For users without permission to view the project's code, the landing page shows:
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **Your work**.
|
||||
|
||||
- The wiki homepage.
|
||||
- The list of issues in the project.
|
||||
On the left sidebar, **Projects** is selected. On the list, on the **Yours** tab,
|
||||
all the projects you are a member of are displayed.
|
||||
|
||||
### Access a project page with the project ID
|
||||
## View personal projects
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53671) in GitLab 11.8.
|
||||
Personal projects are projects created under your personal namespace.
|
||||
|
||||
To access a project from the GitLab UI using the project ID,
|
||||
visit the `/projects/:id` URL in your browser or other tool accessing the project.
|
||||
For example, if you create an account with the username `alex`, and create a project
|
||||
called `my-project` under your username, the project is created at `https://gitlab.example.com/alex/my-project`.
|
||||
|
||||
To view your personal projects:
|
||||
|
||||
1. On the left sidebar, select your avatar and then your username.
|
||||
1. On the left sidebar, select **Personal projects**.
|
||||
|
||||
## View starred projects
|
||||
|
||||
To view projects you have [starred](#star-a-project):
|
||||
|
||||
1. On the left sidebar, select your avatar and then your username.
|
||||
1. On the left sidebar, select **Starred projects**.
|
||||
|
||||
## Organizing projects with topics
|
||||
|
||||
|
|
@ -129,32 +135,6 @@ To add a star to a project:
|
|||
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
|
||||
1. In the upper-right corner of the page, select **Star**.
|
||||
|
||||
## View starred projects
|
||||
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **View all your projects**.
|
||||
1. Select the **Starred** tab.
|
||||
1. GitLab displays information about your starred projects, including:
|
||||
|
||||
- Project description, including name, description, and icon.
|
||||
- Number of times this project has been starred.
|
||||
- Number of times this project has been forked.
|
||||
- Number of open merge requests.
|
||||
- Number of open issues.
|
||||
|
||||
## View personal projects
|
||||
|
||||
Personal projects are projects created under your personal namespace.
|
||||
|
||||
For example, if you create an account with the username `alex`, and create a project
|
||||
called `my-project` under your username, the project is created at `https://gitlab.example.com/alex/my-project`.
|
||||
|
||||
To view your personal projects:
|
||||
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **View all your projects**.
|
||||
1. In the **Yours** tab, select **Personal**.
|
||||
|
||||
## Delete a project
|
||||
|
||||
After you delete a project:
|
||||
|
|
@ -248,6 +228,29 @@ Prerequisite:
|
|||
1. Use the toggle by each feature you want to turn on or off, or change access for.
|
||||
1. Select **Save changes**.
|
||||
|
||||
## Access the Project overview page by using the project ID
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53671) in GitLab 11.8.
|
||||
|
||||
To access a project from the GitLab UI by using the project ID,
|
||||
put the `/projects/:id` URL in your browser or other tool you use to access the project.
|
||||
|
||||
## Who can view the Project overview page
|
||||
|
||||
When you select a project, the **Project overview** page shows the project contents.
|
||||
|
||||
For public projects, and members of internal and private projects
|
||||
with [permissions to view the project's code](../permissions.md#project-members-permissions),
|
||||
the project landing page shows:
|
||||
|
||||
- A [`README` or index file](repository/index.md#readme-and-index-files).
|
||||
- A list of directories in the project's repository.
|
||||
|
||||
For users without permission to view the project's code, the landing page shows:
|
||||
|
||||
- The wiki homepage.
|
||||
- The list of issues in the project.
|
||||
|
||||
## Leave a project
|
||||
|
||||
When you leave a project:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Checks
|
||||
module FileSizeCheck
|
||||
class AnyOversizedBlob
|
||||
def initialize(project:, changes:, file_size_limit_megabytes:)
|
||||
@project = project
|
||||
@newrevs = changes.pluck(:newrev).compact # rubocop:disable CodeReuse/ActiveRecord just plucking from an array
|
||||
@file_size_limit_megabytes = file_size_limit_megabytes
|
||||
end
|
||||
attr_reader :project, :newrevs, :file_size_limit_megabytes
|
||||
|
||||
def find!(timeout: nil)
|
||||
blobs = project.repository.new_blobs(newrevs, dynamic_timeout: timeout)
|
||||
|
||||
blobs.find do |blob|
|
||||
::Gitlab::Utils.bytes_to_megabytes(blob.size) > file_size_limit_megabytes
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -67,7 +67,11 @@ module Gitlab
|
|||
.select(finder_strategy.final_projections)
|
||||
.where("count <> 0") # filter out the initializer row
|
||||
|
||||
model.from(q.arel.as(table_name))
|
||||
if Feature.enabled?(:key_set_optimizer_ignored_columns)
|
||||
model.select(Arel.star).from(q.arel.as(table_name))
|
||||
else
|
||||
model.from(q.arel.as(table_name))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ module Gitlab
|
|||
@finder_query = finder_query
|
||||
@order_by_columns = order_by_columns
|
||||
@table_name = model.table_name
|
||||
@model = model
|
||||
end
|
||||
|
||||
def initializer_columns
|
||||
|
|
@ -30,7 +31,11 @@ module Gitlab
|
|||
end
|
||||
|
||||
def final_projections
|
||||
["(#{RECORDS_COLUMN}).*"]
|
||||
if @model.default_select_columns.is_a?(Array) && Feature.enabled?(:key_set_optimizer_ignored_columns)
|
||||
@model.default_select_columns.map { |column| "(#{RECORDS_COLUMN}).#{column.name}" }
|
||||
else
|
||||
["(#{RECORDS_COLUMN}).*"]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -246,7 +246,12 @@ module Gitlab
|
|||
scopes = where_values.map do |where_value|
|
||||
scope.dup.where(where_value).reorder(self) # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
scope.model.from_union(scopes, remove_duplicates: false, remove_order: false)
|
||||
|
||||
if Feature.enabled?(:key_set_optimizer_ignored_columns)
|
||||
scope.model.select(scope.select_values).from_union(scopes, remove_duplicates: false, remove_order: false)
|
||||
else
|
||||
scope.model.from_union(scopes, remove_duplicates: false, remove_order: false)
|
||||
end
|
||||
end
|
||||
|
||||
def to_sql_literal(column_definitions)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Slack
|
||||
module Manifest
|
||||
class << self
|
||||
delegate :to_json, to: :to_h
|
||||
|
||||
def share_url
|
||||
"https://api.slack.com/apps?new_app=1&manifest_json=#{ERB::Util.url_encode(to_json)}"
|
||||
end
|
||||
|
||||
def to_h
|
||||
{
|
||||
display_information: display_information,
|
||||
features: features,
|
||||
oauth_config: oauth_config,
|
||||
settings: settings
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def display_information
|
||||
{
|
||||
name: "GitLab (#{Gitlab.config.gitlab.host.first(26)})",
|
||||
description: s_('SlackIntegration|Interact with GitLab without leaving your Slack workspace!'),
|
||||
background_color: '#171321',
|
||||
# Each element in this array will become a paragraph joined with `\r\n\r\n'.
|
||||
long_description: [
|
||||
format(
|
||||
s_(
|
||||
'SlackIntegration|Generated for %{host} by GitLab %{version}.'
|
||||
),
|
||||
host: Gitlab.config.gitlab.host,
|
||||
version: Gitlab::VERSION
|
||||
),
|
||||
s_(
|
||||
'SlackIntegration|- *Notifications:* Get notifications to your team\'s Slack channel about events ' \
|
||||
'happening inside your GitLab projects.'
|
||||
),
|
||||
format(
|
||||
s_(
|
||||
'SlackIntegration|- *Slash commands:* Quickly open, access, or close issues from Slack using the ' \
|
||||
'`%{slash_command}` command. Streamline your GitLab deployments with ChatOps.'
|
||||
),
|
||||
slash_command: '/gitlab'
|
||||
)
|
||||
].join("\r\n\r\n")
|
||||
}
|
||||
end
|
||||
|
||||
def features
|
||||
{
|
||||
app_home: {
|
||||
home_tab_enabled: true,
|
||||
messages_tab_enabled: false,
|
||||
messages_tab_read_only_enabled: true
|
||||
},
|
||||
bot_user: {
|
||||
display_name: 'GitLab',
|
||||
always_online: true
|
||||
},
|
||||
slash_commands: [
|
||||
{
|
||||
command: '/gitlab',
|
||||
url: api_v4('slack/trigger'),
|
||||
description: s_('SlackIntegration|GitLab slash commands'),
|
||||
usage_hint: s_('SlackIntegration|your-project-name-or-alias command'),
|
||||
should_escape: false
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def oauth_config
|
||||
{
|
||||
redirect_urls: [
|
||||
Gitlab.config.gitlab.url
|
||||
],
|
||||
scopes: {
|
||||
bot: %w[
|
||||
commands
|
||||
chat:write
|
||||
chat:write.public
|
||||
]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def settings
|
||||
{
|
||||
event_subscriptions: {
|
||||
request_url: api_v4('integrations/slack/events'),
|
||||
bot_events: %w[
|
||||
app_home_opened
|
||||
]
|
||||
},
|
||||
interactivity: {
|
||||
is_enabled: true,
|
||||
request_url: api_v4('integrations/slack/interactions'),
|
||||
message_menu_options_url: api_v4('integrations/slack/options')
|
||||
},
|
||||
org_deploy_enabled: false,
|
||||
socket_mode_enabled: false,
|
||||
token_rotation_enabled: false
|
||||
}
|
||||
end
|
||||
|
||||
def api_v4(path)
|
||||
"#{Gitlab.config.gitlab.url}/api/v4/#{path}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -114,11 +114,6 @@ msgid_plural "%d Modules"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d Other"
|
||||
msgid_plural "%d Others"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d Package"
|
||||
msgid_plural "%d Packages"
|
||||
msgstr[0] ""
|
||||
|
|
@ -672,9 +667,6 @@ msgstr ""
|
|||
msgid "%{count} total weight"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{criticalStart}%{critical} Critical%{criticalEnd} %{highStart}%{high} High%{highEnd} and %{otherStart}%{otherMessage}%{otherEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{dashboard_path} could not be found."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -1036,15 +1028,6 @@ msgstr[1] ""
|
|||
msgid "%{remaining_approvals} left"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{reportType} %{status}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{reportType} detected %{totalStart}%{total}%{totalEnd} potential %{vulnMessage}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{reportType} detected no new vulnerabilities."
|
||||
msgstr ""
|
||||
|
||||
msgid "%{requireStart}Require%{requireEnd} %{approvalsRequired} %{approvalStart}approval%{approvalEnd} from:"
|
||||
msgid_plural "%{requireStart}Require%{requireEnd} %{approvalsRequired} %{approvalStart}approvals%{approvalEnd} from:"
|
||||
msgstr[0] ""
|
||||
|
|
@ -5608,7 +5591,7 @@ msgstr ""
|
|||
msgid "ApplicationSettings|Email restrictions for sign-ups"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Enable Slack application"
|
||||
msgid "ApplicationSettings|Enable GitLab for Slack app"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Enable domain denylist for sign-ups"
|
||||
|
|
@ -5677,9 +5660,6 @@ msgstr ""
|
|||
msgid "ApplicationSettings|This feature is only available on GitLab.com"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|This option is only available on GitLab.com"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Upload denylist file"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -13207,6 +13187,9 @@ msgstr ""
|
|||
msgid "Create or import your first project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create phone verification exemption"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create project"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -33143,6 +33126,15 @@ msgstr ""
|
|||
msgid "Phone"
|
||||
msgstr ""
|
||||
|
||||
msgid "Phone verification exemption"
|
||||
msgstr ""
|
||||
|
||||
msgid "Phone verification exemption has been created."
|
||||
msgstr ""
|
||||
|
||||
msgid "Phone verification exemption has been removed."
|
||||
msgstr ""
|
||||
|
||||
msgid "PhoneVerification|Enter a valid code."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -38245,6 +38237,9 @@ msgstr ""
|
|||
msgid "Remove parent epic from an epic"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove phone verification exemption"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove priority"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40984,12 +40979,6 @@ msgstr ""
|
|||
msgid "Security dashboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
|
||||
msgstr ""
|
||||
|
||||
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|A merge request approval is required when test coverage declines."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -41823,9 +41812,6 @@ msgstr ""
|
|||
msgid "SecurityReports|More info"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|New vulnerabilities are vulnerabilities that the security scan detects in the merge request that are different to existing vulnerabilities in the default branch."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|No longer detected"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -41880,12 +41866,6 @@ msgstr ""
|
|||
msgid "SecurityReports|Security reports can only be accessed by authorized users."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Security reports help page link"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Security scan results"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Security scans have run"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -41985,12 +41965,6 @@ msgstr ""
|
|||
msgid "SecurityReports|Undo dismiss"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Upgrade to interact, track and shift left with vulnerability management features in the UI."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Upgrade to manage vulnerabilities"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Vulnerability report"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43162,12 +43136,6 @@ msgstr ""
|
|||
msgid "Skype:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Slack application"
|
||||
msgstr ""
|
||||
|
||||
msgid "Slack integration allows you to interact with GitLab via slash commands in a chat window."
|
||||
msgstr ""
|
||||
|
||||
msgid "Slack logo"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43180,6 +43148,12 @@ msgstr ""
|
|||
msgid "Slack notifications will be deprecated"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|- *Notifications:* Get notifications to your team's Slack channel about events happening inside your GitLab projects."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|- *Slash commands:* Quickly open, access, or close issues from Slack using the `%{slash_command}` command. Streamline your GitLab deployments with ChatOps."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Are you sure you want to remove this project from the GitLab for Slack app?"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43189,18 +43163,39 @@ msgstr ""
|
|||
msgid "SlackIntegration|Client secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Configure your GitLab for Slack app."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Copy the %{link_start}settings%{link_end} from %{strong_open}%{settings_heading}%{strong_close} in your GitLab for Slack app."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Create Slack app"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Create and read issue data and comments."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Download latest manifest file"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Generated for %{host} by GitLab %{version}."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|GitLab for Slack"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|GitLab for Slack was successfully installed."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|GitLab slash commands"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Install GitLab for Slack app"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Interact with GitLab without leaving your Slack workspace!"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Perform deployments."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43228,6 +43223,12 @@ msgstr ""
|
|||
msgid "SlackIntegration|Signing secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Step 1: Create your GitLab for Slack app"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Step 2: Configure the app settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Team name"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43243,9 +43244,24 @@ msgstr ""
|
|||
msgid "SlackIntegration|Update to the latest version to receive notifications from GitLab."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Update your Slack app"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Used for authenticating API requests from the GitLab for Slack app."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Used for authenticating OAuth requests from the GitLab for Slack app."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Used only for authenticating slash commands from the GitLab for Slack app. This method of authentication is deprecated by Slack."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Verification token"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|When GitLab releases new features for the GitLab for Slack app, you might have to manually update your copy to use the new features."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|You can now close this window and go to your Slack workspace."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43255,9 +43271,15 @@ msgstr ""
|
|||
msgid "SlackIntegration|You may need to reinstall the GitLab for Slack app when we %{linkStart}make updates or change permissions%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|You must do this step only once."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|cannot have more than %{limit} channels"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|your-project-name-or-alias command"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackModal|Are you sure you want to change the project?"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43633,6 +43655,12 @@ msgstr ""
|
|||
msgid "Something went wrong. Try again later."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong. Unable to create phone exemption."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong. Unable to remove phone exemption."
|
||||
msgstr ""
|
||||
|
||||
msgid "Sorry, no projects matched your search"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -47335,6 +47363,9 @@ msgstr ""
|
|||
msgid "This user has the %{access} role in the %{name} project."
|
||||
msgstr ""
|
||||
|
||||
msgid "This user is currently exempt from phone verification. Remove the exemption using the button below."
|
||||
msgstr ""
|
||||
|
||||
msgid "This user is the author of this %{noteable}."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -47887,6 +47918,9 @@ msgstr ""
|
|||
msgid "To remove the %{link_start}read-only%{link_end} state and regain write access, you can reduce the number of users in your top-level group to %{free_limit} users or less. You can also upgrade to a paid tier, which do not have user limits. If you need additional time, you can start a free 30-day trial which includes unlimited users. To minimize the impact to operations, for a limited time, GitLab is offering a %{promotion_link_start}one-time 70 percent discount%{link_end} off the list price at time of purchase for a new, one year subscription of GitLab Premium SaaS to %{offer_availability_link_start}qualifying top-level groups%{link_end}. The offer is valid until %{offer_date}."
|
||||
msgstr ""
|
||||
|
||||
msgid "To replace phone verification with credit card verification, create a phone verification exemption using the button below."
|
||||
msgstr ""
|
||||
|
||||
msgid "To resolve this, try to:"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53708,27 +53742,6 @@ msgstr ""
|
|||
msgid "ciReport|%{improvedNum} improved"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{linkStartTag}Learn more about API Fuzzing%{linkEndTag}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{linkStartTag}Learn more about Container Scanning %{linkEndTag}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{linkStartTag}Learn more about Coverage Fuzzing %{linkEndTag}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{linkStartTag}Learn more about DAST %{linkEndTag}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{linkStartTag}Learn more about Dependency Scanning %{linkEndTag}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{linkStartTag}Learn more about Secret Detection %{linkEndTag}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53738,12 +53751,6 @@ msgstr ""
|
|||
msgid "ciReport|%{remainingPackagesCount} more"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{reportType} is loading"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{reportType}: Loading resulted in an error"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{sameNum} same"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53762,9 +53769,6 @@ msgstr ""
|
|||
msgid "ciReport|%{scanner}: Loading resulted in an error"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|: Loading resulted in an error"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|API Fuzzing"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53834,18 +53838,12 @@ msgstr ""
|
|||
msgid "ciReport|Container Scanning"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Container Scanning detects known vulnerabilities in your container images."
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Container scanning"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Container scanning detects known vulnerabilities in your docker images."
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Could not dismiss vulnerability because the associated pipeline no longer exists. Refresh the page and try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Coverage Fuzzing"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53867,9 +53865,6 @@ msgstr ""
|
|||
msgid "ciReport|Dependency Scanning"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Dependency Scanning detects known vulnerabilities in your project's dependencies."
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Dependency scanning"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53894,9 +53889,6 @@ msgstr ""
|
|||
msgid "ciReport|Dynamic Application Security Testing (DAST)"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Dynamic Application Security Testing (DAST) detects vulnerabilities in your web application."
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Failed to load %{reportName} report"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53959,9 +53951,6 @@ msgstr ""
|
|||
msgid "ciReport|Manually added"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|New"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|New vulnerabilities are vulnerabilities that the security scan detects in the merge request that are different to existing vulnerabilities in the default branch."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53986,9 +53975,6 @@ msgstr ""
|
|||
msgid "ciReport|Secret Detection"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Secret Detection detects leaked credentials in your source code."
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Secret detection"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -54001,9 +53987,6 @@ msgstr ""
|
|||
msgid "ciReport|Security scanning"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Security scanning failed loading any results"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Security scanning is loading"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -54019,9 +54002,6 @@ msgstr ""
|
|||
msgid "ciReport|Static Application Security Testing (SAST)"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Static Application Security Testing (SAST) detects potential vulnerabilities in your source code."
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|TTFB P90"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -54063,12 +54043,6 @@ msgstr ""
|
|||
msgid "ciReport|in"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|is loading"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|is loading, errors when loading results"
|
||||
msgstr ""
|
||||
|
||||
msgid "closed"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ module RuboCop
|
|||
# ignore_column :full_name, remove_after: '2023-05-22', remove_with: '16.0'
|
||||
# end
|
||||
class IgnoredColumns < RuboCop::Cop::Base
|
||||
USE_CONCERN_ADD_MSG = 'Use `IgnoredColumns` concern instead of adding to `self.ignored_columns`.'
|
||||
USE_CONCERN_SET_MSG = 'Use `IgnoredColumns` concern instead of setting `self.ignored_columns`.'
|
||||
USE_CONCERN_ADD_MSG = 'Use `IgnorableColumns` concern instead of adding to `self.ignored_columns`.'
|
||||
USE_CONCERN_SET_MSG = 'Use `IgnorableColumns` concern instead of setting `self.ignored_columns`.'
|
||||
WRONG_MODEL_MSG = <<~MSG
|
||||
If the model exists in CE and EE, the column has to be ignored
|
||||
in the CE model. If the model only exists in EE, then it has to be added there.
|
||||
|
|
|
|||
|
|
@ -170,12 +170,12 @@ RSpec.describe Settings, feature_category: :system_access do
|
|||
|
||||
it 'defaults to using the encrypted_settings_key_base for the key' do
|
||||
expect(Gitlab::EncryptedConfiguration).to receive(:new).with(hash_including(base_key: Gitlab::Application.secrets.encrypted_settings_key_base))
|
||||
Settings.encrypted('tmp/tests/test.enc')
|
||||
described_class.encrypted('tmp/tests/test.enc')
|
||||
end
|
||||
|
||||
it 'returns empty encrypted config when a key has not been set' do
|
||||
allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
|
||||
expect(Settings.encrypted('tmp/tests/test.enc').read).to be_empty
|
||||
expect(described_class.encrypted('tmp/tests/test.enc').read).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -487,6 +487,43 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET #slack_app_manifest_download', feature_category: :integrations do
|
||||
before do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
subject { get :slack_app_manifest_download }
|
||||
|
||||
it 'downloads the GitLab for Slack app manifest' do
|
||||
allow(Slack::Manifest).to receive(:to_h).and_return({ foo: 'bar' })
|
||||
|
||||
subject
|
||||
|
||||
expect(response.body).to eq('{"foo":"bar"}')
|
||||
expect(response.headers['Content-Disposition']).to eq(
|
||||
'attachment; filename="slack_manifest.json"; filename*=UTF-8\'\'slack_manifest.json'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #slack_app_manifest_share', feature_category: :integrations do
|
||||
before do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
subject { get :slack_app_manifest_share }
|
||||
|
||||
it 'redirects the user to the Slack Manifest share URL' do
|
||||
allow(Slack::Manifest).to receive(:to_h).and_return({ foo: 'bar' })
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(
|
||||
"https://api.slack.com/apps?new_app=1&manifest_json=%7B%22foo%22%3A%22bar%22%7D"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #service_usage_data', feature_category: :service_ping do
|
||||
before do
|
||||
stub_usage_data_connections
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
|
|||
end
|
||||
|
||||
context 'when repository container is a project' do
|
||||
it_behaves_like Repositories::GitHttpController do
|
||||
it_behaves_like described_class do
|
||||
let(:container) { project }
|
||||
let(:user) { project.first_owner }
|
||||
let(:access_checker_class) { Gitlab::GitAccess }
|
||||
|
|
@ -133,7 +133,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
|
|||
end
|
||||
|
||||
context 'when the user is a deploy token' do
|
||||
it_behaves_like Repositories::GitHttpController do
|
||||
it_behaves_like described_class do
|
||||
let(:container) { project }
|
||||
let(:user) { create(:deploy_token, :project, projects: [project]) }
|
||||
let(:access_checker_class) { Gitlab::GitAccess }
|
||||
|
|
@ -144,7 +144,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
|
|||
end
|
||||
|
||||
context 'when repository container is a project wiki' do
|
||||
it_behaves_like Repositories::GitHttpController do
|
||||
it_behaves_like described_class do
|
||||
let(:container) { create(:project_wiki, :empty_repo, project: project) }
|
||||
let(:user) { project.first_owner }
|
||||
let(:access_checker_class) { Gitlab::GitAccessWiki }
|
||||
|
|
@ -155,7 +155,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
|
|||
end
|
||||
|
||||
context 'when repository container is a personal snippet' do
|
||||
it_behaves_like Repositories::GitHttpController do
|
||||
it_behaves_like described_class do
|
||||
let(:container) { personal_snippet }
|
||||
let(:user) { personal_snippet.author }
|
||||
let(:access_checker_class) { Gitlab::GitAccessSnippet }
|
||||
|
|
@ -167,7 +167,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
|
|||
end
|
||||
|
||||
context 'when repository container is a project snippet' do
|
||||
it_behaves_like Repositories::GitHttpController do
|
||||
it_behaves_like described_class do
|
||||
let(:container) { project_snippet }
|
||||
let(:user) { project_snippet.author }
|
||||
let(:access_checker_class) { Gitlab::GitAccessSnippet }
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ RSpec.describe ApplicationExperiment, :experiment, feature_category: :experiment
|
|||
# _published_experiments.html.haml partial.
|
||||
application_experiment.publish
|
||||
|
||||
expect(ApplicationExperiment.published_experiments['namespaced/stub']).to include(
|
||||
expect(described_class.published_experiments['namespaced/stub']).to include(
|
||||
experiment: 'namespaced/stub',
|
||||
excluded: false,
|
||||
key: anything,
|
||||
|
|
|
|||
|
|
@ -8,11 +8,9 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
|
|||
include UsageDataHelpers
|
||||
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
let(:dot_com?) { false }
|
||||
|
||||
context 'application setting :admin_mode is enabled', :request_store do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(dot_com?)
|
||||
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
|
||||
sign_in(admin)
|
||||
gitlab_enable_admin_mode_sign_in(admin)
|
||||
|
|
@ -147,9 +145,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
|
|||
end
|
||||
|
||||
context 'Dormant users', feature_category: :user_management do
|
||||
context 'when Gitlab.com' do
|
||||
let(:dot_com?) { true }
|
||||
|
||||
context 'when Gitlab.com', :saas do
|
||||
it 'does not expose the setting section' do
|
||||
# NOTE: not_to have_content may have false positives for content
|
||||
# that might not load instantly, so before checking that
|
||||
|
|
@ -163,8 +159,6 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
|
|||
end
|
||||
|
||||
context 'when not Gitlab.com' do
|
||||
let(:dot_com?) { false }
|
||||
|
||||
it 'exposes the setting section' do
|
||||
expect(page).to have_content('Dormant users')
|
||||
expect(page).to have_field('Deactivate dormant users after a period of inactivity')
|
||||
|
|
@ -366,9 +360,46 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
|
|||
end
|
||||
|
||||
context 'GitLab for Slack app settings', feature_category: :integrations do
|
||||
let(:create_heading) { 'Create your GitLab for Slack app' }
|
||||
let(:configure_heading) { 'Configure the app settings' }
|
||||
let(:update_heading) { 'Update your Slack app' }
|
||||
|
||||
it 'has all sections' do
|
||||
page.within('.as-slack') do
|
||||
expect(page).to have_content(create_heading)
|
||||
expect(page).to have_content(configure_heading)
|
||||
expect(page).to have_content(update_heading)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when GitLab.com', :saas do
|
||||
it 'only has the configure section' do
|
||||
page.within('.as-slack') do
|
||||
expect(page).to have_content(configure_heading)
|
||||
|
||||
expect(page).not_to have_content(create_heading)
|
||||
expect(page).not_to have_content(update_heading)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the `slack_app_self_managed` flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(slack_app_self_managed: false)
|
||||
visit general_admin_application_settings_path
|
||||
end
|
||||
|
||||
it 'does not display any sections' do
|
||||
expect(page).not_to have_selector('.as-slack')
|
||||
expect(page).not_to have_content(configure_heading)
|
||||
expect(page).not_to have_content(create_heading)
|
||||
expect(page).not_to have_content(update_heading)
|
||||
end
|
||||
end
|
||||
|
||||
it 'changes the settings' do
|
||||
page.within('.as-slack') do
|
||||
check 'Enable Slack application'
|
||||
check 'Enable GitLab for Slack app'
|
||||
fill_in 'Client ID', with: 'slack_app_id'
|
||||
fill_in 'Client secret', with: 'slack_app_secret'
|
||||
fill_in 'Signing secret', with: 'slack_app_signing_secret'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_information": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"maxLength": 35
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 140
|
||||
},
|
||||
"background_color": {
|
||||
"type": "string",
|
||||
"pattern": "^#[0-9A-F]{6}$"
|
||||
},
|
||||
"long_description": {
|
||||
"type": "string",
|
||||
"maxLength": 4000
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"features": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app_home": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"home_tab_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"messages_tab_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"messages_tab_read_only_enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bot_user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"maxLength": 80
|
||||
},
|
||||
"always_online": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"slash_commands": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"maxLength": 32
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"usage_hint": {
|
||||
"type": "string",
|
||||
"maxLength": 1000
|
||||
},
|
||||
"should_escape": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"description"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"oauth_config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"redirect_urls": {
|
||||
"type": "array",
|
||||
"maxContains": 1000,
|
||||
"items": [
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scopes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bot": {
|
||||
"type": "array",
|
||||
"maxContains": 255
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"event_subscriptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"bot_events": {
|
||||
"type": "array",
|
||||
"maxContains": 100,
|
||||
"items": [
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"interactivity": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"is_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"request_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"message_menu_options_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"org_deploy_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"socket_mode_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"token_rotation_enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"display_information"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Grouped Issues List renders a smart virtual list with the correct props 1`] = `
|
||||
Object {
|
||||
"length": 4,
|
||||
"remain": 20,
|
||||
"rtag": "div",
|
||||
"size": 32,
|
||||
"wclass": "report-block-list",
|
||||
"wtag": "ul",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Grouped Issues List with data renders a report item with the correct props 1`] = `
|
||||
Object {
|
||||
"component": "CodequalityIssueBody",
|
||||
"iconComponent": "IssueStatusIcon",
|
||||
"isNew": false,
|
||||
"issue": Object {
|
||||
"name": "foo",
|
||||
},
|
||||
"showReportSectionStatusIcon": false,
|
||||
"status": "none",
|
||||
"statusIconSize": 24,
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import GroupedIssuesList from '~/ci/reports/components/grouped_issues_list.vue';
|
||||
import ReportItem from '~/ci/reports/components/report_item.vue';
|
||||
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
|
||||
|
||||
describe('Grouped Issues List', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = ({ propsData = {}, stubs = {} } = {}) => {
|
||||
wrapper = shallowMount(GroupedIssuesList, {
|
||||
propsData,
|
||||
stubs,
|
||||
});
|
||||
};
|
||||
|
||||
const findHeading = (groupName) => wrapper.find(`[data-testid="${groupName}Heading"`);
|
||||
|
||||
it('renders a smart virtual list with the correct props', () => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
resolvedIssues: [{ name: 'foo' }],
|
||||
unresolvedIssues: [{ name: 'bar' }],
|
||||
},
|
||||
stubs: {
|
||||
SmartVirtualList,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.findComponent(SmartVirtualList).props()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('without data', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it.each(['unresolved', 'resolved'])('does not a render a header for %s issues', (issueName) => {
|
||||
expect(findHeading(issueName).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it.each(['resolved', 'unresolved'])('does not render report items for %s issues', () => {
|
||||
expect(wrapper.findComponent(ReportItem).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with data', () => {
|
||||
it.each`
|
||||
givenIssues | givenHeading | groupName
|
||||
${[{ name: 'foo issue' }]} | ${'Foo Heading'} | ${'resolved'}
|
||||
${[{ name: 'bar issue' }]} | ${'Bar Heading'} | ${'unresolved'}
|
||||
`('renders the heading for $groupName issues', ({ givenIssues, givenHeading, groupName }) => {
|
||||
createComponent({
|
||||
propsData: { [`${groupName}Issues`]: givenIssues, [`${groupName}Heading`]: givenHeading },
|
||||
});
|
||||
|
||||
expect(findHeading(groupName).text()).toBe(givenHeading);
|
||||
});
|
||||
|
||||
it.each(['resolved', 'unresolved'])('renders all %s issues', (issueName) => {
|
||||
const issues = [{ name: 'foo' }, { name: 'bar' }];
|
||||
|
||||
createComponent({
|
||||
propsData: { [`${issueName}Issues`]: issues },
|
||||
});
|
||||
|
||||
expect(wrapper.findAllComponents(ReportItem)).toHaveLength(issues.length);
|
||||
});
|
||||
|
||||
it('renders a report item with the correct props', () => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
resolvedIssues: [{ name: 'foo' }],
|
||||
component: 'CodequalityIssueBody',
|
||||
},
|
||||
stubs: {
|
||||
ReportItem,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.findComponent(ReportItem).props()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import HelpPopover from '~/vue_shared/components/help_popover.vue';
|
||||
import SummaryRow from '~/ci/reports/components/summary_row.vue';
|
||||
|
||||
describe('Summary row', () => {
|
||||
let wrapper;
|
||||
|
||||
const summary = 'SAST detected 1 new vulnerability and 1 fixed vulnerability';
|
||||
const popoverOptions = {
|
||||
title: 'Static Application Security Testing (SAST)',
|
||||
content: '<a>Learn more about SAST</a>',
|
||||
};
|
||||
const statusIcon = 'warning';
|
||||
|
||||
const createComponent = ({ props = {}, slots = {} } = {}) => {
|
||||
wrapper = extendedWrapper(
|
||||
mount(SummaryRow, {
|
||||
propsData: {
|
||||
summary,
|
||||
popoverOptions,
|
||||
statusIcon,
|
||||
...props,
|
||||
},
|
||||
slots,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const findSummary = () => wrapper.findByTestId('summary-row-description');
|
||||
const findStatusIcon = () => wrapper.findByTestId('summary-row-icon');
|
||||
const findHelpPopover = () => wrapper.findComponent(HelpPopover);
|
||||
|
||||
it('renders provided summary', () => {
|
||||
createComponent();
|
||||
expect(findSummary().text()).toContain(summary);
|
||||
});
|
||||
|
||||
it('renders provided icon', () => {
|
||||
createComponent();
|
||||
expect(findStatusIcon().find('[data-testid="status_warning-icon"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders help popover if popoverOptions are provided', () => {
|
||||
createComponent();
|
||||
expect(findHelpPopover().props('options')).toEqual(popoverOptions);
|
||||
});
|
||||
|
||||
it('does not render help popover if popoverOptions are not provided', () => {
|
||||
createComponent({ props: { popoverOptions: null } });
|
||||
expect(findHelpPopover().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('summary slot', () => {
|
||||
it('replaces the summary prop', () => {
|
||||
const summarySlotContent = 'Summary slot content';
|
||||
createComponent({ slots: { summary: summarySlotContent } });
|
||||
|
||||
expect(wrapper.text()).not.toContain(summary);
|
||||
expect(findSummary().text()).toContain(summarySlotContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe TimeZoneHelper, '(JavaScript fixtures)' do
|
||||
include JavaScriptFixturesHelpers
|
||||
include TimeZoneHelper
|
||||
include described_class
|
||||
|
||||
let(:response) { @timezones.sort_by! { |tz| tz[:name] }.to_json }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,144 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}0 Critical%{criticalEnd} %{highStart}1 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 0, "high": 1, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = `
|
||||
<span>
|
||||
Security scanning detected
|
||||
<strong>
|
||||
1
|
||||
</strong>
|
||||
potential vulnerability
|
||||
<span
|
||||
class="gl-font-sm"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="gl-pl-4"
|
||||
>
|
||||
|
||||
0 Critical
|
||||
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<strong
|
||||
class="gl-text-red-600 gl-px-2"
|
||||
>
|
||||
|
||||
1 High
|
||||
|
||||
</strong>
|
||||
</span>
|
||||
and
|
||||
<span>
|
||||
<span
|
||||
class="gl-px-2"
|
||||
>
|
||||
|
||||
0 Others
|
||||
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}0 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 0, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = `
|
||||
<span>
|
||||
Security scanning detected
|
||||
<strong>
|
||||
1
|
||||
</strong>
|
||||
potential vulnerability
|
||||
<span
|
||||
class="gl-font-sm"
|
||||
>
|
||||
<span>
|
||||
<strong
|
||||
class="gl-text-red-800 gl-pl-4"
|
||||
>
|
||||
|
||||
1 Critical
|
||||
|
||||
</strong>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span
|
||||
class="gl-px-2"
|
||||
>
|
||||
|
||||
0 High
|
||||
|
||||
</span>
|
||||
</span>
|
||||
and
|
||||
<span>
|
||||
<span
|
||||
class="gl-px-2"
|
||||
>
|
||||
|
||||
0 Others
|
||||
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}2 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 2, "message": "Security scanning detected %{totalStart}3%{totalEnd} potential vulnerabilities", "other": 0, "status": "", "total": 3} interpolates correctly 1`] = `
|
||||
<span>
|
||||
Security scanning detected
|
||||
<strong>
|
||||
3
|
||||
</strong>
|
||||
potential vulnerabilities
|
||||
<span
|
||||
class="gl-font-sm"
|
||||
>
|
||||
<span>
|
||||
<strong
|
||||
class="gl-text-red-800 gl-pl-4"
|
||||
>
|
||||
|
||||
1 Critical
|
||||
|
||||
</strong>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<strong
|
||||
class="gl-text-red-600 gl-px-2"
|
||||
>
|
||||
|
||||
2 High
|
||||
|
||||
</strong>
|
||||
</span>
|
||||
and
|
||||
<span>
|
||||
<span
|
||||
class="gl-px-2"
|
||||
>
|
||||
|
||||
0 Others
|
||||
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`SecuritySummary component given the message {"message": ""} interpolates correctly 1`] = `
|
||||
<span>
|
||||
|
||||
<!---->
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`SecuritySummary component given the message {"message": "foo"} interpolates correctly 1`] = `
|
||||
<span>
|
||||
foo
|
||||
<!---->
|
||||
</span>
|
||||
`;
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import {
|
||||
expectedDownloadDropdownPropsWithTitle,
|
||||
securityReportMergeRequestDownloadPathsQueryResponse,
|
||||
} from 'jest/vue_shared/security_reports/mock_data';
|
||||
import { createAlert } from '~/alert';
|
||||
import Component from '~/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue';
|
||||
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
|
||||
import {
|
||||
REPORT_TYPE_SAST,
|
||||
REPORT_TYPE_SECRET_DETECTION,
|
||||
} from '~/vue_shared/security_reports/constants';
|
||||
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
|
||||
|
||||
jest.mock('~/alert');
|
||||
|
||||
describe('Merge request artifact Download', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultProps = {
|
||||
reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION],
|
||||
targetProjectFullPath: '/path',
|
||||
mrIid: 123,
|
||||
};
|
||||
|
||||
const createWrapper = ({ propsData, options }) => {
|
||||
wrapper = shallowMount(Component, {
|
||||
stubs: {
|
||||
SecurityReportDownloadDropdown,
|
||||
},
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...propsData,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
const pendingHandler = () => new Promise(() => {});
|
||||
const successHandler = () =>
|
||||
Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryResponse });
|
||||
const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
|
||||
const createMockApolloProvider = (handler) => {
|
||||
Vue.use(VueApollo);
|
||||
const requestHandlers = [[securityReportMergeRequestDownloadPathsQuery, handler]];
|
||||
|
||||
return createMockApollo(requestHandlers);
|
||||
};
|
||||
|
||||
const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown);
|
||||
|
||||
describe('given the query is loading', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({
|
||||
options: {
|
||||
apolloProvider: createMockApolloProvider(pendingHandler),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('loading is true', () => {
|
||||
expect(findDownloadDropdown().props('loading')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the query loads successfully', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({
|
||||
options: {
|
||||
apolloProvider: createMockApolloProvider(successHandler),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the download dropdown', () => {
|
||||
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithTitle);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the query fails', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({
|
||||
options: {
|
||||
apolloProvider: createMockApolloProvider(failureHandler),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('calls createAlert correctly', () => {
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: Component.i18n.apiError,
|
||||
captureError: true,
|
||||
error: expect.any(Error),
|
||||
});
|
||||
});
|
||||
|
||||
it('renders nothing', () => {
|
||||
expect(findDownloadDropdown().props('artifacts')).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
import { GlLink, GlPopover } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
|
||||
|
||||
const helpPath = '/docs';
|
||||
const discoverProjectSecurityPath = '/discoverProjectSecurityPath';
|
||||
|
||||
describe('HelpIcon component', () => {
|
||||
let wrapper;
|
||||
|
||||
const createWrapper = (props) => {
|
||||
wrapper = shallowMount(HelpIcon, {
|
||||
propsData: {
|
||||
helpPath,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findLink = () => wrapper.findComponent(GlLink);
|
||||
const findPopover = () => wrapper.findComponent(GlPopover);
|
||||
const findPopoverTarget = () => wrapper.findComponent({ ref: 'discoverProjectSecurity' });
|
||||
|
||||
describe('given a help path only', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
it('does not render a popover', () => {
|
||||
expect(findPopover().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders a help link', () => {
|
||||
expect(findLink().attributes()).toMatchObject({
|
||||
href: helpPath,
|
||||
target: '_blank',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a help path and discover project security path', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ discoverProjectSecurityPath });
|
||||
});
|
||||
|
||||
it('renders a popover', () => {
|
||||
const popover = findPopover();
|
||||
expect(popover.props('target')()).toBe(findPopoverTarget().element);
|
||||
expect(popover.attributes()).toMatchObject({
|
||||
title: HelpIcon.i18n.upgradeToManageVulnerabilities,
|
||||
triggers: 'click blur',
|
||||
});
|
||||
expect(popover.text()).toContain(HelpIcon.i18n.upgradeToInteract);
|
||||
});
|
||||
|
||||
it('renders a link to the discover path', () => {
|
||||
expect(findLink().attributes()).toMatchObject({
|
||||
href: discoverProjectSecurityPath,
|
||||
target: '_blank',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import SecuritySummary from '~/vue_shared/security_reports/components/security_summary.vue';
|
||||
import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
|
||||
|
||||
describe('SecuritySummary component', () => {
|
||||
let wrapper;
|
||||
|
||||
const createWrapper = (message) => {
|
||||
wrapper = shallowMount(SecuritySummary, {
|
||||
propsData: { message },
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe.each([
|
||||
{ message: '' },
|
||||
{ message: 'foo' },
|
||||
groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 0, total: 1 }),
|
||||
groupedTextBuilder({ reportType: 'Security scanning', critical: 0, high: 1, total: 1 }),
|
||||
groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 2, total: 3 }),
|
||||
])('given the message %p', (message) => {
|
||||
beforeEach(() => {
|
||||
createWrapper(message);
|
||||
});
|
||||
|
||||
it('interpolates correctly', () => {
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -341,120 +341,6 @@ export const securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const securityReportMergeRequestDownloadPathsQueryResponse = {
|
||||
project: {
|
||||
id: '1',
|
||||
mergeRequest: {
|
||||
id: 'mr-1',
|
||||
headPipeline: {
|
||||
id: 'gid://gitlab/Ci::Pipeline/176',
|
||||
jobs: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'job-1',
|
||||
name: 'secret_detection',
|
||||
artifacts: {
|
||||
nodes: [
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=trace',
|
||||
fileType: 'TRACE',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=secret_detection',
|
||||
fileType: 'SECRET_DETECTION',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
],
|
||||
__typename: 'CiJobArtifactConnection',
|
||||
},
|
||||
__typename: 'CiJob',
|
||||
},
|
||||
{
|
||||
id: 'job-2',
|
||||
name: 'bandit-sast',
|
||||
artifacts: {
|
||||
nodes: [
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=trace',
|
||||
fileType: 'TRACE',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=sast',
|
||||
fileType: 'SAST',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
],
|
||||
__typename: 'CiJobArtifactConnection',
|
||||
},
|
||||
__typename: 'CiJob',
|
||||
},
|
||||
{
|
||||
id: 'job-3',
|
||||
name: 'eslint-sast',
|
||||
artifacts: {
|
||||
nodes: [
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=trace',
|
||||
fileType: 'TRACE',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=sast',
|
||||
fileType: 'SAST',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
],
|
||||
__typename: 'CiJobArtifactConnection',
|
||||
},
|
||||
__typename: 'CiJob',
|
||||
},
|
||||
{
|
||||
id: 'job-4',
|
||||
name: 'all_artifacts',
|
||||
artifacts: {
|
||||
nodes: [
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=archive',
|
||||
fileType: 'ARCHIVE',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=trace',
|
||||
fileType: 'TRACE',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=metadata',
|
||||
fileType: 'METADATA',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
],
|
||||
__typename: 'CiJobArtifactConnection',
|
||||
},
|
||||
__typename: 'CiJob',
|
||||
},
|
||||
],
|
||||
__typename: 'CiJobConnection',
|
||||
},
|
||||
__typename: 'Pipeline',
|
||||
},
|
||||
__typename: 'MergeRequest',
|
||||
},
|
||||
__typename: 'Project',
|
||||
},
|
||||
};
|
||||
|
||||
export const securityReportPipelineDownloadPathsQueryResponse = {
|
||||
project: {
|
||||
id: 'project-1',
|
||||
|
|
@ -566,9 +452,6 @@ export const securityReportPipelineDownloadPathsQueryResponse = {
|
|||
__typename: 'Project',
|
||||
};
|
||||
|
||||
/**
|
||||
* These correspond to SAST jobs in the securityReportMergeRequestDownloadPathsQueryResponse above.
|
||||
*/
|
||||
export const sastArtifacts = [
|
||||
{
|
||||
name: 'bandit-sast',
|
||||
|
|
@ -582,9 +465,6 @@ export const sastArtifacts = [
|
|||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* These correspond to Secret Detection jobs in the securityReportMergeRequestDownloadPathsQueryResponse above.
|
||||
*/
|
||||
export const secretDetectionArtifacts = [
|
||||
{
|
||||
name: 'secret_detection',
|
||||
|
|
@ -594,13 +474,6 @@ export const secretDetectionArtifacts = [
|
|||
},
|
||||
];
|
||||
|
||||
export const expectedDownloadDropdownPropsWithTitle = {
|
||||
loading: false,
|
||||
artifacts: [...secretDetectionArtifacts, ...sastArtifacts],
|
||||
text: '',
|
||||
title: 'Download results',
|
||||
};
|
||||
|
||||
export const expectedDownloadDropdownPropsWithText = {
|
||||
loading: false,
|
||||
artifacts: [...secretDetectionArtifacts, ...sastArtifacts],
|
||||
|
|
@ -608,9 +481,6 @@ export const expectedDownloadDropdownPropsWithText = {
|
|||
text: 'Download results',
|
||||
};
|
||||
|
||||
/**
|
||||
* These correspond to any jobs with zip archives in the securityReportMergeRequestDownloadPathsQueryResponse above.
|
||||
*/
|
||||
export const archiveArtifacts = [
|
||||
{
|
||||
name: 'all_artifacts Archive',
|
||||
|
|
@ -619,9 +489,6 @@ export const archiveArtifacts = [
|
|||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* These correspond to any jobs with trace data in the securityReportMergeRequestDownloadPathsQueryResponse above.
|
||||
*/
|
||||
export const traceArtifacts = [
|
||||
{
|
||||
name: 'secret_detection Trace',
|
||||
|
|
@ -645,9 +512,6 @@ export const traceArtifacts = [
|
|||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* These correspond to any jobs with metadata data in the securityReportMergeRequestDownloadPathsQueryResponse above.
|
||||
*/
|
||||
export const metadataArtifacts = [
|
||||
{
|
||||
name: 'all_artifacts Metadata',
|
||||
|
|
|
|||
|
|
@ -1,267 +0,0 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { merge } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import Vuex from 'vuex';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import {
|
||||
expectedDownloadDropdownPropsWithText,
|
||||
securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse,
|
||||
securityReportMergeRequestDownloadPathsQueryResponse,
|
||||
sastDiffSuccessMock,
|
||||
secretDetectionDiffSuccessMock,
|
||||
} from 'jest/vue_shared/security_reports/mock_data';
|
||||
import { createAlert } from '~/alert';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
|
||||
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
|
||||
import {
|
||||
REPORT_TYPE_SAST,
|
||||
REPORT_TYPE_SECRET_DETECTION,
|
||||
} from '~/vue_shared/security_reports/constants';
|
||||
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
|
||||
import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue';
|
||||
|
||||
jest.mock('~/alert');
|
||||
|
||||
Vue.use(VueApollo);
|
||||
Vue.use(Vuex);
|
||||
|
||||
const SAST_COMPARISON_PATH = '/sast.json';
|
||||
const SECRET_DETECTION_COMPARISON_PATH = '/secret_detection.json';
|
||||
|
||||
describe('Security reports app', () => {
|
||||
let wrapper;
|
||||
|
||||
const props = {
|
||||
pipelineId: 123,
|
||||
projectId: 456,
|
||||
securityReportsDocsPath: '/docs',
|
||||
discoverProjectSecurityPath: '/discoverProjectSecurityPath',
|
||||
};
|
||||
|
||||
const createComponent = (options) => {
|
||||
wrapper = mount(
|
||||
SecurityReportsApp,
|
||||
merge(
|
||||
{
|
||||
propsData: { ...props },
|
||||
stubs: {
|
||||
HelpIcon: true,
|
||||
},
|
||||
},
|
||||
options,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const pendingHandler = () => new Promise(() => {});
|
||||
const successHandler = () =>
|
||||
Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryResponse });
|
||||
const successEmptyHandler = () =>
|
||||
Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse });
|
||||
const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
|
||||
const createMockApolloProvider = (handler) => {
|
||||
const requestHandlers = [[securityReportMergeRequestDownloadPathsQuery, handler]];
|
||||
|
||||
return createMockApollo(requestHandlers);
|
||||
};
|
||||
|
||||
const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown);
|
||||
const findHelpIconComponent = () => wrapper.findComponent(HelpIcon);
|
||||
|
||||
describe('given the artifacts query is loading', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
apolloProvider: createMockApolloProvider(pendingHandler),
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Remove this assertion as part of
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/273431
|
||||
it('initially renders nothing', () => {
|
||||
expect(wrapper.html()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the artifacts query loads successfully', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
apolloProvider: createMockApolloProvider(successHandler),
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the download dropdown', () => {
|
||||
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
|
||||
});
|
||||
|
||||
it('renders the expected message', () => {
|
||||
expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
|
||||
});
|
||||
|
||||
it('renders a help link', () => {
|
||||
expect(findHelpIconComponent().props()).toEqual({
|
||||
helpPath: props.securityReportsDocsPath,
|
||||
discoverProjectSecurityPath: props.discoverProjectSecurityPath,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the artifacts query loads successfully with no artifacts', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
apolloProvider: createMockApolloProvider(successEmptyHandler),
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Remove this assertion as part of
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/273431
|
||||
it('initially renders nothing', () => {
|
||||
expect(wrapper.html()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the artifacts query fails', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
apolloProvider: createMockApolloProvider(failureHandler),
|
||||
});
|
||||
});
|
||||
|
||||
it('calls createAlert correctly', () => {
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: SecurityReportsApp.i18n.apiError,
|
||||
captureError: true,
|
||||
error: expect.any(Error),
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Remove this assertion as part of
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/273431
|
||||
it('renders nothing', () => {
|
||||
expect(wrapper.html()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => {
|
||||
let mock;
|
||||
|
||||
const createComponentWithFlagEnabled = (options) =>
|
||||
createComponent(
|
||||
merge(options, {
|
||||
provide: {
|
||||
glFeatures: {
|
||||
coreSecurityMrWidgetCounts: true,
|
||||
},
|
||||
},
|
||||
apolloProvider: createMockApolloProvider(successHandler),
|
||||
}),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
const SAST_SUCCESS_MESSAGE =
|
||||
'Security scanning detected 1 potential vulnerability 1 Critical 0 High and 0 Others';
|
||||
const SECRET_DETECTION_SUCCESS_MESSAGE =
|
||||
'Security scanning detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others';
|
||||
describe.each`
|
||||
reportType | pathProp | path | successResponse | successMessage
|
||||
${REPORT_TYPE_SAST} | ${'sastComparisonPath'} | ${SAST_COMPARISON_PATH} | ${sastDiffSuccessMock} | ${SAST_SUCCESS_MESSAGE}
|
||||
${REPORT_TYPE_SECRET_DETECTION} | ${'secretDetectionComparisonPath'} | ${SECRET_DETECTION_COMPARISON_PATH} | ${secretDetectionDiffSuccessMock} | ${SECRET_DETECTION_SUCCESS_MESSAGE}
|
||||
`(
|
||||
'given a $pathProp and $reportType artifact',
|
||||
({ pathProp, path, successResponse, successMessage }) => {
|
||||
describe('when loading', () => {
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios, { delayResponse: 1 });
|
||||
mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse);
|
||||
|
||||
createComponentWithFlagEnabled({
|
||||
propsData: {
|
||||
[pathProp]: path,
|
||||
},
|
||||
});
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('should have loading message', () => {
|
||||
expect(wrapper.text()).toContain('Security scanning is loading');
|
||||
});
|
||||
|
||||
it('renders the download dropdown', () => {
|
||||
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when successfully loaded', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse);
|
||||
|
||||
createComponentWithFlagEnabled({
|
||||
propsData: {
|
||||
[pathProp]: path,
|
||||
},
|
||||
});
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('should show counts', () => {
|
||||
expect(trimText(wrapper.text())).toContain(successMessage);
|
||||
});
|
||||
|
||||
it('renders the download dropdown', () => {
|
||||
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an error occurs', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
|
||||
|
||||
createComponentWithFlagEnabled({
|
||||
propsData: {
|
||||
[pathProp]: path,
|
||||
},
|
||||
});
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('should show error message', () => {
|
||||
expect(trimText(wrapper.text())).toContain('Loading resulted in an error');
|
||||
});
|
||||
|
||||
it('renders the download dropdown', () => {
|
||||
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the comparison endpoint is not provided', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
|
||||
|
||||
createComponentWithFlagEnabled();
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('renders the basic scansHaveRun message', () => {
|
||||
expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
import {
|
||||
groupedSummaryText,
|
||||
allReportsHaveError,
|
||||
areReportsLoading,
|
||||
anyReportHasError,
|
||||
areAllReportsLoading,
|
||||
anyReportHasIssues,
|
||||
summaryCounts,
|
||||
} from '~/vue_shared/security_reports/store/getters';
|
||||
import createSastState from '~/vue_shared/security_reports/store/modules/sast/state';
|
||||
import createSecretDetectionState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
|
||||
import createState from '~/vue_shared/security_reports/store/state';
|
||||
import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
|
||||
import { CRITICAL, HIGH, LOW } from '~/vulnerabilities/constants';
|
||||
|
||||
const generateVuln = (severity) => ({ severity });
|
||||
|
||||
describe('Security reports getters', () => {
|
||||
let state;
|
||||
|
||||
beforeEach(() => {
|
||||
state = createState();
|
||||
state.sast = createSastState();
|
||||
state.secretDetection = createSecretDetectionState();
|
||||
});
|
||||
|
||||
describe('summaryCounts', () => {
|
||||
it('returns 0 count for empty state', () => {
|
||||
expect(summaryCounts(state)).toEqual({
|
||||
critical: 0,
|
||||
high: 0,
|
||||
other: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('combines all reports', () => {
|
||||
it('of the same severity', () => {
|
||||
state.sast.newIssues = [generateVuln(CRITICAL)];
|
||||
state.secretDetection.newIssues = [generateVuln(CRITICAL)];
|
||||
|
||||
expect(summaryCounts(state)).toEqual({
|
||||
critical: 2,
|
||||
high: 0,
|
||||
other: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('of different severities', () => {
|
||||
state.sast.newIssues = [generateVuln(CRITICAL)];
|
||||
state.secretDetection.newIssues = [generateVuln(HIGH), generateVuln(LOW)];
|
||||
|
||||
expect(summaryCounts(state)).toEqual({
|
||||
critical: 1,
|
||||
high: 1,
|
||||
other: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupedSummaryText', () => {
|
||||
it('returns failed text', () => {
|
||||
expect(
|
||||
groupedSummaryText(state, {
|
||||
allReportsHaveError: true,
|
||||
areReportsLoading: false,
|
||||
summaryCounts: {},
|
||||
}),
|
||||
).toEqual({ message: 'Security scanning failed loading any results' });
|
||||
});
|
||||
|
||||
it('returns `is loading` as status text', () => {
|
||||
expect(
|
||||
groupedSummaryText(state, {
|
||||
allReportsHaveError: false,
|
||||
areReportsLoading: true,
|
||||
summaryCounts: {},
|
||||
}),
|
||||
).toEqual(
|
||||
groupedTextBuilder({
|
||||
reportType: 'Security scanning',
|
||||
critical: 0,
|
||||
high: 0,
|
||||
other: 0,
|
||||
status: 'is loading',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns no new status text if there are existing ones', () => {
|
||||
expect(
|
||||
groupedSummaryText(state, {
|
||||
allReportsHaveError: false,
|
||||
areReportsLoading: false,
|
||||
summaryCounts: {},
|
||||
}),
|
||||
).toEqual(
|
||||
groupedTextBuilder({
|
||||
reportType: 'Security scanning',
|
||||
critical: 0,
|
||||
high: 0,
|
||||
other: 0,
|
||||
status: '',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('areReportsLoading', () => {
|
||||
it('returns true when any report is loading', () => {
|
||||
state.sast.isLoading = true;
|
||||
|
||||
expect(areReportsLoading(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when none of the reports are loading', () => {
|
||||
expect(areReportsLoading(state)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('areAllReportsLoading', () => {
|
||||
it('returns true when all reports are loading', () => {
|
||||
state.sast.isLoading = true;
|
||||
state.secretDetection.isLoading = true;
|
||||
|
||||
expect(areAllReportsLoading(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when some of the reports are loading', () => {
|
||||
state.sast.isLoading = true;
|
||||
|
||||
expect(areAllReportsLoading(state)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when none of the reports are loading', () => {
|
||||
expect(areAllReportsLoading(state)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('allReportsHaveError', () => {
|
||||
it('returns true when all reports have error', () => {
|
||||
state.sast.hasError = true;
|
||||
state.secretDetection.hasError = true;
|
||||
|
||||
expect(allReportsHaveError(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when none of the reports have error', () => {
|
||||
expect(allReportsHaveError(state)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when one of the reports does not have error', () => {
|
||||
state.secretDetection.hasError = true;
|
||||
|
||||
expect(allReportsHaveError(state)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('anyReportHasError', () => {
|
||||
it('returns true when any of the reports has error', () => {
|
||||
state.sast.hasError = true;
|
||||
|
||||
expect(anyReportHasError(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when none of the reports has error', () => {
|
||||
expect(anyReportHasError(state)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('anyReportHasIssues', () => {
|
||||
it('returns true when any of the reports has new issues', () => {
|
||||
state.sast.newIssues.push(generateVuln(LOW));
|
||||
|
||||
expect(anyReportHasIssues(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when none of the reports has error', () => {
|
||||
expect(anyReportHasIssues(state)).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import * as actions from '~/vue_shared/security_reports/store/modules/sast/actions';
|
||||
import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types';
|
||||
import createState from '~/vue_shared/security_reports/store/modules/sast/state';
|
||||
|
||||
const diffEndpoint = 'diff-endpoint.json';
|
||||
const blobPath = 'blob-path.json';
|
||||
const reports = {
|
||||
base: 'base',
|
||||
head: 'head',
|
||||
enrichData: 'enrichData',
|
||||
diff: 'diff',
|
||||
};
|
||||
const error = 'Something went wrong';
|
||||
const vulnerabilityFeedbackPath = 'vulnerability-feedback-path';
|
||||
const rootState = { vulnerabilityFeedbackPath, blobPath };
|
||||
|
||||
let state;
|
||||
|
||||
describe('sast report actions', () => {
|
||||
beforeEach(() => {
|
||||
state = createState();
|
||||
});
|
||||
|
||||
describe('setDiffEndpoint', () => {
|
||||
it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => {
|
||||
return testAction(
|
||||
actions.setDiffEndpoint,
|
||||
diffEndpoint,
|
||||
state,
|
||||
[
|
||||
{
|
||||
type: types.SET_DIFF_ENDPOINT,
|
||||
payload: diffEndpoint,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestDiff', () => {
|
||||
it(`should commit ${types.REQUEST_DIFF}`, () => {
|
||||
return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveDiffSuccess', () => {
|
||||
it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => {
|
||||
return testAction(
|
||||
actions.receiveDiffSuccess,
|
||||
reports,
|
||||
state,
|
||||
[
|
||||
{
|
||||
type: types.RECEIVE_DIFF_SUCCESS,
|
||||
payload: reports,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveDiffError', () => {
|
||||
it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => {
|
||||
return testAction(
|
||||
actions.receiveDiffError,
|
||||
error,
|
||||
state,
|
||||
[
|
||||
{
|
||||
type: types.RECEIVE_DIFF_ERROR,
|
||||
payload: error,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchDiff', () => {
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
state.paths.diffEndpoint = diffEndpoint;
|
||||
rootState.canReadVulnerabilityFeedback = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('when diff and vulnerability feedback endpoints respond successfully', () => {
|
||||
beforeEach(() => {
|
||||
mock
|
||||
.onGet(diffEndpoint)
|
||||
.replyOnce(HTTP_STATUS_OK, reports.diff)
|
||||
.onGet(vulnerabilityFeedbackPath)
|
||||
.replyOnce(HTTP_STATUS_OK, reports.enrichData);
|
||||
});
|
||||
|
||||
it('should dispatch the `receiveDiffSuccess` action', () => {
|
||||
const { diff, enrichData } = reports;
|
||||
return testAction(
|
||||
actions.fetchDiff,
|
||||
{},
|
||||
{ ...rootState, ...state },
|
||||
[],
|
||||
[
|
||||
{ type: 'requestDiff' },
|
||||
{
|
||||
type: 'receiveDiffSuccess',
|
||||
payload: {
|
||||
diff,
|
||||
enrichData,
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => {
|
||||
beforeEach(() => {
|
||||
rootState.canReadVulnerabilityFeedback = false;
|
||||
mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff);
|
||||
});
|
||||
|
||||
it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => {
|
||||
const { diff } = reports;
|
||||
const enrichData = [];
|
||||
return testAction(
|
||||
actions.fetchDiff,
|
||||
{},
|
||||
{ ...rootState, ...state },
|
||||
[],
|
||||
[
|
||||
{ type: 'requestDiff' },
|
||||
{
|
||||
type: 'receiveDiffSuccess',
|
||||
payload: {
|
||||
diff,
|
||||
enrichData,
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the vulnerability feedback endpoint fails', () => {
|
||||
beforeEach(() => {
|
||||
mock
|
||||
.onGet(diffEndpoint)
|
||||
.replyOnce(HTTP_STATUS_OK, reports.diff)
|
||||
.onGet(vulnerabilityFeedbackPath)
|
||||
.replyOnce(HTTP_STATUS_NOT_FOUND);
|
||||
});
|
||||
|
||||
it('should dispatch the `receiveError` action', () => {
|
||||
return testAction(
|
||||
actions.fetchDiff,
|
||||
{},
|
||||
{ ...rootState, ...state },
|
||||
[],
|
||||
[{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the diff endpoint fails', () => {
|
||||
beforeEach(() => {
|
||||
mock
|
||||
.onGet(diffEndpoint)
|
||||
.replyOnce(HTTP_STATUS_NOT_FOUND)
|
||||
.onGet(vulnerabilityFeedbackPath)
|
||||
.replyOnce(HTTP_STATUS_OK, reports.enrichData);
|
||||
});
|
||||
|
||||
it('should dispatch the `receiveDiffError` action', () => {
|
||||
return testAction(
|
||||
actions.fetchDiff,
|
||||
{},
|
||||
{ ...rootState, ...state },
|
||||
[],
|
||||
[{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types';
|
||||
import mutations from '~/vue_shared/security_reports/store/modules/sast/mutations';
|
||||
import createState from '~/vue_shared/security_reports/store/modules/sast/state';
|
||||
|
||||
const createIssue = ({ ...config }) => ({ changed: false, ...config });
|
||||
|
||||
describe('sast module mutations', () => {
|
||||
const path = 'path';
|
||||
let state;
|
||||
|
||||
beforeEach(() => {
|
||||
state = createState();
|
||||
});
|
||||
|
||||
describe(types.SET_DIFF_ENDPOINT, () => {
|
||||
it('should set the SAST diff endpoint', () => {
|
||||
mutations[types.SET_DIFF_ENDPOINT](state, path);
|
||||
|
||||
expect(state.paths.diffEndpoint).toBe(path);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.REQUEST_DIFF, () => {
|
||||
it('should set the `isLoading` status to `true`', () => {
|
||||
mutations[types.REQUEST_DIFF](state);
|
||||
|
||||
expect(state.isLoading).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.RECEIVE_DIFF_SUCCESS, () => {
|
||||
beforeEach(() => {
|
||||
const reports = {
|
||||
diff: {
|
||||
added: [
|
||||
createIssue({ cve: 'CVE-1' }),
|
||||
createIssue({ cve: 'CVE-2' }),
|
||||
createIssue({ cve: 'CVE-3' }),
|
||||
],
|
||||
fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })],
|
||||
existing: [createIssue({ cve: 'CVE-6' })],
|
||||
base_report_out_of_date: true,
|
||||
},
|
||||
};
|
||||
state.isLoading = true;
|
||||
mutations[types.RECEIVE_DIFF_SUCCESS](state, reports);
|
||||
});
|
||||
|
||||
it('should set the `isLoading` status to `false`', () => {
|
||||
expect(state.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should set the `baseReportOutofDate` status to `false`', () => {
|
||||
expect(state.baseReportOutofDate).toBe(true);
|
||||
});
|
||||
|
||||
it('should have the relevant `new` issues', () => {
|
||||
expect(state.newIssues).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should have the relevant `resolved` issues', () => {
|
||||
expect(state.resolvedIssues).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should have the relevant `all` issues', () => {
|
||||
expect(state.allIssues).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.RECEIVE_DIFF_ERROR, () => {
|
||||
beforeEach(() => {
|
||||
state.isLoading = true;
|
||||
mutations[types.RECEIVE_DIFF_ERROR](state);
|
||||
});
|
||||
|
||||
it('should set the `isLoading` status to `false`', () => {
|
||||
expect(state.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should set the `hasError` status to `true`', () => {
|
||||
expect(state.hasError).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import * as actions from '~/vue_shared/security_reports/store/modules/secret_detection/actions';
|
||||
import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types';
|
||||
import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
|
||||
|
||||
const diffEndpoint = 'diff-endpoint.json';
|
||||
const blobPath = 'blob-path.json';
|
||||
const reports = {
|
||||
base: 'base',
|
||||
head: 'head',
|
||||
enrichData: 'enrichData',
|
||||
diff: 'diff',
|
||||
};
|
||||
const error = 'Something went wrong';
|
||||
const vulnerabilityFeedbackPath = 'vulnerability-feedback-path';
|
||||
const rootState = { vulnerabilityFeedbackPath, blobPath };
|
||||
|
||||
let state;
|
||||
|
||||
describe('secret detection report actions', () => {
|
||||
beforeEach(() => {
|
||||
state = createState();
|
||||
});
|
||||
|
||||
describe('setDiffEndpoint', () => {
|
||||
it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => {
|
||||
return testAction(
|
||||
actions.setDiffEndpoint,
|
||||
diffEndpoint,
|
||||
state,
|
||||
[
|
||||
{
|
||||
type: types.SET_DIFF_ENDPOINT,
|
||||
payload: diffEndpoint,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestDiff', () => {
|
||||
it(`should commit ${types.REQUEST_DIFF}`, () => {
|
||||
return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveDiffSuccess', () => {
|
||||
it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => {
|
||||
return testAction(
|
||||
actions.receiveDiffSuccess,
|
||||
reports,
|
||||
state,
|
||||
[
|
||||
{
|
||||
type: types.RECEIVE_DIFF_SUCCESS,
|
||||
payload: reports,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveDiffError', () => {
|
||||
it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => {
|
||||
return testAction(
|
||||
actions.receiveDiffError,
|
||||
error,
|
||||
state,
|
||||
[
|
||||
{
|
||||
type: types.RECEIVE_DIFF_ERROR,
|
||||
payload: error,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchDiff', () => {
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
state.paths.diffEndpoint = diffEndpoint;
|
||||
rootState.canReadVulnerabilityFeedback = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('when diff and vulnerability feedback endpoints respond successfully', () => {
|
||||
beforeEach(() => {
|
||||
mock
|
||||
.onGet(diffEndpoint)
|
||||
.replyOnce(HTTP_STATUS_OK, reports.diff)
|
||||
.onGet(vulnerabilityFeedbackPath)
|
||||
.replyOnce(HTTP_STATUS_OK, reports.enrichData);
|
||||
});
|
||||
|
||||
it('should dispatch the `receiveDiffSuccess` action', () => {
|
||||
const { diff, enrichData } = reports;
|
||||
|
||||
return testAction(
|
||||
actions.fetchDiff,
|
||||
{},
|
||||
{ ...rootState, ...state },
|
||||
[],
|
||||
[
|
||||
{ type: 'requestDiff' },
|
||||
{
|
||||
type: 'receiveDiffSuccess',
|
||||
payload: {
|
||||
diff,
|
||||
enrichData,
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => {
|
||||
beforeEach(() => {
|
||||
rootState.canReadVulnerabilityFeedback = false;
|
||||
mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff);
|
||||
});
|
||||
|
||||
it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => {
|
||||
const { diff } = reports;
|
||||
const enrichData = [];
|
||||
return testAction(
|
||||
actions.fetchDiff,
|
||||
{},
|
||||
{ ...rootState, ...state },
|
||||
[],
|
||||
[
|
||||
{ type: 'requestDiff' },
|
||||
{
|
||||
type: 'receiveDiffSuccess',
|
||||
payload: {
|
||||
diff,
|
||||
enrichData,
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the vulnerability feedback endpoint fails', () => {
|
||||
beforeEach(() => {
|
||||
mock
|
||||
.onGet(diffEndpoint)
|
||||
.replyOnce(HTTP_STATUS_OK, reports.diff)
|
||||
.onGet(vulnerabilityFeedbackPath)
|
||||
.replyOnce(HTTP_STATUS_NOT_FOUND);
|
||||
});
|
||||
|
||||
it('should dispatch the `receiveDiffError` action', () => {
|
||||
return testAction(
|
||||
actions.fetchDiff,
|
||||
{},
|
||||
{ ...rootState, ...state },
|
||||
[],
|
||||
[{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the diff endpoint fails', () => {
|
||||
beforeEach(() => {
|
||||
mock
|
||||
.onGet(diffEndpoint)
|
||||
.replyOnce(HTTP_STATUS_NOT_FOUND)
|
||||
.onGet(vulnerabilityFeedbackPath)
|
||||
.replyOnce(HTTP_STATUS_OK, reports.enrichData);
|
||||
});
|
||||
|
||||
it('should dispatch the `receiveDiffError` action', () => {
|
||||
return testAction(
|
||||
actions.fetchDiff,
|
||||
{},
|
||||
{ ...rootState, ...state },
|
||||
[],
|
||||
[{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types';
|
||||
import mutations from '~/vue_shared/security_reports/store/modules/secret_detection/mutations';
|
||||
import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
|
||||
|
||||
const createIssue = ({ ...config }) => ({ changed: false, ...config });
|
||||
|
||||
describe('secret detection module mutations', () => {
|
||||
const path = 'path';
|
||||
let state;
|
||||
|
||||
beforeEach(() => {
|
||||
state = createState();
|
||||
});
|
||||
|
||||
describe(types.SET_DIFF_ENDPOINT, () => {
|
||||
it('should set the secret detection diff endpoint', () => {
|
||||
mutations[types.SET_DIFF_ENDPOINT](state, path);
|
||||
|
||||
expect(state.paths.diffEndpoint).toBe(path);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.REQUEST_DIFF, () => {
|
||||
it('should set the `isLoading` status to `true`', () => {
|
||||
mutations[types.REQUEST_DIFF](state);
|
||||
|
||||
expect(state.isLoading).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.RECEIVE_DIFF_SUCCESS, () => {
|
||||
beforeEach(() => {
|
||||
const reports = {
|
||||
diff: {
|
||||
added: [
|
||||
createIssue({ cve: 'CVE-1' }),
|
||||
createIssue({ cve: 'CVE-2' }),
|
||||
createIssue({ cve: 'CVE-3' }),
|
||||
],
|
||||
fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })],
|
||||
existing: [createIssue({ cve: 'CVE-6' })],
|
||||
base_report_out_of_date: true,
|
||||
},
|
||||
};
|
||||
state.isLoading = true;
|
||||
mutations[types.RECEIVE_DIFF_SUCCESS](state, reports);
|
||||
});
|
||||
|
||||
it('should set the `isLoading` status to `false`', () => {
|
||||
expect(state.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should set the `baseReportOutofDate` status to `true`', () => {
|
||||
expect(state.baseReportOutofDate).toBe(true);
|
||||
});
|
||||
|
||||
it('should have the relevant `new` issues', () => {
|
||||
expect(state.newIssues).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should have the relevant `resolved` issues', () => {
|
||||
expect(state.resolvedIssues).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should have the relevant `all` issues', () => {
|
||||
expect(state.allIssues).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.RECEIVE_DIFF_ERROR, () => {
|
||||
beforeEach(() => {
|
||||
state.isLoading = true;
|
||||
mutations[types.RECEIVE_DIFF_ERROR](state);
|
||||
});
|
||||
|
||||
it('should set the `isLoading` status to `false`', () => {
|
||||
expect(state.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should set the `hasError` status to `true`', () => {
|
||||
expect(state.hasError).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
import { enrichVulnerabilityWithFeedback } from '~/vue_shared/security_reports/store/utils';
|
||||
import {
|
||||
FEEDBACK_TYPE_DISMISSAL,
|
||||
FEEDBACK_TYPE_ISSUE,
|
||||
FEEDBACK_TYPE_MERGE_REQUEST,
|
||||
} from '~/vue_shared/security_reports/constants';
|
||||
|
||||
describe('security reports store utils', () => {
|
||||
const vulnerability = { uuid: 1 };
|
||||
|
||||
describe('enrichVulnerabilityWithFeedback', () => {
|
||||
const dismissalFeedback = {
|
||||
feedback_type: FEEDBACK_TYPE_DISMISSAL,
|
||||
finding_uuid: vulnerability.uuid,
|
||||
};
|
||||
const dismissalVuln = { ...vulnerability, isDismissed: true, dismissalFeedback };
|
||||
|
||||
const issueFeedback = {
|
||||
feedback_type: FEEDBACK_TYPE_ISSUE,
|
||||
issue_iid: 1,
|
||||
finding_uuid: vulnerability.uuid,
|
||||
};
|
||||
const issueVuln = { ...vulnerability, hasIssue: true, issue_feedback: issueFeedback };
|
||||
const mrFeedback = {
|
||||
feedback_type: FEEDBACK_TYPE_MERGE_REQUEST,
|
||||
merge_request_iid: 1,
|
||||
finding_uuid: vulnerability.uuid,
|
||||
};
|
||||
const mrVuln = {
|
||||
...vulnerability,
|
||||
hasMergeRequest: true,
|
||||
merge_request_feedback: mrFeedback,
|
||||
};
|
||||
|
||||
it.each`
|
||||
feedbacks | expected
|
||||
${[dismissalFeedback]} | ${dismissalVuln}
|
||||
${[{ ...issueFeedback, issue_iid: null }]} | ${vulnerability}
|
||||
${[issueFeedback]} | ${issueVuln}
|
||||
${[{ ...mrFeedback, merge_request_iid: null }]} | ${vulnerability}
|
||||
${[mrFeedback]} | ${mrVuln}
|
||||
${[dismissalFeedback, issueFeedback, mrFeedback]} | ${{ ...dismissalVuln, ...issueVuln, ...mrVuln }}
|
||||
`('returns expected enriched vulnerability: $expected', ({ feedbacks, expected }) => {
|
||||
const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks);
|
||||
|
||||
expect(enrichedVulnerability).toEqual(expected);
|
||||
});
|
||||
|
||||
it('matches correct feedback objects to vulnerability', () => {
|
||||
const feedbacks = [
|
||||
dismissalFeedback,
|
||||
issueFeedback,
|
||||
mrFeedback,
|
||||
{ ...dismissalFeedback, finding_uuid: 2 },
|
||||
{ ...issueFeedback, finding_uuid: 2 },
|
||||
{ ...mrFeedback, finding_uuid: 2 },
|
||||
];
|
||||
const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks);
|
||||
|
||||
expect(enrichedVulnerability).toEqual({ ...dismissalVuln, ...issueVuln, ...mrVuln });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import {
|
||||
REPORT_TYPE_SAST,
|
||||
REPORT_TYPE_SECRET_DETECTION,
|
||||
REPORT_FILE_TYPES,
|
||||
} from '~/vue_shared/security_reports/constants';
|
||||
import {
|
||||
extractSecurityReportArtifactsFromMergeRequest,
|
||||
extractSecurityReportArtifactsFromPipeline,
|
||||
} from '~/vue_shared/security_reports/utils';
|
||||
import {
|
||||
securityReportMergeRequestDownloadPathsQueryResponse,
|
||||
securityReportPipelineDownloadPathsQueryResponse,
|
||||
sastArtifacts,
|
||||
secretDetectionArtifacts,
|
||||
archiveArtifacts,
|
||||
traceArtifacts,
|
||||
metadataArtifacts,
|
||||
} from './mock_data';
|
||||
|
||||
describe.each([
|
||||
[
|
||||
'extractSecurityReportArtifactsFromMergeRequest',
|
||||
extractSecurityReportArtifactsFromMergeRequest,
|
||||
securityReportMergeRequestDownloadPathsQueryResponse,
|
||||
],
|
||||
[
|
||||
'extractSecurityReportArtifactsFromPipelines',
|
||||
extractSecurityReportArtifactsFromPipeline,
|
||||
securityReportPipelineDownloadPathsQueryResponse,
|
||||
],
|
||||
])('%s', (funcName, extractFunc, response) => {
|
||||
it.each`
|
||||
reportTypes | expectedArtifacts
|
||||
${[]} | ${[]}
|
||||
${['foo']} | ${[]}
|
||||
${[REPORT_TYPE_SAST]} | ${sastArtifacts}
|
||||
${[REPORT_TYPE_SECRET_DETECTION]} | ${secretDetectionArtifacts}
|
||||
${[REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION]} | ${[...secretDetectionArtifacts, ...sastArtifacts]}
|
||||
${[REPORT_FILE_TYPES.ARCHIVE]} | ${archiveArtifacts}
|
||||
${[REPORT_FILE_TYPES.TRACE]} | ${traceArtifacts}
|
||||
${[REPORT_FILE_TYPES.METADATA]} | ${metadataArtifacts}
|
||||
`(
|
||||
'returns the expected artifacts given report types $reportTypes',
|
||||
({ reportTypes, expectedArtifacts }) => {
|
||||
expect(extractFunc(reportTypes, response)).toEqual(expectedArtifacts);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema do
|
||||
let_it_be(:connections) { GitlabSchema.connections.all_wrappers }
|
||||
let_it_be(:connections) { described_class.connections.all_wrappers }
|
||||
let_it_be(:tracers) { described_class.tracers }
|
||||
|
||||
let(:user) { build :user }
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
|
|||
issuable
|
||||
)
|
||||
|
||||
GraphqlTriggers.issuable_assignees_updated(issuable)
|
||||
described_class.issuable_assignees_updated(issuable)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
|
|||
issuable
|
||||
).and_call_original
|
||||
|
||||
GraphqlTriggers.issuable_title_updated(issuable)
|
||||
described_class.issuable_title_updated(issuable)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
|
|||
issuable
|
||||
).and_call_original
|
||||
|
||||
GraphqlTriggers.issuable_description_updated(issuable)
|
||||
described_class.issuable_description_updated(issuable)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
|
|||
issuable
|
||||
)
|
||||
|
||||
GraphqlTriggers.issuable_labels_updated(issuable)
|
||||
described_class.issuable_labels_updated(issuable)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
|
|||
issuable
|
||||
).and_call_original
|
||||
|
||||
GraphqlTriggers.issuable_dates_updated(issuable)
|
||||
described_class.issuable_dates_updated(issuable)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
|
|||
issuable
|
||||
).and_call_original
|
||||
|
||||
GraphqlTriggers.issuable_milestone_updated(issuable)
|
||||
described_class.issuable_milestone_updated(issuable)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
|
|||
merge_request
|
||||
).and_call_original
|
||||
|
||||
GraphqlTriggers.merge_request_reviewers_updated(merge_request)
|
||||
described_class.merge_request_reviewers_updated(merge_request)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
|
|||
merge_request
|
||||
).and_call_original
|
||||
|
||||
GraphqlTriggers.merge_request_merge_status_updated(merge_request)
|
||||
described_class.merge_request_merge_status_updated(merge_request)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
|
|||
merge_request
|
||||
).and_call_original
|
||||
|
||||
GraphqlTriggers.merge_request_approval_state_updated(merge_request)
|
||||
described_class.merge_request_approval_state_updated(merge_request)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
|
|||
issuable
|
||||
).and_call_original
|
||||
|
||||
GraphqlTriggers.work_item_updated(issuable)
|
||||
described_class.work_item_updated(issuable)
|
||||
end
|
||||
|
||||
context 'when triggered with an Issue' do
|
||||
|
|
@ -154,7 +154,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
|
|||
work_item
|
||||
).and_call_original
|
||||
|
||||
GraphqlTriggers.work_item_updated(issue)
|
||||
described_class.work_item_updated(issue)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -105,12 +105,12 @@ RSpec.describe Types::GlobalIDType do
|
|||
around do |example|
|
||||
# Unset all previously memoized GlobalIDTypes to allow us to define one
|
||||
# that will use the constants stubbed in the `before` block.
|
||||
previous_id_types = Types::GlobalIDType.instance_variable_get(:@id_types)
|
||||
Types::GlobalIDType.instance_variable_set(:@id_types, {})
|
||||
previous_id_types = described_class.instance_variable_get(:@id_types)
|
||||
described_class.instance_variable_set(:@id_types, {})
|
||||
|
||||
example.run
|
||||
ensure
|
||||
Types::GlobalIDType.instance_variable_set(:@id_types, previous_id_types)
|
||||
described_class.instance_variable_set(:@id_types, previous_id_types)
|
||||
end
|
||||
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ require 'google/apis/core/base_service'
|
|||
RSpec.describe Google::Apis::Core::HttpCommand do # rubocop:disable RSpec/FilePath
|
||||
context('with a successful response') do
|
||||
let(:client) { Google::Apis::Core::BaseService.new('', '').client }
|
||||
let(:command) { Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') }
|
||||
let(:command) { described_class.new(:get, 'https://www.googleapis.com/zoo/animals') }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Checks::FileSizeCheck::AnyOversizedBlob, feature_category: :source_code_management do
|
||||
let_it_be(:project) { create(:project, :public, :repository) }
|
||||
let(:any_blob) do
|
||||
described_class.new(
|
||||
project: project,
|
||||
changes: [{ newrev: 'bf12d2567099e26f59692896f73ac819bae45b00' }],
|
||||
file_size_limit_megabytes: 1)
|
||||
end
|
||||
|
||||
describe '#find!' do
|
||||
subject { any_blob.find! }
|
||||
|
||||
# SHA of the 2-mb-file branch
|
||||
let(:newrev) { 'bf12d2567099e26f59692896f73ac819bae45b00' }
|
||||
let(:timeout) { nil }
|
||||
|
||||
before do
|
||||
# Delete branch so Repository#new_blobs can return results
|
||||
project.repository.delete_branch('2-mb-file')
|
||||
end
|
||||
|
||||
it 'returns the blob exceeding the file size limit' do
|
||||
blob = subject
|
||||
expect(blob).to be_kind_of(Gitlab::Git::Blob)
|
||||
expect(blob.path).to eq('file.bin')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -143,6 +143,7 @@ milestone:
|
|||
- boards
|
||||
- milestone_releases
|
||||
- releases
|
||||
- user_agent_detail
|
||||
snippets:
|
||||
- author
|
||||
- project
|
||||
|
|
|
|||
|
|
@ -33,6 +33,18 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
|
|||
]
|
||||
end
|
||||
|
||||
let_it_be(:ignored_column_model) do
|
||||
Class.new(ApplicationRecord) do
|
||||
self.table_name = 'issues'
|
||||
|
||||
include IgnorableColumns
|
||||
|
||||
ignore_column :title, remove_with: '16.4', remove_after: '2023-08-22'
|
||||
end
|
||||
end
|
||||
|
||||
let(:scope_model) { Issue }
|
||||
let(:created_records) { issues }
|
||||
let(:iterator) do
|
||||
Gitlab::Pagination::Keyset::Iterator.new(
|
||||
scope: scope.limit(batch_size),
|
||||
|
|
@ -79,6 +91,55 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the scope model has ignored columns' do
|
||||
let(:scope) { ignored_column_model.order(id: :desc) }
|
||||
let(:expected_order) { ignored_column_model.where(id: issues.map(&:id)).sort_by(&:id).reverse }
|
||||
|
||||
let(:in_operator_optimization_options) do
|
||||
{
|
||||
array_scope: Project.where(namespace_id: top_level_group.self_and_descendants.select(:id)).select(:id),
|
||||
array_mapping_scope: -> (id_expression) { ignored_column_model.where(ignored_column_model.arel_table[:project_id].eq(id_expression)) },
|
||||
finder_query: -> (id_expression) { ignored_column_model.where(ignored_column_model.arel_table[:id].eq(id_expression)) }
|
||||
}
|
||||
end
|
||||
|
||||
context 'when iterating records one by one' do
|
||||
let(:batch_size) { 1 }
|
||||
|
||||
it_behaves_like 'correct ordering examples'
|
||||
|
||||
context 'when scope selects only some columns' do
|
||||
let(:scope) { ignored_column_model.order(id: :desc).select(:id) }
|
||||
|
||||
it_behaves_like 'correct ordering examples'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when iterating records with LIMIT 3' do
|
||||
let(:batch_size) { 3 }
|
||||
|
||||
it_behaves_like 'correct ordering examples'
|
||||
|
||||
context 'when scope selects only some columns' do
|
||||
let(:scope) { ignored_column_model.order(id: :desc).select(:id) }
|
||||
|
||||
it_behaves_like 'correct ordering examples'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when loading records at once' do
|
||||
let(:batch_size) { issues.size + 1 }
|
||||
|
||||
it_behaves_like 'correct ordering examples'
|
||||
|
||||
context 'when scope selects only some columns' do
|
||||
let(:scope) { ignored_column_model.order(id: :desc).select(:id) }
|
||||
|
||||
it_behaves_like 'correct ordering examples'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ordering by issues.id DESC' do
|
||||
let(:scope) { Issue.order(id: :desc) }
|
||||
let(:expected_order) { issues.sort_by(&:id).reverse }
|
||||
|
|
@ -95,6 +156,14 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
|
|||
let(:batch_size) { 1 }
|
||||
|
||||
it_behaves_like 'correct ordering examples'
|
||||
|
||||
context 'when key_set_optimizer_ignored_columns feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(key_set_optimizer_ignored_columns: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'correct ordering examples'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when iterating records with LIMIT 3' do
|
||||
|
|
@ -332,7 +401,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
|
|||
end
|
||||
|
||||
context 'when ordering by JOIN-ed columns' do
|
||||
let(:scope) { cte_with_issues_and_projects.apply_to(Issue.where({})).reorder(order) }
|
||||
let(:scope) { cte_with_issues_and_projects.apply_to(Issue.where({}).select(Arel.star)).reorder(order) }
|
||||
|
||||
let(:cte_with_issues_and_projects) do
|
||||
cte_query = Issue.select('issues.id AS id', 'project_id', 'projects.id AS projects_id', 'projects.name AS projects_name').joins(:project)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::RecordLoaderStrategy do
|
||||
let(:finder_query) { -> (created_at_value, id_value) { Project.where(Project.arel_table[:id].eq(id_value)) } }
|
||||
let(:finder_query) { -> (created_at_value, id_value) { model.where(model.arel_table[:id].eq(id_value)) } }
|
||||
let(:model) { Project }
|
||||
|
||||
let(:keyset_scope) do
|
||||
scope, _ = Gitlab::Pagination::Keyset::SimpleOrderBuilder.build(
|
||||
Project.order(:created_at, :id)
|
||||
model.order(:created_at, :id)
|
||||
)
|
||||
|
||||
scope
|
||||
|
|
@ -22,6 +22,16 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::R
|
|||
Gitlab::Pagination::Keyset::InOperatorOptimization::OrderByColumns.new(keyset_order.column_definitions, model.arel_table)
|
||||
end
|
||||
|
||||
let_it_be(:ignored_column_model) do
|
||||
Class.new(ApplicationRecord) do
|
||||
self.table_name = 'projects'
|
||||
|
||||
include IgnorableColumns
|
||||
|
||||
ignore_column :name, remove_with: '16.4', remove_after: '2023-08-22'
|
||||
end
|
||||
end
|
||||
|
||||
subject(:strategy) { described_class.new(finder_query, model, order_by_columns) }
|
||||
|
||||
describe '#initializer_columns' do
|
||||
|
|
@ -57,4 +67,32 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::R
|
|||
expect(strategy.columns).to eq([expected_loader_query.chomp])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#final_projections' do
|
||||
context 'when model does not have ignored columns' do
|
||||
it 'does not specify the selected column names' do
|
||||
expect(strategy.final_projections).to contain_exactly("(#{described_class::RECORDS_COLUMN}).*")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when model has ignored columns' do
|
||||
let(:model) { ignored_column_model }
|
||||
|
||||
it 'specifies the selected column names' do
|
||||
expect(strategy.final_projections).to match_array(
|
||||
model.default_select_columns.map { |column| "(#{described_class::RECORDS_COLUMN}).#{column.name}" }
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the key_set_optimizer_ignored_columns feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(key_set_optimizer_ignored_columns: false)
|
||||
end
|
||||
|
||||
it 'does not specify the selected column names' do
|
||||
expect(strategy.final_projections).to contain_exactly("(#{described_class::RECORDS_COLUMN}).*")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -645,6 +645,16 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
|
|||
let_it_be(:user_2) { create(:user, created_at: five_months_ago) }
|
||||
let_it_be(:user_3) { create(:user, created_at: 1.month.ago) }
|
||||
let_it_be(:user_4) { create(:user, created_at: 2.months.ago) }
|
||||
let_it_be(:ignored_column_model) do
|
||||
Class.new(ApplicationRecord) do
|
||||
self.table_name = 'users'
|
||||
|
||||
include IgnorableColumns
|
||||
include FromUnion
|
||||
|
||||
ignore_column :username, remove_with: '16.4', remove_after: '2023-08-22'
|
||||
end
|
||||
end
|
||||
|
||||
let(:expected_results) { [user_3, user_4, user_2, user_1] }
|
||||
let(:scope) { User.order(created_at: :desc, id: :desc) }
|
||||
|
|
@ -672,6 +682,36 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
|
|||
iterator_options[:use_union_optimization] = true
|
||||
end
|
||||
|
||||
context 'when the scope model has ignored columns' do
|
||||
let(:ignored_expected_results) { expected_results.map { |r| r.becomes(ignored_column_model) } } # rubocop:disable Cop/AvoidBecomes
|
||||
|
||||
context 'when scope selects all columns' do
|
||||
let(:scope) { ignored_column_model.order(created_at: :desc, id: :desc) }
|
||||
|
||||
it 'returns items in the correct order' do
|
||||
expect(items).to eq(ignored_expected_results)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when scope selects only specific columns' do
|
||||
let(:scope) { ignored_column_model.order(created_at: :desc, id: :desc).select(:id, :created_at) }
|
||||
|
||||
it 'returns items in the correct order' do
|
||||
expect(items).to eq(ignored_expected_results)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when key_set_optimizer_ignored_columns feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(key_set_optimizer_ignored_columns: false)
|
||||
end
|
||||
|
||||
it 'returns items in the correct order' do
|
||||
expect(items).to eq(expected_results)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns items in the correct order' do
|
||||
expect(items).to eq(expected_results)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Slack::Manifest, feature_category: :integrations do
|
||||
describe '.to_h' do
|
||||
it 'creates the correct manifest' do
|
||||
expect(described_class.to_h).to eq({
|
||||
display_information: {
|
||||
name: "GitLab (#{Gitlab.config.gitlab.host})",
|
||||
description: s_('SlackIntegration|Interact with GitLab without leaving your Slack workspace!'),
|
||||
background_color: '#171321',
|
||||
long_description: "Generated for #{Gitlab.config.gitlab.host} by GitLab #{Gitlab::VERSION}.\r\n\r\n" \
|
||||
"- *Notifications:* Get notifications to your team's Slack channel about events " \
|
||||
"happening inside your GitLab projects.\r\n\r\n- *Slash commands:* Quickly open, " \
|
||||
'access, or close issues from Slack using the `/gitlab` command. Streamline your ' \
|
||||
'GitLab deployments with ChatOps.'
|
||||
},
|
||||
features: {
|
||||
app_home: {
|
||||
home_tab_enabled: true,
|
||||
messages_tab_enabled: false,
|
||||
messages_tab_read_only_enabled: true
|
||||
},
|
||||
bot_user: {
|
||||
display_name: 'GitLab',
|
||||
always_online: true
|
||||
},
|
||||
slash_commands: [
|
||||
{
|
||||
command: '/gitlab',
|
||||
url: "#{Gitlab.config.gitlab.url}/api/v4/slack/trigger",
|
||||
description: 'GitLab slash commands',
|
||||
usage_hint: 'your-project-name-or-alias command',
|
||||
should_escape: false
|
||||
}
|
||||
]
|
||||
},
|
||||
oauth_config: {
|
||||
redirect_urls: [
|
||||
Gitlab.config.gitlab.url
|
||||
],
|
||||
scopes: {
|
||||
bot: %w[
|
||||
commands
|
||||
chat:write
|
||||
chat:write.public
|
||||
]
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
event_subscriptions: {
|
||||
request_url: "#{Gitlab.config.gitlab.url}/api/v4/integrations/slack/events",
|
||||
bot_events: %w[
|
||||
app_home_opened
|
||||
]
|
||||
},
|
||||
interactivity: {
|
||||
is_enabled: true,
|
||||
request_url: "#{Gitlab.config.gitlab.url}/api/v4/integrations/slack/interactions",
|
||||
message_menu_options_url: "#{Gitlab.config.gitlab.url}/api/v4/integrations/slack/options"
|
||||
},
|
||||
org_deploy_enabled: false,
|
||||
socket_mode_enabled: false,
|
||||
token_rotation_enabled: false
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
describe '.to_json' do
|
||||
subject(:to_json) { described_class.to_json }
|
||||
|
||||
shared_examples 'a manifest that matches the JSON schema' do
|
||||
it { is_expected.to match_schema('slack/manifest') }
|
||||
end
|
||||
|
||||
it_behaves_like 'a manifest that matches the JSON schema'
|
||||
|
||||
context 'when the host name is very long' do
|
||||
before do
|
||||
allow(Gitlab.config.gitlab).to receive(:host).and_return('abc' * 20)
|
||||
end
|
||||
|
||||
it_behaves_like 'a manifest that matches the JSON schema'
|
||||
end
|
||||
end
|
||||
|
||||
describe '.share_url' do
|
||||
it 'URI encodes the manifest' do
|
||||
allow(described_class).to receive(:to_h).and_return({ foo: 'bar' })
|
||||
|
||||
expect(described_class.share_url).to eq('https://api.slack.com/apps?new_app=1&manifest_json=%7B%22foo%22%3A%22bar%22%7D')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -109,8 +109,14 @@ RSpec.describe Notify do
|
|||
is_expected.to have_body_text issue.description
|
||||
end
|
||||
|
||||
it 'does not add a reason header' do
|
||||
is_expected.not_to have_header('X-GitLab-NotificationReason', /.+/)
|
||||
context 'when issue is confidential' do
|
||||
before do
|
||||
issue.update_attribute(:confidential, true)
|
||||
end
|
||||
|
||||
it 'has a confidential header set to true' do
|
||||
is_expected.to have_header('X-GitLab-ConfidentialIssue', 'true')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sent with a reason' do
|
||||
|
|
@ -819,6 +825,10 @@ RSpec.describe Notify do
|
|||
let_it_be(:second_note) { create(:discussion_note_on_issue, in_reply_to: first_note, project: project) }
|
||||
let_it_be(:third_note) { create(:discussion_note_on_issue, in_reply_to: second_note, project: project) }
|
||||
|
||||
before_all do
|
||||
first_note.noteable.update_attribute(:confidential, "true")
|
||||
end
|
||||
|
||||
subject { described_class.note_issue_email(recipient.id, third_note.id) }
|
||||
|
||||
it_behaves_like 'an email sent to a user'
|
||||
|
|
@ -840,17 +850,29 @@ RSpec.describe Notify do
|
|||
it 'has X-GitLab-Discussion-ID header' do
|
||||
expect(subject.header['X-GitLab-Discussion-ID'].value).to eq(third_note.discussion.id)
|
||||
end
|
||||
|
||||
it 'has a confidential header set to true' do
|
||||
is_expected.to have_header('X-GitLab-ConfidentialIssue', 'true')
|
||||
end
|
||||
end
|
||||
|
||||
context 'individual issue comments' do
|
||||
let_it_be(:note) { create(:note_on_issue, project: project) }
|
||||
|
||||
before_all do
|
||||
note.noteable.update_attribute(:confidential, "true")
|
||||
end
|
||||
|
||||
subject { described_class.note_issue_email(recipient.id, note.id) }
|
||||
|
||||
it_behaves_like 'an email sent to a user'
|
||||
it_behaves_like 'appearance header and footer enabled'
|
||||
it_behaves_like 'appearance header and footer not enabled'
|
||||
|
||||
it 'has a confidential header set to true' do
|
||||
expect(subject.header['X-GitLab-ConfidentialIssue'].value).to eq('true')
|
||||
end
|
||||
|
||||
it 'has In-Reply-To header pointing to the issue' do
|
||||
expect(subject.header['In-Reply-To'].message_ids).to eq(["issue_#{note.noteable.id}@#{host}"])
|
||||
end
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue