Commit ac4708ec authored by Gilbert Roulot's avatar Gilbert Roulot Committed by Kamil Trzciński

Add license management reports

This adds a backend parsing of license management reports.
The reports are parsed async with ReactiveCache,
compared and returned via API for Frontend.
parent 5a610466
...@@ -84,7 +84,7 @@ class Projects::ManagedLicensesController < Projects::ApplicationController ...@@ -84,7 +84,7 @@ class Projects::ManagedLicensesController < Projects::ApplicationController
def software_license_policy def software_license_policy
id = params[:id] id = params[:id]
id = CGI.unescape(id) unless id.is_a?(Integer) || id =~ /^\d+$/ id = CGI.unescape(id) unless id.is_a?(Integer) || id =~ /^\d+$/
@software_license_policy ||= SoftwareLicensePoliciesFinder.new(current_user, project).find_by_name_or_id(id) @software_license_policy ||= SoftwareLicensePoliciesFinder.new(current_user, project, name_or_id: id).find
if @software_license_policy.nil? if @software_license_policy.nil?
# The license was not found # The license was not found
......
...@@ -6,20 +6,43 @@ class SoftwareLicensePoliciesFinder ...@@ -6,20 +6,43 @@ class SoftwareLicensePoliciesFinder
attr_accessor :current_user, :project attr_accessor :current_user, :project
def initialize(current_user, project) def initialize(current_user, project, params = {})
@current_user = current_user @current_user = current_user
@project = project @project = project
@params = params
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def find_by_name_or_id(id) def execute
return nil unless can?(current_user, :read_software_license_policy, project) return SoftwareLicensePolicy.none unless can?(current_user, :read_software_license_policy, project)
software_licenses = SoftwareLicense.arel_table items = init_collection
software_license_policies = SoftwareLicensePolicy.arel_table
project.software_license_policies.joins(:software_license).where( if license_id
software_licenses[:name].eq(id).or(software_license_policies[:id].eq(id)) items.where(id: license_id)
).take elsif license_name
items.with_license_by_name(license_name)
end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def find
execute.take
end
# rubocop: enable CodeReuse/ActiveRecord
private
def init_collection
SoftwareLicensePolicy.with_license.including_license.for_project(@project)
end
def license_id
@params[:name_or_id].to_i if @params[:name_or_id] =~ /\A\d+\Z/
end
def license_name
@params[:name] || @params[:name_or_id]
end
end end
...@@ -21,6 +21,11 @@ module EE ...@@ -21,6 +21,11 @@ module EE
with_existing_job_artifacts(::Ci::JobArtifact.security_reports) with_existing_job_artifacts(::Ci::JobArtifact.security_reports)
.eager_load_job_artifacts .eager_load_job_artifacts
end end
scope :with_license_management_reports, -> do
with_existing_job_artifacts(::Ci::JobArtifact.license_management_reports)
.eager_load_job_artifacts
end
end end
def shared_runners_minutes_limit_enabled? def shared_runners_minutes_limit_enabled?
...@@ -54,7 +59,7 @@ module EE ...@@ -54,7 +59,7 @@ module EE
begin begin
next unless project.feature_available?(LICENSED_PARSER_FEATURES.fetch(file_type)) next unless project.feature_available?(LICENSED_PARSER_FEATURES.fetch(file_type))
::Gitlab::Ci::Parsers::Security.fabricate!(file_type).parse!(blob, security_report) ::Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, security_report)
rescue => e rescue => e
security_report.error = e security_report.error = e
end end
...@@ -62,6 +67,14 @@ module EE ...@@ -62,6 +67,14 @@ module EE
end end
end end
def collect_license_management_reports!(license_management_report)
each_report(::Ci::JobArtifact::LICENSE_MANAGEMENT_REPORT_FILE_TYPES) do |file_type, blob|
::Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, license_management_report)
end
license_management_report
end
private private
def name_in?(names) def name_in?(names)
......
...@@ -12,6 +12,7 @@ module EE ...@@ -12,6 +12,7 @@ module EE
after_destroy :log_geo_deleted_event after_destroy :log_geo_deleted_event
SECURITY_REPORT_FILE_TYPES = %w[sast dependency_scanning container_scanning dast].freeze SECURITY_REPORT_FILE_TYPES = %w[sast dependency_scanning container_scanning dast].freeze
LICENSE_MANAGEMENT_REPORT_FILE_TYPES = %w[license_management].freeze
scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) } scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) }
scope :geo_syncable, -> { with_files_stored_locally.not_expired } scope :geo_syncable, -> { with_files_stored_locally.not_expired }
...@@ -19,6 +20,10 @@ module EE ...@@ -19,6 +20,10 @@ module EE
scope :security_reports, -> do scope :security_reports, -> do
with_file_types(SECURITY_REPORT_FILE_TYPES) with_file_types(SECURITY_REPORT_FILE_TYPES)
end end
scope :license_management_reports, -> do
with_file_types(LICENSE_MANAGEMENT_REPORT_FILE_TYPES)
end
end end
def log_geo_deleted_event def log_geo_deleted_event
......
...@@ -133,6 +133,18 @@ module EE ...@@ -133,6 +133,18 @@ module EE
end end
end end
def has_license_management_reports?
complete? && builds.latest.with_license_management_reports.any?
end
def license_management_report
::Gitlab::Ci::Reports::LicenseManagement::Report.new.tap do |license_management_report|
builds.latest.with_license_management_reports.each do |build|
build.collect_license_management_reports!(license_management_report)
end
end
end
private private
def available_licensed_report_type?(file_type) def available_licensed_report_type?(file_type)
......
...@@ -68,5 +68,17 @@ module EE ...@@ -68,5 +68,17 @@ module EE
::Gitlab::CodeOwners.for_merge_request(self).freeze ::Gitlab::CodeOwners.for_merge_request(self).freeze
end end
end end
def has_license_management_reports?
actual_head_pipeline&.has_license_management_reports?
end
def compare_license_management_reports
unless has_license_management_reports?
return { status: :error, status_reason: 'This merge request does not have license management reports' }
end
compare_reports(::Ci::CompareLicenseManagementReportsService)
end
end end
end end
...@@ -30,6 +30,13 @@ class SoftwareLicensePolicy < ActiveRecord::Base ...@@ -30,6 +30,13 @@ class SoftwareLicensePolicy < ActiveRecord::Base
validates :software_license, uniqueness: { scope: :project_id } validates :software_license, uniqueness: { scope: :project_id }
scope :ordered, -> { SoftwareLicensePolicy.includes(:software_license).order("software_licenses.name ASC") } scope :ordered, -> { SoftwareLicensePolicy.includes(:software_license).order("software_licenses.name ASC") }
scope :for_project, -> (project) { where(project: project) }
scope :with_license, -> { joins(:software_license) }
scope :including_license, -> { includes(:software_license) }
scope :with_license_by_name, -> (license_name) do
joins(:software_license).where(software_licenses: { name: license_name })
end
def name def name
software_license.name software_license.name
......
# frozen_string_literal: true
class LicenseManagementReportDependencyEntity < Grape::Entity
expose :name
end
# frozen_string_literal: true
class LicenseManagementReportLicenseEntity < Grape::Entity
expose :name
expose :dependencies, using: LicenseManagementReportDependencyEntity
end
# frozen_string_literal: true
class LicenseManagementReportsComparerEntity < Grape::Entity
expose :new_licenses, using: LicenseManagementReportLicenseEntity
expose :existing_licenses, using: LicenseManagementReportLicenseEntity
expose :removed_licenses, using: LicenseManagementReportLicenseEntity
end
# frozen_string_literal: true
class LicenseManagementReportsComparerSerializer < BaseSerializer
entity LicenseManagementReportsComparerEntity
end
# frozen_string_literal: true
module Ci
class CompareLicenseManagementReportsService < ::Ci::CompareReportsBaseService
def comparer_class
Gitlab::Ci::Reports::LicenseManagement::ReportsComparer
end
def serializer_class
LicenseManagementReportsComparerSerializer
end
def get_report(pipeline)
pipeline&.license_management_report
end
end
end
...@@ -10,7 +10,8 @@ module API ...@@ -10,7 +10,8 @@ module API
# Make the software license policy specified by id in the request available # Make the software license policy specified by id in the request available
def software_license_policy def software_license_policy
id = params[:managed_license_id] id = params[:managed_license_id]
@software_license_policy ||= SoftwareLicensePoliciesFinder.new(current_user, user_project).find_by_name_or_id(id) @software_license_policy ||=
SoftwareLicensePoliciesFinder.new(current_user, user_project, name_or_id: id).find
end end
def authorize_can_read! def authorize_can_read!
......
# frozen_string_literal: true
module EE
module Gitlab
module Ci
module Parsers
extend ActiveSupport::Concern
class_methods do
def parsers
super.merge({
license_management: ::Gitlab::Ci::Parsers::LicenseManagement::LicenseManagement,
dependency_scanning: ::Gitlab::Ci::Parsers::Security::DependencyScanning,
sast: ::Gitlab::Ci::Parsers::Security::Sast
})
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Parsers
module LicenseManagement
class LicenseManagement
LicenseManagementParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
def parse!(json_data, license_management_report)
root = JSON.parse(json_data)
root['licenses'].each do |license_hash|
license_expression = license_hash['name']
# Extract licenses from the license_expression as it can contain comas.
each_license(license_expression) do |license_name|
license_dependencies = root['dependencies'].select do |dependency|
uses_license?(dependency['license']['name'], license_name)
end
license_dependencies.each do |dependency|
license_management_report.add_dependency(license_name, dependency['dependency']['name'])
end
end
end
rescue JSON::ParserError
raise LicenseManagementParserError, 'JSON parsing failed'
rescue => e
Gitlab::Sentry.track_exception(e)
raise LicenseManagementParserError, 'License management report parsing failed'
end
def remove_suffix(name)
name.gsub(/-or-later$|-only$|\+$/, '')
end
def expression_to_list(expression)
expression.split(',').map(&:strip).map { |name| remove_suffix(name) }
end
# Split the license expression when it is separated by spaces. Removes suffixes
# specified in https://spdx.org/ids-how
def each_license(expression)
expression_to_list(expression).each do |license_name|
yield(license_name)
end
end
# Check that the license expression uses the given license name
def uses_license?(expression, name)
expression_to_list(expression).any? { |name1| name1.casecmp(remove_suffix(name)) == 0 }
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Parsers
module Security
ParserNotFoundError = Class.new(StandardError)
PARSERS = {
sast: ::Gitlab::Ci::Parsers::Security::Sast,
dependency_scanning: ::Gitlab::Ci::Parsers::Security::DependencyScanning
}.freeze
def self.fabricate!(file_type)
PARSERS.fetch(file_type.to_sym).new
rescue KeyError
raise ParserNotFoundError, "Cannot find any parser matching file type '#{file_type}'"
end
end
end
end
end
...@@ -5,7 +5,7 @@ module Gitlab ...@@ -5,7 +5,7 @@ module Gitlab
module Parsers module Parsers
module Security module Security
class Common class Common
SecurityReportParserError = Class.new(StandardError) SecurityReportParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
def parse!(json_data, report) def parse!(json_data, report)
vulnerabilities = JSON.parse!(json_data) vulnerabilities = JSON.parse!(json_data)
...@@ -15,7 +15,8 @@ module Gitlab ...@@ -15,7 +15,8 @@ module Gitlab
end end
rescue JSON::ParserError rescue JSON::ParserError
raise SecurityReportParserError, 'JSON parsing failed' raise SecurityReportParserError, 'JSON parsing failed'
rescue rescue => e
Gitlab::Sentry.track_exception(e)
raise SecurityReportParserError, "#{report.type} security report parsing failed" raise SecurityReportParserError, "#{report.type} security report parsing failed"
end end
......
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module LicenseManagement
class Dependency
attr_reader :name
def initialize(name)
@name = name
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module LicenseManagement
class License
attr_reader :name, :status
def initialize(name)
@name = name
@dependencies = Set.new
end
def add_dependency(name)
@dependencies.add(::Gitlab::Ci::Reports::LicenseManagement::Dependency.new(name))
end
def dependencies
@dependencies.to_a
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module LicenseManagement
class Report
def initialize
@found_licenses = {}
end
def licenses
@found_licenses.values
end
def license_names
@found_licenses.values.map(&:name)
end
def add_dependency(license_name, dependency_name)
key = license_name.upcase
@found_licenses[key] ||= ::Gitlab::Ci::Reports::LicenseManagement::License.new(license_name)
@found_licenses[key].add_dependency(dependency_name)
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module LicenseManagement
class ReportsComparer
include Gitlab::Utils::StrongMemoize
attr_reader :base_report, :head_report
def initialize(base_report, head_report)
@base_report = base_report || ::Gitlab::Ci::Reports::LicenseManagement::Report.new
@head_report = head_report
end
def new_licenses
strong_memoize(:new_licenses) do
names = @head_report.license_names - @base_report.license_names
@head_report.licenses.select { |license| names.include?(license.name) }
end
end
def existing_licenses
strong_memoize(:existing_licenses) do
names = @base_report.license_names & @head_report.license_names
@head_report.licenses.select { |license| names.include?(license.name) }
end
end
def removed_licenses
strong_memoize(:removed_licenses) do
names = @base_report.license_names - @head_report.license_names
@base_report.licenses.select { |license| names.include?(license.name) }
end
end
end
end
end
end
end
...@@ -28,4 +28,22 @@ FactoryBot.define do ...@@ -28,4 +28,22 @@ FactoryBot.define do
end end
end end
end end
trait :license_management_report do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :license_management_report, job: build)
end
end
trait :license_management_report_2 do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :license_management_report_2, job: build)
end
end
trait :corrupted_license_management_report do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :corrupted_license_management_report, job: build)
end
end
end end
...@@ -22,6 +22,36 @@ FactoryBot.define do ...@@ -22,6 +22,36 @@ FactoryBot.define do
end end
end end
trait :license_management_report do
file_type :license_management
file_format :raw
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('ee/spec/fixtures/license_management/report.json'), 'application/json')
end
end
trait :license_management_report_2 do
file_type :license_management
file_format :raw
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('ee/spec/fixtures/license_management/report2.json'), 'application/json')
end
end
trait :corrupted_license_management_report do
file_type :license_management
file_format :raw
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('ee/spec/fixtures/license_management/report_with_corrupted_data.json'), 'application/json')
end
end
trait :performance do trait :performance do
file_format :raw file_format :raw
file_type :performance file_type :performance
......
...@@ -6,5 +6,29 @@ FactoryBot.define do ...@@ -6,5 +6,29 @@ FactoryBot.define do
source :webide source :webide
config_source :webide_source config_source :webide_source
end end
trait :with_license_management_report do
status :success
after(:build) do |pipeline, evaluator|
pipeline.builds << build(:ee_ci_build, :license_management_report, pipeline: pipeline, project: pipeline.project)
end
end
trait :with_license_management_report_2 do
status :success
after(:build) do |pipeline, evaluator|
pipeline.builds << build(:ee_ci_build, :license_management_report_2, pipeline: pipeline, project: pipeline.project)
end
end
trait :with_corrupted_license_management_report do
status :success
after(:build) do |pipeline, evaluator|
pipeline.builds << build(:ee_ci_build, :corrupted_license_management_report, pipeline: pipeline, project: pipeline.project)
end
end
end end
end end
# frozen_string_literal: true
FactoryBot.define do
factory :ci_reports_license_management_report, class: ::Gitlab::Ci::Reports::LicenseManagement::Report do
trait :report_1 do
after(:build) do |report, evaluator|
report.add_dependency('MIT', 'Library1')
report.add_dependency('WTFPL', 'Library2')
end
end
trait :report_2 do
after(:build) do |report, evaluator|
report.add_dependency('MIT', 'Library1')
report.add_dependency('Apache 2.0', 'Library3')
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :ee_merge_request, parent: :merge_request do
trait :with_license_management_reports do
after(:build) do |merge_request|
merge_request.head_pipeline = build(
:ee_ci_pipeline,
:success,
:with_license_management_report,
project: merge_request.source_project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha)
end
end
end
end
...@@ -12,13 +12,35 @@ describe SoftwareLicensePoliciesFinder do ...@@ -12,13 +12,35 @@ describe SoftwareLicensePoliciesFinder do
end end
end end
let(:finder) { described_class.new(user, project) } let(:finder) { described_class.new(user, project, params) }
before do before do
stub_licensed_features(license_management: true) stub_licensed_features(license_management: true)
end end
it 'finds the software license policy' do context 'searched by name' do
expect(finder.find_by_name_or_id(software_license_policy.name)).to eq(software_license_policy) let(:params) { { name: software_license_policy.name } }
it 'by name finds the software license policy by name' do
expect(finder.execute.take).to eq(software_license_policy)
end
end
context 'searched by name_or_id' do
context 'with a name' do
let(:params) { { name_or_id: software_license_policy.name } }
it 'by name_or_id finds the software license policy by name' do
expect(finder.execute.take).to eq(software_license_policy)
end
end
context 'with an id' do
let(:params) { { name_or_id: software_license_policy.id.to_s } }
it 'by name or id finds the software license policy by id' do
expect(finder.execute.take).to eq(software_license_policy)
end
end
end end
end end
This diff is collapsed.
{
"licenses": [
{
"count": 1,
"name": "WTFPL"
},
{
"count": 1,
"name": "MIT"
}
],
"dependencies": [
{
"license": {
"name": "MIT",
"url": "http://opensource.org/licenses/mit-license"
},
"dependency": {
"name": "actioncable",
"url": "http://rubyonrails.org",
"description": "WebSocket framework for Rails.",
"pathes": [
"."
]
}
},
{
"license": {
"name": "WTFPL",
"url": "http://www.wtfpl.net/"
},
"dependency": {
"name": "wtfpl_init",
"url": "https://rubygems.org/gems/wtfpl_init",
"description": "Download WTFPL license file and rename to LICENSE.md or something",
"pathes": [
"."
]
}
}
]
}
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Parsers::LicenseManagement::LicenseManagement do
describe '#parse!' do
subject { described_class.new.parse!(data, report) }
let(:report) { Gitlab::Ci::Reports::LicenseManagement::Report.new }
context 'when data is a JSON license management report' do
let(:data) { File.read(Rails.root.join('ee/spec/fixtures/license_management/report.json')) }
it 'parses without error' do
expect { subject }.not_to raise_error
expect(report.licenses.count).to eq(4)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Reports::LicenseManagement::ReportsComparer do
let(:report_1) { build :ci_reports_license_management_report, :report_1 }
let(:report_2) { build :ci_reports_license_management_report, :report_2 }
let(:report_comparer) { described_class.new(report_1, report_2) }
describe '#new_licenses' do
subject { report_comparer.new_licenses }
it 'reports new licenses' do
expect(subject.count).to eq 1
expect(subject[0].name).to eq 'Apache 2.0'
end
end
describe '#existing_licenses' do
subject { report_comparer.existing_licenses }
it 'reports existing licenses' do
expect(subject.count).to eq 1
expect(subject[0].name).to eq 'MIT'
end
end
describe '#removed_licenses' do
subject { report_comparer.removed_licenses }
it 'reports removed licenses' do
expect(subject.count).to eq 1
expect(subject[0].name).to eq 'WTFPL'
end
end
end
...@@ -225,4 +225,38 @@ describe Ci::Build do ...@@ -225,4 +225,38 @@ describe Ci::Build do
end end
end end
end end
describe '#collect_license_management_reports!' do
subject { job.collect_license_management_reports!(license_management_report) }
let(:license_management_report) { Gitlab::Ci::Reports::LicenseManagement::Report.new }
it { expect(license_management_report.licenses.count).to eq(0) }
context 'when build has a license management report' do
context 'when there is a license management report' do
before do
create(:ee_ci_job_artifact, :license_management_report, job: job, project: job.project)
end
it 'parses blobs and add the results to the report' do
expect { subject }.not_to raise_error
expect(license_management_report.licenses.count).to eq(4)
expect(license_management_report.licenses[0].name).to eq('MIT')
expect(license_management_report.licenses[0].dependencies.count).to eq(52)
end
end
context 'when there is a corrupted license management report' do
before do
create(:ee_ci_job_artifact, :corrupted_license_management_report, job: job, project: job.project)
end
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Ci::Parsers::LicenseManagement::LicenseManagement::LicenseManagementParserError)
end
end
end
end
end end
...@@ -316,4 +316,81 @@ describe Ci::Pipeline do ...@@ -316,4 +316,81 @@ describe Ci::Pipeline do
end end
end end
end end
describe '#has_license_management_reports?' do
subject { pipeline.has_license_management_reports? }
context 'when pipeline has builds with license_management reports' do
before do
create(:ee_ci_build, :license_management_report, pipeline: pipeline, project: project)
end
context 'when pipeline status is running' do
let(:pipeline) { create(:ci_pipeline, :running, project: project) }
it { is_expected.to be_falsey }
end
context 'when pipeline status is success' do
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
it { is_expected.to be_truthy }
end
end
context 'when pipeline does not have builds with license_management reports' do
before do
create(:ci_build, :artifacts, pipeline: pipeline, project: project)
end
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
it { is_expected.to be_falsey }
end
context 'when retried build has license management reports' do
before do
create(:ee_ci_build, :retried, :license_management_report, pipeline: pipeline, project: project)
end
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
it { is_expected.to be_falsey }
end
end
describe '#license_management_reports' do
subject { pipeline.license_management_report }
context 'when pipeline has multiple builds with license management reports' do
let!(:build_1) { create(:ci_build, :success, name: 'license_management', pipeline: pipeline, project: project) }
let!(:build_2) { create(:ci_build, :success, name: 'license_management2', pipeline: pipeline, project: project) }
before do
create(:ee_ci_job_artifact, :license_management_report, job: build_1, project: project)
create(:ee_ci_job_artifact, :license_management_report_2, job: build_2, project: project)
end
it 'returns a license management report with collected data' do
expect(subject.licenses.count).to be(5)
expect(subject.licenses.any? { |license| license.name == 'WTFPL' } ).to be_truthy
expect(subject.licenses.any? { |license| license.name == 'MIT' } ).to be_truthy
end
context 'when builds are retried' do
let!(:build_1) { create(:ci_build, :retried, :success, name: 'license_management', pipeline: pipeline, project: project) }
let!(:build_2) { create(:ci_build, :retried, :success, name: 'license_management2', pipeline: pipeline, project: project) }
it 'does not take retried builds into account' do
expect(subject.licenses.count).to be(0)
end
end
end
context 'when pipeline does not have any builds with license management reports' do
it 'returns an empty license management report' do
expect(subject.licenses.count).to be(0)
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe EE::Ci::JobArtifact do
describe '.license_management_reports' do
subject { Ci::JobArtifact.license_management_reports }
context 'when there is a license management report' do
let!(:artifact) { create(:ee_ci_job_artifact, :license_management_report) }
it { is_expected.to eq([artifact]) }
end
end
end
...@@ -2,6 +2,7 @@ require 'spec_helper' ...@@ -2,6 +2,7 @@ require 'spec_helper'
describe MergeRequest do describe MergeRequest do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
include ReactiveCachingHelpers
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
...@@ -74,4 +75,98 @@ describe MergeRequest do ...@@ -74,4 +75,98 @@ describe MergeRequest do
it { expect(subject.base_pipeline).to eq(pipeline) } it { expect(subject.base_pipeline).to eq(pipeline) }
end end
describe '#has_license_management_reports?' do
subject { merge_request.has_license_management_reports? }
let(:project) { create(:project, :repository) }
before do
stub_licensed_features(license_management: true)
end
context 'when head pipeline has license management reports' do
let(:merge_request) { create(:ee_merge_request, :with_license_management_reports, source_project: project) }
it { is_expected.to be_truthy }
end
context 'when head pipeline does not have license management reports' do
let(:merge_request) { create(:ee_merge_request, source_project: project) }
it { is_expected.to be_falsey }
end
end
describe '#compare_license_management_reports' do
subject { merge_request.compare_license_management_reports }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:base_pipeline) do
create(:ee_ci_pipeline,
:with_license_management_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 license management reports' do
let!(:head_pipeline) do
create(:ee_ci_pipeline,
:with_license_management_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::CompareLicenseManagementReportsService)
.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::CompareLicenseManagementReportsService)
.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 license management 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 management reports')
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe LicenseManagementReportDependencyEntity do
include LicenseManagementReportHelper
let(:dependency) { create_dependency }
let(:entity) { described_class.new(dependency) }
describe '#as_json' do
subject { entity.as_json }
it 'contains the correct dependency name' do
expect(subject[:name]).to eq('Dependency1')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe LicenseManagementReportLicenseEntity do
include LicenseManagementReportHelper
let(:license) { create_license }
let(:entity) { described_class.new(license) }
describe '#as_json' do
subject { entity.as_json }
it 'contains the correct dependencies' do
expect(subject[:dependencies].count).to eq(2)
expect(subject[:dependencies][0][:name]).to eq('Dependency1')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe LicenseManagementReportsComparerEntity do
include LicenseManagementReportHelper
let(:comparer) { create_comparer }
let(:entity) { described_class.new(comparer) }
describe '#as_json' do
subject { entity.as_json }
it 'contains the new existing and removed license lists' do
expect(subject).to have_key(:new_licenses)
expect(subject).to have_key(:existing_licenses)
expect(subject).to have_key(:removed_licenses)
expect(subject[:new_licenses][0][:name]).to eq('License4')
expect(subject[:existing_licenses].count).to be(2)
expect(subject[:removed_licenses][0][:name]).to eq('License1')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe TestReportsComparerEntity do
include TestReportsHelper
let(:entity) { described_class.new(comparer) }
let(:comparer) { Gitlab::Ci::Reports::TestReportsComparer.new(base_reports, head_reports) }
let(:base_reports) { Gitlab::Ci::Reports::TestReports.new }
let(:head_reports) { Gitlab::Ci::Reports::TestReports.new }
describe '#as_json' do
subject { entity.as_json }
context 'when head and base reports include two test suites' do
context 'when the status of head report is success' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_success)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
it 'contains correct compared test reports details' do
expect(subject[:status]).to eq('success')
expect(subject[:summary]).to include(total: 2, resolved: 0, failed: 0)
expect(subject[:suites].first[:name]).to eq('rspec')
expect(subject[:suites].first[:status]).to eq('success')
expect(subject[:suites].second[:name]).to eq('junit')
expect(subject[:suites].second[:status]).to eq('success')
end
end
context 'when the status of head report is failed' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_success)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
end
it 'contains correct compared test reports details' do
expect(subject[:status]).to eq('failed')
expect(subject[:summary]).to include(total: 2, resolved: 0, failed: 1)
expect(subject[:suites].first[:name]).to eq('rspec')
expect(subject[:suites].first[:status]).to eq('success')
expect(subject[:suites].second[:name]).to eq('junit')
expect(subject[:suites].second[:status]).to eq('failed')
end
end
context 'when the status of head report is resolved' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved)
end
let(:create_test_case_java_resolved) do
create_test_case_java_failed.tap do |test_case|
test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
end
it 'contains correct compared test reports details' do
expect(subject[:status]).to eq('success')
expect(subject[:summary]).to include(total: 2, resolved: 1, failed: 0)
expect(subject[:suites].first[:name]).to eq('rspec')
expect(subject[:suites].first[:status]).to eq('success')
expect(subject[:suites].second[:name]).to eq('junit')
expect(subject[:suites].second[:status]).to eq('success')
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe TestReportsComparerSerializer do
include TestReportsHelper
let(:project) { double(:project) }
let(:serializer) { described_class.new(project: project).represent(comparer) }
let(:comparer) { Gitlab::Ci::Reports::TestReportsComparer.new(base_reports, head_reports) }
let(:base_reports) { Gitlab::Ci::Reports::TestReports.new }
let(:head_reports) { Gitlab::Ci::Reports::TestReports.new }
describe '#to_json' do
subject { serializer.to_json }
context 'when head and base reports include two test suites' do
context 'when the status of head report is success' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_success)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
it 'matches the schema' do
expect(subject).to match_schema('entities/test_reports_comparer')
end
end
context 'when the status of head report is failed' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_success)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
end
it 'matches the schema' do
expect(subject).to match_schema('entities/test_reports_comparer')
end
end
context 'when the status of head report is resolved' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved)
end
let(:create_test_case_java_resolved) do
create_test_case_java_failed.tap do |test_case|
test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
end
it 'matches the schema' do
expect(subject).to match_schema('entities/test_reports_comparer')
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe TestSuiteComparerEntity do
include TestReportsHelper
let(:entity) { described_class.new(comparer) }
let(:comparer) { Gitlab::Ci::Reports::TestSuiteComparer.new(name, base_suite, head_suite) }
let(:name) { 'rpsec' }
let(:base_suite) { Gitlab::Ci::Reports::TestSuite.new(name) }
let(:head_suite) { Gitlab::Ci::Reports::TestSuite.new(name) }
let(:test_case_success) { create_test_case_rspec_success }
let(:test_case_failed) { create_test_case_rspec_failed }
let(:test_case_resolved) do
create_test_case_rspec_failed.tap do |test_case|
test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
end
describe '#as_json' do
subject { entity.as_json }
context 'when head sutie has a newly failed test case which does not exist in base' do
before do
base_suite.add_test_case(test_case_success)
head_suite.add_test_case(test_case_failed)
end
it 'contains correct compared test suite details' do
expect(subject[:name]).to eq(name)
expect(subject[:status]).to eq('failed')
expect(subject[:summary]).to include(total: 1, resolved: 0, failed: 1)
subject[:new_failures].first.tap do |new_failure|
expect(new_failure[:status]).to eq(test_case_failed.status)
expect(new_failure[:name]).to eq(test_case_failed.name)
expect(new_failure[:execution_time]).to eq(test_case_failed.execution_time)
expect(new_failure[:system_output]).to eq(test_case_failed.system_output)
end
expect(subject[:resolved_failures]).to be_empty
expect(subject[:existing_failures]).to be_empty
end
end
context 'when head sutie still has a failed test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_failed)
end
it 'contains correct compared test suite details' do
expect(subject[:name]).to eq(name)
expect(subject[:status]).to eq('failed')
expect(subject[:summary]).to include(total: 1, resolved: 0, failed: 1)
expect(subject[:new_failures]).to be_empty
expect(subject[:resolved_failures]).to be_empty
subject[:existing_failures].first.tap do |existing_failure|
expect(existing_failure[:status]).to eq(test_case_failed.status)
expect(existing_failure[:name]).to eq(test_case_failed.name)
expect(existing_failure[:execution_time]).to eq(test_case_failed.execution_time)
expect(existing_failure[:system_output]).to eq(test_case_failed.system_output)
end
end
end
context 'when head sutie has a success test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_resolved)
end
it 'contains correct compared test suite details' do
expect(subject[:name]).to eq(name)
expect(subject[:status]).to eq('success')
expect(subject[:summary]).to include(total: 1, resolved: 1, failed: 0)
expect(subject[:new_failures]).to be_empty
subject[:resolved_failures].first.tap do |resolved_failure|
expect(resolved_failure[:status]).to eq(test_case_resolved.status)
expect(resolved_failure[:name]).to eq(test_case_resolved.name)
expect(resolved_failure[:execution_time]).to eq(test_case_resolved.execution_time)
expect(resolved_failure[:system_output]).to eq(test_case_resolved.system_output)
end
expect(subject[:existing_failures]).to be_empty
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::CompareLicenseManagementReportsService do
let(:service) { described_class.new(project) }
let(:project) { create(:project, :repository) }
describe '#execute' do
subject { service.execute(base_pipeline, head_pipeline) }
context 'when head pipeline has license management reports' do
let!(:base_pipeline) { nil }
let!(:head_pipeline) { create(:ee_ci_pipeline, :with_license_management_report, project: project) }
it 'reports new licenses' do
expect(subject[:status]).to eq(:parsed)
expect(subject[:data]['new_licenses'].count).to be(4)
expect(subject[:data]['new_licenses'].any? { |license| license['name'] == 'MIT' } ).to be_truthy
end
end
context 'when base and head pipelines have test reports' do
let!(:base_pipeline) { create(:ee_ci_pipeline, :with_license_management_report, project: project) }
let!(:head_pipeline) { create(:ee_ci_pipeline, :with_license_management_report_2, project: project) }
it 'reports status as parsed' do
expect(subject[:status]).to eq(:parsed)
end
it 'reports new licenses' do
expect(subject[:data]['new_licenses'].count).to be(1)
expect(subject[:data]['new_licenses'][0]['name']).to eq('WTFPL')
end
it 'reports existing licenses' do
expect(subject[:data]['existing_licenses'].count).to be(1)
expect(subject[:data]['existing_licenses'][0]['name']).to eq('MIT')
end
it 'reports removed licenses' do
expect(subject[:data]['removed_licenses'].count).to be(3)
expect(subject[:data]['removed_licenses'].any? { |license| license['name'] == 'New BSD' } ).to be_truthy
end
end
context 'when head pipeline has corrupted license management reports' do
let!(:base_pipeline) { nil }
let!(:head_pipeline) { create(:ee_ci_pipeline, :with_corrupted_license_management_report, project: project) }
it 'returns status and error message' do
expect(subject[:status]).to eq(:error)
expect(subject[:status_reason]).to include('JSON parsing failed')
end
end
end
end
# frozen_string_literal: true
module LicenseManagementReportHelper
def create_report(dependencies)
Gitlab::Ci::Reports::LicenseManagement::Report.new.tap do |report|
dependencies.each do |license_name, dependencies|
dependencies.each do |dependency_name|
report.add_dependency(license_name.to_s, dependency_name)
end
end
end
end
def create_report1
create_report(
License1: %w(Dependency1 Dependency2),
License2: %w(Dependency1),
License3: %w(Dependency3)
)
end
def create_report2
create_report(
License2: %w(Dependency1),
License3: %w(Dependency3),
License4: %w(Dependency4 Dependency1)
)
end
def create_comparer
Gitlab::Ci::Reports::LicenseManagement::ReportsComparer.new(create_report1, create_report2)
end
def create_license
Gitlab::Ci::Reports::LicenseManagement::License.new('License1').tap do |license|
license.add_dependency('Dependency1')
license.add_dependency('Dependency2')
end
end
def create_dependency
Gitlab::Ci::Reports::LicenseManagement::Dependency.new('Dependency1')
end
end
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Gitlab module Gitlab
module Ci module Ci
module Parsers module Parsers
prepend ::EE::Gitlab::Ci::Parsers
ParserNotFoundError = Class.new(ParserError) ParserNotFoundError = Class.new(ParserError)
def self.parsers def self.parsers
......
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