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