Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-18 18:18:16 +00:00
parent 19ab203bec
commit 9c042f0dad
27 changed files with 209 additions and 421 deletions

View File

@ -136,8 +136,8 @@ export default {
return __('Branch'); return __('Branch');
} }
}, },
commitTitleText() { commitTitle() {
return this.pipeline?.commit?.title || __("Can't find HEAD commit for this branch"); return this.pipeline?.commit?.title;
}, },
hasAuthor() { hasAuthor() {
return ( return (
@ -159,22 +159,27 @@ export default {
<div class="pipeline-tags" data-testid="pipeline-url-table-cell"> <div class="pipeline-tags" data-testid="pipeline-url-table-cell">
<template v-if="rearrangePipelinesTable"> <template v-if="rearrangePipelinesTable">
<div class="commit-title gl-mb-2" data-testid="commit-title-container"> <div class="commit-title gl-mb-2" data-testid="commit-title-container">
<span class="gl-display-flex"> <span v-if="commitTitle" class="gl-display-flex">
<tooltip-on-truncate :title="commitTitleText" class="flex-truncate-child gl-flex-grow-1"> <tooltip-on-truncate :title="commitTitle" class="flex-truncate-child gl-flex-grow-1">
<gl-link <gl-link
:href="pipeline.path" :href="commitUrl"
class="commit-row-message gl-text-blue-600!" class="commit-row-message gl-text-gray-900"
data-testid="commit-title" data-testid="commit-title"
data-qa-selector="pipeline_url_link" >{{ commitTitle }}</gl-link
>{{ commitTitleText }}</gl-link
> >
</tooltip-on-truncate> </tooltip-on-truncate>
</span> </span>
<span v-else>{{ __("Can't find HEAD commit for this branch") }}</span>
</div> </div>
<div class="gl-mb-2"> <div class="gl-mb-2">
<span class="gl-font-weight-bold gl-text-gray-500" data-testid="pipeline-identifier"> <gl-link
:href="pipeline.path"
class="gl-text-decoration-underline gl-text-blue-600!"
data-testid="pipeline-url-link"
data-qa-selector="pipeline_url_link"
>
#{{ pipeline[pipelineKey] }} #{{ pipeline[pipelineKey] }}
</span> </gl-link>
<!--Commit row--> <!--Commit row-->
<div class="icon-container gl-display-inline-block"> <div class="icon-container gl-display-inline-block">
<gl-icon <gl-icon

View File

@ -10,11 +10,11 @@ import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import getRefMixin from '../mixins/get_ref'; import getRefMixin from '../mixins/get_ref';
import blobInfoQuery from '../queries/blob_info.query.graphql'; import blobInfoQuery from '../queries/blob_info.query.graphql';
import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE } from '../constants'; import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE } from '../constants';
import BlobButtonGroup from './blob_button_group.vue'; import BlobButtonGroup from './blob_button_group.vue';
import BlobEdit from './blob_edit.vue';
import ForkSuggestion from './fork_suggestion.vue'; import ForkSuggestion from './fork_suggestion.vue';
import { loadViewer } from './blob_viewers'; import { loadViewer } from './blob_viewers';
@ -24,12 +24,12 @@ export default {
}, },
components: { components: {
BlobHeader, BlobHeader,
BlobEdit,
BlobButtonGroup, BlobButtonGroup,
BlobContent, BlobContent,
GlLoadingIcon, GlLoadingIcon,
GlButton, GlButton,
ForkSuggestion, ForkSuggestion,
WebIdeLink,
}, },
mixins: [getRefMixin, glFeatureFlagMixin()], mixins: [getRefMixin, glFeatureFlagMixin()],
inject: { inject: {
@ -213,12 +213,15 @@ export default {
@viewer-changed="switchViewer" @viewer-changed="switchViewer"
> >
<template #actions> <template #actions>
<blob-edit <web-ide-link
v-if="!blobInfo.archived" v-if="!blobInfo.archived"
:show-edit-button="!isBinaryFileType" :show-edit-button="!isBinaryFileType"
:edit-path="blobInfo.editBlobPath" class="gl-mr-3"
:web-ide-path="blobInfo.ideEditPath" :edit-url="blobInfo.editBlobPath"
:web-ide-url="blobInfo.ideEditPath"
:needs-to-fork="showForkSuggestion" :needs-to-fork="showForkSuggestion"
is-blob
disable-fork-modal
@edit="editBlob" @edit="editBlob"
/> />

View File

@ -1,46 +0,0 @@
<script>
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
export default {
components: {
WebIdeLink,
},
props: {
showEditButton: {
type: Boolean,
required: true,
},
editPath: {
type: String,
required: true,
},
webIdePath: {
type: String,
required: true,
},
needsToFork: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
onEdit(target) {
this.$emit('edit', target);
},
},
};
</script>
<template>
<web-ide-link
:show-edit-button="showEditButton"
class="gl-mr-3"
:edit-url="editPath"
:web-ide-url="webIdePath"
:needs-to-fork="needsToFork"
:is-blob="true"
disable-fork-modal
@edit="onEdit"
/>
</template>

View File

@ -111,6 +111,15 @@ class ApplicationController < ActionController::Base
render plain: e.message, status: :too_many_requests render plain: e.message, status: :too_many_requests
end end
content_security_policy do |p|
next if p.directives.blank?
next unless Gitlab::CurrentSettings.snowplow_enabled? && !Gitlab::CurrentSettings.snowplow_collector_hostname.blank?
default_connect_src = p.directives['connect-src'] || p.directives['default-src']
connect_src_values = Array.wrap(default_connect_src) | [Gitlab::CurrentSettings.snowplow_collector_hostname]
p.connect_src(*connect_src_values)
end
def redirect_back_or_default(default: root_path, options: {}) def redirect_back_or_default(default: root_path, options: {})
redirect_back(fallback_location: default, **options) redirect_back(fallback_location: default, **options)
end end

View File

@ -5,8 +5,19 @@ module Types
class ServiceTypeEnum < BaseEnum class ServiceTypeEnum < BaseEnum
graphql_name 'ServiceType' graphql_name 'ServiceType'
class << self
private
def type_description(type)
"#{type} type"
end
end
# This prepend must stay here because the dynamic block below depends on it.
prepend_mod # rubocop: disable Cop/InjectEnterpriseEditionModule
::Integration.available_integration_types(include_dev: false).each do |type| ::Integration.available_integration_types(include_dev: false).each do |type|
value type.underscore.upcase, value: type, description: "#{type} type" value type.underscore.upcase, value: type, description: type_description(type)
end end
end end
end end

View File

@ -11,7 +11,7 @@ class Integration < ApplicationRecord
include EachBatch include EachBatch
include IgnorableColumns include IgnorableColumns
ignore_column :template, remove_with: '14.10', remove_after: '2022-03-22' ignore_column :template, remove_with: '15.0', remove_after: '2022-04-22'
INTEGRATION_NAMES = %w[ INTEGRATION_NAMES = %w[
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord

View File

@ -38,6 +38,7 @@
= render 'layouts/startup_css', { startup_filename: local_assigns.fetch(:startup_filename, nil) } = render 'layouts/startup_css', { startup_filename: local_assigns.fetch(:startup_filename, nil) }
- if user_application_theme == 'gl-dark' - if user_application_theme == 'gl-dark'
%meta{ name: 'color-scheme', content: 'dark light' }
= stylesheet_link_tag_defer "application_dark" = stylesheet_link_tag_defer "application_dark"
= yield :page_specific_styles = yield :page_specific_styles
= stylesheet_link_tag_defer "application_utilities_dark" = stylesheet_link_tag_defer "application_utilities_dark"

View File

@ -714,7 +714,14 @@ module.exports = {
}, },
host: DEV_SERVER_HOST || 'localhost', host: DEV_SERVER_HOST || 'localhost',
port: DEV_SERVER_PORT || 3808, port: DEV_SERVER_PORT || 3808,
// Setting up hot module reloading
// HMR works by setting up a websocket server and injecting
// a client script which connects to that server.
// The server will push messages to the client to reload parts
// of the JavaScript or reload the page if necessary
webSocketServer: DEV_SERVER_LIVERELOAD ? 'ws' : false,
hot: DEV_SERVER_LIVERELOAD, hot: DEV_SERVER_LIVERELOAD,
liveReload: DEV_SERVER_LIVERELOAD,
// The following settings are mainly needed for HMR support in gitpod. // The following settings are mainly needed for HMR support in gitpod.
// Per default only local hosts are allowed, but here we could // Per default only local hosts are allowed, but here we could
// allow different hosts (e.g. ['.gitpod'], all of gitpod), // allow different hosts (e.g. ['.gitpod'], all of gitpod),

View File

@ -18018,6 +18018,7 @@ State of a Sentry error.
| <a id="servicetypeexternal_wiki_service"></a>`EXTERNAL_WIKI_SERVICE` | ExternalWikiService type. | | <a id="servicetypeexternal_wiki_service"></a>`EXTERNAL_WIKI_SERVICE` | ExternalWikiService type. |
| <a id="servicetypeflowdock_service"></a>`FLOWDOCK_SERVICE` | FlowdockService type. | | <a id="servicetypeflowdock_service"></a>`FLOWDOCK_SERVICE` | FlowdockService type. |
| <a id="servicetypegithub_service"></a>`GITHUB_SERVICE` | GithubService type. | | <a id="servicetypegithub_service"></a>`GITHUB_SERVICE` | GithubService type. |
| <a id="servicetypegitlab_slack_application_service"></a>`GITLAB_SLACK_APPLICATION_SERVICE` | GitlabSlackApplicationService type (Gitlab.com only). |
| <a id="servicetypehangouts_chat_service"></a>`HANGOUTS_CHAT_SERVICE` | HangoutsChatService type. | | <a id="servicetypehangouts_chat_service"></a>`HANGOUTS_CHAT_SERVICE` | HangoutsChatService type. |
| <a id="servicetypeirker_service"></a>`IRKER_SERVICE` | IrkerService type. | | <a id="servicetypeirker_service"></a>`IRKER_SERVICE` | IrkerService type. |
| <a id="servicetypejenkins_service"></a>`JENKINS_SERVICE` | JenkinsService type. | | <a id="servicetypejenkins_service"></a>`JENKINS_SERVICE` | JenkinsService type. |

View File

@ -559,6 +559,8 @@ Decreasing the user cap does not approve pending members.
When the number of billable users reaches the user cap, any new member is put in a pending state When the number of billable users reaches the user cap, any new member is put in a pending state
and must be approved. and must be approved.
Pending members do not count as billable. Members count as billable only after they have been approved and are no longer in a pending state.
Prerequisite: Prerequisite:
- You must be assigned the Owner role) for the group. - You must be assigned the Owner role) for the group.

View File

@ -20,7 +20,7 @@ The default behavior is:
explicitly, the inventory object is stored in the `default` namespace. explicitly, the inventory object is stored in the `default` namespace.
- The `name` is generated from the numeric project ID of the manifest project and the numeric agent ID. - The `name` is generated from the numeric project ID of the manifest project and the numeric agent ID.
This way the Agent constructs the name and local where the inventory object is This way, the Agent constructs the name and location where the inventory object is
stored in the cluster. stored in the cluster.
The Agent cannot locate the existing inventory object if you: The Agent cannot locate the existing inventory object if you:
@ -31,7 +31,7 @@ The Agent cannot locate the existing inventory object if you:
## Inventory object template ## Inventory object template
The inventory object template is a `ConfigMap` object that allows you to configure the namespace and the name of the inventory The inventory object template is a `ConfigMap` object that allows you to configure the namespace and the name of the inventory
object. Store this template with manifest files in a single group. object. Store this template with manifest files as a single logical group.
Example inventory object template: Example inventory object template:

View File

@ -37,8 +37,8 @@ These shortcuts are available in most areas of GitLab:
| <kbd>Shift</kbd> + <kbd>i</kbd> | Go to your Issues page. | | <kbd>Shift</kbd> + <kbd>i</kbd> | Go to your Issues page. |
| <kbd>Shift</kbd> + <kbd>m</kbd> | Go to your [Merge requests](project/merge_requests/index.md) page. | | <kbd>Shift</kbd> + <kbd>m</kbd> | Go to your [Merge requests](project/merge_requests/index.md) page. |
| <kbd>Shift</kbd> + <kbd>t</kbd> | Go to your To-Do List page. | | <kbd>Shift</kbd> + <kbd>t</kbd> | Go to your To-Do List page. |
| <kbd>p</kbd> + <kbd>b</kbd> | Show or hide the Performance Bar. | | <kbd>p</kbd> then <kbd>b</kbd> | Show or hide the Performance Bar. |
| <kbd>g</kbd> + <kbd>x</kbd> | Toggle between [GitLab](https://gitlab.com/) and [GitLab Next](https://next.gitlab.com/) (GitLab SaaS only). | | <kbd>g</kbd> then <kbd>x</kbd> | Toggle between [GitLab](https://gitlab.com/) and [GitLab Next](https://next.gitlab.com/) (GitLab SaaS only). |
| <kbd>.</kbd> | Open the [Web IDE](project/web_ide/index.md). | | <kbd>.</kbd> | Open the [Web IDE](project/web_ide/index.md). |
Additionally, the following shortcuts are available when editing text in text Additionally, the following shortcuts are available when editing text in text

View File

@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Subscriptions Content Security Policy' do RSpec.describe 'Subscriptions Content Security Policy' do
include ContentSecurityPolicyHelpers
let(:installation) { create(:jira_connect_installation) } let(:installation) { create(:jira_connect_installation) }
let(:qsh) { Atlassian::Jwt.create_query_string_hash('https://gitlab.test/subscriptions', 'GET', 'https://gitlab.test') } let(:qsh) { Atlassian::Jwt.create_query_string_hash('https://gitlab.test/subscriptions', 'GET', 'https://gitlab.test') }
let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) } let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) }
@ -11,10 +13,7 @@ RSpec.describe 'Subscriptions Content Security Policy' do
context 'when there is no global config' do context 'when there is no global config' do
before do before do
expect_next_instance_of(JiraConnect::SubscriptionsController) do |controller| setup_csp_for_controller(JiraConnect::SubscriptionsController)
expect(controller).to receive(:current_content_security_policy)
.and_return(ActionDispatch::ContentSecurityPolicy.new)
end
end end
it 'does not add CSP directives' do it 'does not add CSP directives' do
@ -31,9 +30,7 @@ RSpec.describe 'Subscriptions Content Security Policy' do
p.style_src :self, 'https://some-cdn.test' p.style_src :self, 'https://some-cdn.test'
end end
expect_next_instance_of(JiraConnect::SubscriptionsController) do |controller| setup_existing_csp_for_controller(JiraConnect::SubscriptionsController, csp)
expect(controller).to receive(:current_content_security_policy).and_return(csp)
end
end end
it 'appends to CSP directives' do it 'appends to CSP directives' do

View File

@ -64,7 +64,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do page.within('.ci-table') do
expect(page).to have_selector('.ci-created', count: 2) expect(page).to have_selector('.ci-created', count: 2)
expect(first('[data-testid="pipeline-identifier"]')).to have_content("##{detached_merge_request_pipeline.id}") expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
end end
end end
@ -101,16 +101,16 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
page.within('.ci-table') do page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 4) expect(page).to have_selector('.ci-pending', count: 4)
expect(all('[data-testid="pipeline-identifier"]')[0]) expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}") .to have_content("##{detached_merge_request_pipeline_2.id}")
expect(all('[data-testid="pipeline-identifier"]')[1]) expect(all('[data-testid="pipeline-url-link"]')[1])
.to have_content("##{detached_merge_request_pipeline.id}") .to have_content("##{detached_merge_request_pipeline.id}")
expect(all('[data-testid="pipeline-identifier"]')[2]) expect(all('[data-testid="pipeline-url-link"]')[2])
.to have_content("##{push_pipeline_2.id}") .to have_content("##{push_pipeline_2.id}")
expect(all('[data-testid="pipeline-identifier"]')[3]) expect(all('[data-testid="pipeline-url-link"]')[3])
.to have_content("##{push_pipeline.id}") .to have_content("##{push_pipeline.id}")
end end
end end
@ -201,7 +201,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees a branch pipeline in pipeline tab' do it 'sees a branch pipeline in pipeline tab' do
page.within('.ci-table') do page.within('.ci-table') do
expect(page).to have_selector('.ci-created', count: 1) expect(page).to have_selector('.ci-created', count: 1)
expect(first('[data-testid="pipeline-identifier"]')).to have_content("##{push_pipeline.id}") expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{push_pipeline.id}")
end end
end end
@ -252,7 +252,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 2) expect(page).to have_selector('.ci-pending', count: 2)
expect(first('[data-testid="pipeline-identifier"]')).to have_content("##{detached_merge_request_pipeline.id}") expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
end end
end end
@ -295,16 +295,16 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
page.within('.ci-table') do page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 4) expect(page).to have_selector('.ci-pending', count: 4)
expect(all('[data-testid="pipeline-identifier"]')[0]) expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}") .to have_content("##{detached_merge_request_pipeline_2.id}")
expect(all('[data-testid="pipeline-identifier"]')[1]) expect(all('[data-testid="pipeline-url-link"]')[1])
.to have_content("##{detached_merge_request_pipeline.id}") .to have_content("##{detached_merge_request_pipeline.id}")
expect(all('[data-testid="pipeline-identifier"]')[2]) expect(all('[data-testid="pipeline-url-link"]')[2])
.to have_content("##{push_pipeline_2.id}") .to have_content("##{push_pipeline_2.id}")
expect(all('[data-testid="pipeline-identifier"]')[3]) expect(all('[data-testid="pipeline-url-link"]')[3])
.to have_content("##{push_pipeline.id}") .to have_content("##{push_pipeline.id}")
end end
end end

