Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-10-07 15:09:16 +00:00
parent 414fefc8c4
commit 9a1203dfe8
45 changed files with 1030 additions and 320 deletions

View File

@ -1,3 +1,5 @@
import initSearchSettings from '~/search_settings';
import initWebhookForm from '~/webhooks';
initSearchSettings();
initWebhookForm();

View File

@ -0,0 +1,73 @@
<script>
import { GlFormGroup, GlFormInput, GlFormRadio, GlFormRadioGroup } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import FormUrlMaskItem from './form_url_mask_item.vue';
export default {
components: {
FormUrlMaskItem,
GlFormGroup,
GlFormInput,
GlFormRadio,
GlFormRadioGroup,
},
data() {
return {
maskEnabled: false,
url: null,
};
},
computed: {
maskedUrl() {
return this.url;
},
},
i18n: {
radioFullUrlText: s__('Webhooks|Show full URL'),
radioMaskUrlText: s__('Webhooks|Mask portions of URL'),
radioMaskUrlHelp: s__('Webhooks|Do not show sensitive data such as tokens in the UI.'),
urlDescription: s__(
'Webhooks|URL must be percent-encoded if it contains one or more special characters.',
),
urlLabel: __('URL'),
urlPlaceholder: 'http://example.com/trigger-ci.json',
urlPreview: s__('Webhooks|URL preview'),
},
};
</script>
<template>
<div>
<gl-form-group
:label="$options.i18n.urlLabel"
label-for="webhook-url"
:description="$options.i18n.urlDescription"
>
<gl-form-input
id="webhook-url"
v-model="url"
name="hook[url]"
:placeholder="$options.i18n.urlPlaceholder"
/>
</gl-form-group>
<div class="gl-mt-5">
<gl-form-radio-group v-model="maskEnabled">
<gl-form-radio :value="false">{{ $options.i18n.radioFullUrlText }}</gl-form-radio>
<gl-form-radio :value="true"
>{{ $options.i18n.radioMaskUrlText }}
<template #help>
{{ $options.i18n.radioMaskUrlHelp }}
</template>
</gl-form-radio>
</gl-form-radio-group>
<div v-if="maskEnabled" class="gl-ml-6" data-testid="url-mask-section">
<form-url-mask-item :index="0" />
<gl-form-group :label="$options.i18n.urlPreview" label-for="webhook-url-preview">
<gl-form-input id="webhook-url-preview" :value="maskedUrl" readonly />
</gl-form-group>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,61 @@
<script>
import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlButton,
GlFormGroup,
GlFormInput,
},
props: {
index: {
type: Number,
required: false,
default: null,
},
},
computed: {
keyInputId() {
return this.inputId('key');
},
valueInputId() {
return this.inputId('value');
},
},
methods: {
inputId(type) {
return `webhook-url-mask-item-${type}-${this.index}`;
},
inputName(type) {
return `hook[url_variables][][${type}]`;
},
},
i18n: {
keyLabel: s__('Webhooks|How it looks in the UI'),
valueLabel: s__('Webhooks|Sensitive portion of URL'),
},
};
</script>
<template>
<div class="gl-display-flex gl-align-items-flex-end gl-gap-3 gl-mb-5">
<gl-form-group
:label="$options.i18n.valueLabel"
:label-for="valueInputId"
class="gl-flex-grow-1 gl-mb-0"
data-testid="mask-item-value"
>
<gl-form-input :id="valueInputId" :name="inputName('value')" />
</gl-form-group>
<gl-form-group
:label="$options.i18n.keyLabel"
:label-for="keyInputId"
class="gl-flex-grow-1 gl-mb-0"
data-testid="mask-item-key"
>
<gl-form-input :id="keyInputId" :name="inputName('key')" />
</gl-form-group>
<gl-button icon="remove" />
</div>
</template>

View File

@ -0,0 +1,18 @@
import Vue from 'vue';
import FormUrlApp from './components/form_url_app.vue';
export default () => {
const el = document.querySelector('.js-vue-webhook-form');
if (!el) {
return null;
}
return new Vue({
el,
name: 'WebhookFormRoot',
render(createElement) {
return createElement(FormUrlApp, {});
},
});
};

View File

@ -22,6 +22,7 @@ class Note < ApplicationRecord
include ThrottledTouch
include FromUnion
include Sortable
include EachBatch
ISSUE_TASK_SYSTEM_NOTE_PATTERN = /\A.*marked\sthe\stask.+as\s(completed|incomplete).*\z/.freeze

View File

@ -1,10 +1,13 @@
= form_errors(hook)
.form-group
= form.label :url, s_('Webhooks|URL'), class: 'label-bold'
= form.text_field :url, class: 'form-control gl-form-input', placeholder: 'http://example.com/trigger-ci.json'
%p.form-text.text-muted
= s_('Webhooks|URL must be percent-encoded if it contains one or more special characters.')
- if Feature.enabled?(:webhook_form_mask_url)
.js-vue-webhook-form
- else
.form-group
= form.label :url, s_('Webhooks|URL'), class: 'label-bold'
= form.text_field :url, class: 'form-control gl-form-input', placeholder: 'http://example.com/trigger-ci.json'
%p.form-text.text-muted
= s_('Webhooks|URL must be percent-encoded if it contains one or more special characters.')
.form-group
= form.label :token, s_('Webhooks|Secret token'), class: 'label-bold'
= form.text_field :token, class: 'form-control gl-form-input', placeholder: ''

