Commit 5e38e52a authored by Rémy Coutable's avatar Rémy Coutable

Merge branch...

Merge branch '342723-split-relation-tree-restorer-into-project-and-group-specific-classes' into 'master'

Split relation tree restorer into project and group specific classes

See merge request gitlab-org/gitlab!73180
parents 9013d399 e06142fc
...@@ -2455,7 +2455,7 @@ Database/MultipleDatabases: ...@@ -2455,7 +2455,7 @@ Database/MultipleDatabases:
- 'lib/gitlab/gitlab_import/importer.rb' - 'lib/gitlab/gitlab_import/importer.rb'
- 'lib/gitlab/health_checks/db_check.rb' - 'lib/gitlab/health_checks/db_check.rb'
- 'lib/gitlab/import_export/base/relation_factory.rb' - 'lib/gitlab/import_export/base/relation_factory.rb'
- 'lib/gitlab/import_export/relation_tree_restorer.rb' - 'lib/gitlab/import_export/group/relation_tree_restorer.rb'
- 'lib/gitlab/legacy_github_import/importer.rb' - 'lib/gitlab/legacy_github_import/importer.rb'
- 'lib/gitlab/metrics/samplers/database_sampler.rb' - 'lib/gitlab/metrics/samplers/database_sampler.rb'
- 'lib/gitlab/seeder.rb' - 'lib/gitlab/seeder.rb'
......
# frozen_string_literal: true
module Gitlab
module ImportExport
module Group
class RelationTreeRestorer
def initialize( # rubocop:disable Metrics/ParameterLists
user:,
shared:,
relation_reader:,
members_mapper:,
object_builder:,
relation_factory:,
reader:,
importable:,
importable_attributes:,
importable_path:
)
@user = user
@shared = shared
@importable = importable
@relation_reader = relation_reader
@members_mapper = members_mapper
@object_builder = object_builder
@relation_factory = relation_factory
@reader = reader
@importable_attributes = importable_attributes
@importable_path = importable_path
end
def restore
ActiveRecord::Base.uncached do
ActiveRecord::Base.no_touching do
update_params!
BulkInsertableAssociations.with_bulk_insert(enabled: bulk_insert_enabled) do
fix_ci_pipelines_not_sorted_on_legacy_project_json!
create_relations!
end
end
end
# ensure that we have latest version of the restore
@importable.reload # rubocop:disable Cop/ActiveRecordAssociationReload
true
rescue StandardError => e
@shared.error(e)
false
end
private
def bulk_insert_enabled
false
end
# Loops through the tree of models defined in import_export.yml and
# finds them in the imported JSON so they can be instantiated and saved
# in the DB. The structure and relationships between models are guessed from
# the configuration yaml file too.
# Finally, it updates each attribute in the newly imported project/group.
def create_relations!
relations.each do |relation_key, relation_definition|
process_relation!(relation_key, relation_definition)
end
end
def process_relation!(relation_key, relation_definition)
@relation_reader.consume_relation(@importable_path, relation_key).each do |data_hash, relation_index|
process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
end
end
def process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
relation_object = build_relation(relation_key, relation_definition, relation_index, data_hash)
return unless relation_object
return if relation_invalid_for_importable?(relation_object)
relation_object.assign_attributes(importable_class_sym => @importable)
import_failure_service.with_retry(action: 'relation_object.save!', relation_key: relation_key, relation_index: relation_index) do
relation_object.save!
log_relation_creation(@importable, relation_key, relation_object)
end
rescue StandardError => e
import_failure_service.log_import_failure(
source: 'process_relation_item!',
relation_key: relation_key,
relation_index: relation_index,
exception: e)
end
def import_failure_service
@import_failure_service ||= ImportFailureService.new(@importable)
end
def relations
@relations ||=
@reader
.attributes_finder
.find_relations_tree(importable_class_sym)
.deep_stringify_keys
end
def update_params!
params = @importable_attributes.except(*relations.keys.map(&:to_s))
params = params.merge(present_override_params)
# Cleaning all imported and overridden params
params = Gitlab::ImportExport::AttributeCleaner.clean(
relation_hash: params,
relation_class: importable_class,
excluded_keys: excluded_keys_for_relation(importable_class_sym))
@importable.assign_attributes(params)
modify_attributes
Gitlab::Timeless.timeless(@importable) do
@importable.save!
end
end
def present_override_params
# we filter out the empty strings from the overrides
# keeping the default values configured
override_params&.transform_values do |value|
value.is_a?(String) ? value.presence : value
end&.compact
end
def override_params
@importable_override_params ||= importable_override_params
end
def importable_override_params
if @importable.respond_to?(:import_data)
@importable.import_data&.data&.fetch('override_params', nil) || {}
else
{}
end
end
def modify_attributes
# no-op to be overridden on inheritance
end
def build_relations(relation_key, relation_definition, relation_index, data_hashes)
data_hashes
.map { |data_hash| build_relation(relation_key, relation_definition, relation_index, data_hash) }
.tap { |entries| entries.compact! }
end
def build_relation(relation_key, relation_definition, relation_index, data_hash)
# TODO: This is hack to not create relation for the author
# Rather make `RelationFactory#set_note_author` to take care of that
return data_hash if relation_key == 'author' || already_restored?(data_hash)
# create relation objects recursively for all sub-objects
relation_definition.each do |sub_relation_key, sub_relation_definition|
transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition, relation_index)
end
relation = @relation_factory.create(**relation_factory_params(relation_key, relation_index, data_hash))
if relation && !relation.valid?
@shared.logger.warn(
message: "[Project/Group Import] Invalid object relation built",
relation_key: relation_key,
relation_index: relation_index,
relation_class: relation.class.name,
error_messages: relation.errors.full_messages.join(". ")
)
end
relation
end
# Since we update the data hash in place as we restore relation items,
# and since we also de-duplicate items, we might encounter items that
# have already been restored in a previous iteration.
def already_restored?(relation_item)
!relation_item.is_a?(Hash)
end
def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition, relation_index)
sub_data_hash = data_hash[sub_relation_key]
return unless sub_data_hash
# if object is a hash we can create simple object
# as it means that this is 1-to-1 vs 1-to-many
current_item =
if sub_data_hash.is_a?(Array)
build_relations(
sub_relation_key,
sub_relation_definition,
relation_index,
sub_data_hash).presence
else
build_relation(
sub_relation_key,
sub_relation_definition,
relation_index,
sub_data_hash)
end
if current_item
data_hash[sub_relation_key] = current_item
else
data_hash.delete(sub_relation_key)
end
end
def relation_invalid_for_importable?(_relation_object)
false
end
def excluded_keys_for_relation(relation)
@reader.attributes_finder.find_excluded_keys(relation)
end
def importable_class
@importable.class
end
def importable_class_sym
importable_class.to_s.downcase.to_sym
end
def relation_factory_params(relation_key, relation_index, data_hash)
{
relation_index: relation_index,
relation_sym: relation_key.to_sym,
relation_hash: data_hash,
importable: @importable,
members_mapper: @members_mapper,
object_builder: @object_builder,
user: @user,
excluded_keys: excluded_keys_for_relation(relation_key)
}
end
# Temporary fix for https://gitlab.com/gitlab-org/gitlab/-/issues/27883 when import from legacy project.json
# This should be removed once legacy JSON format is deprecated.
# Ndjson export file will fix the order during project export.
def fix_ci_pipelines_not_sorted_on_legacy_project_json!
return unless @relation_reader.legacy?
@relation_reader.sort_ci_pipelines_by_id
end
# Enable logging of each top-level relation creation when Importing
# into a Group if feature flag is enabled
def log_relation_creation(importable, relation_key, relation_object)
root_ancestor_group = importable.try(:root_ancestor)
return unless root_ancestor_group
return unless root_ancestor_group.instance_of?(::Group)
return unless Feature.enabled?(:log_import_export_relation_creation, root_ancestor_group)
@shared.logger.info(
importable_type: importable.class.to_s,
importable_id: importable.id,
relation_key: relation_key,
relation_id: relation_object.id,
author_id: relation_object.try(:author_id),
message: '[Project/Group Import] Created new object relation'
)
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
module Project
class RelationTreeRestorer < ImportExport::Group::RelationTreeRestorer
# Relations which cannot be saved at project level (and have a group assigned)
GROUP_MODELS = [GroupLabel, Milestone, Epic].freeze
private
def bulk_insert_enabled
true
end
def modify_attributes
@importable.reconcile_shared_runners_setting!
@importable.drop_visibility_level!
end
def relation_invalid_for_importable?(relation_object)
GROUP_MODELS.include?(relation_object.class) && relation_object.group_id
end
end
end
end
end
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
module ImportExport module ImportExport
module Project module Project
module Sample module Sample
class RelationTreeRestorer < ImportExport::RelationTreeRestorer class RelationTreeRestorer < ImportExport::Project::RelationTreeRestorer
def initialize(...) def initialize(...)
super(...) super(...)
...@@ -18,10 +18,10 @@ module Gitlab ...@@ -18,10 +18,10 @@ module Gitlab
end end
def dates def dates
return [] if relation_reader.legacy? return [] if @relation_reader.legacy?
RelationFactory::DATE_MODELS.flat_map do |tag| RelationFactory::DATE_MODELS.flat_map do |tag|
relation_reader.consume_relation(@importable_path, tag, mark_as_consumed: false).map do |model| @relation_reader.consume_relation(@importable_path, tag, mark_as_consumed: false).map do |model|
model.first['due_date'] model.first['due_date']
end end
end end
......
# frozen_string_literal: true
module Gitlab
module ImportExport
class RelationTreeRestorer
# Relations which cannot be saved at project level (and have a group assigned)
GROUP_MODELS = [GroupLabel, Milestone, Epic].freeze
attr_reader :user
attr_reader :shared
attr_reader :importable
attr_reader :relation_reader
def initialize( # rubocop:disable Metrics/ParameterLists
user:, shared:, relation_reader:,
members_mapper:, object_builder:,
relation_factory:,
reader:,
importable:,
importable_attributes:,
importable_path:
)
@user = user
@shared = shared
@importable = importable
@relation_reader = relation_reader
@members_mapper = members_mapper
@object_builder = object_builder
@relation_factory = relation_factory
@reader = reader
@importable_attributes = importable_attributes
@importable_path = importable_path
end
def restore
ActiveRecord::Base.uncached do
ActiveRecord::Base.no_touching do
update_params!
BulkInsertableAssociations.with_bulk_insert(enabled: project?) do
fix_ci_pipelines_not_sorted_on_legacy_project_json!
create_relations!
end
end
end
# ensure that we have latest version of the restore
@importable.reload # rubocop:disable Cop/ActiveRecordAssociationReload
true
rescue StandardError => e
@shared.error(e)
false
end
private
def project?
@importable.instance_of?(::Project)
end
# Loops through the tree of models defined in import_export.yml and
# finds them in the imported JSON so they can be instantiated and saved
# in the DB. The structure and relationships between models are guessed from
# the configuration yaml file too.
# Finally, it updates each attribute in the newly imported project/group.
def create_relations!
relations.each do |relation_key, relation_definition|
process_relation!(relation_key, relation_definition)
end
end
def process_relation!(relation_key, relation_definition)
@relation_reader.consume_relation(@importable_path, relation_key).each do |data_hash, relation_index|
process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
end
end
def process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
relation_object = build_relation(relation_key, relation_definition, relation_index, data_hash)
return unless relation_object
return if project? && group_model?(relation_object)
relation_object.assign_attributes(importable_class_sym => @importable)
import_failure_service.with_retry(action: 'relation_object.save!', relation_key: relation_key, relation_index: relation_index) do
relation_object.save!
log_relation_creation(@importable, relation_key, relation_object)
end
rescue StandardError => e
import_failure_service.log_import_failure(
source: 'process_relation_item!',
relation_key: relation_key,
relation_index: relation_index,
exception: e)
end
def import_failure_service
@import_failure_service ||= ImportFailureService.new(@importable)
end
def relations
@relations ||=
@reader
.attributes_finder
.find_relations_tree(importable_class_sym)
.deep_stringify_keys
end
def update_params!
params = @importable_attributes.except(*relations.keys.map(&:to_s))
params = params.merge(present_override_params)
# Cleaning all imported and overridden params
params = Gitlab::ImportExport::AttributeCleaner.clean(
relation_hash: params,
relation_class: importable_class,
excluded_keys: excluded_keys_for_relation(importable_class_sym))
@importable.assign_attributes(params)
modify_attributes
Gitlab::Timeless.timeless(@importable) do
@importable.save!
end
end
def present_override_params
# we filter out the empty strings from the overrides
# keeping the default values configured
override_params&.transform_values do |value|
value.is_a?(String) ? value.presence : value
end&.compact
end
def override_params
@importable_override_params ||= importable_override_params
end
def importable_override_params
if @importable.respond_to?(:import_data)
@importable.import_data&.data&.fetch('override_params', nil) || {}
else
{}
end
end
def modify_attributes
return unless project?
@importable.reconcile_shared_runners_setting!
@importable.drop_visibility_level!
end
def build_relations(relation_key, relation_definition, relation_index, data_hashes)
data_hashes
.map { |data_hash| build_relation(relation_key, relation_definition, relation_index, data_hash) }
.tap { |entries| entries.compact! }
end
def build_relation(relation_key, relation_definition, relation_index, data_hash)
# TODO: This is hack to not create relation for the author
# Rather make `RelationFactory#set_note_author` to take care of that
return data_hash if relation_key == 'author' || already_restored?(data_hash)
# create relation objects recursively for all sub-objects
relation_definition.each do |sub_relation_key, sub_relation_definition|
transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition, relation_index)
end
relation = @relation_factory.create(**relation_factory_params(relation_key, relation_index, data_hash))
if relation && !relation.valid?
@shared.logger.warn(
message: "[Project/Group Import] Invalid object relation built",
relation_key: relation_key,
relation_index: relation_index,
relation_class: relation.class.name,
error_messages: relation.errors.full_messages.join(". ")
)
end
relation
end
# Since we update the data hash in place as we restore relation items,
# and since we also de-duplicate items, we might encounter items that
# have already been restored in a previous iteration.
def already_restored?(relation_item)
!relation_item.is_a?(Hash)
end
def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition, relation_index)
sub_data_hash = data_hash[sub_relation_key]
return unless sub_data_hash
# if object is a hash we can create simple object
# as it means that this is 1-to-1 vs 1-to-many
current_item =
if sub_data_hash.is_a?(Array)
build_relations(
sub_relation_key,
sub_relation_definition,
relation_index,
sub_data_hash).presence
else
build_relation(
sub_relation_key,
sub_relation_definition,
relation_index,
sub_data_hash)
end
if current_item
data_hash[sub_relation_key] = current_item
else
data_hash.delete(sub_relation_key)
end
end
def group_model?(relation_object)
GROUP_MODELS.include?(relation_object.class) && relation_object.group_id
end
def excluded_keys_for_relation(relation)
@reader.attributes_finder.find_excluded_keys(relation)
end
def importable_class
@importable.class
end
def importable_class_sym
importable_class.to_s.downcase.to_sym
end
def relation_factory_params(relation_key, relation_index, data_hash)
{
relation_index: relation_index,
relation_sym: relation_key.to_sym,
relation_hash: data_hash,
importable: @importable,
members_mapper: @members_mapper,
object_builder: @object_builder,
user: @user,
excluded_keys: excluded_keys_for_relation(relation_key)
}
end
# Temporary fix for https://gitlab.com/gitlab-org/gitlab/-/issues/27883 when import from legacy project.json
# This should be removed once legacy JSON format is deprecated.
# Ndjson export file will fix the order during project export.
def fix_ci_pipelines_not_sorted_on_legacy_project_json!
return unless relation_reader.legacy?
relation_reader.sort_ci_pipelines_by_id
end
# Enable logging of each top-level relation creation when Importing
# into a Group if feature flag is enabled
def log_relation_creation(importable, relation_key, relation_object)
root_ancestor_group = importable.try(:root_ancestor)
return unless root_ancestor_group
return unless root_ancestor_group.instance_of?(::Group)
return unless Feature.enabled?(:log_import_export_relation_creation, root_ancestor_group)
@shared.logger.info(
importable_type: importable.class.to_s,
importable_id: importable.id,
relation_key: relation_key,
relation_id: relation_object.id,
author_id: relation_object.try(:author_id),
message: '[Project/Group Import] Created new object relation'
)
end
end
end
end
# frozen_string_literal: true
# This spec is a lightweight version of:
# * project/tree_restorer_spec.rb
#
# In depth testing is being done in the above specs.
# This spec tests that restore project works
# but does not have 100% relation coverage.
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer do
let_it_be(:group) { create(:group) }
let_it_be(:importable) { create(:group, parent: group) }
include_context 'relation tree restorer shared context' do
let(:importable_name) { nil }
end
let(:path) { 'spec/fixtures/lib/gitlab/import_export/group_exports/no_children/group.json' }
let(:relation_reader) do
Gitlab::ImportExport::Json::LegacyReader::File.new(
path,
relation_names: reader.group_relation_names)
end
let(:reader) do
Gitlab::ImportExport::Reader.new(
shared: shared,
config: Gitlab::ImportExport::Config.new(config: Gitlab::ImportExport.legacy_group_config_file).to_h
)
end
let(:relation_tree_restorer) do
described_class.new(
user: user,
shared: shared,
relation_reader: relation_reader,
object_builder: Gitlab::ImportExport::Group::ObjectBuilder,
members_mapper: members_mapper,
relation_factory: Gitlab::ImportExport::Group::RelationFactory,
reader: reader,
importable: importable,
importable_path: nil,
importable_attributes: attributes
)
end
subject { relation_tree_restorer.restore }
shared_examples 'logging of relations creation' do
context 'when log_import_export_relation_creation feature flag is enabled' do
before do
stub_feature_flags(log_import_export_relation_creation: group)
end
it 'logs top-level relation creation' do
expect(shared.logger)
.to receive(:info)
.with(hash_including(message: '[Project/Group Import] Created new object relation'))
.at_least(:once)
subject
end
end
context 'when log_import_export_relation_creation feature flag is disabled' do
before do
stub_feature_flags(log_import_export_relation_creation: false)
end
it 'does not log top-level relation creation' do
expect(shared.logger)
.to receive(:info)
.with(hash_including(message: '[Project/Group Import] Created new object relation'))
.never
subject
end
end
end
it 'restores group tree' do
expect(subject).to eq(true)
end
include_examples 'logging of relations creation'
end
...@@ -9,20 +9,27 @@ ...@@ -9,20 +9,27 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer do
include_context 'relation tree restorer shared context' let_it_be(:importable, reload: true) do
create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
end
include_context 'relation tree restorer shared context' do
let(:importable_name) { 'project' }
end
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
let(:relation_tree_restorer) do let(:relation_tree_restorer) do
described_class.new( described_class.new(
user: user, user: user,
shared: shared, shared: shared,
relation_reader: relation_reader, relation_reader: relation_reader,
object_builder: object_builder, object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
members_mapper: members_mapper, members_mapper: members_mapper,
relation_factory: relation_factory, relation_factory: Gitlab::ImportExport::Project::RelationFactory,
reader: reader, reader: reader,
importable: importable, importable: importable,
importable_path: importable_path, importable_path: 'project',
importable_attributes: attributes importable_attributes: attributes
) )
end end
...@@ -54,7 +61,7 @@ RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do ...@@ -54,7 +61,7 @@ RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do
end end
it 'logs top-level relation creation' do it 'logs top-level relation creation' do
expect(relation_tree_restorer.shared.logger) expect(shared.logger)
.to receive(:info) .to receive(:info)
.with(hash_including(message: '[Project/Group Import] Created new object relation')) .with(hash_including(message: '[Project/Group Import] Created new object relation'))
.at_least(:once) .at_least(:once)
...@@ -69,7 +76,7 @@ RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do ...@@ -69,7 +76,7 @@ RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do
end end
it 'does not log top-level relation creation' do it 'does not log top-level relation creation' do
expect(relation_tree_restorer.shared.logger) expect(shared.logger)
.to receive(:info) .to receive(:info)
.with(hash_including(message: '[Project/Group Import] Created new object relation')) .with(hash_including(message: '[Project/Group Import] Created new object relation'))
.never .never
...@@ -79,106 +86,65 @@ RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do ...@@ -79,106 +86,65 @@ RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do
end end
end end
context 'when restoring a project' do context 'with legacy reader' do
let_it_be(:importable, reload: true) do let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') let(:relation_reader) do
Gitlab::ImportExport::Json::LegacyReader::File.new(
path,
relation_names: reader.project_relation_names,
allowed_path: 'project'
)
end end
let(:importable_name) { 'project' } let(:attributes) { relation_reader.consume_attributes('project') }
let(:importable_path) { 'project' }
let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder }
let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory }
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
context 'using legacy reader' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
let(:relation_reader) do
Gitlab::ImportExport::Json::LegacyReader::File.new(
path,
relation_names: reader.project_relation_names,
allowed_path: 'project'
)
end
let(:attributes) { relation_reader.consume_attributes('project') }
it_behaves_like 'import project successfully'
context 'logging of relations creation' do it_behaves_like 'import project successfully'
let_it_be(:group) { create(:group) }
let_it_be(:importable) do
create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project', group: group)
end
include_examples 'logging of relations creation' context 'with logging of relations creation' do
let_it_be(:group) { create(:group) }
let_it_be(:importable) do
create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project', group: group)
end end
end
context 'using ndjson reader' do include_examples 'logging of relations creation'
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/tree' } end
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) } end
it_behaves_like 'import project successfully'
context 'when inside a group' do context 'with ndjson reader' do
let_it_be(:group) do let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/tree' }
create(:group, :disabled_and_unoverridable) let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
end
before do it_behaves_like 'import project successfully'
importable.update!(shared_runners_enabled: false, group: group)
end
it_behaves_like 'import project successfully' context 'when inside a group' do
let_it_be(:group) do
create(:group, :disabled_and_unoverridable)
end end
end
context 'with invalid relations' do before do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/project_with_invalid_relations/tree' } importable.update!(shared_runners_enabled: false, group: group)
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
it 'logs the invalid relation and its errors' do
expect(relation_tree_restorer.shared.logger)
.to receive(:warn)
.with(
error_messages: "Title can't be blank. Title is invalid",
message: '[Project/Group Import] Invalid object relation built',
relation_class: 'ProjectLabel',
relation_index: 0,
relation_key: 'labels'
).once
relation_tree_restorer.restore
end end
end
end
context 'when restoring a group' do
let_it_be(:group) { create(:group) }
let_it_be(:importable) { create(:group, parent: group) }
let(:path) { 'spec/fixtures/lib/gitlab/import_export/group_exports/no_children/group.json' }
let(:importable_name) { nil }
let(:importable_path) { nil }
let(:object_builder) { Gitlab::ImportExport::Group::ObjectBuilder }
let(:relation_factory) { Gitlab::ImportExport::Group::RelationFactory }
let(:relation_reader) do
Gitlab::ImportExport::Json::LegacyReader::File.new(
path,
relation_names: reader.group_relation_names)
end
let(:reader) do it_behaves_like 'import project successfully'
Gitlab::ImportExport::Reader.new(
shared: shared,
config: Gitlab::ImportExport::Config.new(config: Gitlab::ImportExport.legacy_group_config_file).to_h
)
end end
end
it 'restores group tree' do context 'with invalid relations' do
expect(subject).to eq(true) let(:path) { 'spec/fixtures/lib/gitlab/import_export/project_with_invalid_relations/tree' }
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
it 'logs the invalid relation and its errors' do
expect(shared.logger)
.to receive(:warn)
.with(
error_messages: "Title can't be blank. Title is invalid",
message: '[Project/Group Import] Invalid object relation built',
relation_class: 'ProjectLabel',
relation_index: 0,
relation_key: 'labels'
).once
relation_tree_restorer.restore
end end
include_examples 'logging of relations creation'
end end
end end
...@@ -10,19 +10,26 @@ ...@@ -10,19 +10,26 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::ImportExport::Project::Sample::RelationTreeRestorer do RSpec.describe Gitlab::ImportExport::Project::Sample::RelationTreeRestorer do
include_context 'relation tree restorer shared context' let_it_be(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
include_context 'relation tree restorer shared context' do
let(:importable_name) { 'project' }
end
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
let(:path) { 'spec/fixtures/lib/gitlab/import_export/sample_data/tree' }
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
let(:sample_data_relation_tree_restorer) do let(:sample_data_relation_tree_restorer) do
described_class.new( described_class.new(
user: user, user: user,
shared: shared, shared: shared,
relation_reader: relation_reader, relation_reader: relation_reader,
object_builder: object_builder, object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
members_mapper: members_mapper, members_mapper: members_mapper,
relation_factory: relation_factory, relation_factory: Gitlab::ImportExport::Project::Sample::RelationFactory,
reader: reader, reader: reader,
importable: importable, importable: importable,
importable_path: importable_path, importable_path: 'project',
importable_attributes: attributes importable_attributes: attributes
) )
end end
...@@ -69,32 +76,21 @@ RSpec.describe Gitlab::ImportExport::Project::Sample::RelationTreeRestorer do ...@@ -69,32 +76,21 @@ RSpec.describe Gitlab::ImportExport::Project::Sample::RelationTreeRestorer do
end end
end end
context 'when restoring a project' do it 'initializes relation_factory with date_calculator as parameter' do
let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') } expect(Gitlab::ImportExport::Project::Sample::RelationFactory).to receive(:create).with(hash_including(:date_calculator)).at_least(:once).times
let(:importable_name) { 'project' }
let(:importable_path) { 'project' }
let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder }
let(:relation_factory) { Gitlab::ImportExport::Project::Sample::RelationFactory }
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
let(:path) { 'spec/fixtures/lib/gitlab/import_export/sample_data/tree' }
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
it 'initializes relation_factory with date_calculator as parameter' do
expect(Gitlab::ImportExport::Project::Sample::RelationFactory).to receive(:create).with(hash_including(:date_calculator)).at_least(:once).times
subject subject
end end
context 'when relation tree restorer is initialized' do context 'when relation tree restorer is initialized' do
it 'initializes date calculator with due dates' do it 'initializes date calculator with due dates' do
expect(Gitlab::ImportExport::Project::Sample::DateCalculator).to receive(:new).with(Array) expect(Gitlab::ImportExport::Project::Sample::DateCalculator).to receive(:new).with(Array)
sample_data_relation_tree_restorer sample_data_relation_tree_restorer
end
end end
end
context 'using ndjson reader' do context 'using ndjson reader' do
it_behaves_like 'import project successfully' it_behaves_like 'import project successfully'
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment