# frozen_string_literal: true require 'spec_helper' RSpec.describe Gitlab::Audit::Type::Definition do let(:attributes) do { name: 'group_deploy_token_destroyed', description: 'Group deploy token is deleted', introduced_by_issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/1', introduced_by_mr: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1', group: 'govern::compliance', milestone: '15.4', saved_to_database: true, streamed: true } end let(:path) { File.join('types', 'group_deploy_token_destroyed.yml') } let(:definition) { described_class.new(path, attributes) } let(:yaml_content) { attributes.deep_stringify_keys.to_yaml } describe '#key' do subject { definition.key } it 'returns a symbol from name' do is_expected.to eq(:group_deploy_token_destroyed) end end describe '#validate!', :aggregate_failures do using RSpec::Parameterized::TableSyntax # rubocop:disable Layout/LineLength where(:param, :value, :result) do :path | 'audit_event/types/invalid.yml' | /Audit event type 'group_deploy_token_destroyed' has an invalid path/ :name | nil | %r{property '/name' is not of type: string} :description | nil | %r{property '/description' is not of type: string} :introduced_by_issue | nil | %r{property '/introduced_by_issue' is not of type: string} :introduced_by_mr | nil | %r{property '/introduced_by_mr' is not of type: string} :group | nil | %r{property '/group' is not of type: string} :milestone | nil | %r{property '/milestone' is not of type: string} end # rubocop:enable Layout/LineLength with_them do let(:params) { attributes.merge(path: path) } before do params[param] = value end it do expect do described_class.new( params[:path], params.except(:path) ).validate! end.to raise_error(result) end end context 'when both saved_to_database and streamed are false' do let(:params) { attributes.merge({ path: path, saved_to_database: false, streamed: false }) } it 'raises an exception' do expect do described_class.new( params[:path], params.except(:path) ).validate! end.to raise_error(/root is invalid: error_type=not/) end end end describe '.paths' do it 'returns at least one path' do expect(described_class.paths).not_to be_empty end end describe '.get' do before do allow(described_class).to receive(:definitions) do { definition.key => definition } end end context 'when audit event type is not defined' do let(:undefined_audit_event_type) { 'undefined_audit_event_type' } it 'returns nil' do expect(described_class.get(undefined_audit_event_type)).to be nil end end context 'when audit event type is defined' do let(:audit_event_type) { 'group_deploy_token_destroyed' } it 'returns an instance of Gitlab::Audit::Type::Definition' do expect(described_class.get(audit_event_type)).to be_an_instance_of(described_class) end it 'returns the properties as defined for that audit event type', :aggregate_failures do audit_event_type_definition = described_class.get(audit_event_type) expect(audit_event_type_definition.name).to eq "group_deploy_token_destroyed" expect(audit_event_type_definition.description).to eq "Group deploy token is deleted" expect(audit_event_type_definition.group).to eq "govern::compliance" expect(audit_event_type_definition.milestone).to eq "15.4" expect(audit_event_type_definition.saved_to_database).to be true expect(audit_event_type_definition.streamed).to be true end end end describe '.load_from_file' do it 'properly loads a definition from file' do expect_file_read(path, content: yaml_content) expect(described_class.send(:load_from_file, path).attributes) .to eq(definition.attributes) end context 'for missing file' do let(:path) { 'missing/audit_events/type/file.yml' } it 'raises exception' do expect do described_class.send(:load_from_file, path) end.to raise_error(/Invalid definition for/) end end context 'for invalid definition' do it 'raises exception' do expect_file_read(path, content: '{}') expect do described_class.send(:load_from_file, path) end.to raise_error(%r{property '/name' is not of type: string}) end end end describe '.load_all!' do let(:store1) { Dir.mktmpdir('path1') } let(:store2) { Dir.mktmpdir('path2') } let(:definitions) { {} } before do allow(described_class).to receive(:paths).and_return( [ File.join(store1, '**', '*.yml'), File.join(store2, '**', '*.yml') ] ) end subject { described_class.send(:load_all!) } after do FileUtils.rm_rf(store1) FileUtils.rm_rf(store2) end it "when there are no audit event types a list of definitions is empty" do is_expected.to be_empty end it "when there's a single audit event type it properly loads them" do write_audit_event_type(store1, path, yaml_content) is_expected.to be_one end it "when the same audit event type is stored multiple times raises exception" do write_audit_event_type(store1, path, yaml_content) write_audit_event_type(store2, path, yaml_content) expect { subject } .to raise_error(/Audit event type 'group_deploy_token_destroyed' is already defined/) end it "when one of the YAMLs is invalid it does raise exception" do write_audit_event_type(store1, path, '{}') expect { subject }.to raise_error(/Invalid definition for .* '' must match the filename/) end end describe '.definitions' do let(:store1) { Dir.mktmpdir('path1') } before do allow(described_class).to receive(:paths).and_return( [ File.join(store1, '**', '*.yml') ] ) end subject { described_class.definitions } after do FileUtils.rm_rf(store1) end it "loads the definitions for all the audit event types" do write_audit_event_type(store1, path, yaml_content) is_expected.to be_one end end def write_audit_event_type(store, path, content) path = File.join(store, path) dir = File.dirname(path) FileUtils.mkdir_p(dir) File.write(path, content) end end