Commit d24e999d authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 3abef158 16d2c280
<script>
import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
import { GlButton, GlEmptyState, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
import { getParameterByName } from '~/lib/utils/common_utils';
import SvgBlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import eventHub from '~/pipelines/event_hub';
import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
......@@ -13,12 +12,12 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
GlButton,
GlEmptyState,
GlLink,
GlLoadingIcon,
GlModal,
PipelinesTableComponent,
TablePagination,
SvgBlankState,
},
mixins: [PipelinesMixin, glFeatureFlagMixin()],
props: {
......@@ -183,12 +182,12 @@ export default {
class="prepend-top-20"
/>
<svg-blank-state
<gl-empty-state
v-else-if="shouldRenderErrorState"
:svg-path="errorStateSvgPath"
:message="
:title="
s__(`Pipelines|There was an error fetching the pipelines.
Try again in a few moments or contact your support team.`)
Try again in a few moments or contact your support team.`)
"
/>
......
<script>
export default {
name: 'PipelinesSvgState',
props: {
svgPath: {
type: String,
required: true,
},
message: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="row empty-state">
<div class="col-12">
<div class="svg-content"><img :src="svgPath" /></div>
</div>
<div class="col-12 text-center">
<div class="text-content">
<h4>{{ message }}</h4>
</div>
</div>
</div>
</template>
<script>
import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { isEqual } from 'lodash';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { getParameterByName } from '~/lib/utils/common_utils';
......@@ -10,7 +10,6 @@ import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../
import PipelinesMixin from '../../mixins/pipelines_mixin';
import PipelinesService from '../../services/pipelines_service';
import { validateParams } from '../../utils';
import SvgBlankState from './blank_state.vue';
import EmptyState from './empty_state.vue';
import NavigationControls from './nav_controls.vue';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
......@@ -19,13 +18,13 @@ import PipelinesTableComponent from './pipelines_table.vue';
export default {
components: {
EmptyState,
GlEmptyState,
GlIcon,
GlLoadingIcon,
NavigationTabs,
NavigationControls,
PipelinesFilteredSearch,
PipelinesTableComponent,
SvgBlankState,
TablePagination,
},
mixins: [PipelinesMixin],
......@@ -333,19 +332,19 @@ export default {
:can-set-ci="canCreatePipeline"
/>
<svg-blank-state
<gl-empty-state
v-else-if="stateToRender === $options.stateMap.error"
:svg-path="errorStateSvgPath"
:message="
:title="
s__(`Pipelines|There was an error fetching the pipelines.
Try again in a few moments or contact your support team.`)
"
/>
<svg-blank-state
<gl-empty-state
v-else-if="stateToRender === $options.stateMap.emptyTab"
:svg-path="noPipelinesSvgPath"
:message="emptyTabMessage"
:title="emptyTabMessage"
/>
<div v-else-if="stateToRender === $options.stateMap.tableList">
......
......@@ -48,8 +48,16 @@ export default {
legacyTableMobileClass() {
return !this.glFeatures.newPipelinesTable ? 'table-mobile-content' : '';
},
singleStagePipelineManual() {
return (
this.pipeline.details.manual_actions.length > 0 && this.pipeline.details.stages.length === 1
);
},
showInProgress() {
return !this.duration && !this.finishedTime;
return !this.duration && !this.finishedTime && !this.singleStagePipelineManual;
},
showSkipped() {
return !this.duration && !this.finishedTime && this.singleStagePipelineManual;
},
},
};
......@@ -65,6 +73,11 @@ export default {
{{ s__('Pipeline|In progress') }}
</span>
<span v-if="showSkipped" data-testid="pipeline-skipped">
<gl-icon name="status_skipped_borderless" class="gl-mr-2" :size="16" />
{{ s__('Pipeline|Skipped') }}
</span>
<p v-if="duration" class="duration">
<gl-icon name="timer" class="gl-vertical-align-baseline!" :size="12" />
{{ durationFormatted }}
......
......@@ -71,7 +71,7 @@ class Projects::ServicesController < Projects::ApplicationController
end
result[:data].presence || {}
rescue Gitlab::HTTP::BlockedUrlError => e
rescue *Gitlab::HTTP::HTTP_ERRORS => e
{ error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: e.message, test_failed: true }
end
......
......@@ -995,6 +995,12 @@ class Repository
raw_repository.search_files_by_name(query, ref)
end
def search_files_by_wildcard_path(path, ref = 'HEAD')
# We need to use RE2 to match Gitaly's regexp engine
regexp_string = RE2::Regexp.escape(path).gsub('\*', '.*?')
raw_repository.search_files_by_regexp("^#{regexp_string}$", ref)
end
def copy_gitattributes(ref)
actual_ref = ref || root_ref
begin
......
......@@ -2,7 +2,7 @@
- user_status_data = user_status_properties(current_user)
%header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } }
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
%a.gl-sr-only.gl-accessibility{ href: "#content-body" } Skip to content
.container-fluid
.header-content
.title-container
......
---
title: Enable :jira_issues_show_integration feature flag by default
merge_request: 56182
author:
type: added
---
title: Remove tabindex on skip link that could negatively impact keyboard focus management
and order
merge_request: 55756
author:
type: other
---
title: Support newlines for the chatops "run" command
merge_request: 56668
author:
type: changed
---
title: Adds skipped state to duration cell for single stage manual pipelines
merge_request: 56669
author:
type: changed
---
title: Catch network errors
merge_request: 56457
author: Shubham Kumar (@imskr)
type: fixed
......@@ -8,10 +8,11 @@ product_category: collection
value_type: string
status: data_available
time_frame: none
data_source:
data_source: ruby
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate
......@@ -8121,11 +8121,11 @@ The dismissal reason of the Vulnerability.
| Value | Description |
| ----- | ----------- |
| `ACCEPTABLE_RISK` | The likelihood of the Vulnerability occurring and its impact are deemed acceptable |
| `FALSE_POSITIVE` | The Vulnerability was incorrectly identified as being present |
| `MITIGATING_CONTROL` | There is a mitigating control that eliminates the Vulnerability or makes its risk acceptable |
| `NOT_APPLICABLE` | Other reasons for dismissal |
| `USED_IN_TESTS` | The Vulnerability is used in tests and does not pose an actual risk |
| `ACCEPTABLE_RISK` | The vulnerability is known, and has not been remediated or mitigated, but is considered to be an acceptable business risk. |
| `FALSE_POSITIVE` | An error in reporting in which a test result incorrectly indicates the presence of a vulnerability in a system when the vulnerability is not present. |
| `MITIGATING_CONTROL` | A management, operational, or technical control (that is, safeguard or countermeasure) employed by an organization that provides equivalent or comparable protection for an information system. |
| `NOT_APPLICABLE` | The vulnerability is known, and has not been remediated or mitigated, but is considered to be in a part of the application that will not be updated. |
| `USED_IN_TESTS` | The finding is not a vulnerability because it is part of a test or is test data. |
### `VulnerabilityExternalIssueLinkExternalTracker`
......
This diff is collapsed.
......@@ -7158,7 +7158,7 @@ Group: `group::product intelligence`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `recording_ee_finished_at`
......
......@@ -50,16 +50,16 @@ These notification settings apply only to you. They do not affect the notificati
## Global notification settings
Your **Global notification settings** are the default settings unless you select different values for a project or a group.
Your **Global notification settings** are the default settings unless you select
different values for a project or a group.
- Notification email
- This is the email address your notifications are sent to.
- Global notification level
- This is the default [notification level](#notification-levels) which applies to all your notifications.
- Receive product marketing emails
- Check this checkbox if you want to receive periodic emails related to GitLab features.
- Receive notifications about your own activity.
- Check this checkbox if you want to receive notification about your own activity. Default: Not checked.
- **Notification email**: The email address your notifications are sent to.
- **Global notification level**: The default [notification level](#notification-levels)
which applies to all your notifications.
- **Receive product marketing emails**: Select this check box to receive periodic
emails about GitLab features.
- **Receive notifications about your own activity**: Select this check box to receive
notifications about your own activity. Not selected by default.
### Notification scope
......@@ -67,16 +67,16 @@ You can tune the scope of your notifications by selecting different notification
Notification scope is applied in order of precedence (highest to lowest):
- Project
- For each project, you can select a notification level. Your project setting overrides the group setting.
- Group
- For each group, you can select a notification level. Your group setting overrides your default setting.
- Global (default)
- Your global, or _default_, notification level applies if you have not selected a notification level for the project or group in which the activity occurred.
- **Project**: For each project, you can select a notification level. Your project
setting overrides the group setting.
- **Group**: For each group, you can select a notification level. Your group setting
overrides your default setting.
- **Global (default)**: Your global, or _default_, notification level applies if you
have not selected a notification level for the project or group in which the activity occurred.
#### Project notifications
You can select a notification level for each project. This can be useful if you need to closely monitor activity in select projects.
You can select a notification level for each project to help you closely monitor activity in select projects.
![notification settings](img/notification_project_settings_v12_8.png)
......
......@@ -242,19 +242,38 @@ with a link to the commit that resolved the issue.
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3622) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
You can browse and search issues from a selected Jira project directly in GitLab. This requires [configuration](#configure-gitlab) in GitLab by an administrator.
You can browse, search, and view issues from a selected Jira project directly in GitLab,
if your GitLab administrator [has configured it](#configure-gitlab):
![Jira issues integration enabled](img/jira/open_jira_issues_list_v13.2.png)
1. In the left navigation bar, go to **Jira > Issues list**.
1. The issue list sorts by **Created date** by default, with the newest issues listed at the top:
From the **Jira Issues** menu, click **Issues List**. The issue list defaults to sort by **Created date**, with the newest issues listed at the top. You can change this to **Last updated**.
![Jira issues integration enabled](img/jira/open_jira_issues_list_v13.2.png)
Issues are grouped into tabs based on their [Jira status](https://confluence.atlassian.com/adminjiraserver070/defining-status-field-values-749382903.html).
1. To display the most recently updated issues first, click **Last updated**.
1. In GitLab versions 13.10 and later, you can view [individual Jira issues](#view-a-jira-issue).
Issues are grouped into tabs based on their [Jira status](https://confluence.atlassian.com/adminjiraserver070/defining-status-field-values-749382903.html):
- The **Open** tab displays all issues with a Jira status in any category other than Done.
- The **Closed** tab displays all issues with a Jira status categorized as Done.
- The **All** tab displays all issues of any status.
Click an issue title to open its original Jira issue page for full details.
#### View a Jira issue
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299832) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.10.
> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [enable it](#enable-or-disable-jira-issue-detail-view). **(PREMIUM)**
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
When viewing the [Jira issues list](#view-jira-issues), select an issue from the
list to open it in GitLab:
![Jira issue detail view](img/jira/jira_issue_detail_view_v13.10.png)
#### Search and filter the issues list
......@@ -304,3 +323,22 @@ which may lead to a `401 unauthorized` error when testing your Jira integration.
If CAPTCHA has been triggered, you can't use Jira's REST API to
authenticate with the Jira site. You need to log in to your Jira instance
and complete the CAPTCHA.
## Enable or disable Jira issue detail view
Jira issue detail view is under development but ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:jira_issues_show_integration)
```
To disable it:
```ruby
Feature.disable(:jira_issues_show_integration)
```
......@@ -4,7 +4,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { SAVE_ERROR } from '../constants';
import createComplianceFrameworkMutation from '../graphql/queries/create_compliance_framework.mutation.graphql';
import { initialiseFormData } from '../utils';
import { getSubmissionParams, initialiseFormData } from '../utils';
import FormStatus from './form_status.vue';
import SharedForm from './shared_form.vue';
......@@ -51,18 +51,16 @@ export default {
this.errorMessage = '';
try {
const { name, description, pipelineConfigurationFullPath, color } = this.formData;
const params = getSubmissionParams(
this.formData,
this.pipelineConfigurationFullPathEnabled,
);
const { data } = await this.$apollo.mutate({
mutation: createComplianceFrameworkMutation,
variables: {
input: {
namespacePath: this.groupPath,
params: {
name,
description,
pipelineConfigurationFullPath,
color,
},
params,
},
},
});
......
......@@ -7,7 +7,7 @@ import { __ } from '~/locale';
import { FETCH_ERROR, SAVE_ERROR } from '../constants';
import getComplianceFrameworkQuery from '../graphql/queries/get_compliance_framework.query.graphql';
import updateComplianceFrameworkMutation from '../graphql/queries/update_compliance_framework.mutation.graphql';
import { initialiseFormData } from '../utils';
import { getSubmissionParams, initialiseFormData } from '../utils';
import FormStatus from './form_status.vue';
import SharedForm from './shared_form.vue';
......@@ -114,18 +114,16 @@ export default {
this.saveErrorMessage = '';
try {
const { name, description, pipelineConfigurationFullPath, color } = this.formData;
const params = getSubmissionParams(
this.formData,
this.pipelineConfigurationFullPathEnabled,
);
const { data } = await this.$apollo.mutate({
mutation: updateComplianceFrameworkMutation,
variables: {
input: {
id: this.graphqlId,
params: {
name,
description,
pipelineConfigurationFullPath,
color,
},
params,
},
},
});
......
......@@ -18,6 +18,16 @@ export const initialiseFormData = () => ({
color: null,
});
export const getSubmissionParams = (formData, pipelineConfigurationFullPathEnabled) => {
const params = { ...formData };
if (!pipelineConfigurationFullPathEnabled) {
delete params.pipelineConfigurationFullPath;
}
return params;
};
export const getPipelineConfigurationPathParts = (path) => {
const [, file, group, project] = path.match(PIPELINE_CONFIGURATION_PATH_FORMAT) || [];
......
......@@ -9,11 +9,11 @@ module Vulnerabilities
description 'The dismissal reason of the Vulnerability'
define do
acceptable_risk value: 0, description: 'The likelihood of the Vulnerability occurring and its impact are deemed acceptable'
false_positive value: 1, description: 'The Vulnerability was incorrectly identified as being present'
mitigating_control value: 2, description: 'There is a mitigating control that eliminates the Vulnerability or makes its risk acceptable'
used_in_tests value: 3, description: 'The Vulnerability is used in tests and does not pose an actual risk'
not_applicable value: 4, description: 'Other reasons for dismissal'
acceptable_risk value: 0, description: _('The vulnerability is known, and has not been remediated or mitigated, but is considered to be an acceptable business risk.')
false_positive value: 1, description: _('An error in reporting in which a test result incorrectly indicates the presence of a vulnerability in a system when the vulnerability is not present.')
mitigating_control value: 2, description: _('A management, operational, or technical control (that is, safeguard or countermeasure) employed by an organization that provides equivalent or comparable protection for an information system.')
used_in_tests value: 3, description: _('The finding is not a vulnerability because it is part of a test or is test data.')
not_applicable value: 4, description: _('The vulnerability is known, and has not been remediated or mitigated, but is considered to be in a part of the application that will not be updated.')
end
end
end
# frozen_string_literal: true
module VulnerabilitiesHelper
FINDING_FIELDS = %i[metadata identifiers name issue_feedback merge_request_feedback project project_fingerprint scanner uuid details].freeze
FINDING_FIELDS = %i[metadata identifiers name issue_feedback merge_request_feedback project project_fingerprint scanner uuid details dismissal_feedback].freeze
def vulnerability_details_json(vulnerability, pipeline)
vulnerability_details(vulnerability, pipeline).to_json
......
......@@ -49,6 +49,11 @@ class Vulnerabilities::FeedbackEntity < Grape::Entity
end
expose :project_fingerprint
expose :dismissal_reason
expose :dismissal_descriptions do |feedback|
Vulnerabilities::DismissalReasonEnum.definition.transform_values { |v| v[:description] }
end
alias_method :feedback, :object
private
......
---
title: Expose dismissal reason and dismissal descriptions in Vulnerability details
view
merge_request: 55525
author:
type: added
......@@ -20,6 +20,7 @@ FactoryBot.define do
trait :dismissal do
feedback_type { 'dismissal' }
dismissal_reason { 'acceptable_risk' }
end
trait :comment do
......
......@@ -37,7 +37,11 @@
"project_fingerprint": { "type": "string" },
"branch": { "type": ["string", "null"] },
"destroy_vulnerability_feedback_dismissal_path": { "type": "string" },
"finding_uuid": { "type": ["string", "null"] }
"finding_uuid": { "type": ["string", "null"] },
"dismissal_reason": { "type": ["string", "null"] },
"dismissal_descriptions": {
"type": {"string": "string"}
}
},
"additionalProperties": false
}
......@@ -41,6 +41,43 @@ describe('Utils', () => {
});
});
describe('getSubmissionParams', () => {
const baseFormData = {
name: 'a',
description: 'b',
color: '#000',
};
it.each([true, false])(
'should return the initial object when pipelineConfigurationFullPath is undefined and pipelineConfigurationFullPathEnabled is %s',
(enabled) => {
expect(Utils.getSubmissionParams(baseFormData, enabled)).toStrictEqual(baseFormData);
},
);
it.each`
pipelineConfigurationFullPath | pipelineConfigurationFullPathEnabled
${'a/b'} | ${true}
${null} | ${true}
${'a/b'} | ${false}
${null} | ${false}
`(
'should return the correct object when pipelineConfigurationFullPathEnabled is $pipelineConfigurationFullPathEnabled',
({ pipelineConfigurationFullPath, pipelineConfigurationFullPathEnabled }) => {
const formData = Utils.getSubmissionParams(
{ ...baseFormData, pipelineConfigurationFullPath },
pipelineConfigurationFullPathEnabled,
);
if (pipelineConfigurationFullPathEnabled) {
expect(formData).toStrictEqual({ ...baseFormData, pipelineConfigurationFullPath });
} else {
expect(formData).toStrictEqual(baseFormData);
}
},
);
});
describe('getPipelineConfigurationPathParts', () => {
it.each`
path | parts
......
......@@ -4,9 +4,9 @@ require 'spec_helper'
RSpec.describe VulnerabilitiesHelper do
let_it_be(:user) { create(:user) }
let(:project) { create(:project, :repository, :public) }
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
let(:finding) { create(:vulnerabilities_finding, pipelines: [pipeline], project: project, severity: :high) }
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) }
let_it_be(:finding) { create(:vulnerabilities_finding, pipelines: [pipeline], project: project, severity: :high) }
let(:vulnerability) { create(:vulnerability, title: "My vulnerability", project: project, findings: [finding]) }
before do
......@@ -43,7 +43,7 @@ RSpec.describe VulnerabilitiesHelper do
:details)
end
let(:desired_serializer_fields) { %i[metadata identifiers name issue_feedback merge_request_feedback project project_fingerprint scanner uuid details] }
let(:desired_serializer_fields) { %i[metadata identifiers name issue_feedback merge_request_feedback project project_fingerprint scanner uuid details dismissal_feedback] }
before do
vulnerability_serializer_stub = instance_double("VulnerabilitySerializer")
......@@ -270,7 +270,8 @@ RSpec.describe VulnerabilitiesHelper do
assets: kind_of(Array),
supporting_messages: kind_of(Array),
uuid: kind_of(String),
details: kind_of(Hash)
details: kind_of(Hash),
dismissal_feedback: anything
)
expect(subject[:location]['blob_path']).to match(kind_of(String))
......@@ -286,6 +287,17 @@ RSpec.describe VulnerabilitiesHelper do
expect(subject[:location]).not_to have_key('blob_path')
end
end
context 'with existing dismissal feedback' do
let_it_be(:feedback) { create(:vulnerability_feedback, :comment, :dismissal, project: project, pipeline: pipeline, project_fingerprint: finding.project_fingerprint) }
it 'returns dismissal feedback information', :aggregate_failures do
dismissal_feedback = subject[:dismissal_feedback]
expect(dismissal_feedback[:dismissal_reason]).to eq(feedback.dismissal_reason)
expect(dismissal_feedback[:dismissal_descriptions]).to eq(Vulnerabilities::DismissalReasonEnum.definition.transform_values { |v| v[:description] })
expect(dismissal_feedback[:comment_details][:comment]).to eq(feedback.comment)
end
end
end
describe '#vulnerability_scan_data?' do
......
......@@ -178,4 +178,28 @@ RSpec.describe Vulnerabilities::FeedbackEntity do
expect(subject[:finding_uuid]).to eq(finding.uuid)
end
end
context 'when dismissal_reason is not present' do
let(:feedback) { build_stubbed(:vulnerability_feedback, :issue, project: project) }
it "returns nil" do
expect(subject[:dismissal_reason]).to be_nil
end
end
context 'when dismissal_reason is present' do
let(:feedback) { build_stubbed(:vulnerability_feedback, :dismissal, project: project) }
it 'exposes dismissal_reason' do
expect(subject[:dismissal_reason]).to eq(feedback.dismissal_reason)
end
end
context 'when dismissal descriptions are available' do
let(:feedback) { build_stubbed(:vulnerability_feedback, :dismissal, project: project) }
it 'exposes dismissal_descriptions' do
expect(subject[:dismissal_descriptions]).to eq(Vulnerabilities::DismissalReasonEnum.definition.transform_values { |v| v[:description] })
end
end
end
......@@ -1017,6 +1017,10 @@ module Gitlab
gitaly_repository_client.search_files_by_name(ref, safe_query)
end
def search_files_by_regexp(filter, ref = 'HEAD')
gitaly_repository_client.search_files_by_regexp(ref, filter)
end
def find_commits_by_message(query, ref, path, limit, offset)
wrapped_gitaly_errors do
gitaly_commit_client
......
......@@ -339,6 +339,11 @@ module Gitlab
search_results_from_response(response, options)
end
def search_files_by_regexp(ref, filter)
request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: '.', filter: filter)
GitalyClient.call(@storage, :repository_service, :search_files_by_name, request, timeout: GitalyClient.fast_timeout).flat_map(&:files)
end
def disconnect_alternates
request = Gitaly::DisconnectGitAlternatesRequest.new(
repository: @gitaly_repo
......
......@@ -5,7 +5,7 @@ module Gitlab
# Slash command for triggering chatops jobs.
class Run < BaseCommand
def self.match(text)
/\Arun\s+(?<command>\S+)(\s+(?<arguments>.+))?\z/.match(text)
/\Arun\s+(?<command>\S+)(\s+(?<arguments>.+))?\z/m.match(text)
end
def self.help_message
......
......@@ -1368,6 +1368,9 @@ msgstr ""
msgid "A limit of %{ci_project_subscriptions_limit} subscriptions to or from a project applies."
msgstr ""
msgid "A management, operational, or technical control (that is, safeguard or countermeasure) employed by an organization that provides equivalent or comparable protection for an information system."
msgstr ""
msgid "A member of the abuse team will review your report as soon as possible."
msgstr ""
......@@ -3262,6 +3265,9 @@ msgstr ""
msgid "An error has occurred"
msgstr ""
msgid "An error in reporting in which a test result incorrectly indicates the presence of a vulnerability in a system when the vulnerability is not present."
msgstr ""
msgid "An error occurred adding a draft to the thread."
msgstr ""
......@@ -29992,6 +29998,9 @@ msgstr ""
msgid "The file name should have a .yml extension"
msgstr ""
msgid "The finding is not a vulnerability because it is part of a test or is test data."
msgstr ""
msgid "The following %{user} can also merge into this branch: %{branch}"
msgstr ""
......@@ -30294,6 +30303,12 @@ msgstr ""
msgid "The visualization will appear in this tab when the CI/CD configuration file is populated with valid syntax."
msgstr ""
msgid "The vulnerability is known, and has not been remediated or mitigated, but is considered to be an acceptable business risk."
msgstr ""
msgid "The vulnerability is known, and has not been remediated or mitigated, but is considered to be in a part of the application that will not be updated."
msgstr ""
msgid "The vulnerability is no longer detected. Verify the vulnerability has been fixed or removed before changing its status."
msgstr ""
......
......@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Projects::ServicesController do
include JiraServiceHelper
include AfterNextHelpers
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
......@@ -13,7 +14,6 @@ RSpec.describe Projects::ServicesController do
before do
sign_in(user)
project.add_maintainer(user)
allow(Gitlab::UrlBlocker).to receive(:validate!).and_return([URI.parse('http://example.com'), nil])
end
describe '#test' do
......@@ -114,7 +114,7 @@ RSpec.describe Projects::ServicesController do
end
context 'failure' do
it 'returns success status code and the error message' do
it 'returns an error response when the integration test fails' do
stub_request(:get, 'http://example.com/rest/api/2/serverInfo')
.to_return(status: 404)
......@@ -128,6 +128,36 @@ RSpec.describe Projects::ServicesController do
'test_failed' => true
)
end
context 'with the Slack integration' do
let_it_be(:service) { build(:slack_service) }
it 'returns an error response when the URL is blocked' do
put :test, params: project_params(service: { webhook: 'http://127.0.0.1' })
expect(response).to be_successful
expect(json_response).to eq(
'error' => true,
'message' => 'Connection failed. Please check your settings.',
'service_response' => "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed",
'test_failed' => true
)
end
it 'returns an error response when a network exception is raised' do
expect_next(SlackService).to receive(:test).and_raise(Errno::ECONNREFUSED)
put :test, params: project_params
expect(response).to be_successful
expect(json_response).to eq(
'error' => true,
'message' => 'Connection failed. Please check your settings.',
'service_response' => 'Connection refused',
'test_failed' => true
)
end
end
end
end
......
import { getByText } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
import BlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
describe('Pipelines Blank State', () => {
const wrapper = mount(BlankState, {
propsData: {
svgPath: 'foo',
message: 'Blank State',
},
});
it('should render svg', () => {
expect(wrapper.find('.svg-content img').attributes('src')).toEqual('foo');
});
it('should render message', () => {
expect(getByText(wrapper.element, /Blank State/i)).toBeTruthy();
});
});
import { GlButton, GlFilteredSearch, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { GlButton, GlEmptyState, GlFilteredSearch, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { chunk } from 'lodash';
......@@ -8,8 +8,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import BlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
......@@ -58,11 +56,10 @@ describe('Pipelines', () => {
};
const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findNavigationTabs = () => wrapper.findComponent(NavigationTabs);
const findNavigationControls = () => wrapper.findComponent(NavigationControls);
const findPipelinesTable = () => wrapper.findComponent(PipelinesTableComponent);
const findEmptyState = () => wrapper.findComponent(EmptyState);
const findBlankState = () => wrapper.findComponent(BlankState);
const findTablePagination = () => wrapper.findComponent(TablePagination);
const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`);
......@@ -268,7 +265,7 @@ describe('Pipelines', () => {
});
it('should filter pipelines', async () => {
expect(findBlankState().text()).toBe('There are currently no pipelines.');
expect(findEmptyState().text()).toBe('There are currently no pipelines.');
});
it('should update browser bar', () => {
......@@ -515,7 +512,7 @@ describe('Pipelines', () => {
});
it('renders empty state', () => {
expect(findBlankState().text()).toBe('There are currently no pipelines.');
expect(findEmptyState().text()).toBe('There are currently no pipelines.');
});
it('renders tab empty state finished scope', async () => {
......@@ -528,7 +525,7 @@ describe('Pipelines', () => {
await waitForPromises();
expect(findBlankState().text()).toBe('There are currently no finished pipelines.');
expect(findEmptyState().text()).toBe('There are currently no finished pipelines.');
});
});
......@@ -599,7 +596,7 @@ describe('Pipelines', () => {
});
it('renders empty state', () => {
expect(findBlankState().text()).toBe('There are currently no pipelines.');
expect(findEmptyState().text()).toBe('There are currently no pipelines.');
});
});
});
......@@ -688,7 +685,7 @@ describe('Pipelines', () => {
});
it('shows error state', () => {
expect(findBlankState().text()).toBe(
expect(findEmptyState().text()).toBe(
'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
);
});
......@@ -713,7 +710,7 @@ describe('Pipelines', () => {
});
it('shows error state', () => {
expect(findBlankState().text()).toBe(
expect(findEmptyState().text()).toBe(
'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
);
});
......
......@@ -5,6 +5,41 @@ import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue';
describe('Timeago component', () => {
let wrapper;
const mutlipleStages = {
manual_actions: [
{
name: 'deploy_job',
path: '/root/one-stage-manual/-/jobs/1930/play',
playable: true,
scheduled: false,
},
],
stages: [
{
name: 'deploy',
},
{
name: 'qa',
},
],
};
const singleStageManual = {
manual_actions: [
{
name: 'deploy_job',
path: '/root/one-stage-manual/-/jobs/1930/play',
playable: true,
scheduled: false,
},
],
stages: [
{
name: 'deploy',
},
],
};
const createComponent = (props = {}) => {
wrapper = shallowMount(TimeAgo, {
propsData: {
......@@ -30,6 +65,7 @@ describe('Timeago component', () => {
const duration = () => wrapper.find('.duration');
const finishedAt = () => wrapper.find('.finished-at');
const findInProgress = () => wrapper.find('[data-testid="pipeline-in-progress"]');
const findSkipped = () => wrapper.find('[data-testid="pipeline-skipped"]');
describe('with duration', () => {
beforeEach(() => {
......@@ -46,7 +82,7 @@ describe('Timeago component', () => {
describe('without duration', () => {
beforeEach(() => {
createComponent({ duration: 0, finished_at: '' });
createComponent({ ...singleStageManual, duration: 0, finished_at: '' });
});
it('should not render duration and timer svg', () => {
......@@ -71,7 +107,7 @@ describe('Timeago component', () => {
describe('without finishedTime', () => {
beforeEach(() => {
createComponent({ duration: 0, finished_at: '' });
createComponent({ ...singleStageManual, duration: 0, finished_at: '' });
});
it('should not render time and calendar icon', () => {
......@@ -89,9 +125,36 @@ describe('Timeago component', () => {
`(
'progress state shown: $shouldShow when pipeline duration is $durationTime and finished_at is $finishedAtTime',
({ durationTime, finishedAtTime, shouldShow }) => {
createComponent({ duration: durationTime, finished_at: finishedAtTime });
createComponent({
...mutlipleStages,
duration: durationTime,
finished_at: finishedAtTime,
});
expect(findInProgress().exists()).toBe(shouldShow);
expect(findSkipped().exists()).toBe(false);
},
);
});
describe('skipped', () => {
it.each`
durationTime | finishedAtTime | shouldShow
${10} | ${'2017-04-26T12:40:23.277Z'} | ${false}
${10} | ${''} | ${false}
${0} | ${'2017-04-26T12:40:23.277Z'} | ${false}
${0} | ${''} | ${true}
`(
'progress state shown: $shouldShow when pipeline duration is $durationTime and finished_at is $finishedAtTime',
({ durationTime, finishedAtTime, shouldShow }) => {
createComponent({
...singleStageManual,
duration: durationTime,
finished_at: finishedAtTime,
});
expect(findSkipped().exists()).toBe(shouldShow);
expect(findInProgress().exists()).toBe(false);
},
);
});
......
......@@ -564,6 +564,41 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
end
describe '#search_files_by_regexp' do
let(:ref) { 'master' }
subject(:result) { mutable_repository.search_files_by_regexp(filter, ref) }
context 'when sending a valid regexp' do
let(:filter) { 'files\/.*\/.*\.rb' }
it 'returns matched files' do
expect(result).to contain_exactly('files/links/regex.rb',
'files/ruby/popen.rb',
'files/ruby/regex.rb',
'files/ruby/version_info.rb')
end
end
context 'when sending an ivalid regexp' do
let(:filter) { '*.rb' }
it 'raises error' do
expect { result }.to raise_error(GRPC::InvalidArgument,
/missing argument to repetition operator: `*`/)
end
end
context "when the ref doesn't exist" do
let(:filter) { 'files\/.*\/.*\.rb' }
let(:ref) { 'non-existing-branch' }
it 'returns an empty array' do
expect(result).to eq([])
end
end
end
describe '#find_remote_root_ref' do
it 'gets the remote root ref from GitalyClient' do
expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
......
......@@ -246,6 +246,21 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
end
end
describe '#search_files_by_regexp' do
subject(:result) { client.search_files_by_regexp('master', '.*') }
before do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
.to receive(:search_files_by_name)
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
.and_return([double(files: ['file1.txt']), double(files: ['file2.txt'])])
end
it 'sends a search_files_by_name message and returns a flatten array' do
expect(result).to contain_exactly('file1.txt', 'file2.txt')
end
end
describe '#disconnect_alternates' do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
......
......@@ -3,6 +3,26 @@
require 'spec_helper'
RSpec.describe Gitlab::SlashCommands::Run do
describe '.match' do
it 'returns true for a run command' do
expect(described_class.match('run foo')).to be_an_instance_of(MatchData)
end
it 'returns true for a run command with arguments' do
expect(described_class.match('run foo bar baz'))
.to be_an_instance_of(MatchData)
end
it 'returns true for a command containing newlines' do
expect(described_class.match("run foo\nbar\nbaz"))
.to be_an_instance_of(MatchData)
end
it 'returns false for an unrelated command' do
expect(described_class.match('foo bar')).to be_nil
end
end
describe '.available?' do
it 'returns true when builds are enabled for the project' do
project = double(:project, builds_enabled?: true)
......
......@@ -977,6 +977,57 @@ RSpec.describe Repository do
end
end
describe '#search_files_by_wildcard_path' do
let(:ref) { 'master' }
subject(:result) { repository.search_files_by_wildcard_path(path, ref) }
context 'when specifying a normal path' do
let(:path) { 'files/images/logo-black.png' }
it 'returns the path' do
expect(result).to eq(['files/images/logo-black.png'])
end
end
context 'when specifying a path with wildcard' do
let(:path) { 'files/*/*.png' }
it 'returns all files matching the path' do
expect(result).to contain_exactly('files/images/logo-black.png',
'files/images/logo-white.png')
end
end
context 'when specifying an extension with wildcard' do
let(:path) { '*.rb' }
it 'returns all files matching the extension' do
expect(result).to contain_exactly('encoding/russian.rb',
'files/ruby/popen.rb',
'files/ruby/regex.rb',
'files/ruby/version_info.rb')
end
end
context 'when sending regexp' do
let(:path) { '.*\.rb' }
it 'ignores the regexp and returns an empty array' do
expect(result).to eq([])
end
end
context 'when sending another ref' do
let(:path) { 'files' }
let(:ref) { 'other-branch' }
it 'returns an empty array' do
expect(result).to eq([])
end
end
end
describe '#async_remove_remote' do
before do
masterrev = repository.find_branch('master').dereferenced_target
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment