5134 lines
		
	
	
		
			160 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			5134 lines
		
	
	
		
			160 KiB
		
	
	
	
		
			Ruby
		
	
	
	
| # frozen_string_literal: true
 | |
| 
 | |
| require 'spec_helper'
 | |
| 
 | |
| RSpec.describe MergeRequest, factory_default: :keep do
 | |
|   include RepoHelpers
 | |
|   include ProjectForksHelper
 | |
|   include ReactiveCachingHelpers
 | |
| 
 | |
|   using RSpec::Parameterized::TableSyntax
 | |
| 
 | |
|   let_it_be(:namespace) { create_default(:namespace).freeze }
 | |
|   let_it_be(:project, refind: true) { create_default(:project, :repository).freeze }
 | |
| 
 | |
|   subject { create(:merge_request) }
 | |
| 
 | |
|   describe 'associations' do
 | |
|     subject { build_stubbed(:merge_request) }
 | |
| 
 | |
|     it { is_expected.to belong_to(:target_project).class_name('Project') }
 | |
|     it { is_expected.to belong_to(:source_project).class_name('Project') }
 | |
|     it { is_expected.to belong_to(:merge_user).class_name("User") }
 | |
|     it { is_expected.to have_many(:assignees).through(:merge_request_assignees) }
 | |
|     it { is_expected.to have_many(:reviewers).through(:merge_request_reviewers) }
 | |
|     it { is_expected.to have_many(:merge_request_diffs) }
 | |
|     it { is_expected.to have_many(:user_mentions).class_name("MergeRequestUserMention") }
 | |
|     it { is_expected.to belong_to(:milestone) }
 | |
|     it { is_expected.to belong_to(:iteration) }
 | |
|     it { is_expected.to have_many(:resource_milestone_events) }
 | |
|     it { is_expected.to have_many(:resource_state_events) }
 | |
|     it { is_expected.to have_many(:draft_notes) }
 | |
|     it { is_expected.to have_many(:reviews).inverse_of(:merge_request) }
 | |
|     it { is_expected.to have_one(:cleanup_schedule).inverse_of(:merge_request) }
 | |
| 
 | |
|     context 'for forks' do
 | |
|       let!(:project) { create(:project) }
 | |
|       let!(:fork) { fork_project(project) }
 | |
|       let!(:merge_request) { create(:merge_request, target_project: project, source_project: fork) }
 | |
| 
 | |
|       it 'does not load another project due to inverse relationship' do
 | |
|         expect(project.merge_requests.first.target_project.object_id).to eq(project.object_id)
 | |
|       end
 | |
| 
 | |
|       it 'finds the associated merge request' do
 | |
|         expect(project.merge_requests.find(merge_request.id)).to eq(merge_request)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.from_and_to_forks' do
 | |
|     it 'returns only MRs from and to forks (with no internal MRs)' do
 | |
|       project = create(:project)
 | |
|       fork = fork_project(project)
 | |
|       fork_2 = fork_project(project)
 | |
|       mr_from_fork = create(:merge_request, source_project: fork, target_project: project)
 | |
|       mr_to_fork = create(:merge_request, source_project: project, target_project: fork)
 | |
| 
 | |
|       create(:merge_request, source_project: fork, target_project: fork_2)
 | |
|       create(:merge_request, source_project: project, target_project: project)
 | |
| 
 | |
|       expect(described_class.from_and_to_forks(project)).to contain_exactly(mr_from_fork, mr_to_fork)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.order_merged_at_asc' do
 | |
|     let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) }
 | |
|     let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) }
 | |
| 
 | |
|     it 'returns MRs ordered by merged_at ascending' do
 | |
|       expect(described_class.order_merged_at_asc).to eq([older_mr, newer_mr])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.order_merged_at_desc' do
 | |
|     let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) }
 | |
|     let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) }
 | |
| 
 | |
|     it 'returns MRs ordered by merged_at descending' do
 | |
|       expect(described_class.order_merged_at_desc).to eq([newer_mr, older_mr])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.order_closed_at_asc' do
 | |
|     let_it_be(:older_mr) { create(:merge_request, :closed_last_month) }
 | |
|     let_it_be(:newer_mr) { create(:merge_request, :closed_last_month) }
 | |
| 
 | |
|     it 'returns MRs ordered by closed_at ascending' do
 | |
|       expect(described_class.order_closed_at_asc).to eq([older_mr, newer_mr])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.order_closed_at_desc' do
 | |
|     let_it_be(:older_mr) { create(:merge_request, :closed_last_month) }
 | |
|     let_it_be(:newer_mr) { create(:merge_request, :closed_last_month) }
 | |
| 
 | |
|     it 'returns MRs ordered by closed_at descending' do
 | |
|       expect(described_class.order_closed_at_desc).to eq([newer_mr, older_mr])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.with_jira_issue_keys' do
 | |
|     let_it_be(:mr_with_jira_title) { create(:merge_request, :unique_branches, title: 'Fix TEST-123') }
 | |
|     let_it_be(:mr_with_jira_description) { create(:merge_request, :unique_branches, description: 'this closes TEST-321') }
 | |
|     let_it_be(:mr_without_jira_reference) { create(:merge_request, :unique_branches) }
 | |
| 
 | |
|     subject { described_class.with_jira_issue_keys }
 | |
| 
 | |
|     it { is_expected.to contain_exactly(mr_with_jira_title, mr_with_jira_description) }
 | |
| 
 | |
|     it { is_expected.not_to include(mr_without_jira_reference) }
 | |
|   end
 | |
| 
 | |
|   context 'scopes' do
 | |
|     let_it_be(:user1) { create(:user) }
 | |
|     let_it_be(:user2) { create(:user) }
 | |
| 
 | |
|     let_it_be(:merge_request1) { create(:merge_request, :unique_branches, reviewers: [user1])}
 | |
|     let_it_be(:merge_request2) { create(:merge_request, :unique_branches, reviewers: [user2])}
 | |
|     let_it_be(:merge_request3) { create(:merge_request, :unique_branches, reviewers: [])}
 | |
|     let_it_be(:merge_request4) { create(:merge_request, :draft_merge_request)}
 | |
| 
 | |
|     describe '.review_requested' do
 | |
|       it 'returns MRs that have any review requests' do
 | |
|         expect(described_class.review_requested).to eq([merge_request1, merge_request2])
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe '.no_review_requested' do
 | |
|       it 'returns MRs that have no review requests' do
 | |
|         expect(described_class.no_review_requested).to eq([merge_request3, merge_request4])
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe '.review_requested_to' do
 | |
|       it 'returns MRs that the user has been requested to review' do
 | |
|         expect(described_class.review_requested_to(user1)).to eq([merge_request1])
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe '.no_review_requested_to' do
 | |
|       it 'returns MRs that the user has not been requested to review' do
 | |
|         expect(described_class.no_review_requested_to(user1))
 | |
|           .to eq([merge_request2, merge_request3, merge_request4])
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe '.attention' do
 | |
|       let_it_be(:merge_request5) { create(:merge_request, :unique_branches, assignees: [user2])}
 | |
|       let_it_be(:merge_request6) { create(:merge_request, :unique_branches, assignees: [user2])}
 | |
| 
 | |
|       before do
 | |
|         assignee = merge_request6.find_assignee(user2)
 | |
|         assignee.update!(state: :reviewed)
 | |
|         merge_request2.find_reviewer(user2).update!(state: :attention_requested)
 | |
|         merge_request5.find_assignee(user2).update!(state: :attention_requested)
 | |
|       end
 | |
| 
 | |
|       it 'returns MRs that have any attention requests' do
 | |
|         expect(described_class.attention(user2)).to eq([merge_request2, merge_request5])
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe '.drafts' do
 | |
|       it 'returns MRs where draft == true' do
 | |
|         expect(described_class.drafts).to eq([merge_request4])
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#squash?' do
 | |
|     let(:merge_request) { build(:merge_request, squash: squash) }
 | |
| 
 | |
|     subject { merge_request.squash? }
 | |
| 
 | |
|     context 'disabled in database' do
 | |
|       let(:squash) { false }
 | |
| 
 | |
|       it { is_expected.to be_falsy }
 | |
|     end
 | |
| 
 | |
|     context 'enabled in database' do
 | |
|       let(:squash) { true }
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#default_squash_commit_message' do
 | |
|     let(:project) { subject.project }
 | |
|     let(:is_multiline) { -> (c) { c.description.present? } }
 | |
|     let(:multiline_commits) { subject.commits.select(&is_multiline) }
 | |
|     let(:singleline_commits) { subject.commits.reject(&is_multiline) }
 | |
| 
 | |
|     it 'returns the merge request title' do
 | |
|       expect(subject.default_squash_commit_message).to eq(subject.title)
 | |
|     end
 | |
| 
 | |
|     it 'uses template from target project' do
 | |
|       subject.target_project.squash_commit_template = 'Squashed branch %{source_branch} into %{target_branch}'
 | |
| 
 | |
|       expect(subject.default_squash_commit_message)
 | |
|         .to eq('Squashed branch master into feature')
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe 'modules' do
 | |
|     subject { described_class }
 | |
| 
 | |
|     it { is_expected.to include_module(Issuable) }
 | |
|     it { is_expected.to include_module(Referable) }
 | |
|     it { is_expected.to include_module(Sortable) }
 | |
|     it { is_expected.to include_module(Taskable) }
 | |
|     it { is_expected.to include_module(MilestoneEventable) }
 | |
|     it { is_expected.to include_module(StateEventable) }
 | |
| 
 | |
|     it_behaves_like 'AtomicInternalId' do
 | |
|       let(:internal_id_attribute) { :iid }
 | |
|       let(:instance) { build(:merge_request) }
 | |
|       let(:scope) { :target_project }
 | |
|       let(:scope_attrs) { { project: instance.target_project } }
 | |
|       let(:usage) { :merge_requests }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe 'validation' do
 | |
|     subject { build_stubbed(:merge_request) }
 | |
| 
 | |
|     it { is_expected.to validate_presence_of(:target_branch) }
 | |
|     it { is_expected.to validate_presence_of(:source_branch) }
 | |
| 
 | |
|     context "Validation of merge user with Merge When Pipeline Succeeds" do
 | |
|       it "allows user to be nil when the feature is disabled" do
 | |
|         expect(subject).to be_valid
 | |
|       end
 | |
| 
 | |
|       it "is invalid without merge user" do
 | |
|         subject.merge_when_pipeline_succeeds = true
 | |
|         expect(subject).not_to be_valid
 | |
|       end
 | |
| 
 | |
|       it "is valid with merge user" do
 | |
|         subject.merge_when_pipeline_succeeds = true
 | |
|         subject.merge_user = build(:user)
 | |
| 
 | |
|         expect(subject).to be_valid
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'for branch' do
 | |
|       before do
 | |
|         stub_feature_flags(stricter_mr_branch_name: false)
 | |
|       end
 | |
| 
 | |
|       where(:branch_name, :valid) do
 | |
|         'foo' | true
 | |
|         'foo:bar' | false
 | |
|         '+foo:bar' | false
 | |
|         'foo bar' | false
 | |
|         '-foo' | false
 | |
|         'HEAD' | true
 | |
|         'refs/heads/master' | true
 | |
|       end
 | |
| 
 | |
|       with_them do
 | |
|         it "validates source_branch" do
 | |
|           subject = build(:merge_request, source_branch: branch_name, target_branch: 'master')
 | |
|           subject.valid?
 | |
| 
 | |
|           expect(subject.errors.added?(:source_branch)).to eq(!valid)
 | |
|         end
 | |
| 
 | |
|         it "validates target_branch" do
 | |
|           subject = build(:merge_request, source_branch: 'master', target_branch: branch_name)
 | |
|           subject.valid?
 | |
| 
 | |
|           expect(subject.errors.added?(:target_branch)).to eq(!valid)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'for forks' do
 | |
|       let(:project) { create(:project) }
 | |
|       let(:fork1) { fork_project(project) }
 | |
|       let(:fork2) { fork_project(project) }
 | |
| 
 | |
|       it 'allows merge requests for sibling-forks' do
 | |
|         subject.source_project = fork1
 | |
|         subject.target_project = fork2
 | |
| 
 | |
|         expect(subject).to be_valid
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe 'callbacks' do
 | |
|     describe '#ensure_merge_request_metrics' do
 | |
|       let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|       it 'creates metrics after saving' do
 | |
|         expect(merge_request.metrics).to be_persisted
 | |
|         expect(MergeRequest::Metrics.count).to eq(1)
 | |
|       end
 | |
| 
 | |
|       it 'does not duplicate metrics for a merge request' do
 | |
|         merge_request.mark_as_merged!
 | |
| 
 | |
|         expect(MergeRequest::Metrics.count).to eq(1)
 | |
|       end
 | |
| 
 | |
|       it 'does not create duplicated metrics records when MR is concurrently updated' do
 | |
|         merge_request.metrics.destroy!
 | |
| 
 | |
|         instance1 = MergeRequest.find(merge_request.id)
 | |
|         instance2 = MergeRequest.find(merge_request.id)
 | |
| 
 | |
|         instance1.ensure_metrics
 | |
|         instance2.ensure_metrics
 | |
| 
 | |
|         metrics_records = MergeRequest::Metrics.where(merge_request_id: merge_request.id)
 | |
|         expect(metrics_records.size).to eq(1)
 | |
|       end
 | |
| 
 | |
|       it 'syncs the `target_project_id` to the metrics record' do
 | |
|         project = create(:project)
 | |
| 
 | |
|         merge_request.update!(target_project: project, state: :closed)
 | |
| 
 | |
|         expect(merge_request.target_project_id).to eq(project.id)
 | |
|         expect(merge_request.target_project_id).to eq(merge_request.metrics.target_project_id)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe '#set_draft_status' do
 | |
|       let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|       context 'MR is a draft' do
 | |
|         before do
 | |
|           expect(merge_request.draft).to be_falsy
 | |
| 
 | |
|           merge_request.title = "Draft: #{merge_request.title}"
 | |
|         end
 | |
| 
 | |
|         it 'sets draft to true' do
 | |
|           merge_request.save!
 | |
| 
 | |
|           expect(merge_request.draft).to be_truthy
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'MR is not a draft' do
 | |
|         before do
 | |
|           expect(merge_request.draft).to be_falsey
 | |
| 
 | |
|           merge_request.title = "This is not a draft"
 | |
|         end
 | |
| 
 | |
|         it 'sets draft to true' do
 | |
|           merge_request.save!
 | |
| 
 | |
|           expect(merge_request.draft).to be_falsey
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe 'respond to' do
 | |
|     subject { build(:merge_request) }
 | |
| 
 | |
|     it { is_expected.to respond_to(:unchecked?) }
 | |
|     it { is_expected.to respond_to(:checking?) }
 | |
|     it { is_expected.to respond_to(:can_be_merged?) }
 | |
|     it { is_expected.to respond_to(:cannot_be_merged?) }
 | |
|     it { is_expected.to respond_to(:merge_params) }
 | |
|     it { is_expected.to respond_to(:merge_when_pipeline_succeeds) }
 | |
|   end
 | |
| 
 | |
|   describe '.by_commit_sha' do
 | |
|     subject(:by_commit_sha) { described_class.by_commit_sha(sha) }
 | |
| 
 | |
|     let!(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|     context 'with sha contained in latest merge request diff' do
 | |
|       let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
 | |
| 
 | |
|       it 'returns merge requests' do
 | |
|         expect(by_commit_sha).to eq([merge_request])
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'with sha contained not in latest merge request diff' do
 | |
|       let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
 | |
| 
 | |
|       it 'returns empty requests' do
 | |
|         latest_merge_request_diff = merge_request.merge_request_diffs.create!
 | |
| 
 | |
|         MergeRequestDiffCommit.where(
 | |
|           merge_request_diff_id: latest_merge_request_diff,
 | |
|           sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
 | |
|         ).delete_all
 | |
| 
 | |
|         expect(by_commit_sha).to be_empty
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'with sha not contained in' do
 | |
|       let(:sha) { 'b83d6e3' }
 | |
| 
 | |
|       it 'returns empty result' do
 | |
|         expect(by_commit_sha).to be_empty
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.by_merge_commit_sha' do
 | |
|     it 'returns merge requests that match the given merge commit' do
 | |
|       mr = create(:merge_request, :merged, merge_commit_sha: '123abc')
 | |
| 
 | |
|       create(:merge_request, :merged, merge_commit_sha: '123def')
 | |
| 
 | |
|       expect(described_class.by_merge_commit_sha('123abc')).to eq([mr])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.by_squash_commit_sha' do
 | |
|     subject { described_class.by_squash_commit_sha(sha) }
 | |
| 
 | |
|     let(:sha) { '123abc' }
 | |
|     let(:merge_request) { create(:merge_request, :merged, squash_commit_sha: sha) }
 | |
| 
 | |
|     it 'returns merge requests that match the given squash commit' do
 | |
|       is_expected.to eq([merge_request])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.by_merge_or_squash_commit_sha' do
 | |
|     subject { described_class.by_merge_or_squash_commit_sha([sha1, sha2]) }
 | |
| 
 | |
|     let(:sha1) { '123abc' }
 | |
|     let(:sha2) { '456abc' }
 | |
|     let(:mr1) { create(:merge_request, :merged, squash_commit_sha: sha1) }
 | |
|     let(:mr2) { create(:merge_request, :merged, merge_commit_sha: sha2) }
 | |
| 
 | |
|     it 'returns merge requests that match the given squash and merge commits' do
 | |
|       is_expected.to include(mr1, mr2)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.join_metrics' do
 | |
|     let_it_be(:join_condition) { '"merge_request_metrics"."target_project_id" = 1' }
 | |
| 
 | |
|     context 'when a no target_project_id is available' do
 | |
|       it 'moves target_project_id condition to the merge request metrics' do
 | |
|         expect(described_class.join_metrics(1).to_sql).to include(join_condition)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when a target_project_id is present in the where conditions' do
 | |
|       it 'moves target_project_id condition to the merge request metrics' do
 | |
|         expect(described_class.where(target_project_id: 1).join_metrics.to_sql).to include(join_condition)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.by_related_commit_sha' do
 | |
|     subject { described_class.by_related_commit_sha(sha) }
 | |
| 
 | |
|     context 'when commit is a squash commit' do
 | |
|       let!(:merge_request) { create(:merge_request, :merged, squash_commit_sha: sha) }
 | |
|       let(:sha) { '123abc' }
 | |
| 
 | |
|       it { is_expected.to eq([merge_request]) }
 | |
|     end
 | |
| 
 | |
|     context 'when commit is a part of the merge request' do
 | |
|       let!(:merge_request) { create(:merge_request) }
 | |
|       let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
 | |
| 
 | |
|       it { is_expected.to eq([merge_request]) }
 | |
|     end
 | |
| 
 | |
|     context 'when commit is a merge commit' do
 | |
|       let!(:merge_request) { create(:merge_request, :merged, merge_commit_sha: sha) }
 | |
|       let(:sha) { '123abc' }
 | |
| 
 | |
|       it { is_expected.to eq([merge_request]) }
 | |
|     end
 | |
| 
 | |
|     context 'when commit is not found' do
 | |
|       let(:sha) { '0000' }
 | |
| 
 | |
|       it { is_expected.to be_empty }
 | |
|     end
 | |
| 
 | |
|     context 'when commit is part of the merge request and a squash commit at the same time' do
 | |
|       let!(:merge_request) { create(:merge_request) }
 | |
|       let(:sha) { merge_request.commits.first.id }
 | |
| 
 | |
|       before do
 | |
|         merge_request.update!(squash_commit_sha: sha)
 | |
|       end
 | |
| 
 | |
|       it { is_expected.to eq([merge_request]) }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.in_projects' do
 | |
|     it 'returns the merge requests for a set of projects' do
 | |
|       expect(described_class.in_projects(Project.all)).to eq([subject])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.set_latest_merge_request_diff_ids!' do
 | |
|     def create_merge_request_with_diffs(source_branch, diffs: 2)
 | |
|       params = {
 | |
|         target_project: project,
 | |
|         target_branch: 'master',
 | |
|         source_project: project,
 | |
|         source_branch: source_branch
 | |
|       }
 | |
| 
 | |
|       create(:merge_request, params).tap do |mr|
 | |
|         diffs.times { mr.merge_request_diffs.create! }
 | |
|         mr.create_merge_head_diff
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     let(:project) { create(:project) }
 | |
| 
 | |
|     it 'sets IDs for merge requests, whether they are already set or not' do
 | |
|       merge_requests = [
 | |
|         create_merge_request_with_diffs('feature'),
 | |
|         create_merge_request_with_diffs('feature-conflict'),
 | |
|         create_merge_request_with_diffs('wip', diffs: 0),
 | |
|         create_merge_request_with_diffs('csv')
 | |
|       ]
 | |
| 
 | |
|       merge_requests.take(2).each do |merge_request|
 | |
|         merge_request.update_column(:latest_merge_request_diff_id, nil)
 | |
|       end
 | |
| 
 | |
|       expected = merge_requests.map do |merge_request|
 | |
|         merge_request.merge_request_diffs.maximum(:id)
 | |
|       end
 | |
| 
 | |
|       expect { project.merge_requests.set_latest_merge_request_diff_ids! }
 | |
|         .to change { merge_requests.map { |mr| mr.reload.latest_merge_request_diff_id } }.to(expected)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.recent_target_branches' do
 | |
|     let(:project) { create(:project) }
 | |
|     let!(:merge_request1) { create(:merge_request, :opened, source_project: project, target_branch: 'feature') }
 | |
|     let!(:merge_request2) { create(:merge_request, :closed, source_project: project, target_branch: 'merge-test') }
 | |
|     let!(:merge_request3) { create(:merge_request, :opened, source_project: project, target_branch: 'fix') }
 | |
|     let!(:merge_request4) { create(:merge_request, :closed, source_project: project, target_branch: 'feature') }
 | |
| 
 | |
|     before do
 | |
|       merge_request1.update_columns(updated_at: 1.day.since)
 | |
|       merge_request2.update_columns(updated_at: 2.days.since)
 | |
|       merge_request3.update_columns(updated_at: 3.days.since)
 | |
|       merge_request4.update_columns(updated_at: 4.days.since)
 | |
|     end
 | |
| 
 | |
|     it 'returns target branches sort by updated at desc' do
 | |
|       expect(described_class.recent_target_branches).to match_array(%w[feature merge-test fix])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.sort_by_attribute' do
 | |
|     context 'merged_at' do
 | |
|       let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) }
 | |
|       let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) }
 | |
| 
 | |
|       it 'sorts asc' do
 | |
|         merge_requests = described_class.sort_by_attribute(:merged_at_asc)
 | |
|         expect(merge_requests).to eq([older_mr, newer_mr])
 | |
|       end
 | |
| 
 | |
|       it 'sorts desc' do
 | |
|         merge_requests = described_class.sort_by_attribute(:merged_at_desc)
 | |
|         expect(merge_requests).to eq([newer_mr, older_mr])
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'closed_at' do
 | |
|       let_it_be(:older_mr) { create(:merge_request, :closed_last_month) }
 | |
|       let_it_be(:newer_mr) { create(:merge_request, :closed_last_month) }
 | |
| 
 | |
|       it 'sorts asc' do
 | |
|         merge_requests = described_class.sort_by_attribute(:closed_at_asc)
 | |
|         expect(merge_requests).to eq([older_mr, newer_mr])
 | |
|       end
 | |
| 
 | |
|       it 'sorts desc' do
 | |
|         merge_requests = described_class.sort_by_attribute(:closed_at_desc)
 | |
|         expect(merge_requests).to eq([newer_mr, older_mr])
 | |
|       end
 | |
| 
 | |
|       it 'sorts asc when its closed_at' do
 | |
|         merge_requests = described_class.sort_by_attribute(:closed_at)
 | |
|         expect(merge_requests).to eq([older_mr, newer_mr])
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'title' do
 | |
|       let_it_be(:first_mr) { create(:merge_request, :closed, title: 'One') }
 | |
|       let_it_be(:second_mr) { create(:merge_request, :closed, title: 'Two') }
 | |
| 
 | |
|       it 'sorts asc' do
 | |
|         merge_requests = described_class.sort_by_attribute(:title_asc)
 | |
|         expect(merge_requests).to eq([first_mr, second_mr])
 | |
|       end
 | |
| 
 | |
|       it 'sorts desc' do
 | |
|         merge_requests = described_class.sort_by_attribute(:title_desc)
 | |
|         expect(merge_requests).to eq([second_mr, first_mr])
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe 'time to merge calculations' do
 | |
|     let_it_be(:user) { create(:user) }
 | |
|     let_it_be(:project) { create(:project) }
 | |
| 
 | |
|     let!(:mr1) do
 | |
|       create(
 | |
|         :merge_request,
 | |
|         :with_merged_metrics,
 | |
|         source_project: project,
 | |
|         target_project: project
 | |
|       )
 | |
|     end
 | |
| 
 | |
|     let!(:mr2) do
 | |
|       create(
 | |
|         :merge_request,
 | |
|         :with_merged_metrics,
 | |
|         source_project: project,
 | |
|         target_project: project
 | |
|       )
 | |
|     end
 | |
| 
 | |
|     let!(:mr3) do
 | |
|       create(
 | |
|         :merge_request,
 | |
|         :with_merged_metrics,
 | |
|         source_project: project,
 | |
|         target_project: project
 | |
|       )
 | |
|     end
 | |
| 
 | |
|     let!(:unmerged_mr) do
 | |
|       create(
 | |
|         :merge_request,
 | |
|         source_project: project,
 | |
|         target_project: project
 | |
|       )
 | |
|     end
 | |
| 
 | |
|     before do
 | |
|       project.add_user(user, :developer)
 | |
|     end
 | |
| 
 | |
|     describe '.total_time_to_merge' do
 | |
|       it 'returns the sum of the time to merge for all merged MRs' do
 | |
|         mrs = project.merge_requests
 | |
| 
 | |
|         expect(mrs.total_time_to_merge).to be_within(1).of(expected_total_time(mrs))
 | |
|       end
 | |
| 
 | |
|       context 'when merged_at is earlier than created_at' do
 | |
|         before do
 | |
|           mr1.metrics.update!(merged_at: mr1.metrics.created_at - 1.week)
 | |
|         end
 | |
| 
 | |
|         it 'returns nil' do
 | |
|           mrs = project.merge_requests.where(id: mr1.id)
 | |
| 
 | |
|           expect(mrs.total_time_to_merge).to be_nil
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def expected_total_time(mrs)
 | |
|         mrs = mrs.reject { |mr| mr.merged_at.nil? }
 | |
|         mrs.reduce(0.0) do |sum, mr|
 | |
|           (mr.merged_at - mr.created_at) + sum
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#target_branch_sha' do
 | |
|     let(:project) { create(:project, :repository) }
 | |
| 
 | |
|     subject { create(:merge_request, source_project: project, target_project: project) }
 | |
| 
 | |
|     context 'when the target branch does not exist' do
 | |
|       before do
 | |
|         project.repository.rm_branch(subject.author, subject.target_branch)
 | |
|         subject.clear_memoized_shas
 | |
|       end
 | |
| 
 | |
|       it 'returns nil' do
 | |
|         expect(subject.target_branch_sha).to be_nil
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     it 'returns memoized value' do
 | |
|       subject.target_branch_sha = '8ffb3c15a5475e59ae909384297fede4badcb4c7'
 | |
| 
 | |
|       expect(subject.target_branch_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7'
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#card_attributes' do
 | |
|     it 'includes the author name' do
 | |
|       allow(subject).to receive(:author).and_return(double(name: 'Robert'))
 | |
|       allow(subject).to receive(:assignees).and_return([])
 | |
| 
 | |
|       expect(subject.card_attributes)
 | |
|         .to eq({ 'Author' => 'Robert', 'Assignee' => "" })
 | |
|     end
 | |
| 
 | |
|     it 'includes the assignees name' do
 | |
|       allow(subject).to receive(:author).and_return(double(name: 'Robert'))
 | |
|       allow(subject).to receive(:assignees).and_return([double(name: 'Douwe'), double(name: 'Robert')])
 | |
| 
 | |
|       expect(subject.card_attributes)
 | |
|         .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe and Robert' })
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#assignee_or_author?' do
 | |
|     let(:user) { create(:user) }
 | |
| 
 | |
|     it 'returns true for a user that is assigned to a merge request' do
 | |
|       subject.assignees = [user]
 | |
| 
 | |
|       expect(subject.assignee_or_author?(user)).to eq(true)
 | |
|     end
 | |
| 
 | |
|     it 'returns true for a user that is the author of a merge request' do
 | |
|       subject.author = user
 | |
| 
 | |
|       expect(subject.assignee_or_author?(user)).to eq(true)
 | |
|     end
 | |
| 
 | |
|     it 'returns false for a user that is not the assignee or author' do
 | |
|       expect(subject.assignee_or_author?(user)).to eq(false)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#visible_closing_issues_for' do
 | |
|     let(:guest) { create(:user) }
 | |
|     let(:developer) { create(:user) }
 | |
|     let(:issue_1) { create(:issue, project: subject.source_project) }
 | |
|     let(:issue_2) { create(:issue, project: subject.source_project) }
 | |
|     let(:confidential_issue) { create(:issue, :confidential, project: subject.source_project) }
 | |
| 
 | |
|     before do
 | |
|       subject.project.add_developer(subject.author)
 | |
|       subject.target_branch = subject.project.default_branch
 | |
|       commit = double('commit1', safe_message: "Fixes #{issue_1.to_reference} #{issue_2.to_reference} #{confidential_issue.to_reference}")
 | |
|       allow(subject).to receive(:commits).and_return([commit])
 | |
|     end
 | |
| 
 | |
|     it 'shows only allowed issues to guest' do
 | |
|       subject.project.add_guest(guest)
 | |
| 
 | |
|       subject.cache_merge_request_closes_issues!
 | |
| 
 | |
|       expect(subject.visible_closing_issues_for(guest)).to match_array([issue_1, issue_2])
 | |
|     end
 | |
| 
 | |
|     it 'shows only allowed issues to developer' do
 | |
|       subject.project.add_developer(developer)
 | |
| 
 | |
|       subject.cache_merge_request_closes_issues!
 | |
| 
 | |
|       expect(subject.visible_closing_issues_for(developer)).to match_array([issue_1, confidential_issue, issue_2])
 | |
|     end
 | |
| 
 | |
|     context 'when external issue tracker is enabled' do
 | |
|       let(:project) { create(:project, :repository) }
 | |
| 
 | |
|       subject { create(:merge_request, source_project: project) }
 | |
| 
 | |
|       before do
 | |
|         subject.project.has_external_issue_tracker = true
 | |
|         subject.project.save!
 | |
|       end
 | |
| 
 | |
|       it 'calls non #closes_issues to retrieve data' do
 | |
|         expect(subject).to receive(:closes_issues)
 | |
|         expect(subject).not_to receive(:cached_closes_issues)
 | |
| 
 | |
|         subject.visible_closing_issues_for
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#cache_merge_request_closes_issues!' do
 | |
|     before do
 | |
|       subject.project.add_developer(subject.author)
 | |
|       subject.target_branch = subject.project.default_branch
 | |
|     end
 | |
| 
 | |
|     it 'caches closed issues' do
 | |
|       issue  = create :issue, project: subject.project
 | |
|       commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
 | |
|       allow(subject).to receive(:commits).and_return([commit])
 | |
| 
 | |
|       expect { subject.cache_merge_request_closes_issues!(subject.author) }.to change(subject.merge_requests_closing_issues, :count).by(1)
 | |
|     end
 | |
| 
 | |
|     it 'does not cache closed issues when merge request is closed' do
 | |
|       issue  = create :issue, project: subject.project
 | |
|       commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
 | |
| 
 | |
|       allow(subject).to receive(:commits).and_return([commit])
 | |
|       allow(subject).to receive(:state_id).and_return(described_class.available_states[:closed])
 | |
| 
 | |
|       expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
 | |
|     end
 | |
| 
 | |
|     it 'does not cache closed issues when merge request is merged' do
 | |
|       issue  = create :issue, project: subject.project
 | |
|       commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
 | |
|       allow(subject).to receive(:commits).and_return([commit])
 | |
|       allow(subject).to receive(:state_id).and_return(described_class.available_states[:merged])
 | |
| 
 | |
|       expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
 | |
|     end
 | |
| 
 | |
|     context 'when both internal and external issue trackers are enabled' do
 | |
|       before do
 | |
|         create(:jira_integration, project: subject.project)
 | |
|         subject.project.reload
 | |
|       end
 | |
| 
 | |
|       it 'does not cache issues from external trackers' do
 | |
|         issue  = ExternalIssue.new('JIRA-123', subject.project)
 | |
|         commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
 | |
|         allow(subject).to receive(:commits).and_return([commit])
 | |
| 
 | |
|         expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to raise_error
 | |
|         expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
 | |
|       end
 | |
| 
 | |
|       it 'caches an internal issue' do
 | |
|         issue  = create(:issue, project: subject.project)
 | |
|         commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
 | |
|         allow(subject).to receive(:commits).and_return([commit])
 | |
| 
 | |
|         expect { subject.cache_merge_request_closes_issues!(subject.author) }
 | |
|           .to change(subject.merge_requests_closing_issues, :count).by(1)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when only external issue tracker enabled' do
 | |
|       let(:project) { create(:project, :repository) }
 | |
| 
 | |
|       subject { create(:merge_request, source_project: project) }
 | |
| 
 | |
|       before do
 | |
|         subject.project.has_external_issue_tracker = true
 | |
|         subject.project.issues_enabled = false
 | |
|         subject.project.save!
 | |
|       end
 | |
| 
 | |
|       it 'does not cache issues from external trackers' do
 | |
|         issue  = ExternalIssue.new('JIRA-123', subject.project)
 | |
|         commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
 | |
|         allow(subject).to receive(:commits).and_return([commit])
 | |
| 
 | |
|         expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
 | |
|       end
 | |
| 
 | |
|       it 'does not cache an internal issue' do
 | |
|         issue  = create(:issue, project: subject.project)
 | |
|         commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
 | |
|         allow(subject).to receive(:commits).and_return([commit])
 | |
| 
 | |
|         expect { subject.cache_merge_request_closes_issues!(subject.author) }
 | |
|           .not_to change(subject.merge_requests_closing_issues, :count)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#source_branch_sha' do
 | |
|     let(:last_branch_commit) { subject.source_project.repository.commit(Gitlab::Git::BRANCH_REF_PREFIX + subject.source_branch) }
 | |
| 
 | |
|     context 'with diffs' do
 | |
|       subject { create(:merge_request) }
 | |
| 
 | |
|       it 'returns the sha of the source branch last commit' do
 | |
|         expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'without diffs' do
 | |
|       subject { create(:merge_request, :without_diffs) }
 | |
| 
 | |
|       it 'returns the sha of the source branch last commit' do
 | |
|         expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
 | |
|       end
 | |
| 
 | |
|       context 'when there is a tag name matching the branch name' do
 | |
|         let(:tag_name) { subject.source_branch }
 | |
| 
 | |
|         it 'returns the sha of the source branch last commit' do
 | |
|           subject.source_project.repository.add_tag(subject.author,
 | |
|                                                     tag_name,
 | |
|                                                     subject.target_branch_sha,
 | |
|                                                     'Add a tag')
 | |
| 
 | |
|           expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
 | |
| 
 | |
|           subject.source_project.repository.rm_tag(subject.author, tag_name)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when the merge request is being created' do
 | |
|       subject { build(:merge_request, source_branch: nil, compare_commits: []) }
 | |
| 
 | |
|       it 'returns nil' do
 | |
|         expect(subject.source_branch_sha).to be_nil
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     it 'returns memoized value' do
 | |
|       subject.source_branch_sha = '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'
 | |
| 
 | |
|       expect(subject.source_branch_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#to_reference' do
 | |
|     let(:project) { build(:project, name: 'sample-project') }
 | |
|     let(:merge_request) { build(:merge_request, target_project: project, iid: 1) }
 | |
| 
 | |
|     it 'returns a String reference to the object' do
 | |
|       expect(merge_request.to_reference).to eq "!1"
 | |
|     end
 | |
| 
 | |
|     it 'supports a cross-project reference' do
 | |
|       another_project = build(:project, name: 'another-project', namespace: project.namespace)
 | |
|       expect(merge_request.to_reference(another_project)).to eq "sample-project!1"
 | |
|     end
 | |
| 
 | |
|     it 'returns a String reference with the full path' do
 | |
|       expect(merge_request.to_reference(full: true)).to eq(project.full_path + '!1')
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#raw_diffs' do
 | |
|     let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }
 | |
| 
 | |
|     context 'when there are MR diffs' do
 | |
|       let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|       it 'delegates to the MR diffs' do
 | |
|         expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(options)
 | |
| 
 | |
|         merge_request.raw_diffs(options)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when there are no MR diffs' do
 | |
|       let(:merge_request) { build(:merge_request) }
 | |
| 
 | |
|       it 'delegates to the compare object' do
 | |
|         merge_request.compare = double(:compare)
 | |
| 
 | |
|         expect(merge_request.compare).to receive(:raw_diffs).with(options)
 | |
| 
 | |
|         merge_request.raw_diffs(options)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#diffs' do
 | |
|     let(:merge_request) { build(:merge_request) }
 | |
|     let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }
 | |
| 
 | |
|     context 'when there are MR diffs' do
 | |
|       it 'delegates to the MR diffs' do
 | |
|         merge_request.save!
 | |
| 
 | |
|         expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options)).and_call_original
 | |
| 
 | |
|         merge_request.diffs(options).diff_files
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when there are no MR diffs' do
 | |
|       it 'delegates to the compare object, setting expanded: true' do
 | |
|         merge_request.compare = double(:compare)
 | |
| 
 | |
|         expect(merge_request.compare).to receive(:diffs).with(options.merge(expanded: true))
 | |
| 
 | |
|         merge_request.diffs(options)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#note_positions_for_paths' do
 | |
|     let(:user) { create(:user) }
 | |
|     let(:merge_request) { create(:merge_request) }
 | |
|     let(:project) { merge_request.project }
 | |
|     let!(:diff_note) do
 | |
|       create(:diff_note_on_merge_request, project: project, noteable: merge_request)
 | |
|     end
 | |
| 
 | |
|     let!(:draft_note) do
 | |
|       create(:draft_note_on_text_diff, author: user, merge_request: merge_request)
 | |
|     end
 | |
| 
 | |
|     let(:file_paths) { merge_request.diffs.diff_files.map(&:file_path) }
 | |
| 
 | |
|     subject do
 | |
|       merge_request.note_positions_for_paths(file_paths)
 | |
|     end
 | |
| 
 | |
|     it 'returns a Gitlab::Diff::PositionCollection' do
 | |
|       expect(subject).to be_a(Gitlab::Diff::PositionCollection)
 | |
|     end
 | |
| 
 | |
|     context 'within all diff files' do
 | |
|       it 'returns correct positions' do
 | |
|         expect(subject).to match_array([diff_note.position])
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'within specific diff file' do
 | |
|       let(:file_paths) { [diff_note.position.file_path] }
 | |
| 
 | |
|       it 'returns correct positions' do
 | |
|         expect(subject).to match_array([diff_note.position])
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'within no diff files' do
 | |
|       let(:file_paths) { [] }
 | |
| 
 | |
|       it 'returns no positions' do
 | |
|         expect(subject.to_a).to be_empty
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when user is given' do
 | |
|       subject do
 | |
|         merge_request.note_positions_for_paths(file_paths, user)
 | |
|       end
 | |
| 
 | |
|       it 'returns notes and draft notes positions' do
 | |
|         expect(subject).to match_array([draft_note.position, diff_note.position])
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when user is not given' do
 | |
|       subject do
 | |
|         merge_request.note_positions_for_paths(file_paths)
 | |
|       end
 | |
| 
 | |
|       it 'returns notes positions' do
 | |
|         expect(subject).to match_array([diff_note.position])
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#discussions_diffs' do
 | |
|     let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|     shared_examples 'discussions diffs collection' do
 | |
|       it 'initializes Gitlab::DiscussionsDiff::FileCollection with correct data' do
 | |
|         note_diff_file = diff_note.note_diff_file
 | |
| 
 | |
|         expect(Gitlab::DiscussionsDiff::FileCollection)
 | |
|           .to receive(:new)
 | |
|           .with([note_diff_file])
 | |
|           .and_call_original
 | |
| 
 | |
|         result = merge_request.discussions_diffs
 | |
| 
 | |
|         expect(result).to be_a(Gitlab::DiscussionsDiff::FileCollection)
 | |
|       end
 | |
| 
 | |
|       it 'eager loads relations' do
 | |
|         result = merge_request.discussions_diffs
 | |
| 
 | |
|         recorder = ActiveRecord::QueryRecorder.new do
 | |
|           result.first.diff_note
 | |
|           result.first.diff_note.project
 | |
|         end
 | |
| 
 | |
|         expect(recorder.count).to be_zero
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'with commit diff note' do
 | |
|       let(:other_merge_request) { create(:merge_request, source_project: create(:project, :repository)) }
 | |
| 
 | |
|       let!(:diff_note) do
 | |
|         create(:diff_note_on_commit, project: merge_request.project)
 | |
|       end
 | |
| 
 | |
|       let!(:other_mr_diff_note) do
 | |
|         create(:diff_note_on_commit, project: other_merge_request.project)
 | |
|       end
 | |
| 
 | |
|       it_behaves_like 'discussions diffs collection'
 | |
|     end
 | |
| 
 | |
|     context 'with merge request diff note' do
 | |
|       let!(:diff_note) do
 | |
|         create(:diff_note_on_merge_request, project: merge_request.project, noteable: merge_request)
 | |
|       end
 | |
| 
 | |
|       it_behaves_like 'discussions diffs collection'
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#diff_size' do
 | |
|     let_it_be(:project) { create(:project, :repository) }
 | |
| 
 | |
|     let(:merge_request) do
 | |
|       build(:merge_request, source_project: project, source_branch: 'expand-collapse-files', target_branch: 'master')
 | |
|     end
 | |
| 
 | |
|     context 'when there are MR diffs' do
 | |
|       it 'returns the correct count' do
 | |
|         merge_request.save!
 | |
| 
 | |
|         expect(merge_request.diff_size).to eq('105')
 | |
|       end
 | |
| 
 | |
|       it 'returns the correct overflow count' do
 | |
|         allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
 | |
|         merge_request.save!
 | |
| 
 | |
|         expect(merge_request.diff_size).to eq('2+')
 | |
|       end
 | |
| 
 | |
|       it 'does not perform highlighting' do
 | |
|         merge_request.save!
 | |
| 
 | |
|         expect(Gitlab::Diff::Highlight).not_to receive(:new)
 | |
| 
 | |
|         merge_request.diff_size
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when there are no MR diffs' do
 | |
|       def set_compare(merge_request)
 | |
|         merge_request.compare = CompareService.new(
 | |
|           merge_request.source_project,
 | |
|           merge_request.source_branch
 | |
|         ).execute(
 | |
|           merge_request.target_project,
 | |
|           merge_request.target_branch
 | |
|         )
 | |
|       end
 | |
| 
 | |
|       it 'returns the correct count' do
 | |
|         set_compare(merge_request)
 | |
| 
 | |
|         expect(merge_request.diff_size).to eq('105')
 | |
|       end
 | |
| 
 | |
|       it 'returns the correct overflow count' do
 | |
|         allow(Commit).to receive(:diff_max_files).and_return(2)
 | |
|         set_compare(merge_request)
 | |
| 
 | |
|         expect(merge_request.diff_size).to eq('2+')
 | |
|       end
 | |
| 
 | |
|       it 'does not perform highlighting' do
 | |
|         set_compare(merge_request)
 | |
| 
 | |
|         expect(Gitlab::Diff::Highlight).not_to receive(:new)
 | |
| 
 | |
|         merge_request.diff_size
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#modified_paths' do
 | |
|     let(:paths) { double(:paths) }
 | |
| 
 | |
|     subject(:merge_request) { build(:merge_request) }
 | |
| 
 | |
|     before do
 | |
|       allow(diff).to receive(:modified_paths).and_return(paths)
 | |
|     end
 | |
| 
 | |
|     context 'when past_merge_request_diff is specified' do
 | |
|       let(:another_diff) { double(:merge_request_diff) }
 | |
|       let(:diff) { another_diff }
 | |
| 
 | |
|       it 'returns affected file paths from specified past_merge_request_diff' do
 | |
|         expect(merge_request.modified_paths(past_merge_request_diff: another_diff)).to eq(paths)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when compare is present' do
 | |
|       let(:compare) { double(:compare) }
 | |
|       let(:diff) { compare }
 | |
| 
 | |
|       before do
 | |
|         merge_request.compare = compare
 | |
| 
 | |
|         expect(merge_request).to receive(:diff_stats).and_return(diff_stats)
 | |
|       end
 | |
| 
 | |
|       context 'and diff_stats are not present' do
 | |
|         let(:diff_stats) { nil }
 | |
| 
 | |
|         it 'returns affected file paths from compare' do
 | |
|           expect(merge_request.modified_paths).to eq(paths)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'and diff_stats are present' do
 | |
|         let(:diff_stats) { double(:diff_stats) }
 | |
| 
 | |
|         it 'returns affected file paths from compare' do
 | |
|           diff_stats_path = double(:diff_stats_paths)
 | |
|           expect(diff_stats).to receive(:paths).and_return(diff_stats_path)
 | |
| 
 | |
|           expect(merge_request.modified_paths).to eq(diff_stats_path)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when no arguments provided' do
 | |
|       let(:diff) { merge_request.merge_request_diff }
 | |
| 
 | |
|       subject(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
 | |
| 
 | |
|       it 'returns affected file paths for merge_request_diff' do
 | |
|         expect(merge_request.modified_paths).to eq(paths)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#new_paths' do
 | |
|     let(:merge_request) do
 | |
|       create(:merge_request, source_branch: 'expand-collapse-files', target_branch: 'master')
 | |
|     end
 | |
| 
 | |
|     it 'returns new path of changed files' do
 | |
|       expect(merge_request.new_paths.count).to eq(105)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#related_notes" do
 | |
|     let!(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|     before do
 | |
|       allow(merge_request).to receive(:commits) { [merge_request.source_project.repository.commit] }
 | |
|       create(:note_on_commit, commit_id: merge_request.commits.first.id,
 | |
|                               project: merge_request.project)
 | |
|       create(:note, noteable: merge_request, project: merge_request.project)
 | |
|     end
 | |
| 
 | |
|     it "includes notes for commits" do
 | |
|       expect(merge_request.commits).not_to be_empty
 | |
|       expect(merge_request.related_notes.count).to eq(2)
 | |
|     end
 | |
| 
 | |
|     it "includes notes for commits from target project as well" do
 | |
|       create(:note_on_commit, commit_id: merge_request.commits.first.id,
 | |
|                               project: merge_request.target_project)
 | |
| 
 | |
|       expect(merge_request.commits).not_to be_empty
 | |
|       expect(merge_request.related_notes.count).to eq(3)
 | |
|     end
 | |
| 
 | |
|     it "excludes system notes for commits" do
 | |
|       system_note = create(:note_on_commit, :system, commit_id: merge_request.commits.first.id,
 | |
|                                                      project: merge_request.project)
 | |
| 
 | |
|       expect(merge_request.related_notes.count).to eq(2)
 | |
|       expect(merge_request.related_notes).not_to include(system_note)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#for_fork?' do
 | |
|     it 'returns true if the merge request is for a fork' do
 | |
|       subject.source_project = build_stubbed(:project, namespace: create(:group))
 | |
|       subject.target_project = build_stubbed(:project, namespace: create(:group))
 | |
| 
 | |
|       expect(subject.for_fork?).to be_truthy
 | |
|     end
 | |
| 
 | |
|     it 'returns false if is not for a fork' do
 | |
|       expect(subject.for_fork?).to be_falsey
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#closes_issues' do
 | |
|     let(:project) { create(:project) }
 | |
| 
 | |
|     let(:issue0) { create :issue, project: subject.project }
 | |
|     let(:issue1) { create :issue, project: subject.project }
 | |
| 
 | |
|     let(:commit0) { double('commit0', safe_message: "Fixes #{issue0.to_reference}") }
 | |
|     let(:commit1) { double('commit1', safe_message: "Fixes #{issue0.to_reference}") }
 | |
|     let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") }
 | |
| 
 | |
|     subject { create(:merge_request, source_project: project) }
 | |
| 
 | |
|     before do
 | |
|       subject.project.add_developer(subject.author)
 | |
|       allow(subject).to receive(:commits).and_return([commit0, commit1, commit2])
 | |
|     end
 | |
| 
 | |
|     it 'accesses the set of issues that will be closed on acceptance' do
 | |
|       allow(subject.project).to receive(:default_branch)
 | |
|         .and_return(subject.target_branch)
 | |
| 
 | |
|       closed = subject.closes_issues
 | |
| 
 | |
|       expect(closed).to include(issue0, issue1)
 | |
|     end
 | |
| 
 | |
|     it 'only lists issues as to be closed if it targets the default branch' do
 | |
|       allow(subject.project).to receive(:default_branch).and_return('master')
 | |
|       subject.target_branch = 'something-else'
 | |
| 
 | |
|       expect(subject.closes_issues).to be_empty
 | |
|     end
 | |
| 
 | |
|     it 'ignores referenced issues when auto-close is disabled' do
 | |
|       subject.project.update!(autoclose_referenced_issues: false)
 | |
| 
 | |
|       allow(subject.project).to receive(:default_branch)
 | |
|         .and_return(subject.target_branch)
 | |
| 
 | |
|       expect(subject.closes_issues).to be_empty
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#issues_mentioned_but_not_closing' do
 | |
|     let(:closing_issue) { create :issue, project: subject.project }
 | |
|     let(:mentioned_issue) { create :issue, project: subject.project }
 | |
|     let(:commit) { double('commit', safe_message: "Fixes #{closing_issue.to_reference}") }
 | |
| 
 | |
|     it 'detects issues mentioned in description but not closed' do
 | |
|       subject.project.add_developer(subject.author)
 | |
|       subject.description = "Is related to #{mentioned_issue.to_reference} and #{closing_issue.to_reference}"
 | |
| 
 | |
|       allow(subject).to receive(:commits).and_return([commit])
 | |
|       allow(subject.project).to receive(:default_branch)
 | |
|         .and_return(subject.target_branch)
 | |
|       subject.cache_merge_request_closes_issues!
 | |
| 
 | |
|       expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue])
 | |
|     end
 | |
| 
 | |
|     context 'when the project has an external issue tracker' do
 | |
|       before do
 | |
|         subject.project.add_developer(subject.author)
 | |
|         commit = double(:commit, safe_message: 'Fixes TEST-3')
 | |
| 
 | |
|         create(:jira_integration, project: subject.project)
 | |
|         subject.project.reload
 | |
| 
 | |
|         allow(subject).to receive(:commits).and_return([commit])
 | |
|         allow(subject).to receive(:description).and_return('Is related to TEST-2 and TEST-3')
 | |
|         allow(subject.project).to receive(:default_branch).and_return(subject.target_branch)
 | |
|       end
 | |
| 
 | |
|       it 'detects issues mentioned in description but not closed' do
 | |
|         subject.cache_merge_request_closes_issues!
 | |
| 
 | |
|         expect(subject.issues_mentioned_but_not_closing(subject.author).map(&:to_s)).to match_array(['TEST-2'])
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#work_in_progress?" do
 | |
|     subject { build_stubbed(:merge_request) }
 | |
| 
 | |
|     [
 | |
|       'draft:', 'Draft: ', '[Draft]', '[DRAFT] '
 | |
|     ].each do |draft_prefix|
 | |
|       it "detects the '#{draft_prefix}' prefix" do
 | |
|         subject.title = "#{draft_prefix}#{subject.title}"
 | |
| 
 | |
|         expect(subject.work_in_progress?).to eq true
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     [
 | |
|       'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP: [WIP] WIP:',
 | |
|       "WIP ", "(WIP)",
 | |
|       "draft", "Draft", "Draft -", "draft - ", "Draft ", "draft "
 | |
|     ].each do |draft_prefix|
 | |
|       it "doesn't detect '#{draft_prefix}' at the start of the title as a draft" do
 | |
|         subject.title = "#{draft_prefix}#{subject.title}"
 | |
| 
 | |
|         expect(subject.work_in_progress?).to eq false
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     it "doesn't detect merge request title just saying 'wip'" do
 | |
|       subject.title = "wip"
 | |
| 
 | |
|       expect(subject.work_in_progress?).to eq false
 | |
|     end
 | |
| 
 | |
|     it "does not detect merge request title just saying 'draft'" do
 | |
|       subject.title = "draft"
 | |
| 
 | |
|       expect(subject.work_in_progress?).to eq false
 | |
|     end
 | |
| 
 | |
|     it 'does not detect WIP in the middle of the title' do
 | |
|       subject.title = 'Something with WIP in the middle'
 | |
| 
 | |
|       expect(subject.work_in_progress?).to eq false
 | |
|     end
 | |
| 
 | |
|     it 'does not detect Draft in the middle of the title' do
 | |
|       subject.title = 'Something with Draft in the middle'
 | |
| 
 | |
|       expect(subject.work_in_progress?).to eq false
 | |
|     end
 | |
| 
 | |
|     it 'does not detect Draft: in the middle of the title' do
 | |
|       subject.title = 'Something with Draft: in the middle'
 | |
| 
 | |
|       expect(subject.work_in_progress?).to eq false
 | |
|     end
 | |
| 
 | |
|     it 'does not detect WIP at the end of the title' do
 | |
|       subject.title = 'Something ends with WIP'
 | |
| 
 | |
|       expect(subject.work_in_progress?).to eq false
 | |
|     end
 | |
| 
 | |
|     it 'does not detect Draft at the end of the title' do
 | |
|       subject.title = 'Something ends with Draft'
 | |
| 
 | |
|       expect(subject.work_in_progress?).to eq false
 | |
|     end
 | |
| 
 | |
|     it "doesn't detect WIP for words starting with WIP" do
 | |
|       subject.title = "Wipwap #{subject.title}"
 | |
|       expect(subject.work_in_progress?).to eq false
 | |
|     end
 | |
| 
 | |
|     it "doesn't detect WIP for words containing with WIP" do
 | |
|       subject.title = "WupWipwap #{subject.title}"
 | |
|       expect(subject.work_in_progress?).to eq false
 | |
|     end
 | |
| 
 | |
|     it "doesn't detect draft for words containing with draft" do
 | |
|       subject.title = "Drafting #{subject.title}"
 | |
|       expect(subject.work_in_progress?).to eq false
 | |
|     end
 | |
| 
 | |
|     it "doesn't detect WIP by default" do
 | |
|       expect(subject.work_in_progress?).to eq false
 | |
|     end
 | |
| 
 | |
|     it "is aliased to #draft?" do
 | |
|       expect(subject.method(:work_in_progress?)).to eq(subject.method(:draft?))
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#draftless_title" do
 | |
|     subject { build_stubbed(:merge_request) }
 | |
| 
 | |
|     ['draft:', 'Draft: ', '[Draft]', '[DRAFT] '].each do |draft_prefix|
 | |
|       it "removes a '#{draft_prefix}' prefix" do
 | |
|         draftless_title = subject.title
 | |
|         subject.title = "#{draft_prefix}#{subject.title}"
 | |
| 
 | |
|         expect(subject.draftless_title).to eq draftless_title
 | |
|       end
 | |
| 
 | |
|       it "is satisfies the #work_in_progress? method" do
 | |
|         subject.title = "#{draft_prefix}#{subject.title}"
 | |
|         subject.title = subject.draftless_title
 | |
| 
 | |
|         expect(subject.work_in_progress?).to eq false
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     [
 | |
|       'WIP:', 'WIP: ', '[WIP]', '[WIP] ', '[WIP] WIP: [WIP] WIP:'
 | |
|     ].each do |wip_prefix|
 | |
|       it "doesn't remove a '#{wip_prefix}' prefix" do
 | |
|         subject.title = "#{wip_prefix}#{subject.title}"
 | |
| 
 | |
|         expect(subject.draftless_title).to eq subject.title
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     it 'removes only draft prefix from the MR title' do
 | |
|       subject.title = 'Draft: Implement feature called draft'
 | |
| 
 | |
|       expect(subject.draftless_title).to eq 'Implement feature called draft'
 | |
|     end
 | |
| 
 | |
|     it 'does not remove WIP in the middle of the title' do
 | |
|       subject.title = 'Something with WIP in the middle'
 | |
| 
 | |
|       expect(subject.draftless_title).to eq subject.title
 | |
|     end
 | |
| 
 | |
|     it 'does not remove Draft in the middle of the title' do
 | |
|       subject.title = 'Something with Draft in the middle'
 | |
| 
 | |
|       expect(subject.draftless_title).to eq subject.title
 | |
|     end
 | |
| 
 | |
|     it 'does not remove WIP at the end of the title' do
 | |
|       subject.title = 'Something ends with WIP'
 | |
| 
 | |
|       expect(subject.draftless_title).to eq subject.title
 | |
|     end
 | |
| 
 | |
|     it 'does not remove Draft at the end of the title' do
 | |
|       subject.title = 'Something ends with Draft'
 | |
| 
 | |
|       expect(subject.draftless_title).to eq subject.title
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#draft_title" do
 | |
|     it "adds the Draft: prefix to the title" do
 | |
|       draft_title = "Draft: #{subject.title}"
 | |
| 
 | |
|       expect(subject.draft_title).to eq draft_title
 | |
|     end
 | |
| 
 | |
|     it "does not add the Draft: prefix multiple times" do
 | |
|       draft_title = "Draft: #{subject.title}"
 | |
|       subject.title = subject.draft_title
 | |
|       subject.title = subject.draft_title
 | |
| 
 | |
|       expect(subject.draft_title).to eq draft_title
 | |
|     end
 | |
| 
 | |
|     it "is satisfies the #work_in_progress? method" do
 | |
|       subject.title = subject.draft_title
 | |
| 
 | |
|       expect(subject.work_in_progress?).to eq true
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#permits_force_push?' do
 | |
|     let_it_be(:merge_request) { build_stubbed(:merge_request) }
 | |
| 
 | |
|     subject { merge_request.permits_force_push? }
 | |
| 
 | |
|     context 'when source branch is not protected' do
 | |
|       before do
 | |
|         allow(ProtectedBranch).to receive(:protected?).and_return(false)
 | |
|       end
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
| 
 | |
|     context 'when source branch is protected' do
 | |
|       before do
 | |
|         allow(ProtectedBranch).to receive(:protected?).and_return(true)
 | |
|       end
 | |
| 
 | |
|       context 'when force push is not allowed' do
 | |
|         before do
 | |
|           allow(ProtectedBranch).to receive(:allow_force_push?) { false }
 | |
|         end
 | |
| 
 | |
|         it { is_expected.to be_falsey }
 | |
|       end
 | |
| 
 | |
|       context 'when force push is allowed' do
 | |
|         before do
 | |
|           allow(ProtectedBranch).to receive(:allow_force_push?) { true }
 | |
|         end
 | |
| 
 | |
|         it { is_expected.to be_truthy }
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#can_remove_source_branch?' do
 | |
|     let_it_be(:user) { create(:user) }
 | |
|     let_it_be(:merge_request, reload: true) { create(:merge_request, :simple) }
 | |
| 
 | |
|     subject { merge_request }
 | |
| 
 | |
|     before do
 | |
|       subject.source_project.add_maintainer(user)
 | |
|     end
 | |
| 
 | |
|     it "can't be removed when its a protected branch" do
 | |
|       allow(ProtectedBranch).to receive(:protected?).and_return(true)
 | |
| 
 | |
|       expect(subject.can_remove_source_branch?(user)).to be_falsey
 | |
|     end
 | |
| 
 | |
|     it "can't be removed because source project has been deleted" do
 | |
|       subject.source_project = nil
 | |
| 
 | |
|       expect(subject.can_remove_source_branch?(user)).to be_falsey
 | |
|     end
 | |
| 
 | |
|     it "can't remove a root ref" do
 | |
|       subject.update!(source_branch: 'master', target_branch: 'feature')
 | |
| 
 | |
|       expect(subject.can_remove_source_branch?(user)).to be_falsey
 | |
|     end
 | |
| 
 | |
|     it "is unable to remove the source branch for a project the user cannot push to" do
 | |
|       user2 = create(:user)
 | |
| 
 | |
|       expect(subject.can_remove_source_branch?(user2)).to be_falsey
 | |
|     end
 | |
| 
 | |
|     it "can be removed if the last commit is the head of the source branch" do
 | |
|       allow(subject).to receive(:source_branch_head).and_return(subject.diff_head_commit)
 | |
| 
 | |
|       expect(subject.can_remove_source_branch?(user)).to be_truthy
 | |
|     end
 | |
| 
 | |
|     it "cannot be removed if the last commit is not also the head of the source branch" do
 | |
|       subject.clear_memoized_shas
 | |
|       subject.source_branch = "lfs"
 | |
| 
 | |
|       expect(subject.can_remove_source_branch?(user)).to be_falsey
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#source_branch_exists?" do
 | |
|     let(:project) { create(:project, :repository) }
 | |
|     let(:merge_request) { create(:merge_request, source_project: project) }
 | |
|     let(:repository) { merge_request.source_project.repository }
 | |
| 
 | |
|     context 'when the source project is set' do
 | |
|       it 'returns true when the branch exists' do
 | |
|         expect(merge_request.source_branch_exists?).to eq(true)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when the source project is not set' do
 | |
|       before do
 | |
|         merge_request.source_project = nil
 | |
|       end
 | |
| 
 | |
|       it 'returns false' do
 | |
|         expect(merge_request.source_branch_exists?).to eq(false)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#default_merge_commit_message' do
 | |
|     it 'includes merge information as the title' do
 | |
|       request = build(:merge_request, source_branch: 'source', target_branch: 'target')
 | |
| 
 | |
|       expect(request.default_merge_commit_message)
 | |
|         .to match("Merge branch 'source' into 'target'\n\n")
 | |
|     end
 | |
| 
 | |
|     it 'includes its title in the body' do
 | |
|       request = build(:merge_request, title: 'Remove all technical debt')
 | |
| 
 | |
|       expect(request.default_merge_commit_message)
 | |
|         .to match("Remove all technical debt\n\n")
 | |
|     end
 | |
| 
 | |
|     it 'includes its closed issues in the body' do
 | |
|       issue = create(:issue, project: subject.project)
 | |
| 
 | |
|       subject.project.add_developer(subject.author)
 | |
|       subject.description = "This issue Closes #{issue.to_reference}"
 | |
|       allow(subject.project).to receive(:default_branch).and_return(subject.target_branch)
 | |
|       subject.cache_merge_request_closes_issues!
 | |
| 
 | |
|       expect(subject.default_merge_commit_message)
 | |
|         .to match("Closes #{issue.to_reference}")
 | |
|     end
 | |
| 
 | |
|     it 'includes its reference in the body' do
 | |
|       request = build_stubbed(:merge_request)
 | |
| 
 | |
|       expect(request.default_merge_commit_message)
 | |
|         .to match("See merge request #{request.to_reference(full: true)}")
 | |
|     end
 | |
| 
 | |
|     it 'excludes multiple linebreak runs when description is blank' do
 | |
|       request = build(:merge_request, title: 'Title', description: nil)
 | |
| 
 | |
|       expect(request.default_merge_commit_message).not_to match("Title\n\n\n\n")
 | |
|     end
 | |
| 
 | |
|     it 'includes its description in the body' do
 | |
|       request = build(:merge_request, description: 'By removing all code')
 | |
| 
 | |
|       expect(request.default_merge_commit_message(include_description: true))
 | |
|         .to match("By removing all code\n\n")
 | |
|     end
 | |
| 
 | |
|     it 'does not includes its description in the body' do
 | |
|       request = build(:merge_request, description: 'By removing all code')
 | |
| 
 | |
|       expect(request.default_merge_commit_message)
 | |
|         .not_to match("By removing all code\n\n")
 | |
|     end
 | |
| 
 | |
|     it 'uses template from target project' do
 | |
|       request = build(:merge_request, title: 'Fix everything')
 | |
|       request.target_project.merge_commit_template = '%{title}'
 | |
| 
 | |
|       expect(request.default_merge_commit_message)
 | |
|         .to eq('Fix everything')
 | |
|     end
 | |
| 
 | |
|     it 'ignores template when include_description is true' do
 | |
|       request = build(:merge_request, title: 'Fix everything')
 | |
|       subject.target_project.merge_commit_template = '%{title}'
 | |
| 
 | |
|       expect(request.default_merge_commit_message(include_description: true))
 | |
|         .to match("See merge request #{request.to_reference(full: true)}")
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#auto_merge_strategy" do
 | |
|     subject { merge_request.auto_merge_strategy }
 | |
| 
 | |
|     let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
 | |
| 
 | |
|     it { is_expected.to eq('merge_when_pipeline_succeeds') }
 | |
| 
 | |
|     context 'when auto merge is disabled' do
 | |
|       let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|       it { is_expected.to be_nil }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#committers' do
 | |
|     it 'returns all the committers of every commit in the merge request' do
 | |
|       users = subject.commits.without_merge_commits.map(&:committer_email).uniq.map do |email|
 | |
|         create(:user, email: email)
 | |
|       end
 | |
| 
 | |
|       expect(subject.committers).to match_array(users)
 | |
|     end
 | |
| 
 | |
|     it 'returns an empty array if no committer is associated with a user' do
 | |
|       expect(subject.committers).to be_empty
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#diverged_commits_count' do
 | |
|     let(:project) { create(:project, :repository) }
 | |
|     let(:forked_project) { fork_project(project, nil, repository: true) }
 | |
| 
 | |
|     context 'when the target branch does not exist anymore' do
 | |
|       subject { create(:merge_request, source_project: project, target_project: project) }
 | |
| 
 | |
|       before do
 | |
|         project.repository.raw_repository.delete_branch(subject.target_branch)
 | |
|         subject.clear_memoized_shas
 | |
|       end
 | |
| 
 | |
|       it 'does not crash' do
 | |
|         expect { subject.diverged_commits_count }.not_to raise_error
 | |
|       end
 | |
| 
 | |
|       it 'returns 0' do
 | |
|         expect(subject.diverged_commits_count).to eq(0)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'diverged on same repository' do
 | |
|       subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) }
 | |
| 
 | |
|       it 'counts commits that are on target branch but not on source branch' do
 | |
|         expect(subject.diverged_commits_count).to eq(29)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'diverged on fork' do
 | |
|       subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: forked_project, target_project: project) }
 | |
| 
 | |
|       it 'counts commits that are on target branch but not on source branch', :sidekiq_might_not_need_inline do
 | |
|         expect(subject.diverged_commits_count).to eq(29)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'rebased on fork' do
 | |
|       subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: forked_project, target_project: project) }
 | |
| 
 | |
|       it 'counts commits that are on target branch but not on source branch' do
 | |
|         expect(subject.diverged_commits_count).to eq(0)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe 'caching' do
 | |
|       before do
 | |
|         allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
 | |
|       end
 | |
| 
 | |
|       it 'caches the output' do
 | |
|         expect(subject).to receive(:compute_diverged_commits_count)
 | |
|           .once
 | |
|           .and_return(2)
 | |
| 
 | |
|         subject.diverged_commits_count
 | |
|         subject.diverged_commits_count
 | |
|       end
 | |
| 
 | |
|       it 'invalidates the cache when the source sha changes' do
 | |
|         expect(subject).to receive(:compute_diverged_commits_count)
 | |
|           .twice
 | |
|           .and_return(2)
 | |
| 
 | |
|         subject.diverged_commits_count
 | |
|         allow(subject).to receive(:source_branch_sha).and_return('123abc')
 | |
|         subject.diverged_commits_count
 | |
|       end
 | |
| 
 | |
|       it 'invalidates the cache when the target sha changes' do
 | |
|         expect(subject).to receive(:compute_diverged_commits_count)
 | |
|           .twice
 | |
|           .and_return(2)
 | |
| 
 | |
|         subject.diverged_commits_count
 | |
|         allow(subject).to receive(:target_branch_sha).and_return('123abc')
 | |
|         subject.diverged_commits_count
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   it_behaves_like 'an editable mentionable' do
 | |
|     subject { create(:merge_request, :simple, source_project: create(:project, :repository)) }
 | |
| 
 | |
|     let(:backref_text) { "merge request #{subject.to_reference}" }
 | |
|     let(:set_mentionable_text) { ->(txt) { subject.description = txt } }
 | |
|   end
 | |
| 
 | |
|   it_behaves_like 'a Taskable' do
 | |
|     subject { create :merge_request, :simple }
 | |
|   end
 | |
| 
 | |
|   describe '#commit_shas' do
 | |
|     context 'persisted merge request' do
 | |
|       context 'with a limit' do
 | |
|         it 'returns a limited number of commit shas' do
 | |
|           expect(subject.commit_shas(limit: 2)).to eq(%w[
 | |
|             b83d6e391c22777fca1ed3012fce84f633d7fed0 498214de67004b1da3d820901307bed2a68a8ef6
 | |
|           ])
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'without a limit' do
 | |
|         it 'returns all commit shas of the merge request diff' do
 | |
|           expect(subject.commit_shas.size).to eq(29)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'new merge request' do
 | |
|       let_it_be(:project) { create(:project, :repository) }
 | |
| 
 | |
|       subject { build(:merge_request, source_project: project) }
 | |
| 
 | |
|       context 'compare commits' do
 | |
|         before do
 | |
|           subject.compare_commits = [
 | |
|             double(sha: 'sha1'), double(sha: 'sha2')
 | |
|           ]
 | |
|         end
 | |
| 
 | |
|         context 'without a limit' do
 | |
|           it 'returns all shas of compare commits' do
 | |
|             expect(subject.commit_shas).to eq(%w[sha2 sha1])
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'with a limit' do
 | |
|           it 'returns a limited number of shas' do
 | |
|             expect(subject.commit_shas(limit: 1)).to eq(['sha2'])
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       it 'returns diff_head_sha as an array' do
 | |
|         expect(subject.commit_shas).to eq([subject.diff_head_sha])
 | |
|         expect(subject.commit_shas(limit: 2)).to eq([subject.diff_head_sha])
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   context 'head pipeline' do
 | |
|     let(:diff_head_sha) { Digest::SHA1.hexdigest(SecureRandom.hex) }
 | |
| 
 | |
|     before do
 | |
|       allow(subject).to receive(:diff_head_sha).and_return(diff_head_sha)
 | |
|     end
 | |
| 
 | |
|     describe '#head_pipeline' do
 | |
|       it 'returns nil for MR without head_pipeline_id' do
 | |
|         subject.update_attribute(:head_pipeline_id, nil)
 | |
| 
 | |
|         expect(subject.head_pipeline).to be_nil
 | |
|       end
 | |
| 
 | |
|       context 'when the source project does not exist' do
 | |
|         it 'returns nil' do
 | |
|           allow(subject).to receive(:source_project).and_return(nil)
 | |
| 
 | |
|           expect(subject.head_pipeline).to be_nil
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe '#actual_head_pipeline' do
 | |
|       it 'returns nil for MR with old pipeline' do
 | |
|         pipeline = create(:ci_empty_pipeline, sha: 'notlatestsha')
 | |
|         subject.update_attribute(:head_pipeline_id, pipeline.id)
 | |
| 
 | |
|         expect(subject.actual_head_pipeline).to be_nil
 | |
|       end
 | |
| 
 | |
|       it 'returns the pipeline for MR with recent pipeline' do
 | |
|         pipeline = create(:ci_empty_pipeline, sha: diff_head_sha)
 | |
|         subject.update_attribute(:head_pipeline_id, pipeline.id)
 | |
| 
 | |
|         expect(subject.actual_head_pipeline).to eq(subject.head_pipeline)
 | |
|         expect(subject.actual_head_pipeline).to eq(pipeline)
 | |
|       end
 | |
| 
 | |
|       it 'returns the pipeline for MR with recent merge request pipeline' do
 | |
|         pipeline = create(:ci_empty_pipeline, sha: 'merge-sha', source_sha: diff_head_sha)
 | |
|         subject.update_attribute(:head_pipeline_id, pipeline.id)
 | |
| 
 | |
|         expect(subject.actual_head_pipeline).to eq(subject.head_pipeline)
 | |
|         expect(subject.actual_head_pipeline).to eq(pipeline)
 | |
|       end
 | |
| 
 | |
|       it 'returns nil when source project does not exist' do
 | |
|         allow(subject).to receive(:source_project).and_return(nil)
 | |
| 
 | |
|         expect(subject.actual_head_pipeline).to be_nil
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#merge_pipeline' do
 | |
|     it 'returns nil when not merged' do
 | |
|       expect(subject.merge_pipeline).to be_nil
 | |
|     end
 | |
| 
 | |
|     context 'when the MR is merged' do
 | |
|       let(:sha)      { subject.target_project.commit.id }
 | |
|       let(:pipeline) { create(:ci_empty_pipeline, sha: sha, ref: subject.target_branch, project: subject.target_project) }
 | |
| 
 | |
|       before do
 | |
|         subject.mark_as_merged!
 | |
|       end
 | |
| 
 | |
|       context 'and there is a merge commit' do
 | |
|         before do
 | |
|           subject.update_attribute(:merge_commit_sha, pipeline.sha)
 | |
|         end
 | |
| 
 | |
|         it 'returns the pipeline associated with that merge request' do
 | |
|           expect(subject.merge_pipeline).to eq(pipeline)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'and there is no merge commit, but there is a diff head' do
 | |
|         before do
 | |
|           allow(subject).to receive(:diff_head_sha).and_return(pipeline.sha)
 | |
|         end
 | |
| 
 | |
|         it 'returns the pipeline associated with that merge request' do
 | |
|           expect(subject.merge_pipeline).to eq(pipeline)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'and there is no merge commit, but there is a squash commit' do
 | |
|         before do
 | |
|           subject.update_attribute(:squash_commit_sha, pipeline.sha)
 | |
|         end
 | |
| 
 | |
|         it 'returns the pipeline associated with that merge request' do
 | |
|           expect(subject.merge_pipeline).to eq(pipeline)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#has_ci?' do
 | |
|     let(:merge_request) { build_stubbed(:merge_request) }
 | |
| 
 | |
|     context 'has ci' do
 | |
|       it 'returns true if MR has head_pipeline_id and commits' do
 | |
|         allow(merge_request).to receive_message_chain(:source_project, :ci_integration) { nil }
 | |
|         allow(merge_request).to receive(:head_pipeline_id) { double }
 | |
|         allow(merge_request).to receive(:has_no_commits?) { false }
 | |
| 
 | |
|         expect(merge_request.has_ci?).to be(true)
 | |
|       end
 | |
| 
 | |
|       it 'returns true if MR has any pipeline and commits' do
 | |
|         allow(merge_request).to receive_message_chain(:source_project, :ci_integration) { nil }
 | |
|         allow(merge_request).to receive(:head_pipeline_id) { nil }
 | |
|         allow(merge_request).to receive(:has_no_commits?) { false }
 | |
|         allow(merge_request).to receive(:all_pipelines) { [double] }
 | |
| 
 | |
|         expect(merge_request.has_ci?).to be(true)
 | |
|       end
 | |
| 
 | |
|       it 'returns true if MR has CI integration and commits' do
 | |
|         allow(merge_request).to receive_message_chain(:source_project, :ci_integration) { double }
 | |
|         allow(merge_request).to receive(:head_pipeline_id) { nil }
 | |
|         allow(merge_request).to receive(:has_no_commits?) { false }
 | |
|         allow(merge_request).to receive(:all_pipelines) { [] }
 | |
| 
 | |
|         expect(merge_request.has_ci?).to be(true)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'has no ci' do
 | |
|       it 'returns false if MR has no CI integration nor pipeline, and no commits' do
 | |
|         allow(merge_request).to receive_message_chain(:source_project, :ci_integration) { nil }
 | |
|         allow(merge_request).to receive(:head_pipeline_id) { nil }
 | |
|         allow(merge_request).to receive(:all_pipelines) { [] }
 | |
|         allow(merge_request).to receive(:has_no_commits?) { true }
 | |
| 
 | |
|         expect(merge_request.has_ci?).to be(false)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#update_head_pipeline' do
 | |
|     subject { merge_request.update_head_pipeline }
 | |
| 
 | |
|     let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|     context 'when there is a pipeline with the diff head sha' do
 | |
|       let!(:pipeline) do
 | |
|         create(:ci_empty_pipeline,
 | |
|                project: merge_request.project,
 | |
|                sha: merge_request.diff_head_sha,
 | |
|                ref: merge_request.source_branch)
 | |
|       end
 | |
| 
 | |
|       it 'updates the head pipeline' do
 | |
|         expect { subject }
 | |
|           .to change { merge_request.reload.head_pipeline }
 | |
|           .from(nil).to(pipeline)
 | |
|       end
 | |
| 
 | |
|       context 'when merge request has already had head pipeline' do
 | |
|         before do
 | |
|           merge_request.update!(head_pipeline: pipeline)
 | |
|         end
 | |
| 
 | |
|         context 'when failed to find an actual head pipeline' do
 | |
|           before do
 | |
|             allow(merge_request).to receive(:find_actual_head_pipeline) { }
 | |
|           end
 | |
| 
 | |
|           it 'does not update the current head pipeline' do
 | |
|             expect { subject }
 | |
|               .not_to change { merge_request.reload.head_pipeline }
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when detached merge request pipeline is run on head ref of the merge request' do
 | |
|       let!(:pipeline) do
 | |
|         create(:ci_pipeline,
 | |
|                source: :merge_request_event,
 | |
|                project: merge_request.source_project,
 | |
|                ref: merge_request.ref_path,
 | |
|                sha: sha,
 | |
|                merge_request: merge_request)
 | |
|       end
 | |
| 
 | |
|       let(:sha) { merge_request.diff_head_sha }
 | |
| 
 | |
|       it 'sets the head ref of the merge request to the pipeline ref' do
 | |
|         expect(pipeline.ref).to match(%r{refs/merge-requests/\d+/head})
 | |
|       end
 | |
| 
 | |
|       it 'updates correctly even though the target branch name of the merge request is different from the pipeline ref' do
 | |
|         expect { subject }
 | |
|           .to change { merge_request.reload.head_pipeline }
 | |
|           .from(nil).to(pipeline)
 | |
|       end
 | |
| 
 | |
|       context 'when sha is not HEAD of the source branch' do
 | |
|         let(:sha) { merge_request.diff_base_sha }
 | |
| 
 | |
|         it 'does not update head pipeline' do
 | |
|           expect { subject }.not_to change { merge_request.reload.head_pipeline }
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when there are no pipelines with the diff head sha' do
 | |
|       it 'does not update the head pipeline' do
 | |
|         expect { subject }
 | |
|           .not_to change { merge_request.reload.head_pipeline }
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#has_test_reports?' do
 | |
|     subject { merge_request.has_test_reports? }
 | |
| 
 | |
|     context 'when head pipeline has test reports' do
 | |
|       let(:merge_request) { create(:merge_request, :with_test_reports) }
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
| 
 | |
|     context 'when head pipeline does not have test reports' do
 | |
|       let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#has_accessibility_reports?' do
 | |
|     subject { merge_request.has_accessibility_reports? }
 | |
| 
 | |
|     context 'when head pipeline has an accessibility reports' do
 | |
|       let(:merge_request) { create(:merge_request, :with_accessibility_reports) }
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
| 
 | |
|     context 'when head pipeline does not have accessibility reports' do
 | |
|       let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#has_coverage_reports?' do
 | |
|     subject { merge_request.has_coverage_reports? }
 | |
| 
 | |
|     context 'when head pipeline has coverage reports' do
 | |
|       let(:merge_request) { create(:merge_request, :with_coverage_reports) }
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
| 
 | |
|     context 'when head pipeline does not have coverage reports' do
 | |
|       let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#has_codequality_mr_diff_report?' do
 | |
|     subject { merge_request.has_codequality_mr_diff_report? }
 | |
| 
 | |
|     context 'when head pipeline has codequality mr diff report' do
 | |
|       let(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports) }
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
| 
 | |
|     context 'when head pipeline does not have codeqquality mr diff report' do
 | |
|       let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#has_codequality_reports?' do
 | |
|     subject { merge_request.has_codequality_reports? }
 | |
| 
 | |
|     let(:project) { create(:project, :repository) }
 | |
| 
 | |
|     context 'when head pipeline has a codequality report' do
 | |
|       let(:merge_request) { create(:merge_request, :with_codequality_reports, source_project: project) }
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
| 
 | |
|     context 'when head pipeline does not have a codequality report' do
 | |
|       let(:merge_request) { create(:merge_request, source_project: project) }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#has_terraform_reports?' do
 | |
|     context 'when head pipeline has terraform reports' do
 | |
|       it 'returns true' do
 | |
|         merge_request = create(:merge_request, :with_terraform_reports)
 | |
| 
 | |
|         expect(merge_request.has_terraform_reports?).to be_truthy
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when head pipeline does not have terraform reports' do
 | |
|       it 'returns false' do
 | |
|         merge_request = create(:merge_request)
 | |
| 
 | |
|         expect(merge_request.has_terraform_reports?).to be_falsey
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#has_sast_reports?' do
 | |
|     subject { merge_request.has_sast_reports? }
 | |
| 
 | |
|     let(:project) { create(:project, :repository) }
 | |
| 
 | |
|     before do
 | |
|       stub_licensed_features(sast: true)
 | |
|     end
 | |
| 
 | |
|     context 'when head pipeline has sast reports' do
 | |
|       let(:merge_request) { create(:merge_request, :with_sast_reports, source_project: project) }
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
| 
 | |
|     context 'when head pipeline does not have sast reports' do
 | |
|       let(:merge_request) { create(:merge_request, source_project: project) }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#has_secret_detection_reports?' do
 | |
|     subject { merge_request.has_secret_detection_reports? }
 | |
| 
 | |
|     let(:project) { create(:project, :repository) }
 | |
| 
 | |
|     before do
 | |
|       stub_licensed_features(secret_detection: true)
 | |
|     end
 | |
| 
 | |
|     context 'when head pipeline has secret detection reports' do
 | |
|       let(:merge_request) { create(:merge_request, :with_secret_detection_reports, source_project: project) }
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
| 
 | |
|     context 'when head pipeline does not have secrets detection reports' do
 | |
|       let(:merge_request) { create(:merge_request, source_project: project) }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#calculate_reactive_cache' do
 | |
|     let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|     subject { merge_request.calculate_reactive_cache(service_class_name) }
 | |
| 
 | |
|     context 'when given an unknown service class name' do
 | |
|       let(:service_class_name) { 'Integer' }
 | |
| 
 | |
|       it 'raises a NameError exception' do
 | |
|         expect { subject }.to raise_error(NameError, service_class_name)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when given a known service class name' do
 | |
|       let(:service_class_name) { 'Ci::CompareTestReportsService' }
 | |
| 
 | |
|       it 'does not raises a NameError exception' do
 | |
|         allow_any_instance_of(service_class_name.constantize).to receive(:execute).and_return(nil)
 | |
| 
 | |
|         expect { subject }.not_to raise_error
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#find_exposed_artifacts' do
 | |
|     let(:project) { create(:project, :repository) }
 | |
|     let(:merge_request) { create(:merge_request, :with_test_reports, source_project: project) }
 | |
|     let(:pipeline) { merge_request.head_pipeline }
 | |
| 
 | |
|     subject { merge_request.find_exposed_artifacts }
 | |
| 
 | |
|     context 'when head pipeline has exposed artifacts' do
 | |
|       let!(:job) do
 | |
|         create(:ci_build, options: { artifacts: { expose_as: 'artifact', paths: ['ci_artifacts.txt'] } }, pipeline: pipeline)
 | |
|       end
 | |
| 
 | |
|       let!(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) }
 | |
| 
 | |
|       context 'when reactive cache worker is parsing results asynchronously' do
 | |
|         it 'returns status' do
 | |
|           expect(subject[:status]).to eq(:parsing)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when reactive cache worker is inline' do
 | |
|         before do
 | |
|           synchronous_reactive_cache(merge_request)
 | |
|         end
 | |
| 
 | |
|         it 'returns status and data' do
 | |
|           expect(subject[:status]).to eq(:parsed)
 | |
|         end
 | |
| 
 | |
|         context 'when an error occurrs' do
 | |
|           before do
 | |
|             expect_next_instance_of(Ci::FindExposedArtifactsService) do |service|
 | |
|               expect(service).to receive(:for_pipeline)
 | |
|                 .and_raise(StandardError.new)
 | |
|             end
 | |
|           end
 | |
| 
 | |
|           it 'returns an error message' do
 | |
|             expect(subject[:status]).to eq(:error)
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'when cached results is not latest' do
 | |
|           before do
 | |
|             allow_next_instance_of(Ci::GenerateExposedArtifactsReportService) do |service|
 | |
|               allow(service).to receive(:latest?).and_return(false)
 | |
|             end
 | |
|           end
 | |
| 
 | |
|           it 'raises and InvalidateReactiveCache error' do
 | |
|             expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#find_coverage_reports' do
 | |
|     let(:project) { create(:project, :repository) }
 | |
|     let(:merge_request) { create(:merge_request, :with_coverage_reports, source_project: project) }
 | |
|     let(:pipeline) { merge_request.head_pipeline }
 | |
| 
 | |
|     subject { merge_request.find_coverage_reports }
 | |
| 
 | |
|     context 'when head pipeline has coverage reports' do
 | |
|       context 'when reactive cache worker is parsing results asynchronously' do
 | |
|         it 'returns status' do
 | |
|           expect(subject[:status]).to eq(:parsing)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when reactive cache worker is inline' do
 | |
|         before do
 | |
|           synchronous_reactive_cache(merge_request)
 | |
|         end
 | |
| 
 | |
|         it 'returns status and data' do
 | |
|           expect(subject[:status]).to eq(:parsed)
 | |
|         end
 | |
| 
 | |
|         context 'when an error occurrs' do
 | |
|           before do
 | |
|             merge_request.update!(head_pipeline: nil)
 | |
|           end
 | |
| 
 | |
|           it 'returns an error message' do
 | |
|             expect(subject[:status]).to eq(:error)
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'when cached results is not latest' do
 | |
|           before do
 | |
|             allow_next_instance_of(Ci::GenerateCoverageReportsService) do |service|
 | |
|               allow(service).to receive(:latest?).and_return(false)
 | |
|             end
 | |
|           end
 | |
| 
 | |
|           it 'raises and InvalidateReactiveCache error' do
 | |
|             expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#find_codequality_mr_diff_reports' do
 | |
|     let(:project) { create(:project, :repository) }
 | |
|     let(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports, source_project: project, id: 123456789) }
 | |
|     let(:pipeline) { merge_request.head_pipeline }
 | |
| 
 | |
|     subject(:mr_diff_report) { merge_request.find_codequality_mr_diff_reports }
 | |
| 
 | |
|     context 'when head pipeline has coverage reports' do
 | |
|       context 'when reactive cache worker is parsing results asynchronously' do
 | |
|         it 'returns status' do
 | |
|           expect(mr_diff_report[:status]).to eq(:parsing)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when reactive cache worker is inline' do
 | |
|         before do
 | |
|           synchronous_reactive_cache(merge_request)
 | |
|         end
 | |
| 
 | |
|         it 'returns status and data' do
 | |
|           expect(mr_diff_report[:status]).to eq(:parsed)
 | |
|         end
 | |
| 
 | |
|         context 'when an error occurrs' do
 | |
|           before do
 | |
|             merge_request.update!(head_pipeline: nil)
 | |
|           end
 | |
| 
 | |
|           it 'returns an error message' do
 | |
|             expect(mr_diff_report[:status]).to eq(:error)
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'when cached results is not latest' do
 | |
|           before do
 | |
|             allow_next_instance_of(Ci::GenerateCodequalityMrDiffReportService) do |service|
 | |
|               allow(service).to receive(:latest?).and_return(false)
 | |
|             end
 | |
|           end
 | |
| 
 | |
|           it 'raises and InvalidateReactiveCache error' do
 | |
|             expect { mr_diff_report }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#compare_test_reports' do
 | |
|     subject { merge_request.compare_test_reports }
 | |
| 
 | |
|     let(:project) { create(:project, :repository) }
 | |
|     let(:merge_request) { create(:merge_request, source_project: project) }
 | |
| 
 | |
|     let!(:base_pipeline) do
 | |
|       create(:ci_pipeline,
 | |
|              :with_test_reports,
 | |
|              project: project,
 | |
|              ref: merge_request.target_branch,
 | |
|              sha: merge_request.diff_base_sha)
 | |
|     end
 | |
| 
 | |
|     before do
 | |
|       merge_request.update!(head_pipeline_id: head_pipeline.id)
 | |
|     end
 | |
| 
 | |
|     context 'when head pipeline has test reports' do
 | |
|       let!(:head_pipeline) do
 | |
|         create(:ci_pipeline,
 | |
|                :with_test_reports,
 | |
|                project: project,
 | |
|                ref: merge_request.source_branch,
 | |
|                sha: merge_request.diff_head_sha)
 | |
|       end
 | |
| 
 | |
|       context 'when reactive cache worker is parsing asynchronously' do
 | |
|         it 'returns status' do
 | |
|           expect(subject[:status]).to eq(:parsing)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when reactive cache worker is inline' do
 | |
|         before do
 | |
|           synchronous_reactive_cache(merge_request)
 | |
|         end
 | |
| 
 | |
|         it 'returns status and data' do
 | |
|           expect_any_instance_of(Ci::CompareTestReportsService)
 | |
|             .to receive(:execute).with(base_pipeline, head_pipeline).and_call_original
 | |
| 
 | |
|           subject
 | |
|         end
 | |
| 
 | |
|         context 'when cached results is not latest' do
 | |
|           before do
 | |
|             allow_any_instance_of(Ci::CompareTestReportsService)
 | |
|               .to receive(:latest?).and_return(false)
 | |
|           end
 | |
| 
 | |
|           it 'raises and InvalidateReactiveCache error' do
 | |
|             expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when head pipeline does not have test reports' do
 | |
|       let!(:head_pipeline) do
 | |
|         create(:ci_pipeline,
 | |
|                project: project,
 | |
|                ref: merge_request.source_branch,
 | |
|                sha: merge_request.diff_head_sha)
 | |
|       end
 | |
| 
 | |
|       it 'returns status and error message' do
 | |
|         expect(subject[:status]).to eq(:error)
 | |
|         expect(subject[:status_reason]).to eq('This merge request does not have test reports')
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#compare_accessibility_reports' do
 | |
|     let_it_be(:project) { create(:project, :repository) }
 | |
|     let_it_be(:merge_request, reload: true) { create(:merge_request, :with_accessibility_reports, source_project: project) }
 | |
|     let_it_be(:pipeline) { merge_request.head_pipeline }
 | |
| 
 | |
|     subject { merge_request.compare_accessibility_reports }
 | |
| 
 | |
|     context 'when head pipeline has accessibility reports' do
 | |
|       let(:job) do
 | |
|         create(:ci_build, options: { artifacts: { reports: { pa11y: ['accessibility.json'] } } }, pipeline: pipeline)
 | |
|       end
 | |
| 
 | |
|       let(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) }
 | |
| 
 | |
|       context 'when reactive cache worker is parsing results asynchronously' do
 | |
|         it 'returns parsing status' do
 | |
|           expect(subject[:status]).to eq(:parsing)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when reactive cache worker is inline' do
 | |
|         before do
 | |
|           synchronous_reactive_cache(merge_request)
 | |
|         end
 | |
| 
 | |
|         it 'returns parsed status' do
 | |
|           expect(subject[:status]).to eq(:parsed)
 | |
|           expect(subject[:data]).to be_present
 | |
|         end
 | |
| 
 | |
|         context 'when an error occurrs' do
 | |
|           before do
 | |
|             merge_request.update!(head_pipeline: nil)
 | |
|           end
 | |
| 
 | |
|           it 'returns an error status' do
 | |
|             expect(subject[:status]).to eq(:error)
 | |
|             expect(subject[:status_reason]).to eq("This merge request does not have accessibility reports")
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'when cached result is not latest' do
 | |
|           before do
 | |
|             allow_next_instance_of(Ci::CompareAccessibilityReportsService) do |service|
 | |
|               allow(service).to receive(:latest?).and_return(false)
 | |
|             end
 | |
|           end
 | |
| 
 | |
|           it 'raises an InvalidateReactiveCache error' do
 | |
|             expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#compare_codequality_reports' do
 | |
|     let_it_be(:project) { create(:project, :repository) }
 | |
|     let_it_be(:merge_request, reload: true) { create(:merge_request, :with_codequality_reports, source_project: project) }
 | |
|     let_it_be(:pipeline) { merge_request.head_pipeline }
 | |
| 
 | |
|     subject { merge_request.compare_codequality_reports }
 | |
| 
 | |
|     context 'when head pipeline has codequality report' do
 | |
|       let(:job) do
 | |
|         create(:ci_build, options: { artifacts: { reports: { codeclimate: ['codequality.json'] } } }, pipeline: pipeline)
 | |
|       end
 | |
| 
 | |
|       let(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) }
 | |
| 
 | |
|       context 'when reactive cache worker is parsing results asynchronously' do
 | |
|         it 'returns parsing status' do
 | |
|           expect(subject[:status]).to eq(:parsing)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when reactive cache worker is inline' do
 | |
|         before do
 | |
|           synchronous_reactive_cache(merge_request)
 | |
|         end
 | |
| 
 | |
|         it 'returns parsed status' do
 | |
|           expect(subject[:status]).to eq(:parsed)
 | |
|           expect(subject[:data]).to be_present
 | |
|         end
 | |
| 
 | |
|         context 'when an error occurrs' do
 | |
|           before do
 | |
|             merge_request.update!(head_pipeline: nil)
 | |
|           end
 | |
| 
 | |
|           it 'returns an error status' do
 | |
|             expect(subject[:status]).to eq(:error)
 | |
|             expect(subject[:status_reason]).to eq("This merge request does not have codequality reports")
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'when cached result is not latest' do
 | |
|           before do
 | |
|             allow_next_instance_of(Ci::CompareCodequalityReportsService) do |service|
 | |
|               allow(service).to receive(:latest?).and_return(false)
 | |
|             end
 | |
|           end
 | |
| 
 | |
|           it 'raises an InvalidateReactiveCache error' do
 | |
|             expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#all_commit_shas' do
 | |
|     context 'when merge request is persisted' do
 | |
|       let(:all_commit_shas) do
 | |
|         subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
 | |
|       end
 | |
| 
 | |
|       shared_examples 'returning all SHA' do
 | |
|         it 'returns all SHAs from all merge_request_diffs' do
 | |
|           expect(subject.merge_request_diffs.size).to eq(2)
 | |
|           expect(subject.all_commit_shas).to match_array(all_commit_shas)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'with a completely different branch' do
 | |
|         before do
 | |
|           subject.update!(target_branch: 'csv')
 | |
|         end
 | |
| 
 | |
|         it_behaves_like 'returning all SHA'
 | |
|       end
 | |
| 
 | |
|       context 'with a branch having no difference' do
 | |
|         before do
 | |
|           subject.update!(target_branch: 'branch-merged')
 | |
|           subject.reload # make sure commits were not cached
 | |
|         end
 | |
| 
 | |
|         it_behaves_like 'returning all SHA'
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when merge request is not persisted' do
 | |
|       let_it_be(:project) { create(:project, :repository) }
 | |
| 
 | |
|       context 'when compare commits are set in the service' do
 | |
|         let(:commit) { spy('commit') }
 | |
| 
 | |
|         subject do
 | |
|           build(:merge_request, source_project: project, compare_commits: [commit, commit])
 | |
|         end
 | |
| 
 | |
|         it 'returns commits from compare commits temporary data' do
 | |
|           expect(subject.all_commit_shas).to eq [commit, commit]
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when compare commits are not set in the service' do
 | |
|         subject { build(:merge_request, source_project: project) }
 | |
| 
 | |
|         it 'returns array with diff head sha element only' do
 | |
|           expect(subject.all_commit_shas).to eq [subject.diff_head_sha]
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#short_merge_commit_sha' do
 | |
|     let(:merge_request) { build_stubbed(:merge_request) }
 | |
| 
 | |
|     it 'returns short id when there is a merge_commit_sha' do
 | |
|       merge_request.merge_commit_sha = 'f7ce827c314c9340b075657fd61c789fb01cf74d'
 | |
| 
 | |
|       expect(merge_request.short_merge_commit_sha).to eq('f7ce827c')
 | |
|     end
 | |
| 
 | |
|     it 'returns nil when there is no merge_commit_sha' do
 | |
|       merge_request.merge_commit_sha = nil
 | |
| 
 | |
|       expect(merge_request.short_merge_commit_sha).to be_nil
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#merged_commit_sha' do
 | |
|     it 'returns nil when not merged' do
 | |
|       expect(subject.merged_commit_sha).to be_nil
 | |
|     end
 | |
| 
 | |
|     context 'when the MR is merged' do
 | |
|       let(:sha) { 'f7ce827c314c9340b075657fd61c789fb01cf74d' }
 | |
| 
 | |
|       before do
 | |
|         subject.mark_as_merged!
 | |
|       end
 | |
| 
 | |
|       it 'returns merge_commit_sha when there is a merge_commit_sha' do
 | |
|         subject.update_attribute(:merge_commit_sha, sha)
 | |
| 
 | |
|         expect(subject.merged_commit_sha).to eq(sha)
 | |
|       end
 | |
| 
 | |
|       it 'returns squash_commit_sha when there is a squash_commit_sha' do
 | |
|         subject.update_attribute(:squash_commit_sha, sha)
 | |
| 
 | |
|         expect(subject.merged_commit_sha).to eq(sha)
 | |
|       end
 | |
| 
 | |
|       it 'returns diff_head_sha when there are no merge_commit_sha and squash_commit_sha' do
 | |
|         allow(subject).to receive(:diff_head_sha).and_return(sha)
 | |
| 
 | |
|         expect(subject.merged_commit_sha).to eq(sha)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#short_merged_commit_sha' do
 | |
|     context 'when merged_commit_sha is nil' do
 | |
|       before do
 | |
|         allow(subject).to receive(:merged_commit_sha).and_return(nil)
 | |
|       end
 | |
| 
 | |
|       it 'returns nil' do
 | |
|         expect(subject.short_merged_commit_sha).to be_nil
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when merged_commit_sha is present' do
 | |
|       before do
 | |
|         allow(subject).to receive(:merged_commit_sha).and_return('f7ce827c314c9340b075657fd61c789fb01cf74d')
 | |
|       end
 | |
| 
 | |
|       it 'returns shortened merged_commit_sha' do
 | |
|         expect(subject.short_merged_commit_sha).to eq('f7ce827c')
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#can_be_reverted?' do
 | |
|     subject { create(:merge_request, source_project: create(:project, :repository)) }
 | |
| 
 | |
|     context 'when there is no merge_commit for the MR' do
 | |
|       before do
 | |
|         subject.metrics.update!(merged_at: Time.current.utc)
 | |
|       end
 | |
| 
 | |
|       it 'returns false' do
 | |
|         expect(subject.can_be_reverted?(nil)).to be_falsey
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when the MR has been merged' do
 | |
|       before do
 | |
|         MergeRequests::MergeService
 | |
|           .new(project: subject.target_project, current_user: subject.author, params: { sha: subject.diff_head_sha })
 | |
|           .execute(subject)
 | |
|       end
 | |
| 
 | |
|       context 'when there is no revert commit' do
 | |
|         it 'returns true' do
 | |
|           expect(subject.can_be_reverted?(nil)).to be_truthy
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when there is no merged_at for the MR' do
 | |
|         before do
 | |
|           subject.metrics.update!(merged_at: nil)
 | |
|         end
 | |
| 
 | |
|         it 'returns true' do
 | |
|           expect(subject.can_be_reverted?(nil)).to be_truthy
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when there is a revert commit' do
 | |
|         let(:current_user) { subject.author }
 | |
|         let(:branch) { subject.target_branch }
 | |
|         let(:project) { subject.target_project }
 | |
| 
 | |
|         let(:revert_commit_id) do
 | |
|           params = {
 | |
|             commit: subject.merge_commit,
 | |
|             branch_name: branch,
 | |
|             start_branch: branch
 | |
|           }
 | |
| 
 | |
|           Commits::RevertService.new(project, current_user, params).execute[:result]
 | |
|         end
 | |
| 
 | |
|         before do
 | |
|           project.add_maintainer(current_user)
 | |
| 
 | |
|           ProcessCommitWorker.new.perform(project.id,
 | |
|                                           current_user.id,
 | |
|                                           project.commit(revert_commit_id).to_hash,
 | |
|                                           project.default_branch == branch)
 | |
|         end
 | |
| 
 | |
|         context 'but merged at timestamp cannot be found' do
 | |
|           before do
 | |
|             allow(subject).to receive(:merged_at) { nil }
 | |
|           end
 | |
| 
 | |
|           it 'returns false' do
 | |
|             expect(subject.can_be_reverted?(current_user)).to be_falsey
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'when the revert commit is mentioned in a note after the MR was merged' do
 | |
|           it 'returns false' do
 | |
|             expect(subject.can_be_reverted?(current_user)).to be_falsey
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'when there is no merged_at for the MR' do
 | |
|           before do
 | |
|             subject.metrics.update!(merged_at: nil)
 | |
|           end
 | |
| 
 | |
|           it 'returns false' do
 | |
|             expect(subject.can_be_reverted?(current_user)).to be_falsey
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'when the revert commit is mentioned in a note just before the MR was merged' do
 | |
|           before do
 | |
|             subject.notes.last.update!(created_at: subject.metrics.merged_at - 30.seconds)
 | |
|           end
 | |
| 
 | |
|           it 'returns false' do
 | |
|             expect(subject.can_be_reverted?(current_user)).to be_falsey
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'when the revert commit is mentioned in a note long before the MR was merged' do
 | |
|           before do
 | |
|             subject.notes.last.update!(created_at: subject.metrics.merged_at - 2.minutes)
 | |
|           end
 | |
| 
 | |
|           it 'returns true' do
 | |
|             expect(subject.can_be_reverted?(current_user)).to be_truthy
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#merged_at' do
 | |
|     context 'when MR is not merged' do
 | |
|       let(:merge_request) { create(:merge_request, :closed) }
 | |
| 
 | |
|       it 'returns nil' do
 | |
|         expect(merge_request.merged_at).to be_nil
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when metrics has merged_at data' do
 | |
|       let(:merge_request) { create(:merge_request, :merged) }
 | |
| 
 | |
|       before do
 | |
|         merge_request.metrics.update!(merged_at: 1.day.ago)
 | |
|       end
 | |
| 
 | |
|       it 'returns metrics merged_at' do
 | |
|         expect(merge_request.merged_at).to eq(merge_request.metrics.merged_at)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when merged event is persisted, but no metrics merged_at is persisted' do
 | |
|       let(:user) { create(:user) }
 | |
|       let(:merge_request) { create(:merge_request, :merged) }
 | |
| 
 | |
|       before do
 | |
|         EventCreateService.new.merge_mr(merge_request, user)
 | |
|       end
 | |
| 
 | |
|       it 'returns merged event creation date' do
 | |
|         expect(merge_request.merge_event).to be_persisted
 | |
|         expect(merge_request.merged_at).to eq(merge_request.merge_event.created_at)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when no metrics or merge event exists' do
 | |
|       let(:user) { create(:user) }
 | |
|       let(:merge_request) { create(:merge_request, :merged) }
 | |
| 
 | |
|       before do
 | |
|         merge_request.metrics.destroy!
 | |
|       end
 | |
| 
 | |
|       context 'when resource event for the merge exists' do
 | |
|         before do
 | |
|           SystemNoteService.change_status(merge_request,
 | |
|                                           merge_request.target_project,
 | |
|                                           user,
 | |
|                                           merge_request.state, nil)
 | |
|         end
 | |
| 
 | |
|         it 'returns the resource event creation date' do
 | |
|           expect(merge_request.reload.metrics).to be_nil
 | |
|           expect(merge_request.merge_event).to be_nil
 | |
|           expect(merge_request.resource_state_events.count).to eq(1)
 | |
|           expect(merge_request.merged_at).to eq(merge_request.resource_state_events.first.created_at)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when system note for the merge exists' do
 | |
|         before do
 | |
|           # We do not create these system notes anymore but we need this to work for existing MRs
 | |
|           # that used system notes instead of resource state events
 | |
|           create(:note, :system, noteable: merge_request, note: 'merged')
 | |
|         end
 | |
| 
 | |
|         it 'returns the merging note creation date' do
 | |
|           expect(merge_request.reload.metrics).to be_nil
 | |
|           expect(merge_request.merge_event).to be_nil
 | |
|           expect(merge_request.notes.count).to eq(1)
 | |
|           expect(merge_request.merged_at).to eq(merge_request.notes.first.created_at)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#participants' do
 | |
|     let(:mr) do
 | |
|       create(:merge_request, source_project: project, target_project: project)
 | |
|     end
 | |
| 
 | |
|     let!(:note1) do
 | |
|       create(:note_on_merge_request, noteable: mr, project: project, note: 'a')
 | |
|     end
 | |
| 
 | |
|     let!(:note2) do
 | |
|       create(:note_on_merge_request, noteable: mr, project: project, note: 'b')
 | |
|     end
 | |
| 
 | |
|     it 'includes the merge request author' do
 | |
|       expect(mr.participants).to include(mr.author)
 | |
|     end
 | |
| 
 | |
|     it 'includes the authors of the notes' do
 | |
|       expect(mr.participants).to include(note1.author, note2.author)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe 'cached counts' do
 | |
|     it 'updates when assignees change' do
 | |
|       user1 = create(:user)
 | |
|       user2 = create(:user)
 | |
|       mr = create(:merge_request, assignees: [user1])
 | |
|       mr.project.add_developer(user1)
 | |
|       mr.project.add_developer(user2)
 | |
| 
 | |
|       expect(user1.assigned_open_merge_requests_count).to eq(1)
 | |
|       expect(user2.assigned_open_merge_requests_count).to eq(0)
 | |
| 
 | |
|       mr.assignees = [user2]
 | |
| 
 | |
|       expect(user1.assigned_open_merge_requests_count).to eq(0)
 | |
|       expect(user2.assigned_open_merge_requests_count).to eq(1)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#merge_async' do
 | |
|     it 'enqueues MergeWorker job and updates merge_jid' do
 | |
|       merge_request = create(:merge_request)
 | |
|       user_id = double(:user_id)
 | |
|       params = {}
 | |
|       merge_jid = 'hash-123'
 | |
| 
 | |
|       allow(MergeWorker).to receive(:with_status).and_return(MergeWorker)
 | |
| 
 | |
|       expect(merge_request).to receive(:expire_etag_cache)
 | |
|       expect(MergeWorker).to receive(:perform_async).with(merge_request.id, user_id, params) do
 | |
|         merge_jid
 | |
|       end
 | |
| 
 | |
|       merge_request.merge_async(user_id, params)
 | |
| 
 | |
|       expect(merge_request.reload.merge_jid).to eq(merge_jid)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#rebase_async' do
 | |
|     let(:merge_request) { create(:merge_request) }
 | |
|     let(:user_id) { double(:user_id) }
 | |
|     let(:rebase_jid) { 'rebase-jid' }
 | |
| 
 | |
|     subject(:execute) { merge_request.rebase_async(user_id) }
 | |
| 
 | |
|     before do
 | |
|       allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker)
 | |
|     end
 | |
| 
 | |
|     it 'atomically enqueues a RebaseWorker job and updates rebase_jid' do
 | |
|       expect(RebaseWorker)
 | |
|         .to receive(:perform_async)
 | |
|         .with(merge_request.id, user_id, false)
 | |
|         .and_return(rebase_jid)
 | |
| 
 | |
|       expect(merge_request).to receive(:expire_etag_cache)
 | |
|       expect(merge_request).to receive(:lock!).and_call_original
 | |
| 
 | |
|       execute
 | |
| 
 | |
|       expect(merge_request.rebase_jid).to eq(rebase_jid)
 | |
|     end
 | |
| 
 | |
|     it 'refuses to enqueue a job if a rebase is in progress' do
 | |
|       merge_request.update_column(:rebase_jid, rebase_jid)
 | |
| 
 | |
|       expect(RebaseWorker).not_to receive(:perform_async)
 | |
|       expect(Gitlab::SidekiqStatus)
 | |
|         .to receive(:running?)
 | |
|         .with(rebase_jid)
 | |
|         .and_return(true)
 | |
| 
 | |
|       expect { execute }.to raise_error(ActiveRecord::StaleObjectError)
 | |
|     end
 | |
| 
 | |
|     it 'refuses to enqueue a job if the MR is not open' do
 | |
|       merge_request.update_column(:state_id, 5)
 | |
| 
 | |
|       expect(RebaseWorker).not_to receive(:perform_async)
 | |
| 
 | |
|       expect { execute }.to raise_error(ActiveRecord::StaleObjectError)
 | |
|     end
 | |
| 
 | |
|     it "raises ActiveRecord::LockWaitTimeout after 6 tries" do
 | |
|       expect(merge_request).to receive(:with_lock).exactly(6).times.and_raise(ActiveRecord::LockWaitTimeout)
 | |
|       expect(RebaseWorker).not_to receive(:perform_async)
 | |
| 
 | |
|       expect { execute }.to raise_error(MergeRequest::RebaseLockTimeout)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#mergeable?' do
 | |
|     subject { build_stubbed(:merge_request) }
 | |
| 
 | |
|     it 'returns false if #mergeable_state? is false' do
 | |
|       expect(subject).to receive(:mergeable_state?) { false }
 | |
| 
 | |
|       expect(subject.mergeable?).to be_falsey
 | |
|     end
 | |
| 
 | |
|     it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do
 | |
|       allow(subject).to receive(:mergeable_state?) { true }
 | |
|       expect(subject).to receive(:check_mergeability)
 | |
|       expect(subject).to receive(:can_be_merged?) { true }
 | |
| 
 | |
|       expect(subject.mergeable?).to be_truthy
 | |
|     end
 | |
| 
 | |
|     it 'return true if #mergeable_state? is true and the MR #can_be_merged? is false' do
 | |
|       allow(subject).to receive(:mergeable_state?) { true }
 | |
|       expect(subject).to receive(:check_mergeability)
 | |
|       expect(subject).to receive(:can_be_merged?) { false }
 | |
| 
 | |
|       expect(subject.mergeable?).to be_falsey
 | |
|     end
 | |
| 
 | |
|     context 'with skip_ci_check option' do
 | |
|       before do
 | |
|         allow(subject).to receive_messages(check_mergeability: nil,
 | |
|                                            can_be_merged?: true,
 | |
|                                            broken?: false)
 | |
|       end
 | |
| 
 | |
|       where(:mergeable_ci_state, :skip_ci_check, :expected_mergeable) do
 | |
|         false | false | false
 | |
|         false | true  | true
 | |
|         true  | false | true
 | |
|         true  | true  | true
 | |
|       end
 | |
| 
 | |
|       with_them do
 | |
|         it 'overrides mergeable_ci_state?' do
 | |
|           allow(subject).to receive(:mergeable_ci_state?) { mergeable_ci_state }
 | |
| 
 | |
|           expect(subject.mergeable?(skip_ci_check: skip_ci_check)).to eq(expected_mergeable)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'with skip_discussions_check option' do
 | |
|       before do
 | |
|         allow(subject).to receive_messages(mergeable_ci_state?: true,
 | |
|                                            check_mergeability: nil,
 | |
|                                            can_be_merged?: true,
 | |
|                                            broken?: false)
 | |
|       end
 | |
| 
 | |
|       where(:mergeable_discussions_state, :skip_discussions_check, :expected_mergeable) do
 | |
|         false | false | false
 | |
|         false | true  | true
 | |
|         true  | false | true
 | |
|         true  | true  | true
 | |
|       end
 | |
| 
 | |
|       with_them do
 | |
|         it 'overrides mergeable_discussions_state?' do
 | |
|           allow(subject).to receive(:mergeable_discussions_state?) { mergeable_discussions_state }
 | |
| 
 | |
|           expect(subject.mergeable?(skip_discussions_check: skip_discussions_check)).to eq(expected_mergeable)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#check_mergeability' do
 | |
|     let(:mergeability_service) { double }
 | |
| 
 | |
|     subject { create(:merge_request, merge_status: 'unchecked') }
 | |
| 
 | |
|     before do
 | |
|       allow(MergeRequests::MergeabilityCheckService).to receive(:new) do
 | |
|         mergeability_service
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     shared_examples_for 'method that executes MergeabilityCheckService' do
 | |
|       it 'executes MergeabilityCheckService' do
 | |
|         expect(mergeability_service).to receive(:execute)
 | |
| 
 | |
|         subject.check_mergeability
 | |
|       end
 | |
| 
 | |
|       context 'when async is true' do
 | |
|         it 'executes MergeabilityCheckService asynchronously' do
 | |
|           expect(mergeability_service).to receive(:async_execute)
 | |
| 
 | |
|           subject.check_mergeability(async: true)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'if the merge status is unchecked' do
 | |
|       it_behaves_like 'method that executes MergeabilityCheckService'
 | |
|     end
 | |
| 
 | |
|     context 'if the merge status is checking' do
 | |
|       before do
 | |
|         subject.mark_as_checking!
 | |
|       end
 | |
| 
 | |
|       it_behaves_like 'method that executes MergeabilityCheckService'
 | |
|     end
 | |
| 
 | |
|     context 'if the merge status is checked' do
 | |
|       before do
 | |
|         subject.mark_as_mergeable!
 | |
|       end
 | |
| 
 | |
|       it 'does not call MergeabilityCheckService' do
 | |
|         expect(MergeRequests::MergeabilityCheckService).not_to receive(:new)
 | |
| 
 | |
|         subject.check_mergeability
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   shared_examples 'for mergeable_state' do
 | |
|     subject { create(:merge_request) }
 | |
| 
 | |
|     it 'checks if merge request can be merged' do
 | |
|       allow(subject).to receive(:mergeable_ci_state?) { true }
 | |
|       expect(subject).to receive(:check_mergeability)
 | |
| 
 | |
|       subject.mergeable?
 | |
|     end
 | |
| 
 | |
|     context 'when not open' do
 | |
|       before do
 | |
|         subject.close
 | |
|       end
 | |
| 
 | |
|       it 'returns false' do
 | |
|         expect(subject.mergeable_state?).to be_falsey
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when working in progress' do
 | |
|       before do
 | |
|         subject.title = '[Draft] MR'
 | |
|       end
 | |
| 
 | |
|       it 'returns false' do
 | |
|         expect(subject.mergeable_state?).to be_falsey
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when broken' do
 | |
|       before do
 | |
|         allow(subject).to receive(:broken?) { true }
 | |
|       end
 | |
| 
 | |
|       it 'returns false' do
 | |
|         expect(subject.mergeable_state?).to be_falsey
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when failed' do
 | |
|       context 'when #mergeable_ci_state? is false' do
 | |
|         before do
 | |
|           allow(subject).to receive(:mergeable_ci_state?) { false }
 | |
|         end
 | |
| 
 | |
|         it 'returns false' do
 | |
|           expect(subject.mergeable_state?).to be_falsey
 | |
|         end
 | |
| 
 | |
|         it 'returns true when skipping ci check' do
 | |
|           expect(subject.mergeable_state?(skip_ci_check: true)).to be(true)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when #mergeable_discussions_state? is false' do
 | |
|         before do
 | |
|           allow(subject).to receive(:mergeable_discussions_state?) { false }
 | |
|         end
 | |
| 
 | |
|         it 'returns false' do
 | |
|           expect(subject.mergeable_state?).to be_falsey
 | |
|         end
 | |
| 
 | |
|         it 'returns true when skipping discussions check' do
 | |
|           expect(subject.mergeable_state?(skip_discussions_check: true)).to be(true)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#mergeable_state?' do
 | |
|     it_behaves_like 'for mergeable_state'
 | |
| 
 | |
|     context 'when improved_mergeability_checks is off' do
 | |
|       before do
 | |
|         stub_feature_flags(improved_mergeability_checks: false)
 | |
|       end
 | |
| 
 | |
|       it_behaves_like 'for mergeable_state'
 | |
|     end
 | |
| 
 | |
|     context 'when merge state caching is off' do
 | |
|       before do
 | |
|         stub_feature_flags(mergeability_caching: false)
 | |
|       end
 | |
| 
 | |
|       it_behaves_like 'for mergeable_state'
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#public_merge_status" do
 | |
|     using RSpec::Parameterized::TableSyntax
 | |
|     subject { build(:merge_request, merge_status: status) }
 | |
| 
 | |
|     where(:status, :public_status) do
 | |
|       'cannot_be_merged_rechecking' | 'checking'
 | |
|       'preparing'                   | 'checking'
 | |
|       'checking'                    | 'checking'
 | |
|       'cannot_be_merged'            | 'cannot_be_merged'
 | |
|     end
 | |
| 
 | |
|     with_them do
 | |
|       it { expect(subject.public_merge_status).to eq(public_status) }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#head_pipeline_active? " do
 | |
|     context 'when project lacks a head_pipeline relation' do
 | |
|       before do
 | |
|         subject.head_pipeline = nil
 | |
|       end
 | |
| 
 | |
|       it 'returns false' do
 | |
|         expect(subject.head_pipeline_active?).to be false
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when project has a head_pipeline relation' do
 | |
|       let(:pipeline) { create(:ci_empty_pipeline) }
 | |
| 
 | |
|       before do
 | |
|         allow(subject).to receive(:head_pipeline) { pipeline }
 | |
|       end
 | |
| 
 | |
|       it 'accesses the value from the head_pipeline' do
 | |
|         expect(subject.head_pipeline)
 | |
|           .to receive(:active?)
 | |
| 
 | |
|         subject.head_pipeline_active?
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#actual_head_pipeline_success? " do
 | |
|     context 'when project lacks an actual_head_pipeline relation' do
 | |
|       before do
 | |
|         allow(subject).to receive(:actual_head_pipeline) { nil }
 | |
|       end
 | |
| 
 | |
|       it 'returns false' do
 | |
|         expect(subject.actual_head_pipeline_success?).to be false
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when project has a actual_head_pipeline relation' do
 | |
|       let(:pipeline) { create(:ci_empty_pipeline) }
 | |
| 
 | |
|       before do
 | |
|         allow(subject).to receive(:actual_head_pipeline) { pipeline }
 | |
|       end
 | |
| 
 | |
|       it 'accesses the value from the actual_head_pipeline' do
 | |
|         expect(subject.actual_head_pipeline)
 | |
|           .to receive(:success?)
 | |
| 
 | |
|         subject.actual_head_pipeline_success?
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#actual_head_pipeline_active? " do
 | |
|     context 'when project lacks an actual_head_pipeline relation' do
 | |
|       before do
 | |
|         allow(subject).to receive(:actual_head_pipeline) { nil }
 | |
|       end
 | |
| 
 | |
|       it 'returns false' do
 | |
|         expect(subject.actual_head_pipeline_active?).to be false
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when project has a actual_head_pipeline relation' do
 | |
|       let(:pipeline) { create(:ci_empty_pipeline) }
 | |
| 
 | |
|       before do
 | |
|         allow(subject).to receive(:actual_head_pipeline) { pipeline }
 | |
|       end
 | |
| 
 | |
|       it 'accesses the value from the actual_head_pipeline' do
 | |
|         expect(subject.actual_head_pipeline)
 | |
|           .to receive(:active?)
 | |
| 
 | |
|         subject.actual_head_pipeline_active?
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#mergeable_ci_state?' do
 | |
|     let(:pipeline) { create(:ci_empty_pipeline) }
 | |
| 
 | |
|     context 'when it is only allowed to merge when build is green' do
 | |
|       let_it_be(:project) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true) }
 | |
| 
 | |
|       subject { build(:merge_request, source_project: project) }
 | |
| 
 | |
|       context 'and a failed pipeline is associated' do
 | |
|         before do
 | |
|           pipeline.update!(status: 'failed', sha: subject.diff_head_sha)
 | |
|           allow(subject).to receive(:head_pipeline) { pipeline }
 | |
|         end
 | |
| 
 | |
|         it { expect(subject.mergeable_ci_state?).to be_falsey }
 | |
|       end
 | |
| 
 | |
|       context 'and a successful pipeline is associated' do
 | |
|         before do
 | |
|           pipeline.update!(status: 'success', sha: subject.diff_head_sha)
 | |
|           allow(subject).to receive(:head_pipeline) { pipeline }
 | |
|         end
 | |
| 
 | |
|         it { expect(subject.mergeable_ci_state?).to be_truthy }
 | |
|       end
 | |
| 
 | |
|       context 'and a skipped pipeline is associated' do
 | |
|         before do
 | |
|           pipeline.update!(status: 'skipped', sha: subject.diff_head_sha)
 | |
|           allow(subject).to receive(:head_pipeline).and_return(pipeline)
 | |
|         end
 | |
| 
 | |
|         it { expect(subject.mergeable_ci_state?).to be_falsey }
 | |
|       end
 | |
| 
 | |
|       context 'when no pipeline is associated' do
 | |
|         before do
 | |
|           allow(subject).to receive(:head_pipeline).and_return(nil)
 | |
|         end
 | |
| 
 | |
|         it { expect(subject.mergeable_ci_state?).to be_falsey }
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when it is only allowed to merge when build is green or skipped' do
 | |
|       let_it_be(:project) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true, allow_merge_on_skipped_pipeline: true) }
 | |
| 
 | |
|       subject { build(:merge_request, source_project: project) }
 | |
| 
 | |
|       context 'and a failed pipeline is associated' do
 | |
|         before do
 | |
|           pipeline.update!(status: 'failed', sha: subject.diff_head_sha)
 | |
|           allow(subject).to receive(:head_pipeline).and_return(pipeline)
 | |
|         end
 | |
| 
 | |
|         it { expect(subject.mergeable_ci_state?).to be_falsey }
 | |
|       end
 | |
| 
 | |
|       context 'and a successful pipeline is associated' do
 | |
|         before do
 | |
|           pipeline.update!(status: 'success', sha: subject.diff_head_sha)
 | |
|           allow(subject).to receive(:head_pipeline).and_return(pipeline)
 | |
|         end
 | |
| 
 | |
|         it { expect(subject.mergeable_ci_state?).to be_truthy }
 | |
|       end
 | |
| 
 | |
|       context 'and a skipped pipeline is associated' do
 | |
|         before do
 | |
|           pipeline.update!(status: 'skipped', sha: subject.diff_head_sha)
 | |
|           allow(subject).to receive(:head_pipeline).and_return(pipeline)
 | |
|         end
 | |
| 
 | |
|         it { expect(subject.mergeable_ci_state?).to be_truthy }
 | |
|       end
 | |
| 
 | |
|       context 'when no pipeline is associated' do
 | |
|         before do
 | |
|           allow(subject).to receive(:head_pipeline).and_return(nil)
 | |
|         end
 | |
| 
 | |
|         it { expect(subject.mergeable_ci_state?).to be_falsey }
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when merges are not restricted to green builds' do
 | |
|       let_it_be(:project) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: false) }
 | |
| 
 | |
|       subject { build(:merge_request, source_project: project) }
 | |
| 
 | |
|       context 'and a failed pipeline is associated' do
 | |
|         before do
 | |
|           pipeline.statuses << create(:commit_status, status: 'failed', project: project)
 | |
|           allow(subject).to receive(:head_pipeline) { pipeline }
 | |
|         end
 | |
| 
 | |
|         it { expect(subject.mergeable_ci_state?).to be_truthy }
 | |
|       end
 | |
| 
 | |
|       context 'when no pipeline is associated' do
 | |
|         before do
 | |
|           allow(subject).to receive(:head_pipeline) { nil }
 | |
|         end
 | |
| 
 | |
|         it { expect(subject.mergeable_ci_state?).to be_truthy }
 | |
|       end
 | |
| 
 | |
|       context 'and a skipped pipeline is associated' do
 | |
|         before do
 | |
|           pipeline.update!(status: 'skipped', sha: subject.diff_head_sha)
 | |
|           allow(subject).to receive(:head_pipeline).and_return(pipeline)
 | |
|         end
 | |
| 
 | |
|         it { expect(subject.mergeable_ci_state?).to be_truthy }
 | |
|       end
 | |
| 
 | |
|       context 'when no pipeline is associated' do
 | |
|         before do
 | |
|           allow(subject).to receive(:head_pipeline).and_return(nil)
 | |
|         end
 | |
| 
 | |
|         it { expect(subject.mergeable_ci_state?).to be_truthy }
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#mergeable_discussions_state?' do
 | |
|     let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) }
 | |
| 
 | |
|     context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do
 | |
|       let_it_be(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
 | |
| 
 | |
|       context 'with all discussions resolved' do
 | |
|         before do
 | |
|           merge_request.discussions.each { |d| d.resolve!(merge_request.author) }
 | |
|         end
 | |
| 
 | |
|         it 'returns true' do
 | |
|           expect(merge_request.mergeable_discussions_state?).to be_truthy
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'with unresolved discussions' do
 | |
|         before do
 | |
|           merge_request.discussions.each(&:unresolve!)
 | |
|         end
 | |
| 
 | |
|         it 'returns false' do
 | |
|           expect(merge_request.mergeable_discussions_state?).to be_falsey
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'with no discussions' do
 | |
|         before do
 | |
|           merge_request.notes.destroy_all # rubocop: disable Cop/DestroyAll
 | |
|         end
 | |
| 
 | |
|         it 'returns true' do
 | |
|           expect(merge_request.mergeable_discussions_state?).to be_truthy
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do
 | |
|       let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: false) }
 | |
| 
 | |
|       context 'with unresolved discussions' do
 | |
|         before do
 | |
|           merge_request.discussions.each(&:unresolve!)
 | |
|         end
 | |
| 
 | |
|         it 'returns true' do
 | |
|           expect(merge_request.mergeable_discussions_state?).to be_truthy
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#reload_diff" do
 | |
|     it 'calls MergeRequests::ReloadDiffsService#execute with correct params' do
 | |
|       user = create(:user)
 | |
|       service = instance_double(MergeRequests::ReloadDiffsService, execute: nil)
 | |
| 
 | |
|       expect(MergeRequests::ReloadDiffsService)
 | |
|         .to receive(:new).with(subject, user)
 | |
|         .and_return(service)
 | |
| 
 | |
|       subject.reload_diff(user)
 | |
| 
 | |
|       expect(service).to have_received(:execute)
 | |
|     end
 | |
| 
 | |
|     context 'when using the after_update hook to update' do
 | |
|       context 'when the branches are updated' do
 | |
|         it 'uses the new heads to generate the diff' do
 | |
|           expect { subject.update!(source_branch: subject.target_branch, target_branch: subject.source_branch) }
 | |
|             .to change { subject.merge_request_diff.start_commit_sha }
 | |
|             .and change { subject.merge_request_diff.head_commit_sha }
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#update_diff_discussion_positions' do
 | |
|     subject { create(:merge_request, source_project: project) }
 | |
| 
 | |
|     let(:project) { create(:project, :repository) }
 | |
|     let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") }
 | |
|     let(:modify_commit) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e") }
 | |
|     let(:edit_commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") }
 | |
|     let(:discussion) { create(:diff_note_on_merge_request, noteable: subject, project: project, position: old_position).to_discussion }
 | |
|     let(:path) { "files/ruby/popen.rb" }
 | |
|     let(:new_line) { 9 }
 | |
| 
 | |
|     let(:old_diff_refs) do
 | |
|       Gitlab::Diff::DiffRefs.new(
 | |
|         base_sha: create_commit.parent_id,
 | |
|         head_sha: modify_commit.sha
 | |
|       )
 | |
|     end
 | |
| 
 | |
|     let(:new_diff_refs) do
 | |
|       Gitlab::Diff::DiffRefs.new(
 | |
|         base_sha: create_commit.parent_id,
 | |
|         head_sha: edit_commit.sha
 | |
|       )
 | |
|     end
 | |
| 
 | |
|     let(:old_position) do
 | |
|       Gitlab::Diff::Position.new(
 | |
|         old_path: path,
 | |
|         new_path: path,
 | |
|         old_line: nil,
 | |
|         new_line: new_line,
 | |
|         diff_refs: old_diff_refs
 | |
|       )
 | |
|     end
 | |
| 
 | |
|     it "updates diff discussion positions" do
 | |
|       expect(Discussions::UpdateDiffPositionService).to receive(:new).with(
 | |
|         subject.project,
 | |
|         subject.author,
 | |
|         old_diff_refs: old_diff_refs,
 | |
|         new_diff_refs: new_diff_refs,
 | |
|         paths: discussion.position.paths
 | |
|       ).and_call_original
 | |
| 
 | |
|       expect_any_instance_of(Discussions::UpdateDiffPositionService).to receive(:execute).with(discussion).and_call_original
 | |
| 
 | |
|       subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
 | |
|                                                new_diff_refs: new_diff_refs,
 | |
|                                                current_user: subject.author)
 | |
|     end
 | |
| 
 | |
|     it 'does not call the resolve method' do
 | |
|       expect(MergeRequests::ResolvedDiscussionNotificationService).not_to receive(:new)
 | |
| 
 | |
|       subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
 | |
|                                                new_diff_refs: new_diff_refs,
 | |
|                                                current_user: subject.author)
 | |
|     end
 | |
| 
 | |
|     context 'when resolve_outdated_diff_discussions is set' do
 | |
|       before do
 | |
|         discussion
 | |
| 
 | |
|         subject.project.update!(resolve_outdated_diff_discussions: true)
 | |
|       end
 | |
| 
 | |
|       context 'when the active discussion is resolved in the update' do
 | |
|         it 'calls MergeRequests::ResolvedDiscussionNotificationService' do
 | |
|           expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService)
 | |
|             .to receive(:execute).with(subject)
 | |
| 
 | |
|           subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
 | |
|                                                    new_diff_refs: new_diff_refs,
 | |
|                                                    current_user: subject.author)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when the active discussion does not have resolved in the update' do
 | |
|         let(:new_line) { 16 }
 | |
| 
 | |
|         it 'does not call the resolve method' do
 | |
|           expect(MergeRequests::ResolvedDiscussionNotificationService).not_to receive(:new)
 | |
| 
 | |
|           subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
 | |
|                                                    new_diff_refs: new_diff_refs,
 | |
|                                                    current_user: subject.author)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when the active discussion was already resolved' do
 | |
|         before do
 | |
|           discussion.resolve!(subject.author)
 | |
|         end
 | |
| 
 | |
|         it 'does not call the resolve method' do
 | |
|           expect(MergeRequests::ResolvedDiscussionNotificationService).not_to receive(:new)
 | |
| 
 | |
|           subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
 | |
|                                                    new_diff_refs: new_diff_refs,
 | |
|                                                    current_user: subject.author)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#branch_merge_base_commit' do
 | |
|     let(:project) { create(:project, :repository) }
 | |
| 
 | |
|     subject { create(:merge_request, source_project: project) }
 | |
| 
 | |
|     context 'source and target branch exist' do
 | |
|       it { expect(subject.branch_merge_base_commit.sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
 | |
|       it { expect(subject.branch_merge_base_commit).to be_a(Commit) }
 | |
|     end
 | |
| 
 | |
|     context 'when the target branch does not exist' do
 | |
|       before do
 | |
|         subject.project.repository.rm_branch(subject.author, subject.target_branch)
 | |
|         subject.clear_memoized_shas
 | |
|       end
 | |
| 
 | |
|       it 'returns nil' do
 | |
|         expect(subject.branch_merge_base_commit).to be_nil
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#diff_refs" do
 | |
|     context "with diffs" do
 | |
|       let(:project) { create(:project, :repository) }
 | |
| 
 | |
|       subject { create(:merge_request, source_project: project) }
 | |
| 
 | |
|       let(:expected_diff_refs) do
 | |
|         Gitlab::Diff::DiffRefs.new(
 | |
|           base_sha:  subject.merge_request_diff.base_commit_sha,
 | |
|           start_sha: subject.merge_request_diff.start_commit_sha,
 | |
|           head_sha:  subject.merge_request_diff.head_commit_sha
 | |
|         )
 | |
|       end
 | |
| 
 | |
|       it "does not touch the repository" do
 | |
|         subject # Instantiate the object
 | |
| 
 | |
|         expect_any_instance_of(Repository).not_to receive(:commit)
 | |
| 
 | |
|         subject.diff_refs
 | |
|       end
 | |
| 
 | |
|       it "returns expected diff_refs" do
 | |
|         expect(subject.diff_refs).to eq(expected_diff_refs)
 | |
|       end
 | |
| 
 | |
|       context 'when importing' do
 | |
|         before do
 | |
|           subject.importing = true
 | |
|         end
 | |
| 
 | |
|         it "returns MR diff_refs" do
 | |
|           expect(subject.diff_refs).to eq(expected_diff_refs)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#source_project_missing?" do
 | |
|     let(:project) { create(:project) }
 | |
|     let(:forked_project) { fork_project(project) }
 | |
|     let(:user) { create(:user) }
 | |
|     let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
 | |
| 
 | |
|     context "when the fork exists" do
 | |
|       let(:merge_request) do
 | |
|         create(:merge_request,
 | |
|           source_project: forked_project,
 | |
|           target_project: project)
 | |
|       end
 | |
| 
 | |
|       it { expect(merge_request.source_project_missing?).to be_falsey }
 | |
|     end
 | |
| 
 | |
|     context "when the source project is the same as the target project" do
 | |
|       let(:merge_request) { create(:merge_request, source_project: project) }
 | |
| 
 | |
|       it { expect(merge_request.source_project_missing?).to be_falsey }
 | |
|     end
 | |
| 
 | |
|     context "when the fork does not exist" do
 | |
|       let!(:merge_request) do
 | |
|         create(:merge_request,
 | |
|           source_project: forked_project,
 | |
|           target_project: project)
 | |
|       end
 | |
| 
 | |
|       it "returns true" do
 | |
|         unlink_project.execute
 | |
|         merge_request.reload
 | |
| 
 | |
|         expect(merge_request.source_project_missing?).to be_truthy
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#merge_ongoing?' do
 | |
|     it 'returns true when the merge request is locked' do
 | |
|       merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:locked])
 | |
| 
 | |
|       expect(merge_request.merge_ongoing?).to be(true)
 | |
|     end
 | |
| 
 | |
|     it 'returns true when merge_id, MR is not merged and it has no running job' do
 | |
|       merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:opened], merge_jid: 'foo')
 | |
|       allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { true }
 | |
| 
 | |
|       expect(merge_request.merge_ongoing?).to be(true)
 | |
|     end
 | |
| 
 | |
|     it 'returns false when merge_jid is nil' do
 | |
|       merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:opened], merge_jid: nil)
 | |
| 
 | |
|       expect(merge_request.merge_ongoing?).to be(false)
 | |
|     end
 | |
| 
 | |
|     it 'returns false if MR is merged' do
 | |
|       merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:merged], merge_jid: 'foo')
 | |
| 
 | |
|       expect(merge_request.merge_ongoing?).to be(false)
 | |
|     end
 | |
| 
 | |
|     it 'returns false if there is no merge job running' do
 | |
|       merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:opened], merge_jid: 'foo')
 | |
|       allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { false }
 | |
| 
 | |
|       expect(merge_request.merge_ongoing?).to be(false)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe "#closed_or_merged_without_fork?" do
 | |
|     let(:project) { create(:project) }
 | |
|     let(:forked_project) { fork_project(project) }
 | |
|     let(:user) { create(:user) }
 | |
|     let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
 | |
| 
 | |
|     context "when the merge request is closed" do
 | |
|       let(:closed_merge_request) do
 | |
|         create(:closed_merge_request,
 | |
|           source_project: forked_project,
 | |
|           target_project: project)
 | |
|       end
 | |
| 
 | |
|       it "returns false if the fork exist" do
 | |
|         expect(closed_merge_request.closed_or_merged_without_fork?).to be_falsey
 | |
|       end
 | |
| 
 | |
|       it "returns true if the fork does not exist" do
 | |
|         unlink_project.execute
 | |
|         closed_merge_request.reload
 | |
| 
 | |
|         expect(closed_merge_request.closed_or_merged_without_fork?).to be_truthy
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context "when the merge request was merged" do
 | |
|       let(:merged_merge_request) do
 | |
|         create(:merged_merge_request,
 | |
|           source_project: forked_project,
 | |
|           target_project: project)
 | |
|       end
 | |
| 
 | |
|       it "returns false if the fork exist" do
 | |
|         expect(merged_merge_request.closed_or_merged_without_fork?).to be_falsey
 | |
|       end
 | |
| 
 | |
|       it "returns true if the fork does not exist" do
 | |
|         unlink_project.execute
 | |
|         merged_merge_request.reload
 | |
| 
 | |
|         expect(merged_merge_request.closed_or_merged_without_fork?).to be_truthy
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context "when the merge request is open" do
 | |
|       let(:open_merge_request) do
 | |
|         create(:merge_request,
 | |
|           source_project: forked_project,
 | |
|           target_project: project)
 | |
|       end
 | |
| 
 | |
|       it "returns false" do
 | |
|         expect(open_merge_request.closed_or_merged_without_fork?).to be_falsey
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#reopenable?' do
 | |
|     context 'when the merge request is closed' do
 | |
|       it 'returns true' do
 | |
|         subject.close
 | |
| 
 | |
|         expect(subject.reopenable?).to be_truthy
 | |
|       end
 | |
| 
 | |
|       context 'forked project' do
 | |
|         let(:project)      { create(:project, :public) }
 | |
|         let(:user)         { create(:user) }
 | |
|         let(:forked_project) { fork_project(project, user) }
 | |
| 
 | |
|         let!(:merge_request) do
 | |
|           create(:closed_merge_request,
 | |
|             source_project: forked_project,
 | |
|             target_project: project)
 | |
|         end
 | |
| 
 | |
|         it 'returns false if unforked' do
 | |
|           Projects::UnlinkForkService.new(forked_project, user).execute
 | |
| 
 | |
|           expect(merge_request.reload.reopenable?).to be_falsey
 | |
|         end
 | |
| 
 | |
|         it 'returns false if the source project is deleted' do
 | |
|           Projects::DestroyService.new(forked_project, user).execute
 | |
| 
 | |
|           expect(merge_request.reload.reopenable?).to be_falsey
 | |
|         end
 | |
| 
 | |
|         it 'returns false if the merge request is merged' do
 | |
|           merge_request.update!(state: 'merged')
 | |
| 
 | |
|           expect(merge_request.reload.reopenable?).to be_falsey
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when the merge request is opened' do
 | |
|       it 'returns false' do
 | |
|         expect(subject.reopenable?).to be_falsey
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#pipeline_coverage_delta' do
 | |
|     let!(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|     let!(:source_pipeline) do
 | |
|       create(:ci_pipeline,
 | |
|         project: project,
 | |
|         ref: merge_request.source_branch,
 | |
|         sha: merge_request.diff_head_sha
 | |
|       )
 | |
|     end
 | |
| 
 | |
|     let!(:target_pipeline) do
 | |
|       create(:ci_pipeline,
 | |
|         project: project,
 | |
|         ref: merge_request.target_branch,
 | |
|         sha: merge_request.diff_base_sha
 | |
|       )
 | |
|     end
 | |
| 
 | |
|     def create_build(pipeline, coverage, name)
 | |
|       create(:ci_build, :success, pipeline: pipeline, coverage: coverage, name: name)
 | |
|       merge_request.update_head_pipeline
 | |
|     end
 | |
| 
 | |
|     context 'when both source and target branches have coverage information' do
 | |
|       it 'returns the appropriate coverage delta' do
 | |
|         create_build(source_pipeline, 60.2, 'test:1')
 | |
|         create_build(target_pipeline, 50, 'test:2')
 | |
| 
 | |
|         expect(merge_request.pipeline_coverage_delta).to be_within(0.001).of(10.2)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when target branch does not have coverage information' do
 | |
|       it 'returns nil' do
 | |
|         create_build(source_pipeline, 50, 'test:1')
 | |
| 
 | |
|         expect(merge_request.pipeline_coverage_delta).to be_nil
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when source branch does not have coverage information' do
 | |
|       it 'returns nil for coverage_delta' do
 | |
|         create_build(target_pipeline, 50, 'test:1')
 | |
| 
 | |
|         expect(merge_request.pipeline_coverage_delta).to be_nil
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'neither source nor target branch has coverage information' do
 | |
|       it 'returns nil for coverage_delta' do
 | |
|         expect(merge_request.pipeline_coverage_delta).to be_nil
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#use_merge_base_pipeline_for_comparison?' do
 | |
|     let(:project) { create(:project, :public, :repository) }
 | |
|     let(:merge_request) { create(:merge_request, :with_codequality_reports, source_project: project) }
 | |
| 
 | |
|     subject { merge_request.use_merge_base_pipeline_for_comparison?(service_class) }
 | |
| 
 | |
|     context 'when service class is Ci::CompareMetricsReportsService' do
 | |
|       let(:service_class) { 'Ci::CompareMetricsReportsService' }
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
| 
 | |
|     context 'when service class is Ci::CompareCodequalityReportsService' do
 | |
|       let(:service_class) { 'Ci::CompareCodequalityReportsService' }
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
| 
 | |
|     context 'when service class is different' do
 | |
|       let(:service_class) { 'Ci::GenerateCoverageReportsService' }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#comparison_base_pipeline' do
 | |
|     subject(:pipeline) { merge_request.comparison_base_pipeline(service_class) }
 | |
| 
 | |
|     let(:project) { create(:project, :public, :repository) }
 | |
|     let(:merge_request) { create(:merge_request, :with_codequality_reports, source_project: project) }
 | |
|     let!(:base_pipeline) do
 | |
|       create(:ci_pipeline,
 | |
|         :with_test_reports,
 | |
|         project: project,
 | |
|         ref: merge_request.target_branch,
 | |
|         sha: merge_request.diff_base_sha
 | |
|       )
 | |
|     end
 | |
| 
 | |
|     context 'when service class is Ci::CompareCodequalityReportsService' do
 | |
|       let(:service_class) { 'Ci::CompareCodequalityReportsService' }
 | |
| 
 | |
|       context 'when merge request has a merge request pipeline' do
 | |
|         let(:merge_request) do
 | |
|           create(:merge_request, :with_merge_request_pipeline)
 | |
|         end
 | |
| 
 | |
|         let(:merge_base_pipeline) do
 | |
|           create(:ci_pipeline, ref: merge_request.target_branch, sha: merge_request.target_branch_sha)
 | |
|         end
 | |
| 
 | |
|         before do
 | |
|           merge_base_pipeline
 | |
|           merge_request.update_head_pipeline
 | |
|         end
 | |
| 
 | |
|         it 'returns the merge_base_pipeline' do
 | |
|           expect(pipeline).to eq(merge_base_pipeline)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when merge does not have a merge request pipeline' do
 | |
|         it 'returns the base_pipeline' do
 | |
|           expect(pipeline).to eq(base_pipeline)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when service_class is different' do
 | |
|       let(:service_class) { 'Ci::GenerateCoverageReportsService' }
 | |
| 
 | |
|       it 'returns the base_pipeline' do
 | |
|         expect(pipeline).to eq(base_pipeline)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#base_pipeline' do
 | |
|     let(:pipeline_arguments) do
 | |
|       {
 | |
|         project: project,
 | |
|         ref: merge_request.target_branch,
 | |
|         sha: merge_request.diff_base_sha
 | |
|       }
 | |
|     end
 | |
| 
 | |
|     let(:project) { create(:project, :public, :repository) }
 | |
|     let(:merge_request) { create(:merge_request, source_project: project) }
 | |
| 
 | |
|     let!(:first_pipeline) { create(:ci_pipeline, pipeline_arguments) }
 | |
|     let!(:last_pipeline) { create(:ci_pipeline, pipeline_arguments) }
 | |
|     let!(:last_pipeline_with_other_ref) { create(:ci_pipeline, pipeline_arguments.merge(ref: 'other')) }
 | |
| 
 | |
|     it 'returns latest pipeline for the target branch' do
 | |
|       expect(merge_request.base_pipeline).to eq(last_pipeline)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#merge_base_pipeline' do
 | |
|     let(:merge_request) do
 | |
|       create(:merge_request, :with_merge_request_pipeline)
 | |
|     end
 | |
| 
 | |
|     let(:merge_base_pipeline) do
 | |
|       create(:ci_pipeline, ref: merge_request.target_branch, sha: merge_request.target_branch_sha)
 | |
|     end
 | |
| 
 | |
|     before do
 | |
|       merge_base_pipeline
 | |
|       merge_request.update_head_pipeline
 | |
|     end
 | |
| 
 | |
|     it 'returns a pipeline pointing to a commit on the target ref' do
 | |
|       expect(merge_request.merge_base_pipeline).to eq(merge_base_pipeline)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#has_commits?' do
 | |
|     it 'returns true when merge request diff has commits' do
 | |
|       allow(subject.merge_request_diff).to receive(:commits_count)
 | |
|         .and_return(2)
 | |
| 
 | |
|       expect(subject.has_commits?).to be_truthy
 | |
|     end
 | |
| 
 | |
|     context 'when commits_count is nil' do
 | |
|       it 'returns false' do
 | |
|         allow(subject.merge_request_diff).to receive(:commits_count)
 | |
|         .and_return(nil)
 | |
| 
 | |
|         expect(subject.has_commits?).to be_falsey
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#has_no_commits?' do
 | |
|     before do
 | |
|       allow(subject.merge_request_diff).to receive(:commits_count)
 | |
|         .and_return(0)
 | |
|     end
 | |
| 
 | |
|     it 'returns true when merge request diff has 0 commits' do
 | |
|       expect(subject.has_no_commits?).to be_truthy
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#merge_request_diff_for' do
 | |
|     let(:project) { create(:project, :repository) }
 | |
| 
 | |
|     subject { create(:merge_request, importing: true, source_project: project) }
 | |
| 
 | |
|     let!(:merge_request_diff1) { subject.merge_request_diffs.create!(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
 | |
|     let!(:merge_request_diff2) { subject.merge_request_diffs.create!(head_commit_sha: nil) }
 | |
|     let!(:merge_request_diff3) { subject.merge_request_diffs.create!(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
 | |
| 
 | |
|     context 'with diff refs' do
 | |
|       it 'returns the diffs' do
 | |
|         expect(subject.merge_request_diff_for(merge_request_diff1.diff_refs)).to eq(merge_request_diff1)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'with a commit SHA' do
 | |
|       it 'returns the diffs' do
 | |
|         expect(subject.merge_request_diff_for(merge_request_diff3.head_commit_sha)).to eq(merge_request_diff3)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     it 'runs a single query on the initial call, and none afterwards' do
 | |
|       expect { subject.merge_request_diff_for(merge_request_diff1.diff_refs) }
 | |
|         .not_to exceed_query_limit(1)
 | |
| 
 | |
|       expect { subject.merge_request_diff_for(merge_request_diff2.diff_refs) }
 | |
|         .not_to exceed_query_limit(0)
 | |
| 
 | |
|       expect { subject.merge_request_diff_for(merge_request_diff3.head_commit_sha) }
 | |
|         .not_to exceed_query_limit(0)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#version_params_for' do
 | |
|     let(:project) { create(:project, :repository) }
 | |
| 
 | |
|     subject { create(:merge_request, importing: true, source_project: project) }
 | |
| 
 | |
|     let!(:merge_request_diff1) { subject.merge_request_diffs.create!(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
 | |
|     let!(:merge_request_diff2) { subject.merge_request_diffs.create!(head_commit_sha: nil) }
 | |
|     let!(:merge_request_diff3) { subject.merge_request_diffs.create!(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
 | |
| 
 | |
|     context 'when the diff refs are for an older merge request version' do
 | |
|       let(:diff_refs) { merge_request_diff1.diff_refs }
 | |
| 
 | |
|       it 'returns the diff ID for the version to show' do
 | |
|         expect(subject.version_params_for(diff_refs)).to eq(diff_id: merge_request_diff1.id)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when the diff refs are for a comparison between merge request versions' do
 | |
|       let(:diff_refs) { merge_request_diff3.compare_with(merge_request_diff1.head_commit_sha).diff_refs }
 | |
| 
 | |
|       it 'returns the diff ID and start sha of the versions to compare' do
 | |
|         expect(subject.version_params_for(diff_refs)).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when the diff refs are not for a merge request version' do
 | |
|       let(:diff_refs) { project.commit(sample_commit.id).diff_refs }
 | |
| 
 | |
|       it 'returns nil' do
 | |
|         expect(subject.version_params_for(diff_refs)).to be_nil
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#fetch_ref!' do
 | |
|     let(:project) { create(:project, :repository) }
 | |
| 
 | |
|     subject { create(:merge_request, source_project: project) }
 | |
| 
 | |
|     it 'fetches the ref correctly' do
 | |
|       expect { subject.target_project.repository.delete_refs(subject.ref_path) }.not_to raise_error
 | |
| 
 | |
|       subject.fetch_ref!
 | |
|       expect(subject.target_project.repository.ref_exists?(subject.ref_path)).to be_truthy
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#eager_fetch_ref!' do
 | |
|     let(:project) { create(:project, :repository) }
 | |
| 
 | |
|     # We use build instead of create to test that an IID is allocated
 | |
|     subject { build(:merge_request, source_project: project) }
 | |
| 
 | |
|     it 'fetches the ref correctly' do
 | |
|       expect(subject.iid).to be_nil
 | |
| 
 | |
|       expect { subject.eager_fetch_ref! }.to change { subject.iid.to_i }.by(1)
 | |
| 
 | |
|       expect(subject.target_project.repository.ref_exists?(subject.ref_path)).to be_truthy
 | |
|     end
 | |
| 
 | |
|     it 'only fetches the ref once after saved' do
 | |
|       expect(subject.target_project.repository).to receive(:fetch_source_branch!).once.and_call_original
 | |
| 
 | |
|       subject.save!
 | |
| 
 | |
|       expect(subject.target_project.repository.ref_exists?(subject.ref_path)).to be_truthy
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe 'removing a merge request' do
 | |
|     it 'refreshes the number of open merge requests of the target project' do
 | |
|       project = subject.target_project
 | |
| 
 | |
|       expect { subject.destroy! }
 | |
|         .to change { project.open_merge_requests_count }.from(1).to(0)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   it_behaves_like 'throttled touch' do
 | |
|     subject { create(:merge_request, updated_at: 1.hour.ago) }
 | |
|   end
 | |
| 
 | |
|   context 'state machine transitions' do
 | |
|     let(:project) { create(:project, :repository) }
 | |
| 
 | |
|     describe '#unlock_mr' do
 | |
|       subject { create(:merge_request, state: 'locked', source_project: project, merge_jid: 123) }
 | |
| 
 | |
|       it 'updates merge request head pipeline and sets merge_jid to nil', :sidekiq_might_not_need_inline do
 | |
|         pipeline = create(:ci_empty_pipeline, project: subject.project, ref: subject.source_branch, sha: subject.source_branch_sha)
 | |
| 
 | |
|         subject.unlock_mr
 | |
| 
 | |
|         subject.reload
 | |
|         expect(subject.head_pipeline).to eq(pipeline)
 | |
|         expect(subject.merge_jid).to be_nil
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe '#mark_as_unchecked' do
 | |
|       subject { create(:merge_request, source_project: project, merge_status: merge_status) }
 | |
| 
 | |
|       shared_examples 'for an invalid state transition' do
 | |
|         it 'is not a valid state transition' do
 | |
|           expect { subject.mark_as_unchecked! }.to raise_error(StateMachines::InvalidTransition)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       shared_examples 'for an valid state transition' do
 | |
|         it 'is a valid state transition' do
 | |
|           expect { subject.mark_as_unchecked! }
 | |
|             .to change { subject.merge_status }
 | |
|             .from(merge_status.to_s)
 | |
|             .to(expected_merge_status)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when the status is unchecked' do
 | |
|         let(:merge_status) { :unchecked }
 | |
| 
 | |
|         include_examples 'for an invalid state transition'
 | |
|       end
 | |
| 
 | |
|       context 'when the status is checking' do
 | |
|         let(:merge_status) { :checking }
 | |
|         let(:expected_merge_status) { 'unchecked' }
 | |
| 
 | |
|         include_examples 'for an valid state transition'
 | |
|       end
 | |
| 
 | |
|       context 'when the status is can_be_merged' do
 | |
|         let(:merge_status) { :can_be_merged }
 | |
|         let(:expected_merge_status) { 'unchecked' }
 | |
| 
 | |
|         include_examples 'for an valid state transition'
 | |
|       end
 | |
| 
 | |
|       context 'when the status is cannot_be_merged_recheck' do
 | |
|         let(:merge_status) { :cannot_be_merged_recheck }
 | |
| 
 | |
|         include_examples 'for an invalid state transition'
 | |
|       end
 | |
| 
 | |
|       context 'when the status is cannot_be_merged' do
 | |
|         let(:merge_status) { :cannot_be_merged }
 | |
|         let(:expected_merge_status) { 'cannot_be_merged_recheck' }
 | |
| 
 | |
|         include_examples 'for an valid state transition'
 | |
|       end
 | |
| 
 | |
|       context 'when the status is cannot_be_merged' do
 | |
|         let(:merge_status) { :cannot_be_merged }
 | |
|         let(:expected_merge_status) { 'cannot_be_merged_recheck' }
 | |
| 
 | |
|         include_examples 'for an valid state transition'
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe 'transition to cannot_be_merged' do
 | |
|       let(:notification_service) { double(:notification_service) }
 | |
|       let(:todo_service) { double(:todo_service) }
 | |
| 
 | |
|       subject { create(:merge_request, state, source_project: project, merge_status: :unchecked) }
 | |
| 
 | |
|       before do
 | |
|         allow(NotificationService).to receive(:new).and_return(notification_service)
 | |
|         allow(TodoService).to receive(:new).and_return(todo_service)
 | |
| 
 | |
|         allow(subject.project.repository).to receive(:can_be_merged?).and_return(false)
 | |
|       end
 | |
| 
 | |
|       [:opened, :locked].each do |state|
 | |
|         context state do
 | |
|           let(:state) { state }
 | |
| 
 | |
|           it 'notifies conflict, but does not notify again if rechecking still results in cannot_be_merged' do
 | |
|             expect(notification_service).to receive(:merge_request_unmergeable).with(subject).once
 | |
|             expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).once
 | |
| 
 | |
|             subject.mark_as_unmergeable!
 | |
| 
 | |
|             subject.mark_as_unchecked!
 | |
|             subject.mark_as_unmergeable!
 | |
|           end
 | |
| 
 | |
|           it 'notifies conflict, but does not notify again if rechecking still results in cannot_be_merged with async mergeability check' do
 | |
|             expect(notification_service).to receive(:merge_request_unmergeable).with(subject).once
 | |
|             expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).once
 | |
| 
 | |
|             subject.mark_as_checking!
 | |
|             subject.mark_as_unmergeable!
 | |
| 
 | |
|             subject.mark_as_unchecked!
 | |
|             subject.mark_as_checking!
 | |
|             subject.mark_as_unmergeable!
 | |
|           end
 | |
| 
 | |
|           it 'notifies conflict, whenever newly unmergeable' do
 | |
|             expect(notification_service).to receive(:merge_request_unmergeable).with(subject).twice
 | |
|             expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).twice
 | |
| 
 | |
|             subject.mark_as_unmergeable!
 | |
| 
 | |
|             subject.mark_as_unchecked!
 | |
|             subject.mark_as_mergeable!
 | |
| 
 | |
|             subject.mark_as_unchecked!
 | |
|             subject.mark_as_unmergeable!
 | |
|           end
 | |
| 
 | |
|           it 'notifies conflict, whenever newly unmergeable with async mergeability check' do
 | |
|             expect(notification_service).to receive(:merge_request_unmergeable).with(subject).twice
 | |
|             expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).twice
 | |
| 
 | |
|             subject.mark_as_checking!
 | |
|             subject.mark_as_unmergeable!
 | |
| 
 | |
|             subject.mark_as_unchecked!
 | |
|             subject.mark_as_checking!
 | |
|             subject.mark_as_mergeable!
 | |
| 
 | |
|             subject.mark_as_unchecked!
 | |
|             subject.mark_as_checking!
 | |
|             subject.mark_as_unmergeable!
 | |
|           end
 | |
| 
 | |
|           it 'does not notify whenever merge request is newly unmergeable due to other reasons' do
 | |
|             allow(subject.project.repository).to receive(:can_be_merged?).and_return(true)
 | |
| 
 | |
|             expect(notification_service).not_to receive(:merge_request_unmergeable)
 | |
|             expect(todo_service).not_to receive(:merge_request_became_unmergeable)
 | |
| 
 | |
|             subject.mark_as_unmergeable!
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       [:closed, :merged].each do |state|
 | |
|         let(:state) { state }
 | |
| 
 | |
|         context state do
 | |
|           it 'does not notify' do
 | |
|             expect(notification_service).not_to receive(:merge_request_unmergeable)
 | |
|             expect(todo_service).not_to receive(:merge_request_became_unmergeable)
 | |
| 
 | |
|             subject.mark_as_unmergeable!
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'source branch is missing' do
 | |
|         subject { create(:merge_request, :invalid, :opened, source_project: project, merge_status: :unchecked, target_branch: 'master') }
 | |
| 
 | |
|         before do
 | |
|           allow(subject.project.repository).to receive(:can_be_merged?).and_call_original
 | |
|         end
 | |
| 
 | |
|         it 'does not raise error' do
 | |
|           expect(notification_service).not_to receive(:merge_request_unmergeable)
 | |
|           expect(todo_service).not_to receive(:merge_request_became_unmergeable)
 | |
| 
 | |
|           expect { subject.mark_as_unmergeable }.not_to raise_error
 | |
|           expect(subject.cannot_be_merged?).to eq(true)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     describe 'check_state?' do
 | |
|       it 'indicates whether MR is still checking for mergeability' do
 | |
|         state_machine = described_class.state_machines[:merge_status]
 | |
|         check_states = [:unchecked, :cannot_be_merged_recheck, :cannot_be_merged_rechecking, :checking]
 | |
| 
 | |
|         check_states.each do |merge_status|
 | |
|           expect(state_machine.check_state?(merge_status)).to be true
 | |
|         end
 | |
| 
 | |
|         (state_machine.states.map(&:name) - check_states).each do |merge_status|
 | |
|           expect(state_machine.check_state?(merge_status)).to be false
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#should_be_rebased?' do
 | |
|     it 'returns false for the same source and target branches' do
 | |
|       merge_request = build_stubbed(:merge_request, source_project: project, target_project: project)
 | |
| 
 | |
|       expect(merge_request.should_be_rebased?).to be_falsey
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#rebase_in_progress?' do
 | |
|     where(:rebase_jid, :jid_valid, :result) do
 | |
|       'foo' | true  | true
 | |
|       'foo' | false | false
 | |
|       ''    | true  | false
 | |
|       nil   | true  | false
 | |
|     end
 | |
| 
 | |
|     with_them do
 | |
|       let(:merge_request) { build_stubbed(:merge_request) }
 | |
| 
 | |
|       subject { merge_request.rebase_in_progress? }
 | |
| 
 | |
|       it do
 | |
|         allow(Gitlab::SidekiqStatus).to receive(:running?).with(rebase_jid) { jid_valid }
 | |
| 
 | |
|         merge_request.rebase_jid = rebase_jid
 | |
| 
 | |
|         is_expected.to eq(result)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#allow_collaboration' do
 | |
|     let(:merge_request) do
 | |
|       build(:merge_request, source_branch: 'fixes', allow_collaboration: true)
 | |
|     end
 | |
| 
 | |
|     it 'is false when pushing by a maintainer is not possible' do
 | |
|       expect(merge_request).to receive(:collaborative_push_possible?) { false }
 | |
| 
 | |
|       expect(merge_request.allow_collaboration).to be_falsy
 | |
|     end
 | |
| 
 | |
|     it 'is true when pushing by a maintainer is possible' do
 | |
|       expect(merge_request).to receive(:collaborative_push_possible?) { true }
 | |
| 
 | |
|       expect(merge_request.allow_collaboration).to be_truthy
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#collaborative_push_possible?' do
 | |
|     let(:merge_request) do
 | |
|       build(:merge_request, source_branch: 'fixes')
 | |
|     end
 | |
| 
 | |
|     before do
 | |
|       allow(ProtectedBranch).to receive(:protected?) { false }
 | |
|     end
 | |
| 
 | |
|     it 'does not allow maintainer to push if the source project is the same as the target' do
 | |
|       merge_request.target_project = merge_request.source_project = create(:project, :public)
 | |
| 
 | |
|       expect(merge_request.collaborative_push_possible?).to be_falsy
 | |
|     end
 | |
| 
 | |
|     it 'allows maintainer to push when both source and target are public' do
 | |
|       merge_request.target_project = build(:project, :public)
 | |
|       merge_request.source_project = build(:project, :public)
 | |
| 
 | |
|       expect(merge_request.collaborative_push_possible?).to be_truthy
 | |
|     end
 | |
| 
 | |
|     it 'is not available for protected branches' do
 | |
|       merge_request.target_project = build(:project, :public)
 | |
|       merge_request.source_project = build(:project, :public)
 | |
| 
 | |
|       expect(ProtectedBranch).to receive(:protected?)
 | |
|                                    .with(merge_request.source_project, 'fixes')
 | |
|                                    .and_return(true)
 | |
| 
 | |
|       expect(merge_request.collaborative_push_possible?).to be_falsy
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#can_allow_collaboration?' do
 | |
|     let(:target_project) { create(:project, :public) }
 | |
|     let(:source_project) { fork_project(target_project) }
 | |
|     let(:merge_request) do
 | |
|       create(:merge_request,
 | |
|              source_project: source_project,
 | |
|              source_branch: 'fixes',
 | |
|              target_project: target_project)
 | |
|     end
 | |
| 
 | |
|     let(:user) { create(:user) }
 | |
| 
 | |
|     before do
 | |
|       allow(merge_request).to receive(:collaborative_push_possible?) { true }
 | |
|     end
 | |
| 
 | |
|     it 'is false if the user does not have push access to the source project' do
 | |
|       expect(merge_request.can_allow_collaboration?(user)).to be_falsy
 | |
|     end
 | |
| 
 | |
|     it 'is true when the user has push access to the source project' do
 | |
|       source_project.add_developer(user)
 | |
| 
 | |
|       expect(merge_request.can_allow_collaboration?(user)).to be_truthy
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#merge_participants' do
 | |
|     it 'contains author' do
 | |
|       expect(subject.merge_participants).to contain_exactly(subject.author)
 | |
|     end
 | |
| 
 | |
|     describe 'when merge_when_pipeline_succeeds? is true' do
 | |
|       describe 'when merge user is author' do
 | |
|         let(:user) { create(:user) }
 | |
| 
 | |
|         subject do
 | |
|           create(:merge_request,
 | |
|                  merge_when_pipeline_succeeds: true,
 | |
|                  merge_user: user,
 | |
|                  author: user)
 | |
|         end
 | |
| 
 | |
|         context 'author is not a project member' do
 | |
|           it 'is empty' do
 | |
|             expect(subject.merge_participants).to be_empty
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'author is a project member' do
 | |
|           before do
 | |
|             subject.project.team.add_reporter(user)
 | |
|           end
 | |
| 
 | |
|           it 'contains author only' do
 | |
|             expect(subject.merge_participants).to contain_exactly(subject.author)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       describe 'when merge user and author are different users' do
 | |
|         let(:merge_user) { create(:user) }
 | |
| 
 | |
|         subject do
 | |
|           create(:merge_request,
 | |
|                  merge_when_pipeline_succeeds: true,
 | |
|                  merge_user: merge_user)
 | |
|         end
 | |
| 
 | |
|         before do
 | |
|           subject.project.team.add_reporter(subject.author)
 | |
|         end
 | |
| 
 | |
|         context 'merge user is not a member' do
 | |
|           it 'contains author only' do
 | |
|             expect(subject.merge_participants).to contain_exactly(subject.author)
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'both author and merge users are project members' do
 | |
|           before do
 | |
|             subject.project.team.add_reporter(merge_user)
 | |
|           end
 | |
| 
 | |
|           it 'contains author and merge user' do
 | |
|             expect(subject.merge_participants).to contain_exactly(subject.author, merge_user)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.merge_request_ref?' do
 | |
|     subject { described_class.merge_request_ref?(ref) }
 | |
| 
 | |
|     context 'when ref is ref name of a branch' do
 | |
|       let(:ref) { 'feature' }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
| 
 | |
|     context 'when ref is HEAD ref path of a branch' do
 | |
|       let(:ref) { 'refs/heads/feature' }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
| 
 | |
|     context 'when ref is HEAD ref path of a merge request' do
 | |
|       let(:ref) { 'refs/merge-requests/1/head' }
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
| 
 | |
|     context 'when ref is merge ref path of a merge request' do
 | |
|       let(:ref) { 'refs/merge-requests/1/merge' }
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.merge_train_ref?' do
 | |
|     subject { described_class.merge_train_ref?(ref) }
 | |
| 
 | |
|     context 'when ref is ref name of a branch' do
 | |
|       let(:ref) { 'feature' }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
| 
 | |
|     context 'when ref is HEAD ref path of a branch' do
 | |
|       let(:ref) { 'refs/heads/feature' }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
| 
 | |
|     context 'when ref is HEAD ref path of a merge request' do
 | |
|       let(:ref) { 'refs/merge-requests/1/head' }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
| 
 | |
|     context 'when ref is merge ref path of a merge request' do
 | |
|       let(:ref) { 'refs/merge-requests/1/merge' }
 | |
| 
 | |
|       it { is_expected.to be_falsey }
 | |
|     end
 | |
| 
 | |
|     context 'when ref is train ref path of a merge request' do
 | |
|       let(:ref) { 'refs/merge-requests/1/train' }
 | |
| 
 | |
|       it { is_expected.to be_truthy }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#cleanup_refs' do
 | |
|     subject { merge_request.cleanup_refs(only: only) }
 | |
| 
 | |
|     let(:merge_request) { build(:merge_request, source_project: create(:project, :repository)) }
 | |
| 
 | |
|     context 'when removing all refs' do
 | |
|       let(:only) { :all }
 | |
| 
 | |
|       it 'deletes all refs from the target project' do
 | |
|         expect(merge_request.target_project.repository)
 | |
|           .to receive(:delete_refs)
 | |
|           .with(merge_request.ref_path, merge_request.merge_ref_path, merge_request.train_ref_path)
 | |
| 
 | |
|         subject
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when removing only train ref' do
 | |
|       let(:only) { :train }
 | |
| 
 | |
|       it 'deletes train ref from the target project' do
 | |
|         expect(merge_request.target_project.repository)
 | |
|           .to receive(:delete_refs)
 | |
|           .with(merge_request.train_ref_path)
 | |
| 
 | |
|         subject
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.with_auto_merge_enabled' do
 | |
|     let!(:project) { create(:project) }
 | |
|     let!(:fork) { fork_project(project) }
 | |
|     let!(:merge_request1) do
 | |
|       create(:merge_request,
 | |
|              :merge_when_pipeline_succeeds,
 | |
|              target_project: project,
 | |
|              target_branch: 'master',
 | |
|              source_project: project,
 | |
|              source_branch: 'feature-1')
 | |
|     end
 | |
| 
 | |
|     let!(:merge_request4) do
 | |
|       create(:merge_request,
 | |
|              target_project: project,
 | |
|              target_branch: 'master',
 | |
|              source_project: fork,
 | |
|              source_branch: 'fork-feature-2')
 | |
|     end
 | |
| 
 | |
|     let(:query) { described_class.with_auto_merge_enabled }
 | |
| 
 | |
|     it { expect(query).to contain_exactly(merge_request1) }
 | |
|   end
 | |
| 
 | |
|   it_behaves_like 'versioned description'
 | |
| 
 | |
|   describe '#commits' do
 | |
|     context 'persisted merge request' do
 | |
|       context 'with a limit' do
 | |
|         it 'returns a limited number of commits' do
 | |
|           expect(subject.commits(limit: 2).map(&:sha)).to eq(%w[
 | |
|             b83d6e391c22777fca1ed3012fce84f633d7fed0
 | |
|             498214de67004b1da3d820901307bed2a68a8ef6
 | |
|           ])
 | |
|           expect(subject.commits(limit: 3).map(&:sha)).to eq(%w[
 | |
|             b83d6e391c22777fca1ed3012fce84f633d7fed0
 | |
|             498214de67004b1da3d820901307bed2a68a8ef6
 | |
|             1b12f15a11fc6e62177bef08f47bc7b5ce50b141
 | |
|           ])
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'without a limit' do
 | |
|         it 'returns all commits of the merge request diff' do
 | |
|           expect(subject.commits.size).to eq(29)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'new merge request' do
 | |
|       subject { build(:merge_request) }
 | |
| 
 | |
|       context 'compare commits' do
 | |
|         let(:first_commit) { double }
 | |
|         let(:second_commit) { double }
 | |
| 
 | |
|         before do
 | |
|           subject.compare_commits = [
 | |
|             first_commit, second_commit
 | |
|           ]
 | |
|         end
 | |
| 
 | |
|         context 'without a limit' do
 | |
|           it 'returns all the compare commits' do
 | |
|             expect(subject.commits.to_a).to eq([second_commit, first_commit])
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'with a limit' do
 | |
|           it 'returns a limited number of commits' do
 | |
|             expect(subject.commits(limit: 1).to_a).to eq([second_commit])
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#recent_commits' do
 | |
|     before do
 | |
|       stub_const("#{MergeRequestDiff}::COMMITS_SAFE_SIZE", 2)
 | |
|     end
 | |
| 
 | |
|     it 'returns the safe number of commits' do
 | |
|       expect(subject.recent_commits.map(&:sha)).to eq(%w[
 | |
|         b83d6e391c22777fca1ed3012fce84f633d7fed0 498214de67004b1da3d820901307bed2a68a8ef6
 | |
|       ])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#recent_visible_deployments' do
 | |
|     let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|     it 'returns visible deployments' do
 | |
|       envs = create_list(:environment, 3, project: merge_request.target_project)
 | |
| 
 | |
|       created = create(
 | |
|         :deployment,
 | |
|         :created,
 | |
|         project: merge_request.target_project,
 | |
|         environment: envs[0]
 | |
|       )
 | |
| 
 | |
|       success = create(
 | |
|         :deployment,
 | |
|         :success,
 | |
|         project: merge_request.target_project,
 | |
|         environment: envs[1]
 | |
|       )
 | |
| 
 | |
|       failed = create(
 | |
|         :deployment,
 | |
|         :failed,
 | |
|         project: merge_request.target_project,
 | |
|         environment: envs[2]
 | |
|       )
 | |
| 
 | |
|       merge_request_relation = MergeRequest.where(id: merge_request.id)
 | |
|       created.link_merge_requests(merge_request_relation)
 | |
|       success.link_merge_requests(merge_request_relation)
 | |
|       failed.link_merge_requests(merge_request_relation)
 | |
| 
 | |
|       expect(merge_request.recent_visible_deployments).to eq([failed, success])
 | |
|     end
 | |
| 
 | |
|     it 'only returns a limited number of deployments' do
 | |
|       20.times do
 | |
|         environment = create(:environment, project: merge_request.target_project)
 | |
|         deploy = create(
 | |
|           :deployment,
 | |
|           :success,
 | |
|           project: merge_request.target_project,
 | |
|           environment: environment
 | |
|         )
 | |
| 
 | |
|         deploy.link_merge_requests(MergeRequest.where(id: merge_request.id))
 | |
|       end
 | |
| 
 | |
|       expect(merge_request.recent_visible_deployments.count).to eq(10)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#diffable_merge_ref?' do
 | |
|     let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|     context 'merge request can be merged' do
 | |
|       context 'merge_head diff is not created' do
 | |
|         it 'returns true' do
 | |
|           expect(merge_request.diffable_merge_ref?).to eq(false)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'merge_head diff is created' do
 | |
|         before do
 | |
|           create(:merge_request_diff, :merge_head, merge_request: merge_request)
 | |
|         end
 | |
| 
 | |
|         it 'returns true' do
 | |
|           expect(merge_request.diffable_merge_ref?).to eq(true)
 | |
|         end
 | |
| 
 | |
|         context 'merge request is merged' do
 | |
|           before do
 | |
|             merge_request.mark_as_merged!
 | |
|           end
 | |
| 
 | |
|           it 'returns false' do
 | |
|             expect(merge_request.diffable_merge_ref?).to eq(false)
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'merge request cannot be merged' do
 | |
|           before do
 | |
|             merge_request.mark_as_unchecked!
 | |
|           end
 | |
| 
 | |
|           it 'returns false' do
 | |
|             expect(merge_request.diffable_merge_ref?).to eq(true)
 | |
|           end
 | |
| 
 | |
|           context 'display_merge_conflicts_in_diff is disabled' do
 | |
|             before do
 | |
|               stub_feature_flags(display_merge_conflicts_in_diff: false)
 | |
|             end
 | |
| 
 | |
|             it 'returns false' do
 | |
|               expect(merge_request.diffable_merge_ref?).to eq(false)
 | |
|             end
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#predefined_variables' do
 | |
|     let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|     it 'caches all SQL-sourced data on the first call' do
 | |
|       control = ActiveRecord::QueryRecorder.new { merge_request.predefined_variables }.count
 | |
| 
 | |
|       expect(control).to be > 0
 | |
| 
 | |
|       count = ActiveRecord::QueryRecorder.new { merge_request.predefined_variables }.count
 | |
| 
 | |
|       expect(count).to eq(0)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe 'banzai_render_context' do
 | |
|     let(:project) { build(:project_empty_repo) }
 | |
|     let(:merge_request) { build :merge_request, target_project: project, source_project: project }
 | |
| 
 | |
|     subject(:context) { merge_request.banzai_render_context(:title) }
 | |
| 
 | |
|     it 'sets the label_url_method in the context' do
 | |
|       expect(context[:label_url_method]).to eq(:project_merge_requests_url)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#head_pipeline_builds_with_coverage' do
 | |
|     it 'delegates to head_pipeline' do
 | |
|       expect(subject)
 | |
|         .to delegate_method(:builds_with_coverage)
 | |
|         .to(:head_pipeline)
 | |
|         .with_prefix
 | |
|         .with_arguments(allow_nil: true)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#merge_ref_head' do
 | |
|     let(:merge_request) { create(:merge_request) }
 | |
| 
 | |
|     context 'when merge_ref_sha is not present' do
 | |
|       let!(:result) do
 | |
|         MergeRequests::MergeToRefService
 | |
|           .new(project: merge_request.project, current_user: merge_request.author)
 | |
|           .execute(merge_request)
 | |
|       end
 | |
| 
 | |
|       it 'returns the commit based on merge ref path' do
 | |
|         expect(merge_request.merge_ref_head.id).to eq(result[:commit_id])
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when merge_ref_sha is present' do
 | |
|       before do
 | |
|         merge_request.update!(merge_ref_sha: merge_request.project.repository.commit.id)
 | |
|       end
 | |
| 
 | |
|       it 'returns the commit based on cached merge_ref_sha' do
 | |
|         expect(merge_request.merge_ref_head.id).to eq(merge_request.merge_ref_sha)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#allows_reviewers?' do
 | |
|     it 'returns true' do
 | |
|       merge_request = build_stubbed(:merge_request)
 | |
| 
 | |
|       expect(merge_request.allows_reviewers?).to be(true)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#update_and_mark_in_progress_merge_commit_sha' do
 | |
|     let(:ref) { subject.target_project.repository.commit.id }
 | |
| 
 | |
|     before do
 | |
|       expect(subject.target_project).to receive(:mark_primary_write_location)
 | |
|     end
 | |
| 
 | |
|     it 'updates commit ID' do
 | |
|       expect { subject.update_and_mark_in_progress_merge_commit_sha(ref) }
 | |
|         .to change { subject.in_progress_merge_commit_sha }
 | |
|         .from(nil).to(ref)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#enabled_reports' do
 | |
|     let(:project) { create(:project, :repository) }
 | |
| 
 | |
|     where(:report_type, :with_reports, :feature) do
 | |
|       :sast                | :with_sast_reports                | :sast
 | |
|       :secret_detection    | :with_secret_detection_reports    | :secret_detection
 | |
|     end
 | |
| 
 | |
|     with_them do
 | |
|       subject { merge_request.enabled_reports[report_type] }
 | |
| 
 | |
|       before do
 | |
|         stub_licensed_features({ feature => true })
 | |
|       end
 | |
| 
 | |
|       context "when head pipeline has reports" do
 | |
|         let(:merge_request) { create(:merge_request, with_reports, source_project: project) }
 | |
| 
 | |
|         it { is_expected.to be_truthy }
 | |
|       end
 | |
| 
 | |
|       context "when head pipeline does not have reports" do
 | |
|         let(:merge_request) { create(:merge_request, source_project: project) }
 | |
| 
 | |
|         it { is_expected.to be_falsy }
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#includes_ci_config?' do
 | |
|     let(:merge_request) { build(:merge_request) }
 | |
|     let(:project) { merge_request.project }
 | |
| 
 | |
|     subject(:result) { merge_request.includes_ci_config? }
 | |
| 
 | |
|     before do
 | |
|       allow(merge_request).to receive(:diff_stats).and_return(diff_stats)
 | |
|     end
 | |
| 
 | |
|     context 'when diff_stats is nil' do
 | |
|       let(:diff_stats) {}
 | |
| 
 | |
|       it { is_expected.to eq(false) }
 | |
|     end
 | |
| 
 | |
|     context 'when diff_stats does not include the ci config path of the project' do
 | |
|       let(:diff_stats) { [double(path: 'abc.txt')] }
 | |
| 
 | |
|       it { is_expected.to eq(false) }
 | |
|     end
 | |
| 
 | |
|     context 'when diff_stats includes the ci config path of the project' do
 | |
|       let(:diff_stats) { [double(path: '.gitlab-ci.yml')] }
 | |
| 
 | |
|       it { is_expected.to eq(true) }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.from_fork' do
 | |
|     let!(:project) { create(:project, :repository) }
 | |
|     let!(:forked_project) { fork_project(project) }
 | |
|     let!(:fork_mr) { create(:merge_request, source_project: forked_project, target_project: project) }
 | |
|     let!(:regular_mr) { create(:merge_request, source_project: project) }
 | |
| 
 | |
|     it 'returns merge requests from forks only' do
 | |
|       expect(described_class.from_fork).to eq([fork_mr])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   it_behaves_like 'it has loose foreign keys' do
 | |
|     let(:factory_name) { :merge_request }
 | |
|   end
 | |
| 
 | |
|   context 'loose foreign key on merge_requests.head_pipeline_id' do
 | |
|     it_behaves_like 'cleanup by a loose foreign key' do
 | |
|       let!(:parent) { create(:ci_pipeline) }
 | |
|       let!(:model) { create(:merge_request, head_pipeline: parent) }
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#merge_request_reviewers_with' do
 | |
|     let_it_be(:reviewer1) { create(:user) }
 | |
|     let_it_be(:reviewer2) { create(:user) }
 | |
| 
 | |
|     before do
 | |
|       subject.update!(reviewers: [reviewer1, reviewer2])
 | |
|     end
 | |
| 
 | |
|     it 'returns reviewers' do
 | |
|       reviewers = subject.merge_request_reviewers_with([reviewer1.id])
 | |
| 
 | |
|       expect(reviewers).to match_array([subject.merge_request_reviewers[0]])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#merge_request_assignees_with' do
 | |
|     let_it_be(:assignee1) { create(:user) }
 | |
|     let_it_be(:assignee2) { create(:user) }
 | |
| 
 | |
|     before do
 | |
|       subject.update!(assignees: [assignee1, assignee2])
 | |
|     end
 | |
| 
 | |
|     it 'returns assignees' do
 | |
|       assignees = subject.merge_request_assignees_with([assignee1.id])
 | |
| 
 | |
|       expect(assignees).to match_array([subject.merge_request_assignees[0]])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#recent_diff_head_shas' do
 | |
|     let_it_be(:merge_request_with_diffs) do
 | |
|       params = {
 | |
|         target_project: project,
 | |
|         source_project: project,
 | |
|         target_branch: 'master',
 | |
|         source_branch: 'feature'
 | |
|       }
 | |
| 
 | |
|       create(:merge_request, params).tap do |mr|
 | |
|         4.times { mr.merge_request_diffs.create! }
 | |
|         mr.create_merge_head_diff
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     let(:shas) do
 | |
|       # re-find to avoid caching the association
 | |
|       described_class.find(merge_request_with_diffs.id).merge_request_diffs.order(id: :desc).pluck(:head_commit_sha)
 | |
|     end
 | |
| 
 | |
|     shared_examples 'correctly sorted and limited diff_head_shas' do
 | |
|       it 'has up to MAX_RECENT_DIFF_HEAD_SHAS, ordered most recent first' do
 | |
|         stub_const('MergeRequest::MAX_RECENT_DIFF_HEAD_SHAS', 3)
 | |
| 
 | |
|         expect(subject.recent_diff_head_shas).to eq(shas.first(3))
 | |
|       end
 | |
| 
 | |
|       it 'supports limits' do
 | |
|         expect(subject.recent_diff_head_shas(2)).to eq(shas.first(2))
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when the association is not loaded' do
 | |
|       subject(:mr) { merge_request_with_diffs }
 | |
| 
 | |
|       include_examples 'correctly sorted and limited diff_head_shas'
 | |
|     end
 | |
| 
 | |
|     context 'when the association is loaded' do
 | |
|       subject(:mr) do
 | |
|         described_class.where(id: merge_request_with_diffs.id).preload(:merge_request_diffs).first
 | |
|       end
 | |
| 
 | |
|       include_examples 'correctly sorted and limited diff_head_shas'
 | |
| 
 | |
|       it 'does not issue any queries' do
 | |
|         expect(subject).to be_a(described_class) # preload here
 | |
| 
 | |
|         expect { subject.recent_diff_head_shas }.not_to exceed_query_limit(0)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| end
 |