Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md1
-rw-r--r--.rubocop_todo/rspec/verified_doubles.yml2
-rw-r--r--app/assets/javascripts/content_editor/extensions/paste_markdown.js2
-rw-r--r--app/assets/javascripts/content_editor/services/code_block_language_loader.js10
-rw-r--r--app/assets/javascripts/content_editor/services/content_editor.js4
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js4
-rw-r--r--app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js (renamed from app/assets/javascripts/content_editor/services/markdown_deserializer.js)7
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue35
-rw-r--r--app/graphql/types/ci/runner_type.rb3
-rw-r--r--app/models/concerns/integrations/reset_secret_fields.rb41
-rw-r--r--app/models/concerns/packages/destructible.rb2
-rw-r--r--app/models/integration.rb2
-rw-r--r--app/models/integrations/base_chat_notification.rb4
-rw-r--r--app/models/integrations/field.rb1
-rw-r--r--app/models/integrations/jira.rb23
-rw-r--r--app/models/project_import_state.rb4
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/packages/cleanup_package_file_worker.rb2
-rw-r--r--app/workers/projects/after_import_worker.rb (renamed from app/services/projects/after_import_service.rb)15
-rw-r--r--config/feature_flags/development/graphql_ci_runner_executor.yml8
-rw-r--r--config/feature_flags/development/rendered_diffs_viewer.yml4
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/migrate/20220420192542_add_id_for_cleanup_index_packages_package_files.rb19
-rw-r--r--db/schema_migrations/202204201925421
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/troubleshooting/img/GoogleWorkspace-basic-SAML_v14_10.pngbin69719 -> 39027 bytes
-rw-r--r--doc/administration/troubleshooting/img/GoogleWorkspace-claims_v14_10.pngbin54548 -> 30571 bytes
-rw-r--r--doc/administration/troubleshooting/img/GoogleWorkspace-linkscert_v14_10.pngbin77766 -> 50479 bytes
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/ci/pipelines/merge_trains.md4
-rw-r--r--doc/development/feature_flags/controls.md2
-rw-r--r--doc/development/new_fe_guide/development/performance.md14
-rw-r--r--doc/development/secure_coding_guidelines.md42
-rw-r--r--doc/development/testing_guide/img/k9s.pngbin117900 -> 0 bytes
-rw-r--r--doc/install/aws/gitlab_hybrid_on_aws.md4
-rw-r--r--doc/integration/img/limit_namespaces_projects_options.pngbin48021 -> 24863 bytes
-rw-r--r--doc/subscriptions/img/support_diagram_c.pngbin161323 -> 55374 bytes
-rw-r--r--doc/tutorials/img/branches_dropdown_v14_10.pngbin56137 -> 20781 bytes
-rw-r--r--doc/user/application_security/dast/index.md1
-rw-r--r--doc/user/group/value_stream_analytics/img/vsa_aggregated_data_toggle_v14_9.pngbin145830 -> 49736 bytes
-rw-r--r--doc/user/project/import/img/gitlab_import_history_page_v14_10.pngbin103923 -> 28219 bytes
-rw-r--r--doc/user/project/issues/img/design_management_v14_10.pngbin139386 -> 54706 bytes
-rw-r--r--doc/user/project/merge_requests/approvals/img/security_approvals_v15_0.pngbin23749 -> 13285 bytes
-rw-r--r--doc/user/project/repository/jupyter_notebooks/index.md10
-rw-r--r--lib/banzai/filter/image_lazy_load_filter.rb4
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/fixtures/markdown/markdown_golden_master_examples.yml16
-rw-r--r--spec/frontend/content_editor/services/code_block_language_loader_spec.js18
-rw-r--r--spec/frontend/content_editor/services/content_editor_spec.js8
-rw-r--r--spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js (renamed from spec/frontend/content_editor/services/markdown_deserializer_spec.js)16
-rw-r--r--spec/frontend/content_editor/services/markdown_sourcemap_spec.js2
-rw-r--r--spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/segmented_control_button_group_spec.js104
-rw-r--r--spec/lib/banzai/filter/image_lazy_load_filter_spec.rb5
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb12
-rw-r--r--spec/models/concerns/integrations/reset_secret_fields_spec.rb19
-rw-r--r--spec/models/integrations/base_chat_notification_spec.rb19
-rw-r--r--spec/models/integrations/jira_spec.rb137
-rw-r--r--spec/models/project_import_state_spec.rb31
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/reset_secret_fields_shared_examples.rb110
-rw-r--r--spec/workers/projects/after_import_worker_spec.rb (renamed from spec/services/projects/after_import_service_spec.rb)40
62 files changed, 546 insertions, 303 deletions
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index 5c1b669a88f..4cced5a25fe 100644
--- a/.gitlab/issue_templates/Security developer workflow.md
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -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
diff --git a/.rubocop_todo/rspec/verified_doubles.yml b/.rubocop_todo/rspec/verified_doubles.yml
index 9df856ea4a7..085cd2c08e4 100644
--- a/.rubocop_todo/rspec/verified_doubles.yml
+++ b/.rubocop_todo/rspec/verified_doubles.yml
@@ -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
diff --git a/app/assets/javascripts/content_editor/extensions/paste_markdown.js b/app/assets/javascripts/content_editor/extensions/paste_markdown.js
index e50732bd869..f87e4d8d1dd 100644
--- a/app/assets/javascripts/content_editor/extensions/paste_markdown.js
+++ b/app/assets/javascripts/content_editor/extensions/paste_markdown.js
@@ -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,
diff --git a/app/assets/javascripts/content_editor/services/code_block_language_loader.js b/app/assets/javascripts/content_editor/services/code_block_language_loader.js
index c967dd899de..74018d7e1e3 100644
--- a/app/assets/javascripts/content_editor/services/code_block_language_loader.js
+++ b/app/assets/javascripts/content_editor/services/code_block_language_loader.js
@@ -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]);
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
index 56badf965ee..21843c482a8 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -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)
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index af19a0ab0e4..a7e6bb8d5a2 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -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 });
};
diff --git a/app/assets/javascripts/content_editor/services/markdown_deserializer.js b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js
index cd4863d8eac..3742d14bfd1 100644
--- a/app/assets/javascripts/content_editor/services/markdown_deserializer.js
+++ b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js
@@ -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 };
},
};
};
diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
index 8b76af05ffe..6a03e38a31d 100644
--- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
@@ -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"
diff --git a/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue b/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue
new file mode 100644
index 00000000000..f50706b6de8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue
@@ -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>
diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb
index a7f0730f07e..598b691965f 100644
--- a/app/graphql/types/ci/runner_type.rb
+++ b/app/graphql/types/ci/runner_type.rb
@@ -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,
diff --git a/app/models/concerns/integrations/reset_secret_fields.rb b/app/models/concerns/integrations/reset_secret_fields.rb
new file mode 100644
index 00000000000..f79c4392f19
--- /dev/null
+++ b/app/models/concerns/integrations/reset_secret_fields.rb
@@ -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
diff --git a/app/models/concerns/packages/destructible.rb b/app/models/concerns/packages/destructible.rb
index a3b7d8580c1..647c63b7f60 100644
--- a/app/models/concerns/packages/destructible.rb
+++ b/app/models/concerns/packages/destructible.rb
@@ -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
diff --git a/app/models/integration.rb b/app/models/integration.rb
index c0e244e38b6..31d02016efb 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -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
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index 54bd595892f..9bf208abcf7 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -149,6 +149,10 @@ module Integrations
raise NotImplementedError
end
+ def webhook_placeholder
+ raise NotImplementedError
+ end
+
private
def log_usage(_, _)
diff --git a/app/models/integrations/field.rb b/app/models/integrations/field.rb
index f00c4236a92..ca7833c1a56 100644
--- a/app/models/integrations/field.rb
+++ b/app/models/integrations/field.rb
@@ -10,6 +10,7 @@ module Integrations
non_empty_password_help
non_empty_password_title
api_only
+ exposes_secrets
].freeze
attr_reader :name
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index a800b9e5baa..3f6802febf4 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -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
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index fabbd5b49cb..b1c1a5b6697 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -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
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index bfb70e0d496..2aa255da8c0 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -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
diff --git a/app/workers/packages/cleanup_package_file_worker.rb b/app/workers/packages/cleanup_package_file_worker.rb
index f188017ee7a..68a145920ca 100644
--- a/app/workers/packages/cleanup_package_file_worker.rb
+++ b/app/workers/packages/cleanup_package_file_worker.rb
@@ -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)
diff --git a/app/services/projects/after_import_service.rb b/app/workers/projects/after_import_worker.rb
index bb0d084d191..06211b2d991 100644
--- a/app/services/projects/after_import_service.rb
+++ b/app/workers/projects/after_import_worker.rb
@@ -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
diff --git a/config/feature_flags/development/graphql_ci_runner_executor.yml b/config/feature_flags/development/graphql_ci_runner_executor.yml
deleted file mode 100644
index f58482b32d8..00000000000
--- a/config/feature_flags/development/graphql_ci_runner_executor.yml
+++ /dev/null
@@ -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
diff --git a/config/feature_flags/development/rendered_diffs_viewer.yml b/config/feature_flags/development/rendered_diffs_viewer.yml
index e159b2656e1..bca0d638eaf 100644
--- a/config/feature_flags/development/rendered_diffs_viewer.yml
+++ b/config/feature_flags/development/rendered_diffs_viewer.yml
@@ -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
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 7ec142344ee..9750c9dd14e 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -355,6 +355,8 @@
- 1
- - project_template_export
- 1
+- - projects_after_import
+ - 1
- - projects_git_garbage_collect
- 1
- - projects_post_creation
diff --git a/db/migrate/20220420192542_add_id_for_cleanup_index_packages_package_files.rb b/db/migrate/20220420192542_add_id_for_cleanup_index_packages_package_files.rb
new file mode 100644
index 00000000000..a5a0001ab69
--- /dev/null
+++ b/db/migrate/20220420192542_add_id_for_cleanup_index_packages_package_files.rb
@@ -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
diff --git a/db/schema_migrations/20220420192542 b/db/schema_migrations/20220420192542
new file mode 100644
index 00000000000..fe62709c7a7
--- /dev/null
+++ b/db/schema_migrations/20220420192542
@@ -0,0 +1 @@
+f9f2dc1f24f02571a7919da72b78e54922fd4fe202bc326235485610264d137c \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 00fd254b7a9..3c3dd05f62b 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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);
diff --git a/doc/administration/troubleshooting/img/GoogleWorkspace-basic-SAML_v14_10.png b/doc/administration/troubleshooting/img/GoogleWorkspace-basic-SAML_v14_10.png
index bc11e18fb6f..e002503ddc0 100644
--- a/doc/administration/troubleshooting/img/GoogleWorkspace-basic-SAML_v14_10.png
+++ b/doc/administration/troubleshooting/img/GoogleWorkspace-basic-SAML_v14_10.png
Binary files differ
diff --git a/doc/administration/troubleshooting/img/GoogleWorkspace-claims_v14_10.png b/doc/administration/troubleshooting/img/GoogleWorkspace-claims_v14_10.png
index 78bb1725e9c..2b827e122a8 100644
--- a/doc/administration/troubleshooting/img/GoogleWorkspace-claims_v14_10.png
+++ b/doc/administration/troubleshooting/img/GoogleWorkspace-claims_v14_10.png
Binary files differ
diff --git a/doc/administration/troubleshooting/img/GoogleWorkspace-linkscert_v14_10.png b/doc/administration/troubleshooting/img/GoogleWorkspace-linkscert_v14_10.png
index e665e23058c..8d6c5010670 100644
--- a/doc/administration/troubleshooting/img/GoogleWorkspace-linkscert_v14_10.png
+++ b/doc/administration/troubleshooting/img/GoogleWorkspace-linkscert_v14_10.png
Binary files differ
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 164d678406d..4bfe4cac616 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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. |
diff --git a/doc/ci/pipelines/merge_trains.md b/doc/ci/pipelines/merge_trains.md
index 12ea3a70536..70045971ed6 100644
--- a/doc/ci/pipelines/merge_trains.md
+++ b/doc/ci/pipelines/merge_trains.md
@@ -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**.
diff --git a/doc/development/feature_flags/controls.md b/doc/development/feature_flags/controls.md
index f8f03773c12..68c14c1b0c9 100644
--- a/doc/development/feature_flags/controls.md
+++ b/doc/development/feature_flags/controls.md
@@ -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.
diff --git a/doc/development/new_fe_guide/development/performance.md b/doc/development/new_fe_guide/development/performance.md
index f34c407da84..ee853942cb9 100644
--- a/doc/development/new_fe_guide/development/performance.md
+++ b/doc/development/new_fe_guide/development/performance.md
@@ -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.
diff --git a/doc/development/secure_coding_guidelines.md b/doc/development/secure_coding_guidelines.md
index 2b70ee395a2..17f0a65ddc1 100644
--- a/doc/development/secure_coding_guidelines.md
+++ b/doc/development/secure_coding_guidelines.md
@@ -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
diff --git a/doc/development/testing_guide/img/k9s.png b/doc/development/testing_guide/img/k9s.png
deleted file mode 100644
index 34585b2a43a..00000000000
--- a/doc/development/testing_guide/img/k9s.png
+++ /dev/null
Binary files differ
diff --git a/doc/install/aws/gitlab_hybrid_on_aws.md b/doc/install/aws/gitlab_hybrid_on_aws.md
index 2c40efd5909..8ecc4784aed 100644
--- a/doc/install/aws/gitlab_hybrid_on_aws.md
+++ b/doc/install/aws/gitlab_hybrid_on_aws.md
@@ -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.
diff --git a/doc/integration/img/limit_namespaces_projects_options.png b/doc/integration/img/limit_namespaces_projects_options.png
index 0f89270dfc5..a3cf9933b7e 100644
--- a/doc/integration/img/limit_namespaces_projects_options.png
+++ b/doc/integration/img/limit_namespaces_projects_options.png
Binary files differ
diff --git a/doc/subscriptions/img/support_diagram_c.png b/doc/subscriptions/img/support_diagram_c.png
index a2fed80e912..e98baed8605 100644
--- a/doc/subscriptions/img/support_diagram_c.png
+++ b/doc/subscriptions/img/support_diagram_c.png
Binary files differ
diff --git a/doc/tutorials/img/branches_dropdown_v14_10.png b/doc/tutorials/img/branches_dropdown_v14_10.png
index 73d94f9823a..926baff0ff8 100644
--- a/doc/tutorials/img/branches_dropdown_v14_10.png
+++ b/doc/tutorials/img/branches_dropdown_v14_10.png
Binary files differ
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index ee57803dfc7..c845e4982ac 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -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.
diff --git a/doc/user/group/value_stream_analytics/img/vsa_aggregated_data_toggle_v14_9.png b/doc/user/group/value_stream_analytics/img/vsa_aggregated_data_toggle_v14_9.png
index 5ad8026b8fd..9a25a365229 100644
--- a/doc/user/group/value_stream_analytics/img/vsa_aggregated_data_toggle_v14_9.png
+++ b/doc/user/group/value_stream_analytics/img/vsa_aggregated_data_toggle_v14_9.png
Binary files differ
diff --git a/doc/user/project/import/img/gitlab_import_history_page_v14_10.png b/doc/user/project/import/img/gitlab_import_history_page_v14_10.png
index c93b5ed2b27..812696a8faa 100644
--- a/doc/user/project/import/img/gitlab_import_history_page_v14_10.png
+++ b/doc/user/project/import/img/gitlab_import_history_page_v14_10.png
Binary files differ
diff --git a/doc/user/project/issues/img/design_management_v14_10.png b/doc/user/project/issues/img/design_management_v14_10.png
index a10be15eafd..133c0a52d6b 100644
--- a/doc/user/project/issues/img/design_management_v14_10.png
+++ b/doc/user/project/issues/img/design_management_v14_10.png
Binary files differ
diff --git a/doc/user/project/merge_requests/approvals/img/security_approvals_v15_0.png b/doc/user/project/merge_requests/approvals/img/security_approvals_v15_0.png
index afc9b27a226..b28d216f180 100644
--- a/doc/user/project/merge_requests/approvals/img/security_approvals_v15_0.png
+++ b/doc/user/project/merge_requests/approvals/img/security_approvals_v15_0.png
Binary files differ
diff --git a/doc/user/project/repository/jupyter_notebooks/index.md b/doc/user/project/repository/jupyter_notebooks/index.md
index 39b57f89ceb..d013d2802bf 100644
--- a/doc/user/project/repository/jupyter_notebooks/index.md
+++ b/doc/user/project/repository/jupyter_notebooks/index.md
@@ -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.
diff --git a/lib/banzai/filter/image_lazy_load_filter.rb b/lib/banzai/filter/image_lazy_load_filter.rb
index 916c135b777..a8a275d2039 100644
--- a/lib/banzai/filter/image_lazy_load_filter.rb
+++ b/lib/banzai/filter/image_lazy_load_filter.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a5b5f29b845..c53cc7beb56 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/fixtures/markdown/markdown_golden_master_examples.yml b/spec/fixtures/markdown/markdown_golden_master_examples.yml
index bdd7c13c1a3..08b456d276b 100644
--- a/spec/fixtures/markdown/markdown_golden_master_examples.yml
+++ b/spec/fixtures/markdown/markdown_golden_master_examples.yml
@@ -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: |-
diff --git a/spec/frontend/content_editor/services/code_block_language_loader_spec.js b/spec/frontend/content_editor/services/code_block_language_loader_spec.js
index 905c1685b94..55f4e0ed4e3 100644
--- a/spec/frontend/content_editor/services/code_block_language_loader_spec.js
+++ b/spec/frontend/content_editor/services/code_block_language_loader_spec.js
@@ -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 ');
diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js
index 5b7a27b501d..18c5d89e391 100644
--- a/spec/frontend/content_editor/services/content_editor_spec.js
+++ b/spec/frontend/content_editor/services/content_editor_spec.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);
});
});
diff --git a/spec/frontend/content_editor/services/markdown_deserializer_spec.js b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
index bea43a0effc..e399c1f6bea 100644
--- a/spec/frontend/content_editor/services/markdown_deserializer_spec.js
+++ b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
@@ -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']);
});
});
diff --git a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
index abd9588daff..8a304c73163 100644
--- a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
+++ b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
@@ -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';
diff --git a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
index cafb3f231bd..7bb289408b8 100644
--- a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
@@ -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', () => {
diff --git a/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
new file mode 100644
index 00000000000..88445b6684c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
@@ -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}`),
+ );
+ });
+ });
+});
diff --git a/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb
index 9f5aa558f24..5b32be0ea62 100644
--- a/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb
+++ b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb
@@ -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'
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 44bbbe49cd3..d86191ca0c2 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -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
diff --git a/spec/models/concerns/integrations/reset_secret_fields_spec.rb b/spec/models/concerns/integrations/reset_secret_fields_spec.rb
new file mode 100644
index 00000000000..a372550c70f
--- /dev/null
+++ b/spec/models/concerns/integrations/reset_secret_fields_spec.rb
@@ -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
diff --git a/spec/models/integrations/base_chat_notification_spec.rb b/spec/models/integrations/base_chat_notification_spec.rb
index ac4031a9b7d..672d8de1e14 100644
--- a/spec/models/integrations/base_chat_notification_spec.rb
+++ b/spec/models/integrations/base_chat_notification_spec.rb
@@ -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
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index d244b1d33d5..fd7803c7360 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -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
diff --git a/spec/models/project_import_state_spec.rb b/spec/models/project_import_state_spec.rb
index 42ca8130734..fb17071cc17 100644
--- a/spec/models/project_import_state_spec.rb
+++ b/spec/models/project_import_state_spec.rb
@@ -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
diff --git a/spec/support/shared_examples/models/concerns/integrations/reset_secret_fields_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/reset_secret_fields_shared_examples.rb
new file mode 100644
index 00000000000..873f858e432
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/integrations/reset_secret_fields_shared_examples.rb
@@ -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
diff --git a/spec/services/projects/after_import_service_spec.rb b/spec/workers/projects/after_import_worker_spec.rb
index a16aec891a9..332b547bb66 100644
--- a/spec/services/projects/after_import_service_spec.rb
+++ b/spec/workers/projects/after_import_worker_spec.rb
@@ -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')