View File

@ -134,7 +134,7 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
create_merge_request_pipeline create_merge_request_pipeline
act_on_security_warning(action: 'Run pipeline') act_on_security_warning(action: 'Run pipeline')
check_pipeline(expected_project: parent_project, link_selector: 'pipeline-url-link') check_pipeline(expected_project: parent_project)
check_head_pipeline(expected_project: parent_project) check_head_pipeline(expected_project: parent_project)
end end
@ -179,13 +179,13 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
click_button('Run pipeline') click_button('Run pipeline')
end end
def check_pipeline(expected_project:, link_selector: 'commit-title') def check_pipeline(expected_project:)
page.within('.ci-table') do page.within('.ci-table') do
expect(page).to have_selector('.commit', count: 2) expect(page).to have_selector('.commit', count: 2)
page.within(first('.commit')) do page.within(first('.commit')) do
page.within('.pipeline-tags') do page.within('.pipeline-tags') do
expect(page.find("[data-testid=#{link_selector}]")[:href]).to include(expected_project.full_path) expect(page.find('[data-testid="pipeline-url-link"]')[:href]).to include(expected_project.full_path)
expect(page).to have_content('detached') expect(page).to have_content('detached')
end end
page.within('.pipeline-triggerer') do page.within('.pipeline-triggerer') do

View File

@ -711,7 +711,7 @@ RSpec.describe 'Pipelines', :js do
end end
expect(page.find('[data-testid="pipeline-th"]')).to have_content 'Pipeline' expect(page.find('[data-testid="pipeline-th"]')).to have_content 'Pipeline'
expect(page.find('[data-testid="pipeline-identifier"]')).to have_content "##{pipeline.iid}" expect(page.find('[data-testid="pipeline-url-link"]')).to have_content "##{pipeline.iid}"
end end
end end
end end

View File

@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Tracings Content Security Policy' do RSpec.describe 'Tracings Content Security Policy' do
include ContentSecurityPolicyHelpers
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
@ -18,10 +20,7 @@ RSpec.describe 'Tracings Content Security Policy' do
context 'when there is no global config' do context 'when there is no global config' do
before do before do
expect_next_instance_of(Projects::TracingsController) do |controller| setup_csp_for_controller(Projects::TracingsController)
expect(controller).to receive(:current_content_security_policy)
.and_return(ActionDispatch::ContentSecurityPolicy.new)
end
end end
it 'does not add CSP directives' do it 'does not add CSP directives' do
@ -37,9 +36,7 @@ RSpec.describe 'Tracings Content Security Policy' do
p.frame_src 'https://global-policy.com' p.frame_src 'https://global-policy.com'
end end
expect_next_instance_of(Projects::TracingsController) do |controller| setup_existing_csp_for_controller(Projects::TracingsController, csp)
expect(controller).to receive(:current_content_security_policy).and_return(csp)
end
end end
context 'when external_url is set' do context 'when external_url is set' do

