spec/lib/release_tools/security/implementation_issue_spec.rb (534 lines of code) (raw):
# frozen_string_literal: true
require 'spec_helper'
describe ReleaseTools::Security::ImplementationIssue do
let(:project) do
build(
:project,
iid: 1,
name: 'GitLab'
)
end
let(:references_response) do
double(
'Gitlab::ObjectifiedHash',
short: 430,
relative: "#430",
full: 'gitlab-org/release-tools#430'
)
end
let(:issue_response) do
double(
'Gitlab::ObjectifiedHash',
iid: 430,
project_id: project.iid,
web_url: 'https://gitlab.com/gitlab-org/security/gitlab/-/issues/430',
references: references_response,
labels: []
)
end
let(:release_bot) do
{
id: described_class::GITLAB_RELEASE_BOT_ID,
name: 'GitLab Release Bot'
}
end
let(:mr1) do
build(
:merge_request,
state: described_class::OPENED,
target_branch: 'master',
assignees: [release_bot]
)
end
let(:mr2) do
build(
:merge_request,
state: described_class::OPENED,
target_branch: '12-10-stable-ee',
assignees: [release_bot]
)
end
let(:mr3) do
build(
:merge_request,
state: described_class::OPENED,
target_branch: '12-9-stable-ee',
assignees: [release_bot]
)
end
let(:mr4) do
build(
:merge_request,
state: described_class::OPENED,
target_branch: '12-8-stable-ee',
assignees: [release_bot]
)
end
let(:merge_requests) { [mr1, mr2, mr3, mr4] }
subject { described_class.new(issue_response, merge_requests) }
describe '#project_id' do
it { expect(subject.project_id).to eq(issue_response.project_id) }
end
describe '#iid' do
it { expect(subject.iid).to eq(issue_response.iid) }
end
describe '#merge_requests' do
it { expect(subject.merge_requests).to match_array(merge_requests) }
end
describe '#web_url' do
it { expect(subject.web_url).to eq(issue_response.web_url) }
end
describe '#reference' do
it { expect(subject.reference).to eq(issue_response.references.full) }
end
describe '#ready_to_be_processed?' do
let(:validator) { double(:validator) }
before do
allow(ReleaseTools::Security::MergeRequestsValidator)
.to receive(:new)
.and_return(validator)
allow(validator)
.to receive(:execute)
.and_return([merge_requests, []])
end
context 'with 4 or more merge requests associated and all of them assigned to GitLab bot' do
it { is_expected.to be_ready_to_be_processed }
it 'does not populate pending_reasons' do
expect(subject.pending_reasons).to be_empty
end
end
context 'with less than 4 associated merge requests' do
let(:merge_requests) { [mr1, mr2] }
it { is_expected.not_to be_ready_to_be_processed }
it 'populates pending_reasons' do
subject.ready_to_be_processed?
expect(subject.pending_reasons).to eq([
"Every security issue is expected to have 4 MRs; 3 backports targeting the versions in the " \
"[next scheduled patch release](https://gitlab.com/gitlab-org/gitlab/-/issues/?label_name%5B%5D=upcoming%20security%20release) " \
"and 1 MR targeting master. If this issue legitimately does not need to have all 4 MRs, please add the ~\"reduced backports\" label to this issue"
])
end
it 'is idempotent' do
2.times { subject.ready_to_be_processed? }
expect(subject.pending_reasons).to eq([
"Every security issue is expected to have 4 MRs; 3 backports targeting the versions in the " \
"[next scheduled patch release](https://gitlab.com/gitlab-org/gitlab/-/issues/?label_name%5B%5D=upcoming%20security%20release) " \
"and 1 MR targeting master. If this issue legitimately does not need to have all 4 MRs, please add the ~\"reduced backports\" label to this issue"
])
end
it 'allows an exception label' do
allow(issue_response)
.to receive(:labels)
.and_return([described_class::BACKPORT_EXCEPTION_LABEL])
expect(subject).to be_ready_to_be_processed
expect(subject.pending_reasons).to be_empty
end
context 'with no merge requests' do
let(:merge_requests) { [] }
it 'rejects when no merge requests are present' do
subject.ready_to_be_processed?
expect(subject.pending_reasons).to include(
"There are no MRs linked to this issue. Follow the steps in the issue description to create the required MRs."
)
end
end
end
context 'when a merge request is not assigned to the GitLab Release Bot' do
let(:assignee1) do
{
id: 1234,
name: 'Joe'
}
end
let(:mr4) do
build(
:merge_request,
state: described_class::OPENED,
target_branch: '12-10-stable-ee',
assignees: [assignee1]
)
end
it { is_expected.not_to be_ready_to_be_processed }
it 'populates pending reason' do
subject.ready_to_be_processed?
expect(subject.pending_reasons).to eq(["The following merge requests need to be assigned to the `@gitlab-release-tools-bot`: #{mr4.web_url}"])
end
end
context 'with a merged merge request targeting master' do
let(:mr1) do
build(
:merge_request,
state: 'merged',
target_branch: 'master',
assignees: [release_bot]
)
end
it { is_expected.to be_ready_to_be_processed }
end
context 'with merged backports' do
let(:mr2) do
build(
:merge_request,
state: described_class::MERGED,
target_branch: '12-10-stable-ee',
assignees: []
)
end
it { is_expected.not_to be_ready_to_be_processed }
it 'populates pending reason' do
subject.ready_to_be_processed?
expect(subject.pending_reasons).to eq([
"The following backport MRs need to be in the 'opened' state: #{mr2.web_url}",
"The following merge requests need to be assigned to the `@gitlab-release-tools-bot`: #{mr2.web_url}"
])
end
end
context 'with valid merge requests' do
it { is_expected.to be_ready_to_be_processed }
end
context 'with invalid merge requests' do
before do
allow(validator)
.to receive(:execute)
.and_return([[mr1, mr2, mr3], [mr4]])
end
it { is_expected.not_to be_ready_to_be_processed }
it 'populates pending reason' do
subject.ready_to_be_processed?
expect(subject.pending_reasons).to eq([
"The following merge requests are not ready. A comment has been posted in each MR with more details: #{mr4.web_url}"
])
end
end
context 'with merged merge requests' do
let(:mr1) do
double(
:merge_request,
state: described_class::MERGED,
target_branch: 'master',
assignees: [release_bot]
)
end
it 'does not validate merged merge requests' do
opened_merge_requests = [mr2, mr3, mr4]
expect(validator)
.to receive(:execute)
.with(merge_requests: opened_merge_requests)
subject.ready_to_be_processed?
end
end
end
describe '#merge_requests_targeting_default_branch' do
let(:merge_requests) { [mr1, mr2, mr3, mr4, mr5] }
let(:mr5) do
build(
:merge_request,
state: described_class::MERGED,
target_branch: 'master',
assignees: [release_bot]
)
end
it 'returns the two merge requests targeting master' do
expect(subject.merge_requests_targeting_default_branch).to eq([mr1, mr5])
end
end
describe '#merge_request_targeting_default_branch' do
it 'returns the merge request targeting master' do
expect(subject.merge_request_targeting_default_branch).to eq(mr1)
end
end
describe '#default_merge_request_handled?' do
let(:mr1) do
build(
:merge_request,
state: described_class::MERGED,
target_branch: 'master'
)
end
it 'returns true if merge request targeting master is in merged state' do
expect(subject.default_merge_request_handled?).to be(true)
end
context 'opened MR' do
let(:mr1) do
build(
:merge_request,
state: described_class::OPENED,
target_branch: 'master'
)
end
it 'returns false if merge request targeting master is in non-merged state' do
expect(subject.default_merge_request_handled?).to be(false)
end
end
context 'when there is no default merge request' do
let(:merge_requests) { [mr2] }
let(:labels) { [] }
before do
allow(issue_response)
.to receive(:labels)
.and_return(labels)
end
it 'returns false' do
expect(subject.default_merge_request_handled?).to be(false)
end
context 'with reduced backports label' do
let(:labels) { [described_class::BACKPORT_EXCEPTION_LABEL] }
it 'returns true' do
expect(subject.default_merge_request_handled?).to be(true)
end
end
end
end
describe '#mwps_set_on_default_merge_request?' do
let(:mr1) do
build(
:merge_request,
state: described_class::OPENED,
merge_when_pipeline_succeeds: true,
target_branch: 'master'
)
end
it 'returns true if merge request targeting master is in open state and MWPS is set' do
expect(subject.mwps_set_on_default_merge_request?).to be(true)
end
context 'when there is no default merge request' do
let(:merge_requests) { [mr2] }
it 'returns false' do
expect(subject.mwps_set_on_default_merge_request?).to be(false)
end
end
end
describe '#default_merge_request_deployed?' do
let(:mr1) do
build(
:merge_request,
target_branch: 'master',
project_id: ReleaseTools::Project::GitlabEe.security_id,
first_deployed_to_production_at: Time.now.iso8601,
web_url: 'https://gitlab.com',
iid: 346
)
end
it 'returns true if API returns MR' do
allow(ReleaseTools::GitlabClient)
.to receive(:merge_requests)
.with(mr1.project_id, { iids: [mr1.iid], environment: 'gprd' })
.and_return([mr1])
expect(subject.default_merge_request_deployed?).to be(true)
end
it 'returns false if API returns empty response' do
allow(ReleaseTools::GitlabClient)
.to receive(:merge_requests)
.with(mr1.project_id, { iids: [mr1.iid], environment: 'gprd' })
.and_return([])
expect(subject.default_merge_request_deployed?).to be(false)
end
context 'when there is no default merge request' do
let(:merge_requests) { [mr2] }
it 'returns false' do
expect(subject.default_merge_request_deployed?).to be(false)
end
end
end
describe '#backports_merged?' do
let(:mr1) do
build(
:merge_request,
state: described_class::OPENED,
target_branch: '12-7-stable-ee',
assignees: [release_bot]
)
end
let(:mr2) do
build(
:merge_request,
state: described_class::MERGED,
target_branch: '12-8-stable-ee',
assignees: [release_bot]
)
end
let(:mr3) do
double(
:merge_request,
state: described_class::MERGED,
target_branch: '12-9-stable-ee',
assignees: [release_bot]
)
end
context 'when all backports have not been merged' do
let(:merge_requests) { [mr1, mr2] }
it 'returns false' do
expect(subject.backports_merged?).to be(false)
end
end
context 'when all backports have been merged' do
let(:merge_requests) { [mr2, mr3] }
it 'returns true' do
expect(subject.backports_merged?).to be(true)
end
end
context 'when there are no backports' do
let(:merge_requests) { [] }
it 'returns false' do
expect(subject.backports_merged?).to be(false)
end
end
end
describe '#backports' do
let(:mr5) do
double(
:merge_request,
target_branch: '12-10-stable',
assignees: [release_bot]
)
end
let(:merge_requests) { [mr1, mr2, mr3, mr5] }
it 'returns merge requests targeting stable branches' do
merge_requests_targeting_stable_branches = [
mr2,
mr3,
mr5
]
expect(subject.backports)
.to match_array(merge_requests_targeting_stable_branches)
end
end
describe '#processed?' do
let(:mr1) { build(:merge_request, state: described_class::MERGED) }
let(:mr2) { build(:merge_request, state: described_class::MERGED) }
let(:merge_requests) do
[mr1, mr2]
end
context 'with all merge requests merged' do
it 'returns true' do
issue1 = described_class.new(double.as_null_object, merge_requests)
expect(issue1).to be_processed
end
end
context 'with a merge request opened' do
let(:mr2) do
build(
:merge_request,
state: described_class::OPENED
)
end
it 'returns false' do
issue1 = described_class.new(double.as_null_object, merge_requests)
expect(issue1).not_to be_processed
end
end
end
describe '#allowed_to_early_merge?' do
let(:mr) do
build(
:merge_request,
target_branch: 'master',
state: described_class::OPENED,
project_id: ReleaseTools::Project::GitlabEe.security_id
)
end
subject do
described_class.new(issue_response, [mr])
end
context 'with an MR targeting master' do
context 'with an opened MR that belongs to GitLab Security' do
it { is_expected.to be_allowed_to_early_merge }
end
context 'when an MR belongs to other project' do
let(:mr) do
build(
:merge_request,
state: described_class::OPENED,
target_branch: 'master',
project_id: 1
)
end
it { is_expected.not_to be_allowed_to_early_merge }
end
context 'with a merged MR' do
let(:mr) do
build(
:merge_request,
target_branch: 'master',
state: described_class::MERGED,
project_id: ReleaseTools::Project::GitlabEe.security_id
)
end
it { is_expected.not_to be_allowed_to_early_merge }
end
end
context 'with no MR targeting master' do
let(:mr) do
build(
:merge_request,
target_branch: '13-3-stable-ee',
project_id: 123
)
end
it { is_expected.not_to be_allowed_to_early_merge }
end
end
describe '#pending_merge_requests' do
context 'with a GitLab or Omnibus security issue' do
it 'returns backports' do
described_class::PROJECTS_ALLOWED_TO_EARLY_MERGE.each do |project_id|
issue_response = double(
project_id: project_id, iid: 1, web_url: '', references: references_response
)
issue = described_class.new(issue_response, merge_requests)
expect(issue.pending_merge_requests).to match_array(issue.backports)
end
end
end
context 'with another security issue' do
it 'returns opened merge requests' do
issue = described_class.new(issue_response, merge_requests)
expect(issue.pending_merge_requests).to match_array(merge_requests)
end
end
end
describe '#cves_issue' do
let(:finder) { instance_double(ReleaseTools::Security::RelatedIssuesFinder) }
let(:cves_issue) { create(:issue) }
before do
allow(ReleaseTools::Security::RelatedIssuesFinder)
.to receive(:new)
.and_return(finder)
allow(finder)
.to receive(:cves_issue)
.and_return(cves_issue)
end
context 'when the cve issue can be found' do
it 'returns a CVES issue instance' do
expect(subject.cves_issue)
.to be_a_instance_of(ReleaseTools::Security::CvesIssue)
end
end
context 'when the cve issue cannot be found' do
let(:cves_issue) { nil }
it 'returns nothing' do
expect(subject.cves_issue).to be_nil
end
end
end
describe '#canonical_issue' do
let(:finder) { instance_double(ReleaseTools::Security::RelatedIssuesFinder) }
let(:canonical_issue) { create(:issue) }
before do
allow(ReleaseTools::Security::RelatedIssuesFinder)
.to receive(:new)
.and_return(finder)
allow(finder)
.to receive(:canonical_issue)
.and_return(canonical_issue)
end
it 'returns the canonical issue' do
expect(subject.canonical_issue).to eq(canonical_issue)
end
context 'when the cve issue cannot be found' do
let(:canonical_issue) { nil }
it 'returns nothing' do
expect(subject.canonical_issue).to be_nil
end
end
end
end