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

View File

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

View File

@ -5,8 +5,19 @@ module Types
class ServiceTypeEnum < BaseEnum
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|
value type.underscore.upcase, value: type, description: "#{type} type"
value type.underscore.upcase, value: type, description: type_description(type)
end
end
end

View File

@ -11,7 +11,7 @@ class Integration < ApplicationRecord
include EachBatch
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[
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) }
- if user_application_theme == 'gl-dark'
%meta{ name: 'color-scheme', content: 'dark light' }
= stylesheet_link_tag_defer "application_dark"
= yield :page_specific_styles
= stylesheet_link_tag_defer "application_utilities_dark"

View File

@ -714,7 +714,14 @@ module.exports = {
},
host: DEV_SERVER_HOST || 'localhost',
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,
liveReload: DEV_SERVER_LIVERELOAD,
// The following settings are mainly needed for HMR support in gitpod.
// Per default only local hosts are allowed, but here we could
// 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="servicetypeflowdock_service"></a>`FLOWDOCK_SERVICE` | FlowdockService 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="servicetypeirker_service"></a>`IRKER_SERVICE` | IrkerService 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
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:
- 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.
- 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.
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
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:

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>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>p</kbd> + <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>p</kbd> then <kbd>b</kbd> | Show or hide the Performance Bar. |
| <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). |
Additionally, the following shortcuts are available when editing text in text

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Subscriptions Content Security Policy' do
include ContentSecurityPolicyHelpers
let(:installation) { create(:jira_connect_installation) }
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) }
@ -11,10 +13,7 @@ RSpec.describe 'Subscriptions Content Security Policy' do
context 'when there is no global config' do
before do
expect_next_instance_of(JiraConnect::SubscriptionsController) do |controller|
expect(controller).to receive(:current_content_security_policy)
.and_return(ActionDispatch::ContentSecurityPolicy.new)
end
setup_csp_for_controller(JiraConnect::SubscriptionsController)
end
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'
end
expect_next_instance_of(JiraConnect::SubscriptionsController) do |controller|
expect(controller).to receive(:current_content_security_policy).and_return(csp)
end
setup_existing_csp_for_controller(JiraConnect::SubscriptionsController, csp)
end
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
page.within('.ci-table') do
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
@ -101,16 +101,16 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
page.within('.ci-table') do
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}")
expect(all('[data-testid="pipeline-identifier"]')[1])
expect(all('[data-testid="pipeline-url-link"]')[1])
.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}")
expect(all('[data-testid="pipeline-identifier"]')[3])
expect(all('[data-testid="pipeline-url-link"]')[3])
.to have_content("##{push_pipeline.id}")
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
page.within('.ci-table') do
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
@ -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
page.within('.ci-table') do
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
@ -295,16 +295,16 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
page.within('.ci-table') do
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}")
expect(all('[data-testid="pipeline-identifier"]')[1])
expect(all('[data-testid="pipeline-url-link"]')[1])
.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}")
expect(all('[data-testid="pipeline-identifier"]')[3])
expect(all('[data-testid="pipeline-url-link"]')[3])
.to have_content("##{push_pipeline.id}")
end
end

View File

@ -134,7 +134,7 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
create_merge_request_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)
end
@ -179,13 +179,13 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
click_button('Run pipeline')
end
def check_pipeline(expected_project:, link_selector: 'commit-title')
def check_pipeline(expected_project:)
page.within('.ci-table') do
expect(page).to have_selector('.commit', count: 2)
page.within(first('.commit')) 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')
end
page.within('.pipeline-triggerer') do

View File

@ -711,7 +711,7 @@ RSpec.describe 'Pipelines', :js do
end
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

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Tracings Content Security Policy' do
include ContentSecurityPolicyHelpers
let_it_be(:project) { create(:project) }
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
before do
expect_next_instance_of(Projects::TracingsController) do |controller|
expect(controller).to receive(:current_content_security_policy)
.and_return(ActionDispatch::ContentSecurityPolicy.new)
end
setup_csp_for_controller(Projects::TracingsController)
end
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'
end
expect_next_instance_of(Projects::TracingsController) do |controller|
expect(controller).to receive(:current_content_security_policy).and_return(csp)
end
setup_existing_csp_for_controller(Projects::TracingsController, csp)
end
context 'when external_url is set' do

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Static Site Editor' do
include ContentSecurityPolicyHelpers
let_it_be(:user) { create(:user) }
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
before do
expect_next_instance_of(Projects::StaticSiteEditorController) do |controller|
expect(controller).to receive(:current_content_security_policy)
.and_return(ActionDispatch::ContentSecurityPolicy.new)
end
setup_csp_for_controller(Projects::StaticSiteEditorController)
end
it 'does not add CSP directives' do
@ -101,9 +100,7 @@ RSpec.describe 'Static Site Editor' do
p.frame_src :self, cdn_url
end
expect_next_instance_of(Projects::StaticSiteEditorController) do |controller|
expect(controller).to receive(:current_content_security_policy).and_return(csp)
end
setup_existing_csp_for_controller(Projects::StaticSiteEditorController, csp)
end
it 'appends youtube to the CSP frame-src policy' do

