Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-04-22 12:08:38 +00:00
parent 748ab12e20
commit ae567e129f
62 changed files with 546 additions and 303 deletions

View File

@ -44,6 +44,7 @@ After your merge request has been approved according to our [approval guidelines
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
- [ ] If this includes a breaking change, make sure it is mentioned for the relevant versions in [`doc/update/index.md`](https://gitlab.com/gitlab-org/security/gitlab/-/blob/master/doc/update/index.md#version-specific-upgrading-instructions)
## Summary

View File

@ -1053,7 +1053,7 @@ RSpec/VerifiedDoubles:
- spec/services/packages/nuget/metadata_extraction_service_spec.rb
- spec/services/pages/zip_directory_service_spec.rb
- spec/services/post_receive_service_spec.rb
- spec/services/projects/after_import_service_spec.rb
- spec/workers/projects/after_import_worker_spec.rb
- spec/services/projects/branches_by_mode_service_spec.rb
- spec/services/projects/create_service_spec.rb
- spec/services/projects/destroy_service_spec.rb

View File

@ -2,7 +2,7 @@ import { Extension } from '@tiptap/core';
import { Plugin, PluginKey } from 'prosemirror-state';
import { __ } from '~/locale';
import { VARIANT_DANGER } from '~/flash';
import createMarkdownDeserializer from '../services/markdown_deserializer';
import createMarkdownDeserializer from '../services/gl_api_markdown_deserializer';
import {
ALERT_EVENT,
LOADING_CONTENT_EVENT,

View File

@ -36,16 +36,6 @@ const codeBlockLanguageLoader = {
return this.lowlight.registered(language);
},
loadLanguagesFromDOM(domTree) {
const languages = [];
domTree.querySelectorAll('pre').forEach((preElement) => {
languages.push(preElement.getAttribute('lang'));
});
return this.loadLanguages(languages);
},
loadLanguageFromInputRule(match) {
const { syntax } = this.findLanguageBySyntax(match[1]);

View File

@ -52,9 +52,9 @@ export class ContentEditor {
});
if (Object.keys(result).length !== 0) {
const { document, dom } = result;
const { document, languages } = result;
await languageLoader.loadLanguagesFromDOM(dom);
await languageLoader.loadLanguages(languages);
tr.setSelection(selection)
.replaceSelectionWith(document, false)

View File

@ -58,7 +58,7 @@ import Video from '../extensions/video';
import WordBreak from '../extensions/word_break';
import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer';
import createMarkdownDeserializer from './markdown_deserializer';
import createGlApiMarkdownDeserializer from './gl_api_markdown_deserializer';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
import languageLoader from './code_block_language_loader';
@ -146,7 +146,7 @@ export const createContentEditor = ({
const trackedExtensions = allExtensions.map(trackInputRulesAndShortcuts);
const tiptapEditor = createTiptapEditor({ extensions: trackedExtensions, ...tiptapOptions });
const serializer = createMarkdownSerializer({ serializerConfig });
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
const deserializer = createGlApiMarkdownDeserializer({ render: renderMarkdown });
return new ContentEditor({ tiptapEditor, serializer, eventHub, deserializer, languageLoader });
};

View File

@ -18,6 +18,7 @@ export default ({ render }) => {
return {
deserialize: async ({ schema, content }) => {
const html = await render(content);
const languages = [];
if (!html) return {};
@ -27,7 +28,11 @@ export default ({ render }) => {
// append original source as a comment that nodes can access
body.append(document.createComment(content));
return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body), dom: body };
body.querySelectorAll('pre').forEach((preElement) => {
languages.push(preElement.getAttribute('lang'));
});
return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body), languages };
},
};
};

View File

@ -1,12 +1,12 @@
<script>
import { GlSegmentedControl } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue';
import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
export default {
components: {
GlSegmentedControl,
CiCdAnalyticsAreaChart,
SegmentedControlButtonGroup,
},
props: {
charts: {
@ -38,7 +38,11 @@ export default {
</script>
<template>
<div>
<gl-segmented-control v-model="selectedChart" :options="chartRanges" class="gl-mb-4" />
<segmented-control-button-group
v-model="selectedChart"
:options="chartRanges"
class="gl-mb-4"
/>
<ci-cd-analytics-area-chart
v-if="chart"
v-bind="$attrs"

View File

@ -0,0 +1,35 @@
<script>
import { GlButtonGroup, GlButton } from '@gitlab/ui';
// TODO: We're planning to move this component to GitLab UI
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1787
export default {
components: {
GlButtonGroup,
GlButton,
},
props: {
options: {
type: Array,
required: true,
},
value: {
type: [String, Number, Boolean],
required: true,
},
},
};
</script>
<template>
<gl-button-group>
<gl-button
v-for="opt in options"
:key="opt.value"
:disabled="!!opt.disabled"
:selected="value === opt.value"
@click="$emit('input', opt.value)"
>
<slot name="button-content" v-bind="opt">{{ opt.text }}</slot>
</gl-button>
</gl-button-group>
</template>

View File

@ -34,8 +34,7 @@ module Types
description: 'Admin form URL of the runner. Only available for administrators.'
field :executor_name, GraphQL::Types::String, null: true,
description: 'Executor last advertised by the runner.',
method: :executor_name,
feature_flag: :graphql_ci_runner_executor
method: :executor_name
field :groups, ::Types::GroupType.connection_type, null: true,
description: 'Groups the runner is associated with. For group runners only.'
field :id, ::Types::GlobalIDType[::Ci::Runner], null: false,

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
# Integrations should reset their "secret" fields (type: 'password') when certain "exposing"
# fields are changed (e.g. URLs), to avoid leaking secrets to unauthorized parties.
# The result of this is that users have to reenter the secrets to confirm the change.
module Integrations
module ResetSecretFields
extend ActiveSupport::Concern
included do
before_validation :reset_secret_fields!, if: :reset_secret_fields?
end
def exposing_secrets_fields
# TODO: Once all integrations use `Integrations::Field` we can remove the `.try` here.
# See: https://gitlab.com/groups/gitlab-org/-/epics/7652
fields.select { _1.try(:exposes_secrets) }.pluck(:name)
end
private
def reset_secret_fields?
exposing_secrets_fields.any? do |field|
public_send("#{field}_changed?") # rubocop:disable GitlabSecurity/PublicSend
end
end
def reset_secret_fields!
secret_fields.each do |field|
next if public_send("#{field}_touched?") # rubocop:disable GitlabSecurity/PublicSend
public_send("#{field}=", nil) # rubocop:disable GitlabSecurity/PublicSend
# NOTE: Some of our specs also write to properties in addition to data fields,
# in order to test backwards compatibility. So in those cases we also need to
# clear the field in properties, since the setter above will only affect the data field.
self.properties = properties.except(field) if properties.present?
end
end
end
end

View File

@ -5,7 +5,7 @@ module Packages
extend ActiveSupport::Concern
class_methods do
def next_pending_destruction(order_by: nil)
def next_pending_destruction(order_by:)
set = pending_destruction.limit(1).lock('FOR UPDATE SKIP LOCKED')
set = set.order(order_by) if order_by
set.take

View File

@ -7,6 +7,7 @@ class Integration < ApplicationRecord
include Importable
include ProjectServicesLoggable
include Integrations::HasDataFields
include Integrations::ResetSecretFields
include FromUnion
include EachBatch
include IgnorableColumns
@ -447,6 +448,7 @@ class Integration < ApplicationRecord
# TODO: Once all integrations use `Integrations::Field` we can
# use `#secret?` here.
# See: https://gitlab.com/groups/gitlab-org/-/epics/7652
def secret_fields
fields.select { |f| f[:type] == 'password' }.pluck(:name)
end

View File

@ -149,6 +149,10 @@ module Integrations
raise NotImplementedError
end
def webhook_placeholder
raise NotImplementedError
end
private
def log_usage(_, _)

View File

@ -10,6 +10,7 @@ module Integrations
non_empty_password_help
non_empty_password_title
api_only
exposes_secrets
].freeze
attr_reader :name

View File

@ -31,7 +31,6 @@ module Integrations
# We should use username/password for Jira Server and email/api_token for Jira Cloud,
# for more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/49936.
before_validation :reset_password
after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type?
enum comment_detail: {
@ -46,12 +45,14 @@ module Integrations
required: true,
title: -> { s_('JiraService|Web URL') },
help: -> { s_('JiraService|Base URL of the Jira instance.') },
placeholder: 'https://jira.example.com'
placeholder: 'https://jira.example.com',
exposes_secrets: true
field :api_url,
section: SECTION_TYPE_CONNECTION,
title: -> { s_('JiraService|Jira API URL') },
help: -> { s_('JiraService|If different from Web URL.') }
help: -> { s_('JiraService|If different from Web URL.') },
exposes_secrets: true
field :username,
section: SECTION_TYPE_CONNECTION,
@ -98,13 +99,6 @@ module Integrations
jira_tracker_data || self.build_jira_tracker_data
end
def reset_password
return unless reset_password?
data_fields.password = nil
self.properties = properties.except('password')
end
def set_default_data
return unless issues_tracker.present?
@ -554,15 +548,6 @@ module Integrations
api_url.presence || url
end
def reset_password?
# don't reset the password if a new one is provided
return false if password_touched?
return true if api_url_changed?
return false if api_url.present?
url_changed?
end
def update_deployment_type?
api_url_changed? || url_changed? || username_changed? || password_changed?
end

View File

@ -69,11 +69,9 @@ class ProjectImportState < ApplicationRecord
project.reset_cache_and_import_attrs
if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
# rubocop: disable CodeReuse/ServiceClass
state.run_after_commit do
Projects::AfterImportService.new(project).execute
Projects::AfterImportWorker.perform_async(project.id)
end
# rubocop: enable CodeReuse/ServiceClass
end
end
end

View File

@ -2803,6 +2803,15 @@
:weight: 1
:idempotent:
:tags: []
- :name: projects_after_import
:worker_name: Projects::AfterImportWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: projects_git_garbage_collect
:worker_name: Projects::GitGarbageCollectWorker
:feature_category: :gitaly

View File

@ -32,7 +32,7 @@ module Packages
end
def next_item
model.next_pending_destruction
model.next_pending_destruction(order_by: :id)
end
def log_metadata(package_file)

View File

@ -1,14 +1,19 @@
# frozen_string_literal: true
module Projects
class AfterImportService
class AfterImportWorker
include ApplicationWorker
RESERVED_REF_PREFIXES = Repository::RESERVED_REFS_NAMES.map { |n| File.join('refs', n, '/') }
def initialize(project)
@project = project
end
data_consistency :always
idempotent!
urgency :low
feature_category :importers
def perform(project_id)
@project = Project.find(project_id)
def execute
service = Repositories::HousekeepingService.new(@project)
service.execute do

View File

@ -1,8 +0,0 @@
---
name: graphql_ci_runner_executor
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76534
rollout_issue_url:
milestone: '14.7'
type: development
group: group::runner
default_enabled: false

View File

@ -2,7 +2,7 @@
name: rendered_diffs_viewer
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75500
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352831
milestone: '14.9'
milestone: '15.0'
type: development
group: group::incubation
default_enabled: false
default_enabled: true

View File

@ -355,6 +355,8 @@
- 1
- - project_template_export
- 1
- - projects_after_import
- 1
- - projects_git_garbage_collect
- 1
- - projects_post_creation

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddIdForCleanupIndexPackagesPackageFiles < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
INDEX_NAME = 'index_packages_package_files_on_id_for_cleanup'
PACKAGE_FILE_STATUS_PENDING_DESTRUCTION = 1
def up
where = "status = #{PACKAGE_FILE_STATUS_PENDING_DESTRUCTION}"
add_concurrent_index :packages_package_files, :id, name: INDEX_NAME, where: where
end
def down
remove_concurrent_index_by_name :packages_package_files, name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
f9f2dc1f24f02571a7919da72b78e54922fd4fe202bc326235485610264d137c

View File

@ -28572,6 +28572,8 @@ CREATE INDEX index_packages_package_file_build_infos_on_pipeline_id ON packages_
CREATE INDEX index_packages_package_files_on_file_store ON packages_package_files USING btree (file_store);
CREATE INDEX index_packages_package_files_on_id_for_cleanup ON packages_package_files USING btree (id) WHERE (status = 1);
CREATE INDEX index_packages_package_files_on_package_id_and_file_name ON packages_package_files USING btree (package_id, file_name);
CREATE INDEX index_packages_package_files_on_package_id_id ON packages_package_files USING btree (package_id, id);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -9410,7 +9410,7 @@ Represents the total number of issues and their weights for a particular day.
| <a id="cirunnercreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp of creation of this runner. |
| <a id="cirunnerdescription"></a>`description` | [`String`](#string) | Description of the runner. |
| <a id="cirunnereditadminurl"></a>`editAdminUrl` | [`String`](#string) | Admin form URL of the runner. Only available for administrators. |
| <a id="cirunnerexecutorname"></a>`executorName` | [`String`](#string) | Executor last advertised by the runner. Available only when feature flag `graphql_ci_runner_executor` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. |
| <a id="cirunnerexecutorname"></a>`executorName` | [`String`](#string) | Executor last advertised by the runner. |
| <a id="cirunnergroups"></a>`groups` | [`GroupConnection`](#groupconnection) | Groups the runner is associated with. For group runners only. (see [Connections](#connections)) |
| <a id="cirunnerid"></a>`id` | [`CiRunnerID!`](#cirunnerid) | ID of the runner. |
| <a id="cirunneripaddress"></a>`ipAddress` | [`String`](#string) | IP address of the runner. |

View File

@ -73,6 +73,9 @@ To enable merge trains:
- Your repository must be a GitLab repository, not an
[external repository](../ci_cd_for_external_repos/index.md).
Merge trains do not work with [Semi-linear history merge requests](../../user/project/merge_requests/reviews/index.md)
or [fast-forward merge requests](../../user/project/merge_requests/fast_forward_merge.md).
## Enable merge trains
To enable merge trains for your project:
@ -84,7 +87,6 @@ To enable merge trains for your project:
1. On the left sidebar, select **Settings > General**.
1. Expand **Merge requests**.
1. In the **Merge method** section, verify that **Merge commit** is selected.
You cannot use **Merge commit with semi-linear history** or **Fast-forward merge** with merge trains.
1. In the **Merge options** section, select **Enable merged results pipelines** (if not already selected) and **Enable merge trains**.
1. Select **Save changes**.

View File

@ -262,7 +262,7 @@ To disable a feature flag that has been globally enabled you can run:
To disable a feature flag that has been enabled for a specific project you can run:
```shell
/chatops run feature set --group=gitlab-org some_feature false
/chatops run feature set --project=gitlab-org/gitlab some_feature false
```
You cannot selectively disable feature flags for a specific project/group/user without applying a [specific method of implementing](index.md#selectively-disable-by-actor) the feature flags.

View File

@ -8,15 +8,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## Monitoring
We have a performance dashboard available in one of our [Grafana instances](https://dashboards.gitlab.net/d/1EBTz3Dmz/sitespeed-page-summary?orgId=1). This dashboard automatically aggregates metric data from [sitespeed.io](https://www.sitespeed.io/) every 6 hours. These changes are displayed after a set number of pages are aggregated.
We have a performance dashboard available in one of our [Grafana instances](https://dashboards.gitlab.net/d/000000043/sitespeed-page-summary?orgId=1). This dashboard automatically aggregates metric data from [sitespeed.io](https://www.sitespeed.io/) every 4 hours. These changes are displayed after a set number of pages are aggregated.
These pages can be found inside a text file in the [`gitlab-build-images` repository](https://gitlab.com/gitlab-org/gitlab-build-images) called [`gitlab.txt`](https://gitlab.com/gitlab-org/gitlab-build-images/blob/master/scripts/gitlab.txt)
Any frontend engineer can contribute to this dashboard. They can contribute by adding or removing URLs of pages from this text file. Please have a [frontend monitoring expert](https://about.gitlab.com/company/team/) review your changes before assigning to a maintainer of the `gitlab-build-images` project. The changes are pushed live on the next scheduled run after the changes are merged into `main`.
These pages can be found inside text files in the [`sitespeed-measurement-setup` repository](https://gitlab.com/gitlab-org/frontend/sitespeed-measurement-setup) called [`gitlab`](https://gitlab.com/gitlab-org/frontend/sitespeed-measurement-setup/-/tree/master/gitlab)
Any frontend engineer can contribute to this dashboard. They can contribute by adding or removing URLs of pages to the text files. The changes are pushed live on the next scheduled run after the changes are merged into `main`.
There are 3 recommended high impact metrics to review on each page:
There are 3 recommended high impact metrics (core web vitals) to review on each page:
- [First visual change](https://web.dev/first-meaningful-paint/)
- [Speed Index](https://github.com/WPO-Foundation/webpagetest-docs/blob/master/user/Metrics/SpeedIndex.md)
- [Visual Complete 95%](https://github.com/WPO-Foundation/webpagetest-docs/blob/master/user/Metrics/SpeedIndex.md)
- [Largest Contentful Paint](https://web.dev/lcp/)
- [First Input Delay](https://web.dev/fid/)
- [Cumulative Layout Shift](https://web.dev/cls/)
For these metrics, lower numbers are better as it means that the website is more performant.

View File

@ -461,7 +461,9 @@ References:
### Description
Path Traversal vulnerabilities grant attackers access to arbitrary directories and files on the server that is executing an application, including data, code or credentials.
Path Traversal vulnerabilities grant attackers access to arbitrary directories and files on the server that is executing an application. This data can include data, code or credentials.
Traversal can occur when a path includes directories. A typical malicious example includes one or more `../`, which tells the file system to look in the parent directory. Supplying many of them in a path, for example `../../../../../../../etc/passwd`, usually resolves to `/etc/passwd`. If the file system is instructed to look back to the root directory and can't go back any further, then extra `../` are ignored. The file system then looks from the root, resulting in `/etc/passwd` - a file you definitely do not want exposed to a malicious attacker!
### Impact
@ -510,6 +512,44 @@ requires :file_path, type: String, file_path: true
Absolute paths are not allowed by default. If allowing an absolute path is required, you
need to provide an array of paths to the parameter `allowlist`.
### Misleading behavior
Some methods used to construct file paths can have non-intuitive behavior. To properly validate user input, be aware
of these behaviors.
#### Ruby
The Ruby method [`Pathname.join`](https://ruby-doc.org/stdlib-2.7.4/libdoc/pathname/rdoc/Pathname.html#method-i-join)
joins path names. Using methods in a specific way can result in a path name typically prohibited in
normal use. In the examples below, we see attempts to access `/etc/passwd`, which is a sensitive file:
```ruby
require 'pathname'
p = Pathname.new('tmp')
print(p.join('log', 'etc/passwd', 'foo'))
# => tmp/log/etc/passwd/foo
```
Assuming the second parameter is user-supplied and not validated, submitting a new absolute path
results in a different path:
```ruby
print(p.join('log', '/etc/passwd', ''))
# renders the path to "/etc/passwd", which is not what we expect!
```
#### Golang
Golang has similar behavior with [`path.Clean`](https://pkg.go.dev/path#example-Clean). Remember that with many file systems, using `../../../../` traverses up to the root directory. Any remaining `../` are ignored. This example may give an attacker access to `/etc/passwd`:
```golang
path.Clean("/../../etc/passwd")
// renders the path to "etc/passwd"; the file path is relative to whatever the current directory is
path.Clean("../../etc/passwd")
// renders the path to "../../etc/passwd"; the file path will look back up to two parent directories!
```
## OS command injection guidelines
Command injection is an issue in which an attacker is able to execute arbitrary commands on the host

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

View File

@ -35,8 +35,8 @@ Amazon provides a managed Kubernetes service offering known as [Amazon Elastic K
The [AWS Quick Start for GitLab Cloud Native Hybrid on EKS](https://aws-quickstart.github.io/quickstart-eks-gitlab/) is developed by AWS, GitLab, and the community that contributes to AWS Quick Starts, whether directly to the GitLab Quick Start or to the underlying Quick Start dependencies GitLab inherits (for example, EKS Quick Start).
NOTE:
This automation is in **Developer Preview**. GitLab is working with AWS on resolving [the outstanding issues](https://github.com/aws-quickstart/quickstart-eks-gitlab/issues?q=is%3Aissue+is%3Aopen+%5BHL%5D) before it is fully released. You can subscribe to this issue to be notified of progress and release announcements: [AWS Quick Start for GitLab Cloud Native Hybrid on EKS Status: DEVELOPER PREVIEW](https://gitlab.com/gitlab-com/alliances/aws/public-tracker/-/issues/11).<br><br>
The developer preview deploys Aurora PostgreSQL, but the release version will deploy Amazon RDS PostgreSQL due to [known issues](https://gitlab.com/gitlab-com/alliances/aws/public-tracker/-/issues?label_name%5B%5D=AWS+Known+Issue) with Aurora. All performance testing results will also be redone after this change has been made.
This automation is in **Beta**. GitLab is working with AWS on resolving [the outstanding issues](https://github.com/aws-quickstart/quickstart-eks-gitlab/issues?q=is%3Aissue+is%3Aopen+%5BHL%5D) before it is fully released. You can subscribe to this issue to be notified of progress and release announcements: [AWS Quick Start for GitLab Cloud Native Hybrid on EKS Status: Beta](https://gitlab.com/gitlab-com/alliances/aws/public-tracker/-/issues/11).<br><br>
The Beta version deploys Aurora PostgreSQL, but the release version will deploy Amazon RDS PostgreSQL due to [known issues](https://gitlab.com/gitlab-com/alliances/aws/public-tracker/-/issues?label_name%5B%5D=AWS+Known+Issue) with Aurora. All performance testing results will also be redone after this change has been made.
The [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/tree/main) is an effort made by GitLab to create a multi-cloud, multi-GitLab (Omnibus + Cloud Native Hybrid) toolkit to provision GitLab. GET is developed by GitLab developers and is open to community contributions.
It is helpful to review the [GitLab Environment Toolkit (GET) Issues](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/issues) to understand if any of them may affect your provisioning plans.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1156,6 +1156,7 @@ A site profile contains the following:
- **Password**: The password used to authenticate to the website.
- **Username form field**: The name of username field at the sign-in HTML form.
- **Password form field**: The name of password field at the sign-in HTML form.
- **Submit form field**: The `id` or `name` of the element that when clicked submits the sign-in HTML form.
When an API site type is selected, a [host override](#host-override) is used to ensure the API being scanned is on the same host as the target. This is done to reduce the risk of running an active scan against the wrong API.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -23,21 +23,25 @@ it's rendered into HTML when you view it:
Interactive features, including JavaScript plots, don't work when viewed in
GitLab.
## Cleaner diffs
## Cleaner diffs and raw diffs
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/6589) in GitLab 14.5 as an [Alpha](../../../../policy/alpha-beta-support.md#alpha-features) release [with a flag](../../../../administration/feature_flags.md) named `jupyter_clean_diffs`. Enabled by default.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75500) in GitLab 14.9. Feature flag `jupyter_clean_diffs` removed.
> - [Reintroduced toggle](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85079) in GitLab 15.0 [with a flag](../../../../administration/feature_flags.md) named `ipynb_semantic_diff`. Enabled by default.
> - Selecting between raw and cleaner diffs [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85203) in GitLab 15.0 [with a flag](../../../../administration/feature_flags.md) named `rendered_diffs_viewer`. Enabled by default.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../../../../administration/feature_flags.md) named `ipynb_semantic_diff`.
On self-managed GitLab, by default semantic diffs are available. To hide the feature, ask an administrator to [disable the feature flag](../../../../administration/feature_flags.md) named `ipynb_semantic_diff`.
On GitLab.com, this feature is available.
This feature is ready for production use.
FLAG:
On self-managed GitLab, by default the ability to switch between raw and rendered diffs is available. To hide the feature, ask an administrator to [disable the feature flag](../../../../administration/feature_flags.md)named `rendered_diffs_viewer`. On GitLab.com, this feature is available.
When commits include changes to Jupyter Notebook files, GitLab:
- Transforms the machine-readable `.ipynb` file into a human-readable Markdown file.
- Displays a cleaner version of the diff that includes syntax highlighting.
- Enables switching between raw and rendered diffs on the Commit and Compare pages. (Not available on merge request pages.)
Code suggestions are not available on diffs and merge requests for `.ipynb` files.

View File

@ -4,13 +4,15 @@
module Banzai
module Filter
# HTML filter that moves the value of image `src` attributes to `data-src`
# so they can be lazy loaded.
# so they can be lazy loaded. Also sets decoding to 'async' so that the
# decoding of images doesn't block the loading of other content.
class ImageLazyLoadFilter < HTML::Pipeline::Filter
CSS = 'img'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call
doc.xpath(XPATH).each do |img|
img['decoding'] = 'async'
img.add_class('lazy')
img['data-src'] = img['src']
img['src'] = LazyImageTagHelper.placeholder_image

View File

@ -11530,6 +11530,12 @@ msgstr ""
msgid "DastProfiles|Spider timeout"
msgstr ""
msgid "DastProfiles|Submit button"
msgstr ""
msgid "DastProfiles|Submit button (optional)"
msgstr ""
msgid "DastProfiles|Target URL"
msgstr ""

View File

@ -140,7 +140,7 @@
markdown: |-
![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)
html: |-
<p data-sourcepos="1:1-1:69" dir="auto"><a class="no-attachment-icon gfm" href="/groups/group58/-/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png" data-link="true"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="test-file" class="lazy gfm" data-src="/groups/group58/-/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png" data-canonical-src="/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png"></a></p>
<p data-sourcepos="1:1-1:69" dir="auto"><a class="no-attachment-icon gfm" href="/groups/group58/-/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png" data-link="true"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="test-file" decoding="async" class="lazy gfm" data-src="/groups/group58/-/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png" data-canonical-src="/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png"></a></p>
- name: attachment_image_for_project
api_context: project
@ -149,7 +149,7 @@
markdown: |-
![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)
html: |-
<p data-sourcepos="1:1-1:69" dir="auto"><a class="no-attachment-icon gfm" href="/group58/project22/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png" data-link="true"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="test-file" class="lazy gfm" data-src="/group58/project22/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png" data-canonical-src="/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png"></a></p>
<p data-sourcepos="1:1-1:69" dir="auto"><a class="no-attachment-icon gfm" href="/group58/project22/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png" data-link="true"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="test-file" decoding="async" class="lazy gfm" data-src="/group58/project22/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png" data-canonical-src="/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png"></a></p>
- name: attachment_image_for_project_wiki
api_context: project_wiki
@ -158,7 +158,7 @@
markdown: |-
![test-file](test-file.png)
html: |-
<p data-sourcepos="1:1-1:27" dir="auto"><a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.png"><img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png"></a></p>
<p data-sourcepos="1:1-1:27" dir="auto"><a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.png"><img alt="test-file" decoding="async" class="lazy" data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png"></a></p>
- name: attachment_link_for_group
api_context: group
@ -391,7 +391,7 @@
]
```
html: |-
<a class="no-attachment-icon" href="http://localhost:8000/nomnoml/svg/eNp1jbsOwjAMRfd-haUuIJQBBlRFVZb2L1CGkBqpgtpR6oEhH0_CW6hsts-9xwD1LJHPqKF2zX67ayqAQ3uKbkLTo-fohCMEJ4KRUoYFu2MuOS-m4ykwIUlKG-CAOT0yrdb2EewuY2YWBgxIwwxKmXx8dZ6h95ekgPAqGv4miuk-YnEVFfmIgr-Fzw6tVt-CZb7osdUNUAReJA==" target="_blank" rel="noopener noreferrer" data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,ICAjc3Ryb2tlOiAjYTg2MTI4CiAgWzxmcmFtZT5EZWNvcmF0b3IgcGF0dGVybnwKICAgIFs8YWJzdHJhY3Q+Q29tcG9uZW50fHwrIG9wZXJhdGlvbigpXQogICAgW0NsaWVudF0gZGVwZW5kcyAtLT4gW0NvbXBvbmVudF0KICAgIFtEZWNvcmF0b3J8LSBuZXh0OiBDb21wb25lbnRdCiAgICBbRGVjb3JhdG9yXSBkZWNvcmF0ZXMgLS0gW0NvbmNyZXRlQ29tcG9uZW50XQogICAgW0NvbXBvbmVudF0gPDotIFtEZWNvcmF0b3JdCiAgICBbQ29tcG9uZW50XSA8Oi0gW0NvbmNyZXRlQ29tcG9uZW50XQogIF0K"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" class="js-render-kroki lazy" data-src="http://localhost:8000/nomnoml/svg/eNp1jbsOwjAMRfd-haUuIJQBBlRFVZb2L1CGkBqpgtpR6oEhH0_CW6hsts-9xwD1LJHPqKF2zX67ayqAQ3uKbkLTo-fohCMEJ4KRUoYFu2MuOS-m4ykwIUlKG-CAOT0yrdb2EewuY2YWBgxIwwxKmXx8dZ6h95ekgPAqGv4miuk-YnEVFfmIgr-Fzw6tVt-CZb7osdUNUAReJA=="></a>
<a class="no-attachment-icon" href="http://localhost:8000/nomnoml/svg/eNp1jbsOwjAMRfd-haUuIJQBBlRFVZb2L1CGkBqpgtpR6oEhH0_CW6hsts-9xwD1LJHPqKF2zX67ayqAQ3uKbkLTo-fohCMEJ4KRUoYFu2MuOS-m4ykwIUlKG-CAOT0yrdb2EewuY2YWBgxIwwxKmXx8dZ6h95ekgPAqGv4miuk-YnEVFfmIgr-Fzw6tVt-CZb7osdUNUAReJA==" target="_blank" rel="noopener noreferrer" data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,ICAjc3Ryb2tlOiAjYTg2MTI4CiAgWzxmcmFtZT5EZWNvcmF0b3IgcGF0dGVybnwKICAgIFs8YWJzdHJhY3Q+Q29tcG9uZW50fHwrIG9wZXJhdGlvbigpXQogICAgW0NsaWVudF0gZGVwZW5kcyAtLT4gW0NvbXBvbmVudF0KICAgIFtEZWNvcmF0b3J8LSBuZXh0OiBDb21wb25lbnRdCiAgICBbRGVjb3JhdG9yXSBkZWNvcmF0ZXMgLS0gW0NvbmNyZXRlQ29tcG9uZW50XQogICAgW0NvbXBvbmVudF0gPDotIFtEZWNvcmF0b3JdCiAgICBbQ29tcG9uZW50XSA8Oi0gW0NvbmNyZXRlQ29tcG9uZW50XQogIF0K"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" class="js-render-kroki lazy" decoding="async" data-src="http://localhost:8000/nomnoml/svg/eNp1jbsOwjAMRfd-haUuIJQBBlRFVZb2L1CGkBqpgtpR6oEhH0_CW6hsts-9xwD1LJHPqKF2zX67ayqAQ3uKbkLTo-fohCMEJ4KRUoYFu2MuOS-m4ykwIUlKG-CAOT0yrdb2EewuY2YWBgxIwwxKmXx8dZ6h95ekgPAqGv4miuk-YnEVFfmIgr-Fzw6tVt-CZb7osdUNUAReJA=="></a>
- name: diagram_plantuml
markdown: |-
@ -403,7 +403,7 @@
Alice <-- Bob: Another authentication Response
```
html: |-
<a class="no-attachment-icon" href="http://localhost:8080/png/U9nJK73CoKnELT2rKt3AJx9IS2mjoKZDAybCJYp9pCzJ24ejB4qjBk5I0Cagw09LWPLZKLTSa9zNdCe5L8bcO5u-K6MHGY8kWo7ARNHr2QY7MW00AeWxTG00" target="_blank" rel="noopener noreferrer" data-diagram="plantuml" data-diagram-src="data:text/plain;base64,ICBBbGljZSAtPiBCb2I6IEF1dGhlbnRpY2F0aW9uIFJlcXVlc3QKICBCb2IgLS0+IEFsaWNlOiBBdXRoZW50aWNhdGlvbiBSZXNwb25zZQoKICBBbGljZSAtPiBCb2I6IEFub3RoZXIgYXV0aGVudGljYXRpb24gUmVxdWVzdAogIEFsaWNlIDwtLSBCb2I6IEFub3RoZXIgYXV0aGVudGljYXRpb24gUmVzcG9uc2UK"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" class="lazy" data-src="http://localhost:8080/png/U9nJK73CoKnELT2rKt3AJx9IS2mjoKZDAybCJYp9pCzJ24ejB4qjBk5I0Cagw09LWPLZKLTSa9zNdCe5L8bcO5u-K6MHGY8kWo7ARNHr2QY7MW00AeWxTG00"></a>
<a class="no-attachment-icon" href="http://localhost:8080/png/U9nJK73CoKnELT2rKt3AJx9IS2mjoKZDAybCJYp9pCzJ24ejB4qjBk5I0Cagw09LWPLZKLTSa9zNdCe5L8bcO5u-K6MHGY8kWo7ARNHr2QY7MW00AeWxTG00" target="_blank" rel="noopener noreferrer" data-diagram="plantuml" data-diagram-src="data:text/plain;base64,ICBBbGljZSAtPiBCb2I6IEF1dGhlbnRpY2F0aW9uIFJlcXVlc3QKICBCb2IgLS0+IEFsaWNlOiBBdXRoZW50aWNhdGlvbiBSZXNwb25zZQoKICBBbGljZSAtPiBCb2I6IEFub3RoZXIgYXV0aGVudGljYXRpb24gUmVxdWVzdAogIEFsaWNlIDwtLSBCb2I6IEFub3RoZXIgYXV0aGVudGljYXRpb24gUmVzcG9uc2UK"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" decoding="async" class="lazy" data-src="http://localhost:8080/png/U9nJK73CoKnELT2rKt3AJx9IS2mjoKZDAybCJYp9pCzJ24ejB4qjBk5I0Cagw09LWPLZKLTSa9zNdCe5L8bcO5u-K6MHGY8kWo7ARNHr2QY7MW00AeWxTG00"></a>
- name: div
markdown: |-
@ -449,11 +449,11 @@
</figure>
html: |-
<figure>
<p data-sourcepos="3:1-3:42"><a class="no-attachment-icon" href="elephant-sunset.jpg" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Elephant at sunset" class="lazy" data-src="elephant-sunset.jpg"></a></p>
<p data-sourcepos="3:1-3:42"><a class="no-attachment-icon" href="elephant-sunset.jpg" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Elephant at sunset" decoding="async" class="lazy" data-src="elephant-sunset.jpg"></a></p>
<figcaption>An elephant at sunset</figcaption>
</figure>
<figure>
<p data-sourcepos="9:1-9:44"><a class="no-attachment-icon" href="croc-crocs.jpg" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="A crocodile wearing crocs" class="lazy" data-src="croc-crocs.jpg"></a></p>
<p data-sourcepos="9:1-9:44"><a class="no-attachment-icon" href="croc-crocs.jpg" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="A crocodile wearing crocs" decoding="async" class="lazy" data-src="croc-crocs.jpg"></a></p>
<figcaption>
<p data-sourcepos="13:1-13:28">A crocodile wearing <em>crocs</em>!</p>
</figcaption>
@ -613,7 +613,7 @@
markdown: |-
![alt text](https://gitlab.com/logo.png)
html: |-
<p data-sourcepos="1:1-1:40" dir="auto"><a class="no-attachment-icon" href="https://gitlab.com/logo.png" target="_blank" rel="nofollow noreferrer noopener"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="alt text" class="lazy" data-src="https://gitlab.com/logo.png"></a></p>
<p data-sourcepos="1:1-1:40" dir="auto"><a class="no-attachment-icon" href="https://gitlab.com/logo.png" target="_blank" rel="nofollow noreferrer noopener"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="alt text" decoding="async" class="lazy" data-src="https://gitlab.com/logo.png"></a></p>
- name: inline_code
markdown: |-

View File

@ -76,24 +76,6 @@ describe('content_editor/services/code_block_language_loader', () => {
});
});
describe('loadLanguagesFromDOM', () => {
it('loads highlight.js language packages identified by pre tags in a DOM fragment', async () => {
const parser = new DOMParser();
const { body } = parser.parseFromString(
`
<pre lang="javascript"></pre>
<pre lang="ruby"></pre>
`,
'text/html',
);
await languageLoader.loadLanguagesFromDOM(body);
expect(lowlight.registerLanguage).toHaveBeenCalledWith('javascript', expect.any(Function));
expect(lowlight.registerLanguage).toHaveBeenCalledWith('ruby', expect.any(Function));
});
});
describe('loadLanguageFromInputRule', () => {
it('loads highlight.js language packages identified from the input rule', async () => {
const match = new RegExp(backtickInputRegex).exec('```js ');

View File

@ -28,7 +28,7 @@ describe('content_editor/services/content_editor', () => {
serializer = { deserialize: jest.fn() };
deserializer = { deserialize: jest.fn() };
languageLoader = { loadLanguagesFromDOM: jest.fn() };
languageLoader = { loadLanguages: jest.fn() };
eventHub = eventHubFactory();
contentEditor = new ContentEditor({
tiptapEditor,
@ -51,12 +51,12 @@ describe('content_editor/services/content_editor', () => {
describe('when setSerializedContent succeeds', () => {
let document;
const dom = {};
const languages = ['javascript'];
const testMarkdown = '**bold text**';
beforeEach(() => {
document = doc(p('document'));
deserializer.deserialize.mockResolvedValueOnce({ document, dom });
deserializer.deserialize.mockResolvedValueOnce({ document, languages });
});
it('emits loadingContent and loadingSuccess event in the eventHub', () => {
@ -81,7 +81,7 @@ describe('content_editor/services/content_editor', () => {
it('passes deserialized DOM document to language loader', async () => {
await contentEditor.setSerializedContent(testMarkdown);
expect(languageLoader.loadLanguagesFromDOM).toHaveBeenCalledWith(dom);
expect(languageLoader.loadLanguages).toHaveBeenCalledWith(languages);
});
});

View File

@ -1,8 +1,8 @@
import createMarkdownDeserializer from '~/content_editor/services/markdown_deserializer';
import createMarkdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer';
import Bold from '~/content_editor/extensions/bold';
import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/services/markdown_deserializer', () => {
describe('content_editor/services/gl_api_markdown_deserializer', () => {
let renderMarkdown;
let doc;
let p;
@ -32,7 +32,9 @@ describe('content_editor/services/markdown_deserializer', () => {
beforeEach(async () => {
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
renderMarkdown.mockResolvedValueOnce(`<p><strong>${text}</strong></p>`);
renderMarkdown.mockResolvedValueOnce(
`<p><strong>${text}</strong></p><pre lang="javascript"></pre>`,
);
result = await deserializer.deserialize({
content: 'content',
@ -40,13 +42,13 @@ describe('content_editor/services/markdown_deserializer', () => {
});
});
it('transforms HTML returned by render function to a ProseMirror document', async () => {
const expectedDoc = doc(p(bold(text)));
const document = doc(p(bold(text)));
expect(result.document.toJSON()).toEqual(expectedDoc.toJSON());
expect(result.document.toJSON()).toEqual(document.toJSON());
});
it('returns parsed HTML as a DOM object', () => {
expect(result.dom.innerHTML).toEqual(`<p><strong>${text}</strong></p><!--content-->`);
it('returns languages of code blocks found in the document', () => {
expect(result.languages).toEqual(['javascript']);
});
});

View File

@ -2,7 +2,7 @@ import { Extension } from '@tiptap/core';
import BulletList from '~/content_editor/extensions/bullet_list';
import ListItem from '~/content_editor/extensions/list_item';
import Paragraph from '~/content_editor/extensions/paragraph';
import markdownDeserializer from '~/content_editor/services/markdown_deserializer';
import markdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer';
import { getMarkdownSource, getFullSource } from '~/content_editor/services/markdown_sourcemap';
import { createTestEditor, createDocBuilder } from '../test_utils';

View File

@ -1,8 +1,8 @@
import { nextTick } from 'vue';
import { GlSegmentedControl } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CiCdAnalyticsAreaChart from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue';
import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue';
import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue';
import { transformedAreaChartData, chartOptions } from '../mock_data';
const DEFAULT_PROPS = {
@ -48,7 +48,7 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
});
const findMetricsSlot = () => wrapper.findByTestId('metrics-slot');
const findSegmentedControl = () => wrapper.findComponent(GlSegmentedControl);
const findSegmentedControl = () => wrapper.findComponent(SegmentedControlButtonGroup);
describe('segmented control', () => {
beforeEach(() => {
@ -56,7 +56,7 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
});
it('should default to the first chart', () => {
expect(findSegmentedControl().props('checked')).toBe(0);
expect(findSegmentedControl().props('value')).toBe(0);
});
it('should use the title and index as values', () => {

View File

@ -0,0 +1,104 @@
import { GlButtonGroup, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue';
const DEFAULT_OPTIONS = [
{ text: 'Lorem', value: 'abc' },
{ text: 'Ipsum', value: 'def' },
{ text: 'Foo', value: 'x', disabled: true },
{ text: 'Dolar', value: 'ghi' },
];
describe('~/vue_shared/components/segmented_control_button_group.vue', () => {
let wrapper;
const createComponent = (props = {}, scopedSlots = {}) => {
wrapper = shallowMount(SegmentedControlButtonGroup, {
propsData: {
value: DEFAULT_OPTIONS[0].value,
options: DEFAULT_OPTIONS,
...props,
},
scopedSlots,
});
};
const findButtonGroup = () => wrapper.findComponent(GlButtonGroup);
const findButtons = () => findButtonGroup().findAllComponents(GlButton);
const findButtonsData = () =>
findButtons().wrappers.map((x) => ({
selected: x.props('selected'),
text: x.text(),
disabled: x.props('disabled'),
}));
const findButtonWithText = (text) => findButtons().wrappers.find((x) => x.text() === text);
const optionsAsButtonData = (options) =>
options.map(({ text, disabled = false }) => ({
selected: false,
text,
disabled,
}));
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
beforeEach(() => {
createComponent();
});
it('renders button group', () => {
expect(findButtonGroup().exists()).toBe(true);
});
it('renders buttons', () => {
const expectation = optionsAsButtonData(DEFAULT_OPTIONS);
expectation[0].selected = true;
expect(findButtonsData()).toEqual(expectation);
});
describe.each(DEFAULT_OPTIONS.filter((x) => !x.disabled))(
'when button clicked %p',
({ text, value }) => {
it('emits input with value', () => {
expect(wrapper.emitted('input')).toBeUndefined();
findButtonWithText(text).vm.$emit('click');
expect(wrapper.emitted('input')).toEqual([[value]]);
});
},
);
});
const VALUE_TEST_CASES = [0, 1, 3].map((index) => [DEFAULT_OPTIONS[index].value, index]);
describe.each(VALUE_TEST_CASES)('with value=%s', (value, index) => {
it(`renders selected button at ${index}`, () => {
createComponent({ value });
const expectation = optionsAsButtonData(DEFAULT_OPTIONS);
expectation[index].selected = true;
expect(findButtonsData()).toEqual(expectation);
});
});
describe('with button-content slot', () => {
it('renders button content based on slot', () => {
createComponent(
{},
{
'button-content': `<template #button-content="{ text }">In a slot - {{ text }}</template>`,
},
);
expect(findButtonsData().map((x) => x.text)).toEqual(
DEFAULT_OPTIONS.map((x) => `In a slot - ${x.text}`),
);
});
});
});

View File

@ -23,6 +23,11 @@ RSpec.describe Banzai::Filter::ImageLazyLoadFilter do
expect(doc.at_css('img')['class']).to eq 'test lazy'
end
it 'adds a async decoding attribute' do
doc = filter(image_with_class('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg', 'test'))
expect(doc.at_css('img')['decoding']).to eq 'async'
end
it 'transforms the image src to a data-src' do
doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
expect(doc.at_css('img')['data-src']).to eq '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'

View File

@ -79,7 +79,7 @@ module Gitlab
},
'image with onerror' => {
input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
output: "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt='Alt text\" onerror=\"alert(7)' class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
output: "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt='Alt text\" onerror=\"alert(7)' decoding=\"async\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
}
}
@ -112,13 +112,13 @@ module Gitlab
context "images" do
it "does lazy load and link image" do
input = 'image:https://localhost.com/image.png[]'
output = "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
output = "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" decoding=\"async\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
expect(render(input, context)).to include(output)
end
it "does not automatically link image if link is explicitly defined" do
input = 'image:https://localhost.com/image.png[link=https://gitlab.com]'
output = "<div>\n<p><span><a href=\"https://gitlab.com\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
output = "<div>\n<p><span><a href=\"https://gitlab.com\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" decoding=\"async\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
expect(render(input, context)).to include(output)
end
end
@ -524,7 +524,7 @@ module Gitlab
output = <<~HTML
<div>
<div>
<a class="no-attachment-icon" href="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka"></a>
<a class="no-attachment-icon" href="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" decoding="async" class="lazy" data-src="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka"></a>
</div>
</div>
HTML
@ -578,7 +578,7 @@ module Gitlab
output = <<~HTML
<div>
<div>
<a class=\"no-attachment-icon\" href=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Diagram\" class=\"lazy\" data-src=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\"></a>
<a class=\"no-attachment-icon\" href=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Diagram\" decoding=\"async\" class=\"lazy\" data-src=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\"></a>
</div>
</div>
HTML
@ -625,7 +625,7 @@ module Gitlab
output = <<~HTML
<div>
<div>
<a class="no-attachment-icon" href="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="></a>
<a class="no-attachment-icon" href="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" decoding="async" class="lazy" data-src="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="></a>
</div>
</div>
HTML

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Integrations::ResetSecretFields do
let(:described_class) do
Class.new(Integration) do
field :username, type: 'text'
field :url, type: 'text', exposes_secrets: true
field :api_url, type: 'text', exposes_secrets: true
field :password, type: 'password'
field :token, type: 'password'
end
end
let(:integration) { described_class.new }
it_behaves_like Integrations::ResetSecretFields
end

View File

@ -3,15 +3,14 @@
require 'spec_helper'
RSpec.describe Integrations::BaseChatNotification do
describe 'Associations' do
describe 'validations' do
before do
allow(subject).to receive(:activated?).and_return(true)
allow(subject).to receive(:default_channel_placeholder).and_return('placeholder')
allow(subject).to receive(:webhook_placeholder).and_return('placeholder')
end
it { is_expected.to validate_presence_of :webhook }
end
describe 'validations' do
it { is_expected.to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]).allow_blank }
end
@ -274,4 +273,16 @@ RSpec.describe Integrations::BaseChatNotification do
it_behaves_like 'with channel specified', 'slack-integration, #slack-test, @UDLP91W0A', ['slack-integration', '#slack-test', '@UDLP91W0A']
end
end
describe '#default_channel_placeholder' do
it 'raises an error' do
expect { subject.default_channel_placeholder }.to raise_error(NotImplementedError)
end
end
describe '#webhook_placeholder' do
it 'raises an error' do
expect { subject.webhook_placeholder }.to raise_error(NotImplementedError)
end
end
end

View File

@ -27,6 +27,10 @@ RSpec.describe Integrations::Jira do
WebMock.stub_request(:get, /serverInfo/).to_return(body: server_info_results.to_json )
end
it_behaves_like Integrations::ResetSecretFields do
let(:integration) { jira_integration }
end
describe '#options' do
let(:options) do
{
@ -301,7 +305,7 @@ RSpec.describe Integrations::Jira do
let_it_be(:new_url) { 'http://jira-new.example.com' }
before do
integration.update!(username: new_username, url: new_url)
integration.update!(username: new_username, url: new_url, password: password)
end
it 'stores updated data in jira_tracker_data table' do
@ -318,7 +322,7 @@ RSpec.describe Integrations::Jira do
context 'when updating the url, api_url, username, or password' do
context 'when updating the integration' do
it 'updates deployment type' do
integration.update!(url: 'http://first.url')
integration.update!(url: 'http://first.url', password: password)
integration.jira_tracker_data.update!(deployment_type: 'server')
expect(integration.jira_tracker_data.deployment_server?).to be_truthy
@ -376,135 +380,6 @@ RSpec.describe Integrations::Jira do
expect(WebMock).not_to have_requested(:get, /serverInfo/)
end
end
context 'stored password invalidation' do
context 'when a password was previously set' do
context 'when only web url present' do
let(:data_params) do
{
url: url, api_url: nil,
username: username, password: password,
jira_issue_transition_id: transition_id
}
end
it 'resets password if url changed' do
integration
integration.url = 'http://jira_edited.example.com'
expect(integration).not_to be_valid
expect(integration.url).to eq('http://jira_edited.example.com')
expect(integration.password).to be_nil
end
it 'does not reset password if url "changed" to the same url as before' do
integration.url = 'http://jira.example.com'
expect(integration).to be_valid
expect(integration.url).to eq('http://jira.example.com')
expect(integration.password).not_to be_nil
end
it 'resets password if url not changed but api url added' do
integration.api_url = 'http://jira_edited.example.com/rest/api/2'
expect(integration).not_to be_valid
expect(integration.api_url).to eq('http://jira_edited.example.com/rest/api/2')
expect(integration.password).to be_nil
end
it 'does not reset password if new url is set together with password, even if it\'s the same password' do
integration.url = 'http://jira_edited.example.com'
integration.password = password
expect(integration).to be_valid
expect(integration.password).to eq(password)
expect(integration.url).to eq('http://jira_edited.example.com')
end
it 'resets password if url changed, even if setter called multiple times' do
integration.url = 'http://jira1.example.com/rest/api/2'
integration.url = 'http://jira1.example.com/rest/api/2'
expect(integration).not_to be_valid
expect(integration.password).to be_nil
end
it 'does not reset password if username changed' do
integration.username = 'some_name'
expect(integration).to be_valid
expect(integration.password).to eq(password)
end
it 'does not reset password if password changed' do
integration.url = 'http://jira_edited.example.com'
integration.password = 'new_password'
expect(integration).to be_valid
expect(integration.password).to eq('new_password')
end
it 'does not reset password if the password is touched and same as before' do
integration.url = 'http://jira_edited.example.com'
integration.password = password
expect(integration).to be_valid
expect(integration.password).to eq(password)
end
end
context 'when both web and api url present' do
let(:data_params) do
{
url: url, api_url: 'http://jira.example.com/rest/api/2',
username: username, password: password,
jira_issue_transition_id: transition_id
}
end
it 'resets password if api url changed' do
integration.api_url = 'http://jira_edited.example.com/rest/api/2'
expect(integration).not_to be_valid
expect(integration.password).to be_nil
end
it 'does not reset password if url changed' do
integration.url = 'http://jira_edited.example.com'
expect(integration).to be_valid
expect(integration.password).to eq(password)
end
it 'resets password if api url set to empty' do
integration.api_url = ''
expect(integration).not_to be_valid
expect(integration.password).to be_nil
end
end
end
context 'when no password was previously set' do
let(:data_params) do
{
url: url, username: username
}
end
it 'saves password if new url is set together with password' do
integration.url = 'http://jira_edited.example.com/rest/api/2'
integration.password = 'password'
integration.save!
expect(integration.reload).to have_attributes(
url: 'http://jira_edited.example.com/rest/api/2',
password: 'password'
)
end
end
end
end
end

View File

@ -131,20 +131,6 @@ RSpec.describe ProjectImportState, type: :model do
describe 'import state transitions' do
context 'state transition: [:started] => [:finished]' do
let(:after_import_service) { spy(:after_import_service) }
let(:housekeeping_service) { spy(:housekeeping_service) }
before do
allow(Projects::AfterImportService)
.to receive(:new) { after_import_service }
allow(after_import_service)
.to receive(:execute) { housekeeping_service.execute }
allow(Repositories::HousekeepingService)
.to receive(:new) { housekeeping_service }
end
it 'resets last_error' do
error_message = 'Some error'
import_state = create(:import_state, :started, last_error: error_message)
@ -152,29 +138,28 @@ RSpec.describe ProjectImportState, type: :model do
expect { import_state.finish }.to change { import_state.last_error }.from(error_message).to(nil)
end
it 'performs housekeeping when an import of a fresh project is completed' do
it 'enqueues housekeeping when an import of a fresh project is completed' do
project = create(:project_empty_repo, :import_started, import_type: :github)
project.import_state.finish
expect(Projects::AfterImportWorker).to receive(:perform_async).with(project.id)
expect(after_import_service).to have_received(:execute)
expect(housekeeping_service).to have_received(:execute)
project.import_state.finish
end
it 'does not perform housekeeping when project repository does not exist' do
project = create(:project, :import_started, import_type: :github)
project.import_state.finish
expect(Projects::AfterImportWorker).not_to receive(:perform_async)
expect(housekeeping_service).not_to have_received(:execute)
project.import_state.finish
end
it 'does not perform housekeeping when project does not have a valid import type' do
it 'does not qneueue housekeeping when project does not have a valid import type' do
project = create(:project, :import_started, import_type: nil)
project.import_state.finish
expect(Projects::AfterImportWorker).not_to receive(:perform_async)
expect(housekeeping_service).not_to have_received(:execute)
project.import_state.finish
end
end
end

View File

@ -0,0 +1,110 @@
# frozen_string_literal: true
RSpec.shared_examples Integrations::ResetSecretFields do
describe '#exposing_secrets_fields' do
it 'returns an array of strings' do
expect(integration.exposing_secrets_fields).to be_a(Array)
expect(integration.exposing_secrets_fields).to all(be_a(String))
end
end
describe '#reset_secret_fields?' do
let(:exposing_fields) { integration.exposing_secrets_fields }
it 'returns false if no exposing field has changed' do
exposing_fields.each do |field|
allow(integration).to receive("#{field}_changed?").and_return(false)
end
expect(integration.send(:reset_secret_fields?)).to be(false)
end
it 'returns true if any exposing field has changed' do
exposing_fields.each do |field|
allow(integration).to receive("#{field}_changed?").and_return(true)
other_exposing_fields = exposing_fields.without(field)
other_exposing_fields.each do |other_field|
allow(integration).to receive("#{other_field}_changed?").and_return(false)
end
expect(integration.send(:reset_secret_fields?)).to be(true)
end
end
end
describe 'validation callback' do
before do
# Store a value in each password field
integration.secret_fields.each do |field|
integration.public_send("#{field}=", 'old value')
end
# Treat values as persisted
integration.reset_updated_properties
integration.instance_variable_set('@old_data_fields', nil) if integration.supports_data_fields?
end
context 'when an exposing field has changed' do
let(:exposing_field) { integration.exposing_secrets_fields.first }
before do
integration.public_send("#{exposing_field}=", 'new value')
end
it 'clears all secret fields' do
integration.valid?
integration.secret_fields.each do |field|
expect(integration.public_send(field)).to be_nil
expect(integration.properties[field]).to be_nil if integration.properties.present?
expect(integration.data_fields[field]).to be_nil if integration.supports_data_fields?
end
end
context 'when a secret field has been updated' do
let(:secret_field) { integration.secret_fields.first }
let(:other_secret_fields) { integration.secret_fields.without(secret_field) }
let(:new_value) { 'new value' }
before do
integration.public_send("#{secret_field}=", new_value)
end
it 'does not clear this secret field' do
integration.valid?
expect(integration.public_send(secret_field)).to eq('new value')
other_secret_fields.each do |field|
expect(integration.public_send(field)).to be_nil
end
end
context 'when a secret field has been updated with the same value' do
let(:new_value) { 'old value' }
it 'does not clear this secret field' do
integration.valid?
expect(integration.public_send(secret_field)).to eq('old value')
other_secret_fields.each do |field|
expect(integration.public_send(field)).to be_nil
end
end
end
end
end
context 'when no exposing field has changed' do
it 'does not clear any secret fields' do
integration.valid?
integration.secret_fields.each do |field|
expect(integration.public_send(field)).to eq('old value')
end
end
end
end
end

View File

@ -2,11 +2,12 @@
require 'spec_helper'
RSpec.describe Projects::AfterImportService do
RSpec.describe Projects::AfterImportWorker do
include GitHelpers
subject { described_class.new(project) }
subject { worker.perform(project.id) }
let(:worker) { described_class.new }
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:sha) { project.commit.sha }
@ -24,7 +25,7 @@ RSpec.describe Projects::AfterImportService do
end
it 'performs housekeeping' do
subject.execute
subject
expect(housekeeping_service).to have_received(:execute)
end
@ -34,7 +35,7 @@ RSpec.describe Projects::AfterImportService do
repository.write_ref('refs/pull/1/head', sha)
repository.write_ref('refs/pull/1/merge', sha)
subject.execute
subject
end
it 'removes refs/pull/**/*' do
@ -48,7 +49,7 @@ RSpec.describe Projects::AfterImportService do
before do
repository.write_ref("refs/#{name}/tmp", sha)
subject.execute
subject
end
it "does not remove refs/#{name}/tmp" do
@ -62,13 +63,14 @@ RSpec.describe Projects::AfterImportService do
let(:exception) { StandardError.new('after import error') }
before do
allow(repository)
.to receive(:delete_all_refs_except)
allow_next_instance_of(Repository) do |repository|
allow(repository).to receive(:delete_all_refs_except)
.and_raise(exception)
end
end
it 'throws after import error' do
expect { subject.execute }.to raise_exception('after import error')
expect { subject }.to raise_exception('after import error')
end
end
@ -88,30 +90,28 @@ RSpec.describe Projects::AfterImportService do
'error.message' => exception.to_s
}).and_call_original
subject.execute
subject
end
end
context 'when after import action throw retriable exception one time' do
let(:exception) { GRPC::DeadlineExceeded.new }
before do
expect(repository)
.to receive(:delete_all_refs_except)
.and_raise(exception)
expect(repository)
.to receive(:delete_all_refs_except)
.and_call_original
subject.execute
end
it 'removes refs/pull/**/*' do
subject
expect(rugged.references.map(&:name))
.not_to include(%r{\Arefs/pull/})
end
it 'records the failures in the database', :aggregate_failures do
expect_next_instance_of(Repository) do |repository|
expect(repository).to receive(:delete_all_refs_except).and_raise(exception)
expect(repository).to receive(:delete_all_refs_except).and_call_original
end
subject
import_failure = ImportFailure.last
expect(import_failure.source).to eq('delete_all_refs')