View File

@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Static Site Editor' do RSpec.describe 'Static Site Editor' do
include ContentSecurityPolicyHelpers
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:project) { create(:project, :public, :repository) }
@ -79,10 +81,7 @@ RSpec.describe 'Static Site Editor' do
context 'when no global CSP config exists' do context 'when no global CSP config exists' do
before do before do
expect_next_instance_of(Projects::StaticSiteEditorController) do |controller| setup_csp_for_controller(Projects::StaticSiteEditorController)
expect(controller).to receive(:current_content_security_policy)
.and_return(ActionDispatch::ContentSecurityPolicy.new)
end
end end
it 'does not add CSP directives' do it 'does not add CSP directives' do
@ -101,9 +100,7 @@ RSpec.describe 'Static Site Editor' do
p.frame_src :self, cdn_url p.frame_src :self, cdn_url
end end
expect_next_instance_of(Projects::StaticSiteEditorController) do |controller| setup_existing_csp_for_controller(Projects::StaticSiteEditorController, csp)
expect(controller).to receive(:current_content_security_policy).and_return(csp)
end
end end
it 'appends youtube to the CSP frame-src policy' do it 'appends youtube to the CSP frame-src policy' do

View File

@ -676,7 +676,7 @@ export const mockPipeline = (projectPath) => {
short_id: 'fd6df5b3', short_id: 'fd6df5b3',
created_at: '2021-10-19T21:17:12.000+00:00', created_at: '2021-10-19T21:17:12.000+00:00',
parent_ids: ['7147906b84306e83cb3fec6582a25390b75713c6'], parent_ids: ['7147906b84306e83cb3fec6582a25390b75713c6'],
title: 'Commit Title', title: 'Commit',
message: 'Commit', message: 'Commit',
author_name: 'Administrator', author_name: 'Administrator',
author_email: 'admin@example.com', author_email: 'admin@example.com',
@ -1141,176 +1141,3 @@ export const mockPipelineBranch = () => {
viewType: 'root', viewType: 'root',
}; };
}; };
export const mockPipelineNoCommit = () => {
return {
pipeline: {
id: 268,
iid: 34,
user: {
id: 1,
username: 'root',
name: 'Administrator',
state: 'active',
avatar_url:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
web_url: 'http://gdk.test:3000/root',
show_status: false,
path: '/root',
},
active: false,
source: 'push',
created_at: '2022-01-14T17:40:27.866Z',
updated_at: '2022-01-14T18:02:35.850Z',
path: '/root/mr-widgets/-/pipelines/268',
flags: {
stuck: false,
auto_devops: false,
merge_request: false,
yaml_errors: false,
retryable: true,
cancelable: false,
failure_reason: false,
detached_merge_request_pipeline: false,
merge_request_pipeline: false,
merge_train_pipeline: false,
latest: true,
},
details: {
status: {
icon: 'status_warning',
text: 'passed',
label: 'passed with warnings',
group: 'success-with-warnings',
tooltip: 'passed',
has_details: true,
details_path: '/root/mr-widgets/-/pipelines/268',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
stages: [
{
name: 'validate',
title: 'validate: passed with warnings',
status: {
icon: 'status_warning',
text: 'passed',
label: 'passed with warnings',
group: 'success-with-warnings',
tooltip: 'passed',
has_details: true,
details_path: '/root/mr-widgets/-/pipelines/268#validate',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/mr-widgets/-/pipelines/268#validate',
dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=validate',
},
{
name: 'test',
title: 'test: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/mr-widgets/-/pipelines/268#test',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/mr-widgets/-/pipelines/268#test',
dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=test',
},
{
name: 'build',
title: 'build: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/mr-widgets/-/pipelines/268#build',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/mr-widgets/-/pipelines/268#build',
dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=build',
},
],
duration: 75,
finished_at: '2022-01-14T18:02:35.842Z',
name: 'Pipeline',
manual_actions: [],
scheduled_actions: [],
},
ref: {
name: 'update-ci',
path: '/root/mr-widgets/-/commits/update-ci',
tag: false,
branch: true,
merge_request: false,
},
retry_path: '/root/mr-widgets/-/pipelines/268/retry',
delete_path: '/root/mr-widgets/-/pipelines/268',
failed_builds: [
{
id: 1260,
name: 'fmt',
started: '2022-01-14T17:40:36.435Z',
complete: true,
archived: false,
build_path: '/root/mr-widgets/-/jobs/1260',
retry_path: '/root/mr-widgets/-/jobs/1260/retry',
playable: false,
scheduled: false,
created_at: '2022-01-14T17:40:27.879Z',
updated_at: '2022-01-14T17:40:42.129Z',
status: {
icon: 'status_warning',
text: 'failed',
label: 'failed (allowed to fail)',
group: 'failed-with-warnings',
tooltip: 'failed - (script failure) (allowed to fail)',
has_details: true,
details_path: '/root/mr-widgets/-/jobs/1260',
illustration: {
image:
'/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg',
size: 'svg-430',
title: 'This job does not have a trace.',
},
favicon:
'/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
action: {
icon: 'retry',
title: 'Retry',
path: '/root/mr-widgets/-/jobs/1260/retry',
method: 'post',
button_title: 'Retry this job',
},
},
recoverable: false,
},
],
project: {
id: 23,
name: 'mr-widgets',
full_path: '/root/mr-widgets',
full_name: 'Administrator / mr-widgets',
},
triggered_by: null,
triggered: [],
},
pipelineScheduleUrl: 'foo',
pipelineKey: 'id',
viewType: 'root',
};
};

View File

@ -1,12 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue'; import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import { import { mockPipeline, mockPipelineBranch, mockPipelineTag } from './mock_data';
mockPipeline,
mockPipelineBranch,
mockPipelineTag,
mockPipelineNoCommit,
} from './mock_data';
const projectPath = 'test/test'; const projectPath = 'test/test';
@ -31,7 +26,7 @@ describe('Pipeline Url Component', () => {
const findCommitIconType = () => wrapper.findByTestId('commit-icon-type'); const findCommitIconType = () => wrapper.findByTestId('commit-icon-type');
const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container'); const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container');
const findCommitTitle = () => wrapper.findByTestId('commit-title'); const findCommitTitle = (commitWrapper) => commitWrapper.find('[data-testid="commit-title"]');
const defaultProps = mockPipeline(projectPath); const defaultProps = mockPipeline(projectPath);
@ -237,33 +232,5 @@ describe('Pipeline Url Component', () => {
expect(findCommitIconType().attributes('title')).toBe(expectedTitle); expect(findCommitIconType().attributes('title')).toBe(expectedTitle);
}, },
); );
describe('with commit', () => {
beforeEach(() => {
createComponent({}, true);
});
it('displays commit title with link to pipeline', () => {
expect(findCommitTitle().attributes('href')).toBe(defaultProps.pipeline.path);
});
it('displays commit title text', () => {
expect(findCommitTitle().text()).toBe(defaultProps.pipeline.commit.title);
});
});
describe('without commit', () => {
beforeEach(() => {
createComponent(mockPipelineNoCommit(), true);
});
it('displays cant find head commit text', () => {
expect(findCommitTitle().text()).toBe("Can't find HEAD commit for this branch");
});
it('displays link to pipeline', () => {
expect(findCommitTitle().attributes('href')).toBe(mockPipelineNoCommit().pipeline.path);
});
});
}); });
}); });

