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
def software_license_policy
id = params[:id]
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?
# The license was not found
......
......@@ -6,20 +6,43 @@ class SoftwareLicensePoliciesFinder
attr_accessor :current_user, :project
def initialize(current_user, project)
def initialize(current_user, project, params = {})
@current_user = current_user
@project = project
@params = params
end
# rubocop: disable CodeReuse/ActiveRecord
def find_by_name_or_id(id)
return nil unless can?(current_user, :read_software_license_policy, project)
software_licenses = SoftwareLicense.arel_table
software_license_policies = SoftwareLicensePolicy.arel_table
project.software_license_policies.joins(:software_license).where(
software_licenses[:name].eq(id).or(software_license_policies[:id].eq(id))
).take
def execute
return SoftwareLicensePolicy.none unless can?(current_user, :read_software_license_policy, project)
items = init_collection
if license_id
items.where(id: license_id)
elsif license_name
items.with_license_by_name(license_name)
end
end
# 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
......@@ -21,6 +21,11 @@ module EE
with_existing_job_artifacts(::Ci::JobArtifact.security_reports)
.eager_load_job_artifacts
end
scope :with_license_management_reports, -> do
with_existing_job_artifacts(::Ci::JobArtifact.license_management_reports)
.eager_load_job_artifacts
end
end
def shared_runners_minutes_limit_enabled?
......@@ -54,7 +59,7 @@ module EE
begin
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
security_report.error = e
end
......@@ -62,6 +67,14 @@ module EE
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
def name_in?(names)
......
......@@ -12,6 +12,7 @@ module EE
after_destroy :log_geo_deleted_event
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 :geo_syncable, -> { with_files_stored_locally.not_expired }
......@@ -19,6 +20,10 @@ module EE
scope :security_reports, -> do
with_file_types(SECURITY_REPORT_FILE_TYPES)
end
scope :license_management_reports, -> do
with_file_types(LICENSE_MANAGEMENT_REPORT_FILE_TYPES)
end
end
def log_geo_deleted_event
......
......@@ -133,6 +133,18 @@ module EE
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
def available_licensed_report_type?(file_type)
......
......@@ -68,5 +68,17 @@ module EE
::Gitlab::CodeOwners.for_merge_request(self).freeze
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
......@@ -30,6 +30,13 @@ class SoftwareLicensePolicy < ActiveRecord::Base
validates :software_license, uniqueness: { scope: :project_id }
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
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
# Make the software license policy specified by id in the request available
def software_license_policy
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
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
module Parsers
module Security
class Common
SecurityReportParserError = Class.new(StandardError)
SecurityReportParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
def parse!(json_data, report)
vulnerabilities = JSON.parse!(json_data)
......@@ -15,7 +15,8 @@ module Gitlab
end
rescue JSON::ParserError
raise SecurityReportParserError, 'JSON parsing failed'
rescue
rescue => e
Gitlab::Sentry.track_exception(e)
raise SecurityReportParserError, "#{report.type} security report parsing failed"
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
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
......@@ -22,6 +22,36 @@ FactoryBot.define do
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
file_format :raw
file_type :performance
......
......@@ -6,5 +6,29 @@ FactoryBot.define do
source :webide
config_source :webide_source
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
# 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
end
end
let(:finder) { described_class.new(user, project) }
let(:finder) { described_class.new(user, project, params) }
before do
stub_licensed_features(license_management: true)
end
it 'finds the software license policy' do
expect(finder.find_by_name_or_id(software_license_policy.name)).to eq(software_license_policy)
context 'searched by name' do
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
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
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
......@@ -316,4 +316,81 @@ describe Ci::Pipeline do
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
# 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'
describe MergeRequest do
using RSpec::Parameterized::TableSyntax
include ReactiveCachingHelpers
let(:project) { create(:project, :repository) }
......@@ -74,4 +75,98 @@ describe MergeRequest do
it { expect(subject.base_pipeline).to eq(pipeline) }
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
# 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 @@
module Gitlab
module Ci
module Parsers
prepend ::EE::Gitlab::Ci::Parsers
ParserNotFoundError = Class.new(ParserError)
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