diff options
Diffstat (limited to 'spec/lib/bulk_imports')
15 files changed, 997 insertions, 0 deletions
diff --git a/spec/lib/bulk_imports/clients/http_spec.rb b/spec/lib/bulk_imports/clients/http_spec.rb new file mode 100644 index 00000000000..2d841b7fac2 --- /dev/null +++ b/spec/lib/bulk_imports/clients/http_spec.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Clients::Http do + include ImportSpecHelper + + let(:uri) { 'http://gitlab.example' } + let(:token) { 'token' } + let(:resource) { 'resource' } + + subject { described_class.new(uri: uri, token: token) } + + describe '#get' do + let(:response_double) { double(code: 200, success?: true, parsed_response: {}) } + + shared_examples 'performs network request' do + it 'performs network request' do + expect(Gitlab::HTTP).to receive(:get).with(*expected_args).and_return(response_double) + + subject.get(resource) + end + end + + describe 'request query' do + include_examples 'performs network request' do + let(:expected_args) do + [ + anything, + hash_including( + query: { + page: described_class::DEFAULT_PAGE, + per_page: described_class::DEFAULT_PER_PAGE + } + ) + ] + end + end + end + + describe 'request headers' do + include_examples 'performs network request' do + let(:expected_args) do + [ + anything, + hash_including( + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer #{token}" + } + ) + ] + end + end + end + + describe 'request uri' do + include_examples 'performs network request' do + let(:expected_args) do + ['http://gitlab.example:80/api/v4/resource', anything] + end + end + end + + context 'error handling' do + context 'when error occurred' do + it 'raises ConnectionError' do + allow(Gitlab::HTTP).to receive(:get).and_raise(Errno::ECONNREFUSED) + + expect { subject.get(resource) }.to raise_exception(described_class::ConnectionError) + end + end + + context 'when response is not success' do + it 'raises ConnectionError' do + response_double = double(code: 503, success?: false) + + allow(Gitlab::HTTP).to receive(:get).and_return(response_double) + + expect { subject.get(resource) }.to raise_exception(described_class::ConnectionError) + end + end + end + + describe '#each_page' do + let(:objects1) { [{ object: 1 }, { object: 2 }] } + let(:objects2) { [{ object: 3 }, { object: 4 }] } + let(:response1) { double(success?: true, headers: { 'x-next-page' => 2 }, parsed_response: objects1) } + let(:response2) { double(success?: true, headers: {}, parsed_response: objects2) } + + before do + stub_http_get('groups', { page: 1, per_page: 30 }, response1) + stub_http_get('groups', { page: 2, per_page: 30 }, response2) + end + + context 'with a block' do + it 'yields every retrieved page to the supplied block' do + pages = [] + + subject.each_page(:get, 'groups') { |page| pages << page } + + expect(pages[0]).to be_an_instance_of(Array) + expect(pages[1]).to be_an_instance_of(Array) + + expect(pages[0]).to eq(objects1) + expect(pages[1]).to eq(objects2) + end + end + + context 'without a block' do + it 'returns an Enumerator' do + expect(subject.each_page(:get, :foo)).to be_an_instance_of(Enumerator) + end + end + + private + + def stub_http_get(path, query, response) + uri = "http://gitlab.example:80/api/v4/#{path}" + params = { + follow_redirects: false, + headers: { + "Authorization" => "Bearer token", + "Content-Type" => "application/json" + } + }.merge(query: query) + + allow(Gitlab::HTTP).to receive(:get).with(uri, params).and_return(response) + end + end + end +end diff --git a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb new file mode 100644 index 00000000000..cde8e2d5c18 --- /dev/null +++ b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Common::Extractors::GraphqlExtractor do + let(:graphql_client) { instance_double(BulkImports::Clients::Graphql) } + let(:import_entity) { create(:bulk_import_entity) } + let(:response) { double(original_hash: { foo: :bar }) } + let(:query) { { query: double(to_s: 'test', variables: {}) } } + let(:context) do + instance_double( + BulkImports::Pipeline::Context, + entity: import_entity + ) + end + + subject { described_class.new(query) } + + before do + allow(subject).to receive(:graphql_client).and_return(graphql_client) + allow(graphql_client).to receive(:parse) + end + + describe '#extract' do + before do + allow(subject).to receive(:query_variables).and_return({}) + allow(graphql_client).to receive(:execute).and_return(response) + end + + it 'returns an enumerator with fetched results' do + response = subject.extract(context) + + expect(response).to be_instance_of(Enumerator) + expect(response.first).to eq({ foo: :bar }) + end + end + + describe 'query variables' do + before do + allow(graphql_client).to receive(:execute).and_return(response) + end + + context 'when variables are present' do + let(:query) { { query: double(to_s: 'test', variables: { full_path: :source_full_path }) } } + + it 'builds graphql query variables for import entity' do + expected_variables = { full_path: import_entity.source_full_path } + + expect(graphql_client).to receive(:execute).with(anything, expected_variables) + + subject.extract(context).first + end + end + + context 'when no variables are present' do + let(:query) { { query: double(to_s: 'test', variables: nil) } } + + it 'returns empty hash' do + expect(graphql_client).to receive(:execute).with(anything, nil) + + subject.extract(context).first + end + end + + context 'when variables are empty hash' do + let(:query) { { query: double(to_s: 'test', variables: {}) } } + + it 'makes graphql request with empty hash' do + expect(graphql_client).to receive(:execute).with(anything, {}) + + subject.extract(context).first + end + end + end +end diff --git a/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb b/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb new file mode 100644 index 00000000000..4de7d95172f --- /dev/null +++ b/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Common::Loaders::EntityLoader do + describe '#load' do + it "creates entities for the given data" do + group = create(:group, path: "imported-group") + parent_entity = create(:bulk_import_entity, group: group, bulk_import: create(:bulk_import)) + context = instance_double(BulkImports::Pipeline::Context, entity: parent_entity) + + data = { + source_type: :group_entity, + source_full_path: "parent/subgroup", + destination_name: "subgroup", + destination_namespace: parent_entity.group.full_path, + parent_id: parent_entity.id + } + + expect { subject.load(context, data) }.to change(BulkImports::Entity, :count).by(1) + + subgroup_entity = BulkImports::Entity.last + + expect(subgroup_entity.source_full_path).to eq 'parent/subgroup' + expect(subgroup_entity.destination_namespace).to eq 'imported-group' + expect(subgroup_entity.destination_name).to eq 'subgroup' + expect(subgroup_entity.parent_id).to eq parent_entity.id + end + end +end diff --git a/spec/lib/bulk_imports/common/transformers/graphql_cleaner_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/graphql_cleaner_transformer_spec.rb new file mode 100644 index 00000000000..8f39b6e7c93 --- /dev/null +++ b/spec/lib/bulk_imports/common/transformers/graphql_cleaner_transformer_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Common::Transformers::GraphqlCleanerTransformer do + describe '#transform' do + let_it_be(:expected_output) do + { + 'name' => 'test', + 'fullName' => 'test', + 'description' => 'test', + 'labels' => [ + { 'title' => 'label1' }, + { 'title' => 'label2' }, + { 'title' => 'label3' } + ] + } + end + + it 'deep cleans hash from GraphQL keys' do + data = { + 'data' => { + 'group' => { + 'name' => 'test', + 'fullName' => 'test', + 'description' => 'test', + 'labels' => { + 'edges' => [ + { 'node' => { 'title' => 'label1' } }, + { 'node' => { 'title' => 'label2' } }, + { 'node' => { 'title' => 'label3' } } + ] + } + } + } + } + + transformed_data = described_class.new.transform(nil, data) + + expect(transformed_data).to eq(expected_output) + end + + context 'when data does not have data/group nesting' do + it 'deep cleans hash from GraphQL keys' do + data = { + 'name' => 'test', + 'fullName' => 'test', + 'description' => 'test', + 'labels' => { + 'edges' => [ + { 'node' => { 'title' => 'label1' } }, + { 'node' => { 'title' => 'label2' } }, + { 'node' => { 'title' => 'label3' } } + ] + } + } + + transformed_data = described_class.new.transform(nil, data) + + expect(transformed_data).to eq(expected_output) + end + end + + context 'when data is not a hash' do + it 'does not perform transformation' do + data = 'test' + + transformed_data = described_class.new.transform(nil, data) + + expect(transformed_data).to eq(data) + end + end + + context 'when nested data is not an array or hash' do + it 'only removes top level data/group keys' do + data = { + 'data' => { + 'group' => 'test' + } + } + + transformed_data = described_class.new.transform(nil, data) + + expect(transformed_data).to eq('test') + end + end + end +end diff --git a/spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb new file mode 100644 index 00000000000..cdffa750694 --- /dev/null +++ b/spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Common::Transformers::UnderscorifyKeysTransformer do + describe '#transform' do + it 'deep underscorifies hash keys' do + data = { + 'fullPath' => 'Foo', + 'snakeKeys' => { + 'snakeCaseKey' => 'Bar', + 'moreKeys' => { + 'anotherSnakeCaseKey' => 'Test' + } + } + } + + transformed_data = described_class.new.transform(nil, data) + + expect(transformed_data).to have_key('full_path') + expect(transformed_data).to have_key('snake_keys') + expect(transformed_data['snake_keys']).to have_key('snake_case_key') + expect(transformed_data['snake_keys']).to have_key('more_keys') + expect(transformed_data.dig('snake_keys', 'more_keys')).to have_key('another_snake_case_key') + end + end +end diff --git a/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb new file mode 100644 index 00000000000..b14dfc615a9 --- /dev/null +++ b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Loaders::GroupLoader do + describe '#load' do + let(:user) { create(:user) } + let(:data) { { foo: :bar } } + let(:service_double) { instance_double(::Groups::CreateService) } + let(:entity) { create(:bulk_import_entity) } + let(:context) do + instance_double( + BulkImports::Pipeline::Context, + entity: entity, + current_user: user + ) + end + + subject { described_class.new } + + context 'when user can create group' do + shared_examples 'calls Group Create Service to create a new group' do + it 'calls Group Create Service to create a new group' do + expect(::Groups::CreateService).to receive(:new).with(context.current_user, data).and_return(service_double) + expect(service_double).to receive(:execute) + expect(entity).to receive(:update!) + + subject.load(context, data) + end + end + + context 'when there is no parent group' do + before do + allow(Ability).to receive(:allowed?).with(user, :create_group).and_return(true) + end + + include_examples 'calls Group Create Service to create a new group' + end + + context 'when there is parent group' do + let(:parent) { create(:group) } + let(:data) { { 'parent_id' => parent.id } } + + before do + allow(Ability).to receive(:allowed?).with(user, :create_subgroup, parent).and_return(true) + end + + include_examples 'calls Group Create Service to create a new group' + end + end + + context 'when user cannot create group' do + shared_examples 'does not create new group' do + it 'does not create new group' do + expect(::Groups::CreateService).not_to receive(:new) + + subject.load(context, data) + end + end + + context 'when there is no parent group' do + before do + allow(Ability).to receive(:allowed?).with(user, :create_group).and_return(false) + end + + include_examples 'does not create new group' + end + + context 'when there is parent group' do + let(:parent) { create(:group) } + let(:data) { { 'parent_id' => parent.id } } + + before do + allow(Ability).to receive(:allowed?).with(user, :create_subgroup, parent).and_return(false) + end + + include_examples 'does not create new group' + end + end + end +end diff --git a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb new file mode 100644 index 00000000000..3949dd23b49 --- /dev/null +++ b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do + describe '#run' do + let(:user) { create(:user) } + let(:parent) { create(:group) } + let(:entity) do + create( + :bulk_import_entity, + source_full_path: 'source/full/path', + destination_name: 'My Destination Group', + destination_namespace: parent.full_path + ) + end + + let(:context) do + BulkImports::Pipeline::Context.new( + current_user: user, + entity: entity + ) + end + + let(:group_data) do + { + 'data' => { + 'group' => { + 'name' => 'source_name', + 'fullPath' => 'source/full/path', + 'visibility' => 'private', + 'projectCreationLevel' => 'developer', + 'subgroupCreationLevel' => 'maintainer', + 'description' => 'Group Description', + 'emailsDisabled' => true, + 'lfsEnabled' => false, + 'mentionsDisabled' => true + } + } + } + end + + subject { described_class.new } + + before do + allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| + allow(extractor).to receive(:extract).and_return([group_data]) + end + + parent.add_owner(user) + end + + it 'imports new group into destination group' do + group_path = 'my-destination-group' + + subject.run(context) + + imported_group = Group.find_by_path(group_path) + + expect(imported_group).not_to be_nil + expect(imported_group.parent).to eq(parent) + expect(imported_group.path).to eq(group_path) + expect(imported_group.description).to eq(group_data.dig('data', 'group', 'description')) + expect(imported_group.visibility).to eq(group_data.dig('data', 'group', 'visibility')) + expect(imported_group.project_creation_level).to eq(Gitlab::Access.project_creation_string_options[group_data.dig('data', 'group', 'projectCreationLevel')]) + expect(imported_group.subgroup_creation_level).to eq(Gitlab::Access.subgroup_creation_string_options[group_data.dig('data', 'group', 'subgroupCreationLevel')]) + expect(imported_group.lfs_enabled?).to eq(group_data.dig('data', 'group', 'lfsEnabled')) + expect(imported_group.emails_disabled?).to eq(group_data.dig('data', 'group', 'emailsDisabled')) + expect(imported_group.mentions_disabled?).to eq(group_data.dig('data', 'group', 'mentionsDisabled')) + end + end + + describe 'pipeline parts' do + it { expect(described_class).to include_module(BulkImports::Pipeline) } + it { expect(described_class).to include_module(BulkImports::Pipeline::Attributes) } + it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) } + + it 'has extractors' do + expect(described_class.extractors) + .to contain_exactly( + { + klass: BulkImports::Common::Extractors::GraphqlExtractor, + options: { + query: BulkImports::Groups::Graphql::GetGroupQuery + } + } + ) + end + + it 'has transformers' do + expect(described_class.transformers) + .to contain_exactly( + { klass: BulkImports::Common::Transformers::GraphqlCleanerTransformer, options: nil }, + { klass: BulkImports::Common::Transformers::UnderscorifyKeysTransformer, options: nil }, + { klass: BulkImports::Groups::Transformers::GroupAttributesTransformer, options: nil }) + end + + it 'has loaders' do + expect(described_class.loaders).to contain_exactly({ klass: BulkImports::Groups::Loaders::GroupLoader, options: nil }) + end + end +end diff --git a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb new file mode 100644 index 00000000000..60a4a796682 --- /dev/null +++ b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do + describe '#run' do + let_it_be(:user) { create(:user) } + let(:parent) { create(:group, name: 'imported-group', path: 'imported-group') } + let!(:parent_entity) do + create( + :bulk_import_entity, + destination_namespace: parent.full_path, + group: parent + ) + end + + let(:context) do + instance_double( + BulkImports::Pipeline::Context, + current_user: user, + entity: parent_entity + ) + end + + let(:subgroup_data) do + [ + { + "name" => "subgroup", + "full_path" => "parent/subgroup" + } + ] + end + + subject { described_class.new } + + before do + allow_next_instance_of(BulkImports::Groups::Extractors::SubgroupsExtractor) do |extractor| + allow(extractor).to receive(:extract).and_return(subgroup_data) + end + + parent.add_owner(user) + end + + it 'creates entities for the subgroups' do + expect { subject.run(context) }.to change(BulkImports::Entity, :count).by(1) + + subgroup_entity = BulkImports::Entity.last + + expect(subgroup_entity.source_full_path).to eq 'parent/subgroup' + expect(subgroup_entity.destination_namespace).to eq 'imported-group' + expect(subgroup_entity.destination_name).to eq 'subgroup' + expect(subgroup_entity.parent_id).to eq parent_entity.id + end + end + + describe 'pipeline parts' do + it { expect(described_class).to include_module(BulkImports::Pipeline) } + it { expect(described_class).to include_module(BulkImports::Pipeline::Attributes) } + it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) } + + it 'has extractors' do + expect(described_class.extractors).to contain_exactly( + klass: BulkImports::Groups::Extractors::SubgroupsExtractor, + options: nil + ) + end + + it 'has transformers' do + expect(described_class.transformers).to contain_exactly( + klass: BulkImports::Groups::Transformers::SubgroupToEntityTransformer, + options: nil + ) + end + + it 'has loaders' do + expect(described_class.loaders).to contain_exactly( + klass: BulkImports::Common::Loaders::EntityLoader, + options: nil + ) + end + end +end diff --git a/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb new file mode 100644 index 00000000000..28a7859915d --- /dev/null +++ b/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do + describe '#transform' do + let(:user) { create(:user) } + let(:parent) { create(:group) } + let(:group) { create(:group, name: 'My Source Group', parent: parent) } + let(:entity) do + instance_double( + BulkImports::Entity, + source_full_path: 'source/full/path', + destination_name: group.name, + destination_namespace: parent.full_path + ) + end + + let(:context) do + instance_double( + BulkImports::Pipeline::Context, + current_user: user, + entity: entity + ) + end + + let(:data) do + { + 'name' => 'source_name', + 'full_path' => 'source/full/path', + 'visibility' => 'private', + 'project_creation_level' => 'developer', + 'subgroup_creation_level' => 'maintainer' + } + end + + subject { described_class.new } + + it 'transforms name to destination name' do + transformed_data = subject.transform(context, data) + + expect(transformed_data['name']).not_to eq('source_name') + expect(transformed_data['name']).to eq(group.name) + end + + it 'removes full path' do + transformed_data = subject.transform(context, data) + + expect(transformed_data).not_to have_key('full_path') + end + + it 'transforms path to parameterized name' do + transformed_data = subject.transform(context, data) + + expect(transformed_data['path']).to eq(group.name.parameterize) + end + + it 'transforms visibility level' do + visibility = data['visibility'] + transformed_data = subject.transform(context, data) + + expect(transformed_data).not_to have_key('visibility') + expect(transformed_data['visibility_level']).to eq(Gitlab::VisibilityLevel.string_options[visibility]) + end + + it 'transforms project creation level' do + level = data['project_creation_level'] + transformed_data = subject.transform(context, data) + + expect(transformed_data['project_creation_level']).to eq(Gitlab::Access.project_creation_string_options[level]) + end + + it 'transforms subgroup creation level' do + level = data['subgroup_creation_level'] + transformed_data = subject.transform(context, data) + + expect(transformed_data['subgroup_creation_level']).to eq(Gitlab::Access.subgroup_creation_string_options[level]) + end + + describe 'parent group transformation' do + it 'sets parent id' do + transformed_data = subject.transform(context, data) + + expect(transformed_data['parent_id']).to eq(parent.id) + end + + context 'when destination namespace is user namespace' do + let(:entity) do + instance_double( + BulkImports::Entity, + source_full_path: 'source/full/path', + destination_name: group.name, + destination_namespace: user.namespace.full_path + ) + end + + it 'does not set parent id' do + transformed_data = subject.transform(context, data) + + expect(transformed_data).not_to have_key('parent_id') + end + end + end + end +end diff --git a/spec/lib/bulk_imports/groups/transformers/subgroup_to_entity_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/subgroup_to_entity_transformer_spec.rb new file mode 100644 index 00000000000..2f97a5721e7 --- /dev/null +++ b/spec/lib/bulk_imports/groups/transformers/subgroup_to_entity_transformer_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Transformers::SubgroupToEntityTransformer do + describe "#transform" do + it "transforms subgroups data in entity params" do + parent = create(:group) + parent_entity = instance_double(BulkImports::Entity, group: parent, id: 1) + context = instance_double(BulkImports::Pipeline::Context, entity: parent_entity) + subgroup_data = { + "name" => "subgroup", + "full_path" => "parent/subgroup" + } + + expect(subject.transform(context, subgroup_data)).to eq( + source_type: :group_entity, + source_full_path: "parent/subgroup", + destination_name: "subgroup", + destination_namespace: parent.full_path, + parent_id: 1 + ) + end + end +end diff --git a/spec/lib/bulk_imports/importers/group_importer_spec.rb b/spec/lib/bulk_imports/importers/group_importer_spec.rb new file mode 100644 index 00000000000..95ac5925c97 --- /dev/null +++ b/spec/lib/bulk_imports/importers/group_importer_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Importers::GroupImporter do + let(:user) { create(:user) } + let(:bulk_import) { create(:bulk_import) } + let(:bulk_import_entity) { create(:bulk_import_entity, bulk_import: bulk_import) } + let(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) } + let(:context) do + BulkImports::Pipeline::Context.new( + current_user: user, + entity: bulk_import_entity, + configuration: bulk_import_configuration + ) + end + + subject { described_class.new(bulk_import_entity) } + + before do + allow(BulkImports::Pipeline::Context).to receive(:new).and_return(context) + stub_http_requests + end + + describe '#execute' do + it "starts the entity and run its pipelines" do + expect(bulk_import_entity).to receive(:start).and_call_original + expect_to_run_pipeline BulkImports::Groups::Pipelines::GroupPipeline, context: context + expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context + + subject.execute + + expect(bulk_import_entity.reload).to be_finished + end + end + + def expect_to_run_pipeline(klass, context:) + expect_next_instance_of(klass) do |pipeline| + expect(pipeline).to receive(:run).with(context) + end + end + + def stub_http_requests + double_response = double( + code: 200, + success?: true, + parsed_response: {}, + headers: {} + ) + + allow_next_instance_of(BulkImports::Clients::Http) do |client| + allow(client).to receive(:get).and_return(double_response) + allow(client).to receive(:post).and_return(double_response) + end + end +end diff --git a/spec/lib/bulk_imports/importers/groups_importer_spec.rb b/spec/lib/bulk_imports/importers/groups_importer_spec.rb new file mode 100644 index 00000000000..4865034b0cd --- /dev/null +++ b/spec/lib/bulk_imports/importers/groups_importer_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Importers::GroupsImporter do + let_it_be(:bulk_import) { create(:bulk_import) } + + subject { described_class.new(bulk_import.id) } + + describe '#execute' do + context "when there is entities to be imported" do + let!(:bulk_import_entity) { create(:bulk_import_entity, bulk_import: bulk_import) } + + it "starts the bulk_import and imports its entities" do + expect(BulkImports::Importers::GroupImporter).to receive(:new) + .with(bulk_import_entity).and_return(double(execute: true)) + expect(BulkImportWorker).to receive(:perform_async).with(bulk_import.id) + + subject.execute + + expect(bulk_import.reload).to be_started + end + end + + context "when there is no entities to be imported" do + it "starts the bulk_import and imports its entities" do + expect(BulkImports::Importers::GroupImporter).not_to receive(:new) + expect(BulkImportWorker).not_to receive(:perform_async) + + subject.execute + + expect(bulk_import.reload).to be_finished + end + end + end +end diff --git a/spec/lib/bulk_imports/pipeline/attributes_spec.rb b/spec/lib/bulk_imports/pipeline/attributes_spec.rb new file mode 100644 index 00000000000..54c5dbd4cae --- /dev/null +++ b/spec/lib/bulk_imports/pipeline/attributes_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Pipeline::Attributes do + describe 'pipeline attributes' do + before do + stub_const('BulkImports::Extractor', Class.new) + stub_const('BulkImports::Transformer', Class.new) + stub_const('BulkImports::Loader', Class.new) + + klass = Class.new do + include BulkImports::Pipeline::Attributes + + extractor BulkImports::Extractor, { foo: :bar } + transformer BulkImports::Transformer, { foo: :bar } + loader BulkImports::Loader, { foo: :bar } + end + + stub_const('BulkImports::MyPipeline', klass) + end + + describe 'getters' do + it 'retrieves class attributes' do + expect(BulkImports::MyPipeline.extractors).to contain_exactly({ klass: BulkImports::Extractor, options: { foo: :bar } }) + expect(BulkImports::MyPipeline.transformers).to contain_exactly({ klass: BulkImports::Transformer, options: { foo: :bar } }) + expect(BulkImports::MyPipeline.loaders).to contain_exactly({ klass: BulkImports::Loader, options: { foo: :bar } }) + end + end + + describe 'setters' do + it 'sets class attributes' do + klass = Class.new + options = { test: :test } + + BulkImports::MyPipeline.extractor(klass, options) + BulkImports::MyPipeline.transformer(klass, options) + BulkImports::MyPipeline.loader(klass, options) + + expect(BulkImports::MyPipeline.extractors) + .to contain_exactly( + { klass: BulkImports::Extractor, options: { foo: :bar } }, + { klass: klass, options: options }) + + expect(BulkImports::MyPipeline.transformers) + .to contain_exactly( + { klass: BulkImports::Transformer, options: { foo: :bar } }, + { klass: klass, options: options }) + + expect(BulkImports::MyPipeline.loaders) + .to contain_exactly( + { klass: BulkImports::Loader, options: { foo: :bar } }, + { klass: klass, options: options }) + end + end + end +end diff --git a/spec/lib/bulk_imports/pipeline/context_spec.rb b/spec/lib/bulk_imports/pipeline/context_spec.rb new file mode 100644 index 00000000000..e9af6313ca4 --- /dev/null +++ b/spec/lib/bulk_imports/pipeline/context_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Pipeline::Context do + describe '#initialize' do + it 'initializes with permitted attributes' do + args = { + current_user: create(:user), + entity: create(:bulk_import_entity), + configuration: create(:bulk_import_configuration) + } + + context = described_class.new(args) + + args.each do |k, v| + expect(context.public_send(k)).to eq(v) + end + end + + context 'when invalid argument is passed' do + it 'raises NoMethodError' do + expect { described_class.new(test: 'test').test }.to raise_exception(NoMethodError) + end + end + end +end diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb new file mode 100644 index 00000000000..8c882c799ec --- /dev/null +++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Pipeline::Runner do + describe 'pipeline runner' do + before do + extractor = Class.new do + def initialize(options = {}); end + + def extract(context); end + end + + transformer = Class.new do + def initialize(options = {}); end + + def transform(context, entry); end + end + + loader = Class.new do + def initialize(options = {}); end + + def load(context, entry); end + end + + stub_const('BulkImports::Extractor', extractor) + stub_const('BulkImports::Transformer', transformer) + stub_const('BulkImports::Loader', loader) + + pipeline = Class.new do + include BulkImports::Pipeline + + extractor BulkImports::Extractor + transformer BulkImports::Transformer + loader BulkImports::Loader + end + + stub_const('BulkImports::MyPipeline', pipeline) + end + + it 'runs pipeline extractor, transformer, loader' do + context = instance_double( + BulkImports::Pipeline::Context, + entity: instance_double(BulkImports::Entity, id: 1, source_type: 'group') + ) + entries = [{ foo: :bar }] + + expect_next_instance_of(BulkImports::Extractor) do |extractor| + expect(extractor).to receive(:extract).with(context).and_return(entries) + end + + expect_next_instance_of(BulkImports::Transformer) do |transformer| + expect(transformer).to receive(:transform).with(context, entries.first).and_return(entries.first) + end + + expect_next_instance_of(BulkImports::Loader) do |loader| + expect(loader).to receive(:load).with(context, entries.first) + end + + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger).to receive(:info) + .with(message: "Pipeline started", pipeline: 'BulkImports::MyPipeline', entity: 1, entity_type: 'group') + expect(logger).to receive(:info) + .with(entity: 1, entity_type: 'group', extractor: 'BulkImports::Extractor') + expect(logger).to receive(:info) + .with(entity: 1, entity_type: 'group', transformer: 'BulkImports::Transformer') + expect(logger).to receive(:info) + .with(entity: 1, entity_type: 'group', loader: 'BulkImports::Loader') + end + + BulkImports::MyPipeline.new.run(context) + end + end +end |