Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/models/ci/catalog/resources/sync_event_spec.rb')
-rw-r--r--spec/models/ci/catalog/resources/sync_event_spec.rb190
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