diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-25 22:51:41 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-25 22:51:41 +0300 |
commit | e7b32a77cc40a14eb0dd6ae1bfc6f036819c66cc (patch) | |
tree | 86f33d8f98e4eab9c8a8e900f9e370939921c378 /spec | |
parent | 4711b9334036cc4719fb9e475545709e8fd5b649 (diff) |
Add latest changes from gitlab-org/gitlab@16-0-stable-ee
Diffstat (limited to 'spec')
22 files changed, 1233 insertions, 2 deletions
diff --git a/spec/lib/bitbucket_server/representation/pull_request_spec.rb b/spec/lib/bitbucket_server/representation/pull_request_spec.rb index d7b893e8081..5312bc1d71b 100644 --- a/spec/lib/bitbucket_server/representation/pull_request_spec.rb +++ b/spec/lib/bitbucket_server/representation/pull_request_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe BitbucketServer::Representation::PullRequest do +RSpec.describe BitbucketServer::Representation::PullRequest, feature_category: :importers do let(:sample_data) { Gitlab::Json.parse(fixture_file('importers/bitbucket_server/pull_request.json')) } subject { described_class.new(sample_data) } @@ -105,4 +105,21 @@ RSpec.describe BitbucketServer::Representation::PullRequest do describe '#target_branch_sha' do it { expect(subject.target_branch_sha).to eq('839fa9a2d434eb697815b8fcafaecc51accfdbbc') } end + + describe '#to_hash' do + it do + expect(subject.to_hash).to match( + a_hash_including( + author_email: "joe.montana@49ers.com", + author_username: "username", + author: "root", + description: "Test", + source_branch_name: "refs/heads/root/CODE_OF_CONDUCTmd-1530600625006", + target_branch_name: "refs/heads/master", + target_branch_sha: "839fa9a2d434eb697815b8fcafaecc51accfdbbc", + title: "Added a new line" + ) + ) + end + end end diff --git a/spec/lib/gitlab/bitbucket_server_import/importers/lfs_object_importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importers/lfs_object_importer_spec.rb new file mode 100644 index 00000000000..ba63889ab9d --- /dev/null +++ b/spec/lib/gitlab/bitbucket_server_import/importers/lfs_object_importer_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::Importers::LfsObjectImporter, feature_category: :importers do + let_it_be(:project) { create(:project) } + + let(:lfs_attributes) do + { + 'oid' => 'myoid', + 'size' => 1, + 'link' => 'http://www.gitlab.com/lfs_objects/oid', + 'headers' => { 'X-Some-Header' => '456' } + } + end + + let(:importer) { described_class.new(project, lfs_attributes) } + + describe '#execute' do + it 'calls the LfsDownloadService with the lfs object attributes' do + expect_next_instance_of( + Projects::LfsPointers::LfsDownloadService, project, have_attributes(lfs_attributes) + ) do |service| + expect(service).to receive(:execute).and_return(ServiceResponse.success) + end + + importer.execute + end + + it 'logs its progress' do + allow_next_instance_of(Projects::LfsPointers::LfsDownloadService) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.success) + end + + common_log_message = { + oid: 'myoid', + import_stage: 'import_lfs_object', + class: described_class.name, + project_id: project.id, + project_path: project.full_path + } + + expect(Gitlab::BitbucketServerImport::Logger) + .to receive(:info).with(common_log_message.merge(message: 'starting')).and_call_original + expect(Gitlab::BitbucketServerImport::Logger) + .to receive(:info).with(common_log_message.merge(message: 'finished')).and_call_original + + importer.execute + end + end +end diff --git a/spec/lib/gitlab/bitbucket_server_import/importers/lfs_objects_importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importers/lfs_objects_importer_spec.rb new file mode 100644 index 00000000000..0d66ad7c2ec --- /dev/null +++ b/spec/lib/gitlab/bitbucket_server_import/importers/lfs_objects_importer_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::Importers::LfsObjectsImporter, feature_category: :importers do + let_it_be(:project) do + create(:project, :import_started, + import_data_attributes: { + data: { 'project_key' => 'key', 'repo_slug' => 'slug' }, + credentials: { 'token' => 'token' } + } + ) + end + + let(:lfs_attributes) do + { + oid: 'a' * 64, + size: 1, + link: 'http://www.gitlab.com/lfs_objects/oid', + headers: { 'X-Some-Header' => '456' } + } + end + + let(:lfs_download_object) { LfsDownloadObject.new(**lfs_attributes) } + + let(:common_log_messages) do + { + import_stage: 'import_lfs_objects', + class: described_class.name, + project_id: project.id, + project_path: project.full_path + } + end + + describe '#execute', :clean_gitlab_redis_cache do + context 'when lfs is enabled' do + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + end + + it 'imports each lfs object in parallel' do + importer = described_class.new(project) + + expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service| + expect(service).to receive(:each_list_item).and_yield(lfs_download_object) + end + + expect(Gitlab::BitbucketServerImport::ImportLfsObjectWorker).to receive(:perform_in) + .with(1.second, project.id, lfs_attributes.stringify_keys, start_with(Gitlab::JobWaiter::KEY_PREFIX)) + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(1) + end + + it 'logs its progress' do + importer = described_class.new(project) + + expect(Gitlab::BitbucketServerImport::Logger) + .to receive(:info).with(common_log_messages.merge(message: 'starting')).and_call_original + expect(Gitlab::BitbucketServerImport::Logger) + .to receive(:info).with(common_log_messages.merge(message: 'finished')).and_call_original + + importer.execute + end + + context 'when LFS list download fails' do + let(:exception) { StandardError.new('Invalid Project URL') } + + before do + allow_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service| + allow(service).to receive(:each_list_item).and_raise(exception) + end + end + + it 'rescues and logs the exception' do + importer = described_class.new(project) + + expect(Gitlab::Import::ImportFailureService) + .to receive(:track) + .with( + project_id: project.id, + exception: exception, + error_source: described_class.name + ).and_call_original + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(0) + end + end + end + + context 'when LFS is not enabled' do + before do + allow(project).to receive(:lfs_enabled?).and_return(false) + end + + it 'logs progress but does nothing' do + importer = described_class.new(project) + + expect(Gitlab::BitbucketServerImport::Logger).to receive(:info).twice + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(0) + end + end + end +end diff --git a/spec/lib/gitlab/bitbucket_server_import/importers/notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importers/notes_importer_spec.rb new file mode 100644 index 00000000000..2237694deb6 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_server_import/importers/notes_importer_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::Importers::NotesImporter, feature_category: :importers do + let_it_be(:project) do + create(:project, :import_started, + import_data_attributes: { + data: { 'project_key' => 'key', 'repo_slug' => 'slug' }, + credentials: { 'base_uri' => 'http://bitbucket.org/', 'user' => 'bitbucket', 'password' => 'password' } + } + ) + end + + let_it_be(:merge_request_1) { create(:merge_request, source_project: project, iid: 100, source_branch: 'branch_1') } + let_it_be(:merge_request_2) { create(:merge_request, source_project: project, iid: 101, source_branch: 'branch_2') } + + subject(:importer) { described_class.new(project) } + + describe '#execute', :clean_gitlab_redis_cache do + it 'schedules a job to import notes for each corresponding merge request', :aggregate_failures do + expect(Gitlab::BitbucketServerImport::ImportPullRequestNotesWorker).to receive(:perform_in).twice + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(2) + expect(Gitlab::Cache::Import::Caching.values_from_set(importer.already_processed_cache_key)) + .to match_array(%w[100 101]) + end + + context 'when pull request was already processed' do + before do + Gitlab::Cache::Import::Caching.set_add(importer.already_processed_cache_key, "100") + end + + it 'does not schedule job for processed merge requests', :aggregate_failures do + expect(Gitlab::BitbucketServerImport::ImportPullRequestNotesWorker).to receive(:perform_in).once + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(2) + end + end + end +end diff --git a/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_importer_spec.rb new file mode 100644 index 00000000000..012cdcdd260 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_importer_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestImporter, feature_category: :importers do + include AfterNextHelpers + + let_it_be(:project) { create(:project, :repository) } + + let(:pull_request_data) { Gitlab::Json.parse(fixture_file('importers/bitbucket_server/pull_request.json')) } + let(:pull_request) { BitbucketServer::Representation::PullRequest.new(pull_request_data) } + + subject(:importer) { described_class.new(project, pull_request.to_hash) } + + describe '#execute' do + it 'imports the merge request correctly' do + expect_next(Gitlab::Import::MergeRequestCreator, project).to receive(:execute).and_call_original + expect_next(Gitlab::BitbucketServerImport::UserFinder, project).to receive(:author_id).and_call_original + expect { importer.execute }.to change { MergeRequest.count }.by(1) + + merge_request = project.merge_requests.find_by_iid(pull_request.iid) + + expect(merge_request).to have_attributes( + iid: pull_request.iid, + title: pull_request.title, + source_branch: 'root/CODE_OF_CONDUCTmd-1530600625006', + target_branch: 'master', + state: pull_request.state, + author_id: project.creator_id, + description: "*Created by: #{pull_request.author}*\n\n#{pull_request.description}" + ) + end + + it 'logs its progress' do + expect(Gitlab::BitbucketServerImport::Logger) + .to receive(:info).with(include(message: 'starting', iid: pull_request.iid)).and_call_original + expect(Gitlab::BitbucketServerImport::Logger) + .to receive(:info).with(include(message: 'finished', iid: pull_request.iid)).and_call_original + + importer.execute + end + end +end diff --git a/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_notes_importer_spec.rb new file mode 100644 index 00000000000..c7e91c340b0 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_notes_importer_spec.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestNotesImporter, feature_category: :importers do + include AfterNextHelpers + + let_it_be(:project) do + create(:project, :repository, :import_started, + import_data_attributes: { + data: { 'project_key' => 'key', 'repo_slug' => 'slug' }, + credentials: { 'token' => 'token' } + } + ) + end + + let_it_be(:pull_request_data) { Gitlab::Json.parse(fixture_file('importers/bitbucket_server/pull_request.json')) } + let_it_be(:pull_request) { BitbucketServer::Representation::PullRequest.new(pull_request_data) } + let_it_be(:note_author) { create(:user, username: 'note_author', email: 'note_author@example.org') } + + let_it_be(:pull_request_author) do + create(:user, username: 'pull_request_author', email: 'pull_request_author@example.org') + end + + let(:merge_event) do + instance_double( + BitbucketServer::Representation::Activity, + comment?: false, + merge_event?: true, + committer_email: pull_request_author.email, + merge_timestamp: now, + merge_commit: '12345678' + ) + end + + let(:pr_note) do + instance_double( + BitbucketServer::Representation::Comment, + note: 'Hello world', + author_email: note_author.email, + author_username: note_author.username, + comments: [], + created_at: now, + updated_at: now, + parent_comment: nil) + end + + let(:pr_comment) do + instance_double( + BitbucketServer::Representation::Activity, + comment?: true, + inline_comment?: false, + merge_event?: false, + comment: pr_note) + end + + let_it_be(:sample) { RepoHelpers.sample_compare } + let_it_be(:now) { Time.now.utc.change(usec: 0) } + + def expect_log(stage:, message:) + allow(Gitlab::BitbucketServerImport::Logger).to receive(:info).and_call_original + expect(Gitlab::BitbucketServerImport::Logger) + .to receive(:info).with(include(import_stage: stage, message: message)) + end + + subject(:importer) { described_class.new(project, pull_request.to_hash) } + + describe '#execute', :clean_gitlab_redis_cache do + context 'when a matching merge request is not found' do + it 'does nothing' do + expect { importer.execute }.not_to change { Note.count } + end + + it 'logs its progress' do + expect_log(stage: 'import_pull_request_notes', message: 'starting') + expect_log(stage: 'import_pull_request_notes', message: 'finished') + + importer.execute + end + end + + context 'when a matching merge request is found' do + let_it_be(:merge_request) { create(:merge_request, iid: pull_request.iid, source_project: project) } + + it 'logs its progress' do + allow_next(BitbucketServer::Client).to receive(:activities).and_return([]) + + expect_log(stage: 'import_pull_request_notes', message: 'starting') + expect_log(stage: 'import_pull_request_notes', message: 'finished') + + importer.execute + end + + context 'when PR has comments' do + before do + allow_next(BitbucketServer::Client).to receive(:activities).and_return([pr_comment]) + end + + it 'imports the stand alone comments' do + expect { subject.execute }.to change { Note.count }.by(1) + + expect(merge_request.notes.count).to eq(1) + expect(merge_request.notes.first).to have_attributes( + note: end_with(pr_note.note), + author: note_author, + created_at: pr_note.created_at, + updated_at: pr_note.created_at + ) + end + + it 'logs its progress' do + expect_log(stage: 'import_standalone_pr_comments', message: 'starting') + expect_log(stage: 'import_standalone_pr_comments', message: 'finished') + + importer.execute + end + end + + context 'when PR has threaded discussion' do + let_it_be(:reply_author) { create(:user, username: 'reply_author', email: 'reply_author@example.org') } + let_it_be(:inline_note_author) do + create(:user, username: 'inline_note_author', email: 'inline_note_author@example.org') + end + + let(:reply) do + instance_double( + BitbucketServer::Representation::PullRequestComment, + author_email: reply_author.email, + author_username: reply_author.username, + note: 'I agree', + created_at: now, + updated_at: now, + parent_comment: nil) + end + + let(:pr_inline_note) do + instance_double( + BitbucketServer::Representation::PullRequestComment, + file_type: 'ADDED', + from_sha: pull_request.target_branch_sha, + to_sha: pull_request.source_branch_sha, + file_path: '.gitmodules', + old_pos: nil, + new_pos: 4, + note: 'Hello world', + author_email: inline_note_author.email, + author_username: inline_note_author.username, + comments: [reply], + created_at: now, + updated_at: now, + parent_comment: nil) + end + + let(:pr_inline_comment) do + instance_double( + BitbucketServer::Representation::Activity, + comment?: true, + inline_comment?: true, + merge_event?: false, + comment: pr_inline_note) + end + + before do + allow_next(BitbucketServer::Client).to receive(:activities).and_return([pr_inline_comment]) + end + + it 'imports the threaded discussion' do + expect { subject.execute }.to change { Note.count }.by(2) + + expect(merge_request.discussions.count).to eq(1) + + notes = merge_request.notes.order(:id).to_a + start_note = notes.first + expect(start_note.type).to eq('DiffNote') + expect(start_note.note).to end_with(pr_inline_note.note) + expect(start_note.created_at).to eq(pr_inline_note.created_at) + expect(start_note.updated_at).to eq(pr_inline_note.updated_at) + expect(start_note.position.old_line).to be_nil + expect(start_note.position.new_line).to eq(pr_inline_note.new_pos) + expect(start_note.author).to eq(inline_note_author) + + reply_note = notes.last + expect(reply_note.note).to eq(reply.note) + expect(reply_note.author).to eq(reply_author) + expect(reply_note.created_at).to eq(reply.created_at) + expect(reply_note.updated_at).to eq(reply.created_at) + expect(reply_note.position.old_line).to be_nil + expect(reply_note.position.new_line).to eq(pr_inline_note.new_pos) + end + + it 'logs its progress' do + expect_log(stage: 'import_inline_comments', message: 'starting') + expect_log(stage: 'import_inline_comments', message: 'finished') + + importer.execute + end + end + + context 'when PR has a merge event' do + before do + allow_next(BitbucketServer::Client).to receive(:activities).and_return([merge_event]) + end + + it 'imports the merge event' do + importer.execute + + merge_request.reload + + expect(merge_request.metrics.merged_by).to eq(pull_request_author) + expect(merge_request.metrics.merged_at).to eq(merge_event.merge_timestamp) + expect(merge_request.merge_commit_sha).to eq(merge_event.merge_commit) + end + end + end + end +end diff --git a/spec/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer_spec.rb new file mode 100644 index 00000000000..b9a9c8dac29 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestsImporter, feature_category: :importers do + let_it_be(:project) do + create(:project, :import_started, + import_data_attributes: { + data: { 'project_key' => 'key', 'repo_slug' => 'slug' }, + credentials: { 'base_uri' => 'http://bitbucket.org/', 'user' => 'bitbucket', 'password' => 'password' } + } + ) + end + + subject(:importer) { described_class.new(project) } + + describe '#execute', :clean_gitlab_redis_cache do + before do + allow_next_instance_of(BitbucketServer::Client) do |client| + allow(client).to receive(:pull_requests).and_return( + [ + BitbucketServer::Representation::PullRequest.new({ 'id' => 1 }), + BitbucketServer::Representation::PullRequest.new({ 'id' => 2 }) + ], + [] + ) + end + end + + it 'imports each pull request in parallel', :aggregate_failures do + expect(Gitlab::BitbucketServerImport::ImportPullRequestWorker).to receive(:perform_in).twice + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(2) + expect(Gitlab::Cache::Import::Caching.values_from_set(importer.already_processed_cache_key)) + .to match_array(%w[1 2]) + end + + context 'when pull request was already processed' do + before do + Gitlab::Cache::Import::Caching.set_add(importer.already_processed_cache_key, 1) + end + + it 'does not schedule job for processed pull requests', :aggregate_failures do + expect(Gitlab::BitbucketServerImport::ImportPullRequestWorker).to receive(:perform_in).once + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(2) + end + end + end +end diff --git a/spec/lib/gitlab/bitbucket_server_import/importers/repository_importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importers/repository_importer_spec.rb new file mode 100644 index 00000000000..6c4d500efb7 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_server_import/importers/repository_importer_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::Importers::RepositoryImporter, feature_category: :importers do + let_it_be(:project) { create(:project, import_url: 'http://bitbucket:test@my-bitbucket') } + + subject(:importer) { described_class.new(project) } + + describe '#execute' do + context 'when repository is empty' do + it 'imports the repository' do + expect(project.repository).to receive(:import_repository).with(project.import_url) + expect(project.repository).to receive(:fetch_as_mirror).with(project.import_url, + refmap: ['+refs/pull-requests/*/to:refs/merge-requests/*/head']) + expect(project.last_repository_updated_at).to be_present + + importer.execute + end + end + + context 'when repository is not empty' do + before do + allow(project).to receive(:empty_repo?).and_return(false) + + project.last_repository_updated_at = 1.day.ago + end + + it 'does not import the repository' do + expect(project.repository).not_to receive(:import_repository) + + expect { importer.execute }.not_to change { project.last_repository_updated_at } + end + end + + context 'when a Git CommandError is raised and the repository exists' do + before do + allow(project.repository).to receive(:import_repository).and_raise(::Gitlab::Git::CommandError) + allow(project).to receive(:repository_exists?).and_return(true) + end + + it 'expires repository caches' do + expect(project.repository).to receive(:expire_content_cache) + + expect { importer.execute }.to raise_error(::Gitlab::Git::CommandError) + end + end + end +end diff --git a/spec/lib/gitlab/bitbucket_server_import/parallel_importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/parallel_importer_spec.rb new file mode 100644 index 00000000000..a36f2403403 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_server_import/parallel_importer_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::ParallelImporter, feature_category: :importers do + describe '.async?' do + it 'returns true' do + expect(described_class).to be_async + end + end + + describe '.track_start_import' do + it 'tracks the start of import' do + project = build_stubbed(:project) + + expect_next_instance_of(Gitlab::Import::Metrics, :bitbucket_server_importer, project) do |metric| + expect(metric).to receive(:track_start_import) + end + + described_class.track_start_import(project) + end + end + + describe '#execute', :clean_gitlab_redis_shared_state do + let_it_be(:project) { create(:project) } + let(:importer) { described_class.new(project) } + + before do + create(:import_state, :started, project: project) + end + + it 'schedules the importing of the repository' do + expect(Gitlab::BitbucketServerImport::Stage::ImportRepositoryWorker) + .to receive_message_chain(:with_status, :perform_async).with(project.id) + + expect(importer.execute).to eq(true) + end + + it 'sets the JID in Redis' do + expect(Gitlab::Import::SetAsyncJid).to receive(:set_jid).with(project.import_state).and_call_original + + importer.execute + end + end +end diff --git a/spec/lib/gitlab/bitbucket_server_import/user_finder_spec.rb b/spec/lib/gitlab/bitbucket_server_import/user_finder_spec.rb new file mode 100644 index 00000000000..70923df3064 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_server_import/user_finder_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::UserFinder, :clean_gitlab_redis_cache, feature_category: :importers do + let_it_be(:user) { create(:user) } + + let(:created_id) { 1 } + let(:project) { instance_double(Project, creator_id: created_id, id: 1) } + + subject(:user_finder) { described_class.new(project) } + + describe '#author_id' do + it 'calls uid method' do + object = { author_username: user.username } + + expect(user_finder).to receive(:uid).with(object).and_return(10) + expect(user_finder.author_id(object)).to eq(10) + end + + context 'when corresponding user does not exist' do + it 'fallsback to project creator_id' do + object = { author_email: 'unknown' } + + expect(user_finder.author_id(object)).to eq(created_id) + end + end + end + + describe '#uid' do + context 'when provided object is a Hash' do + it 'maps to an existing user with the same username' do + object = { author_username: user.username } + + expect(user_finder.uid(object)).to eq(user.id) + end + end + + context 'when provided object is a representation Object' do + it 'maps to a existing user with the same username' do + object = instance_double(BitbucketServer::Representation::Comment, author_username: user.username) + + expect(user_finder.uid(object)).to eq(user.id) + end + end + + context 'when corresponding user does not exist' do + it 'returns nil' do + object = { author_username: 'unknown' } + + expect(user_finder.uid(object)).to eq(nil) + end + end + + context 'when bitbucket_server_user_mapping_by_username is disabled' do + before do + stub_feature_flags(bitbucket_server_user_mapping_by_username: false) + end + + context 'when provided object is a Hash' do + it 'maps to an existing user with the same email' do + object = { author_email: user.email } + + expect(user_finder.uid(object)).to eq(user.id) + end + end + + context 'when provided object is a representation Object' do + it 'maps to an existing user with the same email' do + object = instance_double(BitbucketServer::Representation::Comment, author_email: user.email) + + expect(user_finder.uid(object)).to eq(user.id) + end + end + + context 'when corresponding user does not exist' do + it 'returns nil' do + object = { author_email: 'unknown' } + + expect(user_finder.uid(object)).to eq(nil) + end + end + end + end + + describe '#find_user_id' do + context 'when user cannot be found' do + it 'caches and returns nil' do + expect(User).to receive(:find_by_any_email).once.and_call_original + + 2.times do + user_id = user_finder.find_user_id(by: :email, value: 'nobody@example.com') + + expect(user_id).to be_nil + end + end + end + + context 'when user can be found' do + it 'caches and returns the user ID by email' do + expect(User).to receive(:find_by_any_email).once.and_call_original + + 2.times do + user_id = user_finder.find_user_id(by: :email, value: user.email) + + expect(user_id).to eq(user.id) + end + end + + it 'caches and returns the user ID by username' do + expect(User).to receive(:find_by_username).once.and_call_original + + 2.times do + user_id = user_finder.find_user_id(by: :username, value: user.username) + + expect(user_id).to eq(user.id) + end + end + end + end +end diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb index f1ea5f3e85e..b243780a020 100644 --- a/spec/lib/gitlab/import_sources_spec.rb +++ b/spec/lib/gitlab/import_sources_spec.rb @@ -59,7 +59,7 @@ RSpec.describe Gitlab::ImportSources, feature_category: :importers do import_sources = { 'github' => Gitlab::GithubImport::ParallelImporter, 'bitbucket' => Gitlab::BitbucketImport::Importer, - 'bitbucket_server' => Gitlab::BitbucketServerImport::Importer, + 'bitbucket_server' => Gitlab::BitbucketServerImport::ParallelImporter, 'fogbugz' => Gitlab::FogbugzImport::Importer, 'git' => nil, 'gitlab_project' => Gitlab::ImportExport::Importer, @@ -72,6 +72,46 @@ RSpec.describe Gitlab::ImportSources, feature_category: :importers do expect(described_class.importer(name)).to eq(klass) end end + + context 'when flag is disabled' do + before do + stub_feature_flags(bitbucket_server_parallel_importer: false) + end + + it 'returns Gitlab::BitbucketServerImport::Importer when given bitbucket_server' do + expect(described_class.importer('bitbucket_server')).to eq(Gitlab::BitbucketServerImport::Importer) + end + end + end + + describe '.import_table' do + subject { described_class.import_table } + + it 'returns the ParallelImporter for Bitbucket server' do + is_expected.to include( + described_class::ImportSource.new( + 'bitbucket_server', + 'Bitbucket Server', + Gitlab::BitbucketServerImport::ParallelImporter + ) + ) + end + + context 'when flag is disabled' do + before do + stub_feature_flags(bitbucket_server_parallel_importer: false) + end + + it 'returns the legacy Importer for Bitbucket server' do + is_expected.to include( + described_class::ImportSource.new( + 'bitbucket_server', + 'Bitbucket Server', + Gitlab::BitbucketServerImport::Importer + ) + ) + end + end end describe '.title' do diff --git a/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb new file mode 100644 index 00000000000..ec2ae0b8a73 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +RSpec.shared_examples Gitlab::BitbucketServerImport::ObjectImporter do + include AfterNextHelpers + + describe '.sidekiq_retries_exhausted' do + let(:job) { { 'args' => [1, {}, 'key'], 'jid' => 'jid' } } + + it 'notifies the waiter' do + expect(Gitlab::JobWaiter).to receive(:notify).with('key', 'jid') + + described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new) + end + end + + describe '#perform' do + let_it_be(:import_started_project) { create(:project, :import_started) } + + let(:project_id) { project_id } + let(:waiter_key) { 'key' } + + shared_examples 'notifies the waiter' do + specify do + allow_next(worker.importer_class).to receive(:execute) + + expect(Gitlab::JobWaiter).to receive(:notify).with(waiter_key, anything) + + worker.perform(project_id, {}, waiter_key) + end + end + + context 'when project does not exist' do + let(:project_id) { non_existing_record_id } + + it_behaves_like 'notifies the waiter' + end + + context 'when project has import started' do + let_it_be(:project) do + create(:project, :import_started, import_data_attributes: { + data: { 'project_key' => 'key', 'repo_slug' => 'slug' }, + credentials: { 'token' => 'token' } + }) + end + + let(:project_id) { project.id } + + it 'calls the importer' do + expect_next(worker.importer_class, project, kind_of(Hash)).to receive(:execute) + + worker.perform(project_id, {}, waiter_key) + end + + it_behaves_like 'notifies the waiter' + end + + context 'when project import has been cancelled' do + let_it_be(:project_id) { create(:project, :import_canceled).id } + + it 'does not call the importer' do + expect_next(worker.importer_class).not_to receive(:execute) + + worker.perform(project_id, {}, waiter_key) + end + + it_behaves_like 'notifies the waiter' + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/stage_methods_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/stage_methods_shared_examples.rb new file mode 100644 index 00000000000..1246dd2979b --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/stage_methods_shared_examples.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.shared_examples Gitlab::BitbucketServerImport::StageMethods do + describe '.sidekiq_retries_exhausted' do + let(:job) { { 'args' => [project.id] } } + + it 'tracks the import failure' do + expect(Gitlab::Import::ImportFailureService) + .to receive(:track).with( + project_id: project.id, + exception: StandardError.new, + fail_import: true + ) + + described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new) + end + end +end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index 16173e322b6..26dd67bf2c0 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -261,6 +261,12 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'Geo::VerificationWorker' => 3, 'GeoRepositoryDestroyWorker' => 3, 'GitGarbageCollectWorker' => false, + 'Gitlab::BitbucketServerImport::AdvanceStageWorker' => 3, + 'Gitlab::BitbucketServerImport::Stage::FinishImportWorker' => 3, + 'Gitlab::BitbucketServerImport::Stage::ImportLfsObjectsWorker' => 3, + 'Gitlab::BitbucketServerImport::Stage::ImportNotesWorker' => 3, + 'Gitlab::BitbucketServerImport::Stage::ImportPullRequestsWorker' => 3, + 'Gitlab::BitbucketServerImport::Stage::ImportRepositoryWorker' => 3, 'Gitlab::GithubImport::AdvanceStageWorker' => 3, 'Gitlab::GithubImport::ImportReleaseAttachmentsWorker' => 5, 'Gitlab::GithubImport::Attachments::ImportReleaseWorker' => 5, diff --git a/spec/workers/gitlab/bitbucket_server_import/import_lfs_object_worker_spec.rb b/spec/workers/gitlab/bitbucket_server_import/import_lfs_object_worker_spec.rb new file mode 100644 index 00000000000..a74c148cbc9 --- /dev/null +++ b/spec/workers/gitlab/bitbucket_server_import/import_lfs_object_worker_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::ImportLfsObjectWorker, feature_category: :importers do + subject(:worker) { described_class.new } + + it_behaves_like Gitlab::BitbucketServerImport::ObjectImporter do + before do + # Stub the LfsDownloadObject for these tests so it can be passed an empty Hash + allow(LfsDownloadObject).to receive(:new) + end + end +end diff --git a/spec/workers/gitlab/bitbucket_server_import/import_pull_request_notes_worker_spec.rb b/spec/workers/gitlab/bitbucket_server_import/import_pull_request_notes_worker_spec.rb new file mode 100644 index 00000000000..bc400bc59e8 --- /dev/null +++ b/spec/workers/gitlab/bitbucket_server_import/import_pull_request_notes_worker_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::ImportPullRequestNotesWorker, feature_category: :importers do + subject(:worker) { described_class.new } + + it_behaves_like Gitlab::BitbucketServerImport::ObjectImporter +end diff --git a/spec/workers/gitlab/bitbucket_server_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/bitbucket_server_import/import_pull_request_worker_spec.rb new file mode 100644 index 00000000000..dd3235f846c --- /dev/null +++ b/spec/workers/gitlab/bitbucket_server_import/import_pull_request_worker_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::ImportPullRequestWorker, feature_category: :importers do + let_it_be(:project) { create(:project, :import_started) } + + let(:worker) { described_class.new } + + let(:job_waiter_key) { 'ABC' } + + let(:importer_class) { Gitlab::BitbucketServerImport::Importers::PullRequestImporter } + + before do + allow(worker).to receive(:jid).and_return('jid') + end + + it_behaves_like Gitlab::BitbucketServerImport::ObjectImporter + + describe '#perform' do + context 'when the import succeeds' do + before do + allow_next_instance_of(importer_class) do |importer| + allow(importer).to receive(:execute) + end + end + + it 'notifies job waiter' do + expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid') + + worker.perform(project.id, {}, job_waiter_key) + end + + it 'logs stage start and finish' do + expect(Gitlab::BitbucketServerImport::Logger) + .to receive(:info).with(hash_including(message: 'importer started', project_id: project.id)) + expect(Gitlab::BitbucketServerImport::Logger) + .to receive(:info).with(hash_including(message: 'importer finished', project_id: project.id)) + + worker.perform(project.id, {}, job_waiter_key) + end + end + + context 'when project does not exists' do + it 'does not call importer and notifies job waiter' do + expect(importer_class).not_to receive(:new) + expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid') + + worker.perform(-1, {}, job_waiter_key) + end + end + + context 'when project import state is not `started`' do + it 'does not call importer' do + project = create(:project, :import_canceled) + + expect(importer_class).not_to receive(:new) + expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid') + + worker.perform(project.id, {}, job_waiter_key) + end + end + + context 'when the importer fails' do + it 'raises an error' do + exception = StandardError.new('Error') + + allow_next_instance_of(importer_class) do |importer| + allow(importer).to receive(:execute).and_raise(exception) + end + + expect(Gitlab::Import::ImportFailureService) + .to receive(:track).with( + project_id: project.id, + exception: exception, + error_source: importer_class.name, + fail_import: false + ).and_call_original + + expect { worker.perform(project.id, {}, job_waiter_key) }.to raise_error(exception) + end + end + end +end diff --git a/spec/workers/gitlab/bitbucket_server_import/stage/finish_import_worker_spec.rb b/spec/workers/gitlab/bitbucket_server_import/stage/finish_import_worker_spec.rb new file mode 100644 index 00000000000..cb61ee13dd1 --- /dev/null +++ b/spec/workers/gitlab/bitbucket_server_import/stage/finish_import_worker_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::Stage::FinishImportWorker, feature_category: :importers do + let_it_be(:project) { create(:project, :import_started) } + + subject(:worker) { described_class.new } + + it_behaves_like Gitlab::BitbucketServerImport::StageMethods + + describe '#perform' do + it 'finalises the import process' do + expect_next_instance_of(Gitlab::Import::Metrics, :bitbucket_server_importer, project) do |metric| + expect(metric).to receive(:track_finished_import) + end + + worker.perform(project.id) + + expect(project.import_state.reload).to be_finished + end + end +end diff --git a/spec/workers/gitlab/bitbucket_server_import/stage/import_lfs_objects_worker_spec.rb b/spec/workers/gitlab/bitbucket_server_import/stage/import_lfs_objects_worker_spec.rb new file mode 100644 index 00000000000..449e22e67e2 --- /dev/null +++ b/spec/workers/gitlab/bitbucket_server_import/stage/import_lfs_objects_worker_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::Stage::ImportLfsObjectsWorker, feature_category: :importers do + let_it_be(:project) { create(:project, :import_started) } + + subject(:worker) { described_class.new } + + it_behaves_like Gitlab::BitbucketServerImport::StageMethods + + describe '#perform' do + context 'when the import succeeds' do + before do + allow_next_instance_of(Gitlab::BitbucketServerImport::Importers::LfsObjectsImporter) do |importer| + allow(importer).to receive(:execute).and_return(Gitlab::JobWaiter.new(2, '123')) + end + end + + it 'schedules the next stage' do + expect(Gitlab::BitbucketServerImport::AdvanceStageWorker).to receive(:perform_async) + .with(project.id, { '123' => 2 }, :finish) + + worker.perform(project.id) + end + end + end +end diff --git a/spec/workers/gitlab/bitbucket_server_import/stage/import_notes_worker_spec.rb b/spec/workers/gitlab/bitbucket_server_import/stage/import_notes_worker_spec.rb new file mode 100644 index 00000000000..f7512c74d49 --- /dev/null +++ b/spec/workers/gitlab/bitbucket_server_import/stage/import_notes_worker_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::Stage::ImportNotesWorker, feature_category: :importers do + let_it_be(:project) { create(:project, :import_started) } + + subject(:worker) { described_class.new } + + it_behaves_like Gitlab::BitbucketServerImport::StageMethods + + describe '#perform' do + context 'when the import succeeds' do + before do + allow_next_instance_of(Gitlab::BitbucketServerImport::Importers::NotesImporter) do |importer| + allow(importer).to receive(:execute).and_return(Gitlab::JobWaiter.new(2, '123')) + end + end + + it 'schedules the next stage' do + expect(Gitlab::BitbucketServerImport::AdvanceStageWorker).to receive(:perform_async) + .with(project.id, { '123' => 2 }, :lfs_objects) + + worker.perform(project.id) + end + end + end +end diff --git a/spec/workers/gitlab/bitbucket_server_import/stage/import_pull_requests_worker_spec.rb b/spec/workers/gitlab/bitbucket_server_import/stage/import_pull_requests_worker_spec.rb new file mode 100644 index 00000000000..77400cabefa --- /dev/null +++ b/spec/workers/gitlab/bitbucket_server_import/stage/import_pull_requests_worker_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::Stage::ImportPullRequestsWorker, feature_category: :importers do + let_it_be(:project) { create(:project, :import_started) } + + subject(:worker) { described_class.new } + + it_behaves_like Gitlab::BitbucketServerImport::StageMethods + + describe '#perform' do + context 'when the import succeeds' do + before do + allow_next_instance_of(Gitlab::BitbucketServerImport::Importers::PullRequestsImporter) do |importer| + allow(importer).to receive(:execute).and_return(Gitlab::JobWaiter.new(2, '123')) + end + end + + it 'schedules the next stage' do + expect(Gitlab::BitbucketServerImport::AdvanceStageWorker).to receive(:perform_async) + .with(project.id, { '123' => 2 }, :notes) + + worker.perform(project.id) + end + + it 'logs stage start and finish' do + expect(Gitlab::BitbucketServerImport::Logger) + .to receive(:info).with(hash_including(message: 'starting stage', project_id: project.id)) + expect(Gitlab::BitbucketServerImport::Logger) + .to receive(:info).with(hash_including(message: 'stage finished', project_id: project.id)) + + worker.perform(project.id) + end + end + + context 'when project does not exists' do + it 'does not call the importer' do + expect(Gitlab::BitbucketServerImport::Importers::PullRequestsImporter).not_to receive(:new) + + worker.perform(-1) + end + end + + context 'when project import state is not `started`' do + it 'does not call the importer' do + project = create(:project, :import_canceled) + + expect(Gitlab::BitbucketServerImport::Importers::PullRequestsImporter).not_to receive(:new) + + worker.perform(project.id) + end + end + + context 'when the importer fails' do + it 'does not schedule the next stage and raises error' do + exception = StandardError.new('Error') + + allow_next_instance_of(Gitlab::BitbucketServerImport::Importers::PullRequestsImporter) do |importer| + allow(importer).to receive(:execute).and_raise(exception) + end + + expect(Gitlab::Import::ImportFailureService) + .to receive(:track).with( + project_id: project.id, + exception: exception, + error_source: described_class.name, + fail_import: false + ).and_call_original + + expect { worker.perform(project.id) } + .to change { Gitlab::BitbucketServerImport::AdvanceStageWorker.jobs.size }.by(0) + .and raise_error(exception) + end + end + end +end diff --git a/spec/workers/gitlab/bitbucket_server_import/stage/import_repository_worker_spec.rb b/spec/workers/gitlab/bitbucket_server_import/stage/import_repository_worker_spec.rb new file mode 100644 index 00000000000..7ea23041e79 --- /dev/null +++ b/spec/workers/gitlab/bitbucket_server_import/stage/import_repository_worker_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::Stage::ImportRepositoryWorker, feature_category: :importers do + let_it_be(:project) { create(:project, :import_started) } + + let(:worker) { described_class.new } + + it_behaves_like Gitlab::BitbucketServerImport::StageMethods + + describe '#perform' do + context 'when the import succeeds' do + before do + allow_next_instance_of(Gitlab::BitbucketServerImport::Importers::RepositoryImporter) do |importer| + allow(importer).to receive(:execute) + end + end + + it 'schedules the next stage' do + expect(Gitlab::BitbucketServerImport::Stage::ImportPullRequestsWorker).to receive(:perform_async) + .with(project.id) + + worker.perform(project.id) + end + + it 'logs stage start and finish' do + expect(Gitlab::BitbucketServerImport::Logger) + .to receive(:info).with(hash_including(message: 'starting stage', project_id: project.id)) + expect(Gitlab::BitbucketServerImport::Logger) + .to receive(:info).with(hash_including(message: 'stage finished', project_id: project.id)) + + worker.perform(project.id) + end + end + + context 'when project does not exists' do + it 'does not call importer' do + expect(Gitlab::BitbucketServerImport::Importers::RepositoryImporter).not_to receive(:new) + + worker.perform(-1) + end + end + + context 'when project import state is not `started`' do + it 'does not call importer' do + project = create(:project, :import_canceled) + + expect(Gitlab::BitbucketServerImport::Importers::RepositoryImporter).not_to receive(:new) + + worker.perform(project.id) + end + end + + context 'when the importer fails' do + it 'does not schedule the next stage and raises error' do + exception = StandardError.new('Error') + + allow_next_instance_of(Gitlab::BitbucketServerImport::Importers::RepositoryImporter) do |importer| + allow(importer).to receive(:execute).and_raise(exception) + end + + expect(Gitlab::Import::ImportFailureService) + .to receive(:track).with( + project_id: project.id, + exception: exception, + error_source: described_class.name, + fail_import: true + ).and_call_original + + expect { worker.perform(project.id) } + .to change { Gitlab::BitbucketServerImport::Stage::ImportPullRequestsWorker.jobs.size }.by(0) + .and raise_error(exception) + end + end + end +end |