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/models/concerns')
-rw-r--r--spec/models/concerns/approvable_spec.rb24
-rw-r--r--spec/models/concerns/bulk_insert_safe_spec.rb2
-rw-r--r--spec/models/concerns/bulk_insertable_associations_spec.rb2
-rw-r--r--spec/models/concerns/ci/partitionable/switch_spec.rb42
-rw-r--r--spec/models/concerns/ci/partitionable_spec.rb10
-rw-r--r--spec/models/concerns/cross_database_ignored_tables_spec.rb222
-rw-r--r--spec/models/concerns/cross_database_modification_spec.rb2
-rw-r--r--spec/models/concerns/enum_inheritance_spec.rb97
-rw-r--r--spec/models/concerns/integrations/reset_secret_fields_spec.rb12
-rw-r--r--spec/models/concerns/milestoneable_spec.rb25
-rw-r--r--spec/models/concerns/noteable_spec.rb4
-rw-r--r--spec/models/concerns/reset_on_union_error_spec.rb132
-rw-r--r--spec/models/concerns/resolvable_discussion_spec.rb43
-rw-r--r--spec/models/concerns/resolvable_note_spec.rb54
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb2
-rw-r--r--spec/models/concerns/where_composite_spec.rb2
16 files changed, 601 insertions, 74 deletions
diff --git a/spec/models/concerns/approvable_spec.rb b/spec/models/concerns/approvable_spec.rb
index 25a4f51cd82..49b31d7fd89 100644
--- a/spec/models/concerns/approvable_spec.rb
+++ b/spec/models/concerns/approvable_spec.rb
@@ -32,6 +32,24 @@ RSpec.describe Approvable do
end
end
+ describe '#approved?' do
+ context 'when a merge request is approved' do
+ before do
+ create(:approval, merge_request: merge_request, user: user)
+ end
+
+ it 'returns true' do
+ expect(merge_request.approved?).to eq(true)
+ end
+ end
+
+ context 'when a merge request is not approved' do
+ it 'returns false' do
+ expect(merge_request.approved?).to eq(false)
+ end
+ end
+ end
+
describe '#eligible_for_approval_by?' do
subject { merge_request.eligible_for_approval_by?(user) }
@@ -40,14 +58,14 @@ RSpec.describe Approvable do
end
it 'returns true' do
- is_expected.to be_truthy
+ is_expected.to eq(true)
end
context 'when a user has approved' do
let!(:approval) { create(:approval, merge_request: merge_request, user: user) }
it 'returns false' do
- is_expected.to be_falsy
+ is_expected.to eq(false)
end
end
@@ -55,7 +73,7 @@ RSpec.describe Approvable do
let(:user) { nil }
it 'returns false' do
- is_expected.to be_falsy
+ is_expected.to eq(false)
end
end
end
diff --git a/spec/models/concerns/bulk_insert_safe_spec.rb b/spec/models/concerns/bulk_insert_safe_spec.rb
index 65b7da20bbc..3c50003ba2f 100644
--- a/spec/models/concerns/bulk_insert_safe_spec.rb
+++ b/spec/models/concerns/bulk_insert_safe_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe BulkInsertSafe, feature_category: :database do
- before(:all) do
+ before_all do
ActiveRecord::Schema.define do
create_table :_test_bulk_insert_parent_items, force: true do |t|
t.string :name, null: false
diff --git a/spec/models/concerns/bulk_insertable_associations_spec.rb b/spec/models/concerns/bulk_insertable_associations_spec.rb
index 3187dcd8f93..3796f60c705 100644
--- a/spec/models/concerns/bulk_insertable_associations_spec.rb
+++ b/spec/models/concerns/bulk_insertable_associations_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe BulkInsertableAssociations do
end
end
- before(:all) do
+ before_all do
ActiveRecord::Schema.define do
create_table :_test_bulk_parents, force: true do |t|
t.string :name, null: true
diff --git a/spec/models/concerns/ci/partitionable/switch_spec.rb b/spec/models/concerns/ci/partitionable/switch_spec.rb
index 551ae111fa4..0041a33e50e 100644
--- a/spec/models/concerns/ci/partitionable/switch_spec.rb
+++ b/spec/models/concerns/ci/partitionable/switch_spec.rb
@@ -38,6 +38,7 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
id serial NOT NULL PRIMARY KEY,
job_id int,
partition_id int NOT NULL DEFAULT 1,
+ type text,
expanded_environment_name text);
CREATE TABLE _test_p_ci_jobs_metadata (
@@ -89,6 +90,25 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
it { expect(partitioned_model.sequence_name).to eq('_test_ci_jobs_metadata_id_seq') }
+ context 'with singe table inheritance' do
+ let(:child_model) do
+ Class.new(model) do
+ def self.name
+ 'TestSwitchJobMetadataChild'
+ end
+ end
+ end
+
+ it 'adds a Partitioned model for each descendant' do
+ expect(model::Partitioned).not_to eq(child_model::Partitioned)
+ end
+
+ it 'uses the parent name in STI queries' do
+ recorder = ActiveRecord::QueryRecorder.new { child_model.all.load }
+ expect(recorder.log).to include(/"type" = 'TestSwitchJobMetadataChild'/)
+ end
+ end
+
context 'when switching the tables' do
before do
stub_feature_flags(table_rollout_flag => false)
@@ -172,11 +192,11 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
it 'writes' do
rollout_and_rollback_flag(
-> {
- expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.find(job.id).create_metadata! })
+ expect(sql(filter: [/INSERT/, /jobs_metadata/]) { jobs_model.find(job.id).create_metadata! })
.to all match(/INSERT INTO "_test_ci_jobs_metadata"/)
},
-> {
- expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.find(job.id).create_metadata! })
+ expect(sql(filter: [/INSERT/, /jobs_metadata/]) { jobs_model.find(job.id).create_metadata! })
.to all match(/INSERT INTO "_test_p_ci_jobs_metadata"/)
}
)
@@ -190,11 +210,11 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
rollout_and_rollback_flag(
-> {
- expect(sql(filter: /DELETE .* jobs_metadata/) { jobs_model.last.destroy! })
+ expect(sql(filter: [/DELETE/, /jobs_metadata/]) { jobs_model.last.destroy! })
.to all match(/DELETE FROM "_test_ci_jobs_metadata"/)
},
-> {
- expect(sql(filter: /DELETE .* jobs_metadata/) { jobs_model.last.destroy! })
+ expect(sql(filter: [/DELETE/, /jobs_metadata/]) { jobs_model.last.destroy! })
.to all match(/DELETE FROM "_test_p_ci_jobs_metadata"/)
}
)
@@ -252,11 +272,11 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
rollout_and_rollback_flag(
-> {
- expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.create!(attrs) })
+ expect(sql(filter: [/INSERT/, /jobs_metadata/]) { jobs_model.create!(attrs) })
.to all match(/INSERT INTO "_test_ci_jobs_metadata" .* 'test_env_name'/)
},
-> {
- expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.create!(attrs) })
+ expect(sql(filter: [/INSERT/, /jobs_metadata/]) { jobs_model.create!(attrs) })
.to all match(/INSERT INTO "_test_p_ci_jobs_metadata" .* 'test_env_name'/)
}
)
@@ -307,11 +327,9 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
end
def sql(filter: nil, &block)
- result = ActiveRecord::QueryRecorder.new(&block)
- result = result.log
-
- return result unless filter
-
- result.select { |statement| statement.match?(filter) }
+ ActiveRecord::QueryRecorder.new(&block)
+ .log
+ .select { |statement| Array.wrap(filter).all? { |regex| statement.match?(regex) } }
+ .tap { |result| expect(result).not_to be_empty }
end
end
diff --git a/spec/models/concerns/ci/partitionable_spec.rb b/spec/models/concerns/ci/partitionable_spec.rb
index d41654e547e..6daafc78cff 100644
--- a/spec/models/concerns/ci/partitionable_spec.rb
+++ b/spec/models/concerns/ci/partitionable_spec.rb
@@ -25,7 +25,11 @@ RSpec.describe Ci::Partitionable do
end
context 'with through options' do
+ let(:disable_partitionable_switch) { nil }
+
before do
+ stub_env('DISABLE_PARTITIONABLE_SWITCH', disable_partitionable_switch)
+
allow(ActiveSupport::DescendantsTracker).to receive(:store_inherited)
stub_const("#{described_class}::Testing::PARTITIONABLE_MODELS", [ci_model.name])
@@ -39,6 +43,12 @@ RSpec.describe Ci::Partitionable do
it { expect(ci_model.routing_table_name_flag).to eq(:some_flag) }
it { expect(ci_model.ancestors).to include(described_class::Switch) }
+
+ context 'when DISABLE_PARTITIONABLE_SWITCH is set' do
+ let(:disable_partitionable_switch) { true }
+
+ it { expect(ci_model.ancestors).not_to include(described_class::Switch) }
+ end
end
context 'with partitioned options' do
diff --git a/spec/models/concerns/cross_database_ignored_tables_spec.rb b/spec/models/concerns/cross_database_ignored_tables_spec.rb
new file mode 100644
index 00000000000..901a6f39eaf
--- /dev/null
+++ b/spec/models/concerns/cross_database_ignored_tables_spec.rb
@@ -0,0 +1,222 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CrossDatabaseIgnoredTables, feature_category: :cell, query_analyzers: false do
+ # We enable only the PreventCrossDatabaseModification query analyzer in these tests
+ before do
+ stub_const("CiModel", ci_model)
+ allow(Gitlab::Database::QueryAnalyzer.instance).to receive(:all_analyzers).and_return(
+ [Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification]
+ )
+ end
+
+ around do |example|
+ Gitlab::Database::QueryAnalyzer.instance.within { example.run }
+ end
+
+ let(:cross_database_exception) do
+ Gitlab::Database::QueryAnalyzers::
+ PreventCrossDatabaseModification::CrossDatabaseModificationAcrossUnsupportedTablesError
+ end
+
+ let(:ci_model) do
+ Class.new(Ci::ApplicationRecord) do
+ self.table_name = '_test_gitlab_ci_items'
+
+ belongs_to :main_model_object, class_name: 'MainModel',
+ inverse_of: 'ci_model_object', foreign_key: 'main_model_id'
+ end
+ end
+
+ before_all do
+ Ci::ApplicationRecord.connection.execute(
+ 'CREATE TABLE _test_gitlab_ci_items(
+ id BIGSERIAL PRIMARY KEY, main_model_id INTEGER, updated_at timestamp without time zone
+ )'
+ )
+ ApplicationRecord.connection.execute(
+ 'CREATE TABLE _test_gitlab_main_items(
+ id BIGSERIAL PRIMARY KEY, updated_at timestamp without time zone
+ )'
+ )
+ end
+
+ after(:all) do
+ ApplicationRecord.connection.execute('DROP TABLE _test_gitlab_main_items')
+ Ci::ApplicationRecord.connection.execute('DROP TABLE _test_gitlab_ci_items')
+ end
+
+ describe '.cross_database_ignore_tables' do
+ context 'when the tables are not ignored' do
+ before do
+ stub_const("MainModel", create_main_model([], []))
+ end
+
+ it 'raises an error when we doing cross-database modification using create' do
+ expect { MainModel.create! }.to raise_error(cross_database_exception)
+ end
+
+ it 'raises an error when we doing cross-database modification using update' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.update!(updated_at: Time.zone.now) }.to raise_error(cross_database_exception)
+ end
+
+ it 'raises an error when we doing cross-database modification using destroy' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.destroy! }.to raise_error(cross_database_exception)
+ end
+ end
+
+ context 'when the tables are ignored on save' do
+ before do
+ stub_const("MainModel", create_main_model(%w[_test_gitlab_ci_items], %I[save]))
+ end
+
+ it 'does not raise an error when creating a new object' do
+ expect { MainModel.create! }.not_to raise_error
+ end
+
+ it 'does not raise an error when updating an existing object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.update!(updated_at: Time.zone.now) }.not_to raise_error
+ end
+
+ it 'still raises an error when deleting an object' do # save doesn't include destroy
+ main_model_object = create_main_model_object
+ expect { main_model_object.destroy! }.to raise_error(cross_database_exception)
+ end
+ end
+
+ context 'when the tables are ignored on save with if statement' do
+ before do
+ stub_const(
+ "MainModel",
+ create_main_model(
+ %w[_test_gitlab_ci_items],
+ %I[save],
+ & proc { condition }
+ )
+ )
+
+ expect_next_instance_of(MainModel) do |instance|
+ allow(instance).to receive(:condition).and_return(condition_value)
+ end
+ end
+
+ context 'when condition returns true' do
+ let(:condition_value) { true }
+
+ it 'does not raise an error on creating a new object' do
+ expect { MainModel.create! }.not_to raise_error
+ end
+ end
+
+ context 'when condition returns false' do
+ let(:condition_value) { false }
+
+ it 'raises an error on creating a new object' do
+ expect { MainModel.create! }.to raise_error(cross_database_exception)
+ end
+ end
+ end
+
+ context 'when the tables are ignored on create' do
+ before do
+ stub_const("MainModel", create_main_model(%w[_test_gitlab_ci_items], %I[create]))
+ end
+
+ it 'does not raise an error when creating a new object' do
+ expect { MainModel.create! }.not_to raise_error
+ end
+
+ it 'raises an error when updating an existing object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.update!(updated_at: Time.zone.now) }.to raise_error(cross_database_exception)
+ end
+
+ it 'still raises an error when deleting an object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.destroy! }.to raise_error(cross_database_exception)
+ end
+ end
+
+ context 'when the tables are ignored on update' do
+ before do
+ stub_const("MainModel", create_main_model(%w[_test_gitlab_ci_items], %I[update]))
+ end
+
+ it 'raises an error when creating a new object' do
+ expect { MainModel.create! }.to raise_error(cross_database_exception)
+ end
+
+ it 'does not raise an error when updating an existing object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.update!(updated_at: Time.zone.now) }.not_to raise_error
+ end
+
+ it 'still raises an error when deleting an object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.destroy! }.to raise_error(cross_database_exception)
+ end
+ end
+
+ context 'when the tables are ignored on create and destroy' do
+ before do
+ stub_const("MainModel", create_main_model(%w[_test_gitlab_ci_items], %I[create destroy]))
+ end
+
+ it 'does not raise an error when creating a new object' do
+ expect { MainModel.create! }.not_to raise_error
+ end
+
+ it 'raises an error when updating an existing object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.update!(updated_at: Time.zone.now) }.to raise_error(cross_database_exception)
+ end
+
+ it 'does not raise an error when deleting an object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.destroy! }.not_to raise_error
+ end
+ end
+ end
+
+ def create_main_model(ignored_tables, events, &condition_block)
+ klass = Class.new(ApplicationRecord) do
+ include CrossDatabaseIgnoredTables
+
+ self.table_name = '_test_gitlab_main_items'
+
+ has_one :ci_model_object, autosave: true, class_name: 'CiModel',
+ inverse_of: 'main_model_object', foreign_key: 'main_model_id',
+ dependent: :nullify, touch: true
+ before_create :prepare_ci_model_object
+
+ def condition
+ true
+ end
+
+ def prepare_ci_model_object
+ build_ci_model_object
+ end
+ end
+
+ if ignored_tables.any? && events.any?
+ klass.class_eval do
+ cross_database_ignore_tables ignored_tables, on: events, url: "TODO", if: condition_block
+ end
+ end
+
+ klass
+ end
+
+ # This helper allows creating a test model object without raising a cross database exception
+ def create_main_model_object
+ Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
+ [CiModel.table_name], url: "TODO"
+ ) do
+ MainModel.create!
+ end
+ end
+end
diff --git a/spec/models/concerns/cross_database_modification_spec.rb b/spec/models/concerns/cross_database_modification_spec.rb
index eaebf613cb5..bca37ffa9d9 100644
--- a/spec/models/concerns/cross_database_modification_spec.rb
+++ b/spec/models/concerns/cross_database_modification_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe CrossDatabaseModification do
expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
Project.transaction do
- expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_main)
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_main_cell)
Project.first
end
diff --git a/spec/models/concerns/enum_inheritance_spec.rb b/spec/models/concerns/enum_inheritance_spec.rb
new file mode 100644
index 00000000000..492503dad36
--- /dev/null
+++ b/spec/models/concerns/enum_inheritance_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+module EnumInheritableTestCase
+ class Animal < ActiveRecord::Base
+ include EnumInheritance
+
+ def self.table_name = '_test_animals'
+ def self.inheritance_column = 'species'
+
+ enum species: {
+ dog: 1,
+ cat: 2,
+ bird: 3
+ }
+
+ def self.inheritance_column_to_class_map = {
+ dog: 'EnumInheritableTestCase::Dog',
+ cat: 'EnumInheritableTestCase::Cat'
+ }.freeze
+ end
+
+ class Dog < Animal; end
+ class Cat < Animal; end
+end
+
+RSpec.describe EnumInheritance, feature_category: :shared do
+ describe '.sti_class_to_enum_map' do
+ it 'is the inverse of sti_class_to_enum_map' do
+ expect(EnumInheritableTestCase::Animal.sti_class_to_enum_map).to include({
+ 'EnumInheritableTestCase::Dog' => :dog,
+ 'EnumInheritableTestCase::Cat' => :cat
+ })
+ end
+ end
+
+ describe '.sti_class_for' do
+ it 'is the base class if no mapping for type is provided' do
+ expect(EnumInheritableTestCase::Animal.sti_class_for('bird')).to be(EnumInheritableTestCase::Animal)
+ end
+
+ it 'is class if mapping for type is provided' do
+ expect(EnumInheritableTestCase::Animal.sti_class_for('dog')).to be(EnumInheritableTestCase::Dog)
+ end
+ end
+
+ describe '.sti_name' do
+ it 'is nil if map does not exist' do
+ expect(EnumInheritableTestCase::Animal.sti_name).to eq("")
+ end
+
+ it 'is nil if map exists' do
+ expect(EnumInheritableTestCase::Dog.sti_name).to eq("dog")
+ end
+ end
+
+ describe 'querying' do
+ before_all do
+ EnumInheritableTestCase::Animal.connection.execute(<<~SQL)
+ CREATE TABLE _test_animals (
+ id bigserial primary key not null,
+ species bigint not null
+ );
+ SQL
+ end
+
+ let_it_be(:dog) { EnumInheritableTestCase::Dog.create! }
+ let_it_be(:cat) { EnumInheritableTestCase::Cat.create! }
+ let_it_be(:bird) { EnumInheritableTestCase::Animal.create!(species: :bird) }
+
+ describe 'object class when querying' do
+ context 'when mapping for type exists' do
+ it 'is the super class', :aggregate_failures do
+ queried_dog = EnumInheritableTestCase::Animal.find_by(id: dog.id)
+ expect(queried_dog).to eq(dog)
+ # Test below is already part of the test above, but it makes the desired behavior explicit
+ expect(queried_dog.class).to eq(EnumInheritableTestCase::Dog)
+
+ queried_cat = EnumInheritableTestCase::Animal.find_by(id: cat.id)
+ expect(queried_cat).to eq(cat)
+ expect(queried_cat.class).to eq(EnumInheritableTestCase::Cat)
+ end
+ end
+
+ context 'when mapping does not exist' do
+ it 'is the base class' do
+ expect(EnumInheritableTestCase::Animal.find_by(id: bird.id).class).to eq(EnumInheritableTestCase::Animal)
+ end
+ end
+ end
+
+ it 'finds by type' do
+ expect(EnumInheritableTestCase::Animal.where(species: :dog).first!).to eq(dog)
+ end
+ end
+end
diff --git a/spec/models/concerns/integrations/reset_secret_fields_spec.rb b/spec/models/concerns/integrations/reset_secret_fields_spec.rb
index 3b15b95fea9..411c79624de 100644
--- a/spec/models/concerns/integrations/reset_secret_fields_spec.rb
+++ b/spec/models/concerns/integrations/reset_secret_fields_spec.rb
@@ -2,14 +2,14 @@
require 'spec_helper'
-RSpec.describe Integrations::ResetSecretFields do
+RSpec.describe Integrations::ResetSecretFields, feature_category: :integrations do
let(:described_class) do
Class.new(Integration) do
- field :username, type: 'text'
- field :url, type: 'text', exposes_secrets: true
- field :api_url, type: 'text', exposes_secrets: true
- field :password, type: 'password'
- field :token, type: 'password'
+ field :username, type: :text
+ field :url, type: :text, exposes_secrets: true
+ field :api_url, type: :text, exposes_secrets: true
+ field :password, type: :password
+ field :token, type: :password
end
end
diff --git a/spec/models/concerns/milestoneable_spec.rb b/spec/models/concerns/milestoneable_spec.rb
index 961eac4710d..cbb0cbf063f 100644
--- a/spec/models/concerns/milestoneable_spec.rb
+++ b/spec/models/concerns/milestoneable_spec.rb
@@ -3,13 +3,14 @@
require 'spec_helper'
RSpec.describe Milestoneable do
- let(:user) { create(:user) }
- let(:milestone) { create(:milestone, project: project) }
+ let_it_be(:group, reload: true) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
shared_examples_for 'an object that can be assigned a milestone' do
describe 'Validation' do
describe 'milestone' do
- let(:project) { create(:project, :repository) }
let(:milestone_id) { milestone.id }
subject { milestoneable_class.new(params) }
@@ -39,8 +40,6 @@ RSpec.describe Milestoneable do
end
describe '#milestone_available?' do
- let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) }
def build_milestoneable(milestone_id)
@@ -62,9 +61,9 @@ RSpec.describe Milestoneable do
it 'returns true with a milestone from the the parent of the issue project group' do
parent = create(:group)
group.update!(parent: parent)
- milestone = create(:milestone, group: parent)
+ parent_milestone = create(:milestone, group: parent)
- expect(build_milestoneable(milestone.id).milestone_available?).to be(true)
+ expect(build_milestoneable(parent_milestone.id).milestone_available?).to be(true)
end
it 'returns true with a blank milestone' do
@@ -86,9 +85,6 @@ RSpec.describe Milestoneable do
end
describe '#supports_milestone?' do
- let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
-
context "for issues" do
let(:issue) { build(:issue, project: project) }
@@ -215,6 +211,15 @@ RSpec.describe Milestoneable do
end
it_behaves_like 'an object that can be assigned a milestone'
+
+ describe '#milestone_available?' do
+ it 'returns true with a milestone from the issue group' do
+ milestone = create(:milestone, group: group)
+ milestoneable = milestoneable_class.new(namespace: group, milestone_id: milestone.id)
+
+ expect(milestoneable.milestone_available?).to be_truthy
+ end
+ end
end
context 'MergeRequests' do
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index c1323d20d83..dd180749e94 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -391,8 +391,8 @@ RSpec.describe Noteable, feature_category: :code_review_workflow do
end
describe '.resolvable_types' do
- it 'exposes the replyable types' do
- expect(described_class.resolvable_types).to include('MergeRequest', 'DesignManagement::Design')
+ it 'exposes the resolvable types' do
+ expect(described_class.resolvable_types).to include('Issue', 'MergeRequest', 'DesignManagement::Design')
end
end
diff --git a/spec/models/concerns/reset_on_union_error_spec.rb b/spec/models/concerns/reset_on_union_error_spec.rb
new file mode 100644
index 00000000000..70993b92c90
--- /dev/null
+++ b/spec/models/concerns/reset_on_union_error_spec.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ResetOnUnionError, :delete, feature_category: :shared do
+ let(:test_unioned_model) do
+ Class.new(ApplicationRecord) do
+ include FromUnion
+
+ self.table_name = '_test_unioned_model'
+
+ def self.name
+ 'TestUnion'
+ end
+ end
+ end
+
+ before(:context) do
+ ApplicationRecord.connection.execute(<<~SQL)
+ CREATE TABLE _test_unioned_model (
+ id serial NOT NULL PRIMARY KEY,
+ created_at timestamptz NOT NULL
+ );
+ SQL
+ end
+
+ after(:context) do
+ ApplicationRecord.connection.execute(<<~SQL)
+ DROP TABLE _test_unioned_model
+ SQL
+ end
+
+ context 'with mismatched columns due to schema cache' do
+ def load_query
+ scopes = [
+ test_unioned_model.select('*'),
+ test_unioned_model.select(test_unioned_model.column_names.join(','))
+ ]
+
+ test_unioned_model.from_union(scopes).load
+ end
+
+ before do
+ load_query
+
+ ApplicationRecord.connection.execute(<<~SQL)
+ ALTER TABLE _test_unioned_model ADD COLUMN _test_new_column int;
+ SQL
+ end
+
+ after do
+ ApplicationRecord.connection.execute(<<~SQL)
+ ALTER TABLE _test_unioned_model DROP COLUMN _test_new_column;
+ SQL
+
+ test_unioned_model.reset_column_information
+ end
+
+ it 'resets column information when encountering an UNION error' do
+ expect do
+ load_query
+ end.to raise_error(ActiveRecord::StatementInvalid, /must have the same number of columns/)
+ .and change { test_unioned_model.column_names }.from(%w[id created_at]).to(%w[id created_at _test_new_column])
+
+ # Subsequent query load from new schema cache, so no more error
+ expect do
+ load_query
+ end.not_to raise_error
+ end
+
+ it 'logs when column is reset' do
+ expect(Gitlab::ErrorTracking::Logger).to receive(:error)
+ .with(hash_including("extra.reset_model_name" => "TestUnion"))
+ .and_call_original
+
+ expect do
+ load_query
+ end.to raise_error(ActiveRecord::StatementInvalid, /must have the same number of columns/)
+ end
+
+ context 'when reset_column_information_on_statement_invalid FF is disabled' do
+ before do
+ stub_feature_flags(reset_column_information_on_statement_invalid: false)
+ end
+
+ it 'does not reset column information' do
+ expect do
+ load_query
+ end.to raise_error(ActiveRecord::StatementInvalid, /must have the same number of columns/)
+ .and not_change { test_unioned_model.column_names }
+ end
+ end
+ end
+
+ context 'with mismatched columns due to coding error' do
+ def load_mismatched_query
+ scopes = [
+ test_unioned_model.select("id"),
+ test_unioned_model.select("id, created_at")
+ ]
+
+ test_unioned_model.from_union(scopes).load
+ end
+
+ it 'limits reset_column_information calls' do
+ expect(test_unioned_model).to receive(:reset_column_information).and_call_original
+
+ expect do
+ load_mismatched_query
+ end.to raise_error(ActiveRecord::StatementInvalid, /must have the same number of columns/)
+
+ expect(test_unioned_model).not_to receive(:reset_column_information)
+
+ expect do
+ load_mismatched_query
+ end.to raise_error(ActiveRecord::StatementInvalid, /must have the same number of columns/)
+ end
+
+ it 'does reset_column_information after some time has passed' do
+ expect do
+ load_mismatched_query
+ end.to raise_error(ActiveRecord::StatementInvalid, /must have the same number of columns/)
+
+ travel_to(described_class::MAX_RESET_PERIOD.from_now + 1.minute)
+ expect(test_unioned_model).to receive(:reset_column_information).and_call_original
+
+ expect do
+ load_mismatched_query
+ end.to raise_error(ActiveRecord::StatementInvalid, /must have the same number of columns/)
+ end
+ end
+end
diff --git a/spec/models/concerns/resolvable_discussion_spec.rb b/spec/models/concerns/resolvable_discussion_spec.rb
index 7e08f47fb5a..1423b56fa5d 100644
--- a/spec/models/concerns/resolvable_discussion_spec.rb
+++ b/spec/models/concerns/resolvable_discussion_spec.rb
@@ -2,14 +2,16 @@
require 'spec_helper'
-RSpec.describe Discussion, ResolvableDiscussion do
+RSpec.describe Discussion, ResolvableDiscussion, feature_category: :code_review_workflow do
subject { described_class.new([first_note, second_note, third_note]) }
- let(:first_note) { create(:discussion_note_on_merge_request) }
- let(:noteable) { first_note.noteable }
- let(:project) { first_note.project }
- let(:second_note) { create(:discussion_note_on_merge_request, noteable: noteable, project: project, in_reply_to: first_note) }
- let(:third_note) { create(:discussion_note_on_merge_request, noteable: noteable, project: project) }
+ let_it_be(:first_note, reload: true) { create(:discussion_note_on_merge_request) }
+ let_it_be(:noteable) { first_note.noteable }
+ let_it_be(:project) { first_note.project }
+ let_it_be(:second_note, reload: true) { create(:discussion_note_on_merge_request, noteable: noteable, project: project, in_reply_to: first_note) }
+ let_it_be(:third_note, reload: true) { create(:discussion_note_on_merge_request, noteable: noteable, project: project) }
+
+ let_it_be(:current_user) { create(:user) }
describe "#resolvable?" do
context "when potentially resolvable" do
@@ -154,8 +156,6 @@ RSpec.describe Discussion, ResolvableDiscussion do
end
describe "#can_resolve?" do
- let(:current_user) { create(:user) }
-
context "when not resolvable" do
before do
allow(subject).to receive(:resolvable?).and_return(false)
@@ -201,8 +201,8 @@ RSpec.describe Discussion, ResolvableDiscussion do
end
context "when the signed in user can push to the project" do
- before do
- subject.project.add_maintainer(current_user)
+ before_all do
+ project.add_maintainer(current_user)
end
it "returns true" do
@@ -211,7 +211,7 @@ RSpec.describe Discussion, ResolvableDiscussion do
context "when the noteable has no author" do
before do
- subject.noteable.author = nil
+ noteable.author = nil
end
it "returns true" do
@@ -240,8 +240,6 @@ RSpec.describe Discussion, ResolvableDiscussion do
end
describe "#resolve!" do
- let(:current_user) { create(:user) }
-
context "when not resolvable" do
before do
allow(subject).to receive(:resolvable?).and_return(false)
@@ -271,8 +269,8 @@ RSpec.describe Discussion, ResolvableDiscussion do
end
context "when resolvable" do
- let(:user) { create(:user) }
- let(:second_note) { create(:diff_note_on_commit) } # unresolvable
+ let_it_be(:user) { create(:user) }
+ let_it_be(:second_note) { create(:diff_note_on_commit) } # unresolvable
before do
allow(subject).to receive(:resolvable?).and_return(true)
@@ -447,6 +445,12 @@ RSpec.describe Discussion, ResolvableDiscussion do
expect(subject.resolved?).to be true
end
+
+ it "expires the etag cache of the noteable" do
+ expect(subject.noteable).to receive(:expire_note_etag_cache)
+
+ subject.resolve!(current_user)
+ end
end
end
end
@@ -463,7 +467,7 @@ RSpec.describe Discussion, ResolvableDiscussion do
end
context "when resolvable" do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
before do
allow(subject).to receive(:resolvable?).and_return(true)
@@ -527,6 +531,12 @@ RSpec.describe Discussion, ResolvableDiscussion do
expect(subject.resolved?).to be false
end
+
+ it "expires the etag cache of the noteable" do
+ expect(subject.noteable).to receive(:expire_note_etag_cache)
+
+ subject.unresolve!
+ end
end
context "when some resolvable notes are resolved" do
@@ -565,7 +575,6 @@ RSpec.describe Discussion, ResolvableDiscussion do
end
describe "#last_resolved_note" do
- let(:current_user) { create(:user) }
let(:time) { Time.current.utc }
before do
diff --git a/spec/models/concerns/resolvable_note_spec.rb b/spec/models/concerns/resolvable_note_spec.rb
index 09646f6c4eb..c57f3ba6d84 100644
--- a/spec/models/concerns/resolvable_note_spec.rb
+++ b/spec/models/concerns/resolvable_note_spec.rb
@@ -2,41 +2,42 @@
require 'spec_helper'
-RSpec.describe Note, ResolvableNote do
- let(:project) { create(:project, :repository) }
- let(:merge_request) { create(:merge_request, source_project: project) }
+RSpec.describe Note, ResolvableNote, feature_category: :code_review_workflow do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
subject { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
context 'resolvability scopes' do
- let!(:note1) { create(:note, project: project) }
- let!(:note2) { create(:diff_note_on_commit, project: project) }
- let!(:note3) { create(:diff_note_on_merge_request, :resolved, noteable: merge_request, project: project) }
- let!(:note4) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
- let!(:note5) { create(:discussion_note_on_issue, project: project) }
- let!(:note6) { create(:discussion_note_on_merge_request, :system, noteable: merge_request, project: project) }
+ let_it_be(:note1) { create(:note, project: project) }
+ let_it_be(:note2) { create(:diff_note_on_commit, project: project) }
+ let_it_be(:note3) { create(:diff_note_on_merge_request, :resolved, noteable: merge_request, project: project) }
+ let_it_be(:note4) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
+ let_it_be(:note5) { create(:discussion_note_on_issue, project: project) }
+ let_it_be(:note6) { create(:discussion_note_on_merge_request, :system, noteable: merge_request, project: project) }
+ let_it_be(:note7) { create(:discussion_note_on_issue, :resolved, project: project) }
describe '.potentially_resolvable' do
- it 'includes diff and discussion notes on merge requests' do
- expect(described_class.potentially_resolvable).to match_array([note3, note4, note6])
+ it 'includes diff and discussion notes on issues and merge requests' do
+ expect(described_class.potentially_resolvable).to match_array([note3, note4, note5, note6, note7])
end
end
describe '.resolvable' do
- it 'includes non-system diff and discussion notes on merge requests' do
- expect(described_class.resolvable).to match_array([note3, note4])
+ it 'includes non-system diff and discussion notes on issues and merge requests' do
+ expect(described_class.resolvable).to match_array([note3, note4, note5, note7])
end
end
describe '.resolved' do
- it 'includes resolved non-system diff and discussion notes on merge requests' do
- expect(described_class.resolved).to match_array([note3])
+ it 'includes resolved non-system diff and discussion notes on issues and merge requests' do
+ expect(described_class.resolved).to match_array([note3, note7])
end
end
describe '.unresolved' do
- it 'includes non-resolved non-system diff and discussion notes on merge requests' do
- expect(described_class.unresolved).to match_array([note4])
+ it 'includes non-resolved non-system diff and discussion notes on issues and merge requests' do
+ expect(described_class.unresolved).to match_array([note4, note5])
end
end
end
@@ -55,11 +56,13 @@ RSpec.describe Note, ResolvableNote do
unresolved_note.reload
end
- it 'resolves only the resolvable, not yet resolved notes' do
+ it 'resolves only the resolvable, not yet resolved notes', :freeze_time do
expect(commit_note.resolved_at).to be_nil
expect(resolved_note.resolved_by).not_to eq(current_user)
+
expect(unresolved_note.resolved_at).not_to be_nil
expect(unresolved_note.resolved_by).to eq(current_user)
+ expect(unresolved_note.updated_at).to be_like_time(Time.current)
end
end
@@ -72,9 +75,10 @@ RSpec.describe Note, ResolvableNote do
resolved_note.reload
end
- it 'unresolves the resolved notes' do
+ it 'unresolves the resolved notes', :freeze_time do
expect(resolved_note.resolved_by).to be_nil
expect(resolved_note.resolved_at).to be_nil
+ expect(resolved_note.updated_at).to be_like_time(Time.current)
end
end
@@ -272,6 +276,12 @@ RSpec.describe Note, ResolvableNote do
expect(subject.resolved?).to be true
end
+
+ it "updates the updated_at timestamp", :freeze_time do
+ subject.resolve!(current_user)
+
+ expect(subject.updated_at).to be_like_time(Time.current)
+ end
end
end
end
@@ -320,6 +330,12 @@ RSpec.describe Note, ResolvableNote do
expect(subject.resolved?).to be false
end
+
+ it "updates the updated_at timestamp", :freeze_time do
+ subject.unresolve!
+
+ expect(subject.updated_at).to be_like_time(Time.current)
+ end
end
context "when not resolved" do
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index cbfc1df64f1..822e2817d84 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -89,7 +89,7 @@ RSpec.describe ApplicationSetting, 'TokenAuthenticatable' do
end
describe 'multiple token fields' do
- before(:all) do
+ before_all do
described_class.send(:add_authentication_token_field, :yet_another_token)
end
diff --git a/spec/models/concerns/where_composite_spec.rb b/spec/models/concerns/where_composite_spec.rb
index 6abdd12aac5..b2b9602f5ee 100644
--- a/spec/models/concerns/where_composite_spec.rb
+++ b/spec/models/concerns/where_composite_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe WhereComposite do
describe '.where_composite' do
- let_it_be(:test_table_name) { "_test_table_#{SecureRandom.hex(10)}" }
+ let_it_be(:test_table_name) { "_test_table_where_composite" }
let(:model) do
tbl_name = test_table_name