# frozen_string_literal: true require 'spec_helper' RSpec.describe Gitlab::BitbucketImport::Importer do include ImportSpecHelper before do stub_omniauth_provider('bitbucket') stub_feature_flags(stricter_mr_branch_name: false) end let(:statuses) do [ "open", "resolved", "on hold", "invalid", "duplicate", "wontfix", "closed" # undocumented status ] end let(:reporters) do [ nil, { "nickname" => "reporter1" }, nil, { "nickname" => "reporter2" }, { "nickname" => "reporter1" }, nil, { "nickname" => "reporter3" } ] end let(:sample_issues_statuses) do issues = [] statuses.map.with_index do |status, index| issues << { id: index, state: status, title: "Issue #{index}", kind: 'bug', content: { raw: "Some content to issue #{index}", markup: "markdown", html: "Some content to issue #{index}" } } end reporters.map.with_index do |reporter, index| issues[index]['reporter'] = reporter end issues end let(:project_identifier) { 'namespace/repo' } let(:data) do { 'bb_session' => { 'bitbucket_token' => "123456", 'bitbucket_refresh_token' => "secret" } } end let(:project) do create( :project, :repository, import_source: project_identifier, import_url: "https://bitbucket.org/#{project_identifier}.git", import_data_attributes: { credentials: data } ) end let(:importer) { described_class.new(project) } let(:sample) { RepoHelpers.sample_compare } let(:issues_statuses_sample_data) do { count: sample_issues_statuses.count, values: sample_issues_statuses } end let(:counter) { double('counter', increment: true) } subject { described_class.new(project) } describe '#import_pull_requests' do let(:source_branch_sha) { sample.commits.last } let(:target_branch_sha) { sample.commits.first } let(:pull_request) do instance_double( Bitbucket::Representation::PullRequest, iid: 10, source_branch_sha: source_branch_sha, source_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.source_branch, target_branch_sha: target_branch_sha, target_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.target_branch, title: 'This is a title', description: 'This is a test pull request', state: 'merged', author: 'other', created_at: Time.now, updated_at: Time.now) end let(:author_line) { "*Created by: someuser*\n\n" } before do allow(subject).to receive(:import_wiki) allow(subject).to receive(:import_issues) # https://gitlab.com/gitlab-org/gitlab-test/compare/c1acaa58bbcbc3eafe538cb8274ba387047b69f8...5937ac0a7beb003549fc5fd26fc247ad @inline_note = instance_double( Bitbucket::Representation::PullRequestComment, iid: 2, file_path: '.gitmodules', old_pos: nil, new_pos: 4, note: 'Hello world', author: 'someuser', created_at: Time.now, updated_at: Time.now, inline?: true, has_parent?: false) @reply = instance_double( Bitbucket::Representation::PullRequestComment, iid: 3, file_path: '.gitmodules', note: 'Hello world', author: 'someuser', created_at: Time.now, updated_at: Time.now, inline?: true, has_parent?: true, parent_id: 2) comments = [@inline_note, @reply] allow(subject.client).to receive(:repo) allow(subject.client).to receive(:pull_requests).and_return([pull_request]) allow(subject.client).to receive(:pull_request_comments).with(anything, pull_request.iid).and_return(comments) end it 'imports threaded discussions' do expect { subject.execute }.to change { MergeRequest.count }.by(1) merge_request = MergeRequest.first expect(merge_request.state).to eq('merged') expect(merge_request.notes.count).to eq(2) expect(merge_request.notes.map(&:discussion_id).uniq.count).to eq(1) notes = merge_request.notes.order(:id).to_a start_note = notes.first expect(start_note).to be_a(DiffNote) expect(start_note.note).to include(@inline_note.note) expect(start_note.note).to include(author_line) reply_note = notes.last expect(reply_note).to be_a(DiffNote) expect(reply_note.note).to include(@reply.note) expect(reply_note.note).to include(author_line) end context 'when user exists in GitLab' do let!(:existing_user) { create(:user, username: 'someuser') } let!(:identity) { create(:identity, provider: 'bitbucket', extern_uid: existing_user.username, user: existing_user) } it 'does not add author line to comments' do expect { subject.execute }.to change { MergeRequest.count }.by(1) merge_request = MergeRequest.first notes = merge_request.notes.order(:id).to_a start_note = notes.first expect(start_note.note).to eq(@inline_note.note) expect(start_note.note).not_to include(author_line) reply_note = notes.last expect(reply_note.note).to eq(@reply.note) expect(reply_note.note).not_to include(author_line) end end context 'when importing a pull request throws an exception' do before do allow(pull_request).to receive(:raw).and_return({ error: "broken" }) allow(subject.client).to receive(:pull_request_comments).and_raise(Gitlab::HTTP::Error) end it 'logs an error without the backtrace' do expect(Gitlab::ErrorTracking).to receive(:log_exception) .with(instance_of(Gitlab::HTTP::Error), hash_including(raw_response: '{"error":"broken"}')) subject.execute expect(subject.errors.count).to eq(1) expect(subject.errors.first.keys).to match_array(%i(type iid errors)) end end context "when branches' sha is not found in the repository" do let(:source_branch_sha) { 'a' * Commit::MIN_SHA_LENGTH } let(:target_branch_sha) { 'b' * Commit::MIN_SHA_LENGTH } it 'uses the pull request sha references' do expect { subject.execute }.to change { MergeRequest.count }.by(1) merge_request_diff = MergeRequest.first.merge_request_diff expect(merge_request_diff.head_commit_sha).to eq source_branch_sha expect(merge_request_diff.start_commit_sha).to eq target_branch_sha end end context 'metrics' do before do allow(Gitlab::Metrics).to receive(:counter) { counter } allow(pull_request).to receive(:raw).and_return('hello world') end it 'counts imported pull requests' do expect(Gitlab::Metrics).to receive(:counter).with( :bitbucket_importer_imported_merge_requests_total, 'The number of imported merge (pull) requests' ) expect(counter).to receive(:increment) subject.execute end end end context 'issues statuses' do before do # HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this Bitbucket::Representation::Issue.new({}) stub_request( :get, "https://api.bitbucket.org/2.0/repositories/#{project_identifier}" ).to_return(status: 200, headers: { "Content-Type" => "application/json" }, body: { has_issues: true, full_name: project_identifier }.to_json) stub_request( :get, "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues?pagelen=50&sort=created_on" ).to_return(status: 200, headers: { "Content-Type" => "application/json" }, body: issues_statuses_sample_data.to_json) stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on") .with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }) .to_return(status: 200, body: "", headers: {}) sample_issues_statuses.each_with_index do |issue, index| stub_request( :get, "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues/#{issue[:id]}/comments?pagelen=50&sort=created_on" ).to_return( status: 200, headers: { "Content-Type" => "application/json" }, body: { author_info: { username: "username" }, utc_created_on: index }.to_json ) end stub_request( :get, "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/pullrequests?pagelen=50&sort=created_on&state=ALL" ).to_return(status: 200, headers: { "Content-Type" => "application/json" }, body: {}.to_json) end context 'creating labels on project' do before do allow(importer).to receive(:import_wiki) end it 'creates labels as expected' do expect { importer.execute }.to change { Label.count }.from(0).to(Gitlab::BitbucketImport::Importer::LABELS.size) end it 'does not fail if label is already existing' do label = Gitlab::BitbucketImport::Importer::LABELS.first ::Labels::CreateService.new(label).execute(project: project) expect { importer.execute }.not_to raise_error end it 'does not create new labels' do Gitlab::BitbucketImport::Importer::LABELS.each do |label| create(:label, project: project, title: label[:title]) end expect { importer.execute }.not_to change { Label.count } end it 'does not update existing ones' do label_title = Gitlab::BitbucketImport::Importer::LABELS.first[:title] existing_label = create(:label, project: project, title: label_title) # Reload label from database so we avoid timestamp comparison issues related to time precision when comparing # attributes later. existing_label.reload travel_to(Time.now + 1.minute) do importer.execute label_after_import = project.labels.find(existing_label.id) expect(label_after_import.attributes).to eq(existing_label.attributes) end end end it 'maps statuses to open or closed' do allow(importer).to receive(:import_wiki) importer.execute expect(project.issues.where(state_id: Issue.available_states[:closed]).size).to eq(5) expect(project.issues.where(state_id: Issue.available_states[:opened]).size).to eq(2) end describe 'wiki import' do it 'is skipped when the wiki exists' do expect(project.wiki).to receive(:repository_exists?) { true } expect(project.wiki.repository).not_to receive(:import_repository) importer.execute expect(importer.errors).to be_empty end it 'imports to the project disk_path' do expect(project.wiki).to receive(:repository_exists?) { false } expect(project.wiki.repository).to receive(:import_repository) importer.execute expect(importer.errors).to be_empty end end describe 'issue import' do it 'maps reporters to anonymous if bitbucket reporter is nil' do allow(importer).to receive(:import_wiki) importer.execute expect(project.issues.size).to eq(7) expect(project.issues.where("description LIKE ?", '%Anonymous%').size).to eq(3) expect(project.issues.where("description LIKE ?", '%reporter1%').size).to eq(2) expect(project.issues.where("description LIKE ?", '%reporter2%').size).to eq(1) expect(project.issues.where("description LIKE ?", '%reporter3%').size).to eq(1) expect(importer.errors).to be_empty end end context 'metrics' do before do allow(Gitlab::Metrics).to receive(:counter) { counter } end it 'counts imported issues' do expect(Gitlab::Metrics).to receive(:counter).with( :bitbucket_importer_imported_issues_total, 'The number of imported issues' ) expect(counter).to receive(:increment) subject.execute end end end describe '#execute' do context 'metrics' do let(:histogram) { double(:histogram) } before do allow(subject).to receive(:import_wiki) allow(subject).to receive(:import_issues) allow(subject).to receive(:import_pull_requests) allow(Gitlab::Metrics).to receive(:counter).and_return(counter) allow(Gitlab::Metrics).to receive(:histogram).and_return(histogram) allow(histogram).to receive(:observe) allow(counter).to receive(:increment) end it 'counts and measures duration of imported projects' do expect(Gitlab::Metrics).to receive(:counter).with( :bitbucket_importer_imported_projects_total, 'The number of imported projects' ) expect(Gitlab::Metrics).to receive(:histogram).with( :bitbucket_importer_total_duration_seconds, 'Total time spent importing projects, in seconds', {}, Gitlab::Import::Metrics::IMPORT_DURATION_BUCKETS ) expect(counter).to receive(:increment) expect(histogram).to receive(:observe).with({ importer: :bitbucket_importer }, anything) subject.execute end end end end