Commit 051f0c95 authored by Mehmet Emin INAC's avatar Mehmet Emin INAC

Implement ExportService class to encapsulate export logic

parent 0e32b4aa
# frozen_string_literal: true
module VulnerabilityExports
class ExportService
include ::Gitlab::ExclusiveLeaseHelpers
LEASE_TTL = 1.hour
LEASE_NAMESPACE = "vulnerability_exports_export"
EXPORTERS = {
'csv' => VulnerabilityExports::Exporters::CsvService
}.freeze
def self.export(vulnerability_export)
new(vulnerability_export).export
end
def initialize(vulnerability_export)
self.vulnerability_export = vulnerability_export
end
def export
in_lock(lease_key, ttl: LEASE_TTL) do
generate_export if vulnerability_export.created?
end
end
private
attr_accessor :vulnerability_export
delegate :exportable, to: :vulnerability_export, private: true
delegate :format, to: :vulnerability_export, private: true
def lease_key
"#{LEASE_NAMESPACE}:#{vulnerability_export.id}"
end
def generate_export
vulnerability_export.start!
generate_export_file
vulnerability_export.finish!
rescue => error
vulnerability_export.failed!
raise(error)
ensure
schedule_export_deletion
end
def generate_export_file
exporter.generate { |f| vulnerability_export.file = f }
vulnerability_export.file.filename = filename
end
def exporter
EXPORTERS[format].new(vulnerabilities)
end
def vulnerabilities
Security::VulnerabilitiesFinder.new(exportable).execute.with_findings_and_scanner
end
def schedule_export_deletion
VulnerabilityExports::ExportDeletionWorker.perform_in(1.hour, vulnerability_export.id)
end
def filename
[
exportable.full_path.parameterize,
'_vulnerabilities_',
Time.now.utc.strftime('%FT%H%M'),
'.',
format
].join
end
end
end
......@@ -5,8 +5,8 @@ module VulnerabilityExports
class CsvService
attr_reader :vulnerabilities
def initialize(vulnerabilities_relation)
@vulnerabilities = vulnerabilities_relation
def initialize(vulnerabilities)
@vulnerabilities = vulnerabilities
end
def generate(&block)
......@@ -16,7 +16,7 @@ module VulnerabilityExports
private
def csv_builder
@csv_builder ||= CsvBuilder.new(vulnerabilities.with_findings_and_scanner, header_to_value_hash)
@csv_builder ||= CsvBuilder.new(vulnerabilities, header_to_value_hash)
end
def header_to_value_hash
......
# frozen_string_literal: true
require 'spec_helper'
describe VulnerabilityExports::ExportService do
describe '::export' do
let(:vulnerability_export) { create(:vulnerability_export) }
let(:mock_service_object) { instance_double(described_class, export: true) }
subject(:export) { described_class.export(vulnerability_export) }
before do
allow(described_class).to receive(:new).and_return(mock_service_object)
end
it 'instantiates a new instance of the service class and sends export message to it' do
export
expect(described_class).to have_received(:new).with(vulnerability_export)
expect(mock_service_object).to have_received(:export)
end
end
describe '#export' do
let(:vulnerability_export) { create(:vulnerability_export, :created) }
let(:service_object) { described_class.new(vulnerability_export) }
subject(:export) { service_object.export }
context 'generating the export file' do
let(:lease_name) { "vulnerability_exports_export:#{vulnerability_export.id}" }
before do
allow(service_object).to receive(:in_lock)
end
it 'runs synchronized with distributed semaphore' do
export
expect(service_object).to have_received(:in_lock).with(lease_name, ttl: 1.hour)
end
end
context 'when the vulnerability_export is not in `created` state' do
before do
allow(vulnerability_export).to receive(:created?).and_return(false)
allow(service_object).to receive(:generate_export)
end
it 'does not execute export file generation logic' do
export
expect(service_object).not_to have_received(:generate_export)
end
end
context 'when the vulnerability_export is in `created` state' do
before do
allow(VulnerabilityExports::ExportDeletionWorker).to receive(:perform_in)
end
context 'when the export generation fails' do
let(:error) { RuntimeError.new('foo') }
before do
allow(service_object).to receive(:generate_export_file).and_raise(error)
end
it 'marks the export object as `failed` and propagates the error to the caller' do
expect { export }.to raise_error(error)
expect(vulnerability_export.failed?).to be_truthy
end
it 'schedules the export deletion background job' do
expect { export }.to raise_error(error)
expect(VulnerabilityExports::ExportDeletionWorker).to have_received(:perform_in).with(1.hour, vulnerability_export.id)
end
end
context 'when the export generation succeeds' do
before do
allow(service_object).to receive(:generate_export_file)
allow(vulnerability_export).to receive(:start!)
allow(vulnerability_export).to receive(:finish!)
end
it 'marks the state of export object as `started` and then `finished`' do
export
expect(vulnerability_export).to have_received(:start!).ordered
expect(vulnerability_export).to have_received(:finish!).ordered
end
it 'schedules the export deletion background job' do
export
expect(VulnerabilityExports::ExportDeletionWorker).to have_received(:perform_in).with(1.hour, vulnerability_export.id)
end
end
context 'when the export format is csv' do
let(:vulnerabilities) { Vulnerability.none }
let(:mock_relation) { double(:relation, with_findings_and_scanner: vulnerabilities) }
let(:mock_vulnerability_finder_service_object) { instance_double(Security::VulnerabilitiesFinder, execute: mock_relation) }
let(:exportable_full_path) { 'foo' }
let(:time_suffix) { Time.now.utc.strftime('%FT%H%M') }
let(:expected_file_name) { "#{exportable_full_path}_vulnerabilities_#{time_suffix}.csv" }
before do
allow(Security::VulnerabilitiesFinder).to receive(:new).and_return(mock_vulnerability_finder_service_object)
allow(vulnerability_export.exportable).to receive(:full_path).and_return(exportable_full_path)
end
around do |example|
Timecop.freeze { example.run }
end
it 'calls the VulnerabilityExports::Exporters::CsvService which sets the file and filename' do
expect { export }.to change { vulnerability_export.file }
.and change { vulnerability_export.file&.filename }.from(nil).to(expected_file_name)
end
end
end
end
end
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