diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
commit | 48aff82709769b098321c738f3444b9bdaa694c6 (patch) | |
tree | e00c7c43e2d9b603a5a6af576b1685e400410dee /spec/support/shared_examples/models | |
parent | 879f5329ee916a948223f8f43d77fba4da6cd028 (diff) | |
download | gitlab-ce-48aff82709769b098321c738f3444b9bdaa694c6.tar.gz |
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'spec/support/shared_examples/models')
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 |