View File

@ -10,7 +10,7 @@ import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue'; import BlobHeader from '~/blob/components/blob_header.vue';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue'; import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue'; import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import BlobEdit from '~/repository/components/blob_edit.vue'; import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import ForkSuggestion from '~/repository/components/fork_suggestion.vue'; import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer } from '~/repository/components/blob_viewers'; import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue'; import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
@ -99,7 +99,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
describe('Blob content viewer component', () => { describe('Blob content viewer component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findBlobHeader = () => wrapper.findComponent(BlobHeader); const findBlobHeader = () => wrapper.findComponent(BlobHeader);
const findBlobEdit = () => wrapper.findComponent(BlobEdit); const findWebIdeLink = () => wrapper.findComponent(WebIdeLink);
const findPipelineEditor = () => wrapper.findByTestId('pipeline-editor'); const findPipelineEditor = () => wrapper.findByTestId('pipeline-editor');
const findBlobContent = () => wrapper.findComponent(BlobContent); const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup); const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
@ -255,32 +255,32 @@ describe('Blob content viewer component', () => {
describe('BlobHeader action slot', () => { describe('BlobHeader action slot', () => {
const { ideEditPath, editBlobPath } = simpleViewerMock; const { ideEditPath, editBlobPath } = simpleViewerMock;
it('renders BlobHeaderEdit buttons in simple viewer', async () => { it('renders WebIdeLink button in simple viewer', async () => {
await createComponent({ inject: { BlobContent: true, BlobReplace: true } }, mount); await createComponent({ inject: { BlobContent: true, BlobReplace: true } }, mount);
expect(findBlobEdit().props()).toMatchObject({ expect(findWebIdeLink().props()).toMatchObject({
editPath: editBlobPath, editUrl: editBlobPath,
webIdePath: ideEditPath, webIdeUrl: ideEditPath,
showEditButton: true, showEditButton: true,
}); });
}); });
it('renders BlobHeaderEdit button in rich viewer', async () => { it('renders WebIdeLink button in rich viewer', async () => {
await createComponent({ blob: richViewerMock }, mount); await createComponent({ blob: richViewerMock }, mount);
expect(findBlobEdit().props()).toMatchObject({ expect(findWebIdeLink().props()).toMatchObject({
editPath: editBlobPath, editUrl: editBlobPath,
webIdePath: ideEditPath, webIdeUrl: ideEditPath,
showEditButton: true, showEditButton: true,
}); });
}); });
it('renders BlobHeaderEdit button for binary files', async () => { it('renders WebIdeLink button for binary files', async () => {
await createComponent({ blob: richViewerMock, isBinary: true }, mount); await createComponent({ blob: richViewerMock, isBinary: true }, mount);
expect(findBlobEdit().props()).toMatchObject({ expect(findWebIdeLink().props()).toMatchObject({
editPath: editBlobPath, editUrl: editBlobPath,
webIdePath: ideEditPath, webIdeUrl: ideEditPath,
showEditButton: false, showEditButton: false,
}); });
}); });
@ -318,7 +318,7 @@ describe('Blob content viewer component', () => {
expect(findBlobHeader().props('hideViewerSwitcher')).toBe(true); expect(findBlobHeader().props('hideViewerSwitcher')).toBe(true);
expect(findBlobHeader().props('isBinary')).toBe(true); expect(findBlobHeader().props('isBinary')).toBe(true);
expect(findBlobEdit().props('showEditButton')).toBe(false); expect(findWebIdeLink().props('showEditButton')).toBe(false);
}); });
}); });
@ -401,12 +401,12 @@ describe('Blob content viewer component', () => {
beforeEach(() => createComponent({}, mount)); beforeEach(() => createComponent({}, mount));
it('simple edit redirects to the simple editor', () => { it('simple edit redirects to the simple editor', () => {
findBlobEdit().vm.$emit('edit', 'simple'); findWebIdeLink().vm.$emit('edit', 'simple');
expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath); expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath);
}); });
it('IDE edit redirects to the IDE editor', () => { it('IDE edit redirects to the IDE editor', () => {
findBlobEdit().vm.$emit('edit', 'ide'); findWebIdeLink().vm.$emit('edit', 'ide');
expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath); expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath);
}); });
@ -435,7 +435,7 @@ describe('Blob content viewer component', () => {
mount, mount,
); );
findBlobEdit().vm.$emit('edit', 'simple'); findWebIdeLink().vm.$emit('edit', 'simple');
await nextTick(); await nextTick();
expect(findForkSuggestion().exists()).toBe(showForkSuggestion); expect(findForkSuggestion().exists()).toBe(showForkSuggestion);

