Commit 8c28ec09 authored by Saikat Sarkar's avatar Saikat Sarkar

Merge branch 'eb-code-coverage-approvals' into 'master'

Add backend support for Coverage-Check rule

See merge request gitlab-org/gitlab!63079
parents f690b908 c5570094
...@@ -205,6 +205,8 @@ module Ci ...@@ -205,6 +205,8 @@ module Ci
end end
scope :with_coverage, -> { where.not(coverage: nil) } scope :with_coverage, -> { where.not(coverage: nil) }
scope :without_coverage, -> { where(coverage: nil) }
scope :with_coverage_regex, -> { where.not(coverage_regex: nil) }
scope :for_project, -> (project_id) { where(project_id: project_id) } scope :for_project, -> (project_id) { where(project_id: project_id) }
......
...@@ -644,6 +644,10 @@ module Ci ...@@ -644,6 +644,10 @@ module Ci
end end
end end
def update_builds_coverage
builds.with_coverage_regex.without_coverage.each(&:update_coverage)
end
def batch_lookup_report_artifact_for_file_type(file_type) def batch_lookup_report_artifact_for_file_type(file_type)
latest_report_artifacts latest_report_artifacts
.values_at(*::Ci::JobArtifact.associated_file_types_for(file_type.to_s)) .values_at(*::Ci::JobArtifact.associated_file_types_for(file_type.to_s))
......
# frozen_string_literal: true
class RenameSyncSecurityReportApprovalRulesSidekiqQueue < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
sidekiq_queue_migrate 'sync_security_reports_to_report_approval_rules', to: 'ci_sync_reports_to_report_approval_rules'
end
def down
sidekiq_queue_migrate 'ci_sync_reports_to_report_approval_rules', to: 'sync_security_reports_to_report_approval_rules'
end
end
ec4cd687062118b30e516ed7c36677dda056f25c4d96c6ee0b503e457b5a18d4
\ No newline at end of file
...@@ -56,11 +56,13 @@ class ApprovalMergeRequestRule < ApplicationRecord ...@@ -56,11 +56,13 @@ class ApprovalMergeRequestRule < ApplicationRecord
enum report_type: { enum report_type: {
vulnerability: 1, vulnerability: 1,
license_scanning: 2 license_scanning: 2,
code_coverage: 3
} }
scope :vulnerability_report, -> { report_approver.vulnerability } scope :vulnerability_report, -> { report_approver.vulnerability }
scope :license_compliance, -> { report_approver.license_scanning } scope :license_compliance, -> { report_approver.license_scanning }
scope :coverage, -> { report_approver.code_coverage }
scope :with_head_pipeline, -> { includes(merge_request: [:head_pipeline]) } scope :with_head_pipeline, -> { includes(merge_request: [:head_pipeline]) }
scope :open_merge_requests, -> { merge(MergeRequest.opened) } scope :open_merge_requests, -> { merge(MergeRequest.opened) }
scope :for_checks_that_can_be_refreshed, -> { license_compliance.open_merge_requests.with_head_pipeline } scope :for_checks_that_can_be_refreshed, -> { license_compliance.open_merge_requests.with_head_pipeline }
......
...@@ -6,9 +6,11 @@ module ApprovalRuleLike ...@@ -6,9 +6,11 @@ module ApprovalRuleLike
DEFAULT_NAME = 'Default' DEFAULT_NAME = 'Default'
DEFAULT_NAME_FOR_LICENSE_REPORT = 'License-Check' DEFAULT_NAME_FOR_LICENSE_REPORT = 'License-Check'
DEFAULT_NAME_FOR_VULNERABILITY_REPORT = 'Vulnerability-Check' DEFAULT_NAME_FOR_VULNERABILITY_REPORT = 'Vulnerability-Check'
DEFAULT_NAME_FOR_COVERAGE = 'Coverage-Check'
REPORT_TYPES_BY_DEFAULT_NAME = { REPORT_TYPES_BY_DEFAULT_NAME = {
DEFAULT_NAME_FOR_LICENSE_REPORT => :license_scanning, DEFAULT_NAME_FOR_LICENSE_REPORT => :license_scanning,
DEFAULT_NAME_FOR_VULNERABILITY_REPORT => :vulnerability DEFAULT_NAME_FOR_VULNERABILITY_REPORT => :vulnerability,
DEFAULT_NAME_FOR_COVERAGE => :code_coverage
}.freeze }.freeze
APPROVALS_REQUIRED_MAX = 100 APPROVALS_REQUIRED_MAX = 100
ALL_MEMBERS = 'All Members' ALL_MEMBERS = 'All Members'
......
...@@ -65,7 +65,12 @@ module EE ...@@ -65,7 +65,12 @@ module EE
pipeline.run_after_commit do pipeline.run_after_commit do
StoreSecurityReportsWorker.perform_async(pipeline.id) if pipeline.default_branch? StoreSecurityReportsWorker.perform_async(pipeline.id) if pipeline.default_branch?
::Security::StoreScansWorker.perform_async(pipeline.id) ::Security::StoreScansWorker.perform_async(pipeline.id)
SyncSecurityReportsToReportApprovalRulesWorker.perform_async(pipeline.id) end
end
after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
pipeline.run_after_commit do
::Ci::SyncReportsToReportApprovalRulesWorker.perform_async(pipeline.id)
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
module Security module Ci
# Service for syncing security reports results to report_approver approval rules
#
class SyncReportsToApprovalRulesService < ::BaseService class SyncReportsToApprovalRulesService < ::BaseService
def initialize(pipeline) def initialize(pipeline)
@pipeline = pipeline @pipeline = pipeline
...@@ -11,6 +9,7 @@ module Security ...@@ -11,6 +9,7 @@ module Security
def execute def execute
sync_license_scanning_rules sync_license_scanning_rules
sync_vulnerability_rules sync_vulnerability_rules
sync_coverage_rules
success success
rescue StandardError => error rescue StandardError => error
log_error( log_error(
...@@ -42,14 +41,30 @@ module Security ...@@ -42,14 +41,30 @@ module Security
# If we don't have reports, then we should wait until pipeline stops. # If we don't have reports, then we should wait until pipeline stops.
return if reports.empty? && !pipeline.complete? return if reports.empty? && !pipeline.complete?
remove_required_approvals_for(ApprovalMergeRequestRule.vulnerability_report, sync_required_merge_requests) remove_required_approvals_for(ApprovalMergeRequestRule.vulnerability_report, merge_requests_approved_security_reports)
end
def sync_coverage_rules
return unless pipeline.complete?
pipeline.update_builds_coverage
remove_required_approvals_for(ApprovalMergeRequestRule.code_coverage, merge_requests_approved_coverage)
end end
def reports def reports
@reports ||= pipeline.security_reports @reports ||= pipeline.security_reports
end end
def sync_required_merge_requests def merge_requests_approved_coverage
pipeline.merge_requests_as_head_pipeline.reject do |merge_request|
base_pipeline = merge_request.base_pipeline
# if base pipeline is missing we just default to not require approval.
pipeline.coverage < base_pipeline.coverage if base_pipeline.present?
end
end
def merge_requests_approved_security_reports
pipeline.merge_requests_as_head_pipeline.reject do |merge_request| pipeline.merge_requests_as_head_pipeline.reject do |merge_request|
reports.violates_default_policy_against?(merge_request.base_pipeline&.security_reports) reports.violates_default_policy_against?(merge_request.base_pipeline&.security_reports)
end end
......
...@@ -15,7 +15,7 @@ module EE ...@@ -15,7 +15,7 @@ module EE
private private
def schedule_sync_for(pipeline_id) def schedule_sync_for(pipeline_id)
::SyncSecurityReportsToReportApprovalRulesWorker.perform_async(pipeline_id) if pipeline_id ::Ci::SyncReportsToReportApprovalRulesWorker.perform_async(pipeline_id) if pipeline_id
end end
end end
end end
......
...@@ -773,6 +773,15 @@ ...@@ -773,6 +773,15 @@
:weight: 1 :weight: 1
:idempotent: :idempotent:
:tags: [] :tags: []
- :name: pipeline_background:ci_sync_reports_to_report_approval_rules
:worker_name: Ci::SyncReportsToReportApprovalRulesWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 1
:idempotent:
:tags: []
- :name: pipeline_default:ci_trigger_downstream_subscriptions - :name: pipeline_default:ci_trigger_downstream_subscriptions
:worker_name: Ci::TriggerDownstreamSubscriptionsWorker :worker_name: Ci::TriggerDownstreamSubscriptionsWorker
:feature_category: :continuous_integration :feature_category: :continuous_integration
...@@ -819,15 +828,6 @@ ...@@ -819,15 +828,6 @@
:weight: 2 :weight: 2
:idempotent: :idempotent:
:tags: [] :tags: []
- :name: security_scans:sync_security_reports_to_report_approval_rules
:worker_name: SyncSecurityReportsToReportApprovalRulesWorker
:feature_category: :vulnerability_management
:has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 2
:idempotent:
:tags: []
- :name: todos_destroyer:todos_destroyer_confidential_epic - :name: todos_destroyer:todos_destroyer_confidential_epic
:worker_name: TodosDestroyer::ConfidentialEpicWorker :worker_name: TodosDestroyer::ConfidentialEpicWorker
:feature_category: :epics :feature_category: :epics
......
# frozen_string_literal: true
# Worker for syncing report_type approval_rules approvals_required
module Ci
class SyncReportsToReportApprovalRulesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
sidekiq_options retry: 3
include PipelineBackgroundQueue
urgency :high
worker_resource_boundary :cpu
def perform(pipeline_id)
pipeline = Ci::Pipeline.find_by_id(pipeline_id)
return unless pipeline
::Ci::SyncReportsToApprovalRulesService.new(pipeline).execute
end
end
end
# frozen_string_literal: true
# Worker for syncing report_type approval_rules approvals_required
#
class SyncSecurityReportsToReportApprovalRulesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
sidekiq_options retry: 3
include SecurityScansQueue
urgency :high
worker_resource_boundary :cpu
def perform(pipeline_id)
pipeline = Ci::Pipeline.find_by_id(pipeline_id)
return unless pipeline
::Security::SyncReportsToApprovalRulesService.new(pipeline).execute
end
end
...@@ -28,10 +28,20 @@ FactoryBot.define do ...@@ -28,10 +28,20 @@ FactoryBot.define do
approvals_required { rand(1..ApprovalProjectRule::APPROVALS_REQUIRED_MAX) } approvals_required { rand(1..ApprovalProjectRule::APPROVALS_REQUIRED_MAX) }
end end
trait :vulnerability do
name { ApprovalRuleLike::DEFAULT_NAME_FOR_VULNERABILITY_REPORT }
report_type { :vulnerability }
end
trait :license_scanning do trait :license_scanning do
name { ApprovalRuleLike::DEFAULT_NAME_FOR_LICENSE_REPORT } name { ApprovalRuleLike::DEFAULT_NAME_FOR_LICENSE_REPORT }
report_type { :license_scanning } report_type { :license_scanning }
end end
trait :code_coverage do
name { ApprovalRuleLike::DEFAULT_NAME_FOR_COVERAGE }
report_type { :code_coverage }
end
end end
factory :any_approver_rule, parent: :approval_merge_request_rule do factory :any_approver_rule, parent: :approval_merge_request_rule do
...@@ -61,5 +71,10 @@ FactoryBot.define do ...@@ -61,5 +71,10 @@ FactoryBot.define do
name { ApprovalRuleLike::DEFAULT_NAME_FOR_LICENSE_REPORT } name { ApprovalRuleLike::DEFAULT_NAME_FOR_LICENSE_REPORT }
rule_type { :report_approver } rule_type { :report_approver }
end end
trait :code_coverage do
name { ApprovalRuleLike::DEFAULT_NAME_FOR_COVERAGE }
rule_type { :report_approver }
end
end end
end end
...@@ -115,6 +115,8 @@ RSpec.describe ApprovalMergeRequestRule, factory_default: :keep do ...@@ -115,6 +115,8 @@ RSpec.describe ApprovalMergeRequestRule, factory_default: :keep do
let!(:css_rule) { create(:code_owner_rule, name: '*.css') } let!(:css_rule) { create(:code_owner_rule, name: '*.css') }
let!(:approval_rule) { create(:approval_merge_request_rule) } let!(:approval_rule) { create(:approval_merge_request_rule) }
let!(:report_approver_rule) { create(:report_approver_rule) } let!(:report_approver_rule) { create(:report_approver_rule) }
let!(:coverage_rule) { create(:report_approver_rule, :code_coverage) }
let!(:license_rule) { create(:report_approver_rule, :license_scanning) }
describe '.not_matching_pattern' do describe '.not_matching_pattern' do
it 'returns the correct rules' do it 'returns the correct rules' do
...@@ -143,6 +145,20 @@ RSpec.describe ApprovalMergeRequestRule, factory_default: :keep do ...@@ -143,6 +145,20 @@ RSpec.describe ApprovalMergeRequestRule, factory_default: :keep do
.to contain_exactly(report_approver_rule) .to contain_exactly(report_approver_rule)
end end
end end
describe '.license_compliance' do
it 'returns the correct rules' do
expect(described_class.license_compliance)
.to contain_exactly(license_rule)
end
end
describe '.coverage' do
it 'returns the correct rules' do
expect(described_class.coverage)
.to contain_exactly(coverage_rule)
end
end
end end
describe '.find_or_create_code_owner_rule' do describe '.find_or_create_code_owner_rule' do
......
...@@ -353,6 +353,22 @@ RSpec.describe Ci::Pipeline do ...@@ -353,6 +353,22 @@ RSpec.describe Ci::Pipeline do
end end
describe 'state machine transitions' do describe 'state machine transitions' do
context 'on pipeline complete' do
let(:pipeline) { create(:ci_empty_pipeline, status: from_status) }
Ci::HasStatus::ACTIVE_STATUSES.each do |status|
context "from #{status}" do
let(:from_status) { status }
it 'schedules Ci::SyncReportsToReportApprovalRulesWorker' do
expect(Ci::SyncReportsToReportApprovalRulesWorker).to receive(:perform_async).with(pipeline.id)
pipeline.succeed
end
end
end
end
context 'when pipeline has downstream bridges' do context 'when pipeline has downstream bridges' do
before do before do
pipeline.downstream_bridges << create(:ci_bridge) pipeline.downstream_bridges << create(:ci_bridge)
......
...@@ -11,7 +11,7 @@ RSpec.describe MergeRequests::AfterCreateService do ...@@ -11,7 +11,7 @@ RSpec.describe MergeRequests::AfterCreateService do
subject(:execute) { service_object.execute(merge_request) } subject(:execute) { service_object.execute(merge_request) }
before do before do
allow(SyncSecurityReportsToReportApprovalRulesWorker).to receive(:perform_async) allow(Ci::SyncReportsToReportApprovalRulesWorker).to receive(:perform_async)
end end
context 'when the merge request has actual_head_pipeline' do context 'when the merge request has actual_head_pipeline' do
...@@ -26,7 +26,7 @@ RSpec.describe MergeRequests::AfterCreateService do ...@@ -26,7 +26,7 @@ RSpec.describe MergeRequests::AfterCreateService do
execute execute
expect(merge_request).to have_received(:update_head_pipeline).ordered expect(merge_request).to have_received(:update_head_pipeline).ordered
expect(SyncSecurityReportsToReportApprovalRulesWorker).to have_received(:perform_async).ordered.with(pipeline_id) expect(Ci::SyncReportsToReportApprovalRulesWorker).to have_received(:perform_async).ordered.with(pipeline_id)
end end
end end
...@@ -34,7 +34,7 @@ RSpec.describe MergeRequests::AfterCreateService do ...@@ -34,7 +34,7 @@ RSpec.describe MergeRequests::AfterCreateService do
it 'does not schedule a background job to sync security reports to approval rules' do it 'does not schedule a background job to sync security reports to approval rules' do
execute execute
expect(SyncSecurityReportsToReportApprovalRulesWorker).not_to have_received(:perform_async) expect(Ci::SyncReportsToReportApprovalRulesWorker).not_to have_received(:perform_async)
end end
end end
end end
......
...@@ -12,87 +12,64 @@ RSpec.describe MergeRequests::SyncReportApproverApprovalRules do ...@@ -12,87 +12,64 @@ RSpec.describe MergeRequests::SyncReportApproverApprovalRules do
stub_licensed_features(report_approver_rules: true) stub_licensed_features(report_approver_rules: true)
end end
context "when a project has a single `#{ApprovalProjectRule::DEFAULT_NAME_FOR_VULNERABILITY_REPORT}` approval rule" do ApprovalRuleLike::REPORT_TYPES_BY_DEFAULT_NAME.keys.each do |default_name|
let!(:vulnerability_approval_project_rule) { create(:approval_project_rule, :vulnerability_report, project: merge_request.target_project, approvals_required: 2) } context "when a project has a single `#{default_name}` approval rule" do
let(:report_type) { ApprovalRuleLike::REPORT_TYPES_BY_DEFAULT_NAME[default_name] }
context 'when report_approver_rules are enabled' do let!(:report_approval_project_rule) { create(:approval_project_rule, report_type, project: merge_request.target_project, approvals_required: 2) }
let!(:regular_approval_project_rule) { create(:approval_project_rule, project: merge_request.target_project) } let!(:regular_approval_project_rule) { create(:approval_project_rule, project: merge_request.target_project) }
context 'when report_approver_rules are enabled' do
it 'creates rule for report approvers' do it 'creates rule for report approvers' do
expect { service.execute } expect { service.execute }
.to change { merge_request.approval_rules.vulnerability_report.count }.from(0).to(1) .to change { merge_request.approval_rules.where(name: default_name).count }.from(0).to(1)
rule = merge_request.approval_rules.vulnerability_report.first rule = merge_request.approval_rules.find_by(name: default_name)
expect(rule).to be_report_approver expect(rule).to be_report_approver
expect(rule.report_type).to eq 'vulnerability' expect(rule.report_type).to eq(report_type.to_s)
expect(rule.name).to eq(vulnerability_approval_project_rule.name) expect(rule.name).to eq(report_approval_project_rule.name)
expect(rule.approval_project_rule).to eq(vulnerability_approval_project_rule) expect(rule.approvals_required).to eq(report_approval_project_rule.approvals_required)
expect(rule.approval_project_rule).to eq(report_approval_project_rule)
end end
it 'updates previous rules if defined' do it 'updates previous report approval rule if defined' do
mr_rule = create(:report_approver_rule, merge_request: merge_request, approvals_required: 0) previous_rule = create(:report_approver_rule, report_type, merge_request: merge_request, approvals_required: 0)
expect { service.execute } expect { service.execute }
.not_to change { merge_request.approval_rules.vulnerability_report.count } .not_to change { merge_request.approval_rules.where(name: default_name).count }
expect(mr_rule.reload).to be_report_approver expect(previous_rule.reload).to be_report_approver
expect(mr_rule.report_type).to eq 'vulnerability' expect(previous_rule.report_type).to eq(report_type.to_s)
expect(mr_rule.name).to eq(vulnerability_approval_project_rule.name) expect(previous_rule.name).to eq(report_approval_project_rule.name)
expect(mr_rule.approvals_required).to eq vulnerability_approval_project_rule.approvals_required expect(previous_rule.approvals_required).to eq(report_approval_project_rule.approvals_required)
expect(mr_rule.approval_project_rule).to eq(vulnerability_approval_project_rule) expect(previous_rule.approval_project_rule).to eq(report_approval_project_rule)
end
end
end end
context "when a project has a single `#{ApprovalProjectRule::DEFAULT_NAME_FOR_LICENSE_REPORT}` approval rule" do
let!(:project_rule) { create(:approval_project_rule, :license_scanning, project: merge_request.target_project) }
context "when the rule has not been synchronized to the merge request yet" do
let(:result) { merge_request.reload.approval_rules.last }
before do
service.execute
end end
specify { expect(merge_request.reload.approval_rules.count).to be(1) }
specify { expect(result).to be_report_approver }
specify { expect(result.report_type).to eq('license_scanning') }
specify { expect(result.name).to eq(project_rule.name) }
specify { expect(result.approval_project_rule).to eq(project_rule) }
specify { expect(result.approvals_required).to eql(project_rule.approvals_required) }
end
context "when the rule had previously been synchronized" do
let!(:previous_rule) { create(:report_approver_rule, :license_scanning, merge_request: merge_request) }
before do
service.execute
end
specify { expect(merge_request.reload.approval_rules.count).to be(1) }
specify { expect(merge_request.reload.approval_rules[0]).to eql(previous_rule) }
end end
end end
context "when a project has multiple report approval rules" do context "when a project has multiple report approval rules" do
let!(:vulnerability_project_rule) { create(:approval_project_rule, :vulnerability_report, project: merge_request.target_project) } let!(:vulnerability_project_rule) { create(:approval_project_rule, :vulnerability_report, project: merge_request.target_project) }
let!(:license_compliance_project_rule) { create(:approval_project_rule, :license_scanning, project: merge_request.target_project) } let!(:license_compliance_project_rule) { create(:approval_project_rule, :license_scanning, project: merge_request.target_project) }
let!(:coverage_project_rule) { create(:approval_project_rule, :code_coverage, project: merge_request.target_project) }
context "when none of the rules have been synchronized to the merge request yet" do context "when none of the rules have been synchronized to the merge request yet" do
let(:vulnerability_check_rule) { merge_request.reload.approval_rules.vulnerability_report.last } let(:vulnerability_check_rule) { merge_request.reload.approval_rules.vulnerability_report.last }
let(:license_check_rule) { merge_request.reload.approval_rules.find_by(name: ApprovalProjectRule::DEFAULT_NAME_FOR_LICENSE_REPORT) } let(:license_check_rule) { merge_request.reload.approval_rules.license_compliance.last }
let(:coverage_check_rule) { merge_request.reload.approval_rules.coverage.last }
before do before do
vulnerability_project_rule.users << create(:user) vulnerability_project_rule.users << create(:user)
vulnerability_project_rule.groups << create(:group) vulnerability_project_rule.groups << create(:group)
license_compliance_project_rule.users << create(:user) license_compliance_project_rule.users << create(:user)
license_compliance_project_rule.groups << create(:group) license_compliance_project_rule.groups << create(:group)
coverage_project_rule.users << create(:user)
coverage_project_rule.groups << create(:group)
service.execute service.execute
end end
specify { expect(merge_request.reload.approval_rules.count).to be(2) } specify { expect(merge_request.reload.approval_rules.count).to be(3) }
specify { expect(vulnerability_check_rule).to be_report_approver } specify { expect(vulnerability_check_rule).to be_report_approver }
specify { expect(vulnerability_check_rule.approvals_required).to eql(vulnerability_project_rule.approvals_required) } specify { expect(vulnerability_check_rule.approvals_required).to eql(vulnerability_project_rule.approvals_required) }
specify { expect(vulnerability_check_rule).to be_vulnerability } specify { expect(vulnerability_check_rule).to be_vulnerability }
...@@ -103,6 +80,11 @@ RSpec.describe MergeRequests::SyncReportApproverApprovalRules do ...@@ -103,6 +80,11 @@ RSpec.describe MergeRequests::SyncReportApproverApprovalRules do
specify { expect(license_check_rule).to be_license_scanning } specify { expect(license_check_rule).to be_license_scanning }
specify { expect(license_check_rule.name).to eq(license_compliance_project_rule.name) } specify { expect(license_check_rule.name).to eq(license_compliance_project_rule.name) }
specify { expect(license_check_rule.approval_project_rule).to eq(license_compliance_project_rule) } specify { expect(license_check_rule.approval_project_rule).to eq(license_compliance_project_rule) }
specify { expect(coverage_check_rule).to be_report_approver }
specify { expect(coverage_check_rule.approvals_required).to eql(coverage_project_rule.approvals_required) }
specify { expect(coverage_check_rule).to be_code_coverage }
specify { expect(coverage_check_rule.name).to eq(coverage_project_rule.name) }
specify { expect(coverage_check_rule.approval_project_rule).to eq(coverage_project_rule) }
end end
context "when some of the rules have been synchronized to the merge request" do context "when some of the rules have been synchronized to the merge request" do
...@@ -112,9 +94,10 @@ RSpec.describe MergeRequests::SyncReportApproverApprovalRules do ...@@ -112,9 +94,10 @@ RSpec.describe MergeRequests::SyncReportApproverApprovalRules do
service.execute service.execute
end end
specify { expect(merge_request.reload.approval_rules.count).to be(2) } specify { expect(merge_request.reload.approval_rules.count).to be(3) }
specify { expect(merge_request.reload.approval_rules.vulnerability_report.count).to be(1) } specify { expect(merge_request.reload.approval_rules.vulnerability_report.count).to be(1) }
specify { expect(merge_request.reload.approval_rules.where(report_type: :license_scanning)).to match_array([previous_rule]) } specify { expect(merge_request.reload.approval_rules.coverage.count).to be(1) }
specify { expect(merge_request.reload.approval_rules.license_compliance).to match_array([previous_rule]) }
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Security::SyncReportsToApprovalRulesService, '#execute' do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:pipeline) { create(:ee_ci_pipeline, :success, project: project, merge_requests_as_head_pipeline: [merge_request]) }
let(:report_approver_rule) { create(:report_approver_rule, merge_request: merge_request, approvals_required: 2) }
let(:base_pipeline) { create(:ee_ci_pipeline, :success, project: project, ref: merge_request.target_branch, sha: merge_request.diff_base_sha) }
subject { described_class.new(pipeline).execute }
before do
allow(Ci::Pipeline).to receive(:find).with(pipeline.id) { pipeline }
stub_licensed_features(dependency_scanning: true, dast: true, license_scanning: true)
end
context 'when there are reports' do
context 'when pipeline passes' do
context 'when high-severity vulnerabilities are present' do
before do
create(:ee_ci_build, :success, :dependency_scanning, name: 'ds_job', pipeline: pipeline, project: project)
end
context 'when high-severity vulnerabilities already present in target branch pipeline' do
before do
create(:ee_ci_build, :success, :dependency_scanning, name: 'ds_job', pipeline: base_pipeline, project: project)
end
it 'lowers approvals_required count to zero' do
expect { subject }
.to change { report_approver_rule.reload.approvals_required }.from(2).to(0)
end
end
context 'when high-severity vulnerabilities do not present in target branch pipeline' do
it "won't change approvals_required count" do
expect { subject }
.not_to change { report_approver_rule.reload.approvals_required }
end
end
end
context 'when only low-severity vulnerabilities are present' do
before do
create(:ee_ci_build, :success, :low_severity_dast_report, name: 'dast_job', pipeline: pipeline, project: project)
end
it 'lowers approvals_required count to zero' do
expect { subject }
.to change { report_approver_rule.reload.approvals_required }.from(2).to(0)
end
end
context 'when merge_requests are merged' do
let!(:merge_request) { create(:merge_request, :merged) }
before do
create(:ee_ci_build, :success, :dast, name: 'dast_job', pipeline: pipeline, project: project)
end
it "won't change approvals_required count" do
expect { subject }
.not_to change { report_approver_rule.reload.approvals_required }
end
end
context "license compliance policy" do
let!(:license_compliance_rule) { create(:report_approver_rule, :license_scanning, merge_request: merge_request, approvals_required: 1) }
before do
stub_feature_flags(drop_license_management_artifact: false)
end
context "when a license violates the license compliance policy" do
let!(:software_license_policy) { create(:software_license_policy, :denied, project: project, software_license: denied_license) }
let(:denied_license) { create(:software_license, name: license_name) }
let(:license_name) { ci_build.pipeline.license_scanning_report.license_names[0] }
context 'with a new report' do
let!(:ci_build) { create(:ee_ci_build, :success, :license_scanning, pipeline: pipeline, project: project) }
specify { expect { subject }.not_to change { license_compliance_rule.reload.approvals_required } }
specify { expect(subject[:status]).to be(:success) }
end
context 'with an old report' do
let!(:ci_build) { create(:ee_ci_build, :success, :license_management, pipeline: pipeline, project: project) }
specify { expect { subject }.not_to change { license_compliance_rule.reload.approvals_required } }
specify { expect(subject[:status]).to be(:success) }
end
end
context "when no licenses violate the license compliance policy" do
context 'with a new report' do
let!(:ci_build) { create(:ee_ci_build, :success, :license_scanning, pipeline: pipeline, project: project) }
specify { expect { subject }.to change { license_compliance_rule.reload.approvals_required }.from(1).to(0) }
specify { expect(subject[:status]).to be(:success) }
end
context 'with an old report' do
let!(:ci_build) { create(:ee_ci_build, :success, :license_management, pipeline: pipeline, project: project) }
before do
stub_feature_flags(drop_license_management_artifact: false)
end
specify { expect { subject }.to change { license_compliance_rule.reload.approvals_required }.from(1).to(0) }
specify { expect(subject[:status]).to be(:success) }
end
end
context "when an unexpected error occurs" do
before do
allow_next_instance_of(Gitlab::Ci::Reports::LicenseScanning::Report) do |instance|
allow(instance).to receive(:violates?).and_raise('heck')
end
end
specify { expect(subject[:status]).to be(:error) }
specify { expect(subject[:message]).to eql("Failed to update approval rules") }
end
end
end
context 'when pipeline fails' do
before do
pipeline.update!(status: :failed)
end
context 'when high-severity vulnerabilities are present' do
before do
create(:ee_ci_build, :success, :dependency_scanning, name: 'ds_job', pipeline: pipeline, project: project)
end
context 'when high-severity vulnerabilities already present in target branch pipeline' do
before do
create(:ee_ci_build, :success, :dependency_scanning, name: 'ds_job', pipeline: base_pipeline, project: project)
end
it 'lowers approvals_required count to zero' do
expect { subject }
.to change { report_approver_rule.reload.approvals_required }.from(2).to(0)
end
end
context 'when high-severity vulnerabilities do not present in target branch pipeline' do
it "won't change approvals_required count" do
expect { subject }
.not_to change { report_approver_rule.reload.approvals_required }
end
end
end
context 'when only low-severity vulnerabilities are present' do
before do
create(:ee_ci_build, :success, :low_severity_dast_report, name: 'dast_job', pipeline: pipeline, project: project)
end
it 'lowers approvals_required count to zero' do
expect { subject }
.to change { report_approver_rule.reload.approvals_required }.from(2).to(0)
end
end
end
end
context 'without reports' do
let(:pipeline) { create(:ci_pipeline, :running, project: project, merge_requests_as_head_pipeline: [merge_request]) }
it "won't change approvals_required count" do
expect { subject }
.not_to change { report_approver_rule.reload.approvals_required }
end
context "license compliance policy" do
let!(:software_license_policy) { create(:software_license_policy, :denied, project: project, software_license: denied_license) }
let!(:license_compliance_rule) { create(:report_approver_rule, :license_scanning, merge_request: merge_request, approvals_required: 1) }
let!(:denied_license) { create(:software_license) }
specify { expect { subject }.not_to change { license_compliance_rule.reload.approvals_required } }
specify { expect(subject[:status]).to be(:success) }
end
end
end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe SyncSecurityReportsToReportApprovalRulesWorker do RSpec.describe Ci::SyncReportsToReportApprovalRulesWorker do
describe '#perform' do describe '#perform' do
let(:pipeline) { double(:pipeline, id: 42) } let(:pipeline) { double(:pipeline, id: 42) }
let(:sync_service) { double(:service, execute: true) } let(:sync_service) { double(:service, execute: true) }
...@@ -13,7 +13,7 @@ RSpec.describe SyncSecurityReportsToReportApprovalRulesWorker do ...@@ -13,7 +13,7 @@ RSpec.describe SyncSecurityReportsToReportApprovalRulesWorker do
end end
it "executes SyncReportsToApprovalRulesService for given pipeline" do it "executes SyncReportsToApprovalRulesService for given pipeline" do
expect(Security::SyncReportsToApprovalRulesService).to receive(:new) expect(Ci::SyncReportsToApprovalRulesService).to receive(:new)
.with(pipeline).once.and_return(sync_service) .with(pipeline).once.and_return(sync_service)
described_class.new.perform(pipeline.id) described_class.new.perform(pipeline.id)
...@@ -22,7 +22,7 @@ RSpec.describe SyncSecurityReportsToReportApprovalRulesWorker do ...@@ -22,7 +22,7 @@ RSpec.describe SyncSecurityReportsToReportApprovalRulesWorker do
context 'when pipeline is missing' do context 'when pipeline is missing' do
it 'does not execute SyncReportsToApprovalRulesService' do it 'does not execute SyncReportsToApprovalRulesService' do
expect(Security::SyncReportsToApprovalRulesService).not_to receive(:new) expect(Ci::SyncReportsToApprovalRulesService).not_to receive(:new)
described_class.new.perform(pipeline.id) described_class.new.perform(pipeline.id)
end end
......
...@@ -238,6 +238,20 @@ FactoryBot.define do ...@@ -238,6 +238,20 @@ FactoryBot.define do
coverage_regex { '/(d+)/' } coverage_regex { '/(d+)/' }
end end
trait :trace_with_coverage do
coverage { nil }
coverage_regex { '(\d+\.\d+)%' }
transient do
trace_coverage { 60.0 }
end
after(:create) do |build, evaluator|
build.trace.set("Coverage #{evaluator.trace_coverage}%")
build.trace.archive! if build.complete?
end
end
trait :trace_live do trait :trace_live do
after(:create) do |build, evaluator| after(:create) do |build, evaluator|
build.trace.set('BUILD TRACE') build.trace.set('BUILD TRACE')
......
...@@ -5239,4 +5239,20 @@ RSpec.describe Ci::Build do ...@@ -5239,4 +5239,20 @@ RSpec.describe Ci::Build do
end end
end end
end end
describe '.without_coverage' do
let!(:build_with_coverage) { create(:ci_build, pipeline: pipeline, coverage: 100.0) }
it 'returns builds without coverage values' do
expect(described_class.without_coverage).to eq([build])
end
end
describe '.with_coverage_regex' do
let!(:build_with_coverage_regex) { create(:ci_build, pipeline: pipeline, coverage_regex: '\d') }
it 'returns builds with coverage regex values' do
expect(described_class.with_coverage_regex).to eq([build_with_coverage_regex])
end
end
end end
...@@ -744,6 +744,42 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do ...@@ -744,6 +744,42 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end end
end end
describe '#update_builds_coverage' do
let_it_be(:pipeline) { create(:ci_empty_pipeline) }
context 'builds with coverage_regex defined' do
let!(:build_1) { create(:ci_build, :success, :trace_with_coverage, trace_coverage: 60.0, pipeline: pipeline) }
let!(:build_2) { create(:ci_build, :success, :trace_with_coverage, trace_coverage: 80.0, pipeline: pipeline) }
it 'updates the coverage value of each build from the trace' do
pipeline.update_builds_coverage
expect(build_1.reload.coverage).to eq(60.0)
expect(build_2.reload.coverage).to eq(80.0)
end
end
context 'builds without coverage_regex defined' do
let!(:build) { create(:ci_build, :success, :trace_with_coverage, coverage_regex: nil, trace_coverage: 60.0, pipeline: pipeline) }
it 'does not update the coverage value of each build from the trace' do
pipeline.update_builds_coverage
expect(build.reload.coverage).to eq(nil)
end
end
context 'builds with coverage values already present' do
let!(:build) { create(:ci_build, :success, :trace_with_coverage, trace_coverage: 60.0, coverage: 10.0, pipeline: pipeline) }
it 'does not update the coverage value of each build from the trace' do
pipeline.update_builds_coverage
expect(build.reload.coverage).to eq(10.0)
end
end
end
describe '#retryable?' do describe '#retryable?' do
subject { pipeline.retryable? } subject { pipeline.retryable? }
......
...@@ -165,6 +165,7 @@ RSpec.describe 'Every Sidekiq worker' do ...@@ -165,6 +165,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Ci::ResourceGroups::AssignResourceFromResourceGroupWorker' => 3, 'Ci::ResourceGroups::AssignResourceFromResourceGroupWorker' => 3,
'Ci::TestFailureHistoryWorker' => 3, 'Ci::TestFailureHistoryWorker' => 3,
'Ci::TriggerDownstreamSubscriptionsWorker' => 3, 'Ci::TriggerDownstreamSubscriptionsWorker' => 3,
'Ci::SyncReportsToReportApprovalRulesWorker' => 3,
'CleanupContainerRepositoryWorker' => 3, 'CleanupContainerRepositoryWorker' => 3,
'ClusterConfigureIstioWorker' => 3, 'ClusterConfigureIstioWorker' => 3,
'ClusterInstallAppWorker' => 3, 'ClusterInstallAppWorker' => 3,
...@@ -428,7 +429,6 @@ RSpec.describe 'Every Sidekiq worker' do ...@@ -428,7 +429,6 @@ RSpec.describe 'Every Sidekiq worker' do
'StoreSecurityScansWorker' => 3, 'StoreSecurityScansWorker' => 3,
'SyncSeatLinkRequestWorker' => 20, 'SyncSeatLinkRequestWorker' => 20,
'SyncSeatLinkWorker' => 12, 'SyncSeatLinkWorker' => 12,
'SyncSecurityReportsToReportApprovalRulesWorker' => 3,
'SystemHookPushWorker' => 3, 'SystemHookPushWorker' => 3,
'TodosDestroyer::ConfidentialEpicWorker' => 3, 'TodosDestroyer::ConfidentialEpicWorker' => 3,
'TodosDestroyer::ConfidentialIssueWorker' => 3, 'TodosDestroyer::ConfidentialIssueWorker' => 3,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment