diff options
Diffstat (limited to 'spec/support/shared_examples/models/concerns')
3 files changed, 214 insertions, 20 deletions
diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb new file mode 100644 index 00000000000..99a09993900 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.shared_examples_for CounterAttribute do |counter_attributes| + it 'defines a Redis counter_key' do + expect(model.counter_key(:counter_name)) + .to eq("project:{#{model.project_id}}:counters:CounterAttributeModel:#{model.id}:counter_name") + end + + it 'defines a method to store counters' do + expect(model.class.counter_attributes.to_a).to eq(counter_attributes) + end + + counter_attributes.each do |attribute| + describe attribute do + describe '#delayed_increment_counter', :redis do + let(:increment) { 10 } + + subject { model.delayed_increment_counter(attribute, increment) } + + context 'when attribute is a counter attribute' do + where(:increment) { [10, -3] } + + with_them do + it 'increments the counter in Redis' do + subject + + Gitlab::Redis::SharedState.with do |redis| + counter = redis.get(model.counter_key(attribute)) + expect(counter).to eq(increment.to_s) + end + end + + it 'does not increment the counter for the record' do + expect { subject }.not_to change { model.reset.read_attribute(attribute) } + end + + it 'schedules a worker to flush counter increments asynchronously' do + expect(FlushCounterIncrementsWorker).to receive(:perform_in) + .with(CounterAttribute::WORKER_DELAY, model.class.name, model.id, attribute) + .and_call_original + + subject + end + end + + context 'when increment is 0' do + let(:increment) { 0 } + + it 'does nothing' do + expect(FlushCounterIncrementsWorker).not_to receive(:perform_in) + expect(model).not_to receive(:update!) + + subject + end + end + end + + context 'when attribute is not a counter attribute' do + it 'delegates to ActiveRecord update!' do + expect { model.delayed_increment_counter(:unknown_attribute, 10) } + .to raise_error(ActiveModel::MissingAttributeError) + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(efficient_counter_attribute: false) + end + + it 'delegates to ActiveRecord update!' do + expect { subject } + .to change { model.reset.read_attribute(attribute) }.by(increment) + end + + it 'does not increment the counter in Redis' do + subject + + Gitlab::Redis::SharedState.with do |redis| + counter = redis.get(model.counter_key(attribute)) + expect(counter).to be_nil + end + end + end + end + end + end + + describe '.flush_increments_to_database!', :redis do + let(:incremented_attribute) { counter_attributes.first } + + subject { model.flush_increments_to_database!(incremented_attribute) } + + it 'obtains an exclusive lease during processing' do + expect(model) + .to receive(:in_lock) + .with(model.counter_lock_key(incremented_attribute), ttl: described_class::WORKER_LOCK_TTL) + .and_call_original + + subject + end + + context 'when there is a counter to flush' do + before do + model.delayed_increment_counter(incremented_attribute, 10) + model.delayed_increment_counter(incremented_attribute, -3) + end + + it 'updates the record' do + expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(7) + end + + it 'removes the increment entry from Redis' do + Gitlab::Redis::SharedState.with do |redis| + key_exists = redis.exists(model.counter_key(incremented_attribute)) + expect(key_exists).to be_truthy + end + + subject + + Gitlab::Redis::SharedState.with do |redis| + key_exists = redis.exists(model.counter_key(incremented_attribute)) + expect(key_exists).to be_falsey + end + end + end + + context 'when there are no counters to flush' do + context 'when there are no counters in the relative :flushed key' do + it 'does not change the record' do + expect { subject }.not_to change { model.reset.attributes } + end + end + + # This can be the case where updating counters in the database fails with error + # and retrying the worker will retry flushing the counters but the main key has + # disappeared and the increment has been moved to the "<...>:flushed" key. + context 'when there are counters in the relative :flushed key' do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.incrby(model.counter_flushed_key(incremented_attribute), 10) + end + end + + it 'updates the record' do + expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(10) + end + + it 'deletes the relative :flushed key' do + subject + + Gitlab::Redis::SharedState.with do |redis| + key_exists = redis.exists(model.counter_flushed_key(incremented_attribute)) + expect(key_exists).to be_falsey + end + end + end + end + + context 'when deleting :flushed key fails' do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.incrby(model.counter_flushed_key(incremented_attribute), 10) + + expect(redis).to receive(:del).and_raise('could not delete key') + end + end + + it 'does a rollback of the counter update' do + expect { subject }.to raise_error('could not delete key') + + expect(model.reset.read_attribute(incremented_attribute)).to eq(0) + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb b/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb new file mode 100644 index 00000000000..4cb087c47ad --- /dev/null +++ b/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'mounted file in local store' do + it 'is stored locally' do + expect(subject.file_store).to be(ObjectStorage::Store::LOCAL) + expect(subject.file).to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL) + end +end + +RSpec.shared_examples 'mounted file in object store' do + it 'is stored remotely' do + expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE) + expect(subject.file).not_to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE) + end +end diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb index 32d502af5a2..15ca1f56bd0 100644 --- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb @@ -3,7 +3,8 @@ RSpec.shared_examples 'a timebox' do |timebox_type| let(:project) { create(:project, :public) } let(:group) { create(:group) } - let(:timebox) { create(timebox_type, project: project) } + let(:timebox_args) { [] } + let(:timebox) { create(timebox_type, *timebox_args, project: project) } let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym } @@ -12,7 +13,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context 'with a project' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } - let(:instance) { build(timebox_type, project: build(:project), group: nil) } + let(:instance) { build(timebox_type, *timebox_args, project: build(:project), group: nil) } let(:scope) { :project } let(:scope_attrs) { { project: instance.project } } let(:usage) { timebox_table_name } @@ -22,7 +23,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context 'with a group' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } - let(:instance) { build(timebox_type, project: nil, group: build(:group)) } + let(:instance) { build(timebox_type, *timebox_args, project: nil, group: build(:group)) } let(:scope) { :group } let(:scope_attrs) { { namespace: instance.group } } let(:usage) { timebox_table_name } @@ -37,14 +38,14 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe 'start_date' do it 'adds an error when start_date is greater then due_date' do - timebox = build(timebox_type, start_date: Date.tomorrow, due_date: Date.yesterday) + timebox = build(timebox_type, *timebox_args, start_date: Date.tomorrow, due_date: Date.yesterday) expect(timebox).not_to be_valid expect(timebox.errors[:due_date]).to include("must be greater than start date") end it 'adds an error when start_date is greater than 9999-12-31' do - timebox = build(timebox_type, start_date: Date.new(10000, 1, 1)) + timebox = build(timebox_type, *timebox_args, start_date: Date.new(10000, 1, 1)) expect(timebox).not_to be_valid expect(timebox.errors[:start_date]).to include("date must not be after 9999-12-31") @@ -53,7 +54,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe 'due_date' do it 'adds an error when due_date is greater than 9999-12-31' do - timebox = build(timebox_type, due_date: Date.new(10000, 1, 1)) + timebox = build(timebox_type, *timebox_args, due_date: Date.new(10000, 1, 1)) expect(timebox).not_to be_valid expect(timebox.errors[:due_date]).to include("date must not be after 9999-12-31") @@ -64,7 +65,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| it { is_expected.to validate_presence_of(:title) } it 'is invalid if title would be empty after sanitation' do - timebox = build(timebox_type, project: project, title: '<img src=x onerror=prompt(1)>') + timebox = build(timebox_type, *timebox_args, project: project, title: '<img src=x onerror=prompt(1)>') expect(timebox).not_to be_valid expect(timebox.errors[:title]).to include("can't be blank") @@ -73,7 +74,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe '#timebox_type_check' do it 'is invalid if it has both project_id and group_id' do - timebox = build(timebox_type, group: group) + timebox = build(timebox_type, *timebox_args, group: group) timebox.project = project expect(timebox).not_to be_valid @@ -98,7 +99,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end context "per group" do - let(:timebox) { create(timebox_type, group: group) } + let(:timebox) { create(timebox_type, *timebox_args, group: group) } before do project.update(group: group) @@ -111,7 +112,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end it "does not accept the same title of a child project timebox" do - create(timebox_type, project: group.projects.first) + create(timebox_type, *timebox_args, project: group.projects.first) new_timebox = described_class.new(group: group, title: timebox.title) @@ -143,7 +144,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end context 'when project_id is not present' do - let(:timebox) { build(timebox_type, group: group) } + let(:timebox) { build(timebox_type, *timebox_args, group: group) } it 'returns false' do expect(timebox.project_timebox?).to be_falsey @@ -153,7 +154,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe '#group_timebox?' do context 'when group_id is present' do - let(:timebox) { build(timebox_type, group: group) } + let(:timebox) { build(timebox_type, *timebox_args, group: group) } it 'returns true' do expect(timebox.group_timebox?).to be_truthy @@ -168,7 +169,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end describe '#safe_title' do - let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") } + let(:timebox) { create(timebox_type, *timebox_args, title: "<b>foo & bar -> 2.2</b>") } it 'normalizes the title for use as a slug' do expect(timebox.safe_title).to eq('foo-bar-22') @@ -177,7 +178,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe '#resource_parent' do context 'when group is present' do - let(:timebox) { build(timebox_type, group: group) } + let(:timebox) { build(timebox_type, *timebox_args, group: group) } it 'returns the group' do expect(timebox.resource_parent).to eq(group) @@ -192,7 +193,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end describe "#title" do - let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") } + let(:timebox) { create(timebox_type, *timebox_args, title: "<b>foo & bar -> 2.2</b>") } it "sanitizes title" do expect(timebox.title).to eq("foo & bar -> 2.2") @@ -203,28 +204,28 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context "per project" do it "is true for projects with MRs enabled" do project = create(:project, :merge_requests_enabled) - timebox = create(timebox_type, project: project) + timebox = create(timebox_type, *timebox_args, project: project) expect(timebox.merge_requests_enabled?).to be_truthy end it "is false for projects with MRs disabled" do project = create(:project, :repository_enabled, :merge_requests_disabled) - timebox = create(timebox_type, project: project) + timebox = create(timebox_type, *timebox_args, project: project) expect(timebox.merge_requests_enabled?).to be_falsey end it "is false for projects with repository disabled" do project = create(:project, :repository_disabled) - timebox = create(timebox_type, project: project) + timebox = create(timebox_type, *timebox_args, project: project) expect(timebox.merge_requests_enabled?).to be_falsey end end context "per group" do - let(:timebox) { create(timebox_type, group: group) } + let(:timebox) { create(timebox_type, *timebox_args, group: group) } it "is always true for groups, for performance reasons" do expect(timebox.merge_requests_enabled?).to be_truthy @@ -234,7 +235,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe '#to_ability_name' do it 'returns timebox' do - timebox = build(timebox_type) + timebox = build(timebox_type, *timebox_args) expect(timebox.to_ability_name).to eq(timebox_type.to_s) end |