Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-07-11 09:10:29 +00:00
parent c0496e1078
commit 871b886a17
106 changed files with 1444 additions and 3284 deletions

View File

@ -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'

View File

@ -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>

View File

@ -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"
>&nbsp;<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>

View File

@ -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,

View File

@ -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>

View File

@ -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',
};

View File

@ -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>

View File

@ -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>

View File

@ -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.

View File

@ -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>

View File

@ -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';

View File

@ -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);

View File

@ -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,
});

View File

@ -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');

View File

@ -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');
});
};

View File

@ -1,10 +0,0 @@
import * as actions from './actions';
import mutations from './mutations';
import state from './state';
export default {
namespaced: true,
state,
mutations,
actions,
};

View File

@ -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';

View File

@ -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;
},
};

View File

@ -1,14 +0,0 @@
export default () => ({
paths: {
diffEndpoint: null,
},
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
baseReportOutofDate: false,
hasBaseReport: false,
});

View File

@ -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');
});
};

View File

@ -1,10 +0,0 @@
import * as actions from './actions';
import mutations from './mutations';
import state from './state';
export default {
namespaced: true,
state,
mutations,
actions,
};

View File

@ -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';

View File

@ -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;
},
};

View File

@ -1,14 +0,0 @@
export default () => ({
paths: {
diffEndpoint: null,
},
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
baseReportOutofDate: false,
hasBaseReport: false,
});

View File

@ -1,5 +0,0 @@
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
export default () => ({
reportTypes: [MODULE_SAST, MODULE_SECRET_DETECTION],
});

View File

@ -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,
};
};

View File

@ -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);
};

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -1,5 +0,0 @@
# frozen_string_literal: true
class PrometheusAlertSerializer < BaseSerializer
entity PrometheusAlertEntity
end

View File

@ -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

View File

@ -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

View File

@ -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")

View 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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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`

View File

@ -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: []
---

View File

@ -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**

View File

@ -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.

View File

@ -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).

View File

@ -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**.

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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**.

View File

@ -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**.

View File

@ -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.

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

114
lib/slack/manifest.rb Normal file
View File

@ -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

View File

@ -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 ""

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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,

View File

@ -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'

View File

@ -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"
]
}

View File

@ -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,
}
`;

View File

@ -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();
});
});
});

View File

@ -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);
});
});
});

View File

@ -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 }

View File

@ -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>
`;

View File

@ -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([]);
});
});
});

View File

@ -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',
});
});
});
});

View File

@ -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();
});
});
});

View File

@ -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',

View File

@ -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);
});
});
},
);
});
});

View File

@ -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);
});
});
});

View File

@ -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' }],
);
});
});
});
});

View File

@ -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);
});
});
});

View File

@ -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' }],
);
});
});
});
});

View File

@ -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);
});
});
});

View File

@ -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 });
});
});
});

View File

@ -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);
},
);
});

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -143,6 +143,7 @@ milestone:
- boards
- milestone_releases
- releases
- user_agent_detail
snippets:
- author
- project

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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