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/chat_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/models/cluster_application_status_shared_examples.rb152
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb176
-rw-r--r--spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb41
-rw-r--r--spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/models/relative_positioning_shared_examples.rb602
-rw-r--r--spec/support/shared_examples/models/resource_event_shared_examples.rb155
-rw-r--r--spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb75
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb24
12 files changed, 1049 insertions, 276 deletions
diff --git a/spec/support/shared_examples/models/chat_service_shared_examples.rb b/spec/support/shared_examples/models/chat_service_shared_examples.rb
index 0a1c27b32db..ad237ad9f49 100644
--- a/spec/support/shared_examples/models/chat_service_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_service_shared_examples.rb
@@ -198,6 +198,7 @@ RSpec.shared_examples "chat service" do |service_name|
message: "user created page: Awesome wiki_page"
}
end
+
let(:wiki_page) { create(:wiki_page, wiki: project.wiki, **opts) }
let(:sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, "create") }
@@ -250,6 +251,7 @@ RSpec.shared_examples "chat service" do |service_name|
project: project, status: status,
sha: project.commit.sha, ref: project.default_branch)
end
+
let(:sample_data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
context "with failed pipeline" do
diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb
index 239588d3b2f..394253fb699 100644
--- a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb
@@ -28,46 +28,16 @@ RSpec.shared_examples 'cluster application helm specs' do |application_name|
describe '#files' do
subject { application.files }
- context 'managed_apps_local_tiller feature flag is disabled' do
- before do
- stub_feature_flags(managed_apps_local_tiller: false)
- end
-
- context 'when the helm application does not have a ca_cert' do
- before do
- application.cluster.application_helm.ca_cert = nil
- end
-
- it 'does not include cert files when there is no ca_cert entry' do
- expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem')
- end
- end
-
- it 'includes cert files when there is a ca_cert entry' do
- expect(subject).to include(:'ca.pem', :'cert.pem', :'key.pem')
- expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
-
- cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
- expect(cert.not_after).to be < 60.minutes.from_now
- end
+ it 'does not include cert files' do
+ expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem')
end
- context 'managed_apps_local_tiller feature flag is enabled' do
- before do
- stub_feature_flags(managed_apps_local_tiller: application.cluster.clusterable)
- end
+ context 'when cluster does not have helm installed' do
+ let(:application) { create(application_name, :no_helm_installed) }
it 'does not include cert files' do
expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem')
end
-
- context 'when cluster does not have helm installed' do
- let(:application) { create(application_name, :no_helm_installed) }
-
- it 'does not include cert files' do
- expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem')
- end
- end
end
end
end
diff --git a/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb
index 7f0c60d4204..55e458db512 100644
--- a/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb
@@ -6,46 +6,8 @@ RSpec.shared_examples 'cluster application initial status specs' do
subject { described_class.new(cluster: cluster) }
- context 'local tiller feature flag is disabled' do
- before do
- stub_feature_flags(managed_apps_local_tiller: false)
- end
-
- it 'sets a default status' do
- expect(subject.status_name).to be(:not_installable)
- end
- end
-
- context 'local tiller feature flag is enabled' do
- before do
- stub_feature_flags(managed_apps_local_tiller: cluster.clusterable)
- end
-
- it 'sets a default status' do
- expect(subject.status_name).to be(:installable)
- end
- end
-
- context 'when application helm is scheduled' do
- before do
- stub_feature_flags(managed_apps_local_tiller: false)
-
- create(:clusters_applications_helm, :scheduled, cluster: cluster)
- end
-
- it 'defaults to :not_installable' do
- expect(subject.status_name).to be(:not_installable)
- end
- end
-
- context 'when application helm is installed' do
- before do
- create(:clusters_applications_helm, :installed, cluster: cluster)
- end
-
- it 'sets a default status' do
- expect(subject.status_name).to be(:installable)
- end
+ it 'sets a default status' do
+ expect(subject.status_name).to be(:installable)
end
end
end
diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
index f80ca235220..7603787a54e 100644
--- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
@@ -48,43 +48,21 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
expect(subject).to be_installed
end
- context 'managed_apps_local_tiller feature flag disabled' do
- before do
- stub_feature_flags(managed_apps_local_tiller: false)
- end
-
- it 'updates helm version' do
- subject.cluster.application_helm.update!(version: '1.2.3')
+ it 'does not update the helm version' do
+ subject.cluster.application_helm.update!(version: '1.2.3')
+ expect do
subject.make_installed!
subject.cluster.application_helm.reload
-
- expect(subject.cluster.application_helm.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION)
- end
+ end.not_to change { subject.cluster.application_helm.version }
end
- context 'managed_apps_local_tiller feature flag enabled' do
- before do
- stub_feature_flags(managed_apps_local_tiller: subject.cluster.clusterable)
- end
-
- it 'does not update the helm version' do
- subject.cluster.application_helm.update!(version: '1.2.3')
-
- expect do
- subject.make_installed!
-
- subject.cluster.application_helm.reload
- end.not_to change { subject.cluster.application_helm.version }
- end
-
- context 'the cluster has no helm installed' do
- subject { create(application_name, :installing, :no_helm_installed) }
+ context 'the cluster has no helm installed' do
+ subject { create(application_name, :installing, :no_helm_installed) }
- it 'runs without errors' do
- expect { subject.make_installed! }.not_to raise_error
- end
+ it 'runs without errors' do
+ expect { subject.make_installed! }.not_to raise_error
end
end
@@ -97,43 +75,21 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
expect(subject).to be_updated
end
- context 'managed_apps_local_tiller feature flag disabled' do
- before do
- stub_feature_flags(managed_apps_local_tiller: false)
- end
-
- it 'updates helm version' do
- subject.cluster.application_helm.update!(version: '1.2.3')
+ it 'does not update the helm version' do
+ subject.cluster.application_helm.update!(version: '1.2.3')
+ expect do
subject.make_installed!
subject.cluster.application_helm.reload
-
- expect(subject.cluster.application_helm.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION)
- end
+ end.not_to change { subject.cluster.application_helm.version }
end
- context 'managed_apps_local_tiller feature flag enabled' do
- before do
- stub_feature_flags(managed_apps_local_tiller: true)
- end
-
- it 'does not update the helm version' do
- subject.cluster.application_helm.update!(version: '1.2.3')
-
- expect do
- subject.make_installed!
-
- subject.cluster.application_helm.reload
- end.not_to change { subject.cluster.application_helm.version }
- end
-
- context 'the cluster has no helm installed' do
- subject { create(application_name, :updating, :no_helm_installed) }
+ context 'the cluster has no helm installed' do
+ subject { create(application_name, :updating, :no_helm_installed) }
- it 'runs without errors' do
- expect { subject.make_installed! }.not_to raise_error
- end
+ it 'runs without errors' do
+ expect { subject.make_installed! }.not_to raise_error
end
end
end
@@ -185,62 +141,26 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
expect(subject).to be_installed
end
- context 'local tiller flag enabled' do
- before do
- stub_feature_flags(managed_apps_local_tiller: true)
- end
-
- context 'helm record does not exist' do
- subject { build(application_name, :installing, :no_helm_installed) }
-
- it 'does not create a helm record' do
- subject.make_externally_installed!
-
- subject.cluster.reload
- expect(subject.cluster.application_helm).to be_nil
- end
- end
-
- context 'helm record exists' do
- subject { build(application_name, :installing, cluster: old_helm.cluster) }
+ context 'helm record does not exist' do
+ subject { build(application_name, :installing, :no_helm_installed) }
- it 'does not update helm version' do
- subject.make_externally_installed!
+ it 'does not create a helm record' do
+ subject.make_externally_installed!
- subject.cluster.application_helm.reload
-
- expect(subject.cluster.application_helm.version).to eq('1.2.3')
- end
+ subject.cluster.reload
+ expect(subject.cluster.application_helm).to be_nil
end
end
- context 'local tiller flag disabled' do
- before do
- stub_feature_flags(managed_apps_local_tiller: false)
- end
-
- context 'helm record does not exist' do
- subject { build(application_name, :installing, :no_helm_installed) }
-
- it 'creates a helm record' do
- subject.make_externally_installed!
-
- subject.cluster.reload
- expect(subject.cluster.application_helm).to be_present
- expect(subject.cluster.application_helm).to be_persisted
- end
- end
-
- context 'helm record exists' do
- subject { build(application_name, :installing, cluster: old_helm.cluster) }
+ context 'helm record exists' do
+ subject { build(application_name, :installing, cluster: old_helm.cluster) }
- it 'does not update helm version' do
- subject.make_externally_installed!
+ it 'does not update helm version' do
+ subject.make_externally_installed!
- subject.cluster.application_helm.reload
+ subject.cluster.application_helm.reload
- expect(subject.cluster.application_helm.version).to eq('1.2.3')
- end
+ expect(subject.cluster.application_helm.version).to eq('1.2.3')
end
end
@@ -262,6 +182,14 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
expect(subject).to be_installed
end
+
+ it 'clears #status_reason' do
+ expect(subject.status_reason).not_to be_nil
+
+ subject.make_externally_installed!
+
+ expect(subject.status_reason).to be_nil
+ end
end
end
@@ -292,6 +220,14 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
expect(subject).to be_uninstalled
end
+
+ it 'clears #status_reason' do
+ expect(subject.status_reason).not_to be_nil
+
+ subject.make_externally_uninstalled!
+
+ expect(subject.status_reason).to be_nil
+ 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
new file mode 100644
index 00000000000..99a09993900
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
@@ -0,0 +1,176 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.shared_examples_for CounterAttribute do |counter_attributes|
+ it 'defines a Redis counter_key' do
+ expect(model.counter_key(:counter_name))
+ .to eq("project:{#{model.project_id}}:counters:CounterAttributeModel:#{model.id}:counter_name")
+ end
+
+ it 'defines a method to store counters' do
+ expect(model.class.counter_attributes.to_a).to eq(counter_attributes)
+ end
+
+ counter_attributes.each do |attribute|
+ describe attribute do
+ describe '#delayed_increment_counter', :redis do
+ let(:increment) { 10 }
+
+ subject { model.delayed_increment_counter(attribute, increment) }
+
+ context 'when attribute is a counter attribute' do
+ where(:increment) { [10, -3] }
+
+ with_them do
+ it 'increments the counter in Redis' do
+ subject
+
+ Gitlab::Redis::SharedState.with do |redis|
+ counter = redis.get(model.counter_key(attribute))
+ expect(counter).to eq(increment.to_s)
+ end
+ end
+
+ it 'does not increment the counter for the record' do
+ expect { subject }.not_to change { model.reset.read_attribute(attribute) }
+ end
+
+ it 'schedules a worker to flush counter increments asynchronously' do
+ expect(FlushCounterIncrementsWorker).to receive(:perform_in)
+ .with(CounterAttribute::WORKER_DELAY, model.class.name, model.id, attribute)
+ .and_call_original
+
+ subject
+ end
+ end
+
+ context 'when increment is 0' do
+ let(:increment) { 0 }
+
+ it 'does nothing' do
+ expect(FlushCounterIncrementsWorker).not_to receive(:perform_in)
+ expect(model).not_to receive(:update!)
+
+ subject
+ end
+ end
+ end
+
+ context 'when attribute is not a counter attribute' do
+ it 'delegates to ActiveRecord update!' do
+ expect { model.delayed_increment_counter(:unknown_attribute, 10) }
+ .to raise_error(ActiveModel::MissingAttributeError)
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(efficient_counter_attribute: false)
+ end
+
+ it 'delegates to ActiveRecord update!' do
+ expect { subject }
+ .to change { model.reset.read_attribute(attribute) }.by(increment)
+ end
+
+ it 'does not increment the counter in Redis' do
+ subject
+
+ Gitlab::Redis::SharedState.with do |redis|
+ counter = redis.get(model.counter_key(attribute))
+ expect(counter).to be_nil
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '.flush_increments_to_database!', :redis do
+ let(:incremented_attribute) { counter_attributes.first }
+
+ subject { model.flush_increments_to_database!(incremented_attribute) }
+
+ it 'obtains an exclusive lease during processing' do
+ expect(model)
+ .to receive(:in_lock)
+ .with(model.counter_lock_key(incremented_attribute), ttl: described_class::WORKER_LOCK_TTL)
+ .and_call_original
+
+ subject
+ end
+
+ context 'when there is a counter to flush' do
+ before do
+ model.delayed_increment_counter(incremented_attribute, 10)
+ model.delayed_increment_counter(incremented_attribute, -3)
+ end
+
+ it 'updates the record' do
+ expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(7)
+ end
+
+ it 'removes the increment entry from Redis' do
+ Gitlab::Redis::SharedState.with do |redis|
+ key_exists = redis.exists(model.counter_key(incremented_attribute))
+ expect(key_exists).to be_truthy
+ end
+
+ subject
+
+ Gitlab::Redis::SharedState.with do |redis|
+ key_exists = redis.exists(model.counter_key(incremented_attribute))
+ expect(key_exists).to be_falsey
+ end
+ end
+ end
+
+ context 'when there are no counters to flush' do
+ context 'when there are no counters in the relative :flushed key' do
+ it 'does not change the record' do
+ expect { subject }.not_to change { model.reset.attributes }
+ end
+ end
+
+ # This can be the case where updating counters in the database fails with error
+ # and retrying the worker will retry flushing the counters but the main key has
+ # disappeared and the increment has been moved to the "<...>:flushed" key.
+ context 'when there are counters in the relative :flushed key' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.incrby(model.counter_flushed_key(incremented_attribute), 10)
+ end
+ end
+
+ it 'updates the record' do
+ expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(10)
+ end
+
+ it 'deletes the relative :flushed key' do
+ subject
+
+ Gitlab::Redis::SharedState.with do |redis|
+ key_exists = redis.exists(model.counter_flushed_key(incremented_attribute))
+ expect(key_exists).to be_falsey
+ end
+ end
+ end
+ end
+
+ context 'when deleting :flushed key fails' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.incrby(model.counter_flushed_key(incremented_attribute), 10)
+
+ expect(redis).to receive(:del).and_raise('could not delete key')
+ end
+ end
+
+ it 'does a rollback of the counter update' do
+ expect { subject }.to raise_error('could not delete key')
+
+ expect(model.reset.read_attribute(incremented_attribute)).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb b/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb
new file mode 100644
index 00000000000..4cb087c47ad
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'mounted file in local store' do
+ it 'is stored locally' do
+ expect(subject.file_store).to be(ObjectStorage::Store::LOCAL)
+ expect(subject.file).to be_file_storage
+ expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+end
+
+RSpec.shared_examples 'mounted file in object store' do
+ it 'is stored remotely' do
+ expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(subject.file).not_to be_file_storage
+ expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
index 32d502af5a2..15ca1f56bd0 100644
--- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
@@ -3,7 +3,8 @@
RSpec.shared_examples 'a timebox' do |timebox_type|
let(:project) { create(:project, :public) }
let(:group) { create(:group) }
- let(:timebox) { create(timebox_type, project: project) }
+ let(:timebox_args) { [] }
+ let(:timebox) { create(timebox_type, *timebox_args, project: project) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym }
@@ -12,7 +13,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
context 'with a project' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
- let(:instance) { build(timebox_type, project: build(:project), group: nil) }
+ let(:instance) { build(timebox_type, *timebox_args, project: build(:project), group: nil) }
let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) { timebox_table_name }
@@ -22,7 +23,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
context 'with a group' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
- let(:instance) { build(timebox_type, project: nil, group: build(:group)) }
+ let(:instance) { build(timebox_type, *timebox_args, project: nil, group: build(:group)) }
let(:scope) { :group }
let(:scope_attrs) { { namespace: instance.group } }
let(:usage) { timebox_table_name }
@@ -37,14 +38,14 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
describe 'start_date' do
it 'adds an error when start_date is greater then due_date' do
- timebox = build(timebox_type, start_date: Date.tomorrow, due_date: Date.yesterday)
+ timebox = build(timebox_type, *timebox_args, start_date: Date.tomorrow, due_date: Date.yesterday)
expect(timebox).not_to be_valid
expect(timebox.errors[:due_date]).to include("must be greater than start date")
end
it 'adds an error when start_date is greater than 9999-12-31' do
- timebox = build(timebox_type, start_date: Date.new(10000, 1, 1))
+ timebox = build(timebox_type, *timebox_args, start_date: Date.new(10000, 1, 1))
expect(timebox).not_to be_valid
expect(timebox.errors[:start_date]).to include("date must not be after 9999-12-31")
@@ -53,7 +54,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
describe 'due_date' do
it 'adds an error when due_date is greater than 9999-12-31' do
- timebox = build(timebox_type, due_date: Date.new(10000, 1, 1))
+ timebox = build(timebox_type, *timebox_args, due_date: Date.new(10000, 1, 1))
expect(timebox).not_to be_valid
expect(timebox.errors[:due_date]).to include("date must not be after 9999-12-31")
@@ -64,7 +65,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
it { is_expected.to validate_presence_of(:title) }
it 'is invalid if title would be empty after sanitation' do
- timebox = build(timebox_type, project: project, title: '<img src=x onerror=prompt(1)>')
+ timebox = build(timebox_type, *timebox_args, project: project, title: '<img src=x onerror=prompt(1)>')
expect(timebox).not_to be_valid
expect(timebox.errors[:title]).to include("can't be blank")
@@ -73,7 +74,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
describe '#timebox_type_check' do
it 'is invalid if it has both project_id and group_id' do
- timebox = build(timebox_type, group: group)
+ timebox = build(timebox_type, *timebox_args, group: group)
timebox.project = project
expect(timebox).not_to be_valid
@@ -98,7 +99,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
end
context "per group" do
- let(:timebox) { create(timebox_type, group: group) }
+ let(:timebox) { create(timebox_type, *timebox_args, group: group) }
before do
project.update(group: group)
@@ -111,7 +112,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
end
it "does not accept the same title of a child project timebox" do
- create(timebox_type, project: group.projects.first)
+ create(timebox_type, *timebox_args, project: group.projects.first)
new_timebox = described_class.new(group: group, title: timebox.title)
@@ -143,7 +144,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
end
context 'when project_id is not present' do
- let(:timebox) { build(timebox_type, group: group) }
+ let(:timebox) { build(timebox_type, *timebox_args, group: group) }
it 'returns false' do
expect(timebox.project_timebox?).to be_falsey
@@ -153,7 +154,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
describe '#group_timebox?' do
context 'when group_id is present' do
- let(:timebox) { build(timebox_type, group: group) }
+ let(:timebox) { build(timebox_type, *timebox_args, group: group) }
it 'returns true' do
expect(timebox.group_timebox?).to be_truthy
@@ -168,7 +169,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
end
describe '#safe_title' do
- let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") }
+ let(:timebox) { create(timebox_type, *timebox_args, title: "<b>foo & bar -> 2.2</b>") }
it 'normalizes the title for use as a slug' do
expect(timebox.safe_title).to eq('foo-bar-22')
@@ -177,7 +178,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
describe '#resource_parent' do
context 'when group is present' do
- let(:timebox) { build(timebox_type, group: group) }
+ let(:timebox) { build(timebox_type, *timebox_args, group: group) }
it 'returns the group' do
expect(timebox.resource_parent).to eq(group)
@@ -192,7 +193,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
end
describe "#title" do
- let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") }
+ let(:timebox) { create(timebox_type, *timebox_args, title: "<b>foo & bar -> 2.2</b>") }
it "sanitizes title" do
expect(timebox.title).to eq("foo & bar -> 2.2")
@@ -203,28 +204,28 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
context "per project" do
it "is true for projects with MRs enabled" do
project = create(:project, :merge_requests_enabled)
- timebox = create(timebox_type, project: project)
+ timebox = create(timebox_type, *timebox_args, project: project)
expect(timebox.merge_requests_enabled?).to be_truthy
end
it "is false for projects with MRs disabled" do
project = create(:project, :repository_enabled, :merge_requests_disabled)
- timebox = create(timebox_type, project: project)
+ timebox = create(timebox_type, *timebox_args, project: project)
expect(timebox.merge_requests_enabled?).to be_falsey
end
it "is false for projects with repository disabled" do
project = create(:project, :repository_disabled)
- timebox = create(timebox_type, project: project)
+ timebox = create(timebox_type, *timebox_args, project: project)
expect(timebox.merge_requests_enabled?).to be_falsey
end
end
context "per group" do
- let(:timebox) { create(timebox_type, group: group) }
+ let(:timebox) { create(timebox_type, *timebox_args, group: group) }
it "is always true for groups, for performance reasons" do
expect(timebox.merge_requests_enabled?).to be_truthy
@@ -234,7 +235,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
describe '#to_ability_name' do
it 'returns timebox' do
- timebox = build(timebox_type)
+ timebox = build(timebox_type, *timebox_args)
expect(timebox.to_ability_name).to eq(timebox_type.to_s)
end
diff --git a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
index 21ab9b06c33..13ffc1b7f87 100644
--- a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
+++ b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
@@ -38,6 +38,7 @@ RSpec.shared_examples 'issuable hook data' do |kind|
title_html: %w[foo bar]
}
end
+
let(:data) { builder.build(user: user, changes: changes) }
it 'populates the :changes hash' do
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 99e62ebf422..e4668926d74 100644
--- a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
+++ b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
RSpec.shared_examples 'a class that supports relative positioning' do
- let(:item1) { create(factory, default_params) }
- let(:item2) { create(factory, default_params) }
- let(:new_item) { create(factory, default_params) }
+ let(:item1) { create_item }
+ let(:item2) { create_item }
+ let(:new_item) { create_item }
- def create_item(params)
+ def create_item(params = {})
create(factory, params.merge(default_params))
end
@@ -16,31 +16,119 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
describe '.move_nulls_to_end' do
+ let(:item3) { create_item }
+
it 'moves items with null relative_position to the end' do
+ item1.update!(relative_position: 1000)
+ item2.update!(relative_position: nil)
+ item3.update!(relative_position: nil)
+
+ items = [item1, item2, item3]
+ expect(described_class.move_nulls_to_end(items)).to be(2)
+
+ expect(items.sort_by(&:relative_position)).to eq(items)
+ expect(item1.relative_position).to be(1000)
+ expect(item1.prev_relative_position).to be_nil
+ expect(item1.next_relative_position).to eq(item2.relative_position)
+ expect(item2.next_relative_position).to eq(item3.relative_position)
+ expect(item3.next_relative_position).to be_nil
+ end
+
+ it 'preserves relative position' do
item1.update!(relative_position: nil)
item2.update!(relative_position: nil)
described_class.move_nulls_to_end([item1, item2])
- expect(item2.prev_relative_position).to eq item1.relative_position
- expect(item1.prev_relative_position).to eq nil
- expect(item2.next_relative_position).to eq nil
+ expect(item1.relative_position).to be < item2.relative_position
end
it 'moves the item near the start position when there are no existing positions' do
item1.update!(relative_position: nil)
described_class.move_nulls_to_end([item1])
-
- expect(item1.relative_position).to eq(described_class::START_POSITION + described_class::IDEAL_DISTANCE)
+ expect(item1.reset.relative_position).to eq(described_class::START_POSITION + described_class::IDEAL_DISTANCE)
end
it 'does not perform any moves if all items have their relative_position set' do
item1.update!(relative_position: 1)
- expect(item1).not_to receive(:save)
+ expect(described_class.move_nulls_to_start([item1])).to be(0)
+ expect(item1.reload.relative_position).to be(1)
+ end
+
+ it 'manages to move nulls to the end even if there is a sequence at the end' do
+ bunch = create_items_with_positions(run_at_end)
+ item1.update!(relative_position: nil)
described_class.move_nulls_to_end([item1])
+
+ items = [*bunch, item1]
+ items.each(&:reset)
+
+ expect(items.map(&:relative_position)).to all(be_valid_position)
+ expect(items.sort_by(&:relative_position)).to eq(items)
+ 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])
+
+ baseline = ActiveRecord::QueryRecorder.new do
+ described_class.move_nulls_to_end([a, e])
+ end
+
+ expect { described_class.move_nulls_to_end([b, c, d]) }
+ .not_to exceed_query_limit(baseline)
+
+ expect { described_class.move_nulls_to_end([f]) }
+ .not_to exceed_query_limit(baseline.count)
+ end
+ end
+
+ describe '.move_nulls_to_start' do
+ let(:item3) { create_item }
+
+ it 'moves items with null relative_position to the start' do
+ item1.update!(relative_position: nil)
+ item2.update!(relative_position: nil)
+ item3.update!(relative_position: 1000)
+
+ items = [item1, item2, item3]
+ expect(described_class.move_nulls_to_start(items)).to be(2)
+ items.map(&:reload)
+
+ expect(items.sort_by(&:relative_position)).to eq(items)
+ expect(item1.prev_relative_position).to eq nil
+ expect(item1.next_relative_position).to eq item2.relative_position
+ expect(item2.next_relative_position).to eq item3.relative_position
+ expect(item3.next_relative_position).to eq nil
+ expect(item3.relative_position).to be(1000)
+ end
+
+ it 'moves the item near the start position when there are no existing positions' do
+ item1.update!(relative_position: nil)
+
+ described_class.move_nulls_to_start([item1])
+
+ expect(item1.relative_position).to eq(described_class::START_POSITION - described_class::IDEAL_DISTANCE)
+ end
+
+ it 'preserves relative position' do
+ item1.update!(relative_position: nil)
+ item2.update!(relative_position: nil)
+
+ described_class.move_nulls_to_start([item1, item2])
+
+ expect(item1.relative_position).to be < item2.relative_position
+ end
+
+ it 'does not perform any moves if all items have their relative_position set' do
+ item1.update!(relative_position: 1)
+
+ expect(described_class.move_nulls_to_start([item1])).to be(0)
+ expect(item1.reload.relative_position).to be(1)
end
end
@@ -52,8 +140,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do
describe '#prev_relative_position' do
it 'returns previous position if there is an item above' do
- item1.update(relative_position: 5)
- item2.update(relative_position: 15)
+ item1.update!(relative_position: 5)
+ item2.update!(relative_position: 15)
expect(item2.prev_relative_position).to eq item1.relative_position
end
@@ -65,8 +153,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do
describe '#next_relative_position' do
it 'returns next position if there is an item below' do
- item1.update(relative_position: 5)
- item2.update(relative_position: 15)
+ item1.update!(relative_position: 5)
+ item2.update!(relative_position: 15)
expect(item1.next_relative_position).to eq item2.relative_position
end
@@ -76,9 +164,172 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
end
+ describe '#find_next_gap_before' do
+ context 'there is no gap' do
+ let(:items) { create_items_with_positions(run_at_start) }
+
+ it 'returns nil' do
+ items.each do |item|
+ expect(item.send(:find_next_gap_before)).to be_nil
+ end
+ end
+ end
+
+ context 'there is a sequence ending at MAX_POSITION' do
+ let(:items) { create_items_with_positions(run_at_end) }
+
+ let(:gaps) do
+ items.map { |item| item.send(:find_next_gap_before) }
+ end
+
+ it 'can find the gap at the start for any item in the sequence' do
+ gap = { start: items.first.relative_position, end: RelativePositioning::MIN_POSITION }
+
+ expect(gaps).to all(eq(gap))
+ end
+
+ it 'respects lower bounds' do
+ gap = { start: items.first.relative_position, end: 10 }
+ new_item.update!(relative_position: 10)
+
+ expect(gaps).to all(eq(gap))
+ end
+ end
+
+ specify do
+ item1.update!(relative_position: 5)
+
+ (0..10).each do |pos|
+ item2.update!(relative_position: pos)
+
+ gap = item2.send(:find_next_gap_before)
+
+ expect(gap[:start]).to be <= item2.relative_position
+ expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP
+ expect(gap[:start]).to be_valid_position
+ expect(gap[:end]).to be_valid_position
+ end
+ end
+
+ it 'deals with there not being any items to the left' do
+ create_items_with_positions([1, 2, 3])
+ new_item.update!(relative_position: 0)
+
+ expect(new_item.send(:find_next_gap_before)).to eq(start: 0, end: RelativePositioning::MIN_POSITION)
+ end
+
+ it 'finds the next gap to the left, skipping adjacent values' do
+ create_items_with_positions([1, 9, 10])
+ new_item.update!(relative_position: 11)
+
+ expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 1)
+ end
+
+ it 'finds the next gap to the left' do
+ create_items_with_positions([2, 10])
+
+ new_item.update!(relative_position: 15)
+ expect(new_item.send(:find_next_gap_before)).to eq(start: 15, end: 10)
+
+ new_item.update!(relative_position: 11)
+ expect(new_item.send(:find_next_gap_before)).to eq(start: 10, end: 2)
+
+ new_item.update!(relative_position: 9)
+ expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 2)
+
+ new_item.update!(relative_position: 5)
+ expect(new_item.send(:find_next_gap_before)).to eq(start: 5, end: 2)
+ end
+ end
+
+ describe '#find_next_gap_after' do
+ context 'there is no gap' do
+ let(:items) { create_items_with_positions(run_at_end) }
+
+ it 'returns nil' do
+ items.each do |item|
+ expect(item.send(:find_next_gap_after)).to be_nil
+ end
+ end
+ end
+
+ context 'there is a sequence starting at MIN_POSITION' do
+ let(:items) { create_items_with_positions(run_at_start) }
+
+ let(:gaps) do
+ items.map { |item| item.send(:find_next_gap_after) }
+ end
+
+ it 'can find the gap at the end for any item in the sequence' do
+ gap = { start: items.last.relative_position, end: RelativePositioning::MAX_POSITION }
+
+ expect(gaps).to all(eq(gap))
+ end
+
+ it 'respects upper bounds' do
+ gap = { start: items.last.relative_position, end: 10 }
+ new_item.update!(relative_position: 10)
+
+ expect(gaps).to all(eq(gap))
+ end
+ end
+
+ specify do
+ item1.update!(relative_position: 5)
+
+ (0..10).each do |pos|
+ item2.update!(relative_position: pos)
+
+ gap = item2.send(:find_next_gap_after)
+
+ expect(gap[:start]).to be >= item2.relative_position
+ expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP
+ expect(gap[:start]).to be_valid_position
+ expect(gap[:end]).to be_valid_position
+ end
+ end
+
+ it 'deals with there not being any items to the right' do
+ create_items_with_positions([1, 2, 3])
+ new_item.update!(relative_position: 5)
+
+ expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: RelativePositioning::MAX_POSITION)
+ end
+
+ it 'finds the next gap to the right, skipping adjacent values' do
+ create_items_with_positions([1, 2, 10])
+ new_item.update!(relative_position: 0)
+
+ expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10)
+ end
+
+ it 'finds the next gap to the right' do
+ create_items_with_positions([2, 10])
+
+ new_item.update!(relative_position: 0)
+ expect(new_item.send(:find_next_gap_after)).to eq(start: 0, end: 2)
+
+ new_item.update!(relative_position: 1)
+ expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10)
+
+ new_item.update!(relative_position: 3)
+ expect(new_item.send(:find_next_gap_after)).to eq(start: 3, end: 10)
+
+ new_item.update!(relative_position: 5)
+ expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: 10)
+ end
+ end
+
describe '#move_before' do
+ let(:item3) { create(factory, default_params) }
+
it 'moves item before' do
- [item2, item1].each(&:move_to_end)
+ [item2, item1].each do |item|
+ item.move_to_end
+ item.save!
+ end
+
+ expect(item1.relative_position).to be > item2.relative_position
item1.move_before(item2)
@@ -86,12 +337,10 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
context 'when there is no space' do
- let(:item3) { create(factory, default_params) }
-
before do
- item1.update(relative_position: 1000)
- item2.update(relative_position: 1001)
- item3.update(relative_position: 1002)
+ item1.update!(relative_position: 1000)
+ item2.update!(relative_position: 1001)
+ item3.update!(relative_position: 1002)
end
it 'moves items correctly' do
@@ -100,6 +349,73 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(item3.relative_position).to be_between(item1.reload.relative_position, item2.reload.relative_position).exclusive
end
end
+
+ it 'can move the item before an item at the start' do
+ item1.update!(relative_position: RelativePositioning::START_POSITION)
+
+ new_item.move_before(item1)
+
+ expect(new_item.relative_position).to be_valid_position
+ expect(new_item.relative_position).to be < item1.reload.relative_position
+ end
+
+ it 'can move the item before an item at MIN_POSITION' do
+ item1.update!(relative_position: RelativePositioning::MIN_POSITION)
+
+ new_item.move_before(item1)
+
+ expect(new_item.relative_position).to be >= RelativePositioning::MIN_POSITION
+ expect(new_item.relative_position).to be < item1.reload.relative_position
+ end
+
+ it 'can move the item before an item bunched up at MIN_POSITION' do
+ item1, item2, item3 = create_items_with_positions(run_at_start)
+
+ new_item.move_before(item3)
+ new_item.save!
+
+ items = [item1, item2, new_item, item3]
+
+ items.each do |item|
+ expect(item.reset.relative_position).to be_valid_position
+ end
+
+ expect(items.sort_by(&:relative_position)).to eq(items)
+ end
+
+ context 'leap-frogging to the left' do
+ before do
+ start = RelativePositioning::START_POSITION
+ item1.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 0)
+ item2.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 1)
+ item3.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 2)
+ end
+
+ let(:item3) { create(factory, default_params) }
+
+ def leap_frog(steps)
+ a = item1
+ b = item2
+
+ steps.times do |i|
+ a.move_before(b)
+ a.save!
+ a, b = b, a
+ end
+ end
+
+ it 'can leap-frog STEPS - 1 times before needing to rebalance' do
+ # This is less efficient than going right, due to the flooring of
+ # integer division
+ expect { leap_frog(RelativePositioning::STEPS - 1) }
+ .not_to change { item3.reload.relative_position }
+ end
+
+ it 'rebalances after leap-frogging STEPS times' do
+ expect { leap_frog(RelativePositioning::STEPS) }
+ .to change { item3.reload.relative_position }
+ end
+ end
end
describe '#move_after' do
@@ -115,9 +431,17 @@ RSpec.shared_examples 'a class that supports relative positioning' do
let(:item3) { create(factory, default_params) }
before do
- item1.update(relative_position: 1000)
- item2.update(relative_position: 1001)
- item3.update(relative_position: 1002)
+ item1.update!(relative_position: 1000)
+ item2.update!(relative_position: 1001)
+ item3.update!(relative_position: 1002)
+ end
+
+ it 'can move the item after an item at MAX_POSITION' do
+ item1.update!(relative_position: RelativePositioning::MAX_POSITION)
+
+ new_item.move_after(item1)
+ expect(new_item.relative_position).to be_valid_position
+ expect(new_item.relative_position).to be > item1.reset.relative_position
end
it 'moves items correctly' do
@@ -126,12 +450,96 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(item1.relative_position).to be_between(item2.reload.relative_position, item3.reload.relative_position).exclusive
end
end
+
+ it 'can move the item after an item bunched up at MAX_POSITION' do
+ item1, item2, item3 = create_items_with_positions(run_at_end)
+
+ new_item.move_after(item1)
+ new_item.save!
+
+ items = [item1, new_item, item2, item3]
+
+ items.each do |item|
+ expect(item.reset.relative_position).to be_valid_position
+ end
+
+ expect(items.sort_by(&:relative_position)).to eq(items)
+ end
+
+ context 'leap-frogging' do
+ before do
+ start = RelativePositioning::START_POSITION
+ item1.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 0)
+ item2.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 1)
+ item3.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 2)
+ end
+
+ let(:item3) { create(factory, default_params) }
+
+ def leap_frog(steps)
+ a = item1
+ b = item2
+
+ steps.times do |i|
+ a.move_after(b)
+ a.save!
+ a, b = b, a
+ end
+ end
+
+ it 'can leap-frog STEPS times before needing to rebalance' do
+ expect { leap_frog(RelativePositioning::STEPS) }
+ .not_to change { item3.reload.relative_position }
+ end
+
+ it 'rebalances after leap-frogging STEPS+1 times' do
+ expect { leap_frog(RelativePositioning::STEPS + 1) }
+ .to change { item3.reload.relative_position }
+ end
+ end
+ end
+
+ describe '#move_to_start' do
+ before do
+ [item1, item2].each do |item1|
+ item1.move_to_start && item1.save!
+ end
+ end
+
+ it 'moves item to the end' do
+ new_item.move_to_start
+
+ expect(new_item.relative_position).to be < item2.relative_position
+ end
+
+ it 'rebalances when there is already an item at the MIN_POSITION' do
+ item2.update!(relative_position: RelativePositioning::MIN_POSITION)
+
+ new_item.move_to_start
+ item2.reset
+
+ expect(new_item.relative_position).to be < item2.relative_position
+ expect(new_item.relative_position).to be >= RelativePositioning::MIN_POSITION
+ end
+
+ it 'deals with a run of elements at the start' do
+ item1.update!(relative_position: RelativePositioning::MIN_POSITION + 1)
+ item2.update!(relative_position: RelativePositioning::MIN_POSITION)
+
+ new_item.move_to_start
+ item1.reset
+ item2.reset
+
+ expect(item2.relative_position).to be < item1.relative_position
+ expect(new_item.relative_position).to be < item2.relative_position
+ expect(new_item.relative_position).to be >= RelativePositioning::MIN_POSITION
+ end
end
describe '#move_to_end' do
before do
[item1, item2].each do |item1|
- item1.move_to_end && item1.save
+ item1.move_to_end && item1.save!
end
end
@@ -140,12 +548,44 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(new_item.relative_position).to be > item2.relative_position
end
+
+ it 'rebalances when there is already an item at the MAX_POSITION' do
+ item2.update!(relative_position: RelativePositioning::MAX_POSITION)
+
+ new_item.move_to_end
+ item2.reset
+
+ expect(new_item.relative_position).to be > item2.relative_position
+ expect(new_item.relative_position).to be <= RelativePositioning::MAX_POSITION
+ end
+
+ it 'deals with a run of elements at the end' do
+ item1.update!(relative_position: RelativePositioning::MAX_POSITION - 1)
+ item2.update!(relative_position: RelativePositioning::MAX_POSITION)
+
+ new_item.move_to_end
+ item1.reset
+ item2.reset
+
+ expect(item2.relative_position).to be > item1.relative_position
+ expect(new_item.relative_position).to be > item2.relative_position
+ expect(new_item.relative_position).to be <= RelativePositioning::MAX_POSITION
+ end
end
describe '#move_between' do
before do
- [item1, item2].each do |item1|
- item1.move_to_end && item1.save
+ [item1, item2].each do |item|
+ item.move_to_end && item.save!
+ end
+ end
+
+ shared_examples 'moves item between' do
+ it 'moves the middle item to between left and right' do
+ expect do
+ middle.move_between(left, right)
+ middle.save!
+ end.to change { between_exclusive?(left, middle, right) }.from(false).to(true)
end
end
@@ -169,26 +609,26 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
it 'positions items even when after and before positions are the same' do
- item2.update relative_position: item1.relative_position
+ item2.update! relative_position: item1.relative_position
new_item.move_between(item1, item2)
+ [item1, item2].each(&:reset)
expect(new_item.relative_position).to be > item1.relative_position
expect(item1.relative_position).to be < item2.relative_position
end
- it 'positions items between other two if distance is 1' do
- item2.update relative_position: item1.relative_position + 1
-
- new_item.move_between(item1, item2)
+ context 'the two items are next to each other' do
+ let(:left) { item1 }
+ let(:middle) { new_item }
+ let(:right) { create_item(relative_position: item1.relative_position + 1) }
- expect(new_item.relative_position).to be > item1.relative_position
- expect(item1.relative_position).to be < item2.relative_position
+ it_behaves_like 'moves item between'
end
it 'positions item in the middle of other two if distance is big enough' do
- item1.update relative_position: 6000
- item2.update relative_position: 10000
+ item1.update! relative_position: 6000
+ item2.update! relative_position: 10000
new_item.move_between(item1, item2)
@@ -196,7 +636,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
it 'positions item closer to the middle if we are at the very top' do
- item2.update relative_position: 6000
+ item1.update!(relative_position: 6001)
+ item2.update!(relative_position: 6000)
new_item.move_between(nil, item2)
@@ -204,51 +645,53 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
it 'positions item closer to the middle if we are at the very bottom' do
- new_item.update relative_position: 1
- item1.update relative_position: 6000
- item2.destroy
+ new_item.update!(relative_position: 1)
+ item1.update!(relative_position: 6000)
+ item2.update!(relative_position: 5999)
new_item.move_between(item1, nil)
expect(new_item.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE)
end
- it 'positions item in the middle of other two if distance is not big enough' do
- item1.update relative_position: 100
- item2.update relative_position: 400
+ it 'positions item in the middle of other two' do
+ item1.update! relative_position: 100
+ item2.update! relative_position: 400
new_item.move_between(item1, item2)
expect(new_item.relative_position).to eq(250)
end
- it 'positions item in the middle of other two is there is no place' do
- item1.update relative_position: 100
- item2.update relative_position: 101
+ context 'there is no space' do
+ let(:middle) { new_item }
+ let(:left) { create_item(relative_position: 100) }
+ let(:right) { create_item(relative_position: 101) }
- new_item.move_between(item1, item2)
-
- expect(new_item.relative_position).to be_between(item1.relative_position, item2.relative_position).exclusive
+ it_behaves_like 'moves item between'
end
- it 'uses rebalancing if there is no place' do
- item1.update relative_position: 100
- item2.update relative_position: 101
- item3 = create_item(relative_position: 102)
- new_item.update relative_position: 103
+ context 'there is a bunch of items' do
+ let(:items) { create_items_with_positions(100..104) }
+ let(:left) { items[1] }
+ let(:middle) { items[3] }
+ let(:right) { items[2] }
- new_item.move_between(item2, item3)
- new_item.save!
+ it_behaves_like 'moves item between'
+
+ it 'handles bunches correctly' do
+ middle.move_between(left, right)
+ middle.save!
- expect(new_item.relative_position).to be_between(item2.relative_position, item3.relative_position).exclusive
- expect(item1.reload.relative_position).not_to eq(100)
+ expect(items.first.reset.relative_position).to be < middle.relative_position
+ end
end
- it 'positions item right if we pass none-sequential parameters' do
- item1.update relative_position: 99
- item2.update relative_position: 101
+ it 'positions item right if we pass non-sequential parameters' do
+ item1.update! relative_position: 99
+ item2.update! relative_position: 101
item3 = create_item(relative_position: 102)
- new_item.update relative_position: 103
+ new_item.update! relative_position: 103
new_item.move_between(item1, item3)
new_item.save!
@@ -280,6 +723,12 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(positions).to eq([90, 95, 96, 102])
end
+ it 'raises an error if there is no space' do
+ items = create_items_with_positions(run_at_start)
+
+ expect { items.last.move_sequence_before }.to raise_error(RelativePositioning::NoSpaceLeft)
+ end
+
it 'finds a gap if there are unused positions' do
items = create_items_with_positions([100, 101, 102])
@@ -287,7 +736,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do
items.last.save!
positions = items.map { |item| item.reload.relative_position }
- expect(positions).to eq([50, 51, 102])
+
+ expect(positions.last - positions.second).to be > RelativePositioning::MIN_GAP
end
end
@@ -309,7 +759,33 @@ RSpec.shared_examples 'a class that supports relative positioning' do
items.first.save!
positions = items.map { |item| item.reload.relative_position }
- expect(positions).to eq([100, 601, 602])
+ expect(positions.second - positions.first).to be > RelativePositioning::MIN_GAP
end
+
+ it 'raises an error if there is no space' do
+ items = create_items_with_positions(run_at_end)
+
+ expect { items.first.move_sequence_after }.to raise_error(RelativePositioning::NoSpaceLeft)
+ end
+ end
+
+ def be_valid_position
+ be_between(RelativePositioning::MIN_POSITION, RelativePositioning::MAX_POSITION)
+ end
+
+ def between_exclusive?(left, middle, right)
+ a, b, c = [left, middle, right].map { |item| item.reset.relative_position }
+ return false if a.nil? || b.nil?
+ return a < b if c.nil?
+
+ a < b && b < c
+ end
+
+ def run_at_end(size = 3)
+ (RelativePositioning::MAX_POSITION - size)..RelativePositioning::MAX_POSITION
+ end
+
+ def run_at_start(size = 3)
+ (RelativePositioning::MIN_POSITION..).take(size)
end
end
diff --git a/spec/support/shared_examples/models/resource_event_shared_examples.rb b/spec/support/shared_examples/models/resource_event_shared_examples.rb
new file mode 100644
index 00000000000..c0158f9b24b
--- /dev/null
+++ b/spec/support/shared_examples/models/resource_event_shared_examples.rb
@@ -0,0 +1,155 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'a resource event' do
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+
+ let_it_be(:issue1) { create(:issue, author: user1) }
+ let_it_be(:issue2) { create(:issue, author: user1) }
+ let_it_be(:issue3) { create(:issue, author: user2) }
+
+ describe 'importable' do
+ it { is_expected.to respond_to(:importing?) }
+ it { is_expected.to respond_to(:imported?) }
+ end
+
+ describe 'validations' do
+ it { is_expected.not_to allow_value(nil).for(:user) }
+
+ context 'when importing' do
+ before do
+ allow(subject).to receive(:importing?).and_return(true)
+ end
+
+ it { is_expected.to allow_value(nil).for(:user) }
+ end
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe '.created_after' do
+ let!(:created_at1) { 1.day.ago }
+ let!(:created_at2) { 2.days.ago }
+ let!(:created_at3) { 3.days.ago }
+
+ let!(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: created_at1) }
+ let!(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at2) }
+ let!(:event3) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at3) }
+
+ it 'returns the expected events' do
+ events = described_class.created_after(created_at3)
+
+ expect(events).to contain_exactly(event1, event2)
+ end
+
+ it 'returns no events if time is after last record time' do
+ events = described_class.created_after(1.minute.ago)
+
+ expect(events).to be_empty
+ end
+ end
+end
+
+RSpec.shared_examples 'a resource event for issues' do
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+
+ let_it_be(:issue1) { create(:issue, author: user1) }
+ let_it_be(:issue2) { create(:issue, author: user1) }
+ let_it_be(:issue3) { create(:issue, author: user2) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:issue) }
+ end
+
+ describe '.by_issue' do
+ let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1) }
+ let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2) }
+ let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1) }
+
+ it 'returns the expected records for an issue with events' do
+ events = described_class.by_issue(issue1)
+
+ expect(events).to contain_exactly(event1, event3)
+ end
+
+ it 'returns the expected records for an issue with no events' do
+ events = described_class.by_issue(issue3)
+
+ expect(events).to be_empty
+ end
+ end
+
+ describe '.by_issue_ids_and_created_at_earlier_or_equal_to' do
+ let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-10') }
+ let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: '2020-03-10') }
+ let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-12') }
+
+ it 'returns the expected records for an issue with events' do
+ events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to([issue1.id, issue2.id], '2020-03-11 23:59:59')
+
+ expect(events).to contain_exactly(event1, event2)
+ end
+
+ it 'returns the expected records for an issue with no events' do
+ events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to(issue3, '2020-03-12')
+
+ expect(events).to be_empty
+ end
+ end
+
+ if described_class.method_defined?(:issuable)
+ describe '#issuable' do
+ let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue2) }
+
+ it 'returns the expected issuable' do
+ expect(event1.issuable).to eq(issue2)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'a resource event for merge requests' do
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+
+ let_it_be(:merge_request1) { create(:merge_request, author: user1) }
+ let_it_be(:merge_request2) { create(:merge_request, author: user1) }
+ let_it_be(:merge_request3) { create(:merge_request, author: user2) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:merge_request) }
+ end
+
+ describe '.by_merge_request' do
+ let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) }
+ let_it_be(:event2) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) }
+ let_it_be(:event3) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) }
+
+ it 'returns the expected records for an issue with events' do
+ events = described_class.by_merge_request(merge_request1)
+
+ expect(events).to contain_exactly(event1, event3)
+ end
+
+ it 'returns the expected records for an issue with no events' do
+ events = described_class.by_merge_request(merge_request3)
+
+ expect(events).to be_empty
+ end
+ end
+
+ if described_class.method_defined?(:issuable)
+ describe '#issuable' do
+ let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) }
+
+ it 'returns the expected issuable' do
+ expect(event1.issuable).to eq(merge_request2)
+ 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
new file mode 100644
index 00000000000..07552b62cdd
--- /dev/null
+++ b/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'timebox resource event validations' do
+ describe 'validations' do
+ context 'when issue and merge_request are both nil' do
+ subject { build(described_class.name.underscore.to_sym, issue: nil, merge_request: nil) }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when issue and merge_request are both set' do
+ subject { build(described_class.name.underscore.to_sym, issue: build(:issue), merge_request: build(:merge_request)) }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when issue is set' do
+ subject { create(described_class.name.underscore.to_sym, issue: create(:issue), merge_request: nil) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when merge_request is set' do
+ subject { create(described_class.name.underscore.to_sym, issue: nil, merge_request: create(:merge_request)) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+end
+
+RSpec.shared_examples 'timebox resource event states' do
+ describe 'states' do
+ [Issue, MergeRequest].each do |klass|
+ klass.available_states.each do |state|
+ it "supports state #{state.first} for #{klass.name.underscore}" do
+ model = create(klass.name.underscore, state: state[0])
+ key = model.class.name.underscore
+ event = build(described_class.name.underscore.to_sym, key => model, state: model.state)
+
+ expect(event.state).to eq(state[0])
+ end
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'queryable timebox action resource event' do |expected_results_for_actions|
+ [Issue, MergeRequest].each do |klass|
+ expected_results_for_actions.each do |action, expected_result|
+ it "is #{expected_result} for action #{action} on #{klass.name.underscore}" do
+ model = build(klass.name.underscore)
+ key = model.class.name.underscore
+ event = build(described_class.name.underscore.to_sym, key => model, action: action)
+
+ expect(event.send(query_method)).to eq(expected_result)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'timebox resource event actions' do
+ describe '#added?' do
+ it_behaves_like 'queryable timebox action resource event', { add: true, remove: false } do
+ let(:query_method) { :add? }
+ end
+ end
+
+ describe '#removed?' do
+ it_behaves_like 'queryable timebox action resource event', { add: false, remove: true } do
+ let(:query_method) { :remove? }
+ end
+ end
+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 7d70df82ec7..7f0da19996e 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
@@ -17,11 +17,14 @@ RSpec.shared_examples 'UpdateProjectStatistics' do
context 'when creating' do
it 'updates the project statistics' do
- delta = read_attribute
+ delta0 = reload_stat
- expect { subject.save! }
- .to change { reload_stat }
- .by(delta)
+ subject.save!
+
+ delta1 = reload_stat
+
+ expect(delta1).to eq(delta0 + read_attribute)
+ expect(delta1).to be > delta0
end
it 'schedules a namespace statistics worker' do
@@ -80,15 +83,14 @@ RSpec.shared_examples 'UpdateProjectStatistics' do
end
it 'updates the project statistics' do
- delta = -read_attribute
+ delta0 = reload_stat
- expect(ProjectStatistics)
- .to receive(:increment_statistic)
- .and_call_original
+ subject.destroy!
- expect { subject.destroy! }
- .to change { reload_stat }
- .by(delta)
+ delta1 = reload_stat
+
+ expect(delta1).to eq(delta0 - read_attribute)
+ expect(delta1).to be < delta0
end
it 'schedules a namespace statistics worker' do