diff options
Diffstat (limited to 'spec/models/concerns')
-rw-r--r-- | spec/models/concerns/approvable_spec.rb | 24 | ||||
-rw-r--r-- | spec/models/concerns/bulk_insert_safe_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/concerns/bulk_insertable_associations_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/concerns/ci/partitionable/switch_spec.rb | 42 | ||||
-rw-r--r-- | spec/models/concerns/ci/partitionable_spec.rb | 10 | ||||
-rw-r--r-- | spec/models/concerns/cross_database_ignored_tables_spec.rb | 222 | ||||
-rw-r--r-- | spec/models/concerns/cross_database_modification_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/concerns/enum_inheritance_spec.rb | 97 | ||||
-rw-r--r-- | spec/models/concerns/integrations/reset_secret_fields_spec.rb | 12 | ||||
-rw-r--r-- | spec/models/concerns/milestoneable_spec.rb | 25 | ||||
-rw-r--r-- | spec/models/concerns/noteable_spec.rb | 4 | ||||
-rw-r--r-- | spec/models/concerns/reset_on_union_error_spec.rb | 132 | ||||
-rw-r--r-- | spec/models/concerns/resolvable_discussion_spec.rb | 43 | ||||
-rw-r--r-- | spec/models/concerns/resolvable_note_spec.rb | 54 | ||||
-rw-r--r-- | spec/models/concerns/token_authenticatable_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/concerns/where_composite_spec.rb | 2 |
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 |