summaryrefslogtreecommitdiff
path: root/spec/support/shared_examples/models
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support/shared_examples/models')
-rw-r--r--spec/support/shared_examples/models/boards/listable_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/models/chat_integration_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb355
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb104
-rw-r--r--spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/concerns/participable_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb118
-rw-r--r--spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb298
10 files changed, 673 insertions, 272 deletions
diff --git a/spec/support/shared_examples/models/boards/listable_shared_examples.rb b/spec/support/shared_examples/models/boards/listable_shared_examples.rb
index 250a4c1b1bd..ac8655a907f 100644
--- a/spec/support/shared_examples/models/boards/listable_shared_examples.rb
+++ b/spec/support/shared_examples/models/boards/listable_shared_examples.rb
@@ -27,14 +27,6 @@ RSpec.shared_examples 'boards listable model' do |list_factory|
.to eq([list1, list3, list4, list2])
end
end
-
- describe '.without_types' do
- it 'excludes lists of given types' do
- lists = described_class.without_types([:label, :closed])
-
- expect(lists).to match_array([list1])
- end
- end
end
describe '#destroyable?' do
diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
index 6cfeeabc952..6d0462a9ee8 100644
--- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
@@ -166,10 +166,14 @@ RSpec.shared_examples "chat integration" do |integration_name|
let(:opts) { { title: "Awesome issue", description: "please fix" } }
let(:sample_data) do
service = Issues::CreateService.new(project: project, current_user: user, params: opts, spam_params: nil)
- issue = service.execute
+ issue = service.execute[:issue]
service.hook_data(issue, "open")
end
+ before do
+ project.add_developer(user)
+ end
+
it_behaves_like "triggered #{integration_name} integration"
end
diff --git a/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb b/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb
new file mode 100644
index 00000000000..a4db4e25db3
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb
@@ -0,0 +1,355 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'a cascading namespace setting boolean attribute' do
+ |settings_attribute_name:, settings_association: :namespace_settings|
+ let_it_be_with_reload(:group) { create(:group) }
+ let_it_be_with_reload(:subgroup) { create(:group, parent: group) }
+ let(:group_settings) { group.send(settings_association) }
+ let(:subgroup_settings) { subgroup.send(settings_association) }
+
+ describe "##{settings_attribute_name}" do
+ subject(:cascading_attribute) { subgroup_settings.send(settings_attribute_name) }
+
+ before do
+ stub_application_setting(settings_attribute_name => false)
+ end
+
+ context 'when there is no parent' do
+ context 'and the value is not nil' do
+ before do
+ group_settings.update!(settings_attribute_name => true)
+ end
+
+ it 'returns the local value' do
+ expect(group_settings.send(settings_attribute_name)).to eq(true)
+ end
+ end
+
+ context 'and the value is nil' do
+ before do
+ group_settings.update!(settings_attribute_name => nil)
+ end
+
+ it 'returns the application settings value' do
+ expect(group_settings.send(settings_attribute_name)).to eq(false)
+ end
+ end
+ end
+
+ context 'when parent does not lock the attribute' do
+ context 'and value is not nil' do
+ before do
+ group_settings.update!(settings_attribute_name => false)
+ end
+
+ it 'returns local setting when present' do
+ subgroup_settings.update!(settings_attribute_name => true)
+
+ expect(cascading_attribute).to eq(true)
+ end
+
+ it 'returns the parent value when local value is nil' do
+ subgroup_settings.update!(settings_attribute_name => nil)
+
+ expect(cascading_attribute).to eq(false)
+ end
+
+ it 'returns the correct dirty value' do
+ subgroup_settings.send("#{settings_attribute_name}=", true)
+
+ expect(cascading_attribute).to eq(true)
+ end
+
+ it 'does not return the application setting value when parent value is false' do
+ stub_application_setting(settings_attribute_name => true)
+
+ expect(cascading_attribute).to eq(false)
+ end
+ end
+
+ context 'and the value is nil' do
+ before do
+ group_settings.update!(settings_attribute_name => nil, "lock_#{settings_attribute_name}".to_sym => false)
+ subgroup_settings.update!(settings_attribute_name => nil)
+
+ subgroup_settings.clear_memoization(settings_attribute_name)
+ end
+
+ it 'cascades to the application settings value' do
+ expect(cascading_attribute).to eq(false)
+ end
+ end
+
+ context 'when multiple ancestors set a value' do
+ let(:third_level_subgroup) { create(:group, parent: subgroup) }
+
+ before do
+ group_settings.update!(settings_attribute_name => true)
+ subgroup_settings.update!(settings_attribute_name => false)
+ end
+
+ it 'returns the closest ancestor value' do
+ expect(third_level_subgroup.send(settings_association).send(settings_attribute_name)).to eq(false)
+ end
+ end
+ end
+
+ context 'when parent locks the attribute' do
+ before do
+ subgroup_settings.update!(settings_attribute_name => true)
+ group_settings.update!("lock_#{settings_attribute_name}" => true, settings_attribute_name => false)
+
+ subgroup_settings.clear_memoization(settings_attribute_name)
+ subgroup_settings.clear_memoization("#{settings_attribute_name}_locked_ancestor")
+ end
+
+ it 'returns the parent value' do
+ expect(cascading_attribute).to eq(false)
+ end
+
+ it 'does not allow the local value to be saved' do
+ subgroup_settings.send("#{settings_attribute_name}=", nil)
+
+ expect { subgroup_settings.save! }
+ .to raise_error(ActiveRecord::RecordInvalid,
+ /cannot be changed because it is locked by an ancestor/)
+ end
+ end
+
+ context 'when the application settings locks the attribute' do
+ before do
+ subgroup_settings.update!(settings_attribute_name => true)
+ stub_application_setting("lock_#{settings_attribute_name}" => true, settings_attribute_name => true)
+ end
+
+ it 'returns the application setting value' do
+ expect(cascading_attribute).to eq(true)
+ end
+
+ it 'does not allow the local value to be saved' do
+ subgroup_settings.send("#{settings_attribute_name}=", false)
+
+ expect { subgroup_settings.save! }
+ .to raise_error(
+ ActiveRecord::RecordInvalid,
+ /cannot be changed because it is locked by an ancestor/
+ )
+ end
+ end
+
+ context 'when parent locked the attribute then the application settings locks it' do
+ before do
+ subgroup_settings.update!(settings_attribute_name => true)
+ group_settings.update!("lock_#{settings_attribute_name}" => true, settings_attribute_name => false)
+ stub_application_setting("lock_#{settings_attribute_name}" => true, settings_attribute_name => true)
+
+ subgroup_settings.clear_memoization(settings_attribute_name)
+ subgroup_settings.clear_memoization("#{settings_attribute_name}_locked_ancestor")
+ end
+
+ it 'returns the application setting value' do
+ expect(cascading_attribute).to eq(true)
+ end
+ end
+ end
+
+ describe "##{settings_attribute_name}?" do
+ before do
+ subgroup_settings.update!(settings_attribute_name => true)
+ group_settings.update!("lock_#{settings_attribute_name}" => true, settings_attribute_name => false)
+
+ subgroup_settings.clear_memoization(settings_attribute_name)
+ subgroup_settings.clear_memoization("#{settings_attribute_name}_locked_ancestor")
+ end
+
+ it 'aliases the method when the attribute is a boolean' do
+ expect(subgroup_settings.send("#{settings_attribute_name}?"))
+ .to eq(subgroup_settings.send(settings_attribute_name))
+ end
+ end
+
+ describe "##{settings_attribute_name}=" do
+ before do
+ subgroup_settings.update!(settings_attribute_name => nil)
+ group_settings.update!(settings_attribute_name => true)
+ end
+
+ it 'does not save the value locally when it matches the cascaded value' do
+ subgroup_settings.update!(settings_attribute_name => true)
+
+ expect(subgroup_settings.read_attribute(settings_attribute_name)).to eq(nil)
+ end
+ end
+
+ describe "##{settings_attribute_name}_locked?" do
+ shared_examples 'not locked' do
+ it 'is not locked by an ancestor' do
+ expect(subgroup_settings.send("#{settings_attribute_name}_locked_by_ancestor?")).to eq(false)
+ end
+
+ it 'is not locked by application setting' do
+ expect(subgroup_settings.send("#{settings_attribute_name}_locked_by_application_setting?")).to eq(false)
+ end
+
+ it 'does not return a locked namespace' do
+ expect(subgroup_settings.send("#{settings_attribute_name}_locked_ancestor")).to be_nil
+ end
+ end
+
+ context 'when attribute is locked by self' do
+ before do
+ subgroup_settings.update!("lock_#{settings_attribute_name}" => true)
+ end
+
+ it 'is not locked by default' do
+ expect(subgroup_settings.send("#{settings_attribute_name}_locked?")).to eq(false)
+ end
+
+ it 'is locked when including self' do
+ expect(subgroup_settings.send("#{settings_attribute_name}_locked?", include_self: true)).to eq(true)
+ end
+ end
+
+ context 'when parent does not lock the attribute' do
+ it_behaves_like 'not locked'
+ end
+
+ context 'when parent locks the attribute' do
+ before do
+ group_settings.update!("lock_#{settings_attribute_name}".to_sym => true, settings_attribute_name => false)
+
+ subgroup_settings.clear_memoization(settings_attribute_name)
+ subgroup_settings.clear_memoization("#{settings_attribute_name}_locked_ancestor")
+ end
+
+ it 'is locked by an ancestor' do
+ expect(subgroup_settings.send("#{settings_attribute_name}_locked_by_ancestor?")).to eq(true)
+ end
+
+ it 'is not locked by application setting' do
+ expect(subgroup_settings.send("#{settings_attribute_name}_locked_by_application_setting?")).to eq(false)
+ end
+
+ it 'returns a locked namespace settings object' do
+ expect(subgroup_settings.send("#{settings_attribute_name}_locked_ancestor").namespace_id)
+ .to eq(group_settings.namespace_id)
+ end
+ end
+
+ context 'when not locked by application settings' do
+ before do
+ stub_application_setting("lock_#{settings_attribute_name}" => false)
+ end
+
+ it_behaves_like 'not locked'
+ end
+
+ context 'when locked by application settings' do
+ before do
+ stub_application_setting("lock_#{settings_attribute_name}" => true)
+ end
+
+ it 'is not locked by an ancestor' do
+ expect(subgroup_settings.send("#{settings_attribute_name}_locked_by_ancestor?")).to eq(false)
+ end
+
+ it 'is locked by application setting' do
+ expect(subgroup_settings.send("#{settings_attribute_name}_locked_by_application_setting?")).to eq(true)
+ end
+
+ it 'does not return a locked namespace' do
+ expect(subgroup_settings.send("#{settings_attribute_name}_locked_ancestor")).to be_nil
+ end
+ end
+ end
+
+ describe "#lock_#{settings_attribute_name}=" do
+ context 'when parent locks the attribute' do
+ before do
+ group_settings.update!("lock_#{settings_attribute_name}".to_sym => true, settings_attribute_name => false)
+
+ subgroup_settings.clear_memoization(settings_attribute_name)
+ subgroup_settings.clear_memoization("#{settings_attribute_name}_locked_ancestor")
+ end
+
+ it 'does not allow the attribute to be saved' do
+ subgroup_settings.send("lock_#{settings_attribute_name}=", true)
+
+ expect { subgroup_settings.save! }
+ .to raise_error(ActiveRecord::RecordInvalid,
+ /cannot be changed because it is locked by an ancestor/)
+ end
+ end
+
+ context 'when parent does not lock the attribute' do
+ before do
+ group_settings.update!("lock_#{settings_attribute_name}" => false, settings_attribute_name => false)
+
+ subgroup_settings.send("lock_#{settings_attribute_name}=", true)
+ end
+
+ it 'allows the lock to be set when the attribute is not nil' do
+ subgroup_settings.send("#{settings_attribute_name}=", true)
+
+ expect(subgroup_settings.save).to eq(true)
+ end
+
+ it 'does not allow the lock to be saved when the attribute is nil' do
+ subgroup_settings.send("#{settings_attribute_name}=", nil)
+
+ expect { subgroup_settings.save! }
+ .to raise_error(ActiveRecord::RecordInvalid,
+ /cannot be nil when locking the attribute/)
+ end
+
+ it 'copies the cascaded value when locking the attribute if the local value is nil', :aggregate_failures do
+ subgroup_settings.send("#{settings_attribute_name}=", nil)
+ subgroup_settings.send("lock_#{settings_attribute_name}=", true)
+
+ expect(subgroup_settings.read_attribute(settings_attribute_name)).to eq(false)
+ end
+ end
+
+ context 'when application settings locks the attribute' do
+ before do
+ stub_application_setting("lock_#{settings_attribute_name}".to_sym => true)
+ end
+
+ it 'does not allow the attribute to be saved' do
+ subgroup_settings.send("lock_#{settings_attribute_name}=", true)
+
+ expect { subgroup_settings.save! }
+ .to raise_error(ActiveRecord::RecordInvalid,
+ /cannot be changed because it is locked by an ancestor/)
+ end
+ end
+
+ context 'when application_settings does not lock the attribute' do
+ before do
+ stub_application_setting("lock_#{settings_attribute_name}".to_sym => false)
+ end
+
+ it 'allows the attribute to be saved' do
+ subgroup_settings.send("#{settings_attribute_name}=", true)
+ subgroup_settings.send("lock_#{settings_attribute_name}=", true)
+
+ expect(subgroup_settings.save).to eq(true)
+ end
+ end
+ end
+
+ describe 'after update callback' do
+ before do
+ group_settings.update!("lock_#{settings_attribute_name}" => false, settings_attribute_name => false)
+ subgroup_settings.update!("lock_#{settings_attribute_name}" => true, settings_attribute_name => false)
+ end
+
+ it 'clears descendant locks' do
+ group_settings.update!("lock_#{settings_attribute_name}" => true, settings_attribute_name => true)
+
+ expect(subgroup_settings.reload.send("lock_#{settings_attribute_name}")).to eq(false)
+ end
+ end
+end
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
index f3a12578912..a658d02f09a 100644
--- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
@@ -92,8 +92,8 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
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
+ .with(model.counter_lock_key(incremented_attribute), ttl: described_class::WORKER_LOCK_TTL)
+ .and_call_original
subject
end
@@ -104,7 +104,14 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
model.delayed_increment_counter(incremented_attribute, -3)
end
- it 'updates the record and logs it' do
+ it 'updates the record and logs it', :aggregate_failures do
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ hash_including(
+ message: 'Acquiring lease for project statistics update',
+ attributes: [incremented_attribute]
+ )
+ )
+
expect(Gitlab::AppLogger).to receive(:info).with(
hash_including(
message: 'Flush counter attribute to database',
@@ -124,14 +131,14 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
it 'removes the increment entry from Redis' do
Gitlab::Redis::SharedState.with do |redis|
- key_exists = redis.exists(model.counter_key(incremented_attribute))
+ 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))
+ key_exists = redis.exists?(model.counter_key(incremented_attribute))
expect(key_exists).to be_falsey
end
end
@@ -162,7 +169,7 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
subject
Gitlab::Redis::SharedState.with do |redis|
- key_exists = redis.exists(model.counter_flushed_key(incremented_attribute))
+ key_exists = redis.exists?(model.counter_flushed_key(incremented_attribute))
expect(key_exists).to be_falsey
end
end
@@ -186,31 +193,88 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
end
end
- describe '#clear_counter!' do
+ describe '#reset_counter!' do
let(:attribute) { counter_attributes.first }
before do
+ model.update!(attribute => 123)
model.increment_counter(attribute, 10)
end
- it 'deletes the counter key for the given attribute and logs it' do
- expect(Gitlab::AppLogger).to receive(:info).with(
- hash_including(
- message: 'Clear counter attribute',
- attribute: attribute,
- project_id: model.project_id,
- 'correlation_id' => an_instance_of(String),
- 'meta.feature_category' => 'test',
- 'meta.caller_id' => 'caller'
- )
- )
+ subject { model.reset_counter!(attribute) }
- model.clear_counter!(attribute)
+ it 'resets the attribute value to 0 and clears existing counter', :aggregate_failures do
+ expect { subject }.to change { model.reload.send(attribute) }.from(123).to(0)
Gitlab::Redis::SharedState.with do |redis|
- key_exists = redis.exists(model.counter_key(attribute))
+ key_exists = redis.exists?(model.counter_key(attribute))
expect(key_exists).to be_falsey
end
end
+
+ it_behaves_like 'obtaining lease to update database' do
+ context 'when the execution raises error' do
+ before do
+ allow(model).to receive(:update!).and_raise(StandardError, 'Something went wrong')
+ end
+
+ it 'reraises error' do
+ expect { subject }.to raise_error(StandardError, 'Something went wrong')
+ end
+ end
+ end
+ end
+
+ describe '#update_counters_with_lease' do
+ let(:increments) { { build_artifacts_size: 1, packages_size: 2 } }
+
+ subject { model.update_counters_with_lease(increments) }
+
+ it 'updates counters of the record' do
+ expect { subject }
+ .to change { model.reload.build_artifacts_size }.by(1)
+ .and change { model.reload.packages_size }.by(2)
+ end
+
+ it_behaves_like 'obtaining lease to update database' do
+ context 'when the execution raises error' do
+ before do
+ allow(model.class).to receive(:update_counters).and_raise(StandardError, 'Something went wrong')
+ end
+
+ it 'reraises error' do
+ expect { subject }.to raise_error(StandardError, 'Something went wrong')
+ end
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'obtaining lease to update database' do
+ context 'when it is unable to obtain lock' do
+ before do
+ allow(model).to receive(:in_lock).and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
+ end
+
+ it 'logs a warning' do
+ allow(model).to receive(:in_lock).and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
+
+ expect(Gitlab::AppLogger).to receive(:warn).once
+
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context 'when feature flag counter_attribute_db_lease_for_update is disabled' do
+ before do
+ stub_feature_flags(counter_attribute_db_lease_for_update: false)
+ allow(model).to receive(:in_lock).and_call_original
+ end
+
+ it 'does not attempt to get a lock' do
+ expect(model).not_to receive(:in_lock)
+
+ subject
+ end
end
end
diff --git a/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb
index 0357b7462fb..65bc6c10490 100644
--- a/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb
@@ -15,7 +15,7 @@ RSpec.shared_examples 'model with wiki' do
context 'when the repository cannot be created' do
before do
- expect(container.wiki).to receive(:wiki) { raise Wiki::CouldNotCreateWikiError }
+ expect(container.wiki).to receive(:create_wiki_repository) { raise Wiki::CouldNotCreateWikiError }
end
it 'returns false and adds a validation error' do
diff --git a/spec/support/shared_examples/models/concerns/participable_shared_examples.rb b/spec/support/shared_examples/models/concerns/participable_shared_examples.rb
new file mode 100644
index 00000000000..ec7a9105bb2
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/participable_shared_examples.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'visible participants for issuable with read ability' do |model_class|
+ let_it_be(:user1) { create(:user) }
+
+ let(:model) { model_class.to_s.classify.constantize }
+
+ before do
+ allow(Ability).to receive(:allowed?).with(anything, :"read_#{model_class}", anything).and_return(true)
+ allow(model).to receive(:participant_attrs).and_return([:bar])
+ end
+
+ shared_examples 'check for participables read ability' do |ability_name|
+ it 'receives expected ability' do
+ instance = model.new
+
+ allow(instance).to receive(:bar).and_return(participable_source)
+
+ expect(Ability).to receive(:allowed?).with(anything, ability_name, instance)
+
+ expect(instance.visible_participants(user1)).to be_empty
+ end
+ end
+
+ context 'when source is an award emoji' do
+ let(:participable_source) { build(:award_emoji, :upvote) }
+
+ it_behaves_like 'check for participables read ability', :read_issuable_participables
+ end
+
+ context 'when source is a note' do
+ let(:participable_source) { build(:note) }
+
+ it_behaves_like 'check for participables read ability', :read_note
+ end
+
+ context 'when source is an internal note' do
+ let(:participable_source) { build(:note, :confidential) }
+
+ it_behaves_like 'check for participables read ability', :read_internal_note
+ 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 d06e8391a9a..e4958779957 100644
--- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
@@ -1,10 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'a timebox' do |timebox_type|
- let(:project) { create(:project, :public) }
- let(:group) { create(:group) }
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 }
@@ -14,28 +11,6 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
let(:open_on_left) { nil }
let(:open_on_right) { nil }
- describe 'modules' do
- context 'with a project' do
- it_behaves_like 'AtomicInternalId' do
- let(:internal_id_attribute) { :iid }
- let(:instance) { build(timebox_type, *timebox_args, project: create(:project), group: nil) }
- let(:scope) { :project }
- let(:scope_attrs) { { project: instance.project } }
- let(:usage) { timebox_table_name }
- end
- end
-
- context 'with a group' do
- it_behaves_like 'AtomicInternalId' do
- let(:internal_id_attribute) { :iid }
- let(:instance) { build(timebox_type, *timebox_args, project: nil, group: create(:group)) }
- let(:scope) { :group }
- let(:scope_attrs) { { namespace: instance.group } }
- let(:usage) { timebox_table_name }
- end
- end
- end
-
describe "Validation" do
before do
allow(subject).to receive(:set_iid).and_return(false)
@@ -65,21 +40,9 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
expect(timebox.errors[:due_date]).to include("date must not be after 9999-12-31")
end
end
-
- describe '#timebox_type_check' do
- it 'is invalid if it has both project_id and group_id' do
- timebox = build(timebox_type, *timebox_args, group: group)
- timebox.project = project
-
- expect(timebox).not_to be_valid
- expect(timebox.errors[:project_id]).to include("#{timebox_type} should belong either to a project or a group.")
- end
- end
end
describe "Associations" do
- it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:group) }
it { is_expected.to have_many(:issues) }
it { is_expected.to have_many(:merge_requests) }
it { is_expected.to have_many(:labels).through(:issues) }
@@ -91,38 +54,6 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
end
end
- describe '#project_timebox?' do
- context 'when project_id is present' do
- it 'returns true' do
- expect(timebox.project_timebox?).to be_truthy
- end
- end
-
- context 'when project_id is not present' do
- let(:timebox) { build(timebox_type, *timebox_args, group: group) }
-
- it 'returns false' do
- expect(timebox.project_timebox?).to be_falsey
- end
- end
- end
-
- describe '#group_timebox?' do
- context 'when group_id is present' do
- let(:timebox) { build(timebox_type, *timebox_args, group: group) }
-
- it 'returns true' do
- expect(timebox.group_timebox?).to be_truthy
- end
- end
-
- context 'when group_id is not present' do
- it 'returns false' do
- expect(timebox.group_timebox?).to be_falsey
- end
- end
- end
-
describe '#safe_title' do
let(:timebox) { create(timebox_type, *timebox_args, title: "<b>foo & bar -> 2.2</b>") }
@@ -131,22 +62,6 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
end
end
- describe '#resource_parent' do
- context 'when group is present' do
- let(:timebox) { build(timebox_type, *timebox_args, group: group) }
-
- it 'returns the group' do
- expect(timebox.resource_parent).to eq(group)
- end
- end
-
- context 'when project is present' do
- it 'returns the project' do
- expect(timebox.resource_parent).to eq(project)
- end
- end
- end
-
describe "#title" do
let(:timebox) { create(timebox_type, *timebox_args, title: "<b>foo & bar -> 2.2</b>") }
@@ -155,39 +70,6 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
end
end
- describe '#merge_requests_enabled?' do
- context "per project" do
- it "is true for projects with MRs enabled" do
- project = create(:project, :merge_requests_enabled)
- 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, *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, *timebox_args, project: project)
-
- expect(timebox.merge_requests_enabled?).to be_falsey
- end
- end
-
- context "per group" do
- 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
- end
- end
- end
-
describe '#to_ability_name' do
it 'returns timebox' do
timebox = build(timebox_type, *timebox_args)
diff --git a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
index e309aa50c6e..31ec25249d7 100644
--- a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
+++ b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
@@ -4,7 +4,7 @@ RSpec.shared_examples Integrations::HasWebHook do
include AfterNextHelpers
describe 'associations' do
- it { is_expected.to have_one(:service_hook).inverse_of(:integration).with_foreign_key(:service_id) }
+ it { is_expected.to have_one(:service_hook).inverse_of(:integration).with_foreign_key(:integration_id) }
end
describe 'callbacks' do
diff --git a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
index c92e819db19..3caf58da4d2 100644
--- a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
+++ b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
@@ -5,12 +5,14 @@ RSpec.shared_examples 'ci_cd_settings delegation' do
context 'when ci_cd_settings is destroyed but project is not' do
it 'allows methods delegated to ci_cd_settings to be nil', :aggregate_failures do
- project = create(:project)
attributes = project.ci_cd_settings.attributes.keys - %w(id project_id) - exclude_attributes
+
+ expect(attributes).to match_array(attributes_with_prefix.keys)
+
project.ci_cd_settings.destroy!
project.reload
- attributes.each do |attr|
- method = project.respond_to?("ci_#{attr}") ? "ci_#{attr}" : attr
+ attributes_with_prefix.each do |attr, prefix|
+ method = "#{prefix}#{attr}"
expect(project.send(method)).to be_nil, "#{attr} was not nil"
end
end
@@ -20,8 +22,6 @@ end
RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''|
using RSpec::Parameterized::TableSyntax
- let_it_be(:project) { create(:project) }
-
context 'when ci_cd_settings is nil' do
before do
allow(project).to receive(:ci_cd_settings).and_return(nil)
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 5f6a10bd754..b1aa90449e1 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
RSpec.shared_examples 'wiki model' do
+ using RSpec::Parameterized::TableSyntax
+
let_it_be(:user) { create(:user, :commit_email) }
let(:wiki_container) { raise NotImplementedError }
@@ -124,36 +126,6 @@ RSpec.shared_examples 'wiki model' do
end
end
- describe '#wiki' do
- it 'contains a Gitlab::Git::Wiki instance' do
- expect(subject.wiki).to be_a Gitlab::Git::Wiki
- end
-
- it 'creates a new wiki repo if one does not yet exist' do
- expect(subject.create_page('index', 'test content')).to be_truthy
- end
-
- it 'creates a new wiki repo with a default commit message' do
- expect(subject.create_page('index', 'test content', :markdown, '')).to be_truthy
-
- page = subject.find_page('index')
-
- expect(page.last_version.message).to eq("#{user.username} created page: index")
- end
-
- context 'when the repository cannot be created' do
- let(:wiki_container) { wiki_container_without_repo }
-
- before do
- expect(subject.repository).to receive(:create_if_not_exists) { false }
- end
-
- it 'raises CouldNotCreateWikiError' do
- expect { subject.wiki }.to raise_exception(Wiki::CouldNotCreateWikiError)
- end
- end
- end
-
describe '#empty?' do
context 'when the wiki repository is empty' do
it 'returns true' do
@@ -180,70 +152,71 @@ RSpec.shared_examples 'wiki model' do
it 'returns false' do
expect(subject.empty?).to be(false)
end
-
- it 'only instantiates a Wiki page once' do
- expect(WikiPage).to receive(:new).once.and_call_original
-
- subject.empty?
- end
end
end
end
describe '#list_pages' do
- let(:wiki_pages) { subject.list_pages }
+ shared_examples 'wiki model #list_pages' do
+ let(:wiki_pages) { subject.list_pages }
- before do
- subject.create_page('index', 'This is an index')
- subject.create_page('index2', 'This is an index2')
- subject.create_page('an index3', 'This is an index3')
- end
+ before do
+ subject.create_page('index', 'This is an index')
+ subject.create_page('index2', 'This is an index2')
+ subject.create_page('an index3', 'This is an index3')
+ end
- it 'returns an array of WikiPage instances' do
- expect(wiki_pages).to be_present
- expect(wiki_pages).to all(be_a(WikiPage))
- end
+ it 'returns an array of WikiPage instances' do
+ expect(wiki_pages).to be_present
+ expect(wiki_pages).to all(be_a(WikiPage))
+ end
- it 'does not load WikiPage content by default' do
- wiki_pages.each do |page|
- expect(page.content).to be_empty
+ it 'does not load WikiPage content by default' do
+ wiki_pages.each do |page|
+ expect(page.content).to be_empty
+ end
end
- end
- it 'returns all pages by default' do
- expect(wiki_pages.count).to eq(3)
- end
+ it 'returns all pages by default' do
+ expect(wiki_pages.count).to eq(3)
+ end
- context 'with limit option' do
- it 'returns limited set of pages' do
- expect(subject.list_pages(limit: 1).count).to eq(1)
+ context 'with limit option' do
+ it 'returns limited set of pages' do
+ expect(subject.list_pages(limit: 1).count).to eq(1)
+ end
end
- end
- context 'with sorting options' do
- it 'returns pages sorted by title by default' do
- pages = ['an index3', 'index', 'index2']
+ context 'with sorting options' do
+ it 'returns pages sorted by title by default' do
+ pages = ['an index3', 'index', 'index2']
- expect(subject.list_pages.map(&:title)).to eq(pages)
- expect(subject.list_pages(direction: 'desc').map(&:title)).to eq(pages.reverse)
+ expect(subject.list_pages.map(&:title)).to eq(pages)
+ expect(subject.list_pages(direction: 'desc').map(&:title)).to eq(pages.reverse)
+ end
end
- it 'returns pages sorted by created_at' do
- pages = ['index', 'index2', 'an index3']
+ context 'with load_content option' do
+ let(:pages) { subject.list_pages(load_content: true) }
- expect(subject.list_pages(sort: 'created_at').map(&:title)).to eq(pages)
- expect(subject.list_pages(sort: 'created_at', direction: 'desc').map(&:title)).to eq(pages.reverse)
+ it 'loads WikiPage content' do
+ expect(pages.first.content).to eq('This is an index3')
+ expect(pages.second.content).to eq('This is an index')
+ expect(pages.third.content).to eq('This is an index2')
+ end
end
end
- context 'with load_content option' do
- let(:pages) { subject.list_pages(load_content: true) }
-
- it 'loads WikiPage content' do
- expect(pages.first.content).to eq('This is an index3')
- expect(pages.second.content).to eq('This is an index')
- expect(pages.third.content).to eq('This is an index2')
+ context 'list pages with legacy wiki rpcs' do
+ before do
+ stub_feature_flags(wiki_list_page_with_normal_repository_rpcs: false)
end
+
+ it_behaves_like 'wiki model #list_pages'
+ end
+
+ context 'list pages with normal repository rpcs' do
+ it_behaves_like 'wiki model #list_pages'
end
end
@@ -338,6 +311,74 @@ RSpec.shared_examples 'wiki model' do
end
end
+ context "wiki repository's default branch is updated" do
+ before do
+ old_default_branch = wiki.default_branch
+ subject.create_page('page in updated default branch', 'content')
+ subject.repository.add_branch(user, 'another_branch', old_default_branch)
+ subject.repository.rm_branch(user, old_default_branch)
+ subject.repository.expire_status_cache
+ end
+
+ it 'returns the page in the updated default branch' do
+ wiki = described_class.new(wiki_container, user)
+ page = wiki.find_page('page in updated default branch')
+
+ expect(wiki.default_branch).to eql('another_branch')
+ expect(page.title).to eq('page in updated default branch')
+ end
+ end
+
+ context "wiki repository's HEAD is updated" do
+ before do
+ subject.create_page('page in updated HEAD', 'content')
+ subject.repository.add_branch(user, 'another_branch', subject.default_branch)
+ subject.repository.change_head('another_branch')
+ subject.repository.expire_status_cache
+ end
+
+ it 'returns the page in the new HEAD' do
+ wiki = described_class.new(wiki_container, user)
+ page = subject.find_page('page in updated HEAD')
+
+ expect(wiki.default_branch).to eql('another_branch')
+ expect(page.title).to eq('page in updated HEAD')
+ end
+ end
+
+ context 'pages with relative paths' do
+ where(:path, :title) do
+ [
+ ['~hello.md', '~Hello'],
+ ['hello~world.md', 'Hello~World'],
+ ['~~~hello.md', '~~~Hello'],
+ ['~/hello.md', '~/Hello'],
+ ['hello.md', '/Hello'],
+ ['hello.md', '../Hello'],
+ ['hello.md', './Hello'],
+ ['dir/hello.md', '/dir/Hello']
+ ]
+ end
+
+ with_them do
+ before do
+ wiki.repository.create_file(
+ user, path, "content of wiki file",
+ branch_name: wiki.default_branch,
+ message: "created page #{path}",
+ author_email: user.email,
+ author_name: user.name
+ )
+ end
+
+ it "can find page with `#{params[:title]}` title" do
+ page = subject.find_page(title)
+
+ expect(page.content).to eq("content of wiki file")
+ end
+ end
+ end
+
context 'pages with different file extensions' do
where(:extension, :path, :title) do
[
@@ -378,14 +419,6 @@ RSpec.shared_examples 'wiki model' do
end
end
- context 'find page with legacy wiki service' do
- before do
- stub_feature_flags(wiki_find_page_with_normal_repository_rpcs: false)
- end
-
- it_behaves_like 'wiki model #find_page'
- end
-
context 'find page with normal repository RPCs' do
it_behaves_like 'wiki model #find_page'
end
@@ -404,14 +437,6 @@ RSpec.shared_examples 'wiki model' do
end
end
- context 'find sidebar with legacy wiki service' do
- before do
- stub_feature_flags(wiki_find_page_with_normal_repository_rpcs: false)
- end
-
- it_behaves_like 'wiki model #find_sidebar'
- end
-
context 'find sidebar with normal repository RPCs' do
it_behaves_like 'wiki model #find_sidebar'
end
@@ -421,7 +446,7 @@ RSpec.shared_examples 'wiki model' do
let(:image) { File.open(Rails.root.join('spec', 'fixtures', 'big-image.png')) }
before do
- subject.wiki # Make sure the wiki repo exists
+ subject.create_wiki_repository # Make sure the wiki repo exists
subject.repository.create_file(user, 'image.png', image, branch_name: subject.default_branch, message: 'add image')
end
@@ -456,6 +481,22 @@ RSpec.shared_examples 'wiki model' do
expect(file.raw_data).to be_empty
end
end
+
+ context "wiki repository's default branch is updated" do
+ before do
+ old_default_branch = wiki.default_branch
+ subject.repository.add_branch(user, 'another_branch', old_default_branch)
+ subject.repository.rm_branch(user, old_default_branch)
+ subject.repository.expire_status_cache
+ end
+
+ it 'returns the page in the updated default branch' do
+ wiki = described_class.new(wiki_container, user)
+ file = wiki.find_file('image.png')
+
+ expect(file.mime_type).to eq('image/png')
+ end
+ end
end
describe '#create_page' do
@@ -480,7 +521,7 @@ RSpec.shared_examples 'wiki model' do
it 'sets the correct commit message' do
subject.create_page('test page', 'some content', :markdown, 'commit message')
- expect(subject.list_pages.first.page.version.message).to eq('commit message')
+ expect(subject.list_pages.first.version.message).to eq('commit message')
end
it 'sets the correct commit email' do
@@ -577,6 +618,8 @@ RSpec.shared_examples 'wiki model' do
'foo' | :org | ['foo.md'] | false
'foo' | :markdown | ['dir/foo.md'] | true
'/foo' | :markdown | ['foo.md'] | false
+ '~foo' | :markdown | [] | true
+ '~~~foo' | :markdown | [] | true
'./foo' | :markdown | ['foo.md'] | false
'../foo' | :markdown | ['foo.md'] | false
'../../foo' | :markdown | ['foo.md'] | false
@@ -607,14 +650,6 @@ RSpec.shared_examples 'wiki model' do
end
it_behaves_like 'create_page tests'
-
- context 'create page with legacy find_page wiki service' do
- it_behaves_like 'create_page tests' do
- before do
- stub_feature_flags(wiki_find_page_with_normal_repository_rpcs: false)
- end
- end
- end
end
describe '#update_page' do
@@ -687,6 +722,8 @@ RSpec.shared_examples 'wiki model' do
using RSpec::Parameterized::TableSyntax
where(:original_title, :original_format, :updated_title, :updated_format, :expected_title, :expected_path) do
+ 'test page' | :markdown | '~new test page' | :asciidoc | '~new test page' | '~new-test-page.asciidoc'
+ 'test page' | :markdown | '~~~new test page' | :asciidoc | '~~~new test page' | '~~~new-test-page.asciidoc'
'test page' | :markdown | 'new test page' | :asciidoc | 'new test page' | 'new-test-page.asciidoc'
'test page' | :markdown | 'new dir/new test page' | :asciidoc | 'new dir/new test page' | 'new-dir/new-test-page.asciidoc'
'test dir/test page' | :markdown | 'new dir/new test page' | :asciidoc | 'new dir/new test page' | 'new-dir/new-test-page.asciidoc'
@@ -696,13 +733,13 @@ RSpec.shared_examples 'wiki model' do
'test dir/test page' | :markdown | nil | :markdown | 'test dir/test page' | 'test-dir/test-page.md'
'test page' | :markdown | '' | :markdown | 'test page' | 'test-page.md'
'test.page' | :markdown | '' | :markdown | 'test.page' | 'test.page.md'
- 'testpage' | :markdown | '../testpage' | :markdown | 'testpage' | 'testpage.md'
- 'dir/testpage' | :markdown | 'dir/../testpage' | :markdown | 'testpage' | 'testpage.md'
- 'dir/testpage' | :markdown | './dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md'
- 'dir/testpage' | :markdown | '../dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md'
- 'dir/testpage' | :markdown | '../dir/../testpage' | :markdown | 'testpage' | 'testpage.md'
- 'dir/testpage' | :markdown | '../dir/../dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md'
- 'dir/testpage' | :markdown | '../dir/../another/testpage' | :markdown | 'another/testpage' | 'another/testpage.md'
+ 'testpage' | :markdown | '../testpage' | :markdown | 'testpage' | 'testpage.md'
+ 'dir/testpage' | :markdown | 'dir/../testpage' | :markdown | 'testpage' | 'testpage.md'
+ 'dir/testpage' | :markdown | './dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md'
+ 'dir/testpage' | :markdown | '../dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md'
+ 'dir/testpage' | :markdown | '../dir/../testpage' | :markdown | 'testpage' | 'testpage.md'
+ 'dir/testpage' | :markdown | '../dir/../dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md'
+ 'dir/testpage' | :markdown | '../dir/../another/testpage' | :markdown | 'another/testpage' | 'another/testpage.md'
end
end
@@ -711,17 +748,6 @@ RSpec.shared_examples 'wiki model' do
include_context 'extended examples'
end
- context 'update page with legacy find_page wiki service' do
- it_behaves_like 'update_page tests' do
- before do
- stub_feature_flags(wiki_find_page_with_normal_repository_rpcs: false)
- end
-
- include_context 'common examples'
- include_context 'extended examples'
- end
- end
-
context 'when format is invalid' do
let!(:page) { create(:wiki_page, wiki: subject, title: 'test page') }
@@ -862,7 +888,7 @@ RSpec.shared_examples 'wiki model' do
end
describe '#create_wiki_repository' do
- let(:head_path) { Rails.root.join(TestEnv.repos_path, "#{wiki.disk_path}.git", 'HEAD') }
+ let(:head_path) { Gitlab::GitalyClient::StorageSettings.allow_disk_access { Rails.root.join(TestEnv.repos_path, "#{wiki.disk_path}.git", 'HEAD') } }
let(:default_branch) { 'foo' }
before do
@@ -895,4 +921,40 @@ RSpec.shared_examples 'wiki model' do
end
end
end
+
+ describe '#preview_slug' do
+ where(:title, :file_extension, :format, :expected_slug) do
+ 'The Best Thing' | :md | :markdown | 'The-Best-Thing'
+ 'The Best Thing' | :txt | :plaintext | 'The-Best-Thing'
+ 'A Subject/Title Here' | :txt | :plaintext | 'A-Subject/Title-Here'
+ 'A subject' | :txt | :plaintext | 'A-subject'
+ 'A 1/B 2/C 3' | :txt | :plaintext | 'A-1/B-2/C-3'
+ 'subject/title' | :txt | :plaintext | 'subject/title'
+ 'subject/title.md' | :txt | :plaintext | 'subject/title.md'
+ 'foo%2Fbar' | :txt | :plaintext | 'foo%2Fbar'
+ '' | :md | :markdown | '.md'
+ '' | :txt | :plaintext | '.txt'
+ end
+
+ with_them do
+ before do
+ subject.repository.create_file(
+ user, "#{title}.#{file_extension}", 'content',
+ branch_name: subject.default_branch,
+ message: "Add #{title}"
+ )
+ end
+
+ it do
+ expect(described_class.preview_slug(title, file_extension)).to eq(expected_slug)
+ end
+
+ it 'matches the slug generated by gitaly' do
+ skip('Gitaly cannot generate a slug for an empty title') unless title.present?
+
+ gitaly_slug = subject.list_pages.first.slug
+ expect(described_class.preview_slug(title, file_extension)).to eq(gitaly_slug)
+ end
+ end
+ end
end