diff options
Diffstat (limited to 'spec/models')
136 files changed, 4108 insertions, 2403 deletions
diff --git a/spec/models/analytics/cycle_analytics/project_stage_spec.rb b/spec/models/analytics/cycle_analytics/project_stage_spec.rb index fce31af619c..9efe90e7d41 100644 --- a/spec/models/analytics/cycle_analytics/project_stage_spec.rb +++ b/spec/models/analytics/cycle_analytics/project_stage_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Analytics::CycleAnalytics::ProjectStage do end it_behaves_like 'value stream analytics stage' do + let(:factory) { :cycle_analytics_project_stage } let(:parent) { build(:project) } let(:parent_name) { :project } end diff --git a/spec/models/analytics/cycle_analytics/project_value_stream_spec.rb b/spec/models/analytics/cycle_analytics/project_value_stream_spec.rb new file mode 100644 index 00000000000..d84ecedc634 --- /dev/null +++ b/spec/models/analytics/cycle_analytics/project_value_stream_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Analytics::CycleAnalytics::ProjectValueStream, type: :model do + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:stages) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_length_of(:name).is_at_most(100) } + + it 'validates uniqueness of name' do + project = create(:project) + create(:cycle_analytics_project_value_stream, name: 'test', project: project) + + value_stream = build(:cycle_analytics_project_value_stream, name: 'test', project: project) + + expect(value_stream).to be_invalid + expect(value_stream.errors.messages).to eq(name: [I18n.t('errors.messages.taken')]) + end + end + + it 'is not custom' do + expect(described_class.new).not_to be_custom + end + + describe '.build_default_value_stream' do + it 'builds the default value stream' do + project = build(:project) + + value_stream = described_class.build_default_value_stream(project) + expect(value_stream.name).to eq('default') + end + end +end diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb index 37eddf9a22a..2817e177d28 100644 --- a/spec/models/appearance_spec.rb +++ b/spec/models/appearance_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Appearance do create(:appearance) new_row = build(:appearance) - new_row.save + expect { new_row.save! }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Only 1 appearances row can exist') expect(new_row.valid?).to eq(false) end @@ -39,7 +39,7 @@ RSpec.describe Appearance do end it 'returns the path when the upload has been orphaned' do - appearance.send(logo_type).upload.destroy + appearance.send(logo_type).upload.destroy! appearance.reload expect(appearance.send("#{logo_type}_path")).to eq(expected_path) diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb index 7e6ac351e68..24de46cb536 100644 --- a/spec/models/application_record_spec.rb +++ b/spec/models/application_record_spec.rb @@ -13,20 +13,24 @@ RSpec.describe ApplicationRecord do describe '.safe_ensure_unique' do let(:model) { build(:suggestion) } + let_it_be(:note) { create(:diff_note_on_merge_request) } + let(:klass) { model.class } before do - allow(model).to receive(:save).and_raise(ActiveRecord::RecordNotUnique) + allow(model).to receive(:save!).and_raise(ActiveRecord::RecordNotUnique) end it 'returns false when ActiveRecord::RecordNotUnique is raised' do - expect(model).to receive(:save).once - expect(klass.safe_ensure_unique { model.save }).to be_falsey + expect(model).to receive(:save!).once + model.note_id = note.id + expect(klass.safe_ensure_unique { model.save! }).to be_falsey end it 'retries based on retry count specified' do - expect(model).to receive(:save).exactly(3).times - expect(klass.safe_ensure_unique(retries: 2) { model.save }).to be_falsey + expect(model).to receive(:save!).exactly(3).times + model.note_id = note.id + expect(klass.safe_ensure_unique(retries: 2) { model.save! }).to be_falsey end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 808932ce7e4..4b4e7820f7a 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -129,6 +129,11 @@ RSpec.describe ApplicationSetting do it { is_expected.not_to allow_value(nil).for(:notes_create_limit_allowlist) } it { is_expected.to allow_value([]).for(:notes_create_limit_allowlist) } + it { is_expected.to allow_value('all_tiers').for(:whats_new_variant) } + it { is_expected.to allow_value('current_tier').for(:whats_new_variant) } + it { is_expected.to allow_value('disabled').for(:whats_new_variant) } + it { is_expected.not_to allow_value(nil).for(:whats_new_variant) } + context 'help_page_documentation_base_url validations' do it { is_expected.to allow_value(nil).for(:help_page_documentation_base_url) } it { is_expected.to allow_value('https://docs.gitlab.com').for(:help_page_documentation_base_url) } @@ -211,7 +216,8 @@ RSpec.describe ApplicationSetting do setting.spam_check_endpoint_enabled = true end - it { is_expected.to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) } + it { is_expected.to allow_value('grpc://example.org/spam_check').for(:spam_check_endpoint_url) } + it { is_expected.not_to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) } it { is_expected.not_to allow_value('nonsense').for(:spam_check_endpoint_url) } it { is_expected.not_to allow_value(nil).for(:spam_check_endpoint_url) } it { is_expected.not_to allow_value('').for(:spam_check_endpoint_url) } @@ -222,7 +228,8 @@ RSpec.describe ApplicationSetting do setting.spam_check_endpoint_enabled = false end - it { is_expected.to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) } + it { is_expected.to allow_value('grpc://example.org/spam_check').for(:spam_check_endpoint_url) } + it { is_expected.not_to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) } it { is_expected.not_to allow_value('nonsense').for(:spam_check_endpoint_url) } it { is_expected.to allow_value(nil).for(:spam_check_endpoint_url) } it { is_expected.to allow_value('').for(:spam_check_endpoint_url) } @@ -245,7 +252,9 @@ RSpec.describe ApplicationSetting do context "when user accepted let's encrypt terms of service" do before do - setting.update(lets_encrypt_terms_of_service_accepted: true) + expect do + setting.update!(lets_encrypt_terms_of_service_accepted: true) + end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Lets encrypt notification email can't be blank") end it { is_expected.not_to allow_value(nil).for(:lets_encrypt_notification_email) } @@ -295,26 +304,30 @@ RSpec.describe ApplicationSetting do describe 'default_artifacts_expire_in' do it 'sets an error if it cannot parse' do - setting.update(default_artifacts_expire_in: 'a') + expect do + setting.update!(default_artifacts_expire_in: 'a') + end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Default artifacts expire in is not a correct duration") expect_invalid end it 'sets an error if it is blank' do - setting.update(default_artifacts_expire_in: ' ') + expect do + setting.update!(default_artifacts_expire_in: ' ') + end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Default artifacts expire in can't be blank") expect_invalid end it 'sets the value if it is valid' do - setting.update(default_artifacts_expire_in: '30 days') + setting.update!(default_artifacts_expire_in: '30 days') expect(setting).to be_valid expect(setting.default_artifacts_expire_in).to eq('30 days') end it 'sets the value if it is 0' do - setting.update(default_artifacts_expire_in: '0') + setting.update!(default_artifacts_expire_in: '0') expect(setting).to be_valid expect(setting.default_artifacts_expire_in).to eq('0') @@ -393,18 +406,18 @@ RSpec.describe ApplicationSetting do context 'auto_devops_domain setting' do context 'when auto_devops_enabled? is true' do before do - setting.update(auto_devops_enabled: true) + setting.update!(auto_devops_enabled: true) end it 'can be blank' do - setting.update(auto_devops_domain: '') + setting.update!(auto_devops_domain: '') expect(setting).to be_valid end context 'with a valid value' do before do - setting.update(auto_devops_domain: 'domain.com') + setting.update!(auto_devops_domain: 'domain.com') end it 'is valid' do @@ -414,7 +427,9 @@ RSpec.describe ApplicationSetting do context 'with an invalid value' do before do - setting.update(auto_devops_domain: 'definitelynotahostname') + expect do + setting.update!(auto_devops_domain: 'definitelynotahostname') + end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Auto devops domain is not a fully qualified domain name") end it 'is invalid' do @@ -785,6 +800,10 @@ RSpec.describe ApplicationSetting do throttle_authenticated_api_period_in_seconds throttle_authenticated_web_requests_per_period throttle_authenticated_web_period_in_seconds + throttle_unauthenticated_packages_api_requests_per_period + throttle_unauthenticated_packages_api_period_in_seconds + throttle_authenticated_packages_api_requests_per_period + throttle_authenticated_packages_api_period_in_seconds ] end diff --git a/spec/models/board_group_recent_visit_spec.rb b/spec/models/board_group_recent_visit_spec.rb index c6fbd263072..d2d287d8e24 100644 --- a/spec/models/board_group_recent_visit_spec.rb +++ b/spec/models/board_group_recent_visit_spec.rb @@ -3,9 +3,8 @@ require 'spec_helper' RSpec.describe BoardGroupRecentVisit do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:board) { create(:board, group: group) } + let_it_be(:board_parent) { create(:group) } + let_it_be(:board) { create(:board, group: board_parent) } describe 'relationships' do it { is_expected.to belong_to(:user) } @@ -19,56 +18,9 @@ RSpec.describe BoardGroupRecentVisit do it { is_expected.to validate_presence_of(:board) } end - describe '#visited' do - it 'creates a visit if one does not exists' do - expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) - end - - shared_examples 'was visited previously' do - let!(:visit) { create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago } - - it 'updates the timestamp' do - freeze_time do - described_class.visited!(user, board) - - expect(described_class.count).to eq 1 - expect(described_class.first.updated_at).to be_like_time(Time.zone.now) - end - end - end - - it_behaves_like 'was visited previously' - - context 'when we try to create a visit that is not unique' do - before do - expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') - expect(described_class).to receive(:find_or_create_by).and_return(visit) - end - - it_behaves_like 'was visited previously' - end - end - - describe '#latest' do - def create_visit(time) - create :board_group_recent_visit, group: group, user: user, updated_at: time - end - - it 'returns the most recent visited' do - create_visit(7.days.ago) - create_visit(5.days.ago) - recent = create_visit(1.day.ago) - - expect(described_class.latest(user, group)).to eq recent - end - - it 'returns last 3 visited boards' do - create_visit(7.days.ago) - visit1 = create_visit(3.days.ago) - visit2 = create_visit(2.days.ago) - visit3 = create_visit(5.days.ago) - - expect(described_class.latest(user, group, count: 3)).to eq([visit2, visit1, visit3]) - end + it_behaves_like 'boards recent visit' do + let_it_be(:board_relation) { :board } + let_it_be(:board_parent_relation) { :group } + let_it_be(:visit_relation) { :board_group_recent_visit } end end diff --git a/spec/models/board_project_recent_visit_spec.rb b/spec/models/board_project_recent_visit_spec.rb index 145a4f5b1a7..262c3a8faaa 100644 --- a/spec/models/board_project_recent_visit_spec.rb +++ b/spec/models/board_project_recent_visit_spec.rb @@ -3,9 +3,8 @@ require 'spec_helper' RSpec.describe BoardProjectRecentVisit do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:board) { create(:board, project: project) } + let_it_be(:board_parent) { create(:project) } + let_it_be(:board) { create(:board, project: board_parent) } describe 'relationships' do it { is_expected.to belong_to(:user) } @@ -19,56 +18,9 @@ RSpec.describe BoardProjectRecentVisit do it { is_expected.to validate_presence_of(:board) } end - describe '#visited' do - it 'creates a visit if one does not exists' do - expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) - end - - shared_examples 'was visited previously' do - let!(:visit) { create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago } - - it 'updates the timestamp' do - freeze_time do - described_class.visited!(user, board) - - expect(described_class.count).to eq 1 - expect(described_class.first.updated_at).to be_like_time(Time.zone.now) - end - end - end - - it_behaves_like 'was visited previously' - - context 'when we try to create a visit that is not unique' do - before do - expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') - expect(described_class).to receive(:find_or_create_by).and_return(visit) - end - - it_behaves_like 'was visited previously' - end - end - - describe '#latest' do - def create_visit(time) - create :board_project_recent_visit, project: project, user: user, updated_at: time - end - - it 'returns the most recent visited' do - create_visit(7.days.ago) - create_visit(5.days.ago) - recent = create_visit(1.day.ago) - - expect(described_class.latest(user, project)).to eq recent - end - - it 'returns last 3 visited boards' do - create_visit(7.days.ago) - visit1 = create_visit(3.days.ago) - visit2 = create_visit(2.days.ago) - visit3 = create_visit(5.days.ago) - - expect(described_class.latest(user, project, count: 3)).to eq([visit2, visit1, visit3]) - end + it_behaves_like 'boards recent visit' do + let_it_be(:board_relation) { :board } + let_it_be(:board_parent_relation) { :project } + let_it_be(:visit_relation) { :board_project_recent_visit } end end diff --git a/spec/models/board_spec.rb b/spec/models/board_spec.rb index c8a9504d4fc..0b7c21fd0c3 100644 --- a/spec/models/board_spec.rb +++ b/spec/models/board_spec.rb @@ -42,4 +42,46 @@ RSpec.describe Board do expect { project.boards.first_board.find(board_A.id) }.to raise_error(ActiveRecord::RecordNotFound) end end + + describe '#disabled_for?' do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:user) { create(:user) } + + subject { board.disabled_for?(user) } + + shared_examples 'board disabled_for?' do + context 'when current user cannot create non backlog issues' do + it { is_expected.to eq(true) } + end + + context 'when user can create backlog issues' do + before do + board.resource_parent.add_reporter(user) + end + + it { is_expected.to eq(false) } + + context 'when block_issue_repositioning is enabled' do + before do + stub_feature_flags(block_issue_repositioning: group) + end + + it { is_expected.to eq(true) } + end + end + end + + context 'for group board' do + let_it_be(:board) { create(:board, group: group) } + + it_behaves_like 'board disabled_for?' + end + + context 'for project board' do + let_it_be(:board) { create(:board, project: project) } + + it_behaves_like 'board disabled_for?' + end + end end diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index c4d17905637..d981189c6f1 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -120,6 +120,12 @@ RSpec.describe BroadcastMessage do expect(subject.call('/users/name/issues').length).to eq(1) end + it 'returns message if provided a path without a preceding slash' do + create(:broadcast_message, target_path: "/users/*/issues", broadcast_type: broadcast_type) + + expect(subject.call('users/name/issues').length).to eq(1) + end + it 'returns the message for empty target path' do create(:broadcast_message, target_path: "", broadcast_type: broadcast_type) diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb index 652ea431696..d1b7125a6e6 100644 --- a/spec/models/bulk_imports/entity_spec.rb +++ b/spec/models/bulk_imports/entity_spec.rb @@ -125,4 +125,13 @@ RSpec.describe BulkImports::Entity, type: :model do end end end + + describe '#encoded_source_full_path' do + it 'encodes entity source full path' do + expected = 'foo%2Fbar' + entity = build(:bulk_import_entity, source_full_path: 'foo/bar') + + expect(entity.encoded_source_full_path).to eq(expected) + end + end end diff --git a/spec/models/bulk_imports/export_spec.rb b/spec/models/bulk_imports/export_spec.rb new file mode 100644 index 00000000000..d85b77d599b --- /dev/null +++ b/spec/models/bulk_imports/export_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Export, type: :model do + describe 'associations' do + it { is_expected.to belong_to(:group) } + it { is_expected.to belong_to(:project) } + it { is_expected.to have_one(:upload) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:relation) } + it { is_expected.to validate_presence_of(:status) } + + context 'when not associated with a group or project' do + it 'is invalid' do + export = build(:bulk_import_export, group: nil, project: nil) + + expect(export).not_to be_valid + end + end + + context 'when associated with a group' do + it 'is valid' do + export = build(:bulk_import_export, group: build(:group), project: nil) + + expect(export).to be_valid + end + end + + context 'when associated with a project' do + it 'is valid' do + export = build(:bulk_import_export, group: nil, project: build(:project)) + + expect(export).to be_valid + end + end + + context 'when relation is invalid' do + it 'is invalid' do + export = build(:bulk_import_export, relation: 'unsupported') + + expect(export).not_to be_valid + expect(export.errors).to include(:relation) + end + end + end + + describe '#portable' do + context 'when associated with project' do + it 'returns project' do + export = create(:bulk_import_export, project: create(:project), group: nil) + + expect(export.portable).to be_instance_of(Project) + end + end + + context 'when associated with group' do + it 'returns group' do + export = create(:bulk_import_export) + + expect(export.portable).to be_instance_of(Group) + end + end + end + + describe '#config' do + context 'when associated with project' do + it 'returns project config' do + export = create(:bulk_import_export, project: create(:project), group: nil) + + expect(export.config).to be_instance_of(BulkImports::FileTransfer::ProjectConfig) + end + end + + context 'when associated with group' do + it 'returns group config' do + export = create(:bulk_import_export) + + expect(export.config).to be_instance_of(BulkImports::FileTransfer::GroupConfig) + end + end + end +end diff --git a/spec/models/bulk_imports/export_upload_spec.rb b/spec/models/bulk_imports/export_upload_spec.rb new file mode 100644 index 00000000000..641fa4a1b6c --- /dev/null +++ b/spec/models/bulk_imports/export_upload_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::ExportUpload do + subject { described_class.new(export: create(:bulk_import_export)) } + + describe 'associations' do + it { is_expected.to belong_to(:export) } + end + + it 'stores export file' do + method = 'export_file' + filename = 'labels.ndjson.gz' + + subject.public_send("#{method}=", fixture_file_upload("spec/fixtures/bulk_imports/#{filename}")) + subject.save! + + url = "/uploads/-/system/bulk_imports/export_upload/export_file/#{subject.id}/#{filename}" + + expect(subject.public_send(method).url).to eq(url) + end +end diff --git a/spec/models/bulk_imports/file_transfer/group_config_spec.rb b/spec/models/bulk_imports/file_transfer/group_config_spec.rb new file mode 100644 index 00000000000..21da71de3c7 --- /dev/null +++ b/spec/models/bulk_imports/file_transfer/group_config_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::FileTransfer::GroupConfig do + let_it_be(:exportable) { create(:group) } + let_it_be(:hex) { '123' } + + before do + allow(SecureRandom).to receive(:hex).and_return(hex) + end + + subject { described_class.new(exportable) } + + describe '#exportable_tree' do + it 'returns exportable tree' do + expect_next_instance_of(::Gitlab::ImportExport::AttributesFinder) do |finder| + expect(finder).to receive(:find_root).with(:group).and_call_original + end + + expect(subject.portable_tree).not_to be_empty + end + end + + describe '#export_path' do + it 'returns correct export path' do + expect(::Gitlab::ImportExport).to receive(:storage_path).and_return('storage_path') + + expect(subject.export_path).to eq("storage_path/#{exportable.full_path}/#{hex}") + end + end + + describe '#exportable_relations' do + it 'returns a list of top level exportable relations' do + expect(subject.portable_relations).to include('milestones', 'badges', 'boards', 'labels') + end + end +end diff --git a/spec/models/bulk_imports/file_transfer/project_config_spec.rb b/spec/models/bulk_imports/file_transfer/project_config_spec.rb new file mode 100644 index 00000000000..021f96ac2a3 --- /dev/null +++ b/spec/models/bulk_imports/file_transfer/project_config_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::FileTransfer::ProjectConfig do + let_it_be(:exportable) { create(:project) } + let_it_be(:hex) { '123' } + + before do + allow(SecureRandom).to receive(:hex).and_return(hex) + end + + subject { described_class.new(exportable) } + + describe '#exportable_tree' do + it 'returns exportable tree' do + expect_next_instance_of(::Gitlab::ImportExport::AttributesFinder) do |finder| + expect(finder).to receive(:find_root).with(:project).and_call_original + end + + expect(subject.portable_tree).not_to be_empty + end + end + + describe '#export_path' do + it 'returns correct export path' do + expect(::Gitlab::ImportExport).to receive(:storage_path).and_return('storage_path') + + expect(subject.export_path).to eq("storage_path/#{exportable.disk_path}/#{hex}") + end + end + + describe '#exportable_relations' do + it 'returns a list of top level exportable relations' do + expect(subject.portable_relations).to include('issues', 'labels', 'milestones', 'merge_requests') + end + end +end diff --git a/spec/models/bulk_imports/file_transfer_spec.rb b/spec/models/bulk_imports/file_transfer_spec.rb new file mode 100644 index 00000000000..5a2b303626c --- /dev/null +++ b/spec/models/bulk_imports/file_transfer_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::FileTransfer do + describe '.config_for' do + context 'when portable is group' do + it 'returns group config' do + expect(described_class.config_for(build(:group))).to be_instance_of(BulkImports::FileTransfer::GroupConfig) + end + end + + context 'when portable is project' do + it 'returns project config' do + expect(described_class.config_for(build(:project))).to be_instance_of(BulkImports::FileTransfer::ProjectConfig) + end + end + + context 'when portable is unsupported' do + it 'raises an error' do + expect { described_class.config_for(nil) }.to raise_error(BulkImports::FileTransfer::UnsupportedObjectType) + end + end + end +end diff --git a/spec/models/bulk_imports/stage_spec.rb b/spec/models/bulk_imports/stage_spec.rb deleted file mode 100644 index 7765fd4c5c4..00000000000 --- a/spec/models/bulk_imports/stage_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe BulkImports::Stage do - let(:pipelines) do - if Gitlab.ee? - [ - [0, BulkImports::Groups::Pipelines::GroupPipeline], - [1, BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline], - [1, BulkImports::Groups::Pipelines::MembersPipeline], - [1, BulkImports::Groups::Pipelines::LabelsPipeline], - [1, BulkImports::Groups::Pipelines::MilestonesPipeline], - [1, BulkImports::Groups::Pipelines::BadgesPipeline], - [1, 'BulkImports::Groups::Pipelines::IterationsPipeline'.constantize], - [2, 'BulkImports::Groups::Pipelines::EpicsPipeline'.constantize], - [3, 'BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline'.constantize], - [3, 'BulkImports::Groups::Pipelines::EpicEventsPipeline'.constantize], - [4, BulkImports::Groups::Pipelines::EntityFinisher] - ] - else - [ - [0, BulkImports::Groups::Pipelines::GroupPipeline], - [1, BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline], - [1, BulkImports::Groups::Pipelines::MembersPipeline], - [1, BulkImports::Groups::Pipelines::LabelsPipeline], - [1, BulkImports::Groups::Pipelines::MilestonesPipeline], - [1, BulkImports::Groups::Pipelines::BadgesPipeline], - [2, BulkImports::Groups::Pipelines::EntityFinisher] - ] - end - end - - describe '.pipelines' do - it 'list all the pipelines with their stage number, ordered by stage' do - expect(described_class.pipelines).to match_array(pipelines) - end - end - - describe '.pipeline_exists?' do - it 'returns true when the given pipeline name exists in the pipelines list' do - expect(described_class.pipeline_exists?(BulkImports::Groups::Pipelines::GroupPipeline)).to eq(true) - expect(described_class.pipeline_exists?('BulkImports::Groups::Pipelines::GroupPipeline')).to eq(true) - end - - it 'returns false when the given pipeline name exists in the pipelines list' do - expect(described_class.pipeline_exists?('BulkImports::Groups::Pipelines::InexistentPipeline')).to eq(false) - end - end -end diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb index 623e55aad21..4d77bd53158 100644 --- a/spec/models/chat_name_spec.rb +++ b/spec/models/chat_name_spec.rb @@ -6,11 +6,11 @@ RSpec.describe ChatName do let_it_be(:chat_name) { create(:chat_name) } subject { chat_name } - it { is_expected.to belong_to(:service) } + it { is_expected.to belong_to(:integration) } it { is_expected.to belong_to(:user) } it { is_expected.to validate_presence_of(:user) } - it { is_expected.to validate_presence_of(:service) } + it { is_expected.to validate_presence_of(:integration) } it { is_expected.to validate_presence_of(:team_id) } it { is_expected.to validate_presence_of(:chat_id) } @@ -18,7 +18,7 @@ RSpec.describe ChatName do it { is_expected.to validate_uniqueness_of(:chat_id).scoped_to(:service_id, :team_id) } it 'is removed when the project is deleted' do - expect { subject.reload.service.project.delete }.to change { ChatName.count }.by(-1) + expect { subject.reload.integration.project.delete }.to change { ChatName.count }.by(-1) expect(ChatName.where(id: subject.id)).not_to exist end diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb index e343ec0e698..d00d88ae397 100644 --- a/spec/models/ci/build_dependencies_spec.rb +++ b/spec/models/ci/build_dependencies_spec.rb @@ -18,12 +18,8 @@ RSpec.describe Ci::BuildDependencies do let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') } let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') } - before do - stub_feature_flags(ci_validate_build_dependencies_override: false) - end - - describe '#local' do - subject { described_class.new(job).local } + context 'for local dependencies' do + subject { described_class.new(job).all } describe 'jobs from previous stages' do context 'when job is in the first stage' do @@ -52,7 +48,7 @@ RSpec.describe Ci::BuildDependencies do project.add_developer(user) end - let(:retried_job) { Ci::Build.retry(rspec_test, user) } + let!(:retried_job) { Ci::Build.retry(rspec_test, user) } it 'contains the retried job instead of the original one' do is_expected.to contain_exactly(build, retried_job, rubocop_test) @@ -150,7 +146,7 @@ RSpec.describe Ci::BuildDependencies do end end - describe '#cross_pipeline' do + context 'for cross_pipeline dependencies' do let!(:job) do create(:ci_build, pipeline: pipeline, @@ -160,7 +156,7 @@ RSpec.describe Ci::BuildDependencies do subject { described_class.new(job) } - let(:cross_pipeline_deps) { subject.cross_pipeline } + let(:cross_pipeline_deps) { subject.all } context 'when dependency specifications are valid' do context 'when pipeline exists in the hierarchy' do @@ -378,14 +374,6 @@ RSpec.describe Ci::BuildDependencies do end it { is_expected.to eq(false) } - - context 'when ci_validate_build_dependencies_override feature flag is enabled' do - before do - stub_feature_flags(ci_validate_build_dependencies_override: job.project) - end - - it { is_expected.to eq(true) } - end end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 339dffa507f..66d2f5f4ee9 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1132,7 +1132,7 @@ RSpec.describe Ci::Build do it "executes UPDATE query" do recorded = ActiveRecord::QueryRecorder.new { subject } - expect(recorded.log.select { |l| l.match?(/UPDATE.*ci_builds/) }.count).to eq(1) + expect(recorded.log.count { |l| l.match?(/UPDATE.*ci_builds/) }).to eq(1) end end @@ -1140,7 +1140,7 @@ RSpec.describe Ci::Build do it 'does not execute UPDATE query' do recorded = ActiveRecord::QueryRecorder.new { subject } - expect(recorded.log.select { |l| l.match?(/UPDATE.*ci_builds/) }.count).to eq(0) + expect(recorded.log.count { |l| l.match?(/UPDATE.*ci_builds/) }).to eq(0) end end end @@ -1205,7 +1205,7 @@ RSpec.describe Ci::Build do before do allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async) - allow(Deployments::ExecuteHooksWorker).to receive(:perform_async) + allow(Deployments::HooksWorker).to receive(:perform_async) end it 'has deployments record with created status' do @@ -1241,7 +1241,7 @@ RSpec.describe Ci::Build do before do allow(Deployments::UpdateEnvironmentWorker).to receive(:perform_async) - allow(Deployments::ExecuteHooksWorker).to receive(:perform_async) + allow(Deployments::HooksWorker).to receive(:perform_async) end it_behaves_like 'avoid deadlock' @@ -3631,46 +3631,29 @@ RSpec.describe Ci::Build do end let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 1, options: options) } + let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) } - context 'when validates for dependencies is enabled' do - before do - stub_feature_flags(ci_validate_build_dependencies_override: false) - end - - let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) } - - context 'when "dependencies" keyword is not defined' do - let(:options) { {} } - - it { expect(job).to have_valid_build_dependencies } - end - - context 'when "dependencies" keyword is empty' do - let(:options) { { dependencies: [] } } + context 'when "dependencies" keyword is not defined' do + let(:options) { {} } - it { expect(job).to have_valid_build_dependencies } - end + it { expect(job).to have_valid_build_dependencies } + end - context 'when "dependencies" keyword is specified' do - let(:options) { { dependencies: ['test'] } } + context 'when "dependencies" keyword is empty' do + let(:options) { { dependencies: [] } } - it_behaves_like 'validation is active' - end + it { expect(job).to have_valid_build_dependencies } end - context 'when validates for dependencies is disabled' do + context 'when "dependencies" keyword is specified' do let(:options) { { dependencies: ['test'] } } - before do - stub_feature_flags(ci_validate_build_dependencies_override: true) - end - - it_behaves_like 'validation is not active' + it_behaves_like 'validation is active' end end describe 'state transition when build fails' do - let(:service) { ::MergeRequests::AddTodoWhenBuildFailsService.new(project, user) } + let(:service) { ::MergeRequests::AddTodoWhenBuildFailsService.new(project: project, current_user: user) } before do allow(::MergeRequests::AddTodoWhenBuildFailsService).to receive(:new).and_return(service) @@ -4679,25 +4662,30 @@ RSpec.describe Ci::Build do end describe '#execute_hooks' do + before do + build.clear_memoization(:build_data) + end + context 'with project hooks' do + let(:build_data) { double(:BuildData, dup: double(:DupedData)) } + before do create(:project_hook, project: project, job_events: true) end - it 'execute hooks' do - expect_any_instance_of(ProjectHook).to receive(:async_execute) + it 'calls project.execute_hooks(build_data, :job_hooks)' do + expect(::Gitlab::DataBuilder::Build) + .to receive(:build).with(build).and_return(build_data) + expect(build.project) + .to receive(:execute_hooks).with(build_data.dup, :job_hooks) build.execute_hooks end end - context 'without relevant project hooks' do - before do - create(:project_hook, project: project, job_events: false) - end - - it 'does not execute a hook' do - expect_any_instance_of(ProjectHook).not_to receive(:async_execute) + context 'without project hooks' do + it 'does not call project.execute_hooks' do + expect(build.project).not_to receive(:execute_hooks) build.execute_hooks end @@ -4708,8 +4696,10 @@ RSpec.describe Ci::Build do create(:service, active: true, job_events: true, project: project) end - it 'execute services' do - expect_any_instance_of(Service).to receive(:async_execute) + it 'executes services' do + allow_next_found_instance_of(Integration) do |integration| + expect(integration).to receive(:async_execute) + end build.execute_hooks end @@ -4720,8 +4710,10 @@ RSpec.describe Ci::Build do create(:service, active: true, job_events: false, project: project) end - it 'execute services' do - expect_any_instance_of(Service).not_to receive(:async_execute) + it 'does not execute services' do + allow_next_found_instance_of(Integration) do |integration| + expect(integration).not_to receive(:async_execute) + end build.execute_hooks end diff --git a/spec/models/ci/commit_with_pipeline_spec.rb b/spec/models/ci/commit_with_pipeline_spec.rb index 4dd288bde62..320143535e2 100644 --- a/spec/models/ci/commit_with_pipeline_spec.rb +++ b/spec/models/ci/commit_with_pipeline_spec.rb @@ -26,15 +26,47 @@ RSpec.describe Ci::CommitWithPipeline do end end + describe '#lazy_latest_pipeline' do + let(:commit_1) do + described_class.new(Commit.new(RepoHelpers.sample_commit, project)) + end + + let(:commit_2) do + described_class.new(Commit.new(RepoHelpers.another_sample_commit, project)) + end + + let!(:commits) { [commit_1, commit_2] } + + it 'executes only 1 SQL query' do + recorder = ActiveRecord::QueryRecorder.new do + # Running this first ensures we don't run one query for every + # commit. + commits.each(&:lazy_latest_pipeline) + + # This forces the execution of the SQL queries necessary to load the + # data. + commits.each { |c| c.latest_pipeline.try(:id) } + end + + expect(recorder.count).to eq(1) + end + end + describe '#latest_pipeline' do let(:pipeline) { double } shared_examples_for 'fetching latest pipeline' do |ref| it 'returns the latest pipeline for the project' do - expect(commit) - .to receive(:latest_pipeline_for_project) - .with(ref, project) - .and_return(pipeline) + if ref + expect(commit) + .to receive(:latest_pipeline_for_project) + .with(ref, project) + .and_return(pipeline) + else + expect(commit) + .to receive(:lazy_latest_pipeline) + .and_return(pipeline) + end expect(result).to eq(pipeline) end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index cdb123573f1..3c4769764d5 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -602,6 +602,34 @@ RSpec.describe Ci::JobArtifact do end end + context 'FastDestroyAll' do + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:job) { create(:ci_build, pipeline: pipeline, project: project) } + + let!(:job_artifact) { create(:ci_job_artifact, :archive, job: job) } + let(:subjects) { pipeline.job_artifacts } + + describe '.use_fast_destroy' do + it 'performs cascading delete with fast_destroy_all' do + expect(Ci::DeletedObject.count).to eq(0) + expect(subjects.count).to be > 0 + + expect { pipeline.destroy! }.not_to raise_error + + expect(subjects.count).to eq(0) + expect(Ci::DeletedObject.count).to be > 0 + end + + it 'updates project statistics' do + expect(ProjectStatistics).to receive(:increment_statistic).once + .with(project, :build_artifacts_size, -job_artifact.file.size) + + pipeline.destroy! + end + end + end + def file_type_limit_failure_message(type, limit_name) <<~MSG The artifact type `#{type}` is missing its counterpart plan limit which is expected to be named `#{limit_name}`. diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb index 3fe09f05cab..f65483d2290 100644 --- a/spec/models/ci/pipeline_artifact_spec.rb +++ b/spec/models/ci/pipeline_artifact_spec.rb @@ -50,6 +50,30 @@ RSpec.describe Ci::PipelineArtifact, type: :model do end end + describe 'scopes' do + describe '.unlocked' do + subject(:pipeline_artifacts) { described_class.unlocked } + + context 'when pipeline is locked' do + it 'returns an empty collection' do + expect(pipeline_artifacts).to be_empty + end + end + + context 'when pipeline is unlocked' do + before do + create(:ci_pipeline_artifact, :with_coverage_report) + end + + it 'returns unlocked artifacts' do + codequality_report = create(:ci_pipeline_artifact, :with_codequality_mr_diff_report, :unlocked) + + expect(pipeline_artifacts).to eq([codequality_report]) + end + end + end + end + describe 'file is being stored' do subject { create(:ci_pipeline_artifact, :with_coverage_report) } diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 3e5fbbfe823..d5560edbbfd 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -126,16 +126,6 @@ RSpec.describe Ci::PipelineSchedule do end end - context 'when pipeline schedule runs every minute' do - let(:pipeline_schedule) { create(:ci_pipeline_schedule, :every_minute) } - - it "updates next_run_at to the sidekiq worker's execution time" do - travel_to(Time.zone.parse("2019-06-01 12:18:00+0000")) do - expect(pipeline_schedule.next_run_at).to eq(cron_worker_next_run_at) - end - end - end - context 'when there are two different pipeline schedules in different time zones' do let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'Eastern Time (US & Canada)') } let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') } @@ -144,24 +134,6 @@ RSpec.describe Ci::PipelineSchedule do expect(pipeline_schedule_1.next_run_at).not_to eq(pipeline_schedule_2.next_run_at) end end - - context 'when there are two different pipeline schedules in the same time zones' do - let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') } - let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') } - - it 'sets the sames next_run_at' do - expect(pipeline_schedule_1.next_run_at).to eq(pipeline_schedule_2.next_run_at) - end - end - - context 'when updates cron of exsisted pipeline schedule' do - let(:new_cron) { '0 0 1 1 *' } - - it 'updates next_run_at automatically' do - expect { pipeline_schedule.update!(cron: new_cron) } - .to change { pipeline_schedule.next_run_at } - end - end end describe '#schedule_next_run!' do @@ -178,7 +150,7 @@ RSpec.describe Ci::PipelineSchedule do context 'when record is invalid' do before do - allow(pipeline_schedule).to receive(:save!) { raise ActiveRecord::RecordInvalid.new(pipeline_schedule) } + allow(pipeline_schedule).to receive(:save!) { raise ActiveRecord::RecordInvalid, pipeline_schedule } end it 'nullifies the next run at' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index b7f5811e945..b9457055a18 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -68,14 +68,23 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#downloadable_artifacts' do - let(:build) { create(:ci_build, pipeline: pipeline) } + let_it_be(:build) { create(:ci_build, pipeline: pipeline) } + let_it_be(:downloadable_artifact) { create(:ci_job_artifact, :codequality, job: build) } + let_it_be(:expired_artifact) { create(:ci_job_artifact, :junit, :expired, job: build) } + let_it_be(:undownloadable_artifact) { create(:ci_job_artifact, :trace, job: build) } + + context 'when artifacts are locked' do + it 'returns downloadable artifacts including locked artifacts' do + expect(pipeline.downloadable_artifacts).to contain_exactly(downloadable_artifact, expired_artifact) + end + end - it 'returns downloadable artifacts that have not expired' do - downloadable_artifact = create(:ci_job_artifact, :codequality, job: build) - _expired_artifact = create(:ci_job_artifact, :junit, :expired, job: build) - _undownloadable_artifact = create(:ci_job_artifact, :trace, job: build) + context 'when artifacts are unlocked' do + it 'returns only downloadable artifacts not expired' do + expired_artifact.job.pipeline.unlocked! - expect(pipeline.downloadable_artifacts).to contain_exactly(downloadable_artifact) + expect(pipeline.reload.downloadable_artifacts).to contain_exactly(downloadable_artifact) + end end end end @@ -1939,6 +1948,30 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do expect(pipeline.modified_paths).to match(merge_request.modified_paths) end end + + context 'when source is an external pull request' do + let(:pipeline) do + create(:ci_pipeline, source: :external_pull_request_event, external_pull_request: external_pull_request) + end + + let(:external_pull_request) do + create(:external_pull_request, project: project, target_sha: '281d3a7', source_sha: '498214d') + end + + it 'returns external pull request modified paths' do + expect(pipeline.modified_paths).to match(external_pull_request.modified_paths) + end + + context 'when the FF ci_modified_paths_of_external_prs is disabled' do + before do + stub_feature_flags(ci_modified_paths_of_external_prs: false) + end + + it 'returns nil' do + expect(pipeline.modified_paths).to be_nil + end + end + end end describe '#all_worktree_paths' do @@ -3201,18 +3234,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do expect(pipeline.messages.map(&:content)).to contain_exactly('The error message') end - - context 'when feature flag ci_store_pipeline_messages is disabled' do - before do - stub_feature_flags(ci_store_pipeline_messages: false) - end - - it 'does not add pipeline error message' do - pipeline.add_error_message('The error message') - - expect(pipeline.messages).to be_empty - end - end end describe '#has_yaml_errors?' do @@ -4303,26 +4324,80 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end - describe 'reset_ancestor_bridges!' do - let_it_be(:pipeline) { create(:ci_pipeline, :created) } + describe '#reset_source_bridge!' do + let(:pipeline) { create(:ci_pipeline, :created, project: project) } + + subject(:reset_bridge) { pipeline.reset_source_bridge!(project.owner) } + + # This whole block will be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/329194 + # It contains some duplicate checks. + context 'when the FF ci_reset_bridge_with_subsequent_jobs is disabled' do + before do + stub_feature_flags(ci_reset_bridge_with_subsequent_jobs: false) + end + + context 'when the pipeline is a child pipeline and the bridge is depended' do + let!(:parent_pipeline) { create(:ci_pipeline) } + let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) } + + it 'marks source bridge as pending' do + reset_bridge + + expect(bridge.reload).to be_pending + end + + context 'when the parent pipeline has subsequent jobs after the bridge' do + let!(:after_bridge_job) { create(:ci_build, :skipped, pipeline: parent_pipeline, stage_idx: bridge.stage_idx + 1) } + + it 'does not touch subsequent jobs of the bridge' do + reset_bridge + + expect(after_bridge_job.reload).to be_skipped + end + end + + context 'when the parent pipeline has a dependent upstream pipeline' do + let!(:upstream_bridge) do + create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true) + end + + it 'marks all source bridges as pending' do + reset_bridge + + expect(bridge.reload).to be_pending + expect(upstream_bridge.reload).to be_pending + end + end + end + end context 'when the pipeline is a child pipeline and the bridge is depended' do let!(:parent_pipeline) { create(:ci_pipeline) } let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) } it 'marks source bridge as pending' do - pipeline.reset_ancestor_bridges! + reset_bridge expect(bridge.reload).to be_pending end + context 'when the parent pipeline has subsequent jobs after the bridge' do + let!(:after_bridge_job) { create(:ci_build, :skipped, pipeline: parent_pipeline, stage_idx: bridge.stage_idx + 1) } + + it 'marks subsequent jobs of the bridge as processable' do + reset_bridge + + expect(after_bridge_job.reload).to be_created + end + end + context 'when the parent pipeline has a dependent upstream pipeline' do let!(:upstream_bridge) do create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true) end it 'marks all source bridges as pending' do - pipeline.reset_ancestor_bridges! + reset_bridge expect(bridge.reload).to be_pending expect(upstream_bridge.reload).to be_pending @@ -4335,7 +4410,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do let!(:bridge) { create_bridge(parent_pipeline, pipeline, false) } it 'does not touch source bridge' do - pipeline.reset_ancestor_bridges! + reset_bridge expect(bridge.reload).to be_success end @@ -4346,7 +4421,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end it 'does not touch any source bridge' do - pipeline.reset_ancestor_bridges! + reset_bridge expect(bridge.reload).to be_success expect(upstream_bridge.reload).to be_success diff --git a/spec/models/ci/runner_namespace_spec.rb b/spec/models/ci/runner_namespace_spec.rb new file mode 100644 index 00000000000..41d805adb9f --- /dev/null +++ b/spec/models/ci/runner_namespace_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::RunnerNamespace do + it_behaves_like 'includes Limitable concern' do + subject { build(:ci_runner_namespace, group: create(:group, :nested), runner: create(:ci_runner, :group)) } + end +end diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb new file mode 100644 index 00000000000..13369dba2cf --- /dev/null +++ b/spec/models/ci/runner_project_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::RunnerProject do + it_behaves_like 'includes Limitable concern' do + subject { build(:ci_runner_project, project: create(:project), runner: create(:ci_runner, :project)) } + end +end diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index e46d9189c86..5e0fcb4882f 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -286,6 +286,18 @@ RSpec.describe Ci::Stage, :models do end end + context 'when stage has statuses with nil idx' do + before do + create(:ci_build, :running, stage_id: stage.id, stage_idx: nil) + create(:ci_build, :running, stage_id: stage.id, stage_idx: 10) + create(:ci_build, :running, stage_id: stage.id, stage_idx: nil) + end + + it 'sets index to a non-empty value' do + expect { stage.update_legacy_status }.to change { stage.reload.position }.from(nil).to(10) + end + end + context 'when stage does not have statuses' do it 'fallbacks to zero' do expect(stage.reload.position).to be_nil diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb index a85a72eba0b..ea7a55480a8 100644 --- a/spec/models/clusters/agent_spec.rb +++ b/spec/models/clusters/agent_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Clusters::Agent do it { is_expected.to belong_to(:created_by_user).class_name('User').optional } it { is_expected.to belong_to(:project).class_name('::Project') } it { is_expected.to have_many(:agent_tokens).class_name('Clusters::AgentToken') } + it { is_expected.to have_many(:last_used_agent_tokens).class_name('Clusters::AgentToken') } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_length_of(:name).is_at_most(63) } diff --git a/spec/models/clusters/agent_token_spec.rb b/spec/models/clusters/agent_token_spec.rb index 680b351d24a..bde4798abec 100644 --- a/spec/models/clusters/agent_token_spec.rb +++ b/spec/models/clusters/agent_token_spec.rb @@ -9,6 +9,19 @@ RSpec.describe Clusters::AgentToken do it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_presence_of(:name) } + describe 'scopes' do + describe '.order_last_used_at_desc' do + let_it_be(:token_1) { create(:cluster_agent_token, last_used_at: 7.days.ago) } + let_it_be(:token_2) { create(:cluster_agent_token, last_used_at: nil) } + let_it_be(:token_3) { create(:cluster_agent_token, last_used_at: 2.days.ago) } + + it 'sorts by last_used_at descending, with null values at last' do + expect(described_class.order_last_used_at_desc) + .to eq([token_3, token_1, token_2]) + end + end + end + describe '#token' do it 'is generated on save' do agent_token = build(:cluster_agent_token, token_encrypted: nil) diff --git a/spec/models/clusters/applications/elastic_stack_spec.rb b/spec/models/clusters/applications/elastic_stack_spec.rb index 74cacd486b0..af2802d5e47 100644 --- a/spec/models/clusters/applications/elastic_stack_spec.rb +++ b/spec/models/clusters/applications/elastic_stack_spec.rb @@ -10,6 +10,41 @@ RSpec.describe Clusters::Applications::ElasticStack do include_examples 'cluster application version specs', :clusters_applications_elastic_stack include_examples 'cluster application helm specs', :clusters_applications_elastic_stack + describe 'cluster.integration_elastic_stack state synchronization' do + let!(:application) { create(:clusters_applications_elastic_stack) } + let(:cluster) { application.cluster } + let(:integration) { cluster.integration_elastic_stack } + + describe 'after_destroy' do + it 'disables the corresponding integration' do + application.destroy! + + expect(integration).not_to be_enabled + end + end + + describe 'on install' do + it 'enables the corresponding integration' do + application.make_scheduled! + application.make_installing! + application.make_installed! + + expect(integration).to be_enabled + end + end + + describe 'on uninstall' do + it 'disables the corresponding integration' do + application.make_scheduled! + application.make_installing! + application.make_installed! + application.make_externally_uninstalled! + + expect(integration).not_to be_enabled + end + end + end + describe '#install_command' do let!(:elastic_stack) { create(:clusters_applications_elastic_stack) } @@ -138,78 +173,5 @@ RSpec.describe Clusters::Applications::ElasticStack do end end - describe '#elasticsearch_client' do - context 'cluster is nil' do - it 'returns nil' do - expect(subject.cluster).to be_nil - expect(subject.elasticsearch_client).to be_nil - end - end - - context "cluster doesn't have kubeclient" do - let(:cluster) { create(:cluster) } - - subject { create(:clusters_applications_elastic_stack, cluster: cluster) } - - it 'returns nil' do - expect(subject.elasticsearch_client).to be_nil - end - end - - context 'cluster has kubeclient' do - let(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:kubernetes_url) { subject.cluster.platform_kubernetes.api_url } - let(:kube_client) { subject.cluster.kubeclient.core_client } - - subject { create(:clusters_applications_elastic_stack, cluster: cluster) } - - before do - subject.cluster.platform_kubernetes.namespace = 'a-namespace' - stub_kubeclient_discover(cluster.platform_kubernetes.api_url) - - create(:cluster_kubernetes_namespace, - cluster: cluster, - cluster_project: cluster.cluster_project, - project: cluster.cluster_project.project) - end - - it 'creates proxy elasticsearch_client' do - expect(subject.elasticsearch_client).to be_instance_of(Elasticsearch::Transport::Client) - end - - it 'copies proxy_url, options and headers from kube client to elasticsearch_client' do - expect(Elasticsearch::Client) - .to(receive(:new)) - .with(url: a_valid_url) - .and_call_original - - client = subject.elasticsearch_client - faraday_connection = client.transport.connections.first.connection - - expect(faraday_connection.headers["Authorization"]).to eq(kube_client.headers[:Authorization]) - expect(faraday_connection.ssl.cert_store).to be_instance_of(OpenSSL::X509::Store) - expect(faraday_connection.ssl.verify).to eq(1) - expect(faraday_connection.options.timeout).to be_nil - end - - context 'when cluster is not reachable' do - before do - allow(kube_client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) - end - - it 'returns nil' do - expect(subject.elasticsearch_client).to be_nil - end - end - - context 'when timeout is provided' do - it 'sets timeout in elasticsearch_client' do - client = subject.elasticsearch_client(timeout: 123) - faraday_connection = client.transport.connections.first.connection - - expect(faraday_connection.options.timeout).to eq(123) - end - end - end - end + it_behaves_like 'cluster-based #elasticsearch_client', :clusters_applications_elastic_stack end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 5a0ccabd467..549a273e2d7 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -13,16 +13,13 @@ RSpec.describe Clusters::Applications::Prometheus do include_examples 'cluster application initial status specs' describe 'after_destroy' do - context 'cluster type is project' do - let(:cluster) { create(:cluster, :with_installed_helm) } - let(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } + let(:cluster) { create(:cluster, :with_installed_helm) } + let(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } - it 'deactivates prometheus_service after destroy' do - expect(Clusters::Applications::DeactivateServiceWorker) - .to receive(:perform_async).with(cluster.id, 'prometheus') + it 'disables the corresponding integration' do + application.destroy! - application.destroy! - end + expect(cluster.integration_prometheus).not_to be_enabled end end @@ -31,11 +28,10 @@ RSpec.describe Clusters::Applications::Prometheus do let(:cluster) { create(:cluster, :with_installed_helm) } let(:application) { create(:clusters_applications_prometheus, :installing, cluster: cluster) } - it 'schedules post installation job' do - expect(Clusters::Applications::ActivateServiceWorker) - .to receive(:perform_async).with(cluster.id, 'prometheus') - + it 'enables the corresponding integration' do application.make_installed + + expect(cluster.integration_prometheus).to be_enabled end end @@ -44,11 +40,10 @@ RSpec.describe Clusters::Applications::Prometheus do let(:cluster) { create(:cluster, :with_installed_helm) } let(:application) { create(:clusters_applications_prometheus, :installing, cluster: cluster) } - it 'schedules post installation job' do - expect(Clusters::Applications::ActivateServiceWorker) - .to receive(:perform_async).with(cluster.id, 'prometheus') - + it 'enables the corresponding integration' do application.make_externally_installed! + + expect(cluster.integration_prometheus).to be_enabled end end @@ -65,6 +60,26 @@ RSpec.describe Clusters::Applications::Prometheus do end end + describe '#managed_prometheus?' do + subject { prometheus.managed_prometheus? } + + let(:prometheus) { build(:clusters_applications_prometheus) } + + it { is_expected.to be_truthy } + + context 'externally installed' do + let(:prometheus) { build(:clusters_applications_prometheus, :externally_installed) } + + it { is_expected.to be_falsey } + end + + context 'uninstalled' do + let(:prometheus) { build(:clusters_applications_prometheus, :uninstalled) } + + it { is_expected.to be_falsey } + end + end + describe '#can_uninstall?' do let(:prometheus) { create(:clusters_applications_prometheus) } @@ -318,42 +333,10 @@ RSpec.describe Clusters::Applications::Prometheus do describe 'alert manager token' do subject { create(:clusters_applications_prometheus) } - context 'when not set' do - it 'is empty by default' do - expect(subject.alert_manager_token).to be_nil - expect(subject.encrypted_alert_manager_token).to be_nil - expect(subject.encrypted_alert_manager_token_iv).to be_nil - end - - describe '#generate_alert_manager_token!' do - it 'generates a token' do - subject.generate_alert_manager_token! - - expect(subject.alert_manager_token).to match(/\A\h{32}\z/) - end - end - end - - context 'when set' do - let(:token) { SecureRandom.hex } - - before do - subject.update!(alert_manager_token: token) - end - - it 'reads the token' do - expect(subject.alert_manager_token).to eq(token) - expect(subject.encrypted_alert_manager_token).not_to be_nil - expect(subject.encrypted_alert_manager_token_iv).not_to be_nil - end - - describe '#generate_alert_manager_token!' do - it 'does not re-generate the token' do - subject.generate_alert_manager_token! - - expect(subject.alert_manager_token).to eq(token) - end - end + it 'is autogenerated on creation' do + expect(subject.alert_manager_token).to match(/\A\h{32}\z/) + expect(subject.encrypted_alert_manager_token).not_to be_nil + expect(subject.encrypted_alert_manager_token_iv).not_to be_nil end end end diff --git a/spec/models/clusters/integrations/elastic_stack_spec.rb b/spec/models/clusters/integrations/elastic_stack_spec.rb new file mode 100644 index 00000000000..be4d59b52a2 --- /dev/null +++ b/spec/models/clusters/integrations/elastic_stack_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Clusters::Integrations::ElasticStack do + include KubernetesHelpers + include StubRequests + + describe 'associations' do + it { is_expected.to belong_to(:cluster).class_name('Clusters::Cluster') } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:cluster) } + it { is_expected.not_to allow_value(nil).for(:enabled) } + end + + it_behaves_like 'cluster-based #elasticsearch_client', :clusters_integrations_elastic_stack +end diff --git a/spec/models/clusters/integrations/prometheus_spec.rb b/spec/models/clusters/integrations/prometheus_spec.rb index a7be1673ce2..680786189ad 100644 --- a/spec/models/clusters/integrations/prometheus_spec.rb +++ b/spec/models/clusters/integrations/prometheus_spec.rb @@ -15,6 +15,62 @@ RSpec.describe Clusters::Integrations::Prometheus do it { is_expected.not_to allow_value(nil).for(:enabled) } end + describe 'after_destroy' do + subject(:integration) { create(:clusters_integrations_prometheus, cluster: cluster, enabled: true) } + + let(:cluster) { create(:cluster, :with_installed_helm) } + + it 'deactivates prometheus_service' do + expect(Clusters::Applications::DeactivateServiceWorker) + .to receive(:perform_async).with(cluster.id, 'prometheus') + + integration.destroy! + end + end + + describe 'after_save' do + subject(:integration) { create(:clusters_integrations_prometheus, cluster: cluster, enabled: enabled) } + + let(:cluster) { create(:cluster, :with_installed_helm) } + let(:enabled) { true } + + context 'when no change to enabled status' do + it 'does not touch project services' do + integration # ensure integration exists before we set the expectations + + expect(Clusters::Applications::DeactivateServiceWorker) + .not_to receive(:perform_async) + + expect(Clusters::Applications::ActivateServiceWorker) + .not_to receive(:perform_async) + + integration.update!(enabled: enabled) + end + end + + context 'when enabling' do + let(:enabled) { false } + + it 'deactivates prometheus_service' do + expect(Clusters::Applications::ActivateServiceWorker) + .to receive(:perform_async).with(cluster.id, 'prometheus') + + integration.update!(enabled: true) + end + end + + context 'when disabling' do + let(:enabled) { true } + + it 'activates prometheus_service' do + expect(Clusters::Applications::DeactivateServiceWorker) + .to receive(:perform_async).with(cluster.id, 'prometheus') + + integration.update!(enabled: false) + end + end + end + describe '#prometheus_client' do include_examples '#prometheus_client shared' do let(:factory) { :clusters_integrations_prometheus } diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index e64dee2d26f..feb2f3630c1 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -259,6 +259,40 @@ RSpec.describe CommitStatus do end end + describe '#queued_duration' do + subject { commit_status.queued_duration } + + around do |example| + travel_to(Time.current) { example.run } + end + + context 'when created, then enqueued, then started' do + before do + commit_status.queued_at = 30.seconds.ago + commit_status.started_at = 25.seconds.ago + end + + it { is_expected.to eq(5.0) } + end + + context 'when created but not yet enqueued' do + before do + commit_status.queued_at = nil + end + + it { is_expected.to be_nil } + end + + context 'when enqueued, but not started' do + before do + commit_status.queued_at = Time.current - 1.minute + commit_status.started_at = nil + end + + it { is_expected.to eq(1.minute) } + end + end + describe '.latest' do subject { described_class.latest.order(:id) } diff --git a/spec/models/concerns/bulk_insert_safe_spec.rb b/spec/models/concerns/bulk_insert_safe_spec.rb index e40b0cf11ff..ca6df506ee8 100644 --- a/spec/models/concerns/bulk_insert_safe_spec.rb +++ b/spec/models/concerns/bulk_insert_safe_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' RSpec.describe BulkInsertSafe do before(:all) do ActiveRecord::Schema.define do + create_table :bulk_insert_parent_items, force: true do |t| + t.string :name, null: false + end + create_table :bulk_insert_items, force: true do |t| t.string :name, null: true t.integer :enum_value, null: false @@ -12,6 +16,7 @@ RSpec.describe BulkInsertSafe do t.string :encrypted_secret_value_iv, null: false t.binary :sha_value, null: false, limit: 20 t.jsonb :jsonb_value, null: false + t.belongs_to :bulk_insert_parent_item, foreign_key: true, null: true t.index :name, unique: true end @@ -21,9 +26,23 @@ RSpec.describe BulkInsertSafe do after(:all) do ActiveRecord::Schema.define do drop_table :bulk_insert_items, force: true + drop_table :bulk_insert_parent_items, force: true end end + BulkInsertParentItem = Class.new(ActiveRecord::Base) do + self.table_name = :bulk_insert_parent_items + self.inheritance_column = :_type_disabled + + def self.name + table_name.singularize.camelcase + end + end + + let_it_be(:bulk_insert_parent_item) do + BulkInsertParentItem.create!(name: 'parent') + end + let_it_be(:bulk_insert_item_class) do Class.new(ActiveRecord::Base) do self.table_name = 'bulk_insert_items' @@ -33,6 +52,8 @@ RSpec.describe BulkInsertSafe do validates :name, :enum_value, :secret_value, :sha_value, :jsonb_value, presence: true + belongs_to :bulk_insert_parent_item + sha_attribute :sha_value enum enum_value: { case_1: 1 } @@ -51,8 +72,8 @@ RSpec.describe BulkInsertSafe do 'BulkInsertItem' end - def self.valid_list(count) - Array.new(count) { |n| new(name: "item-#{n}", secret_value: 'my-secret') } + def self.valid_list(count, bulk_insert_parent_item: nil) + Array.new(count) { |n| new(name: "item-#{n}", secret_value: 'my-secret', bulk_insert_parent_item: bulk_insert_parent_item) } end def self.invalid_list(count) @@ -117,6 +138,14 @@ RSpec.describe BulkInsertSafe do bulk_insert_item_class.bulk_insert!(items, batch_size: 5) end + it 'inserts items with belongs_to association' do + items = bulk_insert_item_class.valid_list(10, bulk_insert_parent_item: bulk_insert_parent_item) + + bulk_insert_item_class.bulk_insert!(items, batch_size: 5) + + expect(bulk_insert_item_class.last(items.size).map(&:bulk_insert_parent_item)).to eq([bulk_insert_parent_item] * 10) + end + it 'items can be properly fetched from database' do items = bulk_insert_item_class.valid_list(10) @@ -129,8 +158,7 @@ RSpec.describe BulkInsertSafe do it 'rolls back the transaction when any item is invalid' do # second batch is bad - all_items = bulk_insert_item_class.valid_list(10) + - bulk_insert_item_class.invalid_list(10) + all_items = bulk_insert_item_class.valid_list(10) + bulk_insert_item_class.invalid_list(10) expect do bulk_insert_item_class.bulk_insert!(all_items, batch_size: 2) rescue nil diff --git a/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb b/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb index ddff9ce32b4..02cd8557231 100644 --- a/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb +++ b/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb @@ -142,7 +142,7 @@ RSpec.describe NamespaceSetting, 'CascadingNamespaceSettingAttribute' do end it 'does not allow the local value to be saved' do - subgroup_settings.delayed_project_removal = nil + subgroup_settings.delayed_project_removal = false expect { subgroup_settings.save! } .to raise_error(ActiveRecord::RecordInvalid, /Delayed project removal cannot be changed because it is locked by an ancestor/) @@ -164,6 +164,19 @@ RSpec.describe NamespaceSetting, 'CascadingNamespaceSettingAttribute' do end end + describe '#delayed_project_removal=' do + before do + subgroup_settings.update!(delayed_project_removal: nil) + group_settings.update!(delayed_project_removal: true) + end + + it 'does not save the value locally when it matches the cascaded value' do + subgroup_settings.update!(delayed_project_removal: true) + + expect(subgroup_settings.read_attribute(:delayed_project_removal)).to eq(nil) + end + end + describe '#delayed_project_removal_locked?' do shared_examples 'not locked' do it 'is not locked by an ancestor' do @@ -189,6 +202,20 @@ RSpec.describe NamespaceSetting, 'CascadingNamespaceSettingAttribute' do it_behaves_like 'not locked' end + context 'when attribute is locked by self' do + before do + subgroup_settings.update!(lock_delayed_project_removal: true) + end + + it 'is not locked by default' do + expect(subgroup_settings.delayed_project_removal_locked?).to eq(false) + end + + it 'is locked when including self' do + expect(subgroup_settings.delayed_project_removal_locked?(include_self: true)).to eq(true) + end + end + context 'when parent does not lock the attribute' do it_behaves_like 'not locked' end @@ -277,6 +304,13 @@ RSpec.describe NamespaceSetting, 'CascadingNamespaceSettingAttribute' do expect { subgroup_settings.save! } .to raise_error(ActiveRecord::RecordInvalid, /Delayed project removal cannot be nil when locking the attribute/) end + + it 'copies the cascaded value when locking the attribute if the local value is nil', :aggregate_failures do + subgroup_settings.delayed_project_removal = nil + subgroup_settings.lock_delayed_project_removal = true + + expect(subgroup_settings.read_attribute(:delayed_project_removal)).to eq(false) + end end context 'when application settings locks the attribute' do diff --git a/spec/models/concerns/chronic_duration_attribute_spec.rb b/spec/models/concerns/chronic_duration_attribute_spec.rb index e6dbf403b63..00e28e19bd5 100644 --- a/spec/models/concerns/chronic_duration_attribute_spec.rb +++ b/spec/models/concerns/chronic_duration_attribute_spec.rb @@ -56,8 +56,7 @@ RSpec.shared_examples 'ChronicDurationAttribute writer' do subject.send("#{virtual_field}=", '-10m') expect(subject.valid?).to be_falsey - expect(subject.errors&.messages) - .to include(base: ['Maximum job timeout has a value which could not be accepted']) + expect(subject.errors.added?(:base, 'Maximum job timeout has a value which could not be accepted')).to be true end end diff --git a/spec/models/concerns/ci/maskable_spec.rb b/spec/models/concerns/ci/maskable_spec.rb index 840a08b6060..2b13fc21fe8 100644 --- a/spec/models/concerns/ci/maskable_spec.rb +++ b/spec/models/concerns/ci/maskable_spec.rb @@ -66,7 +66,7 @@ RSpec.describe Ci::Maskable do end it 'matches valid strings' do - expect(subject.match?('Hello+World_123/@:-.')).to eq(true) + expect(subject.match?('Hello+World_123/@:-~.')).to eq(true) end end diff --git a/spec/models/concerns/cron_schedulable_spec.rb b/spec/models/concerns/cron_schedulable_spec.rb new file mode 100644 index 00000000000..39c3d5e55d3 --- /dev/null +++ b/spec/models/concerns/cron_schedulable_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe CronSchedulable do + let(:ideal_next_run_at) { schedule.send(:ideal_next_run_from, Time.zone.now) } + let(:cron_worker_next_run_at) { schedule.send(:cron_worker_next_run_from, Time.zone.now) } + + context 'for ci_pipeline_schedule' do + let(:schedule) { create(:ci_pipeline_schedule, :every_minute) } + let(:schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') } + let(:schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') } + let(:new_cron) { '0 0 1 1 *' } + + it_behaves_like 'handles set_next_run_at' + end +end diff --git a/spec/models/concerns/has_integrations_spec.rb b/spec/models/concerns/has_integrations_spec.rb new file mode 100644 index 00000000000..6e55a1c8b01 --- /dev/null +++ b/spec/models/concerns/has_integrations_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe HasIntegrations do + let_it_be(:project_1) { create(:project) } + let_it_be(:project_2) { create(:project) } + let_it_be(:project_3) { create(:project) } + let_it_be(:project_4) { create(:project) } + let_it_be(:instance_integration) { create(:jira_service, :instance) } + + before do + create(:jira_service, project: project_1, inherit_from_id: instance_integration.id) + create(:jira_service, project: project_2, inherit_from_id: nil) + create(:jira_service, group: create(:group), project: nil, inherit_from_id: nil) + create(:jira_service, project: project_3, inherit_from_id: nil) + create(:slack_service, project: project_4, inherit_from_id: nil) + end + + describe '.with_custom_integration_for' do + it 'returns projects with custom integrations' do + # We use pagination to verify that the group is excluded from the query + expect(Project.with_custom_integration_for(instance_integration, 0, 2)).to contain_exactly(project_2, project_3) + expect(Project.with_custom_integration_for(instance_integration)).to contain_exactly(project_2, project_3) + end + end + + describe '.without_integration' do + it 'returns projects without integration' do + expect(Project.without_integration(instance_integration)).to contain_exactly(project_4) + end + end +end diff --git a/spec/models/concerns/has_timelogs_report_spec.rb b/spec/models/concerns/has_timelogs_report_spec.rb index f694fc350ee..f0dca47fae1 100644 --- a/spec/models/concerns/has_timelogs_report_spec.rb +++ b/spec/models/concerns/has_timelogs_report_spec.rb @@ -3,16 +3,20 @@ require 'spec_helper' RSpec.describe HasTimelogsReport do - let(:user) { create(:user) } + let_it_be(:user) { create(:user) } + let(:group) { create(:group) } - let(:issue) { create(:issue, project: create(:project, :public, group: group)) } + let(:project) { create(:project, :public, group: group) } + let(:issue1) { create(:issue, project: project) } + let(:merge_request1) { create(:merge_request, source_project: project) } describe '#timelogs' do - let!(:timelog1) { create_timelog(15.days.ago) } - let!(:timelog2) { create_timelog(10.days.ago) } - let!(:timelog3) { create_timelog(5.days.ago) } - let(:start_time) { 20.days.ago } - let(:end_time) { 8.days.ago } + let_it_be(:start_time) { 20.days.ago } + let_it_be(:end_time) { 8.days.ago } + + let!(:timelog1) { create_timelog(15.days.ago, issue: issue1) } + let!(:timelog2) { create_timelog(10.days.ago, merge_request: merge_request1) } + let!(:timelog3) { create_timelog(5.days.ago, issue: issue1) } before do group.add_developer(user) @@ -45,7 +49,7 @@ RSpec.describe HasTimelogsReport do end end - def create_timelog(time) - create(:timelog, issue: issue, user: user, spent_at: time) + def create_timelog(time, issue: nil, merge_request: nil) + create(:timelog, issue: issue, merge_request: merge_request, user: user, spent_at: time) end end diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb index a7117af81a2..38766d8decd 100644 --- a/spec/models/concerns/noteable_spec.rb +++ b/spec/models/concerns/noteable_spec.rb @@ -288,7 +288,7 @@ RSpec.describe Noteable do end before do - MergeRequests::MergeToRefService.new(merge_request.project, merge_request.author).execute(merge_request) + MergeRequests::MergeToRefService.new(project: merge_request.project, current_user: merge_request.author).execute(merge_request) Discussions::CaptureDiffNotePositionsService.new(merge_request).execute end diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index 6ab87053258..0a433a8cf4f 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -62,7 +62,7 @@ RSpec.describe Routable do end end -RSpec.describe Group, 'Routable' do +RSpec.describe Group, 'Routable', :with_clean_rails_cache do let_it_be_with_reload(:group) { create(:group, name: 'foo') } let_it_be(:nested_group) { create(:group, parent: group) } @@ -165,19 +165,63 @@ RSpec.describe Group, 'Routable' do end end + describe '#parent_loaded?' do + before do + group.parent = create(:group) + group.save! + + group.reload + end + + it 'is false when the parent is not loaded' do + expect(group.parent_loaded?).to be_falsey + end + + it 'is true when the parent is loaded' do + group.parent + + expect(group.parent_loaded?).to be_truthy + end + end + + describe '#route_loaded?' do + it 'is false when the route is not loaded' do + expect(group.route_loaded?).to be_falsey + end + + it 'is true when the route is loaded' do + group.route + + expect(group.route_loaded?).to be_truthy + end + end + describe '#full_path' do it { expect(group.full_path).to eq(group.path) } it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") } + + it 'hits the cache when not preloaded' do + forcibly_hit_cached_lookup(nested_group, :full_path) + + expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") + end end describe '#full_name' do it { expect(group.full_name).to eq(group.name) } it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") } + + it 'hits the cache when not preloaded' do + forcibly_hit_cached_lookup(nested_group, :full_name) + + expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") + end end end -RSpec.describe Project, 'Routable' do - let_it_be(:project) { create(:project) } +RSpec.describe Project, 'Routable', :with_clean_rails_cache do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project) { create(:project, namespace: namespace) } it_behaves_like '.find_by_full_path' do let_it_be(:record) { project } @@ -192,10 +236,30 @@ RSpec.describe Project, 'Routable' do end describe '#full_path' do - it { expect(project.full_path).to eq "#{project.namespace.full_path}/#{project.path}" } + it { expect(project.full_path).to eq "#{namespace.full_path}/#{project.path}" } + + it 'hits the cache when not preloaded' do + forcibly_hit_cached_lookup(project, :full_path) + + expect(project.full_path).to eq("#{namespace.full_path}/#{project.path}") + end end describe '#full_name' do - it { expect(project.full_name).to eq "#{project.namespace.human_name} / #{project.name}" } + it { expect(project.full_name).to eq "#{namespace.human_name} / #{project.name}" } + + it 'hits the cache when not preloaded' do + forcibly_hit_cached_lookup(project, :full_name) + + expect(project.full_name).to eq("#{namespace.human_name} / #{project.name}") + end end end + +def forcibly_hit_cached_lookup(record, method) + stub_feature_flags(cached_route_lookups: true) + expect(record).to receive(:persisted?).and_return(true) + expect(record).to receive(:route_loaded?).and_return(false) + expect(record).to receive(:parent_loaded?).and_return(false) + expect(Gitlab::Cache).to receive(:fetch_once).with([record.cache_key, method]).and_call_original +end diff --git a/spec/models/concerns/sidebars/container_with_html_options_spec.rb b/spec/models/concerns/sidebars/container_with_html_options_spec.rb deleted file mode 100644 index cc83fc84113..00000000000 --- a/spec/models/concerns/sidebars/container_with_html_options_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::ContainerWithHtmlOptions do - subject do - Class.new do - include Sidebars::ContainerWithHtmlOptions - - def title - 'Foo' - end - end.new - end - - describe '#container_html_options' do - it 'includes by default aria-label attribute' do - expect(subject.container_html_options).to eq(aria: { label: 'Foo' }) - end - end -end diff --git a/spec/models/concerns/sidebars/positionable_list_spec.rb b/spec/models/concerns/sidebars/positionable_list_spec.rb deleted file mode 100644 index 231aa5295dd..00000000000 --- a/spec/models/concerns/sidebars/positionable_list_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::PositionableList do - subject do - Class.new do - include Sidebars::PositionableList - end.new - end - - describe '#add_element' do - it 'adds the element to the last position of the list' do - list = [1, 2] - - subject.add_element(list, 3) - - expect(list).to eq([1, 2, 3]) - end - end - - describe '#insert_element_before' do - let(:user) { build(:user) } - let(:list) { [1, user] } - - it 'adds element before the specific element class' do - subject.insert_element_before(list, User, 2) - - expect(list).to eq [1, 2, user] - end - - context 'when reference element does not exist' do - it 'adds the element to the top of the list' do - subject.insert_element_before(list, Project, 2) - - expect(list).to eq [2, 1, user] - end - end - end - - describe '#insert_element_after' do - let(:user) { build(:user) } - let(:list) { [1, user] } - - it 'adds element after the specific element class' do - subject.insert_element_after(list, Integer, 2) - - expect(list).to eq [1, 2, user] - end - - context 'when reference element does not exist' do - it 'adds the element to the end of the list' do - subject.insert_element_after(list, Project, 2) - - expect(list).to eq [1, user, 2] - end - end - end -end diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb index 0ecefff3a97..abaae5b059a 100644 --- a/spec/models/container_repository_spec.rb +++ b/spec/models/container_repository_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe ContainerRepository do + using RSpec::Parameterized::TableSyntax + let(:group) { create(:group, name: 'group') } let(:project) { create(:project, path: 'test', group: group) } @@ -29,18 +31,6 @@ RSpec.describe ContainerRepository do end end - describe '.exists_by_path?' do - it 'returns true for known container repository paths' do - path = ContainerRegistry::Path.new("#{project.full_path}/#{repository.name}") - expect(described_class.exists_by_path?(path)).to be_truthy - end - - it 'returns false for unknown container repository paths' do - path = ContainerRegistry::Path.new('you/dont/know/me') - expect(described_class.exists_by_path?(path)).to be_falsey - end - end - describe '#tag' do it 'has a test tag' do expect(repository.tag('test')).not_to be_nil @@ -359,6 +349,17 @@ RSpec.describe ContainerRepository do it { is_expected.to contain_exactly(repository) } end + describe '.expiration_policy_started_at_nil_or_before' do + let_it_be(:repository1) { create(:container_repository, expiration_policy_started_at: nil) } + let_it_be(:repository2) { create(:container_repository, expiration_policy_started_at: 1.day.ago) } + let_it_be(:repository3) { create(:container_repository, expiration_policy_started_at: 2.hours.ago) } + let_it_be(:repository4) { create(:container_repository, expiration_policy_started_at: 1.week.ago) } + + subject { described_class.expiration_policy_started_at_nil_or_before(3.hours.ago) } + + it { is_expected.to contain_exactly(repository1, repository2, repository4) } + end + describe '.waiting_for_cleanup' do let_it_be(:repository_cleanup_scheduled) { create(:container_repository, :cleanup_scheduled) } let_it_be(:repository_cleanup_unfinished) { create(:container_repository, :cleanup_unfinished) } @@ -368,4 +369,74 @@ RSpec.describe ContainerRepository do it { is_expected.to contain_exactly(repository_cleanup_scheduled, repository_cleanup_unfinished) } end + + describe '.exists_by_path?' do + it 'returns true for known container repository paths' do + path = ContainerRegistry::Path.new("#{project.full_path}/#{repository.name}") + expect(described_class.exists_by_path?(path)).to be_truthy + end + + it 'returns false for unknown container repository paths' do + path = ContainerRegistry::Path.new('you/dont/know/me') + expect(described_class.exists_by_path?(path)).to be_falsey + end + end + + describe '.with_enabled_policy' do + let_it_be(:repository) { create(:container_repository) } + let_it_be(:repository2) { create(:container_repository) } + + subject { described_class.with_enabled_policy } + + before do + repository.project.container_expiration_policy.update!(enabled: true) + end + + it { is_expected.to eq([repository]) } + end + + context 'with repositories' do + let_it_be_with_reload(:repository) { create(:container_repository, :cleanup_unscheduled) } + let_it_be(:other_repository) { create(:container_repository, :cleanup_unscheduled) } + + let(:policy) { repository.project.container_expiration_policy } + + before do + ContainerExpirationPolicy.update_all(enabled: true) + end + + describe '.requiring_cleanup' do + subject { described_class.requiring_cleanup } + + context 'with next_run_at in the future' do + before do + policy.update_column(:next_run_at, 10.minutes.from_now) + end + + it { is_expected.to eq([]) } + end + + context 'with next_run_at in the past' do + before do + policy.update_column(:next_run_at, 10.minutes.ago) + end + + it { is_expected.to eq([repository]) } + end + end + + describe '.with_unfinished_cleanup' do + subject { described_class.with_unfinished_cleanup } + + it { is_expected.to eq([]) } + + context 'with an unfinished repository' do + before do + repository.cleanup_unfinished! + end + + it { is_expected.to eq([repository]) } + end + end + end end diff --git a/spec/models/context_commits_diff_spec.rb b/spec/models/context_commits_diff_spec.rb new file mode 100644 index 00000000000..6e03ea2745e --- /dev/null +++ b/spec/models/context_commits_diff_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ContextCommitsDiff do + let_it_be(:sha1) { "33f3729a45c02fc67d00adb1b8bca394b0e761d9" } + let_it_be(:sha2) { "ae73cb07c9eeaf35924a10f713b364d32b2dd34f" } + let_it_be(:sha3) { "0b4bc9a49b562e85de7cc9e834518ea6828729b9" } + let_it_be(:merge_request) { create(:merge_request) } + let_it_be(:project) { merge_request.project } + let_it_be(:mrcc1) { create(:merge_request_context_commit, merge_request: merge_request, sha: sha1, committed_date: project.commit_by(oid: sha1).committed_date) } + let_it_be(:mrcc2) { create(:merge_request_context_commit, merge_request: merge_request, sha: sha2, committed_date: project.commit_by(oid: sha2).committed_date) } + let_it_be(:mrcc3) { create(:merge_request_context_commit, merge_request: merge_request, sha: sha3, committed_date: project.commit_by(oid: sha3).committed_date) } + + subject { merge_request.context_commits_diff } + + describe ".empty?" do + it 'checks if empty' do + expect(subject.empty?).to be(false) + end + end + + describe '.commits_count' do + it 'reports commits count' do + expect(subject.commits_count).to be(3) + end + end + + describe '.diffs' do + it 'returns instance of Gitlab::Diff::FileCollection::Compare' do + expect(subject.diffs).to be_a(Gitlab::Diff::FileCollection::Compare) + end + + it 'returns all diffs between first and last commits' do + expect(subject.diffs.diff_files.size).to be(5) + end + end + + describe '.raw_diffs' do + before do + allow(subject).to receive(:paths).and_return(["Gemfile.zip", "files/images/6049019_460s.jpg", "files/ruby/feature.rb"]) + end + + it 'returns instance of Gitlab::Git::DiffCollection' do + expect(subject.raw_diffs).to be_a(Gitlab::Git::DiffCollection) + end + + it 'returns only diff for files changed in the context commits' do + expect(subject.raw_diffs.size).to be(3) + end + end + + describe '.diff_refs' do + it 'returns correct sha' do + expect(subject.diff_refs.head_sha).to eq(sha3) + expect(subject.diff_refs.base_sha).to eq("913c66a37b4a45b9769037c55c2d238bd0942d2e") + end + end +end diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb index e34934d393a..4a8b671bab7 100644 --- a/spec/models/custom_emoji_spec.rb +++ b/spec/models/custom_emoji_spec.rb @@ -38,7 +38,7 @@ RSpec.describe CustomEmoji do new_emoji = build(:custom_emoji, name: old_emoji.name, namespace: old_emoji.namespace, group: group) expect(new_emoji).not_to be_valid - expect(new_emoji.errors.messages).to include(name: ["has already been taken"]) + expect(new_emoji.errors.messages).to eq(creator: ["can't be blank"], name: ["has already been taken"]) end it 'disallows non http and https file value' do diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index c9544569ad6..bcd237cbd38 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -72,6 +72,35 @@ RSpec.describe Deployment do end end + describe '.for_environment_name' do + subject { described_class.for_environment_name(project, environment_name) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:production) { create(:environment, :production, project: project) } + let_it_be(:staging) { create(:environment, :staging, project: project) } + let_it_be(:other_project) { create(:project, :repository) } + let_it_be(:other_production) { create(:environment, :production, project: other_project) } + let(:environment_name) { production.name } + + context 'when deployment belongs to the environment' do + let!(:deployment) { create(:deployment, project: project, environment: production) } + + it { is_expected.to eq([deployment]) } + end + + context 'when deployment belongs to the same project but different environment name' do + let!(:deployment) { create(:deployment, project: project, environment: staging) } + + it { is_expected.to be_empty } + end + + context 'when deployment belongs to the same environment name but different project' do + let!(:deployment) { create(:deployment, project: other_project, environment: other_production) } + + it { is_expected.to be_empty } + end + end + describe '.success' do subject { described_class.success } @@ -107,11 +136,13 @@ RSpec.describe Deployment do end end - it 'executes Deployments::ExecuteHooksWorker asynchronously' do - expect(Deployments::ExecuteHooksWorker) - .to receive(:perform_async).with(deployment.id) + it 'executes Deployments::HooksWorker asynchronously' do + freeze_time do + expect(Deployments::HooksWorker) + .to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current) - deployment.run! + deployment.run! + end end it 'executes Deployments::DropOlderDeploymentsWorker asynchronously' do @@ -141,11 +172,13 @@ RSpec.describe Deployment do deployment.succeed! end - it 'executes Deployments::ExecuteHooksWorker asynchronously' do - expect(Deployments::ExecuteHooksWorker) - .to receive(:perform_async).with(deployment.id) + it 'executes Deployments::HooksWorker asynchronously' do + freeze_time do + expect(Deployments::HooksWorker) + .to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current) - deployment.succeed! + deployment.succeed! + end end end @@ -168,11 +201,13 @@ RSpec.describe Deployment do deployment.drop! end - it 'executes Deployments::ExecuteHooksWorker asynchronously' do - expect(Deployments::ExecuteHooksWorker) - .to receive(:perform_async).with(deployment.id) + it 'executes Deployments::HooksWorker asynchronously' do + freeze_time do + expect(Deployments::HooksWorker) + .to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current) - deployment.drop! + deployment.drop! + end end end @@ -195,11 +230,13 @@ RSpec.describe Deployment do deployment.cancel! end - it 'executes Deployments::ExecuteHooksWorker asynchronously' do - expect(Deployments::ExecuteHooksWorker) - .to receive(:perform_async).with(deployment.id) + it 'executes Deployments::HooksWorker asynchronously' do + freeze_time do + expect(Deployments::HooksWorker) + .to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current) - deployment.cancel! + deployment.cancel! + end end end @@ -220,11 +257,13 @@ RSpec.describe Deployment do deployment.skip! end - it 'does not execute Deployments::ExecuteHooksWorker' do - expect(Deployments::ExecuteHooksWorker) - .not_to receive(:perform_async).with(deployment.id) + it 'does not execute Deployments::HooksWorker' do + freeze_time do + expect(Deployments::HooksWorker) + .not_to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current) - deployment.skip! + deployment.skip! + end end end @@ -714,7 +753,7 @@ RSpec.describe Deployment do it 'schedules workers when finishing a deploy' do expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async) expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async) - expect(Deployments::ExecuteHooksWorker).to receive(:perform_async) + expect(Deployments::HooksWorker).to receive(:perform_async) deploy.update_status('success') end diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb index 674d2fc420d..f2ce5e42eaf 100644 --- a/spec/models/design_management/design_spec.rb +++ b/spec/models/design_management/design_spec.rb @@ -512,7 +512,7 @@ RSpec.describe DesignManagement::Design do end describe '#to_reference' do - let(:namespace) { build(:namespace, path: 'sample-namespace') } + let(:namespace) { build(:namespace, id: non_existing_record_id, path: 'sample-namespace') } let(:project) { build(:project, name: 'sample-project', namespace: namespace) } let(:group) { create(:group, name: 'Group', path: 'sample-group') } let(:issue) { build(:issue, iid: 1, project: project) } diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb index 62f2a53ab3c..cd0938682db 100644 --- a/spec/models/email_spec.rb +++ b/spec/models/email_spec.rb @@ -44,12 +44,11 @@ RSpec.describe Email do end end - describe 'delegation' do - let(:user) { create(:user) } - - it 'delegates to :user' do - expect(build(:email, user: user).username).to eq user.username - end + describe 'delegations' do + it { is_expected.to delegate_method(:can?).to(:user) } + it { is_expected.to delegate_method(:username).to(:user) } + it { is_expected.to delegate_method(:pending_invitations).to(:user) } + it { is_expected.to delegate_method(:accept_pending_invitations!).to(:user) } end describe 'Devise emails' do diff --git a/spec/models/external_pull_request_spec.rb b/spec/models/external_pull_request_spec.rb index e0822fc177a..bac2c369d7d 100644 --- a/spec/models/external_pull_request_spec.rb +++ b/spec/models/external_pull_request_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe ExternalPullRequest do - let(:project) { create(:project) } + let_it_be(:project) { create(:project, :repository) } + let(:source_branch) { 'the-branch' } let(:status) { :open } @@ -217,4 +218,18 @@ RSpec.describe ExternalPullRequest do expect(pull_request).not_to be_from_fork end end + + describe '#modified_paths' do + let(:pull_request) do + build(:external_pull_request, project: project, target_sha: '281d3a7', source_sha: '498214d') + end + + subject(:modified_paths) { pull_request.modified_paths } + + it 'returns modified paths' do + expect(modified_paths).to eq ['bar/branch-test.txt', + 'files/js/commit.coffee', + 'with space/README.md'] + end + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 2f82d8a0bbe..5cc5c4d86d6 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Group do it { is_expected.to have_many(:container_repositories) } it { is_expected.to have_many(:milestones) } it { is_expected.to have_many(:group_deploy_keys) } - it { is_expected.to have_many(:services) } + it { is_expected.to have_many(:integrations) } it { is_expected.to have_one(:dependency_proxy_setting) } it { is_expected.to have_many(:dependency_proxy_blobs) } it { is_expected.to have_many(:dependency_proxy_manifests) } @@ -395,18 +395,94 @@ RSpec.describe Group do end end - context 'assigning a new parent' do - let!(:old_parent) { create(:group) } - let!(:new_parent) { create(:group) } + context 'assign a new parent' do let!(:group) { create(:group, parent: old_parent) } + let(:recorded_queries) { ActiveRecord::QueryRecorder.new } + + subject do + recorded_queries.record do + group.update(parent: new_parent) + end + end before do - group.update(parent: new_parent) + subject reload_models(old_parent, new_parent, group) end - it 'updates traversal_ids' do - expect(group.traversal_ids).to eq [new_parent.id, group.id] + context 'within the same hierarchy' do + let!(:root) { create(:group).reload } + let!(:old_parent) { create(:group, parent: root) } + let!(:new_parent) { create(:group, parent: root) } + + it 'updates traversal_ids' do + expect(group.traversal_ids).to eq [root.id, new_parent.id, group.id] + end + + it_behaves_like 'hierarchy with traversal_ids' + it_behaves_like 'locked row' do + let(:row) { root } + end + end + + context 'to another hierarchy' do + let!(:old_parent) { create(:group) } + let!(:new_parent) { create(:group) } + let!(:group) { create(:group, parent: old_parent) } + + it 'updates traversal_ids' do + expect(group.traversal_ids).to eq [new_parent.id, group.id] + end + + it_behaves_like 'locked rows' do + let(:rows) { [old_parent, new_parent] } + end + + context 'old hierarchy' do + let(:root) { old_parent.root_ancestor } + + it_behaves_like 'hierarchy with traversal_ids' + end + + context 'new hierarchy' do + let(:root) { new_parent.root_ancestor } + + it_behaves_like 'hierarchy with traversal_ids' + end + end + + context 'from being a root ancestor' do + let!(:old_parent) { nil } + let!(:new_parent) { create(:group) } + + it 'updates traversal_ids' do + expect(group.traversal_ids).to eq [new_parent.id, group.id] + end + + it_behaves_like 'locked rows' do + let(:rows) { [group, new_parent] } + end + + it_behaves_like 'hierarchy with traversal_ids' do + let(:root) { new_parent } + end + end + + context 'to being a root ancestor' do + let!(:old_parent) { create(:group) } + let!(:new_parent) { nil } + + it 'updates traversal_ids' do + expect(group.traversal_ids).to eq [group.id] + end + + it_behaves_like 'locked rows' do + let(:rows) { [old_parent, group] } + end + + it_behaves_like 'hierarchy with traversal_ids' do + let(:root) { group } + end end end @@ -427,6 +503,58 @@ RSpec.describe Group do end end + context 'traversal queries' do + let_it_be(:group, reload: true) { create(:group, :nested) } + + context 'recursive' do + before do + stub_feature_flags(use_traversal_ids: false) + end + + it_behaves_like 'namespace traversal' + + describe '#self_and_descendants' do + it { expect(group.self_and_descendants.to_sql).not_to include 'traversal_ids @>' } + end + + describe '#descendants' do + it { expect(group.descendants.to_sql).not_to include 'traversal_ids @>' } + end + + describe '#ancestors' do + it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' } + end + end + + context 'linear' do + it_behaves_like 'namespace traversal' + + describe '#self_and_descendants' do + it { expect(group.self_and_descendants.to_sql).to include 'traversal_ids @>' } + end + + describe '#descendants' do + it { expect(group.descendants.to_sql).to include 'traversal_ids @>' } + end + + describe '#ancestors' do + it { expect(group.ancestors.to_sql).to include "\"namespaces\".\"id\" = #{group.parent_id}" } + + it 'hierarchy order' do + expect(group.ancestors(hierarchy_order: :asc).to_sql).to include 'ORDER BY "depth" ASC' + end + + context 'ancestor linear queries feature flag disabled' do + before do + stub_feature_flags(use_traversal_ids_for_ancestors: false) + end + + it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' } + end + end + end + end + describe '.without_integration' do let(:another_group) { create(:group) } let(:instance_integration) { build(:jira_service, :instance) } @@ -504,6 +632,16 @@ RSpec.describe Group do it { is_expected.to match_array([private_group, internal_group]) } end + describe 'with_onboarding_progress' do + subject { described_class.with_onboarding_progress } + + it 'joins onboarding_progress' do + create(:onboarding_progress, namespace: group) + + expect(subject).to eq([group]) + end + end + describe 'for_authorized_group_members' do let_it_be(:group_member1) { create(:group_member, source: private_group, user_id: user1.id, access_level: Gitlab::Access::OWNER) } @@ -579,7 +717,9 @@ RSpec.describe Group do it "is false if avatar is html page" do group.update_attribute(:avatar, 'uploads/avatar.html') - expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp"]) + group.avatar_type + + expect(group.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true end end @@ -953,140 +1093,167 @@ RSpec.describe Group do it { expect(subject.parent).to be_kind_of(described_class) } end - describe '#max_member_access_for_user' do - context 'group shared with another group' do - let(:parent_group_user) { create(:user) } - let(:group_user) { create(:user) } - let(:child_group_user) { create(:user) } - - let_it_be(:group_parent) { create(:group, :private) } - let_it_be(:group) { create(:group, :private, parent: group_parent) } - let_it_be(:group_child) { create(:group, :private, parent: group) } - - let_it_be(:shared_group_parent) { create(:group, :private) } - let_it_be(:shared_group) { create(:group, :private, parent: shared_group_parent) } - let_it_be(:shared_group_child) { create(:group, :private, parent: shared_group) } - - before do - group_parent.add_owner(parent_group_user) - group.add_owner(group_user) - group_child.add_owner(child_group_user) - - create(:group_group_link, { shared_with_group: group, - shared_group: shared_group, - group_access: GroupMember::DEVELOPER }) - end + context "with member access" do + let_it_be(:group_user) { create(:user) } + describe '#max_member_access_for_user' do context 'with user in the group' do - let(:user) { group_user } + before do + group.add_owner(group_user) + end it 'returns correct access level' do - expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) - expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER) - expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER) + expect(group.max_member_access_for_user(group_user)).to eq(Gitlab::Access::OWNER) end + end - context 'with lower group access level than max access level for share' do - let(:user) { create(:user) } + context 'when user is nil' do + it 'returns NO_ACCESS' do + expect(group.max_member_access_for_user(nil)).to eq(Gitlab::Access::NO_ACCESS) + end + end - it 'returns correct access level' do - group.add_reporter(user) + context 'evaluating admin access level' do + let_it_be(:admin) { create(:admin) } - expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) - expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER) - expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER) + context 'when admin mode is enabled', :enable_admin_mode do + it 'returns OWNER by default' do + expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::OWNER) end end - end - context 'with user in the parent group' do - let(:user) { parent_group_user } + context 'when admin mode is disabled' do + it 'returns NO_ACCESS' do + expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::NO_ACCESS) + end + end - it 'returns correct access level' do - expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) - expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) - expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) + it 'returns NO_ACCESS when only concrete membership should be considered' do + expect(group.max_member_access_for_user(admin, only_concrete_membership: true)) + .to eq(Gitlab::Access::NO_ACCESS) end end - context 'with user in the child group' do - let(:user) { child_group_user } + context 'when max_access_for_group is set' do + let(:max_member_access) { 111 } - it 'returns correct access level' do - expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) - expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) - expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) + before do + group_user.max_access_for_group[group.id] = max_member_access end - end - - context 'unrelated project owner' do - let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 } - let!(:group) { create(:group, id: common_id) } - let!(:unrelated_project) { create(:project, id: common_id) } - let(:user) { unrelated_project.owner } - it 'returns correct access level' do - expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) - expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) - expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) + it 'uses the cached value' do + expect(group.max_member_access_for_user(group_user)).to eq(max_member_access) end end + end - context 'user without accepted access request' do - let!(:user) { create(:user) } + describe '#max_member_access' do + context 'group shared with another group' do + let_it_be(:parent_group_user) { create(:user) } + let_it_be(:child_group_user) { create(:user) } + + let_it_be(:group_parent) { create(:group, :private) } + let_it_be(:group) { create(:group, :private, parent: group_parent) } + let_it_be(:group_child) { create(:group, :private, parent: group) } + + let_it_be(:shared_group_parent) { create(:group, :private) } + let_it_be(:shared_group) { create(:group, :private, parent: shared_group_parent) } + let_it_be(:shared_group_child) { create(:group, :private, parent: shared_group) } before do - create(:group_member, :developer, :access_request, user: user, group: group) + group_parent.add_owner(parent_group_user) + group.add_owner(group_user) + group_child.add_owner(child_group_user) + + create(:group_group_link, { shared_with_group: group, + shared_group: shared_group, + group_access: GroupMember::DEVELOPER }) end - it 'returns correct access level' do - expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) - expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) - expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS) + context 'with user in the group' do + it 'returns correct access level' do + expect(shared_group_parent.max_member_access(group_user)).to eq(Gitlab::Access::NO_ACCESS) + expect(shared_group.max_member_access(group_user)).to eq(Gitlab::Access::DEVELOPER) + expect(shared_group_child.max_member_access(group_user)).to eq(Gitlab::Access::DEVELOPER) + end + + context 'with lower group access level than max access level for share' do + let(:user) { create(:user) } + + it 'returns correct access level' do + group.add_reporter(user) + + expect(shared_group_parent.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) + expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::REPORTER) + expect(shared_group_child.max_member_access(user)).to eq(Gitlab::Access::REPORTER) + end + end end - end - end - context 'multiple groups shared with group' do - let(:user) { create(:user) } - let(:group) { create(:group, :private) } - let(:shared_group_parent) { create(:group, :private) } - let(:shared_group) { create(:group, :private, parent: shared_group_parent) } + context 'with user in the parent group' do + it 'returns correct access level' do + expect(shared_group_parent.max_member_access(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS) + expect(shared_group.max_member_access(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS) + expect(shared_group_child.max_member_access(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS) + end + end - before do - group.add_owner(user) + context 'with user in the child group' do + it 'returns correct access level' do + expect(shared_group_parent.max_member_access(child_group_user)).to eq(Gitlab::Access::NO_ACCESS) + expect(shared_group.max_member_access(child_group_user)).to eq(Gitlab::Access::NO_ACCESS) + expect(shared_group_child.max_member_access(child_group_user)).to eq(Gitlab::Access::NO_ACCESS) + end + end - create(:group_group_link, { shared_with_group: group, - shared_group: shared_group, - group_access: GroupMember::DEVELOPER }) - create(:group_group_link, { shared_with_group: group, - shared_group: shared_group_parent, - group_access: GroupMember::MAINTAINER }) - end + context 'unrelated project owner' do + let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 } + let!(:group) { create(:group, id: common_id) } + let!(:unrelated_project) { create(:project, id: common_id) } + let(:user) { unrelated_project.owner } - it 'returns correct access level' do - expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::MAINTAINER) - end - end + it 'returns correct access level' do + expect(shared_group_parent.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) + expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) + expect(shared_group_child.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) + end + end + + context 'user without accepted access request' do + let!(:user) { create(:user) } - context 'evaluating admin access level' do - let_it_be(:admin) { create(:admin) } + before do + create(:group_member, :developer, :access_request, user: user, group: group) + end - context 'when admin mode is enabled', :enable_admin_mode do - it 'returns OWNER by default' do - expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::OWNER) + it 'returns correct access level' do + expect(shared_group_parent.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) + expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) + expect(shared_group_child.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) + end end end - context 'when admin mode is disabled' do - it 'returns NO_ACCESS' do - expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::NO_ACCESS) + context 'multiple groups shared with group' do + let(:user) { create(:user) } + let(:group) { create(:group, :private) } + let(:shared_group_parent) { create(:group, :private) } + let(:shared_group) { create(:group, :private, parent: shared_group_parent) } + + before do + group.add_owner(user) + + create(:group_group_link, { shared_with_group: group, + shared_group: shared_group, + group_access: GroupMember::DEVELOPER }) + create(:group_group_link, { shared_with_group: group, + shared_group: shared_group_parent, + group_access: GroupMember::MAINTAINER }) end - end - it 'returns NO_ACCESS when only concrete membership should be considered' do - expect(group.max_member_access_for_user(admin, only_concrete_membership: true)) - .to eq(Gitlab::Access::NO_ACCESS) + it 'returns correct access level' do + expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::MAINTAINER) + end end end end @@ -1118,7 +1285,7 @@ RSpec.describe Group do end end - describe '#members_with_parents' do + shared_examples_for 'members_with_parents' do let!(:group) { create(:group, :nested) } let!(:maintainer) { group.parent.add_user(create(:user), GroupMember::MAINTAINER) } let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) } @@ -1142,6 +1309,50 @@ RSpec.describe Group do end end + describe '#members_with_parents' do + it_behaves_like 'members_with_parents' + end + + describe '#authorizable_members_with_parents' do + let(:group) { create(:group) } + + it_behaves_like 'members_with_parents' + + context 'members with associated user but also having invite_token' do + let!(:member) { create(:group_member, :developer, :invited, user: create(:user), group: group) } + + it 'includes such members in the result' do + expect(group.authorizable_members_with_parents).to include(member) + end + end + + context 'invited members' do + let!(:member) { create(:group_member, :developer, :invited, group: group) } + + it 'does not include such members in the result' do + expect(group.authorizable_members_with_parents).not_to include(member) + end + end + + context 'members from group shares' do + let(:shared_group) { group } + let(:shared_with_group) { create(:group) } + + before do + create(:group_group_link, shared_group: shared_group, shared_with_group: shared_with_group) + end + + context 'an invited member that is part of the shared_with_group' do + let!(:member) { create(:group_member, :developer, :invited, group: shared_with_group) } + + it 'does not include such members in the result' do + expect(shared_group.authorizable_members_with_parents).not_to( + include(member)) + end + end + end + end + describe '#members_from_self_and_ancestors_with_effective_access_level' do let!(:group_parent) { create(:group, :private) } let!(:group) { create(:group, :private, parent: group_parent) } @@ -1769,13 +1980,35 @@ RSpec.describe Group do allow(project).to receive(:protected_for?).with('ref').and_return(true) end - it 'returns all variables belong to the group and parent groups' do - expected_array1 = [protected_variable, ci_variable] - expected_array2 = [variable_child, variable_child_2, variable_child_3] - got_array = group_child_3.ci_variables_for('ref', project).to_a + context 'traversal queries' do + shared_examples 'correct ancestor order' do + it 'returns all variables belong to the group and parent groups' do + expected_array1 = [protected_variable, ci_variable] + expected_array2 = [variable_child, variable_child_2, variable_child_3] + got_array = group_child_3.ci_variables_for('ref', project).to_a + + expect(got_array.shift(2)).to contain_exactly(*expected_array1) + expect(got_array).to eq(expected_array2) + end + end + + context 'recursive' do + before do + stub_feature_flags(use_traversal_ids: false) + end - expect(got_array.shift(2)).to contain_exactly(*expected_array1) - expect(got_array).to eq(expected_array2) + include_examples 'correct ancestor order' + end + + context 'linear' do + before do + stub_feature_flags(use_traversal_ids: true) + + group_child_3.reload # make sure traversal_ids are reloaded + end + + include_examples 'correct ancestor order' + end end end end @@ -2012,22 +2245,31 @@ RSpec.describe Group do end describe '#access_request_approvers_to_be_notified' do - it 'returns a maximum of ten, active, non_requested owners of the group in recent_sign_in descending order' do - group = create(:group, :public) + let_it_be(:group) { create(:group, :public) } + it 'returns a maximum of ten owners of the group in recent_sign_in descending order' do users = create_list(:user, 12, :with_sign_ins) active_owners = users.map do |user| create(:group_member, :owner, group: group, user: user) end - create(:group_member, :owner, :blocked, group: group) - create(:group_member, :maintainer, group: group) - create(:group_member, :access_request, :owner, group: group) - - active_owners_in_recent_sign_in_desc_order = group.members_and_requesters.where(id: active_owners).order_recent_sign_in.limit(10) + active_owners_in_recent_sign_in_desc_order = group.members_and_requesters + .id_in(active_owners) + .order_recent_sign_in.limit(10) expect(group.access_request_approvers_to_be_notified).to eq(active_owners_in_recent_sign_in_desc_order) end + + it 'returns active, non_invited, non_requested owners of the group' do + owner = create(:group_member, :owner, source: group) + + create(:group_member, :maintainer, group: group) + create(:group_member, :owner, :invited, group: group) + create(:group_member, :owner, :access_request, group: group) + create(:group_member, :owner, :blocked, group: group) + + expect(group.access_request_approvers_to_be_notified.to_a).to eq([owner]) + end end describe '.groups_including_descendants_by' do @@ -2214,17 +2456,17 @@ RSpec.describe Group do end describe "#default_branch_name" do - context "group.namespace_settings does not have a default branch name" do + context "when group.namespace_settings does not have a default branch name" do it "returns nil" do expect(group.default_branch_name).to be_nil end end - context "group.namespace_settings has a default branch name" do + context "when group.namespace_settings has a default branch name" do let(:example_branch_name) { "example_branch_name" } before do - expect(group.namespace_settings) + allow(group.namespace_settings) .to receive(:default_branch_name) .and_return(example_branch_name) end @@ -2361,4 +2603,20 @@ RSpec.describe Group do it { is_expected.to eq(Set.new([child_1.id])) } end + + describe '#to_ability_name' do + it 'returns group' do + group = build(:group) + + expect(group.to_ability_name).to eq('group') + end + end + + describe '#activity_path' do + it 'returns the group activity_path' do + expected_path = "/groups/#{group.name}/-/activity" + + expect(group.activity_path).to eq(expected_path) + end + end end diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb index 69fbc4c3b4f..88149465232 100644 --- a/spec/models/hooks/project_hook_spec.rb +++ b/spec/models/hooks/project_hook_spec.rb @@ -30,4 +30,13 @@ RSpec.describe ProjectHook do expect(described_class.tag_push_hooks).to eq([hook]) end end + + describe '#rate_limit' do + let_it_be(:hook) { create(:project_hook) } + let_it_be(:plan_limits) { create(:plan_limits, :default_plan, web_hook_calls: 100) } + + it 'returns the default limit' do + expect(hook.rate_limit).to be(100) + end + end end diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb index f7045d7ac5e..651716c3280 100644 --- a/spec/models/hooks/service_hook_spec.rb +++ b/spec/models/hooks/service_hook_spec.rb @@ -4,11 +4,11 @@ require 'spec_helper' RSpec.describe ServiceHook do describe 'associations' do - it { is_expected.to belong_to :service } + it { is_expected.to belong_to :integration } end describe 'validations' do - it { is_expected.to validate_presence_of(:service) } + it { is_expected.to validate_presence_of(:integration) } end describe 'execute' do @@ -22,4 +22,12 @@ RSpec.describe ServiceHook do hook.execute(data) end end + + describe '#rate_limit' do + let(:hook) { build(:service_hook) } + + it 'returns nil' do + expect(hook.rate_limit).to be_nil + end + end end diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 02e630cbf27..a72034f1ac5 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -169,4 +169,12 @@ RSpec.describe SystemHook do hook.async_execute(data, hook_name) end end + + describe '#rate_limit' do + let(:hook) { build(:system_hook) } + + it 'returns nil' do + expect(hook.rate_limit).to be_nil + end + end end diff --git a/spec/models/hooks/web_hook_log_archived_spec.rb b/spec/models/hooks/web_hook_log_archived_spec.rb new file mode 100644 index 00000000000..ac726dbaf4f --- /dev/null +++ b/spec/models/hooks/web_hook_log_archived_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WebHookLogArchived do + let(:source_table) { WebHookLog } + let(:destination_table) { described_class } + + it 'has the same columns as the source table' do + column_names_from_source_table = column_names(source_table) + column_names_from_destination_table = column_names(destination_table) + + expect(column_names_from_destination_table).to match_array(column_names_from_source_table) + end + + it 'has the same null constraints as the source table' do + constraints_from_source_table = null_constraints(source_table) + constraints_from_destination_table = null_constraints(destination_table) + + expect(constraints_from_destination_table.to_a).to match_array(constraints_from_source_table.to_a) + end + + it 'inserts the same record as the one in the source table', :aggregate_failures do + expect { create(:web_hook_log) }.to change { destination_table.count }.by(1) + + event_from_source_table = source_table.connection.select_one( + "SELECT * FROM #{source_table.table_name} ORDER BY created_at desc LIMIT 1" + ) + event_from_destination_table = destination_table.connection.select_one( + "SELECT * FROM #{destination_table.table_name} ORDER BY created_at desc LIMIT 1" + ) + + expect(event_from_destination_table).to eq(event_from_source_table) + end + + def column_names(table) + table.connection.select_all(<<~SQL) + SELECT c.column_name + FROM information_schema.columns c + WHERE c.table_name = '#{table.table_name}' + SQL + end + + def null_constraints(table) + table.connection.select_all(<<~SQL) + SELECT c.column_name, c.is_nullable + FROM information_schema.columns c + WHERE c.table_name = '#{table.table_name}' + AND c.column_name != 'created_at' + SQL + end +end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 413e69fb071..b528dbedd2c 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -3,7 +3,15 @@ require 'spec_helper' RSpec.describe WebHook do - let(:hook) { build(:project_hook) } + include AfterNextHelpers + + let_it_be(:project) { create(:project) } + + let(:hook) { build(:project_hook, project: project) } + + around do |example| + freeze_time { example.run } + end describe 'associations' do it { is_expected.to have_many(:web_hook_logs) } @@ -69,18 +77,30 @@ RSpec.describe WebHook do let(:data) { { key: 'value' } } let(:hook_name) { 'project hook' } - before do - expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original + it '#execute' do + expect_next(WebHookService).to receive(:execute) + + hook.execute(data, hook_name) end - it '#execute' do - expect_any_instance_of(WebHookService).to receive(:execute) + it 'does not execute non-executable hooks' do + hook.update!(disabled_until: 1.day.from_now) + + expect(WebHookService).not_to receive(:new) hook.execute(data, hook_name) end it '#async_execute' do - expect_any_instance_of(WebHookService).to receive(:async_execute) + expect_next(WebHookService).to receive(:async_execute) + + hook.async_execute(data, hook_name) + end + + it 'does not async execute non-executable hooks' do + hook.update!(disabled_until: 1.day.from_now) + + expect(WebHookService).not_to receive(:new) hook.async_execute(data, hook_name) end @@ -94,4 +114,170 @@ RSpec.describe WebHook do expect { web_hook.destroy! }.to change(web_hook.web_hook_logs, :count).by(-3) end end + + describe '.executable' do + let(:not_executable) do + [ + [0, Time.current], + [0, 1.minute.from_now], + [1, 1.minute.from_now], + [3, 1.minute.from_now], + [4, nil], + [4, 1.day.ago], + [4, 1.minute.from_now] + ].map do |(recent_failures, disabled_until)| + create(:project_hook, project: project, recent_failures: recent_failures, disabled_until: disabled_until) + end + end + + let(:executables) do + [ + [0, nil], + [0, 1.day.ago], + [1, nil], + [1, 1.day.ago], + [3, nil], + [3, 1.day.ago] + ].map do |(recent_failures, disabled_until)| + create(:project_hook, project: project, recent_failures: recent_failures, disabled_until: disabled_until) + end + end + + it 'finds the correct set of project hooks' do + expect(described_class.where(project_id: project.id).executable).to match_array executables + end + + context 'when the feature flag is not enabled' do + before do + stub_feature_flags(web_hooks_disable_failed: false) + end + + it 'is the same as all' do + expect(described_class.where(project_id: project.id).executable).to match_array(executables + not_executable) + end + end + end + + describe '#executable?' do + let(:web_hook) { create(:project_hook, project: project) } + + where(:recent_failures, :not_until, :executable) do + [ + [0, :not_set, true], + [0, :past, true], + [0, :future, false], + [0, :now, false], + [1, :not_set, true], + [1, :past, true], + [1, :future, false], + [3, :not_set, true], + [3, :past, true], + [3, :future, false], + [4, :not_set, false], + [4, :past, false], + [4, :future, false] + ] + end + + with_them do + # Phasing means we cannot put these values in the where block, + # which is not subject to the frozen time context. + let(:disabled_until) do + case not_until + when :not_set + nil + when :past + 1.minute.ago + when :future + 1.minute.from_now + when :now + Time.current + end + end + + before do + web_hook.update!(recent_failures: recent_failures, disabled_until: disabled_until) + end + + it 'has the correct state' do + expect(web_hook.executable?).to eq(executable) + end + + context 'when the feature flag is enabled for a project' do + before do + stub_feature_flags(web_hooks_disable_failed: project) + end + + it 'has the expected value' do + expect(web_hook.executable?).to eq(executable) + end + end + + context 'when the feature flag is not enabled' do + before do + stub_feature_flags(web_hooks_disable_failed: false) + end + + it 'is executable' do + expect(web_hook).to be_executable + end + end + end + end + + describe '#next_backoff' do + context 'when there was no last backoff' do + before do + hook.backoff_count = 0 + end + + it 'is 10 minutes' do + expect(hook.next_backoff).to eq(described_class::INITIAL_BACKOFF) + end + end + + context 'when we have backed off once' do + before do + hook.backoff_count = 1 + end + + it 'is twice the initial value' do + expect(hook.next_backoff).to eq(20.minutes) + end + end + + context 'when we have backed off 3 times' do + before do + hook.backoff_count = 3 + end + + it 'grows exponentially' do + expect(hook.next_backoff).to eq(80.minutes) + end + end + + context 'when the previous backoff was large' do + before do + hook.backoff_count = 8 # last value before MAX_BACKOFF + end + + it 'does not exceed the max backoff value' do + expect(hook.next_backoff).to eq(described_class::MAX_BACKOFF) + end + end + end + + describe '#enable!' do + it 'makes a hook executable' do + hook.recent_failures = 1000 + + expect { hook.enable! }.to change(hook, :executable?).from(false).to(true) + end + end + + describe '#disable!' do + it 'disables a hook' do + expect { hook.disable! }.to change(hook, :executable?).from(true).to(false) + end + end end diff --git a/spec/models/instance_metadata/kas_spec.rb b/spec/models/instance_metadata/kas_spec.rb new file mode 100644 index 00000000000..f8cc34fa8d3 --- /dev/null +++ b/spec/models/instance_metadata/kas_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::InstanceMetadata::Kas do + it 'has InstanceMetadataPolicy as declarative policy' do + expect(described_class.declarative_policy_class).to eq("InstanceMetadataPolicy") + end + + context 'when KAS is enabled' do + it 'has the correct properties' do + allow(Gitlab::Kas).to receive(:enabled?).and_return(true) + + expect(subject).to have_attributes( + enabled: Gitlab::Kas.enabled?, + version: Gitlab::Kas.version, + external_url: Gitlab::Kas.external_url + ) + end + end + + context 'when KAS is disabled' do + it 'has the correct properties' do + allow(Gitlab::Kas).to receive(:enabled?).and_return(false) + + expect(subject).to have_attributes( + enabled: Gitlab::Kas.enabled?, + version: nil, + external_url: nil + ) + end + end +end diff --git a/spec/models/instance_metadata_spec.rb b/spec/models/instance_metadata_spec.rb index 1835dc8a9af..e3a9167620b 100644 --- a/spec/models/instance_metadata_spec.rb +++ b/spec/models/instance_metadata_spec.rb @@ -6,7 +6,8 @@ RSpec.describe InstanceMetadata do it 'has the correct properties' do expect(subject).to have_attributes( version: Gitlab::VERSION, - revision: Gitlab.revision + revision: Gitlab.revision, + kas: kind_of(::InstanceMetadata::Kas) ) end end diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb index 781e2aece56..77b3778122a 100644 --- a/spec/models/integration_spec.rb +++ b/spec/models/integration_spec.rb @@ -3,31 +3,945 @@ require 'spec_helper' RSpec.describe Integration do - let_it_be(:project_1) { create(:project) } - let_it_be(:project_2) { create(:project) } - let_it_be(:project_3) { create(:project) } - let_it_be(:project_4) { create(:project) } - let_it_be(:instance_integration) { create(:jira_service, :instance) } + using RSpec::Parameterized::TableSyntax - before do - create(:jira_service, project: project_1, inherit_from_id: instance_integration.id) - create(:jira_service, project: project_2, inherit_from_id: nil) - create(:jira_service, group: create(:group), project: nil, inherit_from_id: nil) - create(:jira_service, project: project_3, inherit_from_id: nil) - create(:slack_service, project: project_4, inherit_from_id: nil) + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to belong_to :group } + it { is_expected.to have_one :service_hook } + it { is_expected.to have_one :jira_tracker_data } + it { is_expected.to have_one :issue_tracker_data } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:type) } + + where(:project_id, :group_id, :template, :instance, :valid) do + 1 | nil | false | false | true + nil | 1 | false | false | true + nil | nil | true | false | true + nil | nil | false | true | true + nil | nil | false | false | false + nil | nil | true | true | false + 1 | 1 | false | false | false + 1 | nil | true | false | false + 1 | nil | false | true | false + nil | 1 | true | false | false + nil | 1 | false | true | false + end + + with_them do + it 'validates the service' do + expect(build(:service, project_id: project_id, group_id: group_id, template: template, instance: instance).valid?).to eq(valid) + end + end + + context 'with existing services' do + before_all do + create(:service, :template) + create(:service, :instance) + create(:service, project: project) + create(:service, group: group, project: nil) + end + + it 'allows only one service template per type' do + expect(build(:service, :template)).to be_invalid + end + + it 'allows only one instance service per type' do + expect(build(:service, :instance)).to be_invalid + end + + it 'allows only one project service per type' do + expect(build(:service, project: project)).to be_invalid + end + + it 'allows only one group service per type' do + expect(build(:service, group: group, project: nil)).to be_invalid + end + end + end + + describe 'Scopes' do + describe '.by_type' do + let!(:service1) { create(:jira_service) } + let!(:service2) { create(:jira_service) } + let!(:service3) { create(:redmine_service) } + + subject { described_class.by_type(type) } + + context 'when type is "JiraService"' do + let(:type) { 'JiraService' } + + it { is_expected.to match_array([service1, service2]) } + end + + context 'when type is "RedmineService"' do + let(:type) { 'RedmineService' } + + it { is_expected.to match_array([service3]) } + end + end + + describe '.for_group' do + let!(:service1) { create(:jira_service, project_id: nil, group_id: group.id) } + let!(:service2) { create(:jira_service) } + + it 'returns the right group service' do + expect(described_class.for_group(group)).to match_array([service1]) + end + end + + describe '.confidential_note_hooks' do + it 'includes services where confidential_note_events is true' do + create(:service, active: true, confidential_note_events: true) + + expect(described_class.confidential_note_hooks.count).to eq 1 + end + + it 'excludes services where confidential_note_events is false' do + create(:service, active: true, confidential_note_events: false) + + expect(described_class.confidential_note_hooks.count).to eq 0 + end + end + + describe '.alert_hooks' do + it 'includes services where alert_events is true' do + create(:service, active: true, alert_events: true) + + expect(described_class.alert_hooks.count).to eq 1 + end + + it 'excludes services where alert_events is false' do + create(:service, active: true, alert_events: false) + + expect(described_class.alert_hooks.count).to eq 0 + end + end + end + + describe '#operating?' do + it 'is false when the service is not active' do + expect(build(:service).operating?).to eq(false) + end + + it 'is false when the service is not persisted' do + expect(build(:service, active: true).operating?).to eq(false) + end + + it 'is true when the service is active and persisted' do + expect(create(:service, active: true).operating?).to eq(true) + end + end + + describe "Test Button" do + let(:service) { build(:service, project: project) } + + describe '#can_test?' do + subject { service.can_test? } + + context 'when repository is not empty' do + let(:project) { build(:project, :repository) } + + it { is_expected.to be true } + end + + context 'when repository is empty' do + let(:project) { build(:project) } + + it { is_expected.to be true } + end + + context 'when instance-level service' do + Integration.available_services_types.each do |service_type| + let(:service) do + service_type.constantize.new(instance: true) + end + + it { is_expected.to be_falsey } + end + end + + context 'when group-level service' do + Integration.available_services_types.each do |service_type| + let(:service) do + service_type.constantize.new(group_id: group.id) + end + + it { is_expected.to be_falsey } + end + end + end + + describe '#test' do + let(:data) { 'test' } + + context 'when repository is not empty' do + let(:project) { build(:project, :repository) } + + it 'test runs execute' do + expect(service).to receive(:execute).with(data) + + service.test(data) + end + end + + context 'when repository is empty' do + let(:project) { build(:project) } + + it 'test runs execute' do + expect(service).to receive(:execute).with(data) + + service.test(data) + end + end + end + end + + describe '#project_level?' do + it 'is true when service has a project' do + expect(build(:service, project: project)).to be_project_level + end + + it 'is false when service has no project' do + expect(build(:service, project: nil)).not_to be_project_level + end + end + + describe '#group_level?' do + it 'is true when service has a group' do + expect(build(:service, group: group)).to be_group_level + end + + it 'is false when service has no group' do + expect(build(:service, group: nil)).not_to be_group_level + end + end + + describe '#instance_level?' do + it 'is true when service has instance-level integration' do + expect(build(:service, :instance)).to be_instance_level + end + + it 'is false when service does not have instance-level integration' do + expect(build(:service, instance: false)).not_to be_instance_level + end + end + + describe '.find_or_initialize_non_project_specific_integration' do + let!(:service1) { create(:jira_service, project_id: nil, group_id: group.id) } + let!(:service2) { create(:jira_service) } + + it 'returns the right service' do + expect(Integration.find_or_initialize_non_project_specific_integration('jira', group_id: group)).to eq(service1) + end + + it 'does not create a new service' do + expect { Integration.find_or_initialize_non_project_specific_integration('redmine', group_id: group) }.not_to change { Integration.count } + end + end + + describe '.find_or_initialize_all_non_project_specific' do + shared_examples 'service instances' do + it 'returns the available service instances' do + expect(Integration.find_or_initialize_all_non_project_specific(Integration.for_instance).map(&:to_param)).to match_array(Integration.available_services_names(include_project_specific: false)) + end + + it 'does not create service instances' do + expect { Integration.find_or_initialize_all_non_project_specific(Integration.for_instance) }.not_to change { Integration.count } + end + end + + it_behaves_like 'service instances' + + context 'with all existing instances' do + before do + Integration.insert_all( + Integration.available_services_types(include_project_specific: false).map { |type| { instance: true, type: type } } + ) + end + + it_behaves_like 'service instances' + + context 'with a previous existing service (MockCiService) and a new service (Asana)' do + before do + Integration.insert({ type: 'MockCiService', instance: true }) + Integration.delete_by(type: 'AsanaService', instance: true) + end + + it_behaves_like 'service instances' + end + end + + context 'with a few existing instances' do + before do + create(:jira_service, :instance) + end + + it_behaves_like 'service instances' + end + end + + describe 'template' do + shared_examples 'retrieves service templates' do + it 'returns the available service templates' do + expect(Integration.find_or_create_templates.pluck(:type)).to match_array(Integration.available_services_types(include_project_specific: false)) + end + end + + describe '.find_or_create_templates' do + it 'creates service templates' do + expect { Integration.find_or_create_templates }.to change { Integration.count }.from(0).to(Integration.available_services_names(include_project_specific: false).size) + end + + it_behaves_like 'retrieves service templates' + + context 'with all existing templates' do + before do + Integration.insert_all( + Integration.available_services_types(include_project_specific: false).map { |type| { template: true, type: type } } + ) + end + + it 'does not create service templates' do + expect { Integration.find_or_create_templates }.not_to change { Integration.count } + end + + it_behaves_like 'retrieves service templates' + + context 'with a previous existing service (Previous) and a new service (Asana)' do + before do + Integration.insert({ type: 'PreviousService', template: true }) + Integration.delete_by(type: 'AsanaService', template: true) + end + + it_behaves_like 'retrieves service templates' + end + end + + context 'with a few existing templates' do + before do + create(:jira_service, :template) + end + + it 'creates the rest of the service templates' do + expect { Integration.find_or_create_templates }.to change { Integration.count }.from(1).to(Integration.available_services_names(include_project_specific: false).size) + end + + it_behaves_like 'retrieves service templates' + end + end + + describe '.build_from_integration' do + context 'when integration is invalid' do + let(:integration) do + build(:prometheus_service, :template, active: true, properties: {}) + .tap { |integration| integration.save!(validate: false) } + end + + it 'sets service to inactive' do + service = described_class.build_from_integration(integration, project_id: project.id) + + expect(service).to be_valid + expect(service.active).to be false + end + end + + context 'when integration is an instance-level integration' do + let(:integration) { create(:jira_service, :instance) } + + it 'sets inherit_from_id from integration' do + service = described_class.build_from_integration(integration, project_id: project.id) + + expect(service.inherit_from_id).to eq(integration.id) + end + end + + context 'when integration is a group-level integration' do + let(:integration) { create(:jira_service, group: group, project: nil) } + + it 'sets inherit_from_id from integration' do + service = described_class.build_from_integration(integration, project_id: project.id) + + expect(service.inherit_from_id).to eq(integration.id) + end + end + + describe 'build issue tracker from an integration' do + let(:url) { 'http://jira.example.com' } + let(:api_url) { 'http://api-jira.example.com' } + let(:username) { 'jira-username' } + let(:password) { 'jira-password' } + let(:data_params) do + { + url: url, api_url: api_url, + username: username, password: password + } + end + + shared_examples 'service creation from an integration' do + it 'creates a correct service for a project integration' do + service = described_class.build_from_integration(integration, project_id: project.id) + + expect(service).to be_active + expect(service.url).to eq(url) + expect(service.api_url).to eq(api_url) + expect(service.username).to eq(username) + expect(service.password).to eq(password) + expect(service.template).to eq(false) + expect(service.instance).to eq(false) + expect(service.project).to eq(project) + expect(service.group).to eq(nil) + end + + it 'creates a correct service for a group integration' do + service = described_class.build_from_integration(integration, group_id: group.id) + + expect(service).to be_active + expect(service.url).to eq(url) + expect(service.api_url).to eq(api_url) + expect(service.username).to eq(username) + expect(service.password).to eq(password) + expect(service.template).to eq(false) + expect(service.instance).to eq(false) + expect(service.project).to eq(nil) + expect(service.group).to eq(group) + end + end + + # this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404 + context 'when data are stored in properties' do + let(:properties) { data_params } + let!(:integration) do + create(:jira_service, :without_properties_callback, template: true, properties: properties.merge(additional: 'something')) + end + + it_behaves_like 'service creation from an integration' + end + + context 'when data are stored in separated fields' do + let(:integration) do + create(:jira_service, :template, data_params.merge(properties: {})) + end + + it_behaves_like 'service creation from an integration' + end + + context 'when data are stored in both properties and separated fields' do + let(:properties) { data_params } + let(:integration) do + create(:jira_service, :without_properties_callback, active: true, template: true, properties: properties).tap do |integration| + create(:jira_tracker_data, data_params.merge(integration: integration)) + end + end + + it_behaves_like 'service creation from an integration' + end + end + end + + describe "for pushover service" do + let!(:service_template) do + PushoverService.create!( + template: true, + properties: { + device: 'MyDevice', + sound: 'mic', + priority: 4, + api_key: '123456789' + }) + end + + describe 'is prefilled for projects pushover service' do + it "has all fields prefilled" do + service = project.find_or_initialize_service('pushover') + + expect(service.template).to eq(false) + expect(service.device).to eq('MyDevice') + expect(service.sound).to eq('mic') + expect(service.priority).to eq(4) + expect(service.api_key).to eq('123456789') + end + end + end + end + + describe '.default_integration' do + context 'with an instance-level service' do + let_it_be(:instance_service) { create(:jira_service, :instance) } + + it 'returns the instance service' do + expect(described_class.default_integration('JiraService', project)).to eq(instance_service) + end + + it 'returns nil for nonexistent service type' do + expect(described_class.default_integration('HipchatService', project)).to eq(nil) + end + + context 'with a group service' do + let_it_be(:group_service) { create(:jira_service, group_id: group.id, project_id: nil) } + + it 'returns the group service for a project' do + expect(described_class.default_integration('JiraService', project)).to eq(group_service) + end + + it 'returns the instance service for a group' do + expect(described_class.default_integration('JiraService', group)).to eq(instance_service) + end + + context 'with a subgroup' do + let_it_be(:subgroup) { create(:group, parent: group) } + + let!(:project) { create(:project, group: subgroup) } + + it 'returns the closest group service for a project' do + expect(described_class.default_integration('JiraService', project)).to eq(group_service) + end + + it 'returns the closest group service for a subgroup' do + expect(described_class.default_integration('JiraService', subgroup)).to eq(group_service) + end + + context 'having a service with custom settings' do + let!(:subgroup_service) { create(:jira_service, group_id: subgroup.id, project_id: nil) } + + it 'returns the closest group service for a project' do + expect(described_class.default_integration('JiraService', project)).to eq(subgroup_service) + end + end + + context 'having a service inheriting settings' do + let!(:subgroup_service) { create(:jira_service, group_id: subgroup.id, project_id: nil, inherit_from_id: group_service.id) } + + it 'returns the closest group service which does not inherit from its parent for a project' do + expect(described_class.default_integration('JiraService', project)).to eq(group_service) + end + end + end + end + end + end + + describe '.create_from_active_default_integrations' do + context 'with an active service template' do + let_it_be(:template_integration) { create(:prometheus_service, :template, api_url: 'https://prometheus.template.com/') } + + it 'creates a service from the template' do + described_class.create_from_active_default_integrations(project, :project_id, with_templates: true) + + expect(project.reload.integrations.size).to eq(1) + expect(project.reload.integrations.first.api_url).to eq(template_integration.api_url) + expect(project.reload.integrations.first.inherit_from_id).to be_nil + end + + context 'with an active instance-level integration' do + let!(:instance_integration) { create(:prometheus_service, :instance, api_url: 'https://prometheus.instance.com/') } + + it 'creates a service from the instance-level integration' do + described_class.create_from_active_default_integrations(project, :project_id, with_templates: true) + + expect(project.reload.integrations.size).to eq(1) + expect(project.reload.integrations.first.api_url).to eq(instance_integration.api_url) + expect(project.reload.integrations.first.inherit_from_id).to eq(instance_integration.id) + end + + context 'passing a group' do + it 'creates a service from the instance-level integration' do + described_class.create_from_active_default_integrations(group, :group_id) + + expect(group.reload.integrations.size).to eq(1) + expect(group.reload.integrations.first.api_url).to eq(instance_integration.api_url) + expect(group.reload.integrations.first.inherit_from_id).to eq(instance_integration.id) + end + end + + context 'with an active group-level integration' do + let!(:group_integration) { create(:prometheus_service, group: group, project: nil, api_url: 'https://prometheus.group.com/') } + + it 'creates a service from the group-level integration' do + described_class.create_from_active_default_integrations(project, :project_id, with_templates: true) + + expect(project.reload.integrations.size).to eq(1) + expect(project.reload.integrations.first.api_url).to eq(group_integration.api_url) + expect(project.reload.integrations.first.inherit_from_id).to eq(group_integration.id) + end + + context 'passing a group' do + let!(:subgroup) { create(:group, parent: group) } + + it 'creates a service from the group-level integration' do + described_class.create_from_active_default_integrations(subgroup, :group_id) + + expect(subgroup.reload.integrations.size).to eq(1) + expect(subgroup.reload.integrations.first.api_url).to eq(group_integration.api_url) + expect(subgroup.reload.integrations.first.inherit_from_id).to eq(group_integration.id) + end + end + + context 'with an active subgroup' do + let!(:subgroup_integration) { create(:prometheus_service, group: subgroup, project: nil, api_url: 'https://prometheus.subgroup.com/') } + let!(:subgroup) { create(:group, parent: group) } + let(:project) { create(:project, group: subgroup) } + + it 'creates a service from the subgroup-level integration' do + described_class.create_from_active_default_integrations(project, :project_id, with_templates: true) + + expect(project.reload.integrations.size).to eq(1) + expect(project.reload.integrations.first.api_url).to eq(subgroup_integration.api_url) + expect(project.reload.integrations.first.inherit_from_id).to eq(subgroup_integration.id) + end + + context 'passing a group' do + let!(:sub_subgroup) { create(:group, parent: subgroup) } + + context 'traversal queries' do + shared_examples 'correct ancestor order' do + it 'creates a service from the subgroup-level integration' do + described_class.create_from_active_default_integrations(sub_subgroup, :group_id) + + sub_subgroup.reload + + expect(sub_subgroup.integrations.size).to eq(1) + expect(sub_subgroup.integrations.first.api_url).to eq(subgroup_integration.api_url) + expect(sub_subgroup.integrations.first.inherit_from_id).to eq(subgroup_integration.id) + end + + context 'having a service inheriting settings' do + let!(:subgroup_integration) { create(:prometheus_service, group: subgroup, project: nil, inherit_from_id: group_integration.id, api_url: 'https://prometheus.subgroup.com/') } + + it 'creates a service from the group-level integration' do + described_class.create_from_active_default_integrations(sub_subgroup, :group_id) + + sub_subgroup.reload + + expect(sub_subgroup.integrations.size).to eq(1) + expect(sub_subgroup.integrations.first.api_url).to eq(group_integration.api_url) + expect(sub_subgroup.integrations.first.inherit_from_id).to eq(group_integration.id) + end + end + end + + context 'recursive' do + before do + stub_feature_flags(use_traversal_ids: false) + end + + include_examples 'correct ancestor order' + end + + context 'linear' do + before do + stub_feature_flags(use_traversal_ids: true) + + sub_subgroup.reload # make sure traversal_ids are reloaded + end + + include_examples 'correct ancestor order' + end + end + end + end + end + end + end + end + + describe '.inherited_descendants_from_self_or_ancestors_from' do + let_it_be(:subgroup1) { create(:group, parent: group) } + let_it_be(:subgroup2) { create(:group, parent: group) } + let_it_be(:project1) { create(:project, group: subgroup1) } + let_it_be(:project2) { create(:project, group: subgroup2) } + let_it_be(:group_integration) { create(:prometheus_service, group: group, project: nil) } + let_it_be(:subgroup_integration1) { create(:prometheus_service, group: subgroup1, project: nil, inherit_from_id: group_integration.id) } + let_it_be(:subgroup_integration2) { create(:prometheus_service, group: subgroup2, project: nil) } + let_it_be(:project_integration1) { create(:prometheus_service, group: nil, project: project1, inherit_from_id: group_integration.id) } + let_it_be(:project_integration2) { create(:prometheus_service, group: nil, project: project2, inherit_from_id: subgroup_integration2.id) } + + it 'returns the groups and projects inheriting from integration ancestors', :aggregate_failures do + expect(described_class.inherited_descendants_from_self_or_ancestors_from(group_integration)).to eq([subgroup_integration1, project_integration1]) + expect(described_class.inherited_descendants_from_self_or_ancestors_from(subgroup_integration2)).to eq([project_integration2]) + end end - describe '.with_custom_integration_for' do - it 'returns projects with custom integrations' do - # We use pagination to verify that the group is excluded from the query - expect(Project.with_custom_integration_for(instance_integration, 0, 2)).to contain_exactly(project_2, project_3) - expect(Project.with_custom_integration_for(instance_integration)).to contain_exactly(project_2, project_3) + describe '.service_name_to_model' do + it 'returns the model for the given service name', :aggregate_failures do + expect(described_class.service_name_to_model('asana')).to eq(Integrations::Asana) + # TODO We can remove this test when all models have been namespaced: + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60968#note_570994955 + expect(described_class.service_name_to_model('youtrack')).to eq(YoutrackService) end + + it 'raises an error if service name is invalid' do + expect { described_class.service_name_to_model('foo') }.to raise_exception(NameError, /uninitialized constant FooService/) + end + end + + describe "{property}_changed?" do + let(:service) do + Integrations::Bamboo.create!( + project: project, + properties: { + bamboo_url: 'http://gitlab.com', + username: 'mic', + password: "password" + } + ) + end + + it "returns false when the property has not been assigned a new value" do + service.username = "key_changed" + expect(service.bamboo_url_changed?).to be_falsy + end + + it "returns true when the property has been assigned a different value" do + service.bamboo_url = "http://example.com" + expect(service.bamboo_url_changed?).to be_truthy + end + + it "returns true when the property has been assigned a different value twice" do + service.bamboo_url = "http://example.com" + service.bamboo_url = "http://example.com" + expect(service.bamboo_url_changed?).to be_truthy + end + + it "returns false when the property has been re-assigned the same value" do + service.bamboo_url = 'http://gitlab.com' + expect(service.bamboo_url_changed?).to be_falsy + end + + it "returns false when the property has been assigned a new value then saved" do + service.bamboo_url = 'http://example.com' + service.save! + expect(service.bamboo_url_changed?).to be_falsy + end + end + + describe "{property}_touched?" do + let(:service) do + Integrations::Bamboo.create!( + project: project, + properties: { + bamboo_url: 'http://gitlab.com', + username: 'mic', + password: "password" + } + ) + end + + it "returns false when the property has not been assigned a new value" do + service.username = "key_changed" + expect(service.bamboo_url_touched?).to be_falsy + end + + it "returns true when the property has been assigned a different value" do + service.bamboo_url = "http://example.com" + expect(service.bamboo_url_touched?).to be_truthy + end + + it "returns true when the property has been assigned a different value twice" do + service.bamboo_url = "http://example.com" + service.bamboo_url = "http://example.com" + expect(service.bamboo_url_touched?).to be_truthy + end + + it "returns true when the property has been re-assigned the same value" do + service.bamboo_url = 'http://gitlab.com' + expect(service.bamboo_url_touched?).to be_truthy + end + + it "returns false when the property has been assigned a new value then saved" do + service.bamboo_url = 'http://example.com' + service.save! + expect(service.bamboo_url_changed?).to be_falsy + end + end + + describe "{property}_was" do + let(:service) do + Integrations::Bamboo.create!( + project: project, + properties: { + bamboo_url: 'http://gitlab.com', + username: 'mic', + password: "password" + } + ) + end + + it "returns nil when the property has not been assigned a new value" do + service.username = "key_changed" + expect(service.bamboo_url_was).to be_nil + end + + it "returns the previous value when the property has been assigned a different value" do + service.bamboo_url = "http://example.com" + expect(service.bamboo_url_was).to eq('http://gitlab.com') + end + + it "returns initial value when the property has been re-assigned the same value" do + service.bamboo_url = 'http://gitlab.com' + expect(service.bamboo_url_was).to eq('http://gitlab.com') + end + + it "returns initial value when the property has been assigned multiple values" do + service.bamboo_url = "http://example.com" + service.bamboo_url = "http://example2.com" + expect(service.bamboo_url_was).to eq('http://gitlab.com') + end + + it "returns nil when the property has been assigned a new value then saved" do + service.bamboo_url = 'http://example.com' + service.save! + expect(service.bamboo_url_was).to be_nil + end + end + + describe 'initialize service with no properties' do + let(:service) do + BugzillaService.create!( + project: project, + project_url: 'http://gitlab.example.com' + ) + end + + it 'does not raise error' do + expect { service }.not_to raise_error + end + + it 'sets data correctly' do + expect(service.data_fields.project_url).to eq('http://gitlab.example.com') + end + end + + describe '#api_field_names' do + let(:fake_service) do + Class.new(Integration) do + def fields + [ + { name: 'token' }, + { name: 'api_token' }, + { name: 'key' }, + { name: 'api_key' }, + { name: 'password' }, + { name: 'password_field' }, + { name: 'safe_field' } + ] + end + end + end + + let(:service) do + fake_service.new(properties: [ + { token: 'token-value' }, + { api_token: 'api_token-value' }, + { key: 'key-value' }, + { api_key: 'api_key-value' }, + { password: 'password-value' }, + { password_field: 'password_field-value' }, + { safe_field: 'safe_field-value' } + ]) + end + + it 'filters out sensitive fields' do + expect(service.api_field_names).to eq(['safe_field']) + end + end + + context 'logging' do + let(:service) { build(:service, project: project) } + let(:test_message) { "test message" } + let(:arguments) do + { + service_class: service.class.name, + project_path: project.full_path, + project_id: project.id, + message: test_message, + additional_argument: 'some argument' + } + end + + it 'logs info messages using json logger' do + expect(Gitlab::JsonLogger).to receive(:info).with(arguments) + + service.log_info(test_message, additional_argument: 'some argument') + end + + it 'logs error messages using json logger' do + expect(Gitlab::JsonLogger).to receive(:error).with(arguments) + + service.log_error(test_message, additional_argument: 'some argument') + end + + context 'when project is nil' do + let(:project) { nil } + let(:arguments) do + { + service_class: service.class.name, + project_path: nil, + project_id: nil, + message: test_message, + additional_argument: 'some argument' + } + end + + it 'logs info messages using json logger' do + expect(Gitlab::JsonLogger).to receive(:info).with(arguments) + + service.log_info(test_message, additional_argument: 'some argument') + end + end + end + + describe '#external_wiki?' do + where(:type, :active, :result) do + 'ExternalWikiService' | true | true + 'ExternalWikiService' | false | false + 'SlackService' | true | false + end + + with_them do + it 'returns the right result' do + expect(build(:service, type: type, active: active).external_wiki?).to eq(result) + end + end + end + + describe '.available_services_names' do + it 'calls the right methods' do + expect(described_class).to receive(:services_names).and_call_original + expect(described_class).to receive(:dev_services_names).and_call_original + expect(described_class).to receive(:project_specific_services_names).and_call_original + + described_class.available_services_names + end + + it 'does not call project_specific_services_names with include_project_specific false' do + expect(described_class).to receive(:services_names).and_call_original + expect(described_class).to receive(:dev_services_names).and_call_original + expect(described_class).not_to receive(:project_specific_services_names) + + described_class.available_services_names(include_project_specific: false) + end + + it 'does not call dev_services_names with include_dev false' do + expect(described_class).to receive(:services_names).and_call_original + expect(described_class).not_to receive(:dev_services_names) + expect(described_class).to receive(:project_specific_services_names).and_call_original + + described_class.available_services_names(include_dev: false) + end + + it { expect(described_class.available_services_names).to include('jenkins') } end - describe '.without_integration' do - it 'returns projects without integration' do - expect(Project.without_integration(instance_integration)).to contain_exactly(project_4) + describe '.project_specific_services_names' do + it do + expect(described_class.project_specific_services_names) + .to include(*described_class::PROJECT_SPECIFIC_INTEGRATION_NAMES) end end end diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/integrations/asana_spec.rb index 7a6fe4b1537..4473478910a 100644 --- a/spec/models/project_services/asana_service_spec.rb +++ b/spec/models/integrations/asana_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe AsanaService do +RSpec.describe Integrations::Asana do describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } @@ -54,7 +54,7 @@ RSpec.describe AsanaService do d1 = double('Asana::Resources::Task') expect(d1).to receive(:add_comment).with(text: expected_message) - expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, gid).once.and_return(d1) + expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, gid).once.and_return(d1) @asana.execute(data) end @@ -64,7 +64,7 @@ RSpec.describe AsanaService do d1 = double('Asana::Resources::Task') expect(d1).to receive(:add_comment) expect(d1).to receive(:update).with(completed: true) - expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1) + expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1) @asana.execute(data) end @@ -74,7 +74,7 @@ RSpec.describe AsanaService do d1 = double('Asana::Resources::Task') expect(d1).to receive(:add_comment) expect(d1).to receive(:update).with(completed: true) - expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1) + expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1) @asana.execute(data) end @@ -88,25 +88,25 @@ RSpec.describe AsanaService do d1 = double('Asana::Resources::Task') expect(d1).to receive(:add_comment) expect(d1).to receive(:update).with(completed: true) - expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1) + expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1) d2 = double('Asana::Resources::Task') expect(d2).to receive(:add_comment) expect(d2).to receive(:update).with(completed: true) - expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2) + expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2) d3 = double('Asana::Resources::Task') expect(d3).to receive(:add_comment) - expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3) + expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3) d4 = double('Asana::Resources::Task') expect(d4).to receive(:add_comment) - expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4) + expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4) d5 = double('Asana::Resources::Task') expect(d5).to receive(:add_comment) expect(d5).to receive(:update).with(completed: true) - expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5) + expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5) @asana.execute(data) end diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/integrations/assembla_spec.rb index 207add6f090..bf9033416e9 100644 --- a/spec/models/project_services/assembla_service_spec.rb +++ b/spec/models/integrations/assembla_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe AssemblaService do +RSpec.describe Integrations::Assembla do include StubRequests describe "Associations" do diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/integrations/bamboo_spec.rb index 45afbcca96d..0ba1595bbd8 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/integrations/bamboo_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe BambooService, :use_clean_rails_memory_store_caching do +RSpec.describe Integrations::Bamboo, :use_clean_rails_memory_store_caching do include ReactiveCachingHelpers include StubRequests diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/integrations/campfire_spec.rb index ea3990b339b..b23edf03e8a 100644 --- a/spec/models/project_services/campfire_service_spec.rb +++ b/spec/models/integrations/campfire_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe CampfireService do +RSpec.describe Integrations::Campfire do include StubRequests describe 'Associations' do diff --git a/spec/models/project_services/chat_message/alert_message_spec.rb b/spec/models/integrations/chat_message/alert_message_spec.rb index 4d400990789..9866b2d9185 100644 --- a/spec/models/project_services/chat_message/alert_message_spec.rb +++ b/spec/models/integrations/chat_message/alert_message_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ChatMessage::AlertMessage do +RSpec.describe Integrations::ChatMessage::AlertMessage do subject { described_class.new(args) } let_it_be(:start_time) { Time.current } diff --git a/spec/models/project_services/chat_message/base_message_spec.rb b/spec/models/integrations/chat_message/base_message_spec.rb index a7ddf230758..eada5d1031d 100644 --- a/spec/models/project_services/chat_message/base_message_spec.rb +++ b/spec/models/integrations/chat_message/base_message_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ChatMessage::BaseMessage do +RSpec.describe Integrations::ChatMessage::BaseMessage do let(:base_message) { described_class.new(args) } let(:args) { { project_url: 'https://gitlab-domain.com' } } diff --git a/spec/models/project_services/chat_message/deployment_message_spec.rb b/spec/models/integrations/chat_message/deployment_message_spec.rb index 6bdf2120b36..ff255af11a3 100644 --- a/spec/models/project_services/chat_message/deployment_message_spec.rb +++ b/spec/models/integrations/chat_message/deployment_message_spec.rb @@ -2,14 +2,14 @@ require 'spec_helper' -RSpec.describe ChatMessage::DeploymentMessage do +RSpec.describe Integrations::ChatMessage::DeploymentMessage do describe '#pretext' do it 'returns a message with the data returned by the deployment data builder' do environment = create(:environment, name: "myenvironment") project = create(:project, :repository) commit = project.commit('HEAD') deployment = create(:deployment, status: :success, environment: environment, project: project, sha: commit.sha) - data = Gitlab::DataBuilder::Deployment.build(deployment) + data = Gitlab::DataBuilder::Deployment.build(deployment, Time.current) message = described_class.new(data) @@ -118,7 +118,7 @@ RSpec.describe ChatMessage::DeploymentMessage do job_url = Gitlab::Routing.url_helpers.project_job_url(project, ci_build) commit_url = Gitlab::UrlBuilder.build(deployment.commit) user_url = Gitlab::Routing.url_helpers.user_url(user) - data = Gitlab::DataBuilder::Deployment.build(deployment) + data = Gitlab::DataBuilder::Deployment.build(deployment, Time.current) message = described_class.new(data) diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/integrations/chat_message/issue_message_spec.rb index 4701ef3e49e..31b80ad3169 100644 --- a/spec/models/project_services/chat_message/issue_message_spec.rb +++ b/spec/models/integrations/chat_message/issue_message_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ChatMessage::IssueMessage do +RSpec.describe Integrations::ChatMessage::IssueMessage do subject { described_class.new(args) } let(:args) do diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/integrations/chat_message/merge_message_spec.rb index 71cfe3ff45b..ed1ad6837e2 100644 --- a/spec/models/project_services/chat_message/merge_message_spec.rb +++ b/spec/models/integrations/chat_message/merge_message_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ChatMessage::MergeMessage do +RSpec.describe Integrations::ChatMessage::MergeMessage do subject { described_class.new(args) } let(:args) do diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/integrations/chat_message/note_message_spec.rb index 6a741365d55..668c0da26ae 100644 --- a/spec/models/project_services/chat_message/note_message_spec.rb +++ b/spec/models/integrations/chat_message/note_message_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ChatMessage::NoteMessage do +RSpec.describe Integrations::ChatMessage::NoteMessage do subject { described_class.new(args) } let(:color) { '#345' } diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/integrations/chat_message/pipeline_message_spec.rb index 4eb2f57315b..a80d13d7f5d 100644 --- a/spec/models/project_services/chat_message/pipeline_message_spec.rb +++ b/spec/models/integrations/chat_message/pipeline_message_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe ChatMessage::PipelineMessage do +RSpec.describe Integrations::ChatMessage::PipelineMessage do subject { described_class.new(args) } let(:args) do diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/integrations/chat_message/push_message_spec.rb index e3ba4c2aefe..167487449c3 100644 --- a/spec/models/project_services/chat_message/push_message_spec.rb +++ b/spec/models/integrations/chat_message/push_message_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ChatMessage::PushMessage do +RSpec.describe Integrations::ChatMessage::PushMessage do subject { described_class.new(args) } let(:args) do diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/integrations/chat_message/wiki_page_message_spec.rb index 04c9e5934be..e8672a0f9c8 100644 --- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb +++ b/spec/models/integrations/chat_message/wiki_page_message_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ChatMessage::WikiPageMessage do +RSpec.describe Integrations::ChatMessage::WikiPageMessage do subject { described_class.new(args) } let(:args) do diff --git a/spec/models/project_services/confluence_service_spec.rb b/spec/models/integrations/confluence_spec.rb index 6c7ba2c9f32..c217573f48d 100644 --- a/spec/models/project_services/confluence_service_spec.rb +++ b/spec/models/integrations/confluence_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ConfluenceService do +RSpec.describe Integrations::Confluence do describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } diff --git a/spec/models/project_services/datadog_service_spec.rb b/spec/models/integrations/datadog_spec.rb index d15ea1f351b..165b21840e0 100644 --- a/spec/models/project_services/datadog_service_spec.rb +++ b/spec/models/integrations/datadog_spec.rb @@ -3,7 +3,7 @@ require 'securerandom' require 'spec_helper' -RSpec.describe DatadogService, :model do +RSpec.describe Integrations::Datadog do let_it_be(:project) { create(:project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } let_it_be(:build) { create(:ci_build, project: project) } diff --git a/spec/models/project_services/emails_on_push_service_spec.rb b/spec/models/integrations/emails_on_push_spec.rb index c5927503eec..ca060f4155e 100644 --- a/spec/models/project_services/emails_on_push_service_spec.rb +++ b/spec/models/integrations/emails_on_push_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe EmailsOnPushService do +RSpec.describe Integrations::EmailsOnPush do let_it_be(:project) { create_default(:project).freeze } describe 'Validations' do diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb index 981245627af..390d1552c16 100644 --- a/spec/models/internal_id_spec.rb +++ b/spec/models/internal_id_spec.rb @@ -100,7 +100,8 @@ RSpec.describe InternalId do context 'when executed outside of transaction' do it 'increments counter with in_transaction: "false"' do - expect(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } + allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } + expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment) .with(operation: :generate, usage: 'issues', in_transaction: 'false').and_call_original @@ -158,7 +159,8 @@ RSpec.describe InternalId do let(:value) { 2 } it 'increments counter with in_transaction: "false"' do - expect(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } + allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } + expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment) .with(operation: :reset, usage: 'issues', in_transaction: 'false').and_call_original @@ -228,7 +230,8 @@ RSpec.describe InternalId do context 'when executed outside of transaction' do it 'increments counter with in_transaction: "false"' do - expect(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } + allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } + expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment) .with(operation: :track_greatest, usage: 'issues', in_transaction: 'false').and_call_original diff --git a/spec/models/issue/metrics_spec.rb b/spec/models/issue/metrics_spec.rb index 18b0a46c928..49c891c20da 100644 --- a/spec/models/issue/metrics_spec.rb +++ b/spec/models/issue/metrics_spec.rb @@ -80,5 +80,20 @@ RSpec.describe Issue::Metrics do expect(metrics.first_added_to_board_at).to be_like_time(time) end end + + describe "#record!" do + it "does not cause an N+1 query" do + label = create(:label) + subject.update!(label_ids: [label.id]) + + control_count = ActiveRecord::QueryRecorder.new { Issue::Metrics.find_by(issue: subject).record! }.count + + additional_labels = create_list(:label, 4) + + subject.update!(label_ids: additional_labels.map(&:id)) + + expect { Issue::Metrics.find_by(issue: subject).record! }.not_to exceed_query_limit(control_count) + end + end end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 23caf3647c3..884c476932e 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -242,16 +242,36 @@ RSpec.describe Issue do expect { issue.close }.to change { issue.state_id }.from(open_state).to(closed_state) end + + context 'when an argument is provided' do + context 'and the argument is a User' do + it 'changes closed_by to the given user' do + expect { issue.close(user) }.to change { issue.closed_by }.from(nil).to(user) + end + end + + context 'and the argument is a not a User' do + it 'does not change closed_by' do + expect { issue.close("test") }.not_to change { issue.closed_by } + end + end + end + + context 'when an argument is not provided' do + it 'does not change closed_by' do + expect { issue.close }.not_to change { issue.closed_by } + end + end end describe '#reopen' do let(:issue) { create(:issue, project: reusable_project, state: 'closed', closed_at: Time.current, closed_by: user) } - it 'sets closed_at to nil when an issue is reopend' do + it 'sets closed_at to nil when an issue is reopened' do expect { issue.reopen }.to change { issue.closed_at }.to(nil) end - it 'sets closed_by to nil when an issue is reopend' do + it 'sets closed_by to nil when an issue is reopened' do expect { issue.reopen }.to change { issue.closed_by }.from(user).to(nil) end @@ -297,7 +317,7 @@ RSpec.describe Issue do end context 'when cross-project in different namespace' do - let(:another_namespace) { build(:namespace, path: 'another-namespace') } + let(:another_namespace) { build(:namespace, id: non_existing_record_id, path: 'another-namespace') } let(:another_namespace_project) { build(:project, path: 'another-project', namespace: another_namespace) } it 'returns complete path to the issue' do @@ -1121,11 +1141,37 @@ RSpec.describe Issue do end context "relative positioning" do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:issue1) { create(:issue, project: project, relative_position: nil) } + let_it_be(:issue2) { create(:issue, project: project, relative_position: nil) } + it_behaves_like "a class that supports relative positioning" do let_it_be(:project) { reusable_project } let(:factory) { :issue } let(:default_params) { { project: project } } end + + it 'is not blocked for repositioning by default' do + expect(issue1.blocked_for_repositioning?).to eq(false) + end + + context 'when block_issue_repositioning flag is enabled for group' do + before do + stub_feature_flags(block_issue_repositioning: group) + end + + it 'is blocked for repositioning' do + expect(issue1.blocked_for_repositioning?).to eq(true) + end + + it 'does not move issues with null position' do + payload = [issue1, issue2] + + expect { described_class.move_nulls_to_end(payload) }.to raise_error(Gitlab::RelativePositioning::IssuePositioningDisabled) + expect { described_class.move_nulls_to_start(payload) }.to raise_error(Gitlab::RelativePositioning::IssuePositioningDisabled) + end + end end it_behaves_like 'versioned description' diff --git a/spec/models/label_link_spec.rb b/spec/models/label_link_spec.rb index a95481f3083..e5753b34e72 100644 --- a/spec/models/label_link_spec.rb +++ b/spec/models/label_link_spec.rb @@ -12,4 +12,28 @@ RSpec.describe LabelLink do let(:valid_items_for_bulk_insertion) { build_list(:label_link, 10) } let(:invalid_items_for_bulk_insertion) { [] } # class does not have any validations defined end + + describe 'scopes' do + describe '.for_target' do + it 'returns the label links for a given target' do + label_link = create(:label_link, target: create(:merge_request)) + + create(:label_link, target: create(:issue)) + + expect(described_class.for_target(label_link.target_id, label_link.target_type)) + .to contain_exactly(label_link) + end + end + + describe '.with_remove_on_close_labels' do + it 'responds with label_links that can be removed when an issue is closed' do + issue = create(:issue) + removable_label = create(:label, project: issue.project, remove_on_close: true) + create(:label_link, target: issue) + removable_issue_label_link = create(:label_link, label: removable_label, target: issue) + + expect(described_class.with_remove_on_close_labels).to contain_exactly(removable_issue_label_link) + end + end + end end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 5f3a67b52ba..247be7654d8 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -143,16 +143,10 @@ RSpec.describe Member do @blocked_maintainer = project.members.find_by(user_id: @blocked_maintainer_user.id, access_level: Gitlab::Access::MAINTAINER) @blocked_developer = project.members.find_by(user_id: @blocked_developer_user.id, access_level: Gitlab::Access::DEVELOPER) - @invited_member = create(:project_member, :developer, - project: project, - invite_token: '1234', - invite_email: 'toto1@example.com') + @invited_member = create(:project_member, :invited, :developer, project: project) accepted_invite_user = build(:user, state: :active) - @accepted_invite_member = create(:project_member, :developer, - project: project, - invite_token: '1234', - invite_email: 'toto2@example.com') + @accepted_invite_member = create(:project_member, :invited, :developer, project: project) .tap { |u| u.accept_invite!(accepted_invite_user) } requested_user = create(:user).tap { |u| project.request_access(u) } @@ -325,12 +319,12 @@ RSpec.describe Member do describe '.search_invite_email' do it 'returns only members the matching e-mail' do - create(:group_member, :invited) + invited_member = create(:group_member, :invited, invite_email: 'invited@example.com') - invited = described_class.search_invite_email(@invited_member.invite_email) + invited = described_class.search_invite_email(invited_member.invite_email) expect(invited.count).to eq(1) - expect(invited.first).to eq(@invited_member) + expect(invited.first).to eq(invited_member) expect(described_class.search_invite_email('bad-email@example.com').count).to eq(0) end @@ -414,6 +408,44 @@ RSpec.describe Member do it { is_expected.not_to include @member_with_minimal_access } end + describe '.connected_to_user' do + subject { described_class.connected_to_user.to_a } + + it { is_expected.to include @owner } + it { is_expected.to include @maintainer } + it { is_expected.to include @accepted_invite_member } + it { is_expected.to include @accepted_request_member } + it { is_expected.to include @blocked_maintainer } + it { is_expected.to include @blocked_developer } + it { is_expected.to include @requested_member } + it { is_expected.to include @member_with_minimal_access } + it { is_expected.not_to include @invited_member } + end + + describe '.authorizable' do + subject { described_class.authorizable.to_a } + + it 'includes the member who has an associated user record,'\ + 'but also having an invite_token' do + member = create(:project_member, + :developer, + :invited, + user: create(:user)) + + expect(subject).to include(member) + end + + it { is_expected.to include @owner } + it { is_expected.to include @maintainer } + it { is_expected.to include @accepted_invite_member } + it { is_expected.to include @accepted_request_member } + it { is_expected.to include @blocked_maintainer } + it { is_expected.to include @blocked_developer } + it { is_expected.not_to include @invited_member } + it { is_expected.not_to include @requested_member } + it { is_expected.not_to include @member_with_minimal_access } + end + describe '.distinct_on_user_with_max_access_level' do let_it_be(:other_group) { create(:group) } let_it_be(:member_with_lower_access_level) { create(:group_member, :developer, group: other_group, user: @owner_user) } @@ -884,7 +916,7 @@ RSpec.describe Member do user = create(:user) member = project.add_reporter(user) - member.destroy + member.destroy! expect(user.authorized_projects).not_to include(project) end @@ -901,7 +933,7 @@ RSpec.describe Member do with_them do describe 'create member' do - let!(:source) { create(source_type) } + let!(:source) { create(source_type) } # rubocop:disable Rails/SaveBang subject { create(member_type, :guest, user: user, source: source) } @@ -913,20 +945,20 @@ RSpec.describe Member do describe 'update member' do context 'when access level was changed' do - subject { member.update(access_level: Gitlab::Access::GUEST) } + subject { member.update!(access_level: Gitlab::Access::GUEST) } include_examples 'update highest role with exclusive lease' end context 'when access level was not changed' do - subject { member.update(notification_level: NotificationSetting.levels[:disabled]) } + subject { member.update!(notification_level: NotificationSetting.levels[:disabled]) } include_examples 'does not update the highest role' end end describe 'destroy member' do - subject { member.destroy } + subject { member.destroy! } include_examples 'update highest role with exclusive lease' end diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 908bb9f91a3..3a2db5d8516 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -85,11 +85,11 @@ RSpec.describe GroupMember do expect(user).to receive(:update_two_factor_requirement) - group_member.save + group_member.save! expect(user).to receive(:update_two_factor_requirement) - group_member.destroy + group_member.destroy! end end diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index ce3e86f964d..fa77e319c2c 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -49,13 +49,13 @@ RSpec.describe ProjectMember do it "creates an expired event when left due to expiry" do expired = create(:project_member, project: project, expires_at: 1.day.from_now) - travel_to(2.days.from_now) { expired.destroy } + travel_to(2.days.from_now) { expired.destroy! } expect(Event.recent.first).to be_expired_action end it "creates a left event when left due to leave" do - maintainer.destroy + maintainer.destroy! expect(Event.recent.first).to be_left_action end end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 5b11a7bf079..4075eb96fc2 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -23,7 +23,7 @@ RSpec.describe MergeRequestDiff do expect(subject.valid?).to be false expect(subject.errors.count).to eq 3 - expect(subject.errors).to all(include('is not a valid SHA')) + expect(subject.errors.full_messages).to all(include('is not a valid SHA')) end it 'does not validate uniqueness by default' do @@ -61,7 +61,7 @@ RSpec.describe MergeRequestDiff do let_it_be(:merge_head) do MergeRequests::MergeToRefService - .new(merge_request.project, merge_request.author) + .new(project: merge_request.project, current_user: merge_request.author) .execute(merge_request) merge_request.create_merge_head_diff @@ -485,27 +485,6 @@ RSpec.describe MergeRequestDiff do 'files/whitespace' ]) end - - context 'when sort_diffs feature flag is disabled' do - before do - stub_feature_flags(sort_diffs: false) - end - - it 'does not sort diff files directory first' do - expect(diff_with_commits.diffs_in_batch(1, 10, diff_options: diff_options).diff_file_paths).to eq([ - '.DS_Store', - '.gitattributes', - '.gitignore', - '.gitmodules', - 'CHANGELOG', - 'README', - 'bar/branch-test.txt', - 'custom-highlighting/test.gitlab-custom', - 'encoding/iso8859.txt', - 'files/.DS_Store' - ]) - end - end end end @@ -581,37 +560,6 @@ RSpec.describe MergeRequestDiff do 'gitlab-shell' ]) end - - context 'when sort_diffs feature flag is disabled' do - before do - stub_feature_flags(sort_diffs: false) - end - - it 'does not sort diff files directory first' do - expect(diff_with_commits.diffs(diff_options).diff_file_paths).to eq([ - '.DS_Store', - '.gitattributes', - '.gitignore', - '.gitmodules', - 'CHANGELOG', - 'README', - 'bar/branch-test.txt', - 'custom-highlighting/test.gitlab-custom', - 'encoding/iso8859.txt', - 'files/.DS_Store', - 'files/images/wm.svg', - 'files/js/commit.coffee', - 'files/lfs/lfs_object.iso', - 'files/ruby/popen.rb', - 'files/ruby/regex.rb', - 'files/whitespace', - 'foo/bar/.gitkeep', - 'gitlab-grack', - 'gitlab-shell', - 'with space/README.md' - ]) - end - end end end @@ -718,40 +666,6 @@ RSpec.describe MergeRequestDiff do ]) end - context 'when sort_diffs feature flag is disabled' do - before do - stub_feature_flags(sort_diffs: false) - end - - it 'persists diff files unsorted by directory first' do - mr_diff = create(:merge_request).merge_request_diff - diff_files_paths = mr_diff.merge_request_diff_files.map { |file| file.new_path.presence || file.old_path } - - expect(diff_files_paths).to eq([ - '.DS_Store', - '.gitattributes', - '.gitignore', - '.gitmodules', - 'CHANGELOG', - 'README', - 'bar/branch-test.txt', - 'custom-highlighting/test.gitlab-custom', - 'encoding/iso8859.txt', - 'files/.DS_Store', - 'files/images/wm.svg', - 'files/js/commit.coffee', - 'files/lfs/lfs_object.iso', - 'files/ruby/popen.rb', - 'files/ruby/regex.rb', - 'files/whitespace', - 'foo/bar/.gitkeep', - 'gitlab-grack', - 'gitlab-shell', - 'with space/README.md' - ]) - end - end - it 'expands collapsed diffs before saving' do mr_diff = create(:merge_request, source_branch: 'expand-collapse-lines', target_branch: 'master').merge_request_diff diff_file = mr_diff.merge_request_diff_files.find_by(new_path: 'expand-collapse/file-5.txt') @@ -1166,5 +1080,9 @@ RSpec.describe MergeRequestDiff do it 'loads nothing if the merge request has no diff record' do expect(described_class.latest_diff_for_merge_requests(merge_request_3)).to be_empty end + + it 'loads nothing if nil was passed as merge_request' do + expect(described_class.latest_diff_for_merge_requests(nil)).to be_empty + end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 4b46c98117f..a77ca1e9a51 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2255,7 +2255,7 @@ RSpec.describe MergeRequest, factory_default: :keep do describe '#find_codequality_mr_diff_reports' do let(:project) { create(:project, :repository) } - let(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports, source_project: project) } + let(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports, source_project: project, id: 123456789) } let(:pipeline) { merge_request.head_pipeline } subject(:mr_diff_report) { merge_request.find_codequality_mr_diff_reports } @@ -2628,7 +2628,7 @@ RSpec.describe MergeRequest, factory_default: :keep do context 'when the MR has been merged' do before do MergeRequests::MergeService - .new(subject.target_project, subject.author, { sha: subject.diff_head_sha }) + .new(project: subject.target_project, current_user: subject.author, params: { sha: subject.diff_head_sha }) .execute(subject) end @@ -3876,6 +3876,20 @@ RSpec.describe MergeRequest, factory_default: :keep do subject { merge_request.use_merge_base_pipeline_for_comparison?(service_class) } + context 'when service class is Ci::CompareMetricsReportsService' do + let(:service_class) { 'Ci::CompareMetricsReportsService' } + + it { is_expected.to be_truthy } + + context 'with the metrics report flag disabled' do + before do + stub_feature_flags(merge_base_pipeline_for_metrics_comparison: false) + end + + it { is_expected.to be_falsey } + end + end + context 'when service class is Ci::CompareCodequalityReportsService' do let(:service_class) { 'Ci::CompareCodequalityReportsService' } @@ -4806,7 +4820,7 @@ RSpec.describe MergeRequest, factory_default: :keep do context 'when merge_ref_sha is not present' do let!(:result) do MergeRequests::MergeToRefService - .new(merge_request.project, merge_request.author) + .new(project: merge_request.project, current_user: merge_request.author) .execute(merge_request) end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index e611484f5ee..20dee288052 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -293,21 +293,7 @@ RSpec.describe Milestone do end end - context 'when `optimized_timebox_queries` feature flag is enabled' do - before do - stub_feature_flags(optimized_timebox_queries: true) - end - - it_behaves_like '#for_projects_and_groups' - end - - context 'when `optimized_timebox_queries` feature flag is disabled' do - before do - stub_feature_flags(optimized_timebox_queries: false) - end - - it_behaves_like '#for_projects_and_groups' - end + it_behaves_like '#for_projects_and_groups' describe '.upcoming_ids' do let(:group_1) { create(:group) } diff --git a/spec/models/namespace/package_setting_spec.rb b/spec/models/namespace/package_setting_spec.rb index 097cef8ef3b..4308c8c06bc 100644 --- a/spec/models/namespace/package_setting_spec.rb +++ b/spec/models/namespace/package_setting_spec.rb @@ -14,9 +14,12 @@ RSpec.describe Namespace::PackageSetting do it { is_expected.to allow_value(true).for(:maven_duplicates_allowed) } it { is_expected.to allow_value(false).for(:maven_duplicates_allowed) } it { is_expected.not_to allow_value(nil).for(:maven_duplicates_allowed) } + it { is_expected.to allow_value(true).for(:generic_duplicates_allowed) } + it { is_expected.to allow_value(false).for(:generic_duplicates_allowed) } + it { is_expected.not_to allow_value(nil).for(:generic_duplicates_allowed) } end - describe '#maven_duplicate_exception_regex' do + describe 'regex values' do let_it_be(:package_settings) { create(:namespace_package_setting) } subject { package_settings } @@ -24,12 +27,14 @@ RSpec.describe Namespace::PackageSetting do valid_regexps = %w[SNAPSHOT .* v.+ v10.1.* (?:v.+|SNAPSHOT|TEMP)] invalid_regexps = ['[', '(?:v.+|SNAPSHOT|TEMP'] - valid_regexps.each do |valid_regexp| - it { is_expected.to allow_value(valid_regexp).for(:maven_duplicate_exception_regex) } - end + [:maven_duplicate_exception_regex, :generic_duplicate_exception_regex].each do |attribute| + valid_regexps.each do |valid_regexp| + it { is_expected.to allow_value(valid_regexp).for(attribute) } + end - invalid_regexps.each do |invalid_regexp| - it { is_expected.not_to allow_value(invalid_regexp).for(:maven_duplicate_exception_regex) } + invalid_regexps.each do |invalid_regexp| + it { is_expected.not_to allow_value(invalid_regexp).for(attribute) } + end end end end @@ -41,8 +46,8 @@ RSpec.describe Namespace::PackageSetting do context 'package types with package_settings' do # As more package types gain settings they will be added to this list - [:maven_package].each do |format| - let_it_be(:package) { create(format) } # rubocop:disable Rails/SaveBang + [:maven_package, :generic_package].each do |format| + let_it_be(:package) { create(format, name: 'foo', version: 'beta') } # rubocop:disable Rails/SaveBang let_it_be(:package_type) { package.package_type } let_it_be(:package_setting) { package.project.namespace.package_settings } @@ -50,6 +55,8 @@ RSpec.describe Namespace::PackageSetting do true | '' | true false | '' | false false | '.*' | true + false | 'fo.*' | true + false | 'be.*' | true end with_them do @@ -68,7 +75,7 @@ RSpec.describe Namespace::PackageSetting do end context 'package types without package_settings' do - [:npm_package, :conan_package, :nuget_package, :pypi_package, :composer_package, :generic_package, :golang_package, :debian_package].each do |format| + [:npm_package, :conan_package, :nuget_package, :pypi_package, :composer_package, :golang_package, :debian_package].each do |format| let_it_be(:package) { create(format) } # rubocop:disable Rails/SaveBang let_it_be(:package_setting) { package.project.namespace.package_settings } diff --git a/spec/models/namespace/traversal_hierarchy_spec.rb b/spec/models/namespace/traversal_hierarchy_spec.rb index b166d541171..2cd66f42458 100644 --- a/spec/models/namespace/traversal_hierarchy_spec.rb +++ b/spec/models/namespace/traversal_hierarchy_spec.rb @@ -43,16 +43,6 @@ RSpec.describe Namespace::TraversalHierarchy, type: :model do end end - shared_examples 'locked update query' do - it 'locks query with FOR UPDATE' do - qr = ActiveRecord::QueryRecorder.new do - subject - end - expect(qr.count).to eq 1 - expect(qr.log.first).to match /FOR UPDATE/ - end - end - describe '#incorrect_traversal_ids' do let!(:hierarchy) { described_class.new(root) } @@ -63,12 +53,6 @@ RSpec.describe Namespace::TraversalHierarchy, type: :model do end it { is_expected.to match_array Namespace.all } - - context 'when lock is true' do - subject { hierarchy.incorrect_traversal_ids(lock: true).load } - - it_behaves_like 'locked update query' - end end describe '#sync_traversal_ids!' do @@ -79,14 +63,18 @@ RSpec.describe Namespace::TraversalHierarchy, type: :model do it { expect(hierarchy.incorrect_traversal_ids).to be_empty } it_behaves_like 'hierarchy with traversal_ids' - it_behaves_like 'locked update query' + it_behaves_like 'locked row' do + let(:recorded_queries) { ActiveRecord::QueryRecorder.new } + let(:row) { root } - context 'when deadlocked' do before do - connection_double = double(:connection) + recorded_queries.record { subject } + end + end - allow(Namespace).to receive(:connection).and_return(connection_double) - allow(connection_double).to receive(:exec_query) { raise ActiveRecord::Deadlocked.new } + context 'when deadlocked' do + before do + allow(root).to receive(:lock!) { raise ActiveRecord::Deadlocked } end it { expect { subject }.to raise_error(ActiveRecord::Deadlocked) } diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 96ecc9836d4..56afe49e15f 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -141,7 +141,7 @@ RSpec.describe Namespace do end it 'allows updating other attributes for existing record' do - namespace = build(:namespace, path: 'j') + namespace = build(:namespace, path: 'j', owner: create(:user)) namespace.save(validate: false) namespace.reload @@ -212,6 +212,54 @@ RSpec.describe Namespace do end end + describe "after_commit :expire_child_caches" do + let(:namespace) { create(:group) } + + it "expires the child caches when updated" do + child_1 = create(:group, parent: namespace, updated_at: 1.week.ago) + child_2 = create(:group, parent: namespace, updated_at: 1.day.ago) + grandchild = create(:group, parent: child_1, updated_at: 1.week.ago) + project_1 = create(:project, namespace: namespace, updated_at: 2.days.ago) + project_2 = create(:project, namespace: child_1, updated_at: 3.days.ago) + project_3 = create(:project, namespace: grandchild, updated_at: 4.years.ago) + + freeze_time do + namespace.update!(path: "foo") + + [namespace, child_1, child_2, grandchild, project_1, project_2, project_3].each do |record| + expect(record.reload.updated_at).to eq(Time.zone.now) + end + end + end + + it "expires on name changes" do + expect(namespace).to receive(:expire_child_caches).once + + namespace.update!(name: "Foo") + end + + it "expires on path changes" do + expect(namespace).to receive(:expire_child_caches).once + + namespace.update!(path: "bar") + end + + it "expires on parent changes" do + expect(namespace).to receive(:expire_child_caches).once + + namespace.update!(parent: create(:group)) + end + + it "doesn't expire on other field changes" do + expect(namespace).not_to receive(:expire_child_caches) + + namespace.update!( + description: "Foo bar", + max_artifacts_size: 10 + ) + end + end + describe '#visibility_level_field' do it { expect(namespace.visibility_level_field).to eq(:visibility_level) } end @@ -224,6 +272,41 @@ RSpec.describe Namespace do it { expect(namespace.human_name).to eq(namespace.owner_name) } end + describe '#any_project_has_container_registry_tags?' do + subject { namespace.any_project_has_container_registry_tags? } + + let!(:project_without_registry) { create(:project, namespace: namespace) } + + context 'without tags' do + it { is_expected.to be_falsey } + end + + context 'with tags' do + before do + repositories = create_list(:container_repository, 3) + create(:project, namespace: namespace, container_repositories: repositories) + + stub_container_registry_config(enabled: true) + end + + it 'finds tags' do + stub_container_registry_tags(repository: :any, tags: ['tag']) + + is_expected.to be_truthy + end + + it 'does not cause N+1 query in fetching registries' do + stub_container_registry_tags(repository: :any, tags: []) + control_count = ActiveRecord::QueryRecorder.new { namespace.any_project_has_container_registry_tags? }.count + + other_repositories = create_list(:container_repository, 2) + create(:project, namespace: namespace, container_repositories: other_repositories) + + expect { namespace.any_project_has_container_registry_tags? }.not_to exceed_query_limit(control_count + 1) + end + end + end + describe '#first_project_with_container_registry_tags' do let(:container_repository) { create(:container_repository) } let!(:project) { create(:project, namespace: namespace, container_repositories: [container_repository]) } @@ -880,7 +963,7 @@ RSpec.describe Namespace do end describe '#use_traversal_ids?' do - let_it_be(:namespace) { build(:namespace) } + let_it_be(:namespace, reload: true) { create(:namespace) } subject { namespace.use_traversal_ids? } @@ -901,30 +984,6 @@ RSpec.describe Namespace do end end - context 'when use_traversal_ids feature flag is true' do - it_behaves_like 'namespace traversal' - - describe '#self_and_descendants' do - subject { namespace.self_and_descendants } - - it { expect(subject.to_sql).to include 'traversal_ids @>' } - end - end - - context 'when use_traversal_ids feature flag is false' do - before do - stub_feature_flags(use_traversal_ids: false) - end - - it_behaves_like 'namespace traversal' - - describe '#self_and_descendants' do - subject { namespace.self_and_descendants } - - it { expect(subject.to_sql).not_to include 'traversal_ids @>' } - end - end - describe '#users_with_descendants' do let(:user_a) { create(:user) } let(:user_b) { create(:user) } diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 992b2246f01..4eabc266b40 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -1384,6 +1384,16 @@ RSpec.describe Note do expect(notes.second.id).to eq(note2.id) end end + + describe '.with_suggestions' do + it 'returns the correct note' do + note_with_suggestion = create(:note, suggestions: [create(:suggestion)]) + note_without_suggestion = create(:note) + + expect(described_class.with_suggestions).to include(note_with_suggestion) + expect(described_class.with_suggestions).not_to include(note_without_suggestion) + end + end end describe '#noteable_assignee_or_author?' do diff --git a/spec/models/packages/dependency_spec.rb b/spec/models/packages/dependency_spec.rb index 4437cad46cd..1575dec98c9 100644 --- a/spec/models/packages/dependency_spec.rb +++ b/spec/models/packages/dependency_spec.rb @@ -18,6 +18,7 @@ RSpec.describe Packages::Dependency, type: :model do let_it_be(:package_dependency1) { create(:packages_dependency, name: 'foo', version_pattern: '~1.0.0') } let_it_be(:package_dependency2) { create(:packages_dependency, name: 'bar', version_pattern: '~2.5.0') } let_it_be(:expected_ids) { [package_dependency1.id, package_dependency2.id] } + let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2) } let(:chunk_size) { 50 } let(:rows_limit) { 50 } @@ -40,6 +41,7 @@ RSpec.describe Packages::Dependency, type: :model do context 'with a name bigger than column size' do let_it_be(:big_name) { 'a' * (Packages::Dependency::MAX_STRING_LENGTH + 1) } + let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2).merge(big_name => '~1.0.0') } it { is_expected.to match_array(expected_ids) } @@ -47,6 +49,7 @@ RSpec.describe Packages::Dependency, type: :model do context 'with a version pattern bigger than column size' do let_it_be(:big_version_pattern) { 'a' * (Packages::Dependency::MAX_STRING_LENGTH + 1) } + let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2).merge('test' => big_version_pattern) } it { is_expected.to match_array(expected_ids) } @@ -65,6 +68,7 @@ RSpec.describe Packages::Dependency, type: :model do let_it_be(:package_dependency5) { create(:packages_dependency, name: 'foo5', version_pattern: '~1.5.5') } let_it_be(:package_dependency6) { create(:packages_dependency, name: 'foo6', version_pattern: '~1.5.6') } let_it_be(:package_dependency7) { create(:packages_dependency, name: 'foo7', version_pattern: '~1.5.7') } + let(:expected_ids) { [package_dependency1.id, package_dependency2.id, package_dependency3.id, package_dependency4.id, package_dependency5.id, package_dependency6.id, package_dependency7.id] } let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2, package_dependency3, package_dependency4, package_dependency5, package_dependency6, package_dependency7) } @@ -86,6 +90,7 @@ RSpec.describe Packages::Dependency, type: :model do let_it_be(:package_dependency1) { create(:packages_dependency, name: 'foo', version_pattern: '~1.0.0') } let_it_be(:package_dependency2) { create(:packages_dependency, name: 'bar', version_pattern: '~2.5.0') } let_it_be(:expected_array) { [package_dependency1, package_dependency2] } + let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2) } subject { Packages::Dependency.for_package_names_and_version_patterns(names_and_version_patterns) } diff --git a/spec/models/packages/go/module_version_spec.rb b/spec/models/packages/go/module_version_spec.rb index 7fa416d8537..cace2160878 100644 --- a/spec/models/packages/go/module_version_spec.rb +++ b/spec/models/packages/go/module_version_spec.rb @@ -32,16 +32,19 @@ RSpec.describe Packages::Go::ModuleVersion, type: :model do describe '#name' do context 'with ref and name specified' do let_it_be(:version) { create :go_module_version, mod: mod, name: 'foobar', commit: project.repository.head_commit, ref: project.repository.find_tag('v1.0.0') } + it('returns that name') { expect(version.name).to eq('foobar') } end context 'with ref specified and name unspecified' do let_it_be(:version) { create :go_module_version, mod: mod, commit: project.repository.head_commit, ref: project.repository.find_tag('v1.0.0') } + it('returns the name of the ref') { expect(version.name).to eq('v1.0.0') } end context 'with ref and name unspecified' do let_it_be(:version) { create :go_module_version, mod: mod, commit: project.repository.head_commit } + it('returns nil') { expect(version.name).to eq(nil) } end end @@ -49,11 +52,13 @@ RSpec.describe Packages::Go::ModuleVersion, type: :model do describe '#gomod' do context 'with go.mod missing' do let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.0' } + it('returns nil') { expect(version.gomod).to eq(nil) } end context 'with go.mod present' do let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.1' } + it('returns the contents of go.mod') { expect(version.gomod).to eq("module #{mod.name}\n") } end end @@ -62,6 +67,7 @@ RSpec.describe Packages::Go::ModuleVersion, type: :model do context 'with a root module' do context 'with an empty module path' do let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.2' } + it_behaves_like '#files', 'all the files', 'README.md', 'go.mod', 'a.go', 'pkg/b.go' end end @@ -69,12 +75,14 @@ RSpec.describe Packages::Go::ModuleVersion, type: :model do context 'with a root module and a submodule' do context 'with an empty module path' do let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } + it_behaves_like '#files', 'files excluding the submodule', 'README.md', 'go.mod', 'a.go', 'pkg/b.go' end context 'with the submodule\'s path' do let_it_be(:mod) { create :go_module, project: project, path: 'mod' } let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } + it_behaves_like '#files', 'the submodule\'s files', 'mod/go.mod', 'mod/a.go' end end @@ -84,6 +92,7 @@ RSpec.describe Packages::Go::ModuleVersion, type: :model do context 'with a root module' do context 'with an empty module path' do let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.2' } + it_behaves_like '#archive', 'all the files', 'README.md', 'go.mod', 'a.go', 'pkg/b.go' end end @@ -91,12 +100,14 @@ RSpec.describe Packages::Go::ModuleVersion, type: :model do context 'with a root module and a submodule' do context 'with an empty module path' do let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } + it_behaves_like '#archive', 'files excluding the submodule', 'README.md', 'go.mod', 'a.go', 'pkg/b.go' end context 'with the submodule\'s path' do let_it_be(:mod) { create :go_module, project: project, path: 'mod' } let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' } + it_behaves_like '#archive', 'the submodule\'s files', 'go.mod', 'a.go' end end diff --git a/spec/models/packages/helm/file_metadatum_spec.rb b/spec/models/packages/helm/file_metadatum_spec.rb new file mode 100644 index 00000000000..c7c17b157e4 --- /dev/null +++ b/spec/models/packages/helm/file_metadatum_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Helm::FileMetadatum, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:package_file) } + end + + describe 'validations' do + describe '#package_file' do + it { is_expected.to validate_presence_of(:package_file) } + end + + describe '#valid_helm_package_type' do + let_it_be_with_reload(:helm_package_file) { create(:helm_package_file) } + + let(:helm_file_metadatum) { helm_package_file.helm_file_metadatum } + + before do + helm_package_file.package.package_type = :pypi + end + + it 'validates package of type helm' do + expect(helm_file_metadatum).not_to be_valid + expect(helm_file_metadatum.errors.to_a).to contain_exactly('Package file Package type must be Helm') + end + end + + describe '#channel' do + it 'validates #channel', :aggregate_failures do + is_expected.to validate_presence_of(:channel) + + is_expected.to allow_value('a' * 63).for(:channel) + is_expected.not_to allow_value('a' * 64).for(:channel) + + is_expected.to allow_value('release').for(:channel) + is_expected.to allow_value('my-repo').for(:channel) + is_expected.to allow_value('my-repo42').for(:channel) + + # Do not allow empty + is_expected.not_to allow_value('').for(:channel) + + # Do not allow Unicode + is_expected.not_to allow_value('hé').for(:channel) + end + end + + describe '#metadata' do + it 'validates #metadata', :aggregate_failures do + is_expected.not_to validate_presence_of(:metadata) + is_expected.to allow_value({ 'name': 'foo', 'version': 'v1.0', 'apiVersion': 'v2' }).for(:metadata) + is_expected.not_to allow_value({}).for(:metadata) + is_expected.not_to allow_value({ 'version': 'v1.0', 'apiVersion': 'v2' }).for(:metadata) + is_expected.not_to allow_value({ 'name': 'foo', 'apiVersion': 'v2' }).for(:metadata) + is_expected.not_to allow_value({ 'name': 'foo', 'version': 'v1.0' }).for(:metadata) + end + end + end +end diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb index 9cf998a0639..f8ddd59ddc8 100644 --- a/spec/models/packages/package_file_spec.rb +++ b/spec/models/packages/package_file_spec.rb @@ -2,12 +2,18 @@ require 'spec_helper' RSpec.describe Packages::PackageFile, type: :model do + let_it_be(:project) { create(:project) } + let_it_be(:package_file1) { create(:package_file, :xml, file_name: 'FooBar') } + let_it_be(:package_file2) { create(:package_file, :xml, file_name: 'ThisIsATest') } + let_it_be(:debian_package) { create(:debian_package, project: project) } + describe 'relationships' do it { is_expected.to belong_to(:package) } it { is_expected.to have_one(:conan_file_metadatum) } it { is_expected.to have_many(:package_file_build_infos).inverse_of(:package_file) } it { is_expected.to have_many(:pipelines).through(:package_file_build_infos) } it { is_expected.to have_one(:debian_file_metadatum).inverse_of(:package_file).class_name('Packages::Debian::FileMetadatum') } + it { is_expected.to have_one(:helm_file_metadatum).inverse_of(:package_file).class_name('Packages::Helm::FileMetadatum') } end describe 'validations' do @@ -15,9 +21,6 @@ RSpec.describe Packages::PackageFile, type: :model do end context 'with package filenames' do - let_it_be(:package_file1) { create(:package_file, :xml, file_name: 'FooBar') } - let_it_be(:package_file2) { create(:package_file, :xml, file_name: 'ThisIsATest') } - describe '.with_file_name' do let(:filename) { 'FooBar' } @@ -51,6 +54,13 @@ RSpec.describe Packages::PackageFile, type: :model do end end + describe '.for_package_ids' do + it 'returns matching packages' do + expect(described_class.for_package_ids([package_file1.package.id, package_file2.package.id])) + .to contain_exactly(package_file1, package_file2) + end + end + describe '.with_conan_package_reference' do let_it_be(:non_matching_package_file) { create(:package_file, :nuget) } let_it_be(:metadatum) { create(:conan_file_metadatum, :package_file) } @@ -63,7 +73,6 @@ RSpec.describe Packages::PackageFile, type: :model do end describe '.for_rubygem_with_file_name' do - let_it_be(:project) { create(:project) } let_it_be(:non_ruby_package) { create(:nuget_package, project: project, package_type: :nuget) } let_it_be(:ruby_package) { create(:rubygems_package, project: project, package_type: :rubygems) } let_it_be(:file_name) { 'other.gem' } @@ -77,6 +86,36 @@ RSpec.describe Packages::PackageFile, type: :model do end end + context 'Debian scopes' do + let_it_be(:debian_changes) { debian_package.package_files.last } + let_it_be(:debian_deb) { create(:debian_package_file, package: debian_package)} + let_it_be(:debian_udeb) { create(:debian_package_file, :udeb, package: debian_package)} + + let_it_be(:debian_contrib) do + create(:debian_package_file, package: debian_package).tap do |pf| + pf.debian_file_metadatum.update!(component: 'contrib') + end + end + + let_it_be(:debian_mipsel) do + create(:debian_package_file, package: debian_package).tap do |pf| + pf.debian_file_metadatum.update!(architecture: 'mipsel') + end + end + + describe '#with_debian_file_type' do + it { expect(described_class.with_debian_file_type(:changes)).to contain_exactly(debian_changes) } + end + + describe '#with_debian_component_name' do + it { expect(described_class.with_debian_component_name('contrib')).to contain_exactly(debian_contrib) } + end + + describe '#with_debian_architecture_name' do + it { expect(described_class.with_debian_architecture_name('mipsel')).to contain_exactly(debian_mipsel) } + end + end + describe '#update_file_store callback' do let_it_be(:package_file) { build(:package_file, :nuget, size: nil) } diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index cf52749a186..52ef61e3d44 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -47,6 +47,7 @@ RSpec.describe Packages::Package, type: :model do describe '.sort_by_attribute' do let_it_be(:group) { create(:group, :public) } let_it_be(:project) { create(:project, :public, namespace: group, name: 'project A') } + let!(:package1) { create(:npm_package, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") } let!(:package2) { create(:nuget_package, project: project, version: '2.0.4') } let(:package3) { create(:maven_package, project: project, version: '1.1.1', name: 'zzz') } @@ -113,18 +114,6 @@ RSpec.describe Packages::Package, type: :model do expect(subject).to match_array([package1, package2]) end - - context 'with maven_packages_group_level_improvements disabled' do - before do - stub_feature_flags(maven_packages_group_level_improvements: false) - end - - it 'returns package1 and package2' do - expect(projects).to receive(:any?).and_call_original - - expect(subject).to match_array([package1, package2]) - end - end end describe 'validations' do @@ -184,6 +173,15 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.not_to allow_value('!!().for(:name)().for(:name)').for(:name) } end + context 'helm package' do + subject { build(:helm_package) } + + it { is_expected.to allow_value('prometheus').for(:name) } + it { is_expected.to allow_value('rook-ceph').for(:name) } + it { is_expected.not_to allow_value('a+b').for(:name) } + it { is_expected.not_to allow_value('Hé').for(:name) } + end + context 'nuget package' do subject { build_stubbed(:nuget_package) } @@ -210,6 +208,19 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.not_to allow_value("@scope%2e%2e%fpackage").for(:name) } it { is_expected.not_to allow_value("@scope/sub/package").for(:name) } end + + context 'terraform module package' do + subject { build_stubbed(:terraform_module_package) } + + it { is_expected.to allow_value('my-module/my-system').for(:name) } + it { is_expected.to allow_value('my/module').for(:name) } + it { is_expected.not_to allow_value('my-module').for(:name) } + it { is_expected.not_to allow_value('My-Module').for(:name) } + it { is_expected.not_to allow_value('my_module').for(:name) } + it { is_expected.not_to allow_value('my.module').for(:name) } + it { is_expected.not_to allow_value('../../../my-module').for(:name) } + it { is_expected.not_to allow_value('%2e%2e%2fmy-module').for(:name) } + end end describe '#version' do @@ -387,7 +398,17 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.not_to allow_value(nil).for(:version) } end + context 'helm package' do + subject { build_stubbed(:helm_package) } + + it { is_expected.not_to allow_value(nil).for(:version) } + it { is_expected.not_to allow_value('').for(:version) } + it { is_expected.to allow_value('v1.2.3').for(:version) } + it { is_expected.not_to allow_value('1.2.3').for(:version) } + end + it_behaves_like 'validating version to be SemVer compliant for', :npm_package + it_behaves_like 'validating version to be SemVer compliant for', :terraform_module_package context 'nuget package' do it_behaves_like 'validating version to be SemVer compliant for', :nuget_package @@ -485,6 +506,26 @@ RSpec.describe Packages::Package, type: :model do end end + describe '.with_package_type' do + let!(:package1) { create(:terraform_module_package) } + let!(:package2) { create(:npm_package) } + let(:package_type) { :terraform_module } + + subject { described_class.with_package_type(package_type) } + + it { is_expected.to eq([package1]) } + end + + describe '.without_package_type' do + let!(:package1) { create(:npm_package) } + let!(:package2) { create(:terraform_module_package) } + let(:package_type) { :terraform_module } + + subject { described_class.without_package_type(package_type) } + + it { is_expected.to eq([package1]) } + end + context 'version scopes' do let!(:package1) { create(:npm_package, version: '1.0.0') } let!(:package2) { create(:npm_package, version: '1.0.1') } @@ -565,22 +606,6 @@ RSpec.describe Packages::Package, type: :model do end end - describe '.processed' do - let!(:package1) { create(:nuget_package) } - let!(:package2) { create(:npm_package) } - let!(:package3) { create(:nuget_package) } - - subject { described_class.processed } - - it { is_expected.to match_array([package1, package2, package3]) } - - context 'with temporary packages' do - let!(:package1) { create(:nuget_package, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) } - - it { is_expected.to match_array([package2, package3]) } - end - end - describe '.limit_recent' do let!(:package1) { create(:nuget_package) } let!(:package2) { create(:nuget_package) } @@ -653,27 +678,37 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.to match_array([pypi_package]) } end - describe '.displayable' do + context 'status scopes' do let_it_be(:hidden_package) { create(:maven_package, :hidden) } let_it_be(:processing_package) { create(:maven_package, :processing) } let_it_be(:error_package) { create(:maven_package, :error) } - subject { described_class.displayable } + describe '.displayable' do + subject { described_class.displayable } - it 'does not include non-displayable packages', :aggregate_failures do - is_expected.to include(error_package) - is_expected.not_to include(hidden_package) - is_expected.not_to include(processing_package) + it 'does not include non-displayable packages', :aggregate_failures do + is_expected.to include(error_package) + is_expected.not_to include(hidden_package) + is_expected.not_to include(processing_package) + end end - end - describe '.with_status' do - let_it_be(:hidden_package) { create(:maven_package, :hidden) } + describe '.installable' do + subject { described_class.installable } - subject { described_class.with_status(:hidden) } + it 'does not include non-displayable packages', :aggregate_failures do + is_expected.not_to include(error_package) + is_expected.not_to include(hidden_package) + is_expected.not_to include(processing_package) + end + end + + describe '.with_status' do + subject { described_class.with_status(:hidden) } - it 'returns packages with specified status' do - is_expected.to match_array([hidden_package]) + it 'returns packages with specified status' do + is_expected.to match_array([hidden_package]) + end end end end @@ -896,6 +931,7 @@ RSpec.describe Packages::Package, type: :model do let_it_be(:package_name) { 'composer-package-name' } let_it_be(:json) { { 'name' => package_name } } let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json.to_json } ) } + let!(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } before do diff --git a/spec/models/packages/tag_spec.rb b/spec/models/packages/tag_spec.rb index 18ec99c3d51..842ba7ad518 100644 --- a/spec/models/packages/tag_spec.rb +++ b/spec/models/packages/tag_spec.rb @@ -41,6 +41,7 @@ RSpec.describe Packages::Tag, type: :model do let_it_be(:tag1) { create(:packages_tag, package: package, name: 'tag1') } let_it_be(:tag2) { create(:packages_tag, package: package, name: 'tag2') } let_it_be(:tag3) { create(:packages_tag, package: package, name: 'tag3') } + let(:name) { 'tag1' } subject { described_class.with_name(name) } diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb index f2659771a49..735f2225c21 100644 --- a/spec/models/pages/lookup_path_spec.rb +++ b/spec/models/pages/lookup_path_spec.rb @@ -47,20 +47,13 @@ RSpec.describe Pages::LookupPath do describe '#source' do let(:source) { lookup_path.source } - shared_examples 'uses disk storage' do - it 'uses disk storage', :aggregate_failures do - expect(source[:type]).to eq('file') - expect(source[:path]).to eq(project.full_path + "/public/") - end + it 'uses disk storage', :aggregate_failures do + expect(source[:type]).to eq('file') + expect(source[:path]).to eq(project.full_path + "/public/") end - include_examples 'uses disk storage' - - it 'return nil when legacy storage is disabled and there is no deployment' do - stub_feature_flags(pages_serve_from_legacy_storage: false) - expect(Gitlab::ErrorTracking).to receive(:track_exception) - .with(described_class::LegacyStorageDisabledError, project_id: project.id) - .and_call_original + it 'return nil when local storage is disabled and there is no deployment' do + allow(Settings.pages.local_store).to receive(:enabled).and_return(false) expect(source).to eq(nil) end @@ -107,14 +100,6 @@ RSpec.describe Pages::LookupPath do ) end end - - context 'when pages_serve_with_zip_file_protocol feature flag is disabled' do - before do - stub_feature_flags(pages_serve_with_zip_file_protocol: false) - end - - include_examples 'uses disk storage' - end end context 'when deployment were created during migration' do diff --git a/spec/models/plan_limits_spec.rb b/spec/models/plan_limits_spec.rb index 4259c8b708b..b8c723b3847 100644 --- a/spec/models/plan_limits_spec.rb +++ b/spec/models/plan_limits_spec.rb @@ -210,6 +210,7 @@ RSpec.describe PlanLimits do ci_active_jobs storage_size_limit daily_invites + web_hook_calls ] + disabled_max_artifact_size_columns end diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb index 8313879114f..d5f0b66b210 100644 --- a/spec/models/project_auto_devops_spec.rb +++ b/spec/models/project_auto_devops_spec.rb @@ -70,7 +70,7 @@ RSpec.describe ProjectAutoDevops do it 'does not create a gitlab deploy token' do expect do - auto_devops.save + auto_devops.save! end.not_to change { DeployToken.count } end end @@ -80,7 +80,7 @@ RSpec.describe ProjectAutoDevops do it 'creates a gitlab deploy token' do expect do - auto_devops.save + auto_devops.save! end.to change { DeployToken.count }.by(1) end end @@ -90,7 +90,7 @@ RSpec.describe ProjectAutoDevops do it 'creates a gitlab deploy token' do expect do - auto_devops.save + auto_devops.save! end.to change { DeployToken.count }.by(1) end end @@ -101,7 +101,7 @@ RSpec.describe ProjectAutoDevops do it 'creates a deploy token' do expect do - auto_devops.save + auto_devops.save! end.to change { DeployToken.count }.by(1) end end @@ -114,7 +114,7 @@ RSpec.describe ProjectAutoDevops do allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true) expect do - auto_devops.save + auto_devops.save! end.to change { DeployToken.count }.by(1) end end @@ -125,7 +125,7 @@ RSpec.describe ProjectAutoDevops do it 'does not create a deploy token' do expect do - auto_devops.save + auto_devops.save! end.not_to change { DeployToken.count } end end @@ -137,7 +137,7 @@ RSpec.describe ProjectAutoDevops do it 'does not create a deploy token' do expect do - auto_devops.save + auto_devops.save! end.not_to change { DeployToken.count } end end @@ -149,7 +149,7 @@ RSpec.describe ProjectAutoDevops do it 'does not create a deploy token' do expect do - auto_devops.save + auto_devops.save! end.not_to change { DeployToken.count } end end diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index a56018f0fee..3fd7e57a5db 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -20,7 +20,7 @@ RSpec.describe ProjectFeature do context 'repository related features' do before do - project.project_feature.update( + project.project_feature.update!( merge_requests_access_level: ProjectFeature::DISABLED, builds_access_level: ProjectFeature::DISABLED, repository_access_level: ProjectFeature::PRIVATE diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb index 476d99364b6..62f97873a06 100644 --- a/spec/models/project_services/chat_notification_service_spec.rb +++ b/spec/models/project_services/chat_notification_service_spec.rb @@ -11,6 +11,10 @@ RSpec.describe ChatNotificationService do it { is_expected.to validate_presence_of :webhook } end + describe 'validations' do + it { is_expected.to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]).allow_blank } + end + describe '#can_test?' do context 'with empty repository' do it 'returns true' do @@ -32,8 +36,9 @@ RSpec.describe ChatNotificationService do describe '#execute' do subject(:chat_service) { described_class.new } + let_it_be(:project) { create(:project, :repository) } + let(:user) { create(:user) } - let(:project) { create(:project, :repository) } let(:webhook_url) { 'https://example.gitlab.com/' } let(:data) { Gitlab::DataBuilder::Push.build_sample(subject.project, user) } @@ -76,9 +81,12 @@ RSpec.describe ChatNotificationService do end context 'when the data object has a label' do - let(:label) { create(:label, project: project, name: 'Bug')} - let(:issue) { create(:labeled_issue, project: project, labels: [label]) } - let(:note) { create(:note, noteable: issue, project: project)} + let_it_be(:label) { create(:label, name: 'Bug') } + let_it_be(:label_2) { create(:label, name: 'Community contribution') } + let_it_be(:label_3) { create(:label, name: 'Backend') } + let_it_be(:issue) { create(:labeled_issue, project: project, labels: [label, label_2, label_3]) } + let_it_be(:note) { create(:note, noteable: issue, project: project) } + let(:data) { Gitlab::DataBuilder::Note.build(note, user) } it 'notifies the chat service' do @@ -87,23 +95,139 @@ RSpec.describe ChatNotificationService do chat_service.execute(data) end - context 'and the chat_service has a label filter that does not matches the label' do - subject(:chat_service) { described_class.new(labels_to_be_notified: '~some random label') } + shared_examples 'notifies the chat service' do + specify do + expect(chat_service).to receive(:notify).with(any_args) + + chat_service.execute(data) + end + end - it 'does not notify the chat service' do - expect(chat_service).not_to receive(:notify) + shared_examples 'does not notify the chat service' do + specify do + expect(chat_service).not_to receive(:notify).with(any_args) chat_service.execute(data) end end - context 'and the chat_service has a label filter that matches the label' do - subject(:chat_service) { described_class.new(labels_to_be_notified: '~Backend, ~Bug') } + context 'when labels_to_be_notified_behavior is not defined' do + subject(:chat_service) { described_class.new(labels_to_be_notified: label_filter) } - it 'notifies the chat service' do - expect(chat_service).to receive(:notify).with(any_args) + context 'no matching labels' do + let(:label_filter) { '~some random label' } - chat_service.execute(data) + it_behaves_like 'does not notify the chat service' + end + + context 'only one label matches' do + let(:label_filter) { '~some random label, ~Bug' } + + it_behaves_like 'notifies the chat service' + end + end + + context 'when labels_to_be_notified_behavior is blank' do + subject(:chat_service) { described_class.new(labels_to_be_notified: label_filter, labels_to_be_notified_behavior: '') } + + context 'no matching labels' do + let(:label_filter) { '~some random label' } + + it_behaves_like 'does not notify the chat service' + end + + context 'only one label matches' do + let(:label_filter) { '~some random label, ~Bug' } + + it_behaves_like 'notifies the chat service' + end + end + + context 'when labels_to_be_notified_behavior is match_any' do + subject(:chat_service) do + described_class.new( + labels_to_be_notified: label_filter, + labels_to_be_notified_behavior: 'match_any' + ) + end + + context 'no label filter' do + let(:label_filter) { nil } + + it_behaves_like 'notifies the chat service' + end + + context 'no matching labels' do + let(:label_filter) { '~some random label' } + + it_behaves_like 'does not notify the chat service' + end + + context 'only one label matches' do + let(:label_filter) { '~some random label, ~Bug' } + + it_behaves_like 'notifies the chat service' + end + end + + context 'when labels_to_be_notified_behavior is match_all' do + subject(:chat_service) do + described_class.new( + labels_to_be_notified: label_filter, + labels_to_be_notified_behavior: 'match_all' + ) + end + + context 'no label filter' do + let(:label_filter) { nil } + + it_behaves_like 'notifies the chat service' + end + + context 'no matching labels' do + let(:label_filter) { '~some random label' } + + it_behaves_like 'does not notify the chat service' + end + + context 'only one label matches' do + let(:label_filter) { '~some random label, ~Bug' } + + it_behaves_like 'does not notify the chat service' + end + + context 'labels matches exactly' do + let(:label_filter) { '~Bug, ~Backend, ~Community contribution' } + + it_behaves_like 'notifies the chat service' + end + + context 'labels matches but object has more' do + let(:label_filter) { '~Bug, ~Backend' } + + it_behaves_like 'notifies the chat service' + end + + context 'labels are distributed on multiple objects' do + let(:label_filter) { '~Bug, ~Backend' } + let(:data) do + Gitlab::DataBuilder::Note.build(note, user).merge({ + issue: { + labels: [ + { title: 'Bug' } + ] + }, + merge_request: { + labels: [ + { + title: 'Backend' + } + ] + } + }) + end + + it_behaves_like 'does not notify the chat service' end end end diff --git a/spec/models/project_services/data_fields_spec.rb b/spec/models/project_services/data_fields_spec.rb index 9a3042f9f8d..d3e6afe4978 100644 --- a/spec/models/project_services/data_fields_spec.rb +++ b/spec/models/project_services/data_fields_spec.rb @@ -138,8 +138,8 @@ RSpec.describe DataFields do context 'when data are stored in both properties and data_fields' do let(:service) do - create(:jira_service, :without_properties_callback, active: false, properties: properties).tap do |service| - create(:jira_tracker_data, properties.merge(service: service)) + create(:jira_service, :without_properties_callback, active: false, properties: properties).tap do |integration| + create(:jira_tracker_data, properties.merge(integration: integration)) end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 82a4cde752b..42368c31ba0 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -2,91 +2,35 @@ require 'spec_helper' +# HipchatService is partially removed and it will be remove completely +# after the deletion of all the database records. +# https://gitlab.com/gitlab-org/gitlab/-/issues/27954 RSpec.describe HipchatService do - describe "Associations" do - it { is_expected.to belong_to :project } - it { is_expected.to have_one :service_hook } - end + let_it_be(:project) { create(:project) } - describe 'Validations' do - context 'when service is active' do - before do - subject.active = true - end + subject(:service) { described_class.new(project: project) } - it { is_expected.to validate_presence_of(:token) } - end + it { is_expected.to be_valid } - context 'when service is inactive' do - before do - subject.active = false - end + describe '#to_param' do + subject { service.to_param } - it { is_expected.not_to validate_presence_of(:token) } - end + it { is_expected.to eq('hipchat') } end - describe "Execute" do - let(:hipchat) { described_class.new } - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' } - let(:project_name) { project.full_name.gsub(/\s/, '') } - let(:token) { 'verySecret' } - let(:server_url) { 'https://hipchat.example.com'} - let(:push_sample_data) do - Gitlab::DataBuilder::Push.build_sample(project, user) - end - - before do - allow(hipchat).to receive_messages( - project_id: project.id, - project: project, - room: 123456, - server: server_url, - token: token - ) - WebMock.stub_request(:post, api_url) - end - - it 'does nothing' do - expect { hipchat.execute(push_sample_data) }.not_to raise_error - end + describe '#supported_events' do + subject { service.supported_events } - describe "#message_options" do - it "is set to the defaults" do - expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'yellow' }) - end - - it "sets notify to true" do - allow(hipchat).to receive(:notify).and_return('1') - - expect(hipchat.__send__(:message_options)).to eq({ notify: true, color: 'yellow' }) - end - - it "sets the color" do - allow(hipchat).to receive(:color).and_return('red') - - expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'red' }) - end - - context 'with a successful build' do - it 'uses the green color' do - data = { object_kind: 'pipeline', - object_attributes: { status: 'success' } } - - expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' }) - end - end + it { is_expected.to be_empty } + end - context 'with a failed build' do - it 'uses the red color' do - data = { object_kind: 'pipeline', - object_attributes: { status: 'failed' } } + describe '#save' do + it 'prevents records from being created or updated' do + expect(service.save).to be_falsey - expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' }) - end - end + expect(service.errors.full_messages).to include( + 'HipChat endpoint is deprecated and should not be created or modified.' + ) end end end diff --git a/spec/models/project_services/issue_tracker_data_spec.rb b/spec/models/project_services/issue_tracker_data_spec.rb index 3ddb7d9250f..a229285f09b 100644 --- a/spec/models/project_services/issue_tracker_data_spec.rb +++ b/spec/models/project_services/issue_tracker_data_spec.rb @@ -3,9 +3,7 @@ require 'spec_helper' RSpec.describe IssueTrackerData do - let(:service) { create(:custom_issue_tracker_service, active: false, properties: {}) } - - describe 'Associations' do - it { is_expected.to belong_to :service } + describe 'associations' do + it { is_expected.to belong_to :integration } end end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index b50fa1edbc3..73e91bf9ea8 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -433,8 +433,8 @@ RSpec.describe JiraService do context 'when data are stored in both properties and separated fields' do let(:properties) { data_params } let(:service) do - create(:jira_service, :without_properties_callback, active: false, properties: properties).tap do |service| - create(:jira_tracker_data, data_params.merge(service: service)) + create(:jira_service, :without_properties_callback, active: false, properties: properties).tap do |integration| + create(:jira_tracker_data, data_params.merge(integration: integration)) end end diff --git a/spec/models/project_services/jira_tracker_data_spec.rb b/spec/models/project_services/jira_tracker_data_spec.rb index a698d3fce5f..72bdbe40a74 100644 --- a/spec/models/project_services/jira_tracker_data_spec.rb +++ b/spec/models/project_services/jira_tracker_data_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe JiraTrackerData do describe 'associations' do - it { is_expected.to belong_to(:service) } + it { is_expected.to belong_to(:integration) } end describe 'deployment_type' do diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 4fff3bc56cc..87befdd4303 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -49,7 +49,7 @@ RSpec.describe MattermostSlashCommandsService do end it 'saves the service' do - expect { subject }.to change { project.services.count }.by(1) + expect { subject }.to change { project.integrations.count }.by(1) end it 'saves the token' do diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb index 53ab63ef030..5f3a94a5b99 100644 --- a/spec/models/project_services/microsoft_teams_service_spec.rb +++ b/spec/models/project_services/microsoft_teams_service_spec.rb @@ -73,7 +73,7 @@ RSpec.describe MicrosoftTeamsService do context 'with issue events' do let(:opts) { { title: 'Awesome issue', description: 'please fix' } } let(:issues_sample_data) do - service = Issues::CreateService.new(project, user, opts) + service = Issues::CreateService.new(project: project, current_user: user, params: opts) issue = service.execute service.hook_data(issue, 'open') end @@ -96,7 +96,7 @@ RSpec.describe MicrosoftTeamsService do end let(:merge_sample_data) do - service = MergeRequests::CreateService.new(project, user, opts) + service = MergeRequests::CreateService.new(project: project, current_user: user, params: opts) merge_request = service.execute service.hook_data(merge_request, 'open') end @@ -240,7 +240,7 @@ RSpec.describe MicrosoftTeamsService do chat_service.execute(data) - message = ChatMessage::PipelineMessage.new(data) + message = Integrations::ChatMessage::PipelineMessage.new(data) expect(WebMock).to have_requested(:post, webhook_url) .with(body: hash_including({ summary: message.summary })) diff --git a/spec/models/project_services/open_project_tracker_data_spec.rb b/spec/models/project_services/open_project_tracker_data_spec.rb index e6a3963ba87..1f7f01cfea4 100644 --- a/spec/models/project_services/open_project_tracker_data_spec.rb +++ b/spec/models/project_services/open_project_tracker_data_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.describe OpenProjectTrackerData do - describe 'Associations' do - it { is_expected.to belong_to(:service) } + describe 'associations' do + it { is_expected.to belong_to(:integration) } end describe 'closed_status_id' do diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 688a59fcf09..2e2c1c666d9 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -59,16 +59,22 @@ RSpec.describe SlackService do context 'deployment notification' do let_it_be(:deployment) { create(:deployment, user: user) } - let(:data) { Gitlab::DataBuilder::Deployment.build(deployment) } + let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.current) } it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_deployment_notification' end context 'wiki_page notification' do - let_it_be(:wiki_page) { create(:wiki_page, wiki: project.wiki, message: 'user created page: Awesome wiki_page') } + let(:wiki_page) { create(:wiki_page, wiki: project.wiki, message: 'user created page: Awesome wiki_page') } let(:data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') } + before do + # Skip this method that is not relevant to this test to prevent having + # to update project which is frozen + allow(project.wiki).to receive(:after_wiki_activity) + end + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_wiki_page_notification' end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 12c17e699e3..c57c2792f87 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to belong_to(:creator).class_name('User') } it { is_expected.to belong_to(:pool_repository) } it { is_expected.to have_many(:users) } - it { is_expected.to have_many(:services) } + it { is_expected.to have_many(:integrations) } it { is_expected.to have_many(:events) } it { is_expected.to have_many(:merge_requests) } it { is_expected.to have_many(:merge_request_metrics).class_name('MergeRequest::Metrics') } @@ -46,13 +46,13 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to have_one(:asana_service) } it { is_expected.to have_many(:boards) } it { is_expected.to have_one(:campfire_service) } + it { is_expected.to have_one(:datadog_service) } it { is_expected.to have_one(:discord_service) } it { is_expected.to have_one(:drone_ci_service) } it { is_expected.to have_one(:emails_on_push_service) } it { is_expected.to have_one(:pipelines_email_service) } it { is_expected.to have_one(:irker_service) } it { is_expected.to have_one(:pivotaltracker_service) } - it { is_expected.to have_one(:hipchat_service) } it { is_expected.to have_one(:flowdock_service) } it { is_expected.to have_one(:assembla_service) } it { is_expected.to have_one(:slack_slash_commands_service) } @@ -114,7 +114,8 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to have_many(:lfs_file_locks) } it { is_expected.to have_many(:project_deploy_tokens) } it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) } - it { is_expected.to have_many(:cycle_analytics_stages) } + it { is_expected.to have_many(:cycle_analytics_stages).inverse_of(:project) } + it { is_expected.to have_many(:value_streams).inverse_of(:project) } it { is_expected.to have_many(:external_pull_requests) } it { is_expected.to have_many(:sourced_pipelines) } it { is_expected.to have_many(:source_pipelines) } @@ -131,6 +132,7 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::ProjectDistribution').dependent(:destroy) } it { is_expected.to have_many(:pipeline_artifacts) } it { is_expected.to have_many(:terraform_states).class_name('Terraform::State').inverse_of(:project) } + it { is_expected.to have_many(:timelogs) } # GitLab Pages it { is_expected.to have_many(:pages_domains) } @@ -215,7 +217,7 @@ RSpec.describe Project, factory_default: :keep do it 'does not raise an error' do project = create(:project) - expect { project.update(ci_cd_settings: nil) }.not_to raise_exception + expect { project.update!(ci_cd_settings: nil) }.not_to raise_exception end end @@ -873,13 +875,13 @@ RSpec.describe Project, factory_default: :keep do end it 'returns the most recent timestamp' do - project.update(updated_at: nil, + project.update!(updated_at: nil, last_activity_at: timestamp, last_repository_updated_at: timestamp - 1.hour) expect(project.last_activity_date).to be_like_time(timestamp) - project.update(updated_at: timestamp, + project.update!(updated_at: timestamp, last_activity_at: timestamp - 1.hour, last_repository_updated_at: nil) @@ -1076,14 +1078,14 @@ RSpec.describe Project, factory_default: :keep do it 'returns nil and does not query services when there is no external issue tracker' do project = create(:project) - expect(project).not_to receive(:services) + expect(project).not_to receive(:integrations) expect(project.external_issue_tracker).to eq(nil) end it 'retrieves external_issue_tracker querying services and cache it when there is external issue tracker' do project = create(:redmine_project) - expect(project).to receive(:services).once.and_call_original + expect(project).to receive(:integrations).once.and_call_original 2.times { expect(project.external_issue_tracker).to be_a_kind_of(RedmineService) } end end @@ -1116,7 +1118,7 @@ RSpec.describe Project, factory_default: :keep do it 'becomes false when external issue tracker service is destroyed' do expect do - Service.find(service.id).delete + Integration.find(service.id).delete end.to change { subject }.to(false) end @@ -1133,7 +1135,7 @@ RSpec.describe Project, factory_default: :keep do it 'does not become false when external issue tracker service is destroyed' do expect do - Service.find(service.id).delete + Integration.find(service.id).delete end.not_to change { subject } end @@ -1191,7 +1193,7 @@ RSpec.describe Project, factory_default: :keep do it 'becomes false if the external wiki service is destroyed' do expect do - Service.find(service.id).delete + Integration.find(service.id).delete end.to change { subject }.to(false) end @@ -1277,7 +1279,9 @@ RSpec.describe Project, factory_default: :keep do it 'is false if avatar is html page' do project.update_attribute(:avatar, 'uploads/avatar.html') - expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp']) + project.avatar_type + + expect(project.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true end end @@ -2670,7 +2674,7 @@ RSpec.describe Project, factory_default: :keep do context 'with pending pipeline' do it 'returns empty relation' do - pipeline.update(status: 'pending') + pipeline.update!(status: 'pending') pending_build = create_build(pipeline) expect { project.latest_successful_build_for_ref!(pending_build.name) } @@ -2813,11 +2817,11 @@ RSpec.describe Project, factory_default: :keep do end describe '#remove_import_data' do - let_it_be(:import_data) { ProjectImportData.new(data: { 'test' => 'some data' }) } + let(:import_data) { ProjectImportData.new(data: { 'test' => 'some data' }) } context 'when jira import' do - let_it_be(:project, reload: true) { create(:project, import_type: 'jira', import_data: import_data) } - let_it_be(:jira_import) { create(:jira_import_state, project: project) } + let!(:project) { create(:project, import_type: 'jira', import_data: import_data) } + let!(:jira_import) { create(:jira_import_state, project: project) } it 'does remove import data' do expect(project.mirror?).to be false @@ -2827,8 +2831,7 @@ RSpec.describe Project, factory_default: :keep do end context 'when neither a mirror nor a jira import' do - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project, import_type: 'github', import_data: import_data) } + let!(:project) { create(:project, import_type: 'github', import_data: import_data) } it 'removes import data' do expect(project.mirror?).to be false @@ -2864,7 +2867,7 @@ RSpec.describe Project, factory_default: :keep do end it 'returns false when remote mirror is disabled' do - project.remote_mirrors.first.update(enabled: false) + project.remote_mirrors.first.update!(enabled: false) is_expected.to be_falsy end @@ -2895,7 +2898,7 @@ RSpec.describe Project, factory_default: :keep do end it 'does not sync disabled remote mirrors' do - project.remote_mirrors.first.update(enabled: false) + project.remote_mirrors.first.update!(enabled: false) expect_any_instance_of(RemoteMirror).not_to receive(:sync) @@ -2933,7 +2936,7 @@ RSpec.describe Project, factory_default: :keep do it 'fails stuck remote mirrors' do project = create(:project, :repository, :remote_mirror) - project.remote_mirrors.first.update( + project.remote_mirrors.first.update!( update_status: :started, last_update_started_at: 2.days.ago ) @@ -3191,7 +3194,7 @@ RSpec.describe Project, factory_default: :keep do end it 'returns the root of the fork network when the directs source was deleted' do - forked_project.destroy + forked_project.destroy! expect(second_fork.fork_source).to eq(project) end @@ -3435,7 +3438,7 @@ RSpec.describe Project, factory_default: :keep do let(:environment) { 'foo%bar/test' } it 'matches literally for _' do - ci_variable.update(environment_scope: 'foo%bar/*') + ci_variable.environment_scope = 'foo%bar/*' is_expected.to contain_exactly(ci_variable) end @@ -3676,7 +3679,7 @@ RSpec.describe Project, factory_default: :keep do it "updates the namespace_id when changed" do namespace = create(:namespace) - project.update(namespace: namespace) + project.update!(namespace: namespace) expect(project.statistics.namespace_id).to eq namespace.id end @@ -3969,14 +3972,14 @@ RSpec.describe Project, factory_default: :keep do expect(project).to receive(:visibility_level_allowed_as_fork).and_call_original expect(project).to receive(:visibility_level_allowed_by_group).and_call_original - project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) end it 'does not validate the visibility' do expect(project).not_to receive(:visibility_level_allowed_as_fork).and_call_original expect(project).not_to receive(:visibility_level_allowed_by_group).and_call_original - project.update(updated_at: Time.current) + project.update!(updated_at: Time.current) end end @@ -4060,7 +4063,7 @@ RSpec.describe Project, factory_default: :keep do project_2 = create(:project, :public, :merge_requests_disabled) project_3 = create(:project, :public, :issues_disabled) project_4 = create(:project, :public) - project_4.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, merge_requests_access_level: ProjectFeature::PRIVATE ) + project_4.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE, merge_requests_access_level: ProjectFeature::PRIVATE ) project_ids = described_class.ids_with_issuables_available_for(user).pluck(:id) @@ -4103,7 +4106,7 @@ RSpec.describe Project, factory_default: :keep do let(:project) { create(:project, :public) } it 'returns projects with the project feature access level nil' do - project.project_feature.update(merge_requests_access_level: nil) + project.project_feature.update!(merge_requests_access_level: nil) is_expected.to include(project) end @@ -4391,7 +4394,7 @@ RSpec.describe Project, factory_default: :keep do it 'is run when the project is destroyed' do expect(project).to receive(:legacy_remove_pages).and_call_original - expect { project.destroy }.not_to raise_error + expect { project.destroy! }.not_to raise_error end end @@ -4921,7 +4924,7 @@ RSpec.describe Project, factory_default: :keep do context 'when enabled on group' do it 'has auto devops implicitly enabled' do - project.update(namespace: create(:group, :auto_devops_enabled)) + project.update!(namespace: create(:group, :auto_devops_enabled)) expect(project).to have_auto_devops_implicitly_enabled end @@ -4930,7 +4933,7 @@ RSpec.describe Project, factory_default: :keep do context 'when enabled on parent group' do it 'has auto devops implicitly enabled' do subgroup = create(:group, parent: create(:group, :auto_devops_enabled)) - project.update(namespace: subgroup) + project.update!(namespace: subgroup) expect(project).to have_auto_devops_implicitly_enabled end @@ -5404,7 +5407,7 @@ RSpec.describe Project, factory_default: :keep do before do create_list(:group_badge, 2, group: project_group) - project_group.update(parent: parent_group) + project_group.update!(parent: parent_group) end it 'returns the project and the project nested groups badges' do @@ -5799,16 +5802,16 @@ RSpec.describe Project, factory_default: :keep do end it 'avoids N+1 database queries with more available services' do - allow(Service).to receive(:available_services_names).and_return(%w[pushover]) + allow(Integration).to receive(:available_services_names).and_return(%w[pushover]) control_count = ActiveRecord::QueryRecorder.new { subject.find_or_initialize_services } - allow(Service).to receive(:available_services_names).and_call_original + allow(Integration).to receive(:available_services_names).and_call_original expect { subject.find_or_initialize_services }.not_to exceed_query_limit(control_count) end context 'with disabled services' do before do - allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity]) + allow(Integration).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity]) allow(subject).to receive(:disabled_services).and_return(%w[prometheus]) end @@ -5843,11 +5846,11 @@ RSpec.describe Project, factory_default: :keep do describe '#find_or_initialize_service' do it 'avoids N+1 database queries' do - allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover]) + allow(Integration).to receive(:available_services_names).and_return(%w[prometheus pushover]) control_count = ActiveRecord::QueryRecorder.new { subject.find_or_initialize_service('prometheus') }.count - allow(Service).to receive(:available_services_names).and_call_original + allow(Integration).to receive(:available_services_names).and_call_original expect { subject.find_or_initialize_service('prometheus') }.not_to exceed_query_limit(control_count) end @@ -6301,23 +6304,31 @@ RSpec.describe Project, factory_default: :keep do end describe '#access_request_approvers_to_be_notified' do - it 'returns a maximum of ten, active, non_requested maintainers of the project in recent_sign_in descending order' do - group = create(:group, :public) - project = create(:project, group: group) + let_it_be(:project) { create(:project, group: create(:group, :public)) } + it 'returns a maximum of ten maintainers of the project in recent_sign_in descending order' do users = create_list(:user, 12, :with_sign_ins) active_maintainers = users.map do |user| - create(:project_member, :maintainer, user: user) + create(:project_member, :maintainer, user: user, project: project) end - create(:project_member, :maintainer, :blocked, project: project) - create(:project_member, :developer, project: project) - create(:project_member, :access_request, :maintainer, project: project) - - active_maintainers_in_recent_sign_in_desc_order = project.members_and_requesters.where(id: active_maintainers).order_recent_sign_in.limit(10) + active_maintainers_in_recent_sign_in_desc_order = project.members_and_requesters + .id_in(active_maintainers) + .order_recent_sign_in.limit(10) expect(project.access_request_approvers_to_be_notified).to eq(active_maintainers_in_recent_sign_in_desc_order) end + + it 'returns active, non_invited, non_requested maintainers of the project' do + maintainer = create(:project_member, :maintainer, source: project) + + create(:project_member, :developer, project: project) + create(:project_member, :maintainer, :invited, project: project) + create(:project_member, :maintainer, :access_request, project: project) + create(:project_member, :maintainer, :blocked, project: project) + + expect(project.access_request_approvers_to_be_notified.to_a).to eq([maintainer]) + end end describe '#pages_lookup_path' do @@ -6478,17 +6489,17 @@ RSpec.describe Project, factory_default: :keep do end end - describe 'with services and chat names' do + describe 'with integrations and chat names' do subject { create(:project) } - let(:service) { create(:service, project: subject) } + let(:integration) { create(:service, project: subject) } before do - create_list(:chat_name, 5, service: service) + create_list(:chat_name, 5, integration: integration) end it 'removes chat names on removal' do - expect { subject.destroy }.to change { ChatName.count }.by(-5) + expect { subject.destroy! }.to change { ChatName.count }.by(-5) end end @@ -6823,6 +6834,26 @@ RSpec.describe Project, factory_default: :keep do end end + describe '#parent_loaded?' do + let_it_be(:project) { create(:project) } + + before do + project.namespace = create(:namespace) + + project.reload + end + + it 'is false when the parent is not loaded' do + expect(project.parent_loaded?).to be_falsey + end + + it 'is true when the parent is loaded' do + project.parent + + expect(project.parent_loaded?).to be_truthy + end + end + describe '#bots' do subject { project.bots } @@ -6889,6 +6920,105 @@ RSpec.describe Project, factory_default: :keep do end end end + + describe '#activity_path' do + it 'returns the project activity_path' do + expected_path = "/#{project.namespace.path}/#{project.name}/activity" + + expect(project.activity_path).to eq(expected_path) + end + end + end + + describe '#default_branch_or_main' do + let(:project) { create(:project, :repository) } + + it 'returns default branch' do + expect(project.default_branch_or_main).to eq(project.default_branch) + end + + context 'when default branch is nil' do + let(:project) { create(:project, :empty_repo) } + + it 'returns Gitlab::DefaultBranch.value' do + expect(project.default_branch_or_main).to eq(Gitlab::DefaultBranch.value) + end + end + end + + describe '#increment_statistic_value' do + let(:project) { build_stubbed(:project) } + + subject(:increment) do + project.increment_statistic_value(:build_artifacts_size, -10) + end + + it 'increments the value' do + expect(ProjectStatistics) + .to receive(:increment_statistic) + .with(project, :build_artifacts_size, -10) + + increment + end + + context 'when the project is scheduled for removal' do + let(:project) { build_stubbed(:project, pending_delete: true) } + + it 'does not increment the value' do + expect(ProjectStatistics).not_to receive(:increment_statistic) + + increment + end + end + end + + describe 'topics' do + let_it_be(:project) { create(:project, tag_list: 'topic1, topic2, topic3') } + + it 'topic_list returns correct string array' do + expect(project.topic_list).to match_array(%w[topic1 topic2 topic3]) + end + + it 'topics returns correct tag records' do + expect(project.topics.first.class.name).to eq('ActsAsTaggableOn::Tag') + expect(project.topics.map(&:name)).to match_array(%w[topic1 topic2 topic3]) + end + + context 'aliases' do + it 'tag_list returns correct string array' do + expect(project.tag_list).to match_array(%w[topic1 topic2 topic3]) + end + + it 'tags returns correct tag records' do + expect(project.tags.first.class.name).to eq('ActsAsTaggableOn::Tag') + expect(project.tags.map(&:name)).to match_array(%w[topic1 topic2 topic3]) + end + end + + context 'intermediate state during background migration' do + before do + project.taggings.first.update!(context: 'tags') + project.instance_variable_set("@tag_list", nil) + project.reload + end + + it 'tag_list returns string array including old and new topics' do + expect(project.tag_list).to match_array(%w[topic1 topic2 topic3]) + end + + it 'tags returns old and new tag records' do + expect(project.tags.first.class.name).to eq('ActsAsTaggableOn::Tag') + expect(project.tags.map(&:name)).to match_array(%w[topic1 topic2 topic3]) + expect(project.taggings.map(&:context)).to match_array(%w[tags topics topics]) + end + + it 'update tag_list adds new topics and removes old topics' do + project.update!(tag_list: 'topic1, topic2, topic3, topic4') + + expect(project.tags.map(&:name)).to match_array(%w[topic1 topic2 topic3 topic4]) + expect(project.taggings.map(&:context)).to match_array(%w[topics topics topics topics]) + end + end end def finish_job(export_job) diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index bbc056889d6..ce75e68de32 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -294,7 +294,7 @@ RSpec.describe ProjectTeam do context 'when project is shared with group' do before do group = create(:group) - project.project_group_links.create( + project.project_group_links.create!( group: group, group_access: Gitlab::Access::DEVELOPER) @@ -309,7 +309,7 @@ RSpec.describe ProjectTeam do context 'but share_with_group_lock is true' do before do - project.namespace.update(share_with_group_lock: true) + project.namespace.update!(share_with_group_lock: true) end it { expect(project.team.max_member_access(maintainer.id)).to eq(Gitlab::Access::NO_ACCESS) } @@ -496,7 +496,7 @@ RSpec.describe ProjectTeam do project.add_guest(promoted_guest) project.add_guest(guest) - project.project_group_links.create( + project.project_group_links.create!( group: group, group_access: Gitlab::Access::DEVELOPER ) @@ -505,7 +505,7 @@ RSpec.describe ProjectTeam do group.add_developer(group_developer) group.add_developer(second_developer) - project.project_group_links.create( + project.project_group_links.create!( group: second_group, group_access: Gitlab::Access::MAINTAINER ) diff --git a/spec/models/release_highlight_spec.rb b/spec/models/release_highlight_spec.rb index 673451b5e76..b4dff4c33ff 100644 --- a/spec/models/release_highlight_spec.rb +++ b/spec/models/release_highlight_spec.rb @@ -7,6 +7,7 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do before do allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob) + Gitlab::CurrentSettings.update!(whats_new_variant: ApplicationSetting.whats_new_variants[:all_tiers]) end after do @@ -24,16 +25,16 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do subject { ReleaseHighlight.paginated(page: page) } context 'when there is another page of results' do - let(:page) { 2 } + let(:page) { 3 } it 'responds with paginated results' do expect(subject[:items].first['title']).to eq('bright') - expect(subject[:next_page]).to eq(3) + expect(subject[:next_page]).to eq(4) end end context 'when there is NOT another page of results' do - let(:page) { 3 } + let(:page) { 4 } it 'responds with paginated results and no next_page' do expect(subject[:items].first['title']).to eq("It's gonna be a bright") @@ -54,8 +55,8 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do subject { ReleaseHighlight.paginated } it 'uses multiple levels of cache' do - expect(Rails.cache).to receive(:fetch).with("release_highlight:items:page-1:#{Gitlab.revision}", { expires_in: described_class::CACHE_DURATION }).and_call_original - expect(Rails.cache).to receive(:fetch).with("release_highlight:file_paths:#{Gitlab.revision}", { expires_in: described_class::CACHE_DURATION }).and_call_original + expect(Rails.cache).to receive(:fetch).with("release_highlight:all_tiers:items:page-1:#{Gitlab.revision}", { expires_in: described_class::CACHE_DURATION }).and_call_original + expect(Rails.cache).to receive(:fetch).with("release_highlight:all_tiers:file_paths:#{Gitlab.revision}", { expires_in: described_class::CACHE_DURATION }).and_call_original subject end @@ -101,7 +102,7 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do subject { ReleaseHighlight.most_recent_item_count } it 'uses process memory cache' do - expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:recent_item_count:#{Gitlab.revision}", expires_in: described_class::CACHE_DURATION) + expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:all_tiers:recent_item_count:#{Gitlab.revision}", expires_in: described_class::CACHE_DURATION) subject end @@ -127,7 +128,7 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do subject { ReleaseHighlight.most_recent_version_digest } it 'uses process memory cache' do - expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:most_recent_version_digest:#{Gitlab.revision}", expires_in: described_class::CACHE_DURATION) + expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:all_tiers:most_recent_version_digest:#{Gitlab.revision}", expires_in: described_class::CACHE_DURATION) subject end @@ -148,6 +149,33 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do end end + describe '.load_items' do + context 'whats new for all tiers' do + before do + Gitlab::CurrentSettings.update!(whats_new_variant: ApplicationSetting.whats_new_variants[:all_tiers]) + end + + it 'returns all items' do + items = described_class.load_items(page: 2) + + expect(items.count).to eq(3) + end + end + + context 'whats new for current tier only' do + before do + Gitlab::CurrentSettings.update!(whats_new_variant: ApplicationSetting.whats_new_variants[:current_tier]) + end + + it 'returns items with package=Free' do + items = described_class.load_items(page: 2) + + expect(items.count).to eq(1) + expect(items.first['title']).to eq("View epics on a board") + end + end + end + describe 'QueryResult' do subject { ReleaseHighlight::QueryResult.new(items: items, next_page: 2) } @@ -157,4 +185,12 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do expect(subject.map(&:to_s)).to eq(items.map(&:to_s)) end end + + describe '.current_package' do + subject { described_class.current_package } + + it 'returns Free' do + expect(subject).to eq('Free') + end + end end diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index 540a8068b20..b88813b3328 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Release do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public, :repository) } + let(:release) { create(:release, project: project, author: user) } it { expect(release).to be_valid } @@ -37,6 +38,18 @@ RSpec.describe Release do end end + context 'when description of a release is longer than the limit' do + let(:description) { 'a' * (Gitlab::Database::MAX_TEXT_SIZE_LIMIT + 1) } + let(:release) { build(:release, project: project, description: description) } + + it 'creates a validation error' do + release.validate + + expect(release.errors.full_messages) + .to include("Description is too long (maximum is #{Gitlab::Database::MAX_TEXT_SIZE_LIMIT} characters)") + end + end + context 'when a release is tied to a milestone for another project' do it 'creates a validation error' do milestone = build(:milestone, project: create(:project)) @@ -53,7 +66,7 @@ RSpec.describe Release do end describe '#assets_count' do - subject { release.assets_count } + subject { Release.find(release.id).assets_count } it 'returns the number of sources' do is_expected.to eq(Gitlab::Workhorse::ARCHIVE_FORMATS.count) @@ -67,7 +80,7 @@ RSpec.describe Release do end it "excludes sources count when asked" do - assets_count = release.assets_count(except: [:sources]) + assets_count = Release.find(release.id).assets_count(except: [:sources]) expect(assets_count).to eq(1) end end diff --git a/spec/models/releases/evidence_spec.rb b/spec/models/releases/evidence_spec.rb index ca5d4b67b59..59133b2fa51 100644 --- a/spec/models/releases/evidence_spec.rb +++ b/spec/models/releases/evidence_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Releases::Evidence do let_it_be(:project) { create(:project) } + let(:release) { create(:release, project: project) } describe 'associations' do diff --git a/spec/models/releases/source_spec.rb b/spec/models/releases/source_spec.rb index d10b2140550..227085951c0 100644 --- a/spec/models/releases/source_spec.rb +++ b/spec/models/releases/source_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Releases::Source do let_it_be(:project) { create(:project, :repository, name: 'finance-cal') } + let(:tag_name) { 'v1.0' } describe '.all' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index a739f523008..7748846f6a5 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1006,19 +1006,58 @@ RSpec.describe Repository do end end - context 'when specifying a path with wildcard' do - let(:path) { 'files/*/*.png' } + context 'when specifying a wildcard path' do + let(:path) { '*.md' } + + it 'returns files matching the path in the root folder' do + expect(result).to contain_exactly('CONTRIBUTING.md', + 'MAINTENANCE.md', + 'PROCESS.md', + 'README.md') + end + end + + context 'when specifying a wildcard path for all' do + let(:path) { '**.md' } + + it 'returns all matching files in all folders' do + expect(result).to contain_exactly('CONTRIBUTING.md', + 'MAINTENANCE.md', + 'PROCESS.md', + 'README.md', + 'files/markdown/ruby-style-guide.md', + 'with space/README.md') + end + end + + context 'when specifying a path to subfolders using two asterisks and a slash' do + let(:path) { 'files/**/*.md' } it 'returns all files matching the path' do - expect(result).to contain_exactly('files/images/logo-black.png', - 'files/images/logo-white.png') + expect(result).to contain_exactly('files/markdown/ruby-style-guide.md') + end + end + + context 'when specifying a wildcard path to subfolder with just two asterisks' do + let(:path) { 'files/**.md' } + + it 'returns all files in the matching path' do + expect(result).to contain_exactly('files/markdown/ruby-style-guide.md') + end + end + + context 'when specifying a wildcard path to subfolder with one asterisk' do + let(:path) { 'files/*/*.md' } + + it 'returns all files in the matching path' do + expect(result).to contain_exactly('files/markdown/ruby-style-guide.md') end end - context 'when specifying an extension with wildcard' do - let(:path) { '*.rb' } + context 'when specifying a wildcard path for an unknown number of subfolder levels' do + let(:path) { '**/*.rb' } - it 'returns all files matching the extension' do + it 'returns all matched files in all subfolders' do expect(result).to contain_exactly('encoding/russian.rb', 'files/ruby/popen.rb', 'files/ruby/regex.rb', @@ -1026,6 +1065,14 @@ RSpec.describe Repository do end end + context 'when specifying a wildcard path to one level of subfolders' do + let(:path) { '*/*.rb' } + + it 'returns all matched files in one subfolder' do + expect(result).to contain_exactly('encoding/russian.rb') + end + end + context 'when sending regexp' do let(:path) { '.*\.rb' } diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb deleted file mode 100644 index d8eb4ebc432..00000000000 --- a/spec/models/service_spec.rb +++ /dev/null @@ -1,887 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Service do - using RSpec::Parameterized::TableSyntax - - let_it_be(:group) { create(:group) } - let_it_be(:project) { create(:project, group: group) } - - describe "Associations" do - it { is_expected.to belong_to :project } - it { is_expected.to belong_to :group } - it { is_expected.to have_one :service_hook } - it { is_expected.to have_one :jira_tracker_data } - it { is_expected.to have_one :issue_tracker_data } - end - - describe 'validations' do - it { is_expected.to validate_presence_of(:type) } - - where(:project_id, :group_id, :template, :instance, :valid) do - 1 | nil | false | false | true - nil | 1 | false | false | true - nil | nil | true | false | true - nil | nil | false | true | true - nil | nil | false | false | false - nil | nil | true | true | false - 1 | 1 | false | false | false - 1 | nil | true | false | false - 1 | nil | false | true | false - nil | 1 | true | false | false - nil | 1 | false | true | false - end - - with_them do - it 'validates the service' do - expect(build(:service, project_id: project_id, group_id: group_id, template: template, instance: instance).valid?).to eq(valid) - end - end - - context 'with existing services' do - before_all do - create(:service, :template) - create(:service, :instance) - create(:service, project: project) - create(:service, group: group, project: nil) - end - - it 'allows only one service template per type' do - expect(build(:service, :template)).to be_invalid - end - - it 'allows only one instance service per type' do - expect(build(:service, :instance)).to be_invalid - end - - it 'allows only one project service per type' do - expect(build(:service, project: project)).to be_invalid - end - - it 'allows only one group service per type' do - expect(build(:service, group: group, project: nil)).to be_invalid - end - end - end - - describe 'Scopes' do - describe '.by_type' do - let!(:service1) { create(:jira_service) } - let!(:service2) { create(:jira_service) } - let!(:service3) { create(:redmine_service) } - - subject { described_class.by_type(type) } - - context 'when type is "JiraService"' do - let(:type) { 'JiraService' } - - it { is_expected.to match_array([service1, service2]) } - end - - context 'when type is "RedmineService"' do - let(:type) { 'RedmineService' } - - it { is_expected.to match_array([service3]) } - end - end - - describe '.for_group' do - let!(:service1) { create(:jira_service, project_id: nil, group_id: group.id) } - let!(:service2) { create(:jira_service) } - - it 'returns the right group service' do - expect(described_class.for_group(group)).to match_array([service1]) - end - end - - describe '.confidential_note_hooks' do - it 'includes services where confidential_note_events is true' do - create(:service, active: true, confidential_note_events: true) - - expect(described_class.confidential_note_hooks.count).to eq 1 - end - - it 'excludes services where confidential_note_events is false' do - create(:service, active: true, confidential_note_events: false) - - expect(described_class.confidential_note_hooks.count).to eq 0 - end - end - - describe '.alert_hooks' do - it 'includes services where alert_events is true' do - create(:service, active: true, alert_events: true) - - expect(described_class.alert_hooks.count).to eq 1 - end - - it 'excludes services where alert_events is false' do - create(:service, active: true, alert_events: false) - - expect(described_class.alert_hooks.count).to eq 0 - end - end - end - - describe '#operating?' do - it 'is false when the service is not active' do - expect(build(:service).operating?).to eq(false) - end - - it 'is false when the service is not persisted' do - expect(build(:service, active: true).operating?).to eq(false) - end - - it 'is true when the service is active and persisted' do - expect(create(:service, active: true).operating?).to eq(true) - end - end - - describe "Test Button" do - let(:service) { build(:service, project: project) } - - describe '#can_test?' do - subject { service.can_test? } - - context 'when repository is not empty' do - let(:project) { build(:project, :repository) } - - it { is_expected.to be true } - end - - context 'when repository is empty' do - let(:project) { build(:project) } - - it { is_expected.to be true } - end - - context 'when instance-level service' do - Service.available_services_types.each do |service_type| - let(:service) do - service_type.constantize.new(instance: true) - end - - it { is_expected.to be_falsey } - end - end - - context 'when group-level service' do - Service.available_services_types.each do |service_type| - let(:service) do - service_type.constantize.new(group_id: group.id) - end - - it { is_expected.to be_falsey } - end - end - end - - describe '#test' do - let(:data) { 'test' } - - context 'when repository is not empty' do - let(:project) { build(:project, :repository) } - - it 'test runs execute' do - expect(service).to receive(:execute).with(data) - - service.test(data) - end - end - - context 'when repository is empty' do - let(:project) { build(:project) } - - it 'test runs execute' do - expect(service).to receive(:execute).with(data) - - service.test(data) - end - end - end - end - - describe '#project_level?' do - it 'is true when service has a project' do - expect(build(:service, project: project)).to be_project_level - end - - it 'is false when service has no project' do - expect(build(:service, project: nil)).not_to be_project_level - end - end - - describe '.find_or_initialize_non_project_specific_integration' do - let!(:service1) { create(:jira_service, project_id: nil, group_id: group.id) } - let!(:service2) { create(:jira_service) } - - it 'returns the right service' do - expect(Service.find_or_initialize_non_project_specific_integration('jira', group_id: group)).to eq(service1) - end - - it 'does not create a new service' do - expect { Service.find_or_initialize_non_project_specific_integration('redmine', group_id: group) }.not_to change { Service.count } - end - end - - describe '.find_or_initialize_all_non_project_specific' do - shared_examples 'service instances' do - it 'returns the available service instances' do - expect(Service.find_or_initialize_all_non_project_specific(Service.for_instance).pluck(:type)).to match_array(Service.available_services_types(include_project_specific: false)) - end - - it 'does not create service instances' do - expect { Service.find_or_initialize_all_non_project_specific(Service.for_instance) }.not_to change { Service.count } - end - end - - it_behaves_like 'service instances' - - context 'with all existing instances' do - before do - Service.insert_all( - Service.available_services_types(include_project_specific: false).map { |type| { instance: true, type: type } } - ) - end - - it_behaves_like 'service instances' - - context 'with a previous existing service (MockCiService) and a new service (Asana)' do - before do - Service.insert({ type: 'MockCiService', instance: true }) - Service.delete_by(type: 'AsanaService', instance: true) - end - - it_behaves_like 'service instances' - end - end - - context 'with a few existing instances' do - before do - create(:jira_service, :instance) - end - - it_behaves_like 'service instances' - end - end - - describe 'template' do - shared_examples 'retrieves service templates' do - it 'returns the available service templates' do - expect(Service.find_or_create_templates.pluck(:type)).to match_array(Service.available_services_types(include_project_specific: false)) - end - end - - describe '.find_or_create_templates' do - it 'creates service templates' do - expect { Service.find_or_create_templates }.to change { Service.count }.from(0).to(Service.available_services_names(include_project_specific: false).size) - end - - it_behaves_like 'retrieves service templates' - - context 'with all existing templates' do - before do - Service.insert_all( - Service.available_services_types(include_project_specific: false).map { |type| { template: true, type: type } } - ) - end - - it 'does not create service templates' do - expect { Service.find_or_create_templates }.not_to change { Service.count } - end - - it_behaves_like 'retrieves service templates' - - context 'with a previous existing service (Previous) and a new service (Asana)' do - before do - Service.insert({ type: 'PreviousService', template: true }) - Service.delete_by(type: 'AsanaService', template: true) - end - - it_behaves_like 'retrieves service templates' - end - end - - context 'with a few existing templates' do - before do - create(:jira_service, :template) - end - - it 'creates the rest of the service templates' do - expect { Service.find_or_create_templates }.to change { Service.count }.from(1).to(Service.available_services_names(include_project_specific: false).size) - end - - it_behaves_like 'retrieves service templates' - end - end - - describe '.build_from_integration' do - context 'when integration is invalid' do - let(:integration) do - build(:prometheus_service, :template, active: true, properties: {}) - .tap { |integration| integration.save(validate: false) } - end - - it 'sets service to inactive' do - service = described_class.build_from_integration(integration, project_id: project.id) - - expect(service).to be_valid - expect(service.active).to be false - end - end - - context 'when integration is an instance-level integration' do - let(:integration) { create(:jira_service, :instance) } - - it 'sets inherit_from_id from integration' do - service = described_class.build_from_integration(integration, project_id: project.id) - - expect(service.inherit_from_id).to eq(integration.id) - end - end - - context 'when integration is a group-level integration' do - let(:integration) { create(:jira_service, group: group, project: nil) } - - it 'sets inherit_from_id from integration' do - service = described_class.build_from_integration(integration, project_id: project.id) - - expect(service.inherit_from_id).to eq(integration.id) - end - end - - describe 'build issue tracker from an integration' do - let(:url) { 'http://jira.example.com' } - let(:api_url) { 'http://api-jira.example.com' } - let(:username) { 'jira-username' } - let(:password) { 'jira-password' } - let(:data_params) do - { - url: url, api_url: api_url, - username: username, password: password - } - end - - shared_examples 'service creation from an integration' do - it 'creates a correct service for a project integration' do - service = described_class.build_from_integration(integration, project_id: project.id) - - expect(service).to be_active - expect(service.url).to eq(url) - expect(service.api_url).to eq(api_url) - expect(service.username).to eq(username) - expect(service.password).to eq(password) - expect(service.template).to eq(false) - expect(service.instance).to eq(false) - expect(service.project).to eq(project) - expect(service.group).to eq(nil) - end - - it 'creates a correct service for a group integration' do - service = described_class.build_from_integration(integration, group_id: group.id) - - expect(service).to be_active - expect(service.url).to eq(url) - expect(service.api_url).to eq(api_url) - expect(service.username).to eq(username) - expect(service.password).to eq(password) - expect(service.template).to eq(false) - expect(service.instance).to eq(false) - expect(service.project).to eq(nil) - expect(service.group).to eq(group) - end - end - - # this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404 - context 'when data are stored in properties' do - let(:properties) { data_params } - let!(:integration) do - create(:jira_service, :without_properties_callback, template: true, properties: properties.merge(additional: 'something')) - end - - it_behaves_like 'service creation from an integration' - end - - context 'when data are stored in separated fields' do - let(:integration) do - create(:jira_service, :template, data_params.merge(properties: {})) - end - - it_behaves_like 'service creation from an integration' - end - - context 'when data are stored in both properties and separated fields' do - let(:properties) { data_params } - let(:integration) do - create(:jira_service, :without_properties_callback, active: true, template: true, properties: properties).tap do |service| - create(:jira_tracker_data, data_params.merge(service: service)) - end - end - - it_behaves_like 'service creation from an integration' - end - end - end - - describe "for pushover service" do - let!(:service_template) do - PushoverService.create( - template: true, - properties: { - device: 'MyDevice', - sound: 'mic', - priority: 4, - api_key: '123456789' - }) - end - - describe 'is prefilled for projects pushover service' do - it "has all fields prefilled" do - service = project.find_or_initialize_service('pushover') - - expect(service.template).to eq(false) - expect(service.device).to eq('MyDevice') - expect(service.sound).to eq('mic') - expect(service.priority).to eq(4) - expect(service.api_key).to eq('123456789') - end - end - end - end - - describe '.default_integration' do - context 'with an instance-level service' do - let_it_be(:instance_service) { create(:jira_service, :instance) } - - it 'returns the instance service' do - expect(described_class.default_integration('JiraService', project)).to eq(instance_service) - end - - it 'returns nil for nonexistent service type' do - expect(described_class.default_integration('HipchatService', project)).to eq(nil) - end - - context 'with a group service' do - let_it_be(:group_service) { create(:jira_service, group_id: group.id, project_id: nil) } - - it 'returns the group service for a project' do - expect(described_class.default_integration('JiraService', project)).to eq(group_service) - end - - it 'returns the instance service for a group' do - expect(described_class.default_integration('JiraService', group)).to eq(instance_service) - end - - context 'with a subgroup' do - let_it_be(:subgroup) { create(:group, parent: group) } - let!(:project) { create(:project, group: subgroup) } - - it 'returns the closest group service for a project' do - expect(described_class.default_integration('JiraService', project)).to eq(group_service) - end - - it 'returns the closest group service for a subgroup' do - expect(described_class.default_integration('JiraService', subgroup)).to eq(group_service) - end - - context 'having a service with custom settings' do - let!(:subgroup_service) { create(:jira_service, group_id: subgroup.id, project_id: nil) } - - it 'returns the closest group service for a project' do - expect(described_class.default_integration('JiraService', project)).to eq(subgroup_service) - end - end - - context 'having a service inheriting settings' do - let!(:subgroup_service) { create(:jira_service, group_id: subgroup.id, project_id: nil, inherit_from_id: group_service.id) } - - it 'returns the closest group service which does not inherit from its parent for a project' do - expect(described_class.default_integration('JiraService', project)).to eq(group_service) - end - end - end - end - end - end - - describe '.create_from_active_default_integrations' do - context 'with an active service template' do - let_it_be(:template_integration) { create(:prometheus_service, :template, api_url: 'https://prometheus.template.com/') } - - it 'creates a service from the template' do - described_class.create_from_active_default_integrations(project, :project_id, with_templates: true) - - expect(project.reload.services.size).to eq(1) - expect(project.reload.services.first.api_url).to eq(template_integration.api_url) - expect(project.reload.services.first.inherit_from_id).to be_nil - end - - context 'with an active instance-level integration' do - let!(:instance_integration) { create(:prometheus_service, :instance, api_url: 'https://prometheus.instance.com/') } - - it 'creates a service from the instance-level integration' do - described_class.create_from_active_default_integrations(project, :project_id, with_templates: true) - - expect(project.reload.services.size).to eq(1) - expect(project.reload.services.first.api_url).to eq(instance_integration.api_url) - expect(project.reload.services.first.inherit_from_id).to eq(instance_integration.id) - end - - context 'passing a group' do - it 'creates a service from the instance-level integration' do - described_class.create_from_active_default_integrations(group, :group_id) - - expect(group.reload.services.size).to eq(1) - expect(group.reload.services.first.api_url).to eq(instance_integration.api_url) - expect(group.reload.services.first.inherit_from_id).to eq(instance_integration.id) - end - end - - context 'with an active group-level integration' do - let!(:group_integration) { create(:prometheus_service, group: group, project: nil, api_url: 'https://prometheus.group.com/') } - - it 'creates a service from the group-level integration' do - described_class.create_from_active_default_integrations(project, :project_id, with_templates: true) - - expect(project.reload.services.size).to eq(1) - expect(project.reload.services.first.api_url).to eq(group_integration.api_url) - expect(project.reload.services.first.inherit_from_id).to eq(group_integration.id) - end - - context 'passing a group' do - let!(:subgroup) { create(:group, parent: group) } - - it 'creates a service from the group-level integration' do - described_class.create_from_active_default_integrations(subgroup, :group_id) - - expect(subgroup.reload.services.size).to eq(1) - expect(subgroup.reload.services.first.api_url).to eq(group_integration.api_url) - expect(subgroup.reload.services.first.inherit_from_id).to eq(group_integration.id) - end - end - - context 'with an active subgroup' do - let!(:subgroup_integration) { create(:prometheus_service, group: subgroup, project: nil, api_url: 'https://prometheus.subgroup.com/') } - let!(:subgroup) { create(:group, parent: group) } - let(:project) { create(:project, group: subgroup) } - - it 'creates a service from the subgroup-level integration' do - described_class.create_from_active_default_integrations(project, :project_id, with_templates: true) - - expect(project.reload.services.size).to eq(1) - expect(project.reload.services.first.api_url).to eq(subgroup_integration.api_url) - expect(project.reload.services.first.inherit_from_id).to eq(subgroup_integration.id) - end - - context 'passing a group' do - let!(:sub_subgroup) { create(:group, parent: subgroup) } - - it 'creates a service from the subgroup-level integration' do - described_class.create_from_active_default_integrations(sub_subgroup, :group_id) - - expect(sub_subgroup.reload.services.size).to eq(1) - expect(sub_subgroup.reload.services.first.api_url).to eq(subgroup_integration.api_url) - expect(sub_subgroup.reload.services.first.inherit_from_id).to eq(subgroup_integration.id) - end - - context 'having a service inheriting settings' do - let!(:subgroup_integration) { create(:prometheus_service, group: subgroup, project: nil, inherit_from_id: group_integration.id, api_url: 'https://prometheus.subgroup.com/') } - - it 'creates a service from the group-level integration' do - described_class.create_from_active_default_integrations(sub_subgroup, :group_id) - - expect(sub_subgroup.reload.services.size).to eq(1) - expect(sub_subgroup.reload.services.first.api_url).to eq(group_integration.api_url) - expect(sub_subgroup.reload.services.first.inherit_from_id).to eq(group_integration.id) - end - end - end - end - end - end - end - end - - describe '.inherited_descendants_from_self_or_ancestors_from' do - let_it_be(:subgroup1) { create(:group, parent: group) } - let_it_be(:subgroup2) { create(:group, parent: group) } - let_it_be(:project1) { create(:project, group: subgroup1) } - let_it_be(:project2) { create(:project, group: subgroup2) } - let_it_be(:group_integration) { create(:prometheus_service, group: group, project: nil) } - let_it_be(:subgroup_integration1) { create(:prometheus_service, group: subgroup1, project: nil, inherit_from_id: group_integration.id) } - let_it_be(:subgroup_integration2) { create(:prometheus_service, group: subgroup2, project: nil) } - let_it_be(:project_integration1) { create(:prometheus_service, group: nil, project: project1, inherit_from_id: group_integration.id) } - let_it_be(:project_integration2) { create(:prometheus_service, group: nil, project: project2, inherit_from_id: subgroup_integration2.id) } - - it 'returns the groups and projects inheriting from integration ancestors', :aggregate_failures do - expect(described_class.inherited_descendants_from_self_or_ancestors_from(group_integration)).to eq([subgroup_integration1, project_integration1]) - expect(described_class.inherited_descendants_from_self_or_ancestors_from(subgroup_integration2)).to eq([project_integration2]) - end - end - - describe "{property}_changed?" do - let(:service) do - BambooService.create( - project: project, - properties: { - bamboo_url: 'http://gitlab.com', - username: 'mic', - password: "password" - } - ) - end - - it "returns false when the property has not been assigned a new value" do - service.username = "key_changed" - expect(service.bamboo_url_changed?).to be_falsy - end - - it "returns true when the property has been assigned a different value" do - service.bamboo_url = "http://example.com" - expect(service.bamboo_url_changed?).to be_truthy - end - - it "returns true when the property has been assigned a different value twice" do - service.bamboo_url = "http://example.com" - service.bamboo_url = "http://example.com" - expect(service.bamboo_url_changed?).to be_truthy - end - - it "returns false when the property has been re-assigned the same value" do - service.bamboo_url = 'http://gitlab.com' - expect(service.bamboo_url_changed?).to be_falsy - end - - it "returns false when the property has been assigned a new value then saved" do - service.bamboo_url = 'http://example.com' - service.save - expect(service.bamboo_url_changed?).to be_falsy - end - end - - describe "{property}_touched?" do - let(:service) do - BambooService.create( - project: project, - properties: { - bamboo_url: 'http://gitlab.com', - username: 'mic', - password: "password" - } - ) - end - - it "returns false when the property has not been assigned a new value" do - service.username = "key_changed" - expect(service.bamboo_url_touched?).to be_falsy - end - - it "returns true when the property has been assigned a different value" do - service.bamboo_url = "http://example.com" - expect(service.bamboo_url_touched?).to be_truthy - end - - it "returns true when the property has been assigned a different value twice" do - service.bamboo_url = "http://example.com" - service.bamboo_url = "http://example.com" - expect(service.bamboo_url_touched?).to be_truthy - end - - it "returns true when the property has been re-assigned the same value" do - service.bamboo_url = 'http://gitlab.com' - expect(service.bamboo_url_touched?).to be_truthy - end - - it "returns false when the property has been assigned a new value then saved" do - service.bamboo_url = 'http://example.com' - service.save - expect(service.bamboo_url_changed?).to be_falsy - end - end - - describe "{property}_was" do - let(:service) do - BambooService.create( - project: project, - properties: { - bamboo_url: 'http://gitlab.com', - username: 'mic', - password: "password" - } - ) - end - - it "returns nil when the property has not been assigned a new value" do - service.username = "key_changed" - expect(service.bamboo_url_was).to be_nil - end - - it "returns the previous value when the property has been assigned a different value" do - service.bamboo_url = "http://example.com" - expect(service.bamboo_url_was).to eq('http://gitlab.com') - end - - it "returns initial value when the property has been re-assigned the same value" do - service.bamboo_url = 'http://gitlab.com' - expect(service.bamboo_url_was).to eq('http://gitlab.com') - end - - it "returns initial value when the property has been assigned multiple values" do - service.bamboo_url = "http://example.com" - service.bamboo_url = "http://example2.com" - expect(service.bamboo_url_was).to eq('http://gitlab.com') - end - - it "returns nil when the property has been assigned a new value then saved" do - service.bamboo_url = 'http://example.com' - service.save - expect(service.bamboo_url_was).to be_nil - end - end - - describe 'initialize service with no properties' do - let(:service) do - BugzillaService.create( - project: project, - project_url: 'http://gitlab.example.com' - ) - end - - it 'does not raise error' do - expect { service }.not_to raise_error - end - - it 'sets data correctly' do - expect(service.data_fields.project_url).to eq('http://gitlab.example.com') - end - end - - describe '#api_field_names' do - let(:fake_service) do - Class.new(Service) do - def fields - [ - { name: 'token' }, - { name: 'api_token' }, - { name: 'key' }, - { name: 'api_key' }, - { name: 'password' }, - { name: 'password_field' }, - { name: 'safe_field' } - ] - end - end - end - - let(:service) do - fake_service.new(properties: [ - { token: 'token-value' }, - { api_token: 'api_token-value' }, - { key: 'key-value' }, - { api_key: 'api_key-value' }, - { password: 'password-value' }, - { password_field: 'password_field-value' }, - { safe_field: 'safe_field-value' } - ]) - end - - it 'filters out sensitive fields' do - expect(service.api_field_names).to eq(['safe_field']) - end - end - - context 'logging' do - let(:service) { build(:service, project: project) } - let(:test_message) { "test message" } - let(:arguments) do - { - service_class: service.class.name, - project_path: project.full_path, - project_id: project.id, - message: test_message, - additional_argument: 'some argument' - } - end - - it 'logs info messages using json logger' do - expect(Gitlab::JsonLogger).to receive(:info).with(arguments) - - service.log_info(test_message, additional_argument: 'some argument') - end - - it 'logs error messages using json logger' do - expect(Gitlab::JsonLogger).to receive(:error).with(arguments) - - service.log_error(test_message, additional_argument: 'some argument') - end - - context 'when project is nil' do - let(:project) { nil } - let(:arguments) do - { - service_class: service.class.name, - project_path: nil, - project_id: nil, - message: test_message, - additional_argument: 'some argument' - } - end - - it 'logs info messages using json logger' do - expect(Gitlab::JsonLogger).to receive(:info).with(arguments) - - service.log_info(test_message, additional_argument: 'some argument') - end - end - end - - describe '#external_wiki?' do - where(:type, :active, :result) do - 'ExternalWikiService' | true | true - 'ExternalWikiService' | false | false - 'SlackService' | true | false - end - - with_them do - it 'returns the right result' do - expect(build(:service, type: type, active: active).external_wiki?).to eq(result) - end - end - end - - describe '.available_services_names' do - it 'calls the right methods' do - expect(described_class).to receive(:services_names).and_call_original - expect(described_class).to receive(:dev_services_names).and_call_original - expect(described_class).to receive(:project_specific_services_names).and_call_original - - described_class.available_services_names - end - - it 'does not call project_specific_services_names with include_project_specific false' do - expect(described_class).to receive(:services_names).and_call_original - expect(described_class).to receive(:dev_services_names).and_call_original - expect(described_class).not_to receive(:project_specific_services_names) - - described_class.available_services_names(include_project_specific: false) - end - - it 'does not call dev_services_names with include_dev false' do - expect(described_class).to receive(:services_names).and_call_original - expect(described_class).not_to receive(:dev_services_names) - expect(described_class).to receive(:project_specific_services_names).and_call_original - - described_class.available_services_names(include_dev: false) - end - - it { expect(described_class.available_services_names).to include('jenkins') } - end - - describe '.project_specific_services_names' do - it do - expect(described_class.project_specific_services_names) - .to include(*described_class::PROJECT_SPECIFIC_SERVICE_NAMES) - end - end -end diff --git a/spec/models/sidebars/menu_spec.rb b/spec/models/sidebars/menu_spec.rb deleted file mode 100644 index 320f5f1ad1e..00000000000 --- a/spec/models/sidebars/menu_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::Menu do - let(:menu) { described_class.new(context) } - let(:context) { Sidebars::Context.new(current_user: nil, container: nil) } - - describe '#all_active_routes' do - it 'gathers all active routes of items and the current menu' do - menu_item1 = Sidebars::MenuItem.new(context) - menu_item2 = Sidebars::MenuItem.new(context) - menu_item3 = Sidebars::MenuItem.new(context) - menu.add_item(menu_item1) - menu.add_item(menu_item2) - menu.add_item(menu_item3) - - allow(menu).to receive(:active_routes).and_return({ path: 'foo' }) - allow(menu_item1).to receive(:active_routes).and_return({ path: %w(bar test) }) - allow(menu_item2).to receive(:active_routes).and_return({ controller: 'fooc' }) - allow(menu_item3).to receive(:active_routes).and_return({ controller: 'barc' }) - - expect(menu.all_active_routes).to eq({ path: %w(foo bar test), controller: %w(fooc barc) }) - end - - it 'does not include routes for non renderable items' do - menu_item = Sidebars::MenuItem.new(context) - menu.add_item(menu_item) - - allow(menu).to receive(:active_routes).and_return({ path: 'foo' }) - allow(menu_item).to receive(:render?).and_return(false) - allow(menu_item).to receive(:active_routes).and_return({ controller: 'bar' }) - - expect(menu.all_active_routes).to eq({ path: ['foo'] }) - end - end - - describe '#render?' do - context 'when the menus has no items' do - it 'returns true' do - expect(menu.render?).to be true - end - end - - context 'when the menu has items' do - let(:menu_item) { Sidebars::MenuItem.new(context) } - - before do - menu.add_item(menu_item) - end - - context 'when items are not renderable' do - it 'returns false' do - allow(menu_item).to receive(:render?).and_return(false) - - expect(menu.render?).to be false - end - end - - context 'when there are renderable items' do - it 'returns true' do - expect(menu.render?).to be true - end - end - end - end -end diff --git a/spec/models/sidebars/panel_spec.rb b/spec/models/sidebars/panel_spec.rb deleted file mode 100644 index 0e539460810..00000000000 --- a/spec/models/sidebars/panel_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::Panel do - let(:context) { Sidebars::Context.new(current_user: nil, container: nil) } - let(:panel) { Sidebars::Panel.new(context) } - let(:menu1) { Sidebars::Menu.new(context) } - let(:menu2) { Sidebars::Menu.new(context) } - - describe '#renderable_menus' do - it 'returns only renderable menus' do - panel.add_menu(menu1) - panel.add_menu(menu2) - - allow(menu1).to receive(:render?).and_return(true) - allow(menu2).to receive(:render?).and_return(false) - - expect(panel.renderable_menus).to eq([menu1]) - end - end - - describe '#has_renderable_menus?' do - it 'returns false when no renderable menus' do - expect(panel.has_renderable_menus?).to be false - end - - it 'returns true when no renderable menus' do - panel.add_menu(menu1) - - expect(panel.has_renderable_menus?).to be true - end - end -end diff --git a/spec/models/sidebars/projects/context_spec.rb b/spec/models/sidebars/projects/context_spec.rb deleted file mode 100644 index 44578ae1583..00000000000 --- a/spec/models/sidebars/projects/context_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::Projects::Context do - let(:project) { build(:project) } - - subject { described_class.new(current_user: nil, container: project) } - - it 'sets project attribute reader' do - expect(subject.project).to eq(project) - end -end diff --git a/spec/models/sidebars/projects/menus/learn_gitlab/menu_spec.rb b/spec/models/sidebars/projects/menus/learn_gitlab/menu_spec.rb deleted file mode 100644 index bc1815558d3..00000000000 --- a/spec/models/sidebars/projects/menus/learn_gitlab/menu_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::Projects::Menus::LearnGitlab::Menu do - let(:project) { build(:project) } - let(:experiment_enabled) { true } - let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project, learn_gitlab_experiment_enabled: experiment_enabled) } - - subject { described_class.new(context) } - - it 'does not contain any sub menu' do - expect(subject.instance_variable_get(:@items)).to be_empty - end - - describe '#render?' do - context 'when learn gitlab experiment is enabled' do - it 'returns true' do - expect(subject.render?).to eq true - end - end - - context 'when learn gitlab experiment is disabled' do - let(:experiment_enabled) { false } - - it 'returns false' do - expect(subject.render?).to eq false - end - end - end -end diff --git a/spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb b/spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb deleted file mode 100644 index db124c2252e..00000000000 --- a/spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::Projects::Menus::ProjectOverview::MenuItems::Releases do - let_it_be(:project) { create(:project, :repository) } - - let(:user) { project.owner } - let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } - - subject { described_class.new(context) } - - describe '#render?' do - context 'when project repository is empty' do - it 'returns false' do - allow(project).to receive(:empty_repo?).and_return(true) - - expect(subject.render?).to eq false - end - end - - context 'when project repository is not empty' do - context 'when user can read releases' do - it 'returns true' do - expect(subject.render?).to eq true - end - end - - context 'when user cannot read releases' do - let(:user) { nil } - - it 'returns false' do - expect(subject.render?).to eq false - end - end - end - end -end diff --git a/spec/models/sidebars/projects/menus/project_overview/menu_spec.rb b/spec/models/sidebars/projects/menus/project_overview/menu_spec.rb deleted file mode 100644 index 105a28ce953..00000000000 --- a/spec/models/sidebars/projects/menus/project_overview/menu_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::Projects::Menus::ProjectOverview::Menu do - let(:project) { build(:project) } - let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project) } - - subject { described_class.new(context) } - - it 'has the required items' do - items = subject.instance_variable_get(:@items) - - expect(items[0]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Details) - expect(items[1]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Activity) - expect(items[2]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Releases) - end -end diff --git a/spec/models/sidebars/projects/menus/repository/menu_spec.rb b/spec/models/sidebars/projects/menus/repository/menu_spec.rb deleted file mode 100644 index 04eb3357a6f..00000000000 --- a/spec/models/sidebars/projects/menus/repository/menu_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::Projects::Menus::Repository::Menu do - let_it_be(:project) { create(:project, :repository) } - - let(:user) { project.owner } - let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } - - subject { described_class.new(context) } - - describe '#render?' do - context 'when project repository is empty' do - it 'returns false' do - allow(project).to receive(:empty_repo?).and_return(true) - - expect(subject.render?).to eq false - end - end - - context 'when project repository is not empty' do - context 'when user can download code' do - it 'returns true' do - expect(subject.render?).to eq true - end - end - - context 'when user cannot download code' do - let(:user) { nil } - - it 'returns false' do - expect(subject.render?).to eq false - end - end - end - end -end diff --git a/spec/models/sidebars/projects/panel_spec.rb b/spec/models/sidebars/projects/panel_spec.rb deleted file mode 100644 index bad9b17bc83..00000000000 --- a/spec/models/sidebars/projects/panel_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::Projects::Panel do - let(:project) { build(:project) } - let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project) } - - subject { described_class.new(context) } - - it 'has a scope menu' do - expect(subject.scope_menu).to be_a(Sidebars::Projects::Menus::Scope::Menu) - end -end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 09f9cf8e222..41991821922 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -826,18 +826,6 @@ RSpec.describe Snippet do allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return(default_branch) end - context 'when default branch in settings is "master"' do - let(:default_branch) { 'master' } - - it 'does nothing' do - expect(File.read(head_path).squish).to eq 'ref: refs/heads/master' - - expect(snippet.repository.raw_repository).not_to receive(:write_ref) - - subject - end - end - context 'when default branch in settings is different from "master"' do let(:default_branch) { 'main' } diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb index 6a252b444f9..c3432907112 100644 --- a/spec/models/timelog_spec.rb +++ b/spec/models/timelog_spec.rb @@ -3,11 +3,12 @@ require 'spec_helper' RSpec.describe Timelog do - subject { build(:timelog) } + subject { create(:timelog) } - let(:issue) { create(:issue) } - let(:merge_request) { create(:merge_request) } + let_it_be(:issue) { create(:issue) } + let_it_be(:merge_request) { create(:merge_request) } + it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:issue).touch(true) } it { is_expected.to belong_to(:merge_request).touch(true) } @@ -16,6 +17,8 @@ RSpec.describe Timelog do it { is_expected.to validate_presence_of(:time_spent) } it { is_expected.to validate_presence_of(:user) } + it { expect(subject.project_id).not_to be_nil } + describe 'Issuable validation' do it 'is invalid if issue_id and merge_request_id are missing' do subject.attributes = { issue: nil, merge_request: nil } @@ -51,27 +54,34 @@ RSpec.describe Timelog do end describe 'scopes' do - describe 'for_issues_in_group' do - it 'return timelogs created for group issues' do - group = create(:group) - subgroup = create(:group, parent: group) - - create(:issue_timelog) - timelog1 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: group))) - timelog2 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: subgroup))) - - expect(described_class.for_issues_in_group(group)).to contain_exactly(timelog1, timelog2) + let_it_be(:group) { create(:group) } + let_it_be(:group_project) { create(:project, :empty_repo, group: group) } + let_it_be(:group_issue) { create(:issue, project: group_project) } + let_it_be(:group_merge_request) { create(:merge_request, source_project: group_project) } + + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:subgroup_project) { create(:project, :empty_repo, group: subgroup) } + let_it_be(:subgroup_issue) { create(:issue, project: subgroup_project) } + let_it_be(:subgroup_merge_request) { create(:merge_request, source_project: subgroup_project) } + + let_it_be(:timelog) { create(:issue_timelog, spent_at: 65.days.ago) } + let_it_be(:timelog1) { create(:issue_timelog, spent_at: 15.days.ago, issue: group_issue) } + let_it_be(:timelog2) { create(:issue_timelog, spent_at: 5.days.ago, issue: subgroup_issue) } + let_it_be(:timelog3) { create(:merge_request_timelog, spent_at: 65.days.ago) } + let_it_be(:timelog4) { create(:merge_request_timelog, spent_at: 15.days.ago, merge_request: group_merge_request) } + let_it_be(:timelog5) { create(:merge_request_timelog, spent_at: 5.days.ago, merge_request: subgroup_merge_request) } + + describe 'in_group' do + it 'return timelogs created for group issues and merge requests' do + expect(described_class.in_group(group)).to contain_exactly(timelog1, timelog2, timelog4, timelog5) end end describe 'between_times' do it 'returns collection of timelogs within given times' do - create(:issue_timelog, spent_at: 65.days.ago) - timelog1 = create(:issue_timelog, spent_at: 15.days.ago) - timelog2 = create(:issue_timelog, spent_at: 5.days.ago) - timelogs = described_class.between_times(20.days.ago, 1.day.ago) + timelogs = described_class.between_times(20.days.ago, 10.days.ago) - expect(timelogs).to contain_exactly(timelog1, timelog2) + expect(timelogs).to contain_exactly(timelog1, timelog4) end end end diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index c4146b347d7..caa0a886abf 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -452,11 +452,15 @@ RSpec.describe Todo do end end - describe '.pluck_user_id' do - subject { described_class.pluck_user_id } + describe '.distinct_user_ids' do + subject { described_class.distinct_user_ids } - let_it_be(:todo) { create(:todo) } + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:todo) { create(:todo, user: user1) } + let_it_be(:todo) { create(:todo, user: user1) } + let_it_be(:todo) { create(:todo, user: user2) } - it { is_expected.to eq([todo.user_id]) } + it { is_expected.to contain_exactly(user1.id, user2.id) } end end diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb index 27ddaea763d..5806f123871 100644 --- a/spec/models/user_preference_spec.rb +++ b/spec/models/user_preference_spec.rb @@ -61,7 +61,7 @@ RSpec.describe UserPreference do describe 'sort_by preferences' do shared_examples_for 'a sort_by preference' do it 'allows nil sort fields' do - user_preference.update(attribute => nil) + user_preference.update!(attribute => nil) expect(user_preference).to be_valid end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3abf2a651a0..cb34917f073 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -20,6 +20,10 @@ RSpec.describe User do it { is_expected.to include_module(AsyncDeviseEmail) } end + describe 'constants' do + it { expect(described_class::COUNT_CACHE_VALIDITY_PERIOD).to be_a(Integer) } + end + describe 'delegations' do it { is_expected.to delegate_method(:path).to(:namespace).with_prefix } @@ -79,6 +83,7 @@ RSpec.describe User do it { is_expected.to have_one(:user_detail) } it { is_expected.to have_one(:atlassian_identity) } it { is_expected.to have_one(:user_highest_role) } + it { is_expected.to have_one(:credit_card_validation) } it { is_expected.to have_many(:snippets).dependent(:destroy) } it { is_expected.to have_many(:members) } it { is_expected.to have_many(:project_members) } @@ -132,7 +137,7 @@ RSpec.describe User do it 'creates `user_detail` when `bio` is first updated' do user = create(:user) - expect { user.update(bio: 'my bio') }.to change { user.user_detail.persisted? }.from(false).to(true) + expect { user.update!(bio: 'my bio') }.to change { user.user_detail.persisted? }.from(false).to(true) end end @@ -655,9 +660,10 @@ RSpec.describe User do it 'does not accept not verified emails' do email = create(:email) user = email.user - user.update(notification_email: email.email) + user.notification_email = email.email expect(user).to be_invalid + expect(user.errors[:notification_email]).to include('is not an email you own') end end @@ -665,7 +671,7 @@ RSpec.describe User do it 'accepts verified emails' do email = create(:email, :confirmed, email: 'test@test.com') user = email.user - user.update(public_email: email.email) + user.notification_email = email.email expect(user).to be_valid end @@ -673,9 +679,10 @@ RSpec.describe User do it 'does not accept not verified emails' do email = create(:email) user = email.user - user.update(public_email: email.email) + user.public_email = email.email expect(user).to be_invalid + expect(user.errors[:public_email]).to include('is not an email you own') end end @@ -721,6 +728,7 @@ RSpec.describe User do let_it_be(:blocked_user) { create(:user, :blocked) } let_it_be(:ldap_blocked_user) { create(:omniauth_user, :ldap_blocked) } let_it_be(:blocked_pending_approval_user) { create(:user, :blocked_pending_approval) } + let_it_be(:banned_user) { create(:user, :banned) } describe '.blocked' do subject { described_class.blocked } @@ -731,7 +739,7 @@ RSpec.describe User do ldap_blocked_user ) - expect(subject).not_to include(active_user, blocked_pending_approval_user) + expect(subject).not_to include(active_user, blocked_pending_approval_user, banned_user) end end @@ -742,6 +750,14 @@ RSpec.describe User do expect(subject).to contain_exactly(blocked_pending_approval_user) end end + + describe '.banned' do + subject { described_class.banned } + + it 'returns only banned users' do + expect(subject).to contain_exactly(banned_user) + end + end end describe ".with_two_factor" do @@ -1056,6 +1072,21 @@ RSpec.describe User do .to contain_exactly(user) end end + + describe '.for_todos' do + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:issue) { create(:issue) } + + let_it_be(:todo1) { create(:todo, target: issue, author: user1, user: user1) } + let_it_be(:todo2) { create(:todo, target: issue, author: user1, user: user1) } + let_it_be(:todo3) { create(:todo, target: issue, author: user2, user: user2) } + + it 'returns users for the given todos' do + expect(described_class.for_todos(issue.todos)) + .to contain_exactly(user1, user2) + end + end end describe "Respond to" do @@ -1274,7 +1305,7 @@ RSpec.describe User do let(:secondary) { create(:email, :confirmed, email: 'secondary@example.com', user: user) } before do - user.emails.create(email_attrs) + user.emails.create!(email_attrs) user.tap { |u| u.update!(notification_email: email_attrs[:email]) }.reload end @@ -1366,6 +1397,26 @@ RSpec.describe User do end end + describe '#credit_card_validated_at' do + let_it_be(:user) { create(:user) } + + context 'when credit_card_validation does not exist' do + it 'returns nil' do + expect(user.credit_card_validated_at).to be nil + end + end + + context 'when credit_card_validation exists' do + it 'returns the credit card validated time' do + credit_card_validated_time = Time.current - 1.day + + create(:credit_card_validation, credit_card_validated_at: credit_card_validated_time, user: user) + + expect(user.credit_card_validated_at).to eq(credit_card_validated_time) + end + end + end + describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state do let(:request) { OpenStruct.new(remote_ip: "127.0.0.1") } let(:user) { create(:user) } @@ -1428,7 +1479,7 @@ RSpec.describe User do let!(:accessible_deploy_keys_project) { create(:deploy_keys_project, project: project) } before do - public_deploy_keys_project.deploy_key.update(public: true) + public_deploy_keys_project.deploy_key.update!(public: true) project.add_developer(user) end @@ -1518,13 +1569,13 @@ RSpec.describe User do it 'receives callback when external changes' do expect(user).to receive(:ensure_user_rights_and_limits) - user.update(external: false) + user.update!(external: false) end it 'ensures correct rights and limits for user' do stub_config_setting(default_can_create_group: true) - expect { user.update(external: false) }.to change { user.can_create_group }.to(true) + expect { user.update!(external: false) }.to change { user.can_create_group }.to(true) .and change { user.projects_limit }.to(Gitlab::CurrentSettings.default_projects_limit) end end @@ -1535,11 +1586,11 @@ RSpec.describe User do it 'receives callback when external changes' do expect(user).to receive(:ensure_user_rights_and_limits) - user.update(external: true) + user.update!(external: true) end it 'ensures correct rights and limits for user' do - expect { user.update(external: true) }.to change { user.can_create_group }.to(false) + expect { user.update!(external: true) }.to change { user.can_create_group }.to(false) .and change { user.projects_limit }.to(0) end end @@ -1892,6 +1943,12 @@ RSpec.describe User do expect(described_class.filter_items('blocked')).to include user end + it 'filters by banned' do + expect(described_class).to receive(:banned).and_return([user]) + + expect(described_class.filter_items('banned')).to include user + end + it 'filters by blocked pending approval' do expect(described_class).to receive(:blocked_pending_approval).and_return([user]) @@ -2435,7 +2492,7 @@ RSpec.describe User do end context 'with a redirect route matching the given path' do - let!(:redirect_route) { user.namespace.redirect_routes.create(path: 'foo') } + let!(:redirect_route) { user.namespace.redirect_routes.create!(path: 'foo') } context 'without the follow_redirects option' do it 'returns nil' do @@ -2511,8 +2568,9 @@ RSpec.describe User do it 'is false if avatar is html page' do user.update_attribute(:avatar, 'uploads/avatar.html') + user.avatar_type - expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp']) + expect(user.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true end end @@ -2535,7 +2593,7 @@ RSpec.describe User do expect(Gitlab::AvatarCache).to receive(:delete_by_email).with(*user.verified_emails) - user.update(avatar: fixture_file_upload('spec/fixtures/dk.png')) + user.update!(avatar: fixture_file_upload('spec/fixtures/dk.png')) end end @@ -2849,12 +2907,12 @@ RSpec.describe User do expect(user.starred?(project1)).to be_truthy expect(user.starred?(project2)).to be_truthy - star1.destroy + star1.destroy! expect(user.starred?(project1)).to be_falsey expect(user.starred?(project2)).to be_truthy - star2.destroy + star2.destroy! expect(user.starred?(project1)).to be_falsey expect(user.starred?(project2)).to be_falsey @@ -3404,7 +3462,7 @@ RSpec.describe User do expect(user.authorized_projects).to include(project) - member.destroy + member.destroy! expect(user.authorized_projects).not_to include(project) end @@ -3429,7 +3487,7 @@ RSpec.describe User do expect(user2.authorized_projects).to include(project) - project.destroy + project.destroy! expect(user2.authorized_projects).not_to include(project) end @@ -3443,7 +3501,7 @@ RSpec.describe User do expect(user.authorized_projects).to include(project) - group.destroy + group.destroy! expect(user.authorized_projects).not_to include(project) end @@ -4200,16 +4258,45 @@ RSpec.describe User do end describe '#invalidate_issue_cache_counts' do - let(:user) { build_stubbed(:user) } + let_it_be(:user) { create(:user) } - it 'invalidates cache for issue counter' do - cache_mock = double + subject do + user.invalidate_issue_cache_counts + user.save! + end - expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_issues_count']) + shared_examples 'invalidates the cached value' do + it 'invalidates cache for issue counter' do + expect(Rails.cache).to receive(:delete).with(['users', user.id, 'assigned_open_issues_count']) - allow(Rails).to receive(:cache).and_return(cache_mock) + subject + end + end - user.invalidate_issue_cache_counts + it_behaves_like 'invalidates the cached value' + + context 'if feature flag assigned_open_issues_cache is enabled' do + it 'calls the recalculate worker' do + expect(Users::UpdateOpenIssueCountWorker).to receive(:perform_async).with(user.id) + + subject + end + + it_behaves_like 'invalidates the cached value' + end + + context 'if feature flag assigned_open_issues_cache is disabled' do + before do + stub_feature_flags(assigned_open_issues_cache: false) + end + + it 'does not call the recalculate worker' do + expect(Users::UpdateOpenIssueCountWorker).not_to receive(:perform_async).with(user.id) + + subject + end + + it_behaves_like 'invalidates the cached value' end end @@ -4414,9 +4501,10 @@ RSpec.describe User do end it 'adds the namespace errors to the user' do - user.update(username: new_username) + user.username = new_username - expect(user.errors.full_messages.first).to eq('A user, alias, or group already exists with that username.') + expect(user).to be_invalid + expect(user.errors[:base]).to include('A user, alias, or group already exists with that username.') end end end @@ -5238,6 +5326,26 @@ RSpec.describe User do end end + describe 'user credit card validation' do + context 'when user is initialized' do + let(:user) { build(:user) } + + it { expect(user.credit_card_validation).not_to be_present } + end + + context 'when create user without credit card validation' do + let(:user) { create(:user) } + + it { expect(user.credit_card_validation).not_to be_present } + end + + context 'when user credit card validation exists' do + let(:user) { create(:user, :with_credit_card_validation) } + + it { expect(user.credit_card_validation).to be_persisted } + end + end + describe 'user detail' do context 'when user is initialized' do let(:user) { build(:user) } @@ -5307,21 +5415,21 @@ RSpec.describe User do with_them do context 'when state was changed' do - subject { user.update(attributes) } + subject { user.update!(attributes) } include_examples 'update highest role with exclusive lease' end end context 'when state was not changed' do - subject { user.update(email: 'newmail@example.com') } + subject { user.update!(email: 'newmail@example.com') } include_examples 'does not update the highest role' end end describe 'destroy user' do - subject { user.destroy } + subject { user.destroy! } include_examples 'does not update the highest role' end @@ -5343,7 +5451,7 @@ RSpec.describe User do context 'when user is a ghost user' do before do - user.update(user_type: :ghost) + user.update!(user_type: :ghost) end it { is_expected.to be false } @@ -5361,7 +5469,7 @@ RSpec.describe User do with_them do before do - user.update(user_type: user_type) + user.update!(user_type: user_type) end it { is_expected.to be expected_result } @@ -5384,7 +5492,7 @@ RSpec.describe User do context 'when user is an internal user' do before do - user.update(user_type: :ghost) + user.update!(user_type: :ghost) end it { is_expected.to be :forbidden } @@ -5418,7 +5526,7 @@ RSpec.describe User do context 'when user is an internal user' do before do - user.update(user_type: 'alert_bot') + user.update!(user_type: 'alert_bot') end it_behaves_like 'does not require password to be present' @@ -5426,7 +5534,7 @@ RSpec.describe User do context 'when user is a project bot user' do before do - user.update(user_type: 'project_bot') + user.update!(user_type: 'project_bot') end it_behaves_like 'does not require password to be present' @@ -5600,4 +5708,47 @@ RSpec.describe User do end end end + + describe '.dormant' do + it 'returns dormant users' do + freeze_time do + not_that_long_ago = (described_class::MINIMUM_INACTIVE_DAYS - 1).days.ago.to_date + too_long_ago = described_class::MINIMUM_INACTIVE_DAYS.days.ago.to_date + + create(:user, :deactivated, last_activity_on: too_long_ago) + + User::INTERNAL_USER_TYPES.map do |user_type| + create(:user, state: :active, user_type: user_type, last_activity_on: too_long_ago) + end + + create(:user, last_activity_on: not_that_long_ago) + + dormant_user = create(:user, last_activity_on: too_long_ago) + + expect(described_class.dormant).to contain_exactly(dormant_user) + end + end + end + + describe '.with_no_activity' do + it 'returns users with no activity' do + freeze_time do + not_that_long_ago = (described_class::MINIMUM_INACTIVE_DAYS - 1).days.ago.to_date + too_long_ago = described_class::MINIMUM_INACTIVE_DAYS.days.ago.to_date + + create(:user, :deactivated, last_activity_on: nil) + + User::INTERNAL_USER_TYPES.map do |user_type| + create(:user, state: :active, user_type: user_type, last_activity_on: nil) + end + + create(:user, last_activity_on: not_that_long_ago) + create(:user, last_activity_on: too_long_ago) + + user_with_no_activity = create(:user, last_activity_on: nil) + + expect(described_class.with_no_activity).to contain_exactly(user_with_no_activity) + end + end + end end diff --git a/spec/models/user_status_spec.rb b/spec/models/user_status_spec.rb index 51dd91149cc..87d1fa14aca 100644 --- a/spec/models/user_status_spec.rb +++ b/spec/models/user_status_spec.rb @@ -15,7 +15,7 @@ RSpec.describe UserStatus do it 'is expected to be deleted when the user is deleted' do status = create(:user_status) - expect { status.user.destroy }.to change { described_class.count }.from(1).to(0) + expect { status.user.destroy! }.to change { described_class.count }.from(1).to(0) end describe '#clear_status_after=' do diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb new file mode 100644 index 00000000000..fb9f6e35038 --- /dev/null +++ b/spec/models/users/credit_card_validation_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::CreditCardValidation do + it { is_expected.to belong_to(:user) } +end diff --git a/spec/models/wiki_page/meta_spec.rb b/spec/models/wiki_page/meta_spec.rb index 24906d4fb79..37a282657d9 100644 --- a/spec/models/wiki_page/meta_spec.rb +++ b/spec/models/wiki_page/meta_spec.rb @@ -42,7 +42,7 @@ RSpec.describe WikiPage::Meta do subject { described_class.find(meta.id) } let_it_be(:meta) do - described_class.create(title: generate(:wiki_page_title), project: project) + described_class.create!(title: generate(:wiki_page_title), project: project) end context 'there are no slugs' do @@ -124,6 +124,7 @@ RSpec.describe WikiPage::Meta do context 'the slug is already in the DB (but not canonical)' do let_it_be(:slug_record) { create(:wiki_page_slug, wiki_page_meta: meta) } + let(:slug) { slug_record.slug } let(:query_limit) { 4 } @@ -132,6 +133,7 @@ RSpec.describe WikiPage::Meta do context 'the slug is already in the DB (and canonical)' do let_it_be(:slug_record) { create(:wiki_page_slug, :canonical, wiki_page_meta: meta) } + let(:slug) { slug_record.slug } let(:query_limit) { 4 } @@ -181,7 +183,7 @@ RSpec.describe WikiPage::Meta do # an old slug that = canonical_slug different_slug = generate(:sluggified_title) create(:wiki_page_meta, project: project, canonical_slug: different_slug) - .slugs.create(slug: wiki_page.slug) + .slugs.create!(slug: wiki_page.slug) end shared_examples 'metadata examples' do diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index be94eca550c..579a9e664cf 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -620,16 +620,12 @@ RSpec.describe WikiPage do end describe "#versions" do - include_context 'subject is persisted page' + let(:subject) { create_wiki_page } it "returns an array of all commits for the page" do - 3.times { |i| subject.update(content: "content #{i}") } - - expect(subject.versions.count).to eq(4) - end - - it 'returns instances of WikiPageVersion' do - expect(subject.versions).to all( be_a(Gitlab::Git::WikiPageVersion) ) + expect do + 3.times { |i| subject.update(content: "content #{i}") } + end.to change { subject.versions.count }.by(3) end end @@ -640,6 +636,7 @@ RSpec.describe WikiPage do let_it_be(:existing_page) { create_wiki_page(title: 'test page') } let_it_be(:directory_page) { create_wiki_page(title: 'parent directory/child page') } let_it_be(:page_with_special_characters) { create_wiki_page(title: 'test+page') } + let(:untitled_page) { described_class.new(wiki) } where(:page, :title, :changed) do @@ -776,8 +773,11 @@ RSpec.describe WikiPage do end describe '#historical?' do - include_context 'subject is persisted page' + let!(:container) { create(:project) } + + subject { create_wiki_page } + let(:wiki) { subject.wiki } let(:old_version) { subject.versions.last.id } let(:old_page) { wiki.find_page(subject.title, old_version) } let(:latest_version) { subject.versions.first.id } |