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/support/shared_examples/models')
-rw-r--r--spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/models/concerns/shardable_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb86
-rw-r--r--spec/support/shared_examples/models/mentionable_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/models/relative_positioning_shared_examples.rb182
-rw-r--r--spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/snippet_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/throttled_touch_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb260
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb85
11 files changed, 616 insertions, 103 deletions
diff --git a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
index f37ef3533c3..826ee453919 100644
--- a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
@@ -6,6 +6,14 @@ RSpec.shared_examples 'model with repository' do
let(:expected_full_path) { raise NotImplementedError }
let(:expected_web_url_path) { expected_full_path }
let(:expected_repo_url_path) { expected_full_path }
+ let(:expected_lfs_enabled) { false }
+
+ it 'container class includes HasRepository' do
+ # NOTE: This is not enforced at runtime, since we also need to support Geo::DeletedProject
+ expect(described_class).to include_module(HasRepository)
+ expect(container).to be_kind_of(HasRepository)
+ expect(stubbed_container).to be_kind_of(HasRepository)
+ end
describe '#commits_by' do
let(:commits) { container.repository.commits('HEAD', limit: 3).commits }
@@ -74,6 +82,10 @@ RSpec.shared_examples 'model with repository' do
it 'returns valid repo' do
expect(container.repository).to be_kind_of(Repository)
end
+
+ it 'uses the same container' do
+ expect(container.repository.container).to be(container)
+ end
end
describe '#storage' do
@@ -88,6 +100,16 @@ RSpec.shared_examples 'model with repository' do
end
end
+ describe '#lfs_enabled?' do
+ before do
+ stub_lfs_setting(enabled: true)
+ end
+
+ it 'returns the expected value' do
+ expect(container.lfs_enabled?).to eq(expected_lfs_enabled)
+ end
+ end
+
describe '#empty_repo?' do
context 'when the repo does not exist' do
it 'returns true' do
diff --git a/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb b/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb
new file mode 100644
index 00000000000..fa929d5b791
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'shardable scopes' do
+ let_it_be(:secondary_shard) { create(:shard, name: 'test_second_storage') }
+
+ before do
+ record_2.update!(shard: secondary_shard)
+ end
+
+ describe '.for_repository_storage' do
+ it 'returns the objects for a given repository storage' do
+ expect(described_class.for_repository_storage('default')).to eq([record_1])
+ end
+ end
+
+ describe '.excluding_repository_storage' do
+ it 'returns the objects excluding the given repository storage' do
+ expect(described_class.excluding_repository_storage('default')).to eq([record_2])
+ end
+ 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 d199bae4170..f91e4bd8cf7 100644
--- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
@@ -9,6 +9,11 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
let(:user) { create(:user) }
let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym }
+ # Values implementions can override
+ let(:mid_point) { Time.now.utc.to_date }
+ let(:open_on_left) { nil }
+ let(:open_on_right) { nil }
+
describe 'modules' do
context 'with a project' do
it_behaves_like 'AtomicInternalId' do
@@ -240,4 +245,85 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
expect(timebox.to_ability_name).to eq(timebox_type.to_s)
end
end
+
+ describe '.within_timeframe' do
+ let(:factory) { timebox_type }
+ let(:min_date) { mid_point - 10.days }
+ let(:max_date) { mid_point + 10.days }
+
+ def box(from, to)
+ create(factory, *timebox_args,
+ start_date: from || open_on_left,
+ due_date: to || open_on_right)
+ end
+
+ it 'can find overlapping timeboxes' do
+ fully_open = box(nil, nil)
+ # ----| ................ # Not overlapping
+ non_overlapping_open_on_left = box(nil, min_date - 1.day)
+ # |--| ................ # Not overlapping
+ non_overlapping_closed_on_left = box(min_date - 2.days, min_date - 1.day)
+ # ------|............... # Overlapping
+ overlapping_open_on_left_just = box(nil, min_date)
+ # -----------------------| # Overlapping
+ overlapping_open_on_left_fully = box(nil, max_date + 1.day)
+ # ---------|............ # Overlapping
+ overlapping_open_on_left_partial = box(nil, min_date + 1.day)
+ # |-----|............ # Overlapping
+ overlapping_closed_partial = box(min_date - 1.day, min_date + 1.day)
+ # |--------------| # Overlapping
+ exact_match = box(min_date, max_date)
+ # |--------------------| # Overlapping
+ larger = box(min_date - 1.day, max_date + 1.day)
+ # ...|-----|...... # Overlapping
+ smaller = box(min_date + 1.day, max_date - 1.day)
+ # .........|-----| # Overlapping
+ at_end = box(max_date - 1.day, max_date)
+ # .........|--------- # Overlapping
+ at_end_open = box(max_date - 1.day, nil)
+ # |-------------------- # Overlapping
+ cover_from_left = box(min_date - 1.day, nil)
+ # .........|--------| # Overlapping
+ cover_from_middle_closed = box(max_date - 1.day, max_date + 1.day)
+ # ...............|--| # Overlapping
+ overlapping_at_end_just = box(max_date, max_date + 1.day)
+ # ............... |-| # Not Overlapping
+ not_overlapping_at_right_closed = box(max_date + 1.day, max_date + 2.days)
+ # ............... |-- # Not Overlapping
+ not_overlapping_at_right_open = box(max_date + 1.day, nil)
+
+ matches = described_class.within_timeframe(min_date, max_date)
+
+ expect(matches).to include(
+ overlapping_open_on_left_just,
+ overlapping_open_on_left_fully,
+ overlapping_open_on_left_partial,
+ overlapping_closed_partial,
+ exact_match,
+ larger,
+ smaller,
+ at_end,
+ at_end_open,
+ cover_from_left,
+ cover_from_middle_closed,
+ overlapping_at_end_just
+ )
+
+ expect(matches).not_to include(
+ non_overlapping_open_on_left,
+ non_overlapping_closed_on_left,
+ not_overlapping_at_right_closed,
+ not_overlapping_at_right_open
+ )
+
+ # Whether we match the 'fully-open' range depends on whether
+ # it is in fact open (i.e. whether the class allows infinite
+ # ranges)
+ if open_on_left.nil? && open_on_right.nil?
+ expect(matches).not_to include(fully_open)
+ else
+ expect(matches).to include(fully_open)
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb
index 94c52bdaaa6..0ee0b7e6d88 100644
--- a/spec/support/shared_examples/models/mentionable_shared_examples.rb
+++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb
@@ -207,29 +207,8 @@ RSpec.shared_examples 'an editable mentionable' do
end
RSpec.shared_examples 'mentions in description' do |mentionable_type|
- describe 'when store_mentioned_users_to_db feature disabled' do
+ describe 'when storing user mentions' do
before do
- stub_feature_flags(store_mentioned_users_to_db: false)
- mentionable.store_mentions!
- end
-
- context 'when mentionable description contains mentions' do
- let(:user) { create(:user) }
- let(:mentionable) { create(mentionable_type, description: "#{user.to_reference} some description") }
-
- it 'stores no mentions' do
- expect(mentionable.user_mentions.count).to eq 0
- end
-
- it 'renders description_html correctly' do
- expect(mentionable.description_html).to include("<a href=\"/#{user.username}\" data-user=\"#{user.id}\"")
- end
- end
- end
-
- describe 'when store_mentioned_users_to_db feature enabled' do
- before do
- stub_feature_flags(store_mentioned_users_to_db: true)
mentionable.store_mentions!
end
diff --git a/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb b/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb
index 7701ab42007..66cd8d1df12 100644
--- a/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb
+++ b/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb
@@ -60,4 +60,20 @@ RSpec.shared_examples 'latest successful build for sha or ref' do
expect(subject).to be_nil
end
end
+
+ context 'with build belonging to a child pipeline' do
+ let(:child_pipeline) { create_pipeline(project) }
+ let(:parent_bridge) { create(:ci_bridge, pipeline: pipeline, project: pipeline.project) }
+ let!(:pipeline_source) { create(:ci_sources_pipeline, source_job: parent_bridge, pipeline: child_pipeline)}
+ let!(:child_build) { create_build(child_pipeline, 'child-build') }
+ let(:build_name) { child_build.name }
+
+ before do
+ child_pipeline.update!(source: :parent_pipeline)
+ end
+
+ it 'returns the child build' do
+ expect(subject).to eq(child_build)
+ end
+ end
end
diff --git a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
index d1437244082..b8d12a6da59 100644
--- a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
+++ b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
@@ -31,6 +31,41 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
end
+ def as_item(item)
+ item # Override to perform a transformation, if necessary
+ end
+
+ def as_items(items)
+ items.map { |item| as_item(item) }
+ end
+
+ describe '#scoped_items' do
+ it 'includes all items with the same scope' do
+ scope = as_items([item1, item2, new_item, create_item])
+ irrelevant = create(factory, {}) # This should not share the scope
+ context = RelativePositioning.mover.context(item1)
+
+ same_scope = as_items(context.scoped_items)
+
+ expect(same_scope).to include(*scope)
+ expect(same_scope).not_to include(as_item(irrelevant))
+ end
+ end
+
+ describe '#relative_siblings' do
+ it 'includes all items with the same scope, except self' do
+ scope = as_items([item2, new_item, create_item])
+ irrelevant = create(factory, {}) # This should not share the scope
+ context = RelativePositioning.mover.context(item1)
+
+ siblings = as_items(context.relative_siblings)
+
+ expect(siblings).to include(*scope)
+ expect(siblings).not_to include(as_item(item1))
+ expect(siblings).not_to include(as_item(irrelevant))
+ end
+ end
+
describe '.move_nulls_to_end' do
let(:item3) { create_item }
let(:sibling_query) { item1.class.relative_positioning_query_base(item1) }
@@ -47,7 +82,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(item1.relative_position).to be(1000)
expect(sibling_query.where(relative_position: nil)).not_to exist
- expect(sibling_query.reorder(:relative_position, :id)).to eq([item1, item2, item3])
+ expect(as_items(sibling_query.reorder(:relative_position, :id))).to eq(as_items([item1, item2, item3]))
end
it 'preserves relative position' do
@@ -117,19 +152,36 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(bunch.map(&:relative_position)).to all(be < nils.map(&:relative_position).min)
end
+ it 'manages to move nulls found in the relative scope' do
+ nils = create_items_with_positions([nil] * 4)
+
+ described_class.move_nulls_to_end(sibling_query.to_a)
+ positions = nils.map { |item| item.reset.relative_position }
+
+ expect(positions).to all(be_present)
+ expect(positions).to all(be_valid_position)
+ end
+
+ it 'can move many nulls' do
+ nils = create_items_with_positions([nil] * 101)
+
+ described_class.move_nulls_to_end(nils)
+
+ expect(nils.map(&:relative_position)).to all(be_valid_position)
+ end
+
it 'does not have an N+1 issue' do
create_items_with_positions(10..12)
-
- a, b, c, d, e, f = create_items_with_positions([nil, nil, nil, nil, nil, nil])
+ a, b, c, d, e, f, *xs = create_items_with_positions([nil] * 10)
baseline = ActiveRecord::QueryRecorder.new do
- described_class.move_nulls_to_end([a, e])
+ described_class.move_nulls_to_end([a, b])
end
- expect { described_class.move_nulls_to_end([b, c, d]) }
+ expect { described_class.move_nulls_to_end([c, d, e, f]) }
.not_to exceed_query_limit(baseline)
- expect { described_class.move_nulls_to_end([f]) }
+ expect { described_class.move_nulls_to_end(xs) }
.not_to exceed_query_limit(baseline.count)
end
end
@@ -149,7 +201,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(items.sort_by(&:relative_position)).to eq(items)
expect(sibling_query.where(relative_position: nil)).not_to exist
- expect(sibling_query.reorder(:relative_position, :id)).to eq(items)
+ expect(as_items(sibling_query.reorder(:relative_position, :id))).to eq(as_items(items))
expect(item3.relative_position).to be(1000)
end
@@ -652,3 +704,119 @@ RSpec.shared_examples 'a class that supports relative positioning' do
(RelativePositioning::MIN_POSITION..).take(size)
end
end
+
+RSpec.shared_examples 'no-op relative positioning' do
+ def create_item(**params)
+ create(factory, params.merge(default_params))
+ end
+
+ let_it_be(:item1) { create_item }
+ let_it_be(:item2) { create_item }
+ let_it_be(:new_item) { create_item(relative_position: nil) }
+
+ def any_relative_positions
+ new_item.class.reorder(:relative_position, :id).pluck(:id, :relative_position)
+ end
+
+ shared_examples 'a no-op method' do
+ it 'does not raise errors' do
+ expect { perform }.not_to raise_error
+ end
+
+ it 'does not perform any DB queries' do
+ expect { perform }.not_to exceed_query_limit(0)
+ end
+
+ it 'does not change any relative_position' do
+ expect { perform }.not_to change { any_relative_positions }
+ end
+ end
+
+ describe '.scoped_items' do
+ subject { RelativePositioning.mover.context(item1).scoped_items }
+
+ it 'is empty' do
+ expect(subject).to be_empty
+ end
+ end
+
+ describe '.relative_siblings' do
+ subject { RelativePositioning.mover.context(item1).relative_siblings }
+
+ it 'is empty' do
+ expect(subject).to be_empty
+ end
+ end
+
+ describe '.move_nulls_to_end' do
+ subject { item1.class.move_nulls_to_end([new_item, item1]) }
+
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject
+ end
+ end
+
+ it 'does not move any items' do
+ expect(subject).to eq(0)
+ end
+ end
+
+ describe '.move_nulls_to_start' do
+ subject { item1.class.move_nulls_to_start([new_item, item1]) }
+
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject
+ end
+ end
+
+ it 'does not move any items' do
+ expect(subject).to eq(0)
+ end
+ end
+
+ describe 'instance methods' do
+ subject { new_item }
+
+ describe '#move_to_start' do
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject.move_to_start
+ end
+ end
+ end
+
+ describe '#move_to_end' do
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject.move_to_end
+ end
+ end
+ end
+
+ describe '#move_between' do
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject.move_between(item1, item2)
+ end
+ end
+ end
+
+ describe '#move_before' do
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject.move_before(item1)
+ end
+ end
+ end
+
+ describe '#move_after' do
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject.move_after(item1)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb b/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb
index 07552b62cdd..5198508d48b 100644
--- a/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb
+++ b/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb
@@ -73,3 +73,13 @@ RSpec.shared_examples 'timebox resource event actions' do
end
end
end
+
+RSpec.shared_examples 'timebox resource tracks issue metrics' do |type|
+ describe '#usage_metrics' do
+ it 'tracks usage' do
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:"track_issue_#{type}_changed_action")
+
+ create(described_class.name.underscore.to_sym, issue: create(:issue))
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/snippet_shared_examples.rb b/spec/support/shared_examples/models/snippet_shared_examples.rb
new file mode 100644
index 00000000000..a8fdf9bb81e
--- /dev/null
+++ b/spec/support/shared_examples/models/snippet_shared_examples.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'size checker for snippet' do |action|
+ it 'sets up size checker', :aggregate_failures do
+ expect(checker.current_size).to eq(current_size.megabytes)
+ expect(checker.limit).to eq(Gitlab::CurrentSettings.snippet_size_limit)
+ expect(checker.enabled?).to eq(true)
+ expect(checker.instance_variable_get(:@namespace)).to eq(namespace)
+ end
+end
diff --git a/spec/support/shared_examples/models/throttled_touch_shared_examples.rb b/spec/support/shared_examples/models/throttled_touch_shared_examples.rb
index 14b851d2828..e869cbce6ae 100644
--- a/spec/support/shared_examples/models/throttled_touch_shared_examples.rb
+++ b/spec/support/shared_examples/models/throttled_touch_shared_examples.rb
@@ -13,8 +13,8 @@ RSpec.shared_examples 'throttled touch' do
first_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 2)
second_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 1.5)
- Timecop.freeze(first_updated_at) { subject.touch }
- Timecop.freeze(second_updated_at) { subject.touch }
+ travel_to(first_updated_at) { subject.touch }
+ travel_to(second_updated_at) { subject.touch }
expect(subject.updated_at).to be_like_time(first_updated_at)
end
diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
index 557025569b8..7b591ad84d1 100644
--- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
+++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'UpdateProjectStatistics' do
+RSpec.shared_examples 'UpdateProjectStatistics' do |with_counter_attribute|
let(:project) { subject.project }
let(:project_statistics_name) { described_class.project_statistics_name }
let(:statistic_attribute) { described_class.statistic_attribute }
@@ -13,108 +13,230 @@ RSpec.shared_examples 'UpdateProjectStatistics' do
subject.read_attribute(statistic_attribute).to_i
end
- it { is_expected.to be_new_record }
+ def read_pending_increment
+ Gitlab::Redis::SharedState.with do |redis|
+ key = project.statistics.counter_key(project_statistics_name)
+ redis.get(key).to_i
+ end
+ end
- context 'when creating' do
- it 'updates the project statistics' do
- delta0 = reload_stat
+ it { is_expected.to be_new_record }
- subject.save!
+ context 'when feature flag efficient_counter_attribute is disabled' do
+ before do
+ stub_feature_flags(efficient_counter_attribute: false)
+ end
- delta1 = reload_stat
+ context 'when creating' do
+ it 'updates the project statistics' do
+ delta0 = reload_stat
- expect(delta1).to eq(delta0 + read_attribute)
- expect(delta1).to be > delta0
- end
+ subject.save!
- it 'schedules a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .to receive(:perform_async).once
+ delta1 = reload_stat
- subject.save!
- end
- end
+ expect(delta1).to eq(delta0 + read_attribute)
+ expect(delta1).to be > delta0
+ end
- context 'when updating' do
- let(:delta) { 42 }
+ it 'schedules a namespace statistics worker' do
+ expect(Namespaces::ScheduleAggregationWorker)
+ .to receive(:perform_async).once
- before do
- subject.save!
+ subject.save!
+ end
end
- it 'updates project statistics' do
- expect(ProjectStatistics)
- .to receive(:increment_statistic)
- .and_call_original
+ context 'when updating' do
+ let(:delta) { 42 }
- subject.write_attribute(statistic_attribute, read_attribute + delta)
+ before do
+ subject.save!
+ end
- expect { subject.save! }
- .to change { reload_stat }
- .by(delta)
- end
+ it 'updates project statistics' do
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
- it 'schedules a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .to receive(:perform_async).once
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
- subject.write_attribute(statistic_attribute, read_attribute + delta)
- subject.save!
- end
+ expect { subject.save! }
+ .to change { reload_stat }
+ .by(delta)
+ end
- it 'avoids N + 1 queries' do
- subject.write_attribute(statistic_attribute, read_attribute + delta)
+ it 'schedules a namespace statistics worker' do
+ expect(Namespaces::ScheduleAggregationWorker)
+ .to receive(:perform_async).once
- control_count = ActiveRecord::QueryRecorder.new do
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
subject.save!
end
- subject.write_attribute(statistic_attribute, read_attribute + delta)
+ it 'avoids N + 1 queries' do
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
- expect do
- subject.save!
- end.not_to exceed_query_limit(control_count)
- end
- end
+ control_count = ActiveRecord::QueryRecorder.new do
+ subject.save!
+ end
- context 'when destroying' do
- before do
- subject.save!
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
+
+ expect do
+ subject.save!
+ end.not_to exceed_query_limit(control_count)
+ end
end
- it 'updates the project statistics' do
- delta0 = reload_stat
+ context 'when destroying' do
+ before do
+ subject.save!
+ end
- subject.destroy!
+ it 'updates the project statistics' do
+ delta0 = reload_stat
- delta1 = reload_stat
+ subject.destroy!
- expect(delta1).to eq(delta0 - read_attribute)
- expect(delta1).to be < delta0
- end
+ delta1 = reload_stat
+
+ expect(delta1).to eq(delta0 - read_attribute)
+ expect(delta1).to be < delta0
+ end
+
+ it 'schedules a namespace statistics worker' do
+ expect(Namespaces::ScheduleAggregationWorker)
+ .to receive(:perform_async).once
- it 'schedules a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .to receive(:perform_async).once
+ subject.destroy!
+ end
+
+ context 'when it is destroyed from the project level' do
+ it 'does not update the project statistics' do
+ expect(ProjectStatistics)
+ .not_to receive(:increment_statistic)
+
+ project.update!(pending_delete: true)
+ project.destroy!
+ end
+
+ it 'does not schedule a namespace statistics worker' do
+ expect(Namespaces::ScheduleAggregationWorker)
+ .not_to receive(:perform_async)
- subject.destroy!
+ project.update!(pending_delete: true)
+ project.destroy!
+ end
+ end
end
+ end
- context 'when it is destroyed from the project level' do
- it 'does not update the project statistics' do
- expect(ProjectStatistics)
- .not_to receive(:increment_statistic)
+ def expect_flush_counter_increments_worker_performed
+ expect(FlushCounterIncrementsWorker)
+ .to receive(:perform_in)
+ .with(CounterAttribute::WORKER_DELAY, project.statistics.class.name, project.statistics.id, project_statistics_name)
+ expect(FlushCounterIncrementsWorker)
+ .to receive(:perform_in)
+ .with(CounterAttribute::WORKER_DELAY, project.statistics.class.name, project.statistics.id, :storage_size)
- project.update!(pending_delete: true)
- project.destroy!
+ yield
+
+ # simulate worker running now
+ expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async)
+ FlushCounterIncrementsWorker.new.perform(project.statistics.class.name, project.statistics.id, project_statistics_name)
+ end
+
+ if with_counter_attribute
+ context 'when statistic is a counter attribute', :clean_gitlab_redis_shared_state do
+ context 'when creating' do
+ it 'stores pending increments for async update' do
+ initial_stat = reload_stat
+ expected_increment = read_attribute
+
+ expect_flush_counter_increments_worker_performed do
+ subject.save!
+
+ expect(read_pending_increment).to eq(expected_increment)
+ expect(expected_increment).to be > initial_stat
+ expect(expected_increment).to be_positive
+ end
+ end
end
- it 'does not schedule a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .not_to receive(:perform_async)
+ context 'when updating' do
+ let(:delta) { 42 }
+
+ before do
+ subject.save!
+ redis_shared_state_cleanup!
+ end
+
+ it 'stores pending increments for async update' do
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
+
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
+
+ expect_flush_counter_increments_worker_performed do
+ subject.save!
+
+ expect(read_pending_increment).to eq(delta)
+ end
+ end
+
+ it 'avoids N + 1 queries' do
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ subject.save!
+ end
+
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
+
+ expect do
+ subject.save!
+ end.not_to exceed_query_limit(control_count)
+ end
+ end
- project.update!(pending_delete: true)
- project.destroy!
+ context 'when destroying' do
+ before do
+ subject.save!
+ redis_shared_state_cleanup!
+ end
+
+ it 'stores pending increment for async update' do
+ initial_stat = reload_stat
+ expected_increment = -read_attribute
+
+ expect_flush_counter_increments_worker_performed do
+ subject.destroy!
+
+ expect(read_pending_increment).to eq(expected_increment)
+ expect(expected_increment).to be < initial_stat
+ expect(expected_increment).to be_negative
+ end
+ end
+
+ context 'when it is destroyed from the project level' do
+ it 'does not update the project statistics' do
+ expect(ProjectStatistics)
+ .not_to receive(:increment_statistic)
+
+ project.update!(pending_delete: true)
+ project.destroy!
+ end
+
+ it 'does not schedule a namespace statistics worker' do
+ expect(Namespaces::ScheduleAggregationWorker)
+ .not_to receive(:perform_async)
+
+ project.update!(pending_delete: true)
+ project.destroy!
+ end
+ end
end
end
end
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index b87f7fe97e1..62da9e15259 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -4,21 +4,99 @@ RSpec.shared_examples 'wiki model' do
let_it_be(:user) { create(:user, :commit_email) }
let(:wiki_container) { raise NotImplementedError }
let(:wiki_container_without_repo) { raise NotImplementedError }
+ let(:wiki_lfs_enabled) { false }
let(:wiki) { described_class.new(wiki_container, user) }
let(:commit) { subject.repository.head_commit }
subject { wiki }
+ it 'container class includes HasWiki' do
+ # NOTE: This is not enforced at runtime, since we also need to support Geo::DeletedProject
+ expect(wiki_container).to be_kind_of(HasWiki)
+ expect(wiki_container_without_repo).to be_kind_of(HasWiki)
+ end
+
it_behaves_like 'model with repository' do
let(:container) { wiki }
let(:stubbed_container) { described_class.new(wiki_container_without_repo, user) }
let(:expected_full_path) { "#{container.container.full_path}.wiki" }
let(:expected_web_url_path) { "#{container.container.web_url(only_path: true).sub(%r{^/}, '')}/-/wikis/home" }
+ let(:expected_lfs_enabled) { wiki_lfs_enabled }
+ end
+
+ describe '.container_class' do
+ it 'is set to the container class' do
+ expect(described_class.container_class).to eq(wiki_container.class)
+ end
+ end
+
+ describe '.find_by_id' do
+ it 'returns a wiki instance if the container is found' do
+ wiki = described_class.find_by_id(wiki_container.id)
+
+ expect(wiki).to be_a(described_class)
+ expect(wiki.container).to eq(wiki_container)
+ end
+
+ it 'returns nil if the container is not found' do
+ expect(described_class.find_by_id(-1)).to be_nil
+ end
+ end
+
+ describe '#initialize' do
+ it 'accepts a valid user' do
+ expect do
+ described_class.new(wiki_container, user)
+ end.not_to raise_error
+ end
+
+ it 'accepts a blank user' do
+ expect do
+ described_class.new(wiki_container, nil)
+ end.not_to raise_error
+ end
+
+ it 'raises an error for invalid users' do
+ expect do
+ described_class.new(wiki_container, Object.new)
+ end.to raise_error(ArgumentError, 'user must be a User, got Object')
+ end
+ end
+
+ describe '#run_after_commit' do
+ it 'delegates to the container' do
+ expect(wiki_container).to receive(:run_after_commit)
+
+ wiki.run_after_commit
+ end
+ end
+
+ describe '#==' do
+ it 'returns true for wikis from the same container' do
+ expect(wiki).to eq(described_class.new(wiki_container))
+ end
+
+ it 'returns false for wikis from different containers' do
+ expect(wiki).not_to eq(described_class.new(wiki_container_without_repo))
+ end
+ end
+
+ describe '#id' do
+ it 'returns the ID of the container' do
+ expect(wiki.id).to eq(wiki_container.id)
+ end
+ end
+
+ describe '#to_global_id' do
+ it 'returns a global ID' do
+ expect(wiki.to_global_id.to_s).to eq("gid://gitlab/#{wiki.class.name}/#{wiki.id}")
+ end
end
describe '#repository' do
it 'returns a wiki repository' do
expect(subject.repository.repo_type).to be_wiki
+ expect(subject.repository.container).to be(subject)
end
end
@@ -164,7 +242,7 @@ RSpec.shared_examples 'wiki model' do
def total_pages(entries)
entries.sum do |entry|
- entry.is_a?(WikiDirectory) ? entry.pages.size : 1
+ entry.is_a?(WikiDirectory) ? total_pages(entry.entries) : 1
end
end
@@ -204,8 +282,9 @@ RSpec.shared_examples 'wiki model' do
expect(page.title).to eq('index page')
end
- it 'returns nil if the page does not exist' do
- expect(subject.find_page('non-existent')).to eq(nil)
+ it 'returns nil if the page or version does not exist' do
+ expect(subject.find_page('non-existent')).to be_nil
+ expect(subject.find_page('index page', 'non-existent')).to be_nil
end
it 'can find a page by slug' do