View File

@ -1020,6 +1020,24 @@
:weight: 1
:idempotent: false
:tags: []
- :name: github_importer:github_import_attachments_import_note
:worker_name: Gitlab::GithubImport::Attachments::ImportNoteWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: false
:tags: []
- :name: github_importer:github_import_attachments_import_release
:worker_name: Gitlab::GithubImport::Attachments::ImportReleaseWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: false
:tags: []
- :name: github_importer:github_import_import_diff_note
:worker_name: Gitlab::GithubImport::ImportDiffNoteWorker
:feature_category: :importers

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Attachments
class ImportNoteWorker # rubocop:disable Scalability/IdempotentWorker
include ObjectImporter
def representation_class
Representation::NoteText
end
def importer_class
Importer::NoteAttachmentsImporter
end
def object_type
:note_attachment
end
end
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Attachments
class ImportReleaseWorker # rubocop:disable Scalability/IdempotentWorker
include ObjectImporter
def representation_class
Representation::NoteText
end
def importer_class
Importer::NoteAttachmentsImporter
end
def object_type
:release_attachment
end
end
end
end
end

View File

@ -1,16 +1,18 @@
# frozen_string_literal: true
# TODO: remove in 16.X milestone
# https://gitlab.com/gitlab-org/gitlab/-/issues/377059
module Gitlab
module GithubImport
class ImportReleaseAttachmentsWorker # rubocop:disable Scalability/IdempotentWorker
include ObjectImporter
def representation_class
Representation::ReleaseAttachments
Representation::NoteText
end
def importer_class
Importer::ReleaseAttachmentsImporter
Importer::NoteAttachmentsImporter
end
def object_type

View File

@ -28,7 +28,10 @@ module Gitlab
# For future issue/mr/milestone/etc attachments importers
def importers
[::Gitlab::GithubImport::Importer::ReleasesAttachmentsImporter]
[
::Gitlab::GithubImport::Importer::Attachments::ReleasesImporter,
::Gitlab::GithubImport::Importer::Attachments::NotesImporter
]
end
def start_importer(project, importer, client)

View File

@ -0,0 +1,8 @@
---
name: webhook_form_mask_url
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/99995
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/376106
milestone: '15.5'
type: development
group: group::integrations
default_enabled: false

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddIndexReleasesProjectIdId < Gitlab::Database::Migration[2.0]
INDEX_NAME = 'index_releases_on_project_id_id'
disable_ddl_transaction!
def up
add_concurrent_index :releases, %i[project_id id], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :releases, name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
0f625bd9df16d035dd8cd73d5097e5b5f4d1b900183bd6ccf4b1489319535df6

View File

@ -30160,6 +30160,8 @@ CREATE INDEX index_releases_on_author_id_id_created_at ON releases USING btree (
CREATE INDEX index_releases_on_project_id_and_released_at_and_id ON releases USING btree (project_id, released_at, id);
CREATE INDEX index_releases_on_project_id_id ON releases USING btree (project_id, id);
CREATE UNIQUE INDEX index_releases_on_project_tag_unique ON releases USING btree (project_id, tag);
CREATE INDEX index_releases_on_released_at ON releases USING btree (released_at);

View File

@ -127,13 +127,15 @@ comments.
### 10. Stage::ImportAttachmentsWorker
This worker imports release notes attachments that are linked inside Markdown.
For every release of the project, we schedule a job of
`Gitlab::GithubImport::ImportReleaseAttachmentsWorker` for every comment.
This worker imports note attachments that are linked inside Markdown.
For each entity with Markdown text in the project, we schedule a job of:
- `Gitlab::GithubImport::ImportReleaseAttachmentsWorker` for every release.
- `Gitlab::GithubImport::ImportNoteAttachmentsWorker` for every note.
Each job:
1. Iterates over all attachment links inside of a specific release note.
1. Iterates over all attachment links inside of a specific record.
1. Downloads the attachment.
1. Replaces the old link with a newly-generated link to GitLab.

View File

@ -178,6 +178,11 @@ The following items of a project are imported:
- Milestones.
- Labels.
- Release note descriptions.
- Attachments for:
- Release notes. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15620) in GitLab 15.4.
- Comments and notes. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18052) in GitLab 15.5.
NOTE: All attachment importers work under `github_importer_attachments_import` [feature flag](../../../administration/feature_flags.md) disabled by default.
- Release note attachments. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15620) in GitLab 15.4 with `github_importer_attachments_import`
[feature flag](../../../administration/feature_flags.md) disabled by default.
- Pull request review comments.

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Exceptions
# Sometimes it's not clear which of not implemented interfaces caused this error.
# We need custom exception to be able to add text that gives extra context.
NotImplementedError = Class.new(StandardError)
end
end
end

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Importer
module Attachments
class BaseImporter
include ParallelScheduling
BATCH_SIZE = 100
# The method that will be called for traversing through all the objects to
# import, yielding them to the supplied block.
def each_object_to_import
collection.each_batch(of: BATCH_SIZE) do |batch|
batch.each do |record|
next if already_imported?(record)
Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :fetched)
yield record
# We mark the object as imported immediately so we don't end up
# scheduling it multiple times.
mark_as_imported(record)
end
end
end
def representation_class
Representation::NoteText
end
def importer_class
NoteAttachmentsImporter
end
private
def collection
raise Gitlab::GithubImport::Exceptions::NotImplementedError, '#collection'
end
def object_representation(object)
representation_class.from_db_record(object)
end
end
end
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Importer
module Attachments
class NotesImporter < ::Gitlab::GithubImport::Importer::Attachments::BaseImporter
def sidekiq_worker_class
::Gitlab::GithubImport::Attachments::ImportNoteWorker
end
def collection_method
:note_attachments
end
def object_type
:note_attachment
end
def id_for_already_imported_cache(note)
note.id
end
private
# TODO: exclude :system, :noteable_type from select after removing override Note#note method
# https://gitlab.com/gitlab-org/gitlab/-/issues/369923
def collection
project.notes.user.select(:id, :note, :system, :noteable_type)
end
end
end
end
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Importer
module Attachments
class ReleasesImporter < ::Gitlab::GithubImport::Importer::Attachments::BaseImporter
def sidekiq_worker_class
::Gitlab::GithubImport::Attachments::ImportReleaseWorker
end
def collection_method
:release_attachments
end
def object_type
:release_attachment
end
def id_for_already_imported_cache(release)
release.id
end
private
def collection
project.releases.select(:id, :description)
end
end
end
end
end
end

