diff options
Diffstat (limited to 'spec/models/ci/catalog/resources/sync_event_spec.rb')
-rw-r--r-- | spec/models/ci/catalog/resources/sync_event_spec.rb | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/spec/models/ci/catalog/resources/sync_event_spec.rb b/spec/models/ci/catalog/resources/sync_event_spec.rb new file mode 100644 index 00000000000..5d907aae9b6 --- /dev/null +++ b/spec/models/ci/catalog/resources/sync_event_spec.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Catalog::Resources::SyncEvent, type: :model, feature_category: :pipeline_composition do + let_it_be_with_reload(:project1) { create(:project) } + let_it_be_with_reload(:project2) { create(:project) } + let_it_be(:resource1) { create(:ci_catalog_resource, project: project1) } + + it { is_expected.to belong_to(:catalog_resource).class_name('Ci::Catalog::Resource') } + it { is_expected.to belong_to(:project) } + + describe 'PG triggers' do + context 'when the associated project of a catalog resource is updated' do + context 'when project name is updated' do + it 'creates a sync event record' do + expect do + project1.update!(name: 'New name') + end.to change { described_class.count }.by(1) + end + end + + context 'when project description is updated' do + it 'creates a sync event record' do + expect do + project1.update!(description: 'New description') + end.to change { described_class.count }.by(1) + end + end + + context 'when project visibility_level is updated' do + it 'creates a sync event record' do + expect do + project1.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end.to change { described_class.count }.by(1) + end + end + end + + context 'when a project without an associated catalog resource is updated' do + it 'does not create a sync event record' do + expect do + project2.update!(name: 'New name') + end.not_to change { described_class.count } + end + end + end + + describe 'when there are sync event records' do + let_it_be(:resource2) { create(:ci_catalog_resource, project: project2) } + + before_all do + create(:ci_catalog_resource_sync_event, catalog_resource: resource1, status: :processed) + create(:ci_catalog_resource_sync_event, catalog_resource: resource1) + create_list(:ci_catalog_resource_sync_event, 2, catalog_resource: resource2) + end + + describe '.unprocessed_events' do + it 'returns the events in pending status' do + # 1 pending event from resource1 + 2 pending events from resource2 + expect(described_class.unprocessed_events.size).to eq(3) + end + + it 'selects the partition attribute in the result' do + described_class.unprocessed_events.each do |event| + expect(event.partition).not_to be_nil + end + end + end + + describe '.mark_records_processed' do + it 'updates the records to processed status' do + expect(described_class.status_pending.count).to eq(3) + expect(described_class.status_processed.count).to eq(1) + + described_class.mark_records_processed(described_class.unprocessed_events) + + expect(described_class.pluck(:status).uniq).to eq(['processed']) + + expect(described_class.status_pending.count).to eq(0) + expect(described_class.status_processed.count).to eq(4) + end + end + end + + describe '.upper_bound_count' do + it 'returns 0 when there are no records in the table' do + expect(described_class.upper_bound_count).to eq(0) + end + + it 'returns an estimated number of unprocessed records' do + create_list(:ci_catalog_resource_sync_event, 5, catalog_resource: resource1) + described_class.order(:id).limit(2).update_all(status: :processed) + + expect(described_class.upper_bound_count).to eq(3) + end + end + + describe 'sliding_list partitioning' do + let(:partition_manager) { Gitlab::Database::Partitioning::PartitionManager.new(described_class) } + + describe 'next_partition_if callback' do + let(:active_partition) { described_class.partitioning_strategy.active_partition } + + subject(:value) { described_class.partitioning_strategy.next_partition_if.call(active_partition) } + + context 'when the partition is empty' do + it { is_expected.to eq(false) } + end + + context 'when the partition has records' do + before do + create(:ci_catalog_resource_sync_event, catalog_resource: resource1, status: :processed) + create(:ci_catalog_resource_sync_event, catalog_resource: resource1) + end + + it { is_expected.to eq(false) } + end + + context 'when the first record of the partition is older than PARTITION_DURATION' do + before do + create(:ci_catalog_resource_sync_event, catalog_resource: resource1) + described_class.first.update!(created_at: (described_class::PARTITION_DURATION + 1.day).ago) + end + + it { is_expected.to eq(true) } + end + end + + describe 'detach_partition_if callback' do + let(:active_partition) { described_class.partitioning_strategy.active_partition } + + subject(:value) { described_class.partitioning_strategy.detach_partition_if.call(active_partition) } + + before_all do + create(:ci_catalog_resource_sync_event, catalog_resource: resource1, status: :processed) + create(:ci_catalog_resource_sync_event, catalog_resource: resource1) + end + + context 'when the partition contains unprocessed records' do + it { is_expected.to eq(false) } + end + + context 'when the partition contains only processed records' do + before do + described_class.update_all(status: :processed) + end + + it { is_expected.to eq(true) } + end + end + + describe 'strategy behavior' do + it 'moves records to new partitions as time passes', :freeze_time do + # We start with partition 1 + expect(described_class.partitioning_strategy.current_partitions.map(&:value)).to eq([1]) + + # Add one record so the initial partition is not empty + create(:ci_catalog_resource_sync_event, catalog_resource: resource1) + + # It's not a day old yet so no new partitions are created + partition_manager.sync_partitions + + expect(described_class.partitioning_strategy.current_partitions.map(&:value)).to eq([1]) + + # After traveling forward a day + travel(described_class::PARTITION_DURATION + 1.second) + + # a new partition is created + partition_manager.sync_partitions + + expect(described_class.partitioning_strategy.current_partitions.map(&:value)).to contain_exactly(1, 2) + + # and we can insert to the new partition + create(:ci_catalog_resource_sync_event, catalog_resource: resource1) + + # After processing records in partition 1 + described_class.mark_records_processed(described_class.for_partition(1).select_with_partition) + + partition_manager.sync_partitions + + # partition 1 is removed + expect(described_class.partitioning_strategy.current_partitions.map(&:value)).to eq([2]) + + # and we only have the newly created partition left. + expect(described_class.count).to eq(1) + end + end + end +end |