View File

@ -1,72 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import BlobEdit from '~/repository/components/blob_edit.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
const DEFAULT_PROPS = {
editPath: 'some_file.js/edit',
webIdePath: 'some_file.js/ide/edit',
showEditButton: true,
needsToFork: false,
};
describe('BlobEdit component', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(BlobEdit, {
propsData: {
...DEFAULT_PROPS,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findEditButton = () => wrapper.find('[data-testid="edit"]');
const findWebIdeLink = () => wrapper.find(WebIdeLink);
it('renders component', () => {
createComponent();
const { editPath, webIdePath } = DEFAULT_PROPS;
expect(wrapper.props()).toMatchObject({
editPath,
webIdePath,
});
});
it('renders WebIdeLink component', () => {
createComponent();
const { editPath: editUrl, webIdePath: webIdeUrl, needsToFork } = DEFAULT_PROPS;
expect(findWebIdeLink().props()).toMatchObject({
editUrl,
webIdeUrl,
isBlob: true,
showEditButton: true,
needsToFork,
});
});
describe('Without Edit button', () => {
const showEditButton = false;
it('renders WebIdeLink component without an edit button', () => {
createComponent({ showEditButton });
expect(findWebIdeLink().props()).toMatchObject({ showEditButton });
});
it('does not render an Edit button', () => {
createComponent({ showEditButton });
expect(findEditButton().exists()).toBe(false);
});
});
});

View File

@ -566,6 +566,12 @@ RSpec.describe Integration do
end end
end end
describe '.integration_name_to_type' do
it 'transforms the name to a type' do
expect(described_class.integration_name_to_type('asana')).to eq('AsanaService')
end
end
describe "{property}_changed?" do describe "{property}_changed?" do
let(:integration) do let(:integration) do
Integrations::Bamboo.create!( Integrations::Bamboo.create!(
@ -774,35 +780,33 @@ RSpec.describe Integration do
end end
describe '.available_integration_names' do describe '.available_integration_names' do
it 'calls the right methods' do subject { described_class.available_integration_names }
expect(described_class).to receive(:integration_names).and_call_original
expect(described_class).to receive(:dev_integration_names).and_call_original
expect(described_class).to receive(:project_specific_integration_names).and_call_original
described_class.available_integration_names before do
allow(described_class).to receive(:integration_names).and_return(%w(foo))
allow(described_class).to receive(:project_specific_integration_names).and_return(['bar'])
allow(described_class).to receive(:dev_integration_names).and_return(['baz'])
end end
it 'does not call project_specific_integration_names with include_project_specific false' do it { is_expected.to include('foo', 'bar', 'baz') }
expect(described_class).to receive(:integration_names).and_call_original
expect(described_class).to receive(:dev_integration_names).and_call_original
expect(described_class).not_to receive(:project_specific_integration_names)
described_class.available_integration_names(include_project_specific: false) context 'when `include_project_specific` is false' do
subject { described_class.available_integration_names(include_project_specific: false) }
it { is_expected.to include('foo', 'baz') }
it { is_expected.not_to include('bar') }
end end
it 'does not call dev_integration_names with include_dev false' do context 'when `include_dev` is false' do
expect(described_class).to receive(:integration_names).and_call_original subject { described_class.available_integration_names(include_dev: false) }
expect(described_class).not_to receive(:dev_integration_names)
expect(described_class).to receive(:project_specific_integration_names).and_call_original
described_class.available_integration_names(include_dev: false) it { is_expected.to include('foo', 'bar') }
it { is_expected.not_to include('baz') }
end end
it { expect(described_class.available_integration_names).to include('jenkins') }
end end
describe '.project_specific_integration_names' do describe '.project_specific_integration_names' do
it do specify do
expect(described_class.project_specific_integration_names) expect(described_class.project_specific_integration_names)
.to include(*described_class::PROJECT_SPECIFIC_INTEGRATION_NAMES) .to include(*described_class::PROJECT_SPECIFIC_INTEGRATION_NAMES)
end end

View File

@ -10,6 +10,14 @@ RSpec.describe API::Integrations do
create(:project, creator_id: user.id, namespace: user.namespace) create(:project, creator_id: user.id, namespace: user.namespace)
end end
# The API supports all integrations except the GitLab Slack Application
# integration; this integration must be installed via the UI.
def self.integration_names
names = Integration.available_integration_names
names.delete(Integrations::GitlabSlackApplication.to_param) if Gitlab.ee?
names
end
%w[integrations services].each do |endpoint| %w[integrations services].each do |endpoint|
describe "GET /projects/:id/#{endpoint}" do describe "GET /projects/:id/#{endpoint}" do
it 'returns authentication error when unauthenticated' do it 'returns authentication error when unauthenticated' do
@ -43,7 +51,7 @@ RSpec.describe API::Integrations do
end end
end end
Integration.available_integration_names.each do |integration| integration_names.each do |integration|
describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do
include_context integration include_context integration

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
require 'spec_helper'
# The AnonymousController doesn't support setting the CSP
# This is why an arbitrary test request was chosen instead
# of testing in application_controller_spec.
RSpec.describe 'Content Security Policy' do
let(:snowplow_host) { 'snowplow.example.com' }
shared_examples 'snowplow is not in the CSP' do
it 'does not add the snowplow collector hostname to the CSP' do
get explore_root_url
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Security-Policy']).not_to include(snowplow_host)
end
end
describe 'GET #explore' do
context 'snowplow is enabled' do
before do
stub_application_setting(snowplow_enabled: true, snowplow_collector_hostname: snowplow_host)
end
it 'adds the snowplow collector hostname to the CSP' do
get explore_root_url
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Security-Policy']).to include(snowplow_host)
end
end
context 'snowplow is enabled but host is not configured' do
before do
stub_application_setting(snowplow_enabled: true)
end
it_behaves_like 'snowplow is not in the CSP'
end
context 'snowplow is disabled' do
before do
stub_application_setting(snowplow_enabled: false, snowplow_collector_hostname: snowplow_host)
end
it_behaves_like 'snowplow is not in the CSP'
end
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
module ContentSecurityPolicyHelpers
# Expecting 2 calls to current_content_security_policy by default, once for
# the call that's being tested and once for the call in ApplicationController
def setup_csp_for_controller(controller_class, times = 2)
expect_next_instance_of(controller_class) do |controller|
expect(controller).to receive(:current_content_security_policy)
.and_return(ActionDispatch::ContentSecurityPolicy.new).exactly(times).times
end
end
# Expecting 2 calls to current_content_security_policy by default, once for
# the call that's being tested and once for the call in ApplicationController
def setup_existing_csp_for_controller(controller_class, csp, times = 2)
expect_next_instance_of(controller_class) do |controller|
expect(controller).to receive(:current_content_security_policy).and_return(csp).exactly(times).times
end
end
end