Add latest changes from gitlab-org/gitlab@master
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -149,6 +149,10 @@ module Integrations
|
|||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def webhook_placeholder
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def log_usage(_, _)
|
||||
|
|
|
@ -10,6 +10,7 @@ module Integrations
|
|||
non_empty_password_help
|
||||
non_empty_password_title
|
||||
api_only
|
||||
exposes_secrets
|
||||
].freeze
|
||||
|
||||
attr_reader :name
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -355,6 +355,8 @@
|
|||
- 1
|
||||
- - project_template_export
|
||||
- 1
|
||||
- - projects_after_import
|
||||
- 1
|
||||
- - projects_git_garbage_collect
|
||||
- 1
|
||||
- - projects_post_creation
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
f9f2dc1f24f02571a7919da72b78e54922fd4fe202bc326235485610264d137c
|
|
@ -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);
|
||||
|
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 49 KiB |
|
@ -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. |
|
||||
|
|
|
@ -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**.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Before Width: | Height: | Size: 115 KiB |
|
@ -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.
|
||||
|
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 20 KiB |
|
@ -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.
|
||||
|
||||
|
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 13 KiB |
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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: |-
|
||||
|
|
|
@ -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 ');
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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}`),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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')
|