View File

@ -676,7 +676,7 @@ export const mockPipeline = (projectPath) => {
short_id: 'fd6df5b3',
created_at: '2021-10-19T21:17:12.000+00:00',
parent_ids: ['7147906b84306e83cb3fec6582a25390b75713c6'],
title: 'Commit Title',
title: 'Commit',
message: 'Commit',
author_name: 'Administrator',
author_email: 'admin@example.com',
@ -1141,176 +1141,3 @@ export const mockPipelineBranch = () => {
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 { trimText } from 'helpers/text_helper';
import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import {
mockPipeline,
mockPipelineBranch,
mockPipelineTag,
mockPipelineNoCommit,
} from './mock_data';
import { mockPipeline, mockPipelineBranch, mockPipelineTag } from './mock_data';
const projectPath = 'test/test';
@ -31,7 +26,7 @@ describe('Pipeline Url Component', () => {
const findCommitIconType = () => wrapper.findByTestId('commit-icon-type');
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);
@ -237,33 +232,5 @@ describe('Pipeline Url Component', () => {
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 BlobButtonGroup from '~/repository/components/blob_button_group.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 { loadViewer } from '~/repository/components/blob_viewers';
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', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findBlobHeader = () => wrapper.findComponent(BlobHeader);
const findBlobEdit = () => wrapper.findComponent(BlobEdit);
const findWebIdeLink = () => wrapper.findComponent(WebIdeLink);
const findPipelineEditor = () => wrapper.findByTestId('pipeline-editor');
const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
@ -255,32 +255,32 @@ describe('Blob content viewer component', () => {
describe('BlobHeader action slot', () => {
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);
expect(findBlobEdit().props()).toMatchObject({
editPath: editBlobPath,
webIdePath: ideEditPath,
expect(findWebIdeLink().props()).toMatchObject({
editUrl: editBlobPath,
webIdeUrl: ideEditPath,
showEditButton: true,
});
});
it('renders BlobHeaderEdit button in rich viewer', async () => {
it('renders WebIdeLink button in rich viewer', async () => {
await createComponent({ blob: richViewerMock }, mount);
expect(findBlobEdit().props()).toMatchObject({
editPath: editBlobPath,
webIdePath: ideEditPath,
expect(findWebIdeLink().props()).toMatchObject({
editUrl: editBlobPath,
webIdeUrl: ideEditPath,
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);
expect(findBlobEdit().props()).toMatchObject({
editPath: editBlobPath,
webIdePath: ideEditPath,
expect(findWebIdeLink().props()).toMatchObject({
editUrl: editBlobPath,
webIdeUrl: ideEditPath,
showEditButton: false,
});
});
@ -318,7 +318,7 @@ describe('Blob content viewer component', () => {
expect(findBlobHeader().props('hideViewerSwitcher')).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));
it('simple edit redirects to the simple editor', () => {
findBlobEdit().vm.$emit('edit', 'simple');
findWebIdeLink().vm.$emit('edit', 'simple');
expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath);
});
it('IDE edit redirects to the IDE editor', () => {
findBlobEdit().vm.$emit('edit', 'ide');
findWebIdeLink().vm.$emit('edit', 'ide');
expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath);
});
@ -435,7 +435,7 @@ describe('Blob content viewer component', () => {
mount,
);
findBlobEdit().vm.$emit('edit', 'simple');
findWebIdeLink().vm.$emit('edit', 'simple');
await nextTick();
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
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
let(:integration) do
Integrations::Bamboo.create!(
@ -774,35 +780,33 @@ RSpec.describe Integration do
end
describe '.available_integration_names' do
it 'calls the right methods' do
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
subject { described_class.available_integration_names }
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
it 'does not call project_specific_integration_names with include_project_specific false' do
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)
it { is_expected.to include('foo', 'bar', 'baz') }
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
it 'does not call dev_integration_names with include_dev false' do
expect(described_class).to receive(:integration_names).and_call_original
expect(described_class).not_to receive(:dev_integration_names)
expect(described_class).to receive(:project_specific_integration_names).and_call_original
context 'when `include_dev` is false' do
subject { described_class.available_integration_names(include_dev: false) }
described_class.available_integration_names(include_dev: false)
it { is_expected.to include('foo', 'bar') }
it { is_expected.not_to include('baz') }
end
it { expect(described_class.available_integration_names).to include('jenkins') }
end
describe '.project_specific_integration_names' do
it do
specify do
expect(described_class.project_specific_integration_names)
.to include(*described_class::PROJECT_SPECIFIC_INTEGRATION_NAMES)
end

View File

@ -10,6 +10,14 @@ RSpec.describe API::Integrations do
create(:project, creator_id: user.id, namespace: user.namespace)
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|
describe "GET /projects/:id/#{endpoint}" do
it 'returns authentication error when unauthenticated' do
@ -43,7 +51,7 @@ RSpec.describe API::Integrations do
end
end
Integration.available_integration_names.each do |integration|
integration_names.each do |integration|
describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do
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