View File

@ -3,26 +3,27 @@
module Gitlab
module GithubImport
module Importer
class ReleaseAttachmentsImporter
attr_reader :release_db_id, :release_description, :project
class NoteAttachmentsImporter
attr_reader :note_text, :project
# release - An instance of `ReleaseAttachments`.
# note_text - An instance of `NoteText`.
# project - An instance of `Project`.
# client - An instance of `Gitlab::GithubImport::Client`.
def initialize(release_attachments, project, _client = nil)
@release_db_id = release_attachments.release_db_id
@release_description = release_attachments.description
def initialize(note_text, project, _client = nil)
@note_text = note_text
@project = project
end
def execute
attachment_urls = MarkdownText.fetch_attachment_urls(release_description)
new_description = attachment_urls.reduce(release_description) do |description, url|
attachment_urls = MarkdownText.fetch_attachment_urls(note_text.text)
return if attachment_urls.blank?
new_text = attachment_urls.reduce(note_text.text) do |text, url|
new_url = download_attachment(url)
description.gsub(url, new_url)
text.gsub(url, new_url)
end
Release.find(release_db_id).update_column(:description, new_description)
update_note_record(new_text)
end
private
@ -52,6 +53,15 @@ module Gitlab
def extract_name_from_markdown(text)
text.match(%r{^!?\[.*\]}).to_a[0]
end
def update_note_record(text)
case note_text.record_type
when ::Release.name
::Release.find(note_text.record_db_id).update_column(:description, text)
when ::Note.name
::Note.find(note_text.record_db_id).update_column(:note, text)
end
end
end
end
end

View File

@ -36,6 +36,9 @@ module Gitlab
# We're using bulk_insert here so we can bypass any validations and
# callbacks. Running these would result in a lot of unnecessary SQL
# queries being executed when importing large projects.
# Note: if you're going to replace `legacy_bulk_insert` with something that trigger callback
# to generate HTML version - you also need to regenerate it in
# Gitlab::GithubImport::Importer::NoteAttachmentsImporter.
ApplicationRecord.legacy_bulk_insert(Note.table_name, [attributes]) # rubocop:disable Gitlab/BulkInsert
rescue ActiveRecord::InvalidForeignKey
# It's possible the project and the issue have been deleted since

View File

@ -1,59 +0,0 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Importer
class ReleasesAttachmentsImporter
include ParallelScheduling
BATCH_SIZE = 100
# The method that will be called for traversing through all the objects to
# import, yielding them to the supplied block.
def each_object_to_import
project.releases.select(:id, :description).each_batch(of: BATCH_SIZE, column: :id) do |batch|
batch.each do |release|
next if already_imported?(release)
Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :fetched)
yield release
# We mark the object as imported immediately so we don't end up
# scheduling it multiple times.
mark_as_imported(release)
end
end
end
def representation_class
Representation::ReleaseAttachments
end
def importer_class
ReleaseAttachmentsImporter
end
def sidekiq_worker_class
ImportReleaseAttachmentsWorker
end
def collection_method
:release_attachments
end
def object_type
:release_attachment
end
def id_for_already_imported_cache(release)
release.id
end
def object_representation(object)
representation_class.from_db_record(object)
end
end
end
end
end

View File

@ -12,6 +12,9 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
# Note: if you're going to replace `legacy_bulk_insert` with something that triggers callback
# to generate HTML version - you also need to regenerate it in
# Gitlab::GithubImport::Importer::NoteAttachmentsImporter.
def execute
bulk_insert(Release, build_releases)
end

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
# This class only partly represents Release record from DB and
# is used to connect ReleasesAttachmentsImporter, NotesAttachmentsImporter etc.
# with NoteAttachmentsImporter without modifying ObjectImporter a lot.
# Attachments are inside release's `description`.
module Gitlab
module GithubImport
module Representation
class NoteText
include ToHash
include ExposeAttribute
MODELS_WHITELIST = [::Release, ::Note].freeze
ModelNotSupported = Class.new(StandardError)
attr_reader :attributes
expose_attribute :record_db_id, :record_type, :text
class << self
# Builds a note text representation from DB record of Note or Release.
#
# record - An instance of `Release` or `Note` model.
def from_db_record(record)
check_record_class!(record)
record_type = record.class.name
text = record.is_a?(Release) ? record.description : record.note
new(
record_db_id: record.id,
record_type: record_type,
text: text
)
end
def from_json_hash(raw_hash)
new Representation.symbolize_hash(raw_hash)
end
private
def check_record_class!(record)
raise ModelNotSupported, record.class.name if MODELS_WHITELIST.exclude?(record.class)
end
end
# attributes - A Hash containing the event details. The keys of this
# Hash (and any nested hashes) must be symbols.
def initialize(attributes)
@attributes = attributes
end
def github_identifiers
{ db_id: record_db_id }
end
end
end
end
end

View File

@ -1,44 +0,0 @@
# frozen_string_literal: true
# This class only partly represents Release record from DB and
# is used to connect ReleasesAttachmentsImporter with ReleaseAttachmentsImporter
# without modifying ObjectImporter a lot.
# Attachments are inside release's `description`.
module Gitlab
module GithubImport
module Representation
class ReleaseAttachments
include ToHash
include ExposeAttribute
attr_reader :attributes
expose_attribute :release_db_id, :description
# Builds a event from a GitHub API response.
#
# release - An instance of `Release` model.
def self.from_db_record(release)
new(
release_db_id: release.id,
description: release.description
)
end
def self.from_json_hash(raw_hash)
new Representation.symbolize_hash(raw_hash)
end
# attributes - A Hash containing the event details. The keys of this
# Hash (and any nested hashes) must be symbols.
def initialize(attributes)
@attributes = attributes
end
def github_identifiers
{ db_id: release_db_id }
end
end
end
end
end

View File

@ -45011,6 +45011,9 @@ msgstr ""
msgid "Webhooks|Deployment events"
msgstr ""
msgid "Webhooks|Do not show sensitive data such as tokens in the UI."
msgstr ""
msgid "Webhooks|Enable SSL verification"
msgstr ""
@ -45026,12 +45029,18 @@ msgstr ""
msgid "Webhooks|Go to webhooks"
msgstr ""
msgid "Webhooks|How it looks in the UI"
msgstr ""
msgid "Webhooks|Issues events"
msgstr ""
msgid "Webhooks|Job events"
msgstr ""
msgid "Webhooks|Mask portions of URL"
msgstr ""
msgid "Webhooks|Member events"
msgstr ""
@ -45056,6 +45065,12 @@ msgstr ""
msgid "Webhooks|Secret token"
msgstr ""
msgid "Webhooks|Sensitive portion of URL"
msgstr ""
msgid "Webhooks|Show full URL"
msgstr ""
msgid "Webhooks|Subgroup events"
msgstr ""
@ -45080,6 +45095,9 @@ msgstr ""
msgid "Webhooks|URL must be percent-encoded if it contains one or more special characters."
msgstr ""
msgid "Webhooks|URL preview"
msgstr ""
msgid "Webhooks|Used to validate received payloads. Sent with the request in the %{code_start}X-Gitlab-Token HTTP%{code_end} header."
msgstr ""

View File

@ -48,10 +48,10 @@ RSpec.describe 'Projects > Settings > Webhook Settings' do
expect(page).to have_content('Releases events')
end
it 'create webhook' do
it 'create webhook', :js do
visit webhooks_path
fill_in 'hook_url', with: url
fill_in 'URL', with: url
check 'Tag push events'
fill_in 'hook_push_events_branch_filter', with: 'master'
check 'Enable SSL verification'
@ -66,12 +66,12 @@ RSpec.describe 'Projects > Settings > Webhook Settings' do
expect(page).to have_content('Job events')
end
it 'edit existing webhook' do
it 'edit existing webhook', :js do
hook
visit webhooks_path
click_link 'Edit'
fill_in 'hook_url', with: url
fill_in 'URL', with: url
check 'Enable SSL verification'
click_button 'Save changes'

View File

@ -0,0 +1,53 @@
import { nextTick } from 'vue';
import { GlFormRadio, GlFormRadioGroup } from '@gitlab/ui';
import FormUrlApp from '~/webhooks/components/form_url_app.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('FormUrlApp', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(FormUrlApp);
};
afterEach(() => {
wrapper.destroy();
});
const findAllRadioButtons = () => wrapper.findAllComponents(GlFormRadio);
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findUrlMaskDisable = () => findAllRadioButtons().at(0);
const findUrlMaskEnable = () => findAllRadioButtons().at(1);
const findUrlMaskSection = () => wrapper.findByTestId('url-mask-section');
describe('template', () => {
it('renders radio buttons for URL masking', () => {
createComponent();
expect(findAllRadioButtons().length).toBe(2);
expect(findUrlMaskDisable().text()).toBe(FormUrlApp.i18n.radioFullUrlText);
expect(findUrlMaskEnable().text()).toBe(FormUrlApp.i18n.radioMaskUrlText);
});
it('does not render mask section', () => {
createComponent();
expect(findUrlMaskSection().exists()).toBe(false);
});
describe('on radio select', () => {
beforeEach(async () => {
createComponent();
findRadioGroup().vm.$emit('input', true);
await nextTick();
});
it('renders mask section', () => {
expect(findUrlMaskSection().exists()).toBe(true);
});
});
});
});

View File

@ -0,0 +1,51 @@
import { GlButton, GlFormInput } from '@gitlab/ui';
import FormUrlMaskItem from '~/webhooks/components/form_url_mask_item.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('FormUrlMaskItem', () => {
let wrapper;
const defaultProps = {
index: 0,
};
const createComponent = () => {
wrapper = shallowMountExtended(FormUrlMaskItem, {
propsData: { ...defaultProps },
});
};
afterEach(() => {
wrapper.destroy();
});
const findMaskItemKey = () => wrapper.findByTestId('mask-item-key');
const findMaskItemValue = () => wrapper.findByTestId('mask-item-value');
const findRemoveButton = () => wrapper.findComponent(GlButton);
describe('template', () => {
it('renders input for key and value', () => {
createComponent();
const keyInput = findMaskItemKey();
expect(keyInput.attributes('label')).toBe(FormUrlMaskItem.i18n.keyLabel);
expect(keyInput.findComponent(GlFormInput).attributes('name')).toBe(
'hook[url_variables][][key]',
);
const valueInput = findMaskItemValue();
expect(valueInput.attributes('label')).toBe(FormUrlMaskItem.i18n.valueLabel);
expect(valueInput.findComponent(GlFormInput).attributes('name')).toBe(
'hook[url_variables][][value]',
);
});
it('renders remove button', () => {
createComponent();
expect(findRemoveButton().props('icon')).toBe('remove');
});
});
});

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::Attachments::BaseImporter do
subject(:importer) { importer_class.new(project, client) }
let(:project) { instance_double(Project, id: 1) }
let(:client) { instance_double(Gitlab::GithubImport::Client) }
let(:importer_class) do
Class.new(described_class) do
private
def collection_method
'test'
end
end
end
describe '#each_object_to_import' do
context 'with not implemented #collection interface' do
it 'raises NotImplementedError' do
expect { importer.each_object_to_import }
.to raise_error(Gitlab::GithubImport::Exceptions::NotImplementedError, '#collection')
end
end
end
end

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::Attachments::NotesImporter do
subject(:importer) { described_class.new(project, client) }
let_it_be(:project) { create(:project) }
let(:client) { instance_double(Gitlab::GithubImport::Client) }
describe '#sequential_import', :clean_gitlab_redis_cache do
let_it_be(:note_1) { create(:note, project: project) }
let_it_be(:note_2) { create(:note, project: project) }
let_it_be(:system_note) { create(:note, :system, project: project) }
let(:importer_stub) { instance_double('Gitlab::GithubImport::Importer::NoteAttachmentsImporter') }
let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] }
it 'imports each project user note' do
expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new)
.with(*importer_attrs).twice.and_return(importer_stub)
expect(importer_stub).to receive(:execute).twice
importer.sequential_import
end
context 'when note is already processed' do
it "doesn't import this note" do
importer.mark_as_imported(note_1)
expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new)
.with(*importer_attrs).once.and_return(importer_stub)
expect(importer_stub).to receive(:execute).once
importer.sequential_import
end
end
end
describe '#representation_class' do
it { expect(importer.representation_class).to eq(Gitlab::GithubImport::Representation::NoteText) }
end
describe '#importer_class' do
it { expect(importer.importer_class).to eq(Gitlab::GithubImport::Importer::NoteAttachmentsImporter) }
end
describe '#sidekiq_worker_class' do
it { expect(importer.sidekiq_worker_class).to eq(Gitlab::GithubImport::Attachments::ImportNoteWorker) }
end
describe '#collection_method' do
it { expect(importer.collection_method).to eq(:note_attachments) }
end
describe '#object_type' do
it { expect(importer.object_type).to eq(:note_attachment) }
end
describe '#id_for_already_imported_cache' do
let(:note) { build_stubbed(:note) }
it { expect(importer.id_for_already_imported_cache(note)).to eq(note.id) }
end
end

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::Attachments::ReleasesImporter do
subject(:importer) { described_class.new(project, client) }
let_it_be(:project) { create(:project) }
let(:client) { instance_double(Gitlab::GithubImport::Client) }
describe '#sequential_import', :clean_gitlab_redis_cache do
let_it_be(:release_1) { create(:release, project: project) }
let_it_be(:release_2) { create(:release, project: project) }
let(:importer_stub) { instance_double('Gitlab::GithubImport::Importer::NoteAttachmentsImporter') }
let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] }
it 'imports each project release' do
expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new)
.with(*importer_attrs).twice.and_return(importer_stub)
expect(importer_stub).to receive(:execute).twice
importer.sequential_import
end
context 'when note is already processed' do
it "doesn't import this release" do
importer.mark_as_imported(release_1)
expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new)
.with(*importer_attrs).once.and_return(importer_stub)
expect(importer_stub).to receive(:execute).once
importer.sequential_import
end
end
end
describe '#representation_class' do
it { expect(importer.representation_class).to eq(Gitlab::GithubImport::Representation::NoteText) }
end
describe '#importer_class' do
it { expect(importer.importer_class).to eq(Gitlab::GithubImport::Importer::NoteAttachmentsImporter) }
end
describe '#sidekiq_worker_class' do
it { expect(importer.sidekiq_worker_class).to eq(Gitlab::GithubImport::Attachments::ImportReleaseWorker) }
end
describe '#collection_method' do
it { expect(importer.collection_method).to eq(:release_attachments) }
end
describe '#object_type' do
it { expect(importer.object_type).to eq(:release_attachment) }
end
describe '#id_for_already_imported_cache' do
let(:release) { build_stubbed(:release) }
it { expect(importer.id_for_already_imported_cache(release)).to eq(release.id) }
end
end

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do
subject(:importer) { described_class.new(note_text, project, client) }
let_it_be(:project) { create(:project) }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
let(:doc_url) { 'https://github.com/nickname/public-test-repo/files/9020437/git-cheat-sheet.txt' }
let(:image_url) { 'https://user-images.githubusercontent.com/6833842/0cf366b61ef2.jpeg' }
let(:text) do
<<-TEXT.strip
Some text...
[special-doc](#{doc_url})
![image.jpeg](#{image_url})
TEXT
end
describe '#execute' do
let(:downloader_stub) { instance_double(Gitlab::GithubImport::AttachmentsDownloader) }
let(:tmp_stub_doc) { Tempfile.create('attachment_download_test.txt') }
let(:tmp_stub_image) { Tempfile.create('image.jpeg') }
before do
allow(Gitlab::GithubImport::AttachmentsDownloader).to receive(:new).with(doc_url)
.and_return(downloader_stub)
allow(Gitlab::GithubImport::AttachmentsDownloader).to receive(:new).with(image_url)
.and_return(downloader_stub)
allow(downloader_stub).to receive(:perform).and_return(tmp_stub_doc, tmp_stub_image)
allow(downloader_stub).to receive(:delete).twice
allow(UploadService).to receive(:new)
.with(project, tmp_stub_doc, FileUploader).and_call_original
allow(UploadService).to receive(:new)
.with(project, tmp_stub_image, FileUploader).and_call_original
end
context 'when importing release attachments' do
let(:release) { create(:release, project: project, description: text) }
let(:note_text) { Gitlab::GithubImport::Representation::NoteText.from_db_record(release) }
it 'updates release description with new attachment urls' do
importer.execute
release.reload
expect(release.description).to start_with("Some text...\n\n [special-doc](/uploads/")
expect(release.description).to include('![image.jpeg](/uploads/')
end
end
context 'when importing note attachments' do
let(:note) { create(:note, project: project, note: text) }
let(:note_text) { Gitlab::GithubImport::Representation::NoteText.from_db_record(note) }
it 'updates note text with new attachment urls' do
importer.execute
note.reload
expect(note.note).to start_with("Some text...\n\n [special-doc](/uploads/")
expect(note.note).to include('![image.jpeg](/uploads/')
end
end
end
end

View File

@ -160,6 +160,13 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do
expect(project.notes.take).to be_valid
end
# rubocop:disable RSpec/AnyInstanceOf
it 'skips markdown field cache callback' do
expect_any_instance_of(Note).not_to receive(:refresh_markdown_cache)
importer.execute
end
# rubocop:enable RSpec/AnyInstanceOf
end
describe '#find_noteable_id' do

View File

@ -1,57 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::ReleaseAttachmentsImporter do
subject(:importer) { described_class.new(release_attachments, project, client) }
let_it_be(:project) { create(:project) }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
let(:release) { create(:release, project: project, description: description) }
let(:release_attachments) do
Gitlab::GithubImport::Representation::ReleaseAttachments
.from_json_hash(release_db_id: release.id, description: release.description)
end
let(:doc_url) { 'https://github.com/nickname/public-test-repo/files/9020437/git-cheat-sheet.txt' }
let(:image_url) { 'https://user-images.githubusercontent.com/6833842/0cf366b61ef2.jpeg' }
let(:description) do
<<-TEXT.strip
Some text...
[special-doc](#{doc_url})
![image.jpeg](#{image_url})
TEXT
end
describe '#execute' do
let(:downloader_stub) { instance_double(Gitlab::GithubImport::AttachmentsDownloader) }
let(:tmp_stub_doc) { Tempfile.create('attachment_download_test.txt') }
let(:tmp_stub_image) { Tempfile.create('image.jpeg') }
context 'when importing doc attachment' do
before do
allow(Gitlab::GithubImport::AttachmentsDownloader).to receive(:new).with(doc_url)
.and_return(downloader_stub)
allow(Gitlab::GithubImport::AttachmentsDownloader).to receive(:new).with(image_url)
.and_return(downloader_stub)
allow(downloader_stub).to receive(:perform).and_return(tmp_stub_doc, tmp_stub_image)
allow(downloader_stub).to receive(:delete).twice
allow(UploadService).to receive(:new)
.with(project, tmp_stub_doc, FileUploader).and_call_original
allow(UploadService).to receive(:new)
.with(project, tmp_stub_image, FileUploader).and_call_original
end
it 'updates release description with new attachment url' do
importer.execute
release.reload
expect(release.description).to start_with("Some text...\n\n [special-doc](/uploads/")
expect(release.description).to include('![image.jpeg](/uploads/')
end
end
end
end

View File

@ -1,74 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::ReleasesAttachmentsImporter do
subject { described_class.new(project, client) }
let_it_be(:project) { create(:project) }
let(:client) { instance_double(Gitlab::GithubImport::Client) }
describe '#each_object_to_import', :clean_gitlab_redis_cache do
let!(:release_1) { create(:release, project: project) }
let!(:release_2) { create(:release, project: project) }
it 'iterates each project release' do
list = []
subject.each_object_to_import do |object|
list << object
end
expect(list).to contain_exactly(release_1, release_2)
end
context 'when release is already processed' do
it "doesn't process this release" do
subject.mark_as_imported(release_1)
list = []
subject.each_object_to_import do |object|
list << object
end
expect(list).to contain_exactly(release_2)
end
end
end
describe '#representation_class' do
it { expect(subject.representation_class).to eq(Gitlab::GithubImport::Representation::ReleaseAttachments) }
end
describe '#importer_class' do
it { expect(subject.importer_class).to eq(Gitlab::GithubImport::Importer::ReleaseAttachmentsImporter) }
end
describe '#sidekiq_worker_class' do
it { expect(subject.sidekiq_worker_class).to eq(Gitlab::GithubImport::ImportReleaseAttachmentsWorker) }
end
describe '#collection_method' do
it { expect(subject.collection_method).to eq(:release_attachments) }
end
describe '#object_type' do
it { expect(subject.object_type).to eq(:release_attachment) }
end
describe '#id_for_already_imported_cache' do
let(:release) { build_stubbed(:release) }
it { expect(subject.id_for_already_imported_cache(release)).to eq(release.id) }
end
describe '#object_representation' do
let(:release) { build_stubbed(:release) }
it 'returns release attachments representation' do
representation = subject.object_representation(release)
expect(representation.class).to eq subject.representation_class
expect(representation.release_db_id).to eq release.id
expect(representation.description).to eq release.description
end
end
end

View File

@ -0,0 +1,64 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Representation::NoteText do
shared_examples 'a Note text data' do |match_record_type|
it 'returns an instance of ReleaseAttachments' do
expect(representation).to be_an_instance_of(described_class)
end
it 'includes record DB id' do
expect(representation.record_db_id).to eq 42
end
it 'includes record type' do
expect(representation.record_type).to eq match_record_type
end
it 'includes note text' do
expect(representation.text).to eq 'Some text here..'
end
end
describe '.from_db_record' do
context 'with Release' do
let(:record) { build_stubbed(:release, id: 42, description: 'Some text here..') }
it_behaves_like 'a Note text data', 'Release' do
let(:representation) { described_class.from_db_record(record) }
end
end
context 'with Note' do
let(:record) { build_stubbed(:note, id: 42, note: 'Some text here..') }
it_behaves_like 'a Note text data', 'Note' do
let(:representation) { described_class.from_db_record(record) }
end
end
end
describe '.from_json_hash' do
it_behaves_like 'a Note text data', 'Release' do
let(:hash) do
{
'record_db_id' => 42,
'record_type' => 'Release',
'text' => 'Some text here..'
}
end
let(:representation) { described_class.from_json_hash(hash) }
end
end
describe '#github_identifiers' do
it 'returns a hash with needed identifiers' do
record_id = rand(100)
representation = described_class.new(record_db_id: record_id, text: 'text')
expect(representation.github_identifiers).to eq({ db_id: record_id })
end
end
end

View File

@ -1,49 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Representation::ReleaseAttachments do
shared_examples 'a Release attachments data' do
it 'returns an instance of ReleaseAttachments' do
expect(representation).to be_an_instance_of(described_class)
end
it 'includes release DB id' do
expect(representation.release_db_id).to eq 42
end
it 'includes release description' do
expect(representation.description).to eq 'Some text here..'
end
end
describe '.from_db_record' do
let(:release) { build_stubbed(:release, id: 42, description: 'Some text here..') }
it_behaves_like 'a Release attachments data' do
let(:representation) { described_class.from_db_record(release) }
end
end
describe '.from_json_hash' do
it_behaves_like 'a Release attachments data' do
let(:hash) do
{
'release_db_id' => 42,
'description' => 'Some text here..'
}
end
let(:representation) { described_class.from_json_hash(hash) }
end
end
describe '#github_identifiers' do
it 'returns a hash with needed identifiers' do
release_id = rand(100)
representation = described_class.new(release_db_id: release_id, description: 'text')
expect(representation.github_identifiers).to eq({ db_id: release_id })
end
end
end

View File

@ -258,6 +258,8 @@ RSpec.describe 'Every Sidekiq worker' do
'GitGarbageCollectWorker' => false,
'Gitlab::GithubImport::AdvanceStageWorker' => 3,
'Gitlab::GithubImport::ImportReleaseAttachmentsWorker' => 5,
'Gitlab::GithubImport::Attachments::ImportReleaseWorker' => 5,
'Gitlab::GithubImport::Attachments::ImportNoteWorker' => 5,
'Gitlab::GithubImport::ImportDiffNoteWorker' => 5,
'Gitlab::GithubImport::ImportIssueWorker' => 5,
'Gitlab::GithubImport::ImportIssueEventWorker' => 5,

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Attachments::ImportNoteWorker do
subject(:worker) { described_class.new }
describe '#import' do
let(:import_state) { create(:import_state, :started) }
let(:project) do
instance_double('Project', full_path: 'foo/bar', id: 1, import_state: import_state)
end
let(:client) { instance_double('Gitlab::GithubImport::Client') }
let(:importer) { instance_double('Gitlab::GithubImport::Importer::NoteAttachmentsImporter') }
let(:note_hash) do
{
'record_db_id' => rand(100),
'record_type' => 'Note',
'text' => <<-TEXT
Some text...
![special-image](https://user-images.githubusercontent.com...)
TEXT
}
end
it 'imports an release attachments' do
expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter)
.to receive(:new)
.with(
an_instance_of(Gitlab::GithubImport::Representation::NoteText),
project,
client
)
.and_return(importer)
expect(importer).to receive(:execute)
expect(Gitlab::GithubImport::ObjectCounter)
.to receive(:increment)
.and_call_original
worker.import(project, client, note_hash)
end
end
end

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Attachments::ImportReleaseWorker do
subject(:worker) { described_class.new }
describe '#import' do
let(:import_state) { create(:import_state, :started) }
let(:project) do
instance_double('Project', full_path: 'foo/bar', id: 1, import_state: import_state)
end
let(:client) { instance_double('Gitlab::GithubImport::Client') }
let(:importer) { instance_double('Gitlab::GithubImport::Importer::NoteAttachmentsImporter') }
let(:note_hash) do
{
'record_db_id' => rand(100),
'record_type' => 'Release',
'text' => <<-TEXT
Some text...
![special-image](https://user-images.githubusercontent.com...)
TEXT
}
end
it 'imports an release attachments' do
expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter)
.to receive(:new)
.with(
an_instance_of(Gitlab::GithubImport::Representation::NoteText),
project,
client
)
.and_return(importer)
expect(importer).to receive(:execute)
expect(Gitlab::GithubImport::ObjectCounter)
.to receive(:increment)
.and_call_original
worker.import(project, client, note_hash)
end
end
end

View File

@ -13,7 +13,7 @@ RSpec.describe Gitlab::GithubImport::ImportReleaseAttachmentsWorker do
end
let(:client) { instance_double('Gitlab::GithubImport::Client') }
let(:importer) { instance_double('Gitlab::GithubImport::Importer::ReleaseAttachmentsImporter') }
let(:importer) { instance_double('Gitlab::GithubImport::Importer::NoteAttachmentsImporter') }
let(:release_hash) do
{
@ -27,10 +27,10 @@ RSpec.describe Gitlab::GithubImport::ImportReleaseAttachmentsWorker do
end
it 'imports an issue event' do
expect(Gitlab::GithubImport::Importer::ReleaseAttachmentsImporter)
expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter)
.to receive(:new)
.with(
an_instance_of(Gitlab::GithubImport::Representation::ReleaseAttachments),
an_instance_of(Gitlab::GithubImport::Representation::NoteText),
project,
client
)

View File

@ -10,26 +10,32 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportAttachmentsWorker do
let(:feature_flag_state) { [group] }
describe '#import' do
let(:importer) { instance_double('Gitlab::GithubImport::Importer::ReleasesAttachmentsImporter') }
let(:releases_importer) { instance_double('Gitlab::GithubImport::Importer::Attachments::ReleasesImporter') }
let(:notes_importer) { instance_double('Gitlab::GithubImport::Importer::Attachments::NotesImporter') }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
let(:releases_waiter) { Gitlab::JobWaiter.new(2, '123') }
let(:notes_waiter) { Gitlab::JobWaiter.new(3, '234') }
before do
stub_feature_flags(github_importer_attachments_import: feature_flag_state)
end
it 'imports release attachments' do
waiter = Gitlab::JobWaiter.new(2, '123')
expect(Gitlab::GithubImport::Importer::ReleasesAttachmentsImporter)
expect(Gitlab::GithubImport::Importer::Attachments::ReleasesImporter)
.to receive(:new)
.with(project, client)
.and_return(importer)
.and_return(releases_importer)
expect(releases_importer).to receive(:execute).and_return(releases_waiter)
expect(importer).to receive(:execute).and_return(waiter)
expect(Gitlab::GithubImport::Importer::Attachments::NotesImporter)
.to receive(:new)
.with(project, client)
.and_return(notes_importer)
expect(notes_importer).to receive(:execute).and_return(notes_waiter)
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
.with(project.id, { '123' => 2 }, :protected_branches)
.with(project.id, { '123' => 2, '234' => 3 }, :protected_branches)
worker.import(client, project)
end
@ -38,7 +44,8 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportAttachmentsWorker do
let(:feature_flag_state) { false }
it 'skips release attachments import and calls next stage' do
expect(Gitlab::GithubImport::Importer::ReleasesAttachmentsImporter).not_to receive(:new)
expect(Gitlab::GithubImport::Importer::Attachments::ReleasesImporter).not_to receive(:new)
expect(Gitlab::GithubImport::Importer::Attachments::NotesImporter).not_to receive(:new)
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async).with(project.id, {}, :protected_branches)