ee/spec/models/merge_request_spec.rb (2,699 lines of code) (raw):
# frozen_string_literal: true
require 'spec_helper'
# Store feature-specific specs in `ee/spec/models/merge_request instead of
# making this file longer.
#
# For instance, `ee/spec/models/merge_request/blocking_spec.rb` tests the
# "blocking MRs" feature.
RSpec.describe MergeRequest, feature_category: :code_review_workflow do
using RSpec::Parameterized::TableSyntax
include ReactiveCachingHelpers
let_it_be_with_reload(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
describe 'associations' do
subject { build_stubbed(:merge_request) }
it { is_expected.to belong_to(:iteration) }
it { is_expected.to have_many(:approvals).dependent(:delete_all) }
it { is_expected.to have_many(:approvers).dependent(:delete_all) }
it { is_expected.to have_many(:approver_users).through(:approvers) }
it { is_expected.to have_many(:approver_groups).dependent(:delete_all) }
it { is_expected.to have_many(:approved_by_users) }
it { is_expected.to have_one(:merge_train_car) }
it { is_expected.to have_many(:approval_rules) }
it { is_expected.to have_many(:approval_merge_request_rule_sources).through(:approval_rules) }
it { is_expected.to have_many(:approval_project_rules).through(:approval_merge_request_rule_sources) }
it { is_expected.to have_many(:status_check_responses).class_name('MergeRequests::StatusCheckResponse').inverse_of(:merge_request) }
it { is_expected.to have_many(:scan_result_policy_reads_through_violations).through(:scan_result_policy_violations).class_name('Security::ScanResultPolicyRead') }
it { is_expected.to have_many(:scan_result_policy_reads_through_approval_rules).through(:approval_rules).class_name('Security::ScanResultPolicyRead') }
it { is_expected.to have_many(:security_policies_through_violations).through(:scan_result_policy_violations).class_name('Security::Policy') }
it { is_expected.to have_many(:change_requesters).through(:requested_changes) }
describe 'policy violations' do
let(:policy_1) { create(:scan_result_policy_read, project: project) }
let(:policy_2) { create(:scan_result_policy_read, project: project) }
let(:policy_3) { create(:scan_result_policy_read, project: project) }
let(:policy_4) { create(:scan_result_policy_read, project: project) }
let!(:running_violation_1) do
create(:scan_result_policy_violation, :running, project: project, merge_request: merge_request,
scan_result_policy_read: policy_1, violation_data: nil)
end
let!(:running_violation_2) do
create(:scan_result_policy_violation, :running, project: project, merge_request: merge_request,
scan_result_policy_read: policy_2, violation_data: nil)
end
let!(:failed_violation_1) do
create(:scan_result_policy_violation, :failed, project: project, merge_request: merge_request,
scan_result_policy_read: policy_3, violation_data: nil)
end
let!(:failed_violation_2) do
create(:scan_result_policy_violation, :failed, project: project, merge_request: merge_request,
scan_result_policy_read: policy_4, violation_data: nil)
end
describe '.running_scan_result_policy_violations' do
it 'returns only the running violations' do
expect(merge_request.running_scan_result_policy_violations).to contain_exactly(running_violation_1,
running_violation_2)
end
end
describe '.failed_scan_result_policy_violations' do
it 'returns only the completed violations' do
expect(merge_request.failed_scan_result_policy_violations).to contain_exactly(failed_violation_1,
failed_violation_2)
end
end
end
describe 'approval_rules association' do
describe 'applicable_post_merge_approval_rules' do
it 'returns only the rules applicable_post_merge true or nil' do
create(:approval_merge_request_rule, merge_request: merge_request, applicable_post_merge: false)
rule_2 = create(:approval_merge_request_rule, merge_request: merge_request, applicable_post_merge: true)
rule_3 = create(:approval_merge_request_rule, merge_request: merge_request, applicable_post_merge: true)
expect(merge_request.applicable_post_merge_approval_rules).to contain_exactly(rule_2, rule_3)
end
end
describe '#applicable_to_branch' do
let!(:rule) { create(:approval_merge_request_rule, merge_request: merge_request) }
let(:branch) { 'stable' }
subject { merge_request.approval_rules.applicable_to_branch(branch) }
shared_examples_for 'with applicable rules to specified branch' do
it { is_expected.to eq([rule]) }
end
context 'when there are no associated source rules' do
it_behaves_like 'with applicable rules to specified branch'
end
context 'when there are associated source rules' do
let!(:source_rule) { create(:approval_project_rule, project: merge_request.target_project) }
let!(:rule) { create(:approval_merge_request_rule, merge_request: merge_request, approval_project_rule: source_rule) }
context 'and rule is not modified_from_project_rule' do
before do
rule.update!(
name: source_rule.name,
approvals_required: source_rule.approvals_required,
users: source_rule.users,
groups: source_rule.groups
)
end
context 'and there are no associated protected branches to source rule' do
it_behaves_like 'with applicable rules to specified branch'
end
context 'and there are associated protected branches to source rule' do
before do
source_rule.update!(protected_branches: protected_branches)
end
context 'and branch matches' do
let(:protected_branches) { [create(:protected_branch, name: branch)] }
it_behaves_like 'with applicable rules to specified branch'
end
context 'and branch does not match anything' do
let(:protected_branches) { [create(:protected_branch, name: branch.reverse)] }
it { is_expected.to be_empty }
end
end
end
context 'and rule is modified_from_project_rule' do
before do
rule.update!(name: 'Overridden Rule')
end
it_behaves_like 'with applicable rules to specified branch'
end
context 'and rule is overridden but not modified_from_project_rule' do
let!(:rule) { create(:approval_merge_request_rule, name: 'test', merge_request: merge_request, approval_project_rule: source_rule) }
it_behaves_like 'with applicable rules to specified branch'
context 'and protected branches exist but branch does not match anything' do
let(:protected_branches) { [create(:protected_branch, name: branch.reverse)] }
before do
source_rule.update!(protected_branches: protected_branches)
end
it 'does not find applicable rules' do
expect(subject).to be_empty
end
end
end
end
end
describe '#set_applicable_when_copying_rules' do
let!(:rule_2) { create(:approval_merge_request_rule, merge_request: merge_request, applicable_post_merge: nil) }
let!(:rule_1) { create(:approval_merge_request_rule, merge_request: merge_request, applicable_post_merge: nil) }
let(:ids) { [rule_2.id] }
subject { merge_request.approval_rules.set_applicable_when_copying_rules(ids) }
it 'sets all the ids to true and others to false' do
pre_set = merge_request.approval_rules.all? { |rule| rule.applicable_post_merge.nil? }
expect(pre_set).to eq(true)
subject
expect(rule_1.reload.applicable_post_merge).to eq(false)
expect(rule_2.reload.applicable_post_merge).to eq(true)
end
end
end
describe '#merge_requests_author_approval?' do
context 'when project lacks a target_project relation' do
before do
merge_request.target_project = nil
end
it 'returns false' do
expect(merge_request.merge_requests_author_approval?).to be false
end
end
context 'when project has a target_project relation' do
it 'accesses the value from the target_project' do
expect(merge_request.target_project)
.to receive(:merge_requests_author_approval?)
merge_request.merge_requests_author_approval?
end
context 'when overriden by scan result policy' do
let(:policy) do
create(
:scan_result_policy_read,
:prevent_approval_by_author,
commits: :any,
project: merge_request.target_project)
end
before do
merge_request.target_project.update_attribute(:merge_requests_author_approval, true)
create(
:approval_merge_request_rule,
:any_merge_request,
merge_request: merge_request,
scan_result_policy_read: policy)
create(
:scan_result_policy_violation,
project: project,
merge_request: merge_request,
scan_result_policy_read: policy)
end
it 'returns false' do
expect(merge_request.merge_requests_author_approval?).to be(false)
end
end
end
end
describe '#merge_requests_disable_committers_approval?' do
context 'when project lacks a target_project relation' do
before do
merge_request.target_project = nil
end
it 'returns false' do
expect(merge_request.merge_requests_disable_committers_approval?).to be false
end
end
context 'when project has a target_project relation' do
it 'accesses the value from the target_project' do
expect(merge_request.target_project)
.to receive(:merge_requests_disable_committers_approval?)
merge_request.merge_requests_disable_committers_approval?
end
context 'when overriden by scan result policy' do
let(:policy) do
create(
:scan_result_policy_read,
:prevent_approval_by_commit_author,
commits: :any,
project: merge_request.target_project)
end
before do
merge_request.target_project.update_attribute(:merge_requests_disable_committers_approval, false)
create(
:approval_merge_request_rule,
:any_merge_request,
merge_request: merge_request,
scan_result_policy_read: policy)
create(
:scan_result_policy_violation,
project: project,
merge_request: merge_request,
scan_result_policy_read: policy)
end
it 'returns false' do
expect(merge_request.merge_requests_disable_committers_approval?).to be(true)
end
end
end
end
describe '#require_password_to_approve?' do
subject { merge_request.require_password_to_approve? }
let(:password_required?) { true }
before do
merge_request.target_project.update!(require_password_to_approve: password_required?)
end
context 'when target project requires password' do
it { is_expected.to be(password_required?) }
end
context 'when target project does not require password' do
let(:password_required?) { false }
it { is_expected.to be(password_required?) }
context 'when overridden by scan result policy' do
let(:policy) do
create(
:scan_result_policy_read,
:require_password_to_approve,
commits: :any,
project: merge_request.target_project)
end
before do
merge_request.target_project.update_attribute(:merge_requests_disable_committers_approval, false)
create(
:approval_merge_request_rule,
:any_merge_request,
merge_request: merge_request,
scan_result_policy_read: policy)
create(
:scan_result_policy_violation,
project: project,
merge_request: merge_request,
scan_result_policy_read: policy)
end
it { is_expected.to be(true) }
end
end
describe '#policy_approval_settings' do
let(:project_approval_settings) do
{ prevent_approval_by_author: true,
prevent_approval_by_commit_author: false,
remove_approvals_with_new_commit: true,
require_password_to_approve: false }
end
let(:policy) do
create(:scan_result_policy_read,
project: merge_request.target_project,
project_approval_settings: project_approval_settings)
end
let(:overrides) { project_approval_settings.select { |_, v| v } }
subject(:approval_settings) { merge_request.policy_approval_settings }
context 'with scan finding rule' do
let!(:approval_merge_request_rule) do
create(:report_approver_rule,
:scan_finding,
merge_request: merge_request,
scan_result_policy_read: policy)
end
context 'when violated' do
before do
create(:scan_result_policy_violation, project: project, merge_request: merge_request,
scan_result_policy_read: approval_merge_request_rule.scan_result_policy_read)
end
it { is_expected.to eq(overrides) }
context 'when scan_result_policy_violations is already loaded' do
before do
merge_request.scan_result_policy_violations.load
end
it { is_expected.to eq(overrides) }
end
end
context 'when unviolated' do
it { is_expected.to be_empty }
end
end
context 'with any_merge_request rule' do
let!(:approval_merge_request_rule) do
create(
:approval_merge_request_rule,
:any_merge_request,
merge_request: merge_request,
scan_result_policy_read: policy,
approvals_required: 1)
end
context 'when violated' do
before do
create(
:scan_result_policy_violation,
project: project,
merge_request: merge_request,
scan_result_policy_read: approval_merge_request_rule.scan_result_policy_read)
end
it { is_expected.to eq(overrides) }
end
context 'when unviolated' do
it { is_expected.to be_empty }
end
context 'when rule is violated by other merge request' do
let(:merge_request_2) do
create(:merge_request, source_branch: 'test', source_project: project, target_project: project,
state: :opened)
end
let(:project_approval_settings_2) do
{ prevent_approval_by_author: true,
prevent_approval_by_commit_author: true,
remove_approvals_with_new_commit: true,
require_password_to_approve: true }
end
let(:policy_2) do
create(:scan_result_policy_read, project: project, project_approval_settings: project_approval_settings_2)
end
let!(:approval_merge_request_rule_2) do
create(:approval_merge_request_rule, :any_merge_request, merge_request: merge_request,
scan_result_policy_read: policy_2, approvals_required: 1)
end
before do
create(:scan_result_policy_violation, project: project, merge_request: merge_request,
scan_result_policy_read: policy)
create(:scan_result_policy_violation, project: project, merge_request: merge_request_2,
scan_result_policy_read: policy_2)
end
it 'applies violations for the correct merge request' do
is_expected.to eq(overrides)
end
end
context 'with competing rules' do
let(:other_policy) do
create(
:scan_result_policy_read,
project: merge_request.target_project,
project_approval_settings: project_approval_settings.transform_values { false },
commits: :any)
end
let!(:approval_merge_request_rule) do
create(
:approval_merge_request_rule,
:any_merge_request,
merge_request: merge_request,
scan_result_policy_read: policy,
approvals_required: 1)
end
let!(:other_approval_merge_request_rule) do
create(
:approval_merge_request_rule,
:any_merge_request,
merge_request: merge_request,
scan_result_policy_read: other_policy,
approvals_required: 1)
end
before do
[approval_merge_request_rule, other_approval_merge_request_rule].each do |rule|
create(
:scan_result_policy_violation,
project: project,
merge_request: merge_request,
scan_result_policy_read: rule.scan_result_policy_read)
end
end
it { is_expected.to eq(overrides) }
end
end
end
end
end
describe '#policies_overriding_approval_settings' do
let(:project_approval_settings) do
{ prevent_approval_by_author: true,
prevent_approval_by_commit_author: false,
remove_approvals_with_new_commit: true,
require_password_to_approve: false }
end
let(:overrides) { project_approval_settings.compact_blank }
subject(:overriding_policies) { merge_request.policies_overriding_approval_settings }
context 'with security policies populated' do
let(:policy) do
create(:security_policy, content: { approval_settings: project_approval_settings })
end
let(:policy_rule) do
create(:approval_policy_rule, :any_merge_request, security_policy: policy)
end
context 'when violated' do
before do
create(:scan_result_policy_violation, project: project, merge_request: merge_request,
approval_policy_rule: policy_rule)
end
it 'returns policies as keys' do
expect(overriding_policies.keys).to contain_exactly(policy)
end
it 'has overrides as values' do
expect(overriding_policies.values).to contain_exactly(overrides)
end
context 'when policy does not set approval_settings' do
let(:policy) { create(:security_policy) }
it { is_expected.to be_empty }
end
context 'when policy does not set overriding approval_settings' do
let(:policy) do
create(:security_policy, content: { approval_settings: { require_password_to_approve: false } })
end
it { is_expected.to be_empty }
end
context 'when there are multiple policies' do
let(:other_approval_settings) { { require_password_to_approve: true } }
let(:other_policy) do
create(:security_policy, content: { approval_settings: other_approval_settings })
end
let(:other_policy_rule) do
create(:approval_policy_rule, :any_merge_request, security_policy: other_policy)
end
before do
create(:scan_result_policy_violation, project: project, merge_request: merge_request,
approval_policy_rule: other_policy_rule)
end
it 'returns results by policy' do
expect(overriding_policies.keys).to contain_exactly(policy, other_policy)
end
it 'returns project overrides by policy' do
expect(overriding_policies.values).to contain_exactly(overrides, other_approval_settings)
end
context 'when other policies do not override approval settings' do
let(:other_approval_settings) { { require_password_to_approve: false } }
it 'only returns policies that override settings' do
expect(overriding_policies.keys).to contain_exactly(policy)
end
end
end
end
context 'when not violated' do
it { is_expected.to be_empty }
end
end
context 'when approval_policy_rule is not populated' do
let!(:scan_result_policy_read) do
create(:scan_result_policy_read,
project: merge_request.target_project,
project_approval_settings: project_approval_settings)
end
context 'when violated' do
before do
create(:scan_result_policy_violation, project: project, merge_request: merge_request,
scan_result_policy_read: scan_result_policy_read)
end
it 'constructs policy object as key' do
first_policy = overriding_policies.each_key.first
expect(first_policy).to be_a(Security::Policy)
expect(first_policy)
.to have_attributes(
name: nil,
security_orchestration_policy_configuration_id: scan_result_policy_read.security_orchestration_policy_configuration_id,
policy_index: scan_result_policy_read.orchestration_policy_idx
)
end
it 'has overrides as values' do
expect(overriding_policies.values).to contain_exactly(overrides)
end
context 'when there are multiple policies' do
let(:other_approval_settings) { { require_password_to_approve: true } }
let!(:other_scan_result_policy_read) do
create(:scan_result_policy_read,
project: merge_request.target_project,
orchestration_policy_idx: 1,
project_approval_settings: other_approval_settings)
end
before do
create(:scan_result_policy_violation, project: project, merge_request: merge_request,
scan_result_policy_read: other_scan_result_policy_read)
end
it 'returns results by policy' do
expect(overriding_policies.keys.size).to eq(2)
expect(overriding_policies.keys.last).to have_attributes(
name: nil,
security_orchestration_policy_configuration_id: other_scan_result_policy_read.security_orchestration_policy_configuration_id,
policy_index: other_scan_result_policy_read.orchestration_policy_idx
)
end
it 'returns project overrides by policy' do
expect(overriding_policies.values).to contain_exactly(overrides, other_approval_settings)
end
context 'when other policies do not override approval settings' do
let(:other_approval_settings) { { require_password_to_approve: false } }
it 'only returns policies that override settings', :aggregate_failures do
expect(overriding_policies.keys).to be_one
expect(overriding_policies.values).to contain_exactly(overrides)
end
end
end
end
context 'when not violated' do
it { is_expected.to be_empty }
end
end
end
it_behaves_like 'an editable mentionable with EE-specific mentions' do
subject { create(:merge_request, :simple) }
let(:backref_text) { "merge request #{subject.to_reference}" }
let(:set_mentionable_text) { ->(txt) { subject.description = txt } }
end
describe '#allow_external_status_checks?' do
subject { merge_request.allow_external_status_checks? }
let_it_be(:merge_request) { build_stubbed(:merge_request) }
context 'when licensed feature `external_status_checks` is `false`' do
before do
stub_licensed_features(external_status_checks: false)
end
it { is_expected.to be false }
end
context 'when licensed feature `external_status_checks` is `true`' do
before do
stub_licensed_features(external_status_checks: true)
end
it { is_expected.to be true }
end
end
describe '#participants' do
subject(:participants) { merge_request.participants }
context 'with approval rule' do
before do
approver = create(:approver, target: project)
second_approver = create(:approver, target: project)
create(:approval_merge_request_rule, merge_request: merge_request, users: [approver.user, second_approver.user])
end
it 'returns only the author as a participant' do
expect(participants).to contain_exactly(merge_request.author)
end
end
end
describe '#has_denied_policies?', feature_category: :software_composition_analysis do
let(:merge_request) { create(:ee_merge_request, :with_license_scanning_reports, source_project: project) }
let(:apache) { build(:software_license, :apache_2_0) }
let!(:head_pipeline) do
create(
:ee_ci_pipeline,
:with_license_scanning_feature_branch,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha
)
end
let!(:base_pipeline) do
create(
:ee_ci_pipeline,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha
)
end
before do
allow_any_instance_of(Ci::CompareSecurityReportsService)
.to receive(:execute).with(base_pipeline, head_pipeline).and_call_original
end
subject { merge_request.has_denied_policies? }
context 'without existing pipeline' do
it { is_expected.to be_falsey }
end
context 'with existing pipeline' do
before do
stub_licensed_features(license_scanning: true)
end
context 'without license_scanning report' do
let(:merge_request) { create(:ee_merge_request, :with_dependency_scanning_reports, source_project: project) }
it { is_expected.to be_falsey }
end
context 'with license_scanning report' do
context 'without denied policy' do
it { is_expected.to be_falsey }
end
context 'with allowed policy' do
let(:allowed_policy) { create(:software_license_policy, :allowed, software_license: apache) }
before do
project.software_license_policies << allowed_policy
synchronous_reactive_cache(merge_request)
end
it { is_expected.to be_falsey }
end
context 'with denied policy' do
let(:denied_policy) do
create(:software_license_policy, :denied, software_license: apache,
software_license_spdx_identifier: apache.spdx_identifier)
end
let(:merge_request) { create(:ee_merge_request, :with_cyclonedx_reports, source_project: project) }
before do
project.software_license_policies << denied_policy
synchronous_reactive_cache(merge_request)
create(:pm_package, name: "nokogiri", purl_type: "gem",
other_licenses: [{ license_names: ["Apache-2.0"], versions: ["1.8.0"] }])
end
it { is_expected.to be_truthy }
context 'with disabled licensed feature' do
before do
stub_licensed_features(license_scanning: false)
end
it { is_expected.to be_falsey }
end
context 'with License-Check enabled' do
let!(:license_check) { create(:report_approver_rule, :license_scanning, merge_request: merge_request) }
context 'when rule is not approved' do
let(:software_license) { build(:software_license, :apache_2_0) }
let(:merge_request) { create(:ee_merge_request, :with_cyclonedx_reports, source_project: project) }
let(:denied_policy) do
create(:software_license_policy, :denied, software_license: software_license,
software_license_spdx_identifier: software_license.spdx_identifier)
end
before do
allow_any_instance_of(ApprovalWrappedRule).to receive(:approved?).and_return(false)
create(
:pm_package, name: "nokogiri", purl_type: "gem",
other_licenses: [{ license_names: ["Apache-2.0"], versions: ["1.8.0"] }]
)
end
context 'when the feature flag `static_licenses` is disabled' do
before do
stub_feature_flags(static_licenses: false)
end
it { is_expected.to be_truthy }
end
it { is_expected.to be_truthy }
end
context 'when rule is approved' do
before do
allow_any_instance_of(ApprovalWrappedRule).to receive(:approved?).and_return(true)
end
it { is_expected.to be_falsey }
end
end
end
end
end
context 'without license_scanning report in base pipeline' do
before do
stub_licensed_features(license_scanning: true)
synchronous_reactive_cache(merge_request)
end
let(:merge_request) { create(:ee_merge_request, :with_cyclonedx_reports, source_project: project) }
context 'when the base pipeline is nil' do
let!(:base_pipeline) { nil }
it { is_expected.to be_falsey }
end
context 'when the base pipeline does not have license reports' do
it { is_expected.to be_falsey }
end
end
end
describe '#enabled_reports' do
where(:report_type, :with_reports, :feature) do
:sast | [:with_sast_reports] | :sast
:container_scanning | [:with_container_scanning_reports] | :container_scanning
:dast | [:with_dast_reports] | :dast
:dependency_scanning | [:with_dependency_scanning_reports] | :dependency_scanning
:dependency_scanning | [:with_cyclonedx_reports] | :dependency_scanning
:license_scanning | [:with_cyclonedx_reports] | :license_scanning
:coverage_fuzzing | [:with_coverage_fuzzing_reports] | :coverage_fuzzing
:secret_detection | [:with_secret_detection_reports] | :secret_detection
:api_fuzzing | [:with_api_fuzzing_reports] | :api_fuzzing
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(:ee_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(:ee_merge_request, source_project: project) }
it { is_expected.to be_falsy }
end
end
end
describe '#approvals_before_merge' do
where(:license_value, :db_value, :expected) do
true | 5 | 5
true | nil | nil
false | 5 | nil
false | nil | nil
end
with_them do
let(:merge_request) { build(:merge_request, approvals_before_merge: db_value) }
subject { merge_request.approvals_before_merge }
before do
stub_licensed_features(merge_request_approvers: license_value)
end
it { is_expected.to eq(expected) }
end
end
describe '#has_security_reports?' do
subject { merge_request.has_security_reports? }
before do
stub_licensed_features(dast: true)
end
context 'when head pipeline has security reports' do
let(:merge_request) { create(:ee_merge_request, :with_dast_reports, source_project: project) }
it { is_expected.to be_truthy }
context 'when head pipeline is blocked by manual jobs' do
before do
merge_request.diff_head_pipeline.block!
end
it { is_expected.to be_truthy }
end
end
context 'when head pipeline does not have security reports' do
let(:merge_request) { create(:ee_merge_request, source_project: project) }
it { is_expected.to be_falsey }
end
end
describe '#has_dependency_scanning_reports?' do
subject { merge_request.has_dependency_scanning_reports? }
before do
stub_licensed_features(container_scanning: true)
end
context 'when head pipeline has dependency scannning reports' do
let(:merge_request) { create(:ee_merge_request, :with_dependency_scanning_reports, source_project: project) }
it { is_expected.to be_truthy }
context 'when head pipeline is blocked by manual jobs' do
before do
merge_request.diff_head_pipeline.block!
end
it { is_expected.to be_truthy }
end
context 'when head pipeline has cyclonedx reports' do
let(:merge_request) { create(:ee_merge_request, :with_cyclonedx_reports, source_project: project) }
it { is_expected.to be_truthy }
end
end
context 'when head pipeline does not have dependency scanning reports' do
let(:merge_request) { create(:ee_merge_request, source_project: project) }
it { is_expected.to be_falsey }
end
end
describe '#has_container_scanning_reports?' do
subject { merge_request.has_container_scanning_reports? }
before do
stub_licensed_features(container_scanning: true)
end
context 'when head pipeline has container scanning reports' do
let(:merge_request) { create(:ee_merge_request, :with_container_scanning_reports, source_project: project) }
it { is_expected.to be_truthy }
context 'when head pipeline is blocked by manual jobs' do
before do
merge_request.diff_head_pipeline.block!
end
it { is_expected.to be_truthy }
end
end
context 'when head pipeline does not have container scanning reports' do
let(:merge_request) { create(:ee_merge_request, source_project: project) }
it { is_expected.to be_falsey }
end
end
describe '#has_dast_reports?' do
subject { merge_request.has_dast_reports? }
before do
stub_licensed_features(dast: true)
end
context 'when head pipeline has dast reports' do
let(:merge_request) { create(:ee_merge_request, :with_dast_reports, source_project: project) }
it { is_expected.to be_truthy }
context 'when head pipeline is blocked by manual jobs' do
before do
merge_request.diff_head_pipeline.block!
end
it { is_expected.to be_truthy }
end
end
context 'when pipeline ran for an older commit than the branch head' do
let(:pipeline) { create(:ci_empty_pipeline, sha: 'notlatestsha') }
let(:merge_request) { create(:ee_merge_request, source_project: project, head_pipeline: pipeline) }
it { is_expected.to be_falsey }
end
context 'when head pipeline does not have dast reports' do
let(:merge_request) { create(:ee_merge_request, source_project: project) }
it { is_expected.to be_falsey }
end
end
describe '#has_metrics_reports?' do
subject { merge_request.has_metrics_reports? }
before do
stub_licensed_features(metrics_reports: true)
end
context 'when head pipeline has metrics reports' do
let(:merge_request) { create(:ee_merge_request, :with_metrics_reports, source_project: project) }
it { is_expected.to be_truthy }
context 'when the head pipeline is still in progress' do
it 'does not return any metrics reports' do
merge_request.head_pipeline.update!(status: 'running')
expect(subject).to be_falsey
end
end
end
context 'when head pipeline does not have metrics reports' do
let(:merge_request) { create(:ee_merge_request, :with_head_pipeline, source_project: project) }
before do
merge_request.head_pipeline.update!(status: 'success')
end
it { is_expected.to be_falsey }
context 'when child pipeline has metrics reports' do
it 'returns child report' do
create(:ee_ci_pipeline, :success, :with_metrics_report, child_of: merge_request.head_pipeline)
expect(subject).to be_truthy
end
context 'when ff show_child_reports_in_mr_page is disabled' do
before do
stub_feature_flags(show_child_reports_in_mr_page: false)
end
it { is_expected.to be_falsey }
end
end
end
end
describe '#has_coverage_fuzzing_reports?' do
subject { merge_request.has_coverage_fuzzing_reports? }
before do
stub_licensed_features(coverage_fuzzing: true)
end
context 'when head pipeline has coverage fuzzing reports' do
let(:merge_request) { create(:ee_merge_request, :with_coverage_fuzzing_reports, source_project: project) }
it { is_expected.to be_truthy }
context 'when head pipeline is blocked by manual jobs' do
before do
merge_request.diff_head_pipeline.block!
end
it { is_expected.to be_truthy }
end
end
context 'when head pipeline does not have coverage fuzzing reports' do
let(:merge_request) { create(:ee_merge_request, source_project: project) }
it { is_expected.to be_falsey }
end
end
describe '#has_api_fuzzing_reports?' do
subject { merge_request.has_api_fuzzing_reports? }
before do
stub_licensed_features(api_fuzzing: true)
end
context 'when head pipeline has coverage fuzzing reports' do
let(:merge_request) { create(:ee_merge_request, :with_api_fuzzing_reports, source_project: project) }
it { is_expected.to be_truthy }
context 'when head pipeline is blocked by manual jobs' do
before do
merge_request.diff_head_pipeline.block!
end
it { is_expected.to be_truthy }
end
end
context 'when head pipeline does not have coverage fuzzing reports' do
let(:merge_request) { create(:ee_merge_request, source_project: project) }
it { is_expected.to be_falsey }
end
end
describe '#calculate_reactive_cache with current_user' do
let(:current_user) { project.users.take }
let(:merge_request) { create(:merge_request, source_project: project) }
subject { merge_request.calculate_reactive_cache(service_class_name, current_user&.id) }
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 '#compare_container_scanning_reports' do
subject { merge_request.compare_container_scanning_reports(current_user) }
let(:current_user) { project.users.first }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:base_pipeline) do
create(
:ee_ci_pipeline,
:with_container_scanning_report,
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 container scanning reports' do
let!(:head_pipeline) do
create(
:ee_ci_pipeline,
:with_container_scanning_report,
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::CompareSecurityReportsService)
.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::CompareSecurityReportsService)
.to receive(:latest?).and_return(false)
end
it 'raises and InvalidateReactiveCache error' do
expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
end
end
end
end
end
describe '#compare_secret_detection_reports' do
subject { merge_request.compare_secret_detection_reports(current_user) }
let(:current_user) { project.users.first }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:base_pipeline) do
create(
:ee_ci_pipeline,
:with_secret_detection_report,
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 secret detection reports' do
let!(:head_pipeline) do
create(
:ee_ci_pipeline,
:with_secret_detection_report,
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::CompareSecurityReportsService)
.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::CompareSecurityReportsService)
.to receive(:latest?).and_return(false)
end
it 'raises and InvalidateReactiveCache error' do
expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
end
end
end
end
end
describe '#compare_sast_reports' do
subject { merge_request.compare_sast_reports(current_user) }
let(:current_user) { project.users.first }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:base_pipeline) do
create(
:ee_ci_pipeline,
:with_sast_report,
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 sast reports' do
let!(:head_pipeline) do
create(
:ee_ci_pipeline,
:with_sast_report,
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::CompareSecurityReportsService)
.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::CompareSecurityReportsService)
.to receive(:latest?).and_return(false)
end
it 'raises and InvalidateReactiveCache error' do
expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
end
end
end
end
end
describe '#compare_license_scanning_reports', feature_category: :software_composition_analysis do
subject { merge_request.compare_license_scanning_reports(current_user) }
let(:current_user) { project.users.first }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:base_pipeline) do
create(
:ee_ci_pipeline,
:with_cyclonedx_report,
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 cyclonedx reports' do
let!(:head_pipeline) do
create(
:ee_ci_pipeline,
:with_cyclonedx_report,
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::CompareLicenseScanningReportsService)
.to receive(:execute).with(base_pipeline, head_pipeline).and_call_original
subject
end
context 'cache key includes software license policies' do
let(:apache_2_0) { build(:software_license, :apache_2_0) }
let!(:license_1) { create(:software_license_policy, project: project, software_license: apache_2_0, software_license_spdx_identifier: apache_2_0.spdx_identifier) }
let(:mit) { build(:software_license, :mit) }
let!(:license_2) { create(:software_license_policy, project: project, software_license: mit, software_license_spdx_identifier: mit.spdx_identifier) }
context 'when the feature flag `static_licenses` is disabled' do
before do
stub_feature_flags(static_licenses: false)
end
it 'returns key with license information' do
expect_any_instance_of(Ci::CompareLicenseScanningReportsService)
.to receive(:execute).with(base_pipeline, head_pipeline).and_call_original
expect(subject[:key].last).to include("software_license_policies/query-")
expect(subject[:data]['existing_licenses'].last.dig('classification', 'approval_status')).to eq('unclassified')
end
end
it 'returns key with license information' do
expect_any_instance_of(Ci::CompareLicenseScanningReportsService)
.to receive(:execute).with(base_pipeline, head_pipeline).and_call_original
expect(subject[:key].last).to include("software_license_policies/query-")
expect(subject[:data]['existing_licenses'].last.dig('classification', 'approval_status')).to eq('unclassified')
end
end
context 'when cached results is not latest' do
before do
allow_any_instance_of(Ci::CompareLicenseScanningReportsService)
.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 cyclonedx 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 license scanning reports')
end
end
context "when a cyclonedx report is produced from the head pipeline" do
where(:pipeline_status, :build_types, :expected_status) do
[
[:blocked, [:container_scanning], :error],
[:blocked, [:cyclonedx], :parsed],
[:blocked, [:cyclonedx, :container_scanning], :parsed],
[:blocked, [], :error],
[:failed, [:container_scanning], :error],
[:failed, [:cyclonedx], :parsed],
[:failed, [:cyclonedx, :container_scanning], :parsed],
[:failed, [], :error],
[:running, [:container_scanning], :error],
[:running, [:cyclonedx], :error],
[:running, [:cyclonedx, :container_scanning], :error],
[:running, [], :error],
[:success, [:container_scanning], :error],
[:success, [:cyclonedx], :parsed],
[:success, [:cyclonedx, :container_scanning], :parsed],
[:success, [], :error]
]
end
with_them do
let!(:head_pipeline) { create(:ci_pipeline, pipeline_status, project: project, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, builds: builds) }
let(:builds) { build_types.map { |build_type| create(:ee_ci_build, build_type) } }
before do
synchronous_reactive_cache(merge_request)
end
specify { expect(subject[:status]).to eq(expected_status) }
end
end
end
describe '#compare_license_scanning_reports_collapsed', feature_category: :software_composition_analysis do
subject(:report) { merge_request.compare_license_scanning_reports_collapsed(current_user) }
let(:current_user) { project.users.first }
let!(:base_pipeline) do
create(
:ee_ci_pipeline,
:with_license_scanning_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha
)
end
let!(:head_pipeline) do
create(
:ci_pipeline,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha
)
end
context 'when service can be executed' do
before do
merge_request.update!(head_pipeline_id: head_pipeline.id)
allow_next_instance_of(::Gitlab::LicenseScanning::SbomScanner) do |scanner|
allow(scanner).to receive(:results_available?).and_return(true)
end
end
it 'returns compared report' do
expect(report[:status]).to eq(:parsing)
end
end
context 'when head pipeline does not have license scanning reports' do
it 'returns status and error message' do
expect(subject[:status]).to eq(:error)
end
end
end
describe '#compare_metrics_reports' do
subject { merge_request.compare_metrics_reports }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:base_pipeline) do
create(
:ee_ci_pipeline,
:with_metrics_report,
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 metrics reports' do
let!(:head_pipeline) do
create(
:ee_ci_pipeline,
:with_metrics_report,
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::CompareMetricsReportsService)
.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::CompareMetricsReportsService)
.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 metrics 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 metrics reports')
end
end
end
describe '#compare_coverage_fuzzing_reports' do
subject { merge_request.compare_coverage_fuzzing_reports(current_user) }
let(:current_user) { project.users.first }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:base_pipeline) do
create(
:ee_ci_pipeline,
:with_coverage_fuzzing_report,
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 coverage fuzzing reports' do
let!(:head_pipeline) do
create(
:ee_ci_pipeline,
:with_coverage_fuzzing_report,
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::CompareSecurityReportsService)
.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::CompareSecurityReportsService)
.to receive(:latest?).and_return(false)
end
it 'raises and InvalidateReactiveCache error' do
expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
end
end
end
end
end
describe '#compare_api_fuzzing_reports' do
subject { merge_request.compare_api_fuzzing_reports(current_user) }
let(:current_user) { project.users.first }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:base_pipeline) do
create(
:ee_ci_pipeline,
:with_api_fuzzing_report,
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 api fuzzing reports' do
let!(:head_pipeline) do
create(
:ee_ci_pipeline,
:with_api_fuzzing_report,
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::CompareSecurityReportsService)
.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::CompareSecurityReportsService)
.to receive(:latest?).and_return(false)
end
it 'raises an InvalidateReactiveCache error' do
expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
end
end
end
end
end
describe '#use_merge_base_pipeline_for_comparison?' do
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 eq(true) }
end
context 'when service class is Ci::CompareCodequalityReportsService' do
let(:service_class) { ::Ci::CompareCodequalityReportsService }
it { is_expected.to eq(true) }
end
context 'when service class is Ci::CompareSecurityReportsService' do
let(:service_class) { ::Ci::CompareSecurityReportsService }
it { is_expected.to eq(true) }
end
context 'when service class is Ci::CompareLicenseScanningReportsCollapsedService' do
let(:service_class) { ::Ci::CompareLicenseScanningReportsCollapsedService }
it { is_expected.to eq(true) }
end
context 'when service class is Ci::CompareLicenseScanningReportsService' do
let(:service_class) { ::Ci::CompareLicenseScanningReportsService }
it { is_expected.to eq(true) }
end
context 'when service class is different' do
let(:service_class) { ::Ci::GenerateCoverageReportsService }
it { is_expected.to eq(false) }
end
end
describe '#approver_group_ids=' do
it 'create approver_groups' do
group = create :group
group1 = create :group
merge_request = create :merge_request
merge_request.approver_group_ids = "#{group.id}, #{group1.id}"
merge_request.save!
expect(merge_request.approver_groups.map(&:group)).to match_array([group, group1])
end
end
describe '#predefined_variables' do
context 'when merge request has approver feature' do
before do
stub_licensed_features(merge_request_approvers: true)
end
context 'without any rules' do
it 'includes variable CI_MERGE_REQUEST_APPROVED=true' do
expect(merge_request.predefined_variables.to_hash).to include('CI_MERGE_REQUEST_APPROVED' => 'true')
end
context 'when the mr is temporarily unapproved' do
it 'does not include variable CI_MERGE_REQUEST_APPROVED' do
expect(merge_request.approval_state).to receive(:temporarily_unapproved?).and_return(true)
expect(merge_request.predefined_variables.to_hash.keys).not_to include('CI_MERGE_REQUEST_APPROVED')
end
end
end
context 'with a rule' do
let(:approver) { create(:user) }
let!(:rule) { create(:approval_merge_request_rule, merge_request: merge_request, approvals_required: 1, users: [approver]) }
context 'that has been approved' do
it 'includes variable CI_MERGE_REQUEST_APPROVED=true' do
create(:approval, merge_request: merge_request, user: approver)
expect(merge_request.predefined_variables.to_hash).to include('CI_MERGE_REQUEST_APPROVED' => 'true')
end
end
context 'that has not been approved' do
it 'does not include variable CI_MERGE_REQUEST_APPROVED' do
expect(merge_request.predefined_variables.to_hash.keys).not_to include('CI_MERGE_REQUEST_APPROVED')
end
end
end
end
context 'when merge request does not have approver feature' do
before do
stub_licensed_features(merge_request_approvers: false)
end
it 'does not include variable CI_MERGE_REQUEST_APPROVED' do
expect(merge_request.predefined_variables.to_hash.keys).not_to include('CI_MERGE_REQUEST_APPROVED')
end
end
end
describe '#mergeable?' do
context 'with skip_approved_check option' do
before do
allow(subject).to receive_messages(mergeable_ci_state?: true,
mergeable_discussions_state?: true,
check_mergeability: nil,
can_be_merged?: true,
broken?: false)
end
where(:approved_state, :skip_approved_check, :expected_mergeable) do
false | false | false
false | true | true
true | false | true
true | true | true
end
with_them do
it 'overrides mergeable?' do
allow(merge_request).to receive(:approved?) { approved_state }
expect(merge_request.mergeable?(skip_approved_check: skip_approved_check)).to eq(expected_mergeable)
end
end
end
end
describe '#skipped_mergeable_checks' do
subject { build_stubbed(:merge_request).skipped_mergeable_checks(options) }
let(:options) { { auto_merge_strategy: auto_merge_strategy } }
where(:auto_merge_strategy, :skip_approved_check, :skip_draft_check, :skip_blocked_check, :skip_discussions_check, :skip_external_status_check, :skip_locked_paths_check) do
'' | false | false | false | false | false | false
AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS | true | true | true | true | true | true
end
with_them do
it do
is_expected.to include(skip_approved_check: skip_approved_check, skip_draft_check: skip_draft_check,
skip_blocked_check: skip_blocked_check, skip_discussions_check: skip_discussions_check,
skip_external_status_check: skip_external_status_check)
end
end
end
describe '#mergeable_state?' do
subject { merge_request.mergeable_state?(**params) }
let(:params) { {} }
let(:project_with_approver) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project_with_approver, target_project: project_with_approver) }
let_it_be(:user) { create(:user) }
context 'when using approvals' do
before do
merge_request.target_project.update!(approvals_before_merge: 1)
project.add_developer(user)
end
context 'when not approved' do
it 'is not mergeable' do
is_expected.to be_falsey
end
end
context 'when approved' do
before do
merge_request.approvals.create!(user: user)
end
it 'is mergeable' do
is_expected.to be_truthy
end
end
end
context 'when validating jira associations' do
let_it_be_with_reload(:project_jira) { create(:project, :repository, :with_jira_integration) }
let_it_be_with_reload(:merge_request) { create(:merge_request, source_project: project_jira, target_project: project_jira) }
before do
allow(merge_request.project).to receive(:prevent_merge_without_jira_issue?).and_return(true)
end
context 'when the merge request does not reference a jira issue' do
before do
merge_request.update!(description: '')
end
it 'is not mergeable' do
is_expected.to be_falsey
end
end
context 'when the merge request references a jira issue' do
before do
merge_request.update!(description: 'PROJECT-1')
end
it 'is mergeable' do
is_expected.to be_truthy
end
end
end
context 'when using external status checks' do
let(:any_external_status_checks_not_passed?) { false }
before do
allow(merge_request).to receive(:only_allow_merge_if_all_status_checks_passed_enabled?).and_return(true)
allow(merge_request.project).to receive(:only_allow_merge_if_all_status_checks_passed).and_return(true)
allow(merge_request.project).to receive(:any_external_status_checks_not_passed?)
.and_return(any_external_status_checks_not_passed?)
stub_licensed_features(external_status_checks: true)
end
context 'when external checks pass' do
let!(:status_code) { create(:status_check_response, status: :passed, merge_request: merge_request) }
it 'is mergeable' do
is_expected.to be_truthy
end
end
context 'when external status checks are not passed' do
let(:any_external_status_checks_not_passed?) { true }
let!(:status_code) { create(:status_check_response, status: :failed, merge_request: merge_request) }
it 'is not mergeable' do
is_expected.to be_falsey
end
context 'when skip external status checks is true' do
let(:params) { { skip_external_status_check: true } }
it 'is mergeable' do
is_expected.to be_truthy
end
end
end
end
context 'when blocking merge requests' do
before do
stub_licensed_features(blocking_merge_requests: true)
end
context 'when the merge request is blocked' do
let(:merge_request) { create(:merge_request, :blocked, source_project: project, target_project: project) }
it 'is not mergeable' do
is_expected.to be_falsey
end
context 'when skip blocked check' do
let(:params) { { skip_blocked_check: true } }
it 'is mergeable' do
is_expected.to be_truthy
end
end
end
context 'when merge request is not blocked' do
it 'is mergeable' do
is_expected.to be_truthy
end
end
end
end
describe '#on_train?' do
subject { merge_request.on_train? }
context 'when the merge request is on a merge train' do
let(:merge_request) do
create(:merge_request, :on_train, source_project: project, target_project: project)
end
it { is_expected.to be_truthy }
end
context 'when the merge request was on a merge train' do
let(:merge_request) do
create(:merge_request, :on_train,
status: MergeTrains::Car.state_machines[:status].states[:merged].value,
source_project: project, target_project: project)
end
it { is_expected.to be_falsy }
end
context 'when the merge request is not on a merge train' do
let(:merge_request) do
create(:merge_request, source_project: project, target_project: project)
end
it { is_expected.to be_falsy }
end
end
describe 'review time sorting' do
def create_mr(metrics_data = {})
create(:merge_request, :with_productivity_metrics, metrics_data: metrics_data)
end
it 'orders by first_comment_at or first_approved_at whatever is earlier' do
mr1 = create_mr(first_comment_at: 1.day.ago)
mr2 = create_mr(first_comment_at: 3.days.ago)
mr3 = create_mr(first_approved_at: 5.days.ago)
mr4 = create_mr(first_comment_at: 1.day.ago, first_approved_at: 4.days.ago)
mr5 = create_mr(first_comment_at: nil, first_approved_at: nil)
expect(described_class.order_review_time_desc).to match([mr3, mr4, mr2, mr1, mr5])
expect(described_class.sort_by_attribute('review_time_desc')).to match([mr3, mr4, mr2, mr1, mr5])
end
end
describe '#security_reports_up_to_date?' do
let(:merge_request) do
create(
:ee_merge_request,
source_project: project,
source_branch: 'feature1',
target_branch: project.default_branch
)
end
before do
create(
:ee_ci_pipeline,
:with_sast_report,
project: project,
ref: merge_request.target_branch
)
end
subject { merge_request.security_reports_up_to_date? }
context 'when the target branch security reports are up to date' do
it { is_expected.to be true }
end
context 'when the target branch security reports are out of date' do
before do
create(:ee_ci_pipeline, :failed, project: project, ref: merge_request.target_branch)
end
it { is_expected.to be false }
end
end
describe '#audit_details' do
it 'equals to the title' do
merge_request = create(:merge_request, title: 'I am a title')
expect(merge_request.audit_details).to eq(merge_request.title)
end
end
describe '#validate_reviewer_length' do
let(:reviewer1) { create(:user) }
let(:reviewer2) { create(:user) }
let(:reviewer3) { create(:user) }
subject { create(:merge_request) }
before do
stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 2)
end
it 'will not exceed the reviewer limit' do
expect do
subject.update!(reviewers: [reviewer1, reviewer2, reviewer3])
end.to raise_error(ActiveRecord::RecordInvalid)
end
end
describe '#sync_project_approval_rules_for_policy_configuration' do
let_it_be(:merge_request) { create(:ee_merge_request, source_project: project) }
let_it_be(:policy_configuration) { create(:security_orchestration_policy_configuration, project: project) }
let_it_be(:project_approval_rule_without_configuration) { create(:approval_project_rule, project: project) }
let_it_be(:project_approval_rule) do
create(:approval_project_rule, :scan_finding,
project: project,
security_orchestration_policy_configuration: policy_configuration,
scanners: %w[sast],
approvals_required: 2,
scan_result_policy_read: create(:scan_result_policy_read)
)
end
subject { merge_request.sync_project_approval_rules_for_policy_configuration(policy_configuration.id) }
it 'creates approval rules for project' do
subject
expect(merge_request.approval_rules.first.approval_project_rule).to eq(project_approval_rule)
end
it 'does not create approval rules for other configuration' do
subject
expect(merge_request.approval_rules.map(&:approval_project_rule)).not_to include(project_approval_rule_without_configuration)
end
context 'when mr approval rules already exist' do
let_it_be(:mr_approval_rule) do
create(:report_approver_rule, :scan_finding,
merge_request: merge_request,
approvals_required: 1
)
end
let_it_be(:approval_rule_source) do
create(:approval_merge_request_rule_source,
approval_merge_request_rule: mr_approval_rule,
approval_project_rule: project_approval_rule
)
end
it 'updates approval rule' do
subject
expect(mr_approval_rule.reload.approvals_required).to eq(2)
end
end
context 'when merge request is already merged' do
let_it_be(:merge_request) { create(:ee_merge_request, source_project: project, state: :merged) }
it 'does not create or update approval rule' do
subject
expect(merge_request.approval_rules).to be_empty
end
end
end
describe '#sync_project_approval_rules_for_approval_policy_rules' do
let_it_be(:merge_request) { create(:ee_merge_request, source_project: project) }
let_it_be(:security_policy) { create(:security_policy) }
let_it_be(:approval_policy_rule) { create(:approval_policy_rule, security_policy: security_policy) }
let_it_be(:project_approval_rule_without_policy_rule) { create(:approval_project_rule, project: project) }
let_it_be(:project_approval_rule) do
create(:approval_project_rule, :scan_finding,
project: project,
approval_policy_rule: approval_policy_rule,
scanners: %w[sast],
approvals_required: 2
)
end
subject do
merge_request.sync_project_approval_rules_for_approval_policy_rules(security_policy.approval_policy_rules)
end
it 'creates approval rules for project' do
subject
expect(merge_request.approval_rules.first.approval_project_rule).to eq(project_approval_rule)
end
it 'does not create approval rules for other configuration' do
subject
expect(merge_request.approval_rules.map(&:approval_project_rule)).not_to include(project_approval_rule_without_policy_rule)
end
context 'when mr approval rules already exist' do
let_it_be(:mr_approval_rule) do
create(:report_approver_rule, :scan_finding,
merge_request: merge_request,
approvals_required: 1
)
end
let_it_be(:approval_rule_source) do
create(:approval_merge_request_rule_source,
approval_merge_request_rule: mr_approval_rule,
approval_project_rule: project_approval_rule
)
end
it 'updates approval rule' do
subject
expect(mr_approval_rule.reload.approvals_required).to eq(2)
end
end
context 'when merge request is already merged' do
let_it_be(:merge_request) { create(:ee_merge_request, source_project: project, state: :merged) }
it 'does not create or update approval rule' do
subject
expect(merge_request.approval_rules).to be_empty
end
end
end
describe '#delete_approval_rules_for_policy_configuration' do
let_it_be(:merge_request) { create(:ee_merge_request, source_project: project) }
let_it_be(:policy_configuration) { create(:security_orchestration_policy_configuration, project: project) }
let_it_be(:other_policy_configuration) { create(:security_orchestration_policy_configuration) }
let_it_be(:mr_approval_rule) do
create(:report_approver_rule, :scan_finding,
merge_request: merge_request,
security_orchestration_policy_configuration: policy_configuration
)
end
let_it_be(:other_mr_approval_rule) do
create(:report_approver_rule, :scan_finding,
merge_request: merge_request,
security_orchestration_policy_configuration: other_policy_configuration
)
end
subject(:delete_approval_rules_for_policy_configuration) do
merge_request.delete_approval_rules_for_policy_configuration(policy_configuration.id)
end
context 'when the merge request is not merged' do
it 'deletes approval rules for the given policy configuration' do
expect { delete_approval_rules_for_policy_configuration }.to change { ApprovalMergeRequestRule.count }.from(2).to(1)
end
end
context 'when the merge request is merged' do
before do
merge_request.update!(state: 'merged')
end
it 'does not delete any approval rules' do
expect { delete_approval_rules_for_policy_configuration }.not_to change { ApprovalMergeRequestRule.count }
end
end
end
describe '#reset_required_approvals' do
subject(:execute) { merge_request.reset_required_approvals(approval_rules) }
let_it_be(:merge_request) { create(:ee_merge_request, source_project: project) }
let_it_be(:policy_configuration) { create(:security_orchestration_policy_configuration, project: project) }
let_it_be(:scan_finding_project_rule) do
create(:approval_project_rule, :scan_finding, project: project, scanners: %w[sast], approvals_required: 2,
security_orchestration_policy_configuration: policy_configuration)
end
let_it_be(:license_project_rule) do
create(:approval_project_rule, :license_scanning, project: project, approvals_required: 2,
security_orchestration_policy_configuration: policy_configuration)
end
let_it_be(:other_project_rule) do
create(:approval_project_rule, :scan_finding, project: project, scanners: %w[sast], approvals_required: 2,
security_orchestration_policy_configuration: policy_configuration)
end
context 'when report_approver_rule has a project source' do
let_it_be(:approval_rules) do
[
create(:report_approver_rule, :scan_finding, merge_request: merge_request, approvals_required: 0,
approval_project_rule: scan_finding_project_rule),
create(:report_approver_rule, :license_scanning, merge_request: merge_request, approvals_required: 0,
approval_project_rule: license_project_rule)
]
end
let_it_be(:other_approval_rule_with_project_source) do
create(:report_approver_rule, :scan_finding, merge_request: merge_request, approvals_required: 0,
approval_project_rule: other_project_rule)
end
it 'resets the approvals_required for the provided approval_rules to match the project rules' do
expect { execute }.to change { approval_rules.first.reload.approvals_required }.from(0).to(2)
.and change { approval_rules.second.reload.approvals_required }.from(0).to(2)
end
it 'does not reset other approval rules' do
expect { execute }.not_to change { other_approval_rule_with_project_source.reload.approvals_required }
end
context 'when merge request is already merged' do
before do
merge_request.update!(state: 'merged')
end
after do
merge_request.update!(state: 'opened')
end
it 'does not update the approval rules' do
expect { execute }.to not_change { approval_rules.first.reload.approvals_required }
.and not_change { approval_rules.second.reload.approvals_required }
end
end
end
context "when report_approver_rule doesn't have a project source" do
let_it_be(:approval_rules) do
[create(:report_approver_rule, :scan_finding, merge_request: merge_request, approvals_required: 0)]
end
it "doesn't reset the approval_rules" do
expect { execute }.not_to change { approval_rules.first.reload }
end
end
end
context 'scopes' do
let_it_be(:merge_request) { create(:ee_merge_request) }
let_it_be(:merge_request_with_head_pipeline) { create(:ee_merge_request, :with_metrics_reports) }
describe '.with_head_pipeline' do
it 'returns MRs that have a head pipeline' do
expect(described_class.with_head_pipeline).to eq([merge_request_with_head_pipeline])
end
end
describe '.not_merged' do
let(:opened_merge_request) { create(:merge_request, :opened) }
let(:merged_merge_request) { create(:merge_request, :merged) }
let(:closed_merge_request) { create(:merge_request, :closed) }
let(:locked_merge_request) { create(:merge_request, :locked) }
it 'returns everything except the merged mr' do
expect(described_class.not_merged).to contain_exactly(merge_request, merge_request_with_head_pipeline, opened_merge_request, closed_merge_request, locked_merge_request)
end
end
describe '.with_applied_scan_result_policies' do
let_it_be(:code_coverage_approval_rule) { create(:report_approver_rule, :code_coverage) }
let_it_be(:scan_finding_approval_rule) { create(:report_approver_rule, :scan_finding) }
let_it_be(:license_scanning_approval_rule) { create(:report_approver_rule, :license_scanning) }
let_it_be(:any_merge_request_approval_rule) { create(:report_approver_rule, :any_merge_request) }
it 'returns MRs that have applied scan result policies' do
expect(described_class.with_applied_scan_result_policies).to eq([
scan_finding_approval_rule.merge_request,
license_scanning_approval_rule.merge_request,
any_merge_request_approval_rule.merge_request
])
end
end
describe '.for_projects_with_security_policy_project' do
let_it_be(:security_orchestration_policy_configuration) { create(:security_orchestration_policy_configuration) }
let_it_be(:merge_request_with_security_policy_project) do
create(:ee_merge_request, source_project: security_orchestration_policy_configuration.project)
end
let_it_be(:merge_request_without_security_policy_project) { create(:ee_merge_request) }
it 'returns MRs for projects with security policy project on target project' do
expect(described_class.for_projects_with_security_policy_project).to eq(
[merge_request_with_security_policy_project])
end
end
end
context 'after_commit hooks' do
describe 'create_pending_status_check_responses' do
subject(:create_merge_request) do
create :merge_request, :opened, source_project: project, target_project: project
end
let_it_be(:status_check) { create(:external_status_check, project: project) }
let_it_be(:another_status_check) { create(:external_status_check, project: project) }
let(:merge_request) { described_class.last }
context 'when project can have status checks' do
before do
stub_licensed_features(external_status_checks: true)
end
it 'creates `pending` status check responses' do
expect(ComplianceManagement::PendingStatusCheckWorker).to receive(:perform_async).and_call_original
create_merge_request
end
context 'when the the diff head SHA is empty' do
it 'does not enqueue a status check' do
expect(ComplianceManagement::PendingStatusCheckWorker).not_to receive(:perform_async)
create :merge_request, :opened, source_branch: 'main', target_branch: 'foo'
end
end
end
context 'when project can not have status checks' do
it 'does not create status check responses' do
expect(ComplianceManagement::PendingStatusCheckWorker).not_to receive(:perform_async)
create_merge_request
end
end
end
end
context 'after_update hooks' do
describe 'sync_merge_request_compliance_violation' do
let_it_be(:merge_request) do
create(:merge_request, source_project: project, target_project: project, state: :merged, title: 'old MR title')
end
let_it_be(:compliance_violation) do
create(:compliance_violation, :approved_by_committer, severity_level: :low, merge_request: merge_request, title: 'old MR title')
end
it "calls sync_merge_request_compliance_violation when the MR title is updated" do
expect(merge_request.compliance_violations.pluck(:title)).to contain_exactly('old MR title')
expect(merge_request).to receive(:sync_merge_request_compliance_violation).and_call_original
merge_request.update_attribute(:title, "new MR title")
expect(merge_request.compliance_violations.pluck(:title)).to contain_exactly('new MR title')
end
it "does not call sync_merge_request_compliance_violation when the MR title is not updated" do
expect(merge_request).not_to receive(:sync_merge_request_compliance_violation)
merge_request.update_attribute(:milestone, Milestone.last)
end
end
end
describe '#merge_train' do
subject { merge_request.merge_train }
before do
allow(project).to receive(:merge_trains_enabled?).and_return(setting)
end
context 'with MergeTrains disabled' do
let(:setting) { false }
it { is_expected.to be_nil }
end
context 'with MergeTrains enabled' do
let(:setting) { true }
it { is_expected.to be_a(MergeTrains::Train) }
end
end
describe '#should_be_rebased?' do
subject { merge_request.should_be_rebased? }
shared_examples 'ff car on train' do
context 'when MR is on an up-to-date fast-forward merge train' do
before do
car = create(:merge_train_car, merge_request: merge_request)
merge_request.update!(
merge_params: merge_request.merge_params.merge(
'train_ref' => { 'commit_sha' => car.pipeline.sha }
)
)
end
it { is_expected.to eq false }
end
end
context 'when MR source branch needs to be rebased to be merged' do
before do
stub_foss_conditions_met
end
context 'when the project is using ff trains' do
before do
allow(MergeTrains::Train).to receive(:project_using_ff?).and_return(true)
end
it { is_expected.to eq false }
it_behaves_like 'ff car on train'
end
context 'when project not using ff trains' do
before do
allow(MergeTrains::Train).to receive(:project_using_ff?).and_return(false)
end
it { is_expected.to eq true }
it_behaves_like 'ff car on train'
end
end
def stub_foss_conditions_met
allow(project).to receive(:ff_merge_must_be_possible?).and_return(true)
allow(merge_request).to receive(:ff_merge_possible?).and_return(false)
end
end
describe '#comparison_base_pipeline' do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be_with_refind(:merge_request) do
create(:merge_request, :with_merge_request_pipeline, source_project: project)
end
let_it_be(:base_pipeline) do
create(:ci_pipeline,
:with_test_reports,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha
)
end
subject(:pipeline) { merge_request.comparison_base_pipeline(service_class) }
context 'when the service class is not `Ci::CompareSecurityReportsService`' do
let(:service_class) { ::Ci::CompareCodequalityReportsService }
before do
allow(merge_request).to receive_messages(merge_base_pipeline: nil, base_pipeline: nil)
end
it 'follows the normal execution order' do
pipeline
expect(merge_request).to have_received(:merge_base_pipeline)
expect(merge_request).to have_received(:base_pipeline)
end
end
context 'when the service class is `Ci::CompareSecurityReportsService`' do
let(:service_class) { ::Ci::CompareSecurityReportsService }
before do
merge_request.update_head_pipeline
end
context 'when there are merge base pipelines' do
let_it_be(:old_merge_base_pipeline) do
create(:ci_pipeline,
:success,
project: project,
ref: merge_request.target_branch,
sha: merge_request.target_branch_sha
)
end
let_it_be(:most_recent_merge_base_pipeline) do
create(:ci_pipeline,
:success,
project: project,
ref: merge_request.target_branch,
sha: merge_request.target_branch_sha
)
end
context 'when all pipelines have security reports' do
before do
build_1 = build(:ci_build,
:sast_report,
pipeline: old_merge_base_pipeline,
project: project)
build_2 = build(:ci_build,
:sast_report,
pipeline: most_recent_merge_base_pipeline,
project: project)
old_merge_base_pipeline.builds << build_1
most_recent_merge_base_pipeline.builds << build_2
end
it 'returns the most recent merge base pipeline' do
expect(pipeline).to eq(most_recent_merge_base_pipeline)
end
end
context 'when the most recent pipeline does not have security reports' do
before do
build = build(:ci_build,
:sast_report,
pipeline: old_merge_base_pipeline,
project: project)
old_merge_base_pipeline.builds << build
end
it 'returns the latest merge base pipeline with security reports' do
expect(pipeline).to eq(old_merge_base_pipeline)
end
end
end
context 'when there is no merge base pipeline' do
let_it_be(:old_base_pipeline) do
create(:ci_pipeline,
:success,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha
)
end
let_it_be(:most_recent_base_pipeline) do
create(:ci_pipeline,
:success,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha
)
end
context 'when all pipelines have security reports' do
before do
build_1 = build(:ci_build,
:sast_report,
pipeline: old_base_pipeline,
project: project)
build_2 = build(:ci_build,
:sast_report,
pipeline: most_recent_base_pipeline,
project: project)
old_base_pipeline.builds << build_1
most_recent_base_pipeline.builds << build_2
end
it 'returns the most recent base pipeline' do
expect(pipeline).to eq(most_recent_base_pipeline)
end
end
context 'when the most recent pipeline does not have security reports' do
before do
build = build(:ci_build,
:sast_report,
pipeline: old_base_pipeline,
project: project)
old_base_pipeline.builds << build
end
it 'returns the latest base pipeline with security reports' do
expect(pipeline).to eq(old_base_pipeline)
end
end
end
end
end
describe '#latest_comparison_pipeline_with_sbom_reports' do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be_with_refind(:merge_request) do
create(:merge_request, :with_merge_request_pipeline, source_project: project)
end
let_it_be(:base_pipeline) do
create(
:ci_pipeline,
:with_test_reports,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha)
end
subject(:pipeline) { merge_request.latest_comparison_pipeline_with_sbom_reports }
before do
merge_request.update_head_pipeline
end
context 'when there are merge base pipelines' do
let_it_be(:old_merge_base_pipeline) do
create(
:ee_ci_pipeline,
:with_cyclonedx_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.target_branch_sha)
end
let_it_be(:most_recent_merge_base_pipeline) do
create(
:ee_ci_pipeline,
:with_cyclonedx_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.target_branch_sha)
end
context 'when all pipelines have SBOM artifacts' do
it 'returns the most recent merge base pipeline' do
expect(pipeline).to eq(most_recent_merge_base_pipeline)
end
end
context 'when the most recent pipeline does not have an SBOM artifact' do
before do
most_recent_merge_base_pipeline.job_artifacts.cyclonedx.delete_all
end
it 'returns the latest merge base pipeline with an SBOM artifact' do
expect(pipeline).to eq(old_merge_base_pipeline)
end
end
end
context 'when there is no merge base pipeline' do
let_it_be(:old_base_pipeline) do
create(
:ee_ci_pipeline,
:with_cyclonedx_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha)
end
let_it_be(:most_recent_base_pipeline) do
create(
:ee_ci_pipeline,
:with_cyclonedx_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha)
end
context 'when all pipelines have SBOM artifacts' do
it 'returns the most recent base pipeline' do
expect(pipeline).to eq(most_recent_base_pipeline)
end
end
context 'when the most recent pipeline does not have an SBOM artifact' do
before do
most_recent_base_pipeline.job_artifacts.cyclonedx.delete_all
end
it 'returns the latest base pipeline with an SBOM artifact' do
expect(pipeline).to eq(old_base_pipeline)
end
end
end
end
describe '#diff_head_pipeline?' do
let_it_be_with_refind(:merge_request) do
create(:merge_request, :with_merge_request_pipeline, source_project: project)
end
subject(:diff_head_pipeline) { merge_request.diff_head_pipeline?(pipeline) }
context 'when the pipeline is the head pipeline' do
let_it_be(:pipeline) do
create(:ee_ci_pipeline,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha
)
end
it { is_expected.to be_truthy }
end
context 'when the pipeline is merged_results_pipeline' do
let_it_be(:pipeline) do
create(:ci_pipeline, :merged_result_pipeline, :success, project: project, merge_request: merge_request)
end
it { is_expected.to be_truthy }
end
context 'when the pipeline is base_pipeline' do
let_it_be(:pipeline) do
create(:ee_ci_pipeline,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_base_sha
)
end
it { is_expected.to be_falsey }
end
end
describe '#latest_scan_finding_comparison_pipeline' do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be_with_refind(:merge_request) do
create(:merge_request, :with_merge_request_pipeline, source_project: project)
end
let_it_be(:base_pipeline) do
create(
:ee_ci_pipeline,
:with_dependency_scanning_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha)
end
subject(:pipeline) { merge_request.latest_scan_finding_comparison_pipeline }
before do
merge_request.update_head_pipeline
end
context 'when there are merge base pipelines' do
let_it_be(:old_merge_base_pipeline) do
create(
:ee_ci_pipeline,
:success,
:with_dependency_scanning_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.target_branch_sha)
end
let_it_be(:most_recent_merge_base_pipeline) do
create(
:ee_ci_pipeline,
:success,
:with_dependency_scanning_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.target_branch_sha)
end
context 'when all pipelines have security reports' do
it 'returns the most recent merge base pipeline' do
expect(pipeline).to eq(most_recent_merge_base_pipeline)
end
end
context 'when the most recent pipeline does not have security reports' do
before do
most_recent_merge_base_pipeline.job_artifacts.security_reports.delete_all
end
context 'when other merge base pipeline has security reports' do
it 'returns the latest merge base pipeline with security reports' do
expect(pipeline).to eq(old_merge_base_pipeline)
end
end
context 'when no other merge base pipeline has security reports' do
before do
old_merge_base_pipeline.job_artifacts.security_reports.delete_all
end
context 'when base pipeline has security reports' do
it 'returns the base pipeline' do
expect(pipeline).to eq(base_pipeline)
end
end
context 'when no base pipeline has security reports' do
before do
base_pipeline.job_artifacts.security_reports.delete_all
most_recent_merge_base_pipeline.update!(status: :manual)
end
it 'returns nil' do
expect(pipeline).to be_nil
end
end
end
end
end
context 'when there is no merge base pipeline' do
let_it_be_with_refind(:old_base_pipeline) do
create(
:ee_ci_pipeline,
:with_dependency_scanning_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha)
end
let_it_be_with_refind(:most_recent_base_pipeline) do
create(
:ee_ci_pipeline,
:with_dependency_scanning_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha)
end
context 'when all pipelines have security reports' do
it 'returns the most recent base pipeline' do
expect(pipeline).to eq(most_recent_base_pipeline)
end
end
context 'when the most recent pipeline does not have security reports' do
before do
most_recent_base_pipeline.job_artifacts.security_reports.delete_all
end
it 'returns the latest base pipeline with security reports' do
expect(pipeline).to eq(old_base_pipeline)
end
context 'when no base pipeline has security reports' do
before do
base_pipeline.job_artifacts.security_reports.delete_all
old_base_pipeline.job_artifacts.security_reports.delete_all
most_recent_base_pipeline.job_artifacts.security_reports.delete_all
end
it 'returns nil' do
expect(pipeline).to be_nil
end
end
context 'when no base pipeline has completed' do
before do
base_pipeline.update!(status: :waiting_for_resource)
old_base_pipeline.update!(status: :waiting_for_resource)
most_recent_base_pipeline.update!(status: :waiting_for_resource)
end
context 'when comparison prior to pipeline completion is disabled' do
it 'returns nil' do
expect(pipeline).to be_nil
end
end
end
end
context 'when there are multiple diff start pipelines' do
let!(:old_diff_start_pipeline) do
create(
:ee_ci_pipeline,
:with_cyclonedx_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_start_sha,
created_at: 1.hour.ago
)
end
let!(:most_recent_diff_start_pipeline) do
create(
:ee_ci_pipeline,
:with_cyclonedx_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_start_sha,
created_at: Time.current
)
end
let_it_be(:third_diff_start_pipeline) do
create(
:ee_ci_pipeline,
:with_dependency_scanning_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_start_sha
)
end
context 'when all pipelines have security reports' do
before do
allow(old_diff_start_pipeline).to receive(:has_sbom_reports?).and_return(true)
allow(most_recent_diff_start_pipeline).to receive(:has_sbom_reports?).and_return(true)
end
it 'returns the most recent pipeline within the limit' do
expect(pipeline).to eq(most_recent_diff_start_pipeline)
end
end
context 'when the most recent pipeline does not have security reports' do
before do
# Ensure most recent pipeline has no SBOM reports
most_recent_diff_start_pipeline.job_artifacts.cyclonedx.delete_all
# Explicitly set has_sbom_reports? behavior
allow(most_recent_diff_start_pipeline).to receive(:has_sbom_reports?).and_return(false)
allow(old_diff_start_pipeline).to receive(:has_sbom_reports?).and_return(true)
end
it 'returns the latest diff start pipeline with security reports' do
expect(pipeline).to eq(old_diff_start_pipeline)
end
end
context 'with child pipelines' do
let!(:child_pipeline) do
create(
:ee_ci_pipeline,
:with_cyclonedx_report,
project: project,
ref: merge_request.target_branch
)
end
before do
create(:ci_sources_pipeline, source_pipeline: most_recent_diff_start_pipeline, pipeline: child_pipeline)
# Ensure parent pipeline doesn't have SBOM reports directly
most_recent_diff_start_pipeline.job_artifacts.cyclonedx.delete_all
# Mock the has_sbom_reports? method to consider child pipeline reports
allow(most_recent_diff_start_pipeline).to receive(:has_sbom_reports?).and_return(true)
end
it 'considers child pipeline reports' do
expect(pipeline).to eq(most_recent_diff_start_pipeline)
end
end
end
end
context 'when there are no merge base and diff start pipelines' do
before do
base_pipeline.job_artifacts.security_reports.delete_all
end
it 'returns nil' do
expect(pipeline).to be_nil
end
end
end
describe '#find_pipeline_with_dependency_scanning_reports' do
let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:pipeline_with_ds) { create(:ee_ci_pipeline, :with_dependency_scanning_feature_branch) }
let_it_be(:pipeline_without_ds) { create(:ee_ci_pipeline) }
it 'returns the pipeline with dependency_scanning reports' do
pipelines = [pipeline_without_ds, pipeline_with_ds]
result = merge_request.find_pipeline_with_dependency_scanning_reports(pipelines)
expect(result).to eq(pipeline_with_ds)
end
it 'returns nil if no pipeline has dependency_scanning reports' do
pipelines = [pipeline_without_ds]
result = merge_request.find_pipeline_with_dependency_scanning_reports(pipelines)
expect(result).to be_nil
end
it 'returns the first pipeline with dependency_scanning reports if multiple exist' do
another_pipeline_with_ds = create(:ee_ci_pipeline, :with_dependency_scanning_feature_branch)
pipelines = [pipeline_without_ds, pipeline_with_ds, another_pipeline_with_ds]
result = merge_request.find_pipeline_with_dependency_scanning_reports(pipelines)
expect(result).to eq(pipeline_with_ds)
end
end
describe '#blocking_merge_requests_feature_available?' do
let(:merge_request) { build_stubbed(:merge_request) }
subject(:result) { merge_request.blocking_merge_requests_feature_available? }
before do
stub_licensed_features(blocking_merge_requests: blocking_merge_requests_enabled)
end
context 'when blocking_merge_requests feature is enabled' do
let(:blocking_merge_requests_enabled) { true }
it { is_expected.to eq(true) }
end
context 'when blocking_merge_requests feature is disabled' do
let(:blocking_merge_requests_enabled) { false }
it { is_expected.to eq(false) }
end
end
describe '#license_scanning_feature_available?' do
let(:merge_request) { build_stubbed(:merge_request) }
subject(:result) { merge_request.license_scanning_feature_available? }
before do
stub_licensed_features(license_scanning: license_scanning_enabled)
end
context 'when license_scanning feature is enabled' do
let(:license_scanning_enabled) { true }
it { is_expected.to eq(true) }
end
context 'when license_scanning feature is disabled' do
let(:license_scanning_enabled) { false }
it { is_expected.to eq(false) }
end
end
describe '#notify_approvers' do
let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:approvers) { create_list(:user, 2) }
let_it_be(:approval_rule) { create(:approval_merge_request_rule, merge_request: merge_request, users: approvers) }
it 'calls NotificationService.added_as_approver' do
expect_any_instance_of(EE::NotificationService).to receive(:added_as_approver)
.with(merge_request.wrapped_approval_rules.flat_map(&:approvers), merge_request)
merge_request.notify_approvers
end
end
describe '#has_changes_requested?' do
it { expect(merge_request.has_changes_requested?).to be_falsey }
describe 'when merge request has changes requested' do
before do
create(:merge_request_requested_changes, merge_request: merge_request, project: merge_request.project, user: create(:user))
end
it { expect(merge_request.has_changes_requested?).to be_truthy }
end
end
describe '#create_requested_changes' do
let_it_be(:user) { create(:user) }
it 'creates a merge request requested changes' do
expect { merge_request.create_requested_changes(user) }.to change { merge_request.requested_changes.count }.from(0).to(1)
end
end
describe '#destroy_requested_changes' do
let_it_be(:user) { create(:user) }
before do
create(:merge_request_requested_changes, merge_request: merge_request, project: merge_request.project, user: user)
end
it 'destroys a merge request requested changes' do
expect { merge_request.destroy_requested_changes(user) }.to change { merge_request.requested_changes.count }.from(1).to(0)
end
end
describe '#requested_changes_for_users' do
let_it_be(:user) { create(:user) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let_it_be(:requested_changes) do
create(:merge_request_requested_changes, merge_request: merge_request, project: merge_request.project, user: user)
end
before do
create(:merge_request_requested_changes, merge_request: merge_request, project: merge_request.project, user: create(:user))
end
it 'returns requested changes for user IDs' do
expect(merge_request.requested_changes_for_users([user.id])).to contain_exactly(
requested_changes
)
end
end
describe '#ai_review_merge_request_allowed?' do
let_it_be(:project) { create(:project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let_it_be(:current_user) { create(:user, developer_of: project) }
let(:authorizer) { instance_double(::Gitlab::Llm::FeatureAuthorizer) }
subject(:ai_review_merge_request_allowed?) { merge_request.ai_review_merge_request_allowed?(current_user) }
before do
stub_licensed_features(review_merge_request: true)
allow(::Gitlab::Llm::FeatureAuthorizer).to receive(:new).and_return(authorizer)
end
context 'when user does not have permission' do
before do
allow(project).to receive(:duo_features_enabled).and_return(false)
allow(current_user).to receive(:allowed_to_use?)
.with(:review_merge_request, licensed_feature: :review_merge_request).and_return(false)
end
it { is_expected.to eq(false) }
end
context "when feature is authorized" do
before do
allow(authorizer).to receive(:allowed?).and_return(true)
allow(project).to receive(:duo_features_enabled).and_return(true)
allow(current_user).to receive(:allowed_to_use?)
.with(:review_merge_request, licensed_feature: :review_merge_request).and_return(true)
end
it { is_expected.to eq(true) }
context 'when license is not set' do
before do
stub_licensed_features(review_merge_request: false)
allow(current_user).to receive(:allowed_to_use?)
.with(:review_merge_request, licensed_feature: :review_merge_request).and_return(false)
end
it { is_expected.to eq(false) }
end
context 'when user cannot create note' do
let(:current_user) { create(:user, guest_of: project) }
it { is_expected.to eq(false) }
end
end
context "when feature is not authorized" do
before do
allow(authorizer).to receive(:allowed?).and_return(false)
end
it { is_expected.to eq(false) }
end
end
describe '#temporarily_unapproved?' do
subject(:temporarily_unapproved) { merge_request.temporarily_unapproved? }
let(:merge_request) { create(:merge_request) }
context 'when the MR is not temporarily unapproved' do
it 'returns false' do
expect(temporarily_unapproved).to eq(false)
end
end
context 'when the MR is temporarily unapproved' do
before do
merge_request.approval_state.temporarily_unapprove!
end
it 'returns true' do
expect(temporarily_unapproved).to eq(true)
end
end
end
describe '#ai_reviewable_diff_files' do
let(:merge_request) { build_stubbed(:merge_request) }
let(:diff_file_a) { instance_double(Gitlab::Diff::File, ai_reviewable?: true) }
let(:diff_file_b) { instance_double(Gitlab::Diff::File, ai_reviewable?: false) }
let(:diff_file_c) { instance_double(Gitlab::Diff::File, ai_reviewable?: true) }
let(:diff_files) { [diff_file_a, diff_file_b, diff_file_c] }
let(:diffs) { instance_double(Gitlab::Diff::FileCollection::MergeRequestDiff, diff_files: diff_files) }
before do
allow(merge_request).to receive(:diffs).and_return(diffs)
end
it 'returns only AI reviewable diff files' do
expect(merge_request.ai_reviewable_diff_files).to match_array([diff_file_a, diff_file_c])
end
end
describe '#squash_option' do
let_it_be(:target_branch) { 'target_branch' }
let(:merge_request) { build(:merge_request, project: project, target_branch: target_branch) }
let(:project_setting) { project.project_setting }
subject { merge_request.squash_option }
context 'when the target branch matches a wildcard protected branch' do
let_it_be_with_refind(:protected_branch) { create(:protected_branch, name: '*', project_id: project.id) }
it { is_expected.to eq(project_setting) }
end
context 'when the target branch exact matches a protected branch' do
let_it_be_with_refind(:protected_branch) { create(:protected_branch, name: target_branch, project_id: project.id) }
context 'and the protected branch has a defined squash_option' do
let_it_be(:squash_option) { create(:branch_rule_squash_option, protected_branch: protected_branch, project: project) }
it { is_expected.to eq(squash_option) }
end
context 'and the protected branch does not have a squash_option' do
it { is_expected.to eq(project_setting) }
end
end
it { is_expected.to eq(project_setting) }
end
describe '#missing_required_squash?' do
using RSpec::Parameterized::TableSyntax
context 'when the target branch is not protected' do
where(:squash, :require_squash, :expected) do
false | true | true
false | false | false
true | true | false
true | false | false
end
with_them do
let(:merge_request) { build_stubbed(:merge_request, squash: squash, project: project) }
subject { merge_request.missing_required_squash? }
before do
allow(project.project_setting).to receive(:squash_always?).and_return(require_squash)
end
it { is_expected.to eq(expected) }
end
end
context 'when the target branch is protected' do
let(:protected_branch) { create(:protected_branch, name: merge_request.target_branch, project: project) }
where(:squash, :require_squash, :expected) do
false | true | true
false | false | false
true | true | false
true | false | false
end
with_them do
let(:merge_request) { build_stubbed(:merge_request, squash: squash, project: project) }
subject { merge_request.missing_required_squash? }
before do
allow(project.project_setting).to receive(:squash_always?).and_return(require_squash)
end
it { is_expected.to eq(expected) }
end
context 'when the target branch is protected and specifies a squash option' do
let(:squash_option) { create(:branch_rule_squash_option, protected_branch: protected_branch, project: project) }
where(:squash, :project_require_squash, :require_squash, :expected) do
false | false | true | true
false | true | false | false
true | false | true | false
true | true | false | false
end
with_them do
let(:merge_request) { build_stubbed(:merge_request, squash: squash, project: project) }
subject { merge_request.missing_required_squash? }
before do
allow(project.project_setting).to receive(:squash_always?).and_return(project_require_squash)
allow(merge_request).to receive(:squash_option).and_return(squash_option)
allow(squash_option).to receive(:squash_always?).and_return(require_squash)
end
it { is_expected.to eq(expected) }
end
end
end
end
describe '#schedule_policy_synchronization' do
subject(:execute) { merge_request.schedule_policy_synchronization }
it_behaves_like 'synchronizes policies for a merge request'
end
end