diff options
Diffstat (limited to 'spec/models')
81 files changed, 2022 insertions, 626 deletions
diff --git a/spec/models/alert_management/http_integration_spec.rb b/spec/models/alert_management/http_integration_spec.rb index 37d67dfe09a..a3e7b47c116 100644 --- a/spec/models/alert_management/http_integration_spec.rb +++ b/spec/models/alert_management/http_integration_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe AlertManagement::HttpIntegration do + include ::Gitlab::Routing.url_helpers + let_it_be(:project) { create(:project) } subject(:integration) { build(:alert_management_http_integration) } @@ -15,19 +17,17 @@ RSpec.describe AlertManagement::HttpIntegration 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(255) } - it { is_expected.to validate_presence_of(:endpoint_identifier) } - it { is_expected.to validate_length_of(:endpoint_identifier).is_at_most(255) } context 'when active' do # Using `create` instead of `build` the integration so `token` is set. # Uniqueness spec saves integration with `validate: false` otherwise. - subject { create(:alert_management_http_integration) } + subject { create(:alert_management_http_integration, :legacy) } it { is_expected.to validate_uniqueness_of(:endpoint_identifier).scoped_to(:project_id, :active) } end context 'when inactive' do - subject { create(:alert_management_http_integration, :inactive) } + subject { create(:alert_management_http_integration, :legacy, :inactive) } it { is_expected.not_to validate_uniqueness_of(:endpoint_identifier).scoped_to(:project_id, :active) } end @@ -51,10 +51,6 @@ RSpec.describe AlertManagement::HttpIntegration do context 'when unsaved' do context 'when unassigned' do - before do - integration.valid? - end - it_behaves_like 'valid token' end @@ -89,4 +85,75 @@ RSpec.describe AlertManagement::HttpIntegration do end end end + + describe '#endpoint_identifier' do + subject { integration.endpoint_identifier } + + context 'when defined on initialize' do + let(:integration) { described_class.new } + + it { is_expected.to match(/\A\h{16}\z/) } + end + + context 'when included in initialization args' do + let(:integration) { described_class.new(endpoint_identifier: 'legacy') } + + it { is_expected.to eq('legacy') } + end + + context 'when reassigning' do + let(:integration) { create(:alert_management_http_integration) } + let!(:starting_identifier) { subject } + + it 'does not allow reassignment' do + integration.endpoint_identifier = 'newValidId' + integration.save! + + expect(integration.reload.endpoint_identifier).to eq(starting_identifier) + end + end + end + + describe '#url' do + subject { integration.url } + + it do + is_expected.to eq( + project_alert_http_integration_url( + integration.project, + 'datadog', + integration.endpoint_identifier, + format: :json + ) + ) + end + + context 'when name is not defined' do + let(:integration) { described_class.new(project: project) } + + it do + is_expected.to eq( + project_alert_http_integration_url( + integration.project, + 'http-endpoint', + integration.endpoint_identifier, + format: :json + ) + ) + end + end + + context 'for a legacy integration' do + let(:integration) { build(:alert_management_http_integration, :legacy) } + + it do + is_expected.to eq( + project_alerts_notify_url( + integration.project, + format: :json + ) + ) + end + end + end end diff --git a/spec/models/analytics/cycle_analytics/project_stage_spec.rb b/spec/models/analytics/cycle_analytics/project_stage_spec.rb index 4675f037957..fce31af619c 100644 --- a/spec/models/analytics/cycle_analytics/project_stage_spec.rb +++ b/spec/models/analytics/cycle_analytics/project_stage_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Analytics::CycleAnalytics::ProjectStage do end end - it_behaves_like 'cycle analytics stage' do + it_behaves_like 'value stream analytics stage' do let(:parent) { build(:project) } let(:parent_name) { :project } end diff --git a/spec/models/analytics/devops_adoption/segment_selection_spec.rb b/spec/models/analytics/devops_adoption/segment_selection_spec.rb new file mode 100644 index 00000000000..5866cbaa48e --- /dev/null +++ b/spec/models/analytics/devops_adoption/segment_selection_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Analytics::DevopsAdoption::SegmentSelection, type: :model do + subject { build(:devops_adoption_segment_selection, :project) } + + describe 'validation' do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project) } + + it { is_expected.to validate_presence_of(:segment) } + + context do + subject { create(:devops_adoption_segment_selection, :project, project: project) } + + it { is_expected.to validate_uniqueness_of(:project_id).scoped_to(:segment_id) } + end + + context do + subject { create(:devops_adoption_segment_selection, :group, group: group) } + + it { is_expected.to validate_uniqueness_of(:group_id).scoped_to(:segment_id) } + end + + it 'project is required' do + selection = build(:devops_adoption_segment_selection, project: nil, group: nil) + + selection.validate + + expect(selection.errors).to have_key(:project) + end + + it 'project is not required when a group is given' do + selection = build(:devops_adoption_segment_selection, :group, group: group) + + expect(selection).to be_valid + end + + it 'does not allow group to be set when project is present' do + selection = build(:devops_adoption_segment_selection) + + selection.group = group + selection.project = project + + selection.validate + + expect(selection.errors[:group]).to eq([s_('DevopsAdoptionSegmentSelection|The selection cannot be configured for a project and for a group at the same time')]) + end + + context 'limit the number of segment selections' do + let_it_be(:segment) { create(:devops_adoption_segment) } + + subject { build(:devops_adoption_segment_selection, segment: segment, project: project) } + + before do + create(:devops_adoption_segment_selection, :project, segment: segment) + + stub_const("#{described_class}::ALLOWED_SELECTIONS_PER_SEGMENT", 1) + end + + it 'shows validation error' do + subject.validate + + expect(subject.errors[:segment]).to eq([s_('DevopsAdoptionSegment|The maximum number of selections has been reached')]) + end + end + end +end diff --git a/spec/models/analytics/instance_statistics/measurement_spec.rb b/spec/models/analytics/instance_statistics/measurement_spec.rb index 379272cfcb9..dbb16c5ffbe 100644 --- a/spec/models/analytics/instance_statistics/measurement_spec.rb +++ b/spec/models/analytics/instance_statistics/measurement_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Analytics::InstanceStatistics::Measurement, type: :model do describe 'identifiers enum' do it 'maps to the correct values' do - expect(described_class.identifiers).to eq({ + identifiers = { projects: 1, users: 2, issues: 3, @@ -24,8 +24,11 @@ RSpec.describe Analytics::InstanceStatistics::Measurement, type: :model do pipelines_succeeded: 7, pipelines_failed: 8, pipelines_canceled: 9, - pipelines_skipped: 10 - }.with_indifferent_access) + pipelines_skipped: 10, + billable_users: 11 + } + + expect(described_class.identifiers).to eq(identifiers.with_indifferent_access) end end @@ -45,29 +48,71 @@ RSpec.describe Analytics::InstanceStatistics::Measurement, type: :model do it { is_expected.to match_array([measurement_1, measurement_2]) } end - end - describe '#measurement_identifier_values' do - subject { described_class.measurement_identifier_values.count } + describe '.recorded_after' do + subject { described_class.recorded_after(8.days.ago) } - context 'when the `store_ci_pipeline_counts_by_status` feature flag is off' do - let(:expected_count) { Analytics::InstanceStatistics::Measurement.identifiers.size - Analytics::InstanceStatistics::Measurement::EXPERIMENTAL_IDENTIFIERS.size } + it { is_expected.to match_array([measurement_2, measurement_3]) } - before do - stub_feature_flags(store_ci_pipeline_counts_by_status: false) + context 'when nil is given' do + subject { described_class.recorded_after(nil) } + + it 'does not apply filtering' do + expect(subject).to match_array([measurement_1, measurement_2, measurement_3]) + end end + end + + describe '.recorded_before' do + subject { described_class.recorded_before(4.days.ago) } - it { is_expected.to eq(expected_count) } + it { is_expected.to match_array([measurement_1, measurement_3]) } + + context 'when nil is given' do + subject { described_class.recorded_after(nil) } + + it 'does not apply filtering' do + expect(subject).to match_array([measurement_1, measurement_2, measurement_3]) + end + end end + end + + describe '.identifier_query_mapping' do + subject { described_class.identifier_query_mapping } + + it { is_expected.to be_a Hash } + end + + describe '.identifier_min_max_queries' do + subject { described_class.identifier_min_max_queries } + + it { is_expected.to be_a Hash } + end + + describe '.measurement_identifier_values' do + let(:expected_count) { described_class.identifiers.size } + + subject { described_class.measurement_identifier_values.count } + + it { is_expected.to eq(expected_count) } + end - context 'when the `store_ci_pipeline_counts_by_status` feature flag is on' do - let(:expected_count) { Analytics::InstanceStatistics::Measurement.identifiers.size } + describe '.find_latest_or_fallback' do + subject(:count) { described_class.find_latest_or_fallback(:pipelines_skipped).count } - before do - stub_feature_flags(store_ci_pipeline_counts_by_status: true) + context 'with instance statistics' do + let!(:measurement) { create(:instance_statistics_measurement, :pipelines_skipped_count) } + + it 'returns the latest stored measurement' do + expect(count).to eq measurement.count end + end - it { is_expected.to eq(expected_count) } + context 'without instance statistics' do + it 'returns the realtime query of the measurement' do + expect(count).to eq 0 + end end end end diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb index d080b298e2f..6a0f2290b4c 100644 --- a/spec/models/application_record_spec.rb +++ b/spec/models/application_record_spec.rb @@ -67,7 +67,8 @@ RSpec.describe ApplicationRecord do end it 'raises a validation error if the record was not persisted' do - expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid) + expect { Suggestion.safe_find_or_create_by!(note: nil) } + .to raise_error(ActiveRecord::RecordInvalid) end it 'passes a block to find_or_create_by' do @@ -75,6 +76,14 @@ RSpec.describe ApplicationRecord do Suggestion.safe_find_or_create_by!(suggestion_attributes, &block) end.to yield_with_args(an_object_having_attributes(suggestion_attributes)) end + + it 'raises a record not found error in case of attributes mismatch' do + suggestion = Suggestion.safe_find_or_create_by!(suggestion_attributes) + attributes = suggestion_attributes.merge(outdated: !suggestion.outdated) + + expect { Suggestion.safe_find_or_create_by!(attributes) } + .to raise_error(ActiveRecord::RecordNotFound) + end end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index fb702d10a42..efe62a1d086 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -72,6 +72,7 @@ RSpec.describe ApplicationSetting do it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) } it { is_expected.to validate_numericality_of(:container_registry_delete_tags_service_timeout).only_integer.is_greater_than_or_equal_to(0) } + it { is_expected.to validate_numericality_of(:container_registry_expiration_policies_worker_capacity).only_integer.is_greater_than_or_equal_to(0) } it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) } it { is_expected.to validate_numericality_of(:wiki_page_max_content_bytes).only_integer.is_greater_than_or_equal_to(1024) } @@ -647,6 +648,37 @@ RSpec.describe ApplicationSetting do end end end + + describe '#ci_jwt_signing_key' do + it { is_expected.not_to allow_value('').for(:ci_jwt_signing_key) } + it { is_expected.not_to allow_value('invalid RSA key').for(:ci_jwt_signing_key) } + it { is_expected.to allow_value(nil).for(:ci_jwt_signing_key) } + it { is_expected.to allow_value(OpenSSL::PKey::RSA.new(1024).to_pem).for(:ci_jwt_signing_key) } + + it 'is encrypted' do + subject.ci_jwt_signing_key = OpenSSL::PKey::RSA.new(1024).to_pem + + aggregate_failures do + expect(subject.encrypted_ci_jwt_signing_key).to be_present + expect(subject.encrypted_ci_jwt_signing_key_iv).to be_present + expect(subject.encrypted_ci_jwt_signing_key).not_to eq(subject.ci_jwt_signing_key) + end + end + end + + describe '#cloud_license_auth_token' do + it { is_expected.to allow_value(nil).for(:cloud_license_auth_token) } + + it 'is encrypted' do + subject.cloud_license_auth_token = 'token-from-customers-dot' + + aggregate_failures do + expect(subject.encrypted_cloud_license_auth_token).to be_present + expect(subject.encrypted_cloud_license_auth_token_iv).to be_present + expect(subject.encrypted_cloud_license_auth_token).not_to eq(subject.cloud_license_auth_token) + end + end + end end context 'static objects external storage' do diff --git a/spec/models/authentication_event_spec.rb b/spec/models/authentication_event_spec.rb index 483d45c08be..83598fa6765 100644 --- a/spec/models/authentication_event_spec.rb +++ b/spec/models/authentication_event_spec.rb @@ -37,15 +37,11 @@ RSpec.describe AuthenticationEvent do describe '.providers' do before do - create(:authentication_event, provider: :ldapmain) - create(:authentication_event, provider: :google_oauth2) - create(:authentication_event, provider: :standard) - create(:authentication_event, provider: :standard) - create(:authentication_event, provider: :standard) + allow(Devise).to receive(:omniauth_providers).and_return(%w(ldapmain google_oauth2)) end it 'returns an array of distinct providers' do - expect(described_class.providers).to match_array %w(ldapmain google_oauth2 standard) + expect(described_class.providers).to match_array %w(ldapmain google_oauth2 standard two-factor two-factor-via-u2f-device two-factor-via-webauthn-device) end end end diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index fc463c6af52..c4d17905637 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -161,6 +161,12 @@ RSpec.describe BroadcastMessage do expect(subject.call('/group/issues/test').length).to eq(1) end + + it "does not return message if the target path is set but no current path is provided" do + create(:broadcast_message, target_path: "*/issues/*", broadcast_type: broadcast_type) + + expect(subject.call.length).to eq(0) + end end describe '.current', :use_clean_rails_memory_store_caching do diff --git a/spec/models/bulk_imports/tracker_spec.rb b/spec/models/bulk_imports/tracker_spec.rb new file mode 100644 index 00000000000..8eb5a6c27dd --- /dev/null +++ b/spec/models/bulk_imports/tracker_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Tracker, type: :model do + describe 'associations' do + it { is_expected.to belong_to(:entity).required } + end + + describe 'validations' do + before do + create(:bulk_import_tracker) + end + + it { is_expected.to validate_presence_of(:relation) } + it { is_expected.to validate_uniqueness_of(:relation).scoped_to(:bulk_import_entity_id) } + + context 'when has_next_page is true' do + it "validates presence of `next_page`" do + tracker = build(:bulk_import_tracker, has_next_page: true) + + expect(tracker).not_to be_valid + expect(tracker.errors).to include(:next_page) + end + end + end +end diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index c464e176c17..51e82061d97 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -55,6 +55,17 @@ RSpec.describe Ci::Bridge do expect(bridge.scoped_variables_hash.keys).to include(*variables) end + + context 'when bridge has dependency which has dotenv variable' do + let(:test) { create(:ci_build, pipeline: pipeline, stage_idx: 0) } + let(:bridge) { create(:ci_bridge, pipeline: pipeline, stage_idx: 1, options: { dependencies: [test.name] }) } + + let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: test) } + + it 'includes inherited variable' do + expect(bridge.scoped_variables_hash).to include(job_variable.key => job_variable.value) + end + end end describe 'state machine transitions' do @@ -357,4 +368,53 @@ RSpec.describe Ci::Bridge do it { is_expected.to be_falsey } end end + + describe '#dependency_variables' do + subject { bridge.dependency_variables } + + shared_context 'when ci_bridge_dependency_variables is disabled' do + before do + stub_feature_flags(ci_bridge_dependency_variables: false) + end + + it { is_expected.to be_empty } + end + + context 'when downloading from previous stages' do + let!(:prepare1) { create(:ci_build, name: 'prepare1', pipeline: pipeline, stage_idx: 0) } + let!(:bridge) { create(:ci_bridge, pipeline: pipeline, stage_idx: 1) } + + let!(:job_variable_1) { create(:ci_job_variable, :dotenv_source, job: prepare1) } + let!(:job_variable_2) { create(:ci_job_variable, job: prepare1) } + + it 'inherits only dependent variables' do + expect(subject.to_hash).to eq(job_variable_1.key => job_variable_1.value) + end + + it_behaves_like 'when ci_bridge_dependency_variables is disabled' + end + + context 'when using needs' do + let!(:prepare1) { create(:ci_build, name: 'prepare1', pipeline: pipeline, stage_idx: 0) } + let!(:prepare2) { create(:ci_build, name: 'prepare2', pipeline: pipeline, stage_idx: 0) } + let!(:prepare3) { create(:ci_build, name: 'prepare3', pipeline: pipeline, stage_idx: 0) } + let!(:bridge) do + create(:ci_bridge, pipeline: pipeline, + stage_idx: 1, + scheduling_type: 'dag', + needs_attributes: [{ name: 'prepare1', artifacts: true }, + { name: 'prepare2', artifacts: false }]) + end + + let!(:job_variable_1) { create(:ci_job_variable, :dotenv_source, job: prepare1) } + let!(:job_variable_2) { create(:ci_job_variable, :dotenv_source, job: prepare2) } + let!(:job_variable_3) { create(:ci_job_variable, :dotenv_source, job: prepare3) } + + it 'inherits only needs with artifacts variables' do + expect(subject.to_hash).to eq(job_variable_1.key => job_variable_1.value) + end + + it_behaves_like 'when ci_bridge_dependency_variables is disabled' + end + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index f1d51324bbf..5ff9b4dd493 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2464,7 +2464,7 @@ RSpec.describe Ci::Build do end before do - allow(Gitlab::Ci::Jwt).to receive(:for_build).with(build).and_return('ci.job.jwt') + allow(Gitlab::Ci::Jwt).to receive(:for_build).and_return('ci.job.jwt') build.set_token('my-token') build.yaml_variables = [] end @@ -2482,12 +2482,17 @@ RSpec.describe Ci::Build do end context 'when CI_JOB_JWT generation fails' do - it 'CI_JOB_JWT is not included' do - expect(Gitlab::Ci::Jwt).to receive(:for_build).and_raise(OpenSSL::PKey::RSAError, 'Neither PUB key nor PRIV key: not enough data') - expect(Gitlab::ErrorTracking).to receive(:track_exception) - - expect { subject }.not_to raise_error - expect(subject.pluck(:key)).not_to include('CI_JOB_JWT') + [ + OpenSSL::PKey::RSAError, + Gitlab::Ci::Jwt::NoSigningKeyError + ].each do |reason_to_fail| + it 'CI_JOB_JWT is not included' do + expect(Gitlab::Ci::Jwt).to receive(:for_build).and_raise(reason_to_fail) + expect(Gitlab::ErrorTracking).to receive(:track_exception) + + expect { subject }.not_to raise_error + expect(subject.pluck(:key)).not_to include('CI_JOB_JWT') + end end end diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb index 871f279db08..dce7b1d30ca 100644 --- a/spec/models/ci/build_trace_chunk_spec.rb +++ b/spec/models/ci/build_trace_chunk_spec.rb @@ -100,15 +100,15 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do subject { described_class.all_stores } it 'returns a correctly ordered array' do - is_expected.to eq(%w[redis database fog]) + is_expected.to eq(%i[redis database fog]) end it 'returns redis store as the lowest precedence' do - expect(subject.first).to eq('redis') + expect(subject.first).to eq(:redis) end it 'returns fog store as the highest precedence' do - expect(subject.last).to eq('fog') + expect(subject.last).to eq(:fog) end end @@ -135,32 +135,40 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do context 'when data_store is fog' do let(:data_store) { :fog } - context 'when legacy Fog is enabled' do - before do - stub_feature_flags(ci_trace_new_fog_store: false) - build_trace_chunk.send(:unsafe_set_data!, +'Sample data in fog') - end + before do + build_trace_chunk.send(:unsafe_set_data!, +'Sample data in fog') + end - it { is_expected.to eq('Sample data in fog') } + it { is_expected.to eq('Sample data in fog') } - it 'returns a LegacyFog store' do - expect(described_class.get_store_class(data_store)).to be_a(Ci::BuildTraceChunks::LegacyFog) - end + it 'returns a new Fog store' do + expect(described_class.get_store_class(data_store)).to be_a(Ci::BuildTraceChunks::Fog) end + end + end - context 'when new Fog is enabled' do - before do - stub_feature_flags(ci_trace_new_fog_store: true) - build_trace_chunk.send(:unsafe_set_data!, +'Sample data in fog') - end + describe '#get_store_class' do + using RSpec::Parameterized::TableSyntax - it { is_expected.to eq('Sample data in fog') } + where(:data_store, :expected_store) do + :redis | Ci::BuildTraceChunks::Redis + :database | Ci::BuildTraceChunks::Database + :fog | Ci::BuildTraceChunks::Fog + end - it 'returns a new Fog store' do - expect(described_class.get_store_class(data_store)).to be_a(Ci::BuildTraceChunks::Fog) + with_them do + context "with store" do + it 'returns an instance of the right class' do + expect(expected_store).to receive(:new).twice.and_call_original + expect(described_class.get_store_class(data_store.to_s)).to be_a(expected_store) + expect(described_class.get_store_class(data_store.to_sym)).to be_a(expected_store) end end end + + it 'raises an error' do + expect { described_class.get_store_class('unknown') }.to raise_error('Unknown store type: unknown') + end end describe '#append' do @@ -614,23 +622,19 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do context 'when the chunk is being locked by a different worker' do let(:metrics) { spy('metrics') } - it 'does not raise an exception' do - lock_chunk do - expect { build_trace_chunk.persist_data! }.not_to raise_error - end - end - it 'increments stalled chunk trace metric' do allow(build_trace_chunk) .to receive(:metrics) .and_return(metrics) - lock_chunk { build_trace_chunk.persist_data! } + expect do + subject - expect(metrics) - .to have_received(:increment_trace_operation) - .with(operation: :stalled) - .once + expect(metrics) + .to have_received(:increment_trace_operation) + .with(operation: :stalled) + .once + end.to raise_error(described_class::FailedToPersistDataError) end def lock_chunk(&block) diff --git a/spec/models/ci/build_trace_chunks/legacy_fog_spec.rb b/spec/models/ci/build_trace_chunks/legacy_fog_spec.rb deleted file mode 100644 index ca4b414b992..00000000000 --- a/spec/models/ci/build_trace_chunks/legacy_fog_spec.rb +++ /dev/null @@ -1,164 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Ci::BuildTraceChunks::LegacyFog do - let(:data_store) { described_class.new } - - before do - stub_artifacts_object_storage - end - - describe '#available?' do - subject { data_store.available? } - - context 'when object storage is enabled' do - it { is_expected.to be_truthy } - end - - context 'when object storage is disabled' do - before do - stub_artifacts_object_storage(enabled: false) - end - - it { is_expected.to be_falsy } - end - end - - describe '#data' do - subject { data_store.data(model) } - - context 'when data exists' do - let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') } - - it 'returns the data' do - is_expected.to eq('sample data in fog') - end - end - - context 'when data does not exist' do - let(:model) { create(:ci_build_trace_chunk, :fog_without_data) } - - it 'returns nil' do - expect(data_store.data(model)).to be_nil - end - end - end - - describe '#set_data' do - let(:new_data) { 'abc123' } - - context 'when data exists' do - let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') } - - it 'overwrites data' do - expect(data_store.data(model)).to eq('sample data in fog') - - data_store.set_data(model, new_data) - - expect(data_store.data(model)).to eq new_data - end - end - - context 'when data does not exist' do - let(:model) { create(:ci_build_trace_chunk, :fog_without_data) } - - it 'sets new data' do - expect(data_store.data(model)).to be_nil - - data_store.set_data(model, new_data) - - expect(data_store.data(model)).to eq new_data - end - end - end - - describe '#delete_data' do - subject { data_store.delete_data(model) } - - context 'when data exists' do - let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') } - - it 'deletes data' do - expect(data_store.data(model)).to eq('sample data in fog') - - subject - - expect(data_store.data(model)).to be_nil - end - end - - context 'when data does not exist' do - let(:model) { create(:ci_build_trace_chunk, :fog_without_data) } - - it 'does nothing' do - expect(data_store.data(model)).to be_nil - - subject - - expect(data_store.data(model)).to be_nil - end - end - end - - describe '#size' do - context 'when data exists' do - let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'üabcd') } - - it 'returns data bytesize correctly' do - expect(data_store.size(model)).to eq 6 - end - end - - context 'when data does not exist' do - let(:model) { create(:ci_build_trace_chunk, :fog_without_data) } - - it 'returns zero' do - expect(data_store.size(model)).to be_zero - end - end - end - - describe '#keys' do - subject { data_store.keys(relation) } - - let(:build) { create(:ci_build) } - let(:relation) { build.trace_chunks } - - before do - create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 0, build: build) - create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 1, build: build) - end - - it 'returns keys' do - is_expected.to eq([[build.id, 0], [build.id, 1]]) - end - end - - describe '#delete_keys' do - subject { data_store.delete_keys(keys) } - - let(:build) { create(:ci_build) } - let(:relation) { build.trace_chunks } - let(:keys) { data_store.keys(relation) } - - before do - create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 0, build: build) - create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 1, build: build) - end - - it 'deletes multiple data' do - ::Fog::Storage.new(JobArtifactUploader.object_store_credentials).tap do |connection| - expect(connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/0.log")[:body]).to be_present - expect(connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/1.log")[:body]).to be_present - end - - subject - - ::Fog::Storage.new(JobArtifactUploader.object_store_credentials).tap do |connection| - expect { connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/0.log")[:body] }.to raise_error(Excon::Error::NotFound) - expect { connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/1.log")[:body] }.to raise_error(Excon::Error::NotFound) - end - end - end -end diff --git a/spec/models/ci/daily_build_group_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb index 326366666cb..f16396d62c9 100644 --- a/spec/models/ci/daily_build_group_report_result_spec.rb +++ b/spec/models/ci/daily_build_group_report_result_spec.rb @@ -81,4 +81,81 @@ RSpec.describe Ci::DailyBuildGroupReportResult do end end end + + describe 'scopes' do + let_it_be(:project) { create(:project) } + let(:recent_build_group_report_result) { create(:ci_daily_build_group_report_result, project: project) } + let(:old_build_group_report_result) do + create(:ci_daily_build_group_report_result, date: 1.week.ago, project: project) + end + + describe '.by_projects' do + subject { described_class.by_projects([project.id]) } + + it 'returns records by projects' do + expect(subject).to contain_exactly(recent_build_group_report_result, old_build_group_report_result) + end + end + + describe '.with_coverage' do + subject { described_class.with_coverage } + + it 'returns data with coverage' do + expect(subject).to contain_exactly(recent_build_group_report_result, old_build_group_report_result) + end + end + + describe '.with_default_branch' do + subject(:coverages) { described_class.with_default_branch } + + context 'when coverage for the default branch exist' do + let!(:recent_build_group_report_result) { create(:ci_daily_build_group_report_result, project: project) } + let!(:coverage_feature_branch) { create(:ci_daily_build_group_report_result, :on_feature_branch, project: project) } + + it 'returns coverage with the default branch' do + expect(coverages).to contain_exactly(recent_build_group_report_result) + end + end + + context 'when coverage for the default branch does not exist' do + it 'returns an empty collection' do + expect(coverages).to be_empty + end + end + end + + describe '.by_date' do + subject(:coverages) { described_class.by_date(start_date) } + + let!(:coverage_1) { create(:ci_daily_build_group_report_result, date: 1.week.ago) } + + context 'when project has several coverage' do + let!(:coverage_2) { create(:ci_daily_build_group_report_result, date: 2.weeks.ago) } + let(:start_date) { 1.week.ago.to_date.to_s } + + it 'returns the coverage from the start_date' do + expect(coverages).to contain_exactly(coverage_1) + end + end + + context 'when start_date is over 90 days' do + let!(:coverage_2) { create(:ci_daily_build_group_report_result, date: 90.days.ago) } + let!(:coverage_3) { create(:ci_daily_build_group_report_result, date: 91.days.ago) } + let(:start_date) { 1.year.ago.to_date.to_s } + + it 'returns the coverage in the last 90 days' do + expect(coverages).to contain_exactly(coverage_1, coverage_2) + end + end + + context 'when start_date is not a string' do + let!(:coverage_2) { create(:ci_daily_build_group_report_result, date: 90.days.ago) } + let(:start_date) { 1.week.ago } + + it 'returns the coverage in the last 90 days' do + expect(coverages).to contain_exactly(coverage_1, coverage_2) + end + end + end + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 88d08f1ec45..1ca370dc950 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -625,7 +625,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end - describe "coverage" do + describe '#coverage' do let(:project) { create(:project, build_coverage_regex: "/.*/") } let(:pipeline) { create(:ci_empty_pipeline, project: project) } @@ -1972,6 +1972,32 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe '.latest_running_for_ref' do + include_context 'with some outdated pipelines' + + let!(:latest_running_pipeline) do + create_pipeline(:running, 'ref', 'D', project) + end + + it 'returns the latest running pipeline' do + expect(described_class.latest_running_for_ref('ref')) + .to eq(latest_running_pipeline) + end + end + + describe '.latest_failed_for_ref' do + include_context 'with some outdated pipelines' + + let!(:latest_failed_pipeline) do + create_pipeline(:failed, 'ref', 'D', project) + end + + it 'returns the latest failed pipeline' do + expect(described_class.latest_failed_for_ref('ref')) + .to eq(latest_failed_pipeline) + end + end + describe '.latest_successful_for_sha' do include_context 'with some outdated pipelines' diff --git a/spec/models/ci/test_case_failure_spec.rb b/spec/models/ci/test_case_failure_spec.rb new file mode 100644 index 00000000000..34f89b663ed --- /dev/null +++ b/spec/models/ci/test_case_failure_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::TestCaseFailure do + describe 'relationships' do + it { is_expected.to belong_to(:build) } + it { is_expected.to belong_to(:test_case) } + end + + describe 'validations' do + subject { build(:ci_test_case_failure) } + + it { is_expected.to validate_presence_of(:test_case) } + it { is_expected.to validate_presence_of(:build) } + it { is_expected.to validate_presence_of(:failed_at) } + end + + describe '.recent_failures_count' do + let_it_be(:project) { create(:project) } + + subject(:recent_failures) do + described_class.recent_failures_count( + project: project, + test_case_keys: test_case_keys + ) + end + + context 'when test case failures are within the date range and are for the test case keys' do + let(:tc_1) { create(:ci_test_case, project: project) } + let(:tc_2) { create(:ci_test_case, project: project) } + let(:test_case_keys) { [tc_1.key_hash, tc_2.key_hash] } + + before do + create_list(:ci_test_case_failure, 3, test_case: tc_1, failed_at: 1.day.ago) + create_list(:ci_test_case_failure, 2, test_case: tc_2, failed_at: 3.days.ago) + end + + it 'returns the number of failures for each test case key hash for the past 14 days by default' do + expect(recent_failures).to eq( + tc_1.key_hash => 3, + tc_2.key_hash => 2 + ) + end + end + + context 'when test case failures are within the date range but are not for the test case keys' do + let(:tc) { create(:ci_test_case, project: project) } + let(:test_case_keys) { ['some-other-key-hash'] } + + before do + create(:ci_test_case_failure, test_case: tc, failed_at: 1.day.ago) + end + + it 'excludes them from the count' do + expect(recent_failures[tc.key_hash]).to be_nil + end + end + + context 'when test case failures are not within the date range but are for the test case keys' do + let(:tc) { create(:ci_test_case, project: project) } + let(:test_case_keys) { [tc.key_hash] } + + before do + create(:ci_test_case_failure, test_case: tc, failed_at: 15.days.ago) + end + + it 'excludes them from the count' do + expect(recent_failures[tc.key_hash]).to be_nil + end + end + end +end diff --git a/spec/models/ci/test_case_spec.rb b/spec/models/ci/test_case_spec.rb new file mode 100644 index 00000000000..45311e285a6 --- /dev/null +++ b/spec/models/ci/test_case_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::TestCase do + describe 'relationships' do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:test_case_failures) } + end + + describe 'validations' do + subject { build(:ci_test_case) } + + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:key_hash) } + end + + describe '.find_or_create_by_batch' do + it 'finds or creates records for the given test case keys', :aggregate_failures do + project = create(:project) + existing_tc = create(:ci_test_case, project: project) + new_key = Digest::SHA256.hexdigest(SecureRandom.hex) + keys = [existing_tc.key_hash, new_key] + + result = described_class.find_or_create_by_batch(project, keys) + + expect(result.map(&:key_hash)).to match_array([existing_tc.key_hash, new_key]) + expect(result).to all(be_persisted) + end + end +end diff --git a/spec/models/clusters/agent_token_spec.rb b/spec/models/clusters/agent_token_spec.rb index ad9dd11b24e..9110fdeda52 100644 --- a/spec/models/clusters/agent_token_spec.rb +++ b/spec/models/clusters/agent_token_spec.rb @@ -14,5 +14,10 @@ RSpec.describe Clusters::AgentToken do expect(agent_token.token).to be_present end + + it 'is at least 50 characters' do + agent_token = create(:cluster_agent_token) + expect(agent_token.token.length).to be >= 50 + end end end diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb index 7ca7f533a27..3044260a000 100644 --- a/spec/models/clusters/applications/cert_manager_spec.rb +++ b/spec/models/clusters/applications/cert_manager_spec.rb @@ -40,7 +40,7 @@ RSpec.describe Clusters::Applications::CertManager do subject { cert_manager.install_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) } it 'is initialized with cert_manager arguments' do expect(subject.name).to eq('certmanager') @@ -90,7 +90,7 @@ RSpec.describe Clusters::Applications::CertManager do describe '#uninstall_command' do subject { cert_manager.uninstall_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand) } it 'is initialized with cert_manager arguments' do expect(subject.name).to eq('certmanager') diff --git a/spec/models/clusters/applications/crossplane_spec.rb b/spec/models/clusters/applications/crossplane_spec.rb index a41c5f6586b..7082576028b 100644 --- a/spec/models/clusters/applications/crossplane_spec.rb +++ b/spec/models/clusters/applications/crossplane_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Clusters::Applications::Crossplane do subject { crossplane.install_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) } it 'is initialized with crossplane arguments' do expect(subject.name).to eq('crossplane') diff --git a/spec/models/clusters/applications/elastic_stack_spec.rb b/spec/models/clusters/applications/elastic_stack_spec.rb index 62123ffa542..74cacd486b0 100644 --- a/spec/models/clusters/applications/elastic_stack_spec.rb +++ b/spec/models/clusters/applications/elastic_stack_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Clusters::Applications::ElasticStack do subject { elastic_stack.install_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) } it 'is initialized with elastic stack arguments' do expect(subject.name).to eq('elastic-stack') @@ -57,7 +57,7 @@ RSpec.describe Clusters::Applications::ElasticStack do it 'includes a preinstall script' do expect(subject.preinstall).not_to be_empty - expect(subject.preinstall.first).to include("delete") + expect(subject.preinstall.first).to include("helm uninstall") end end @@ -69,7 +69,7 @@ RSpec.describe Clusters::Applications::ElasticStack do it 'includes a preinstall script' do expect(subject.preinstall).not_to be_empty - expect(subject.preinstall.first).to include("delete") + expect(subject.preinstall.first).to include("helm uninstall") end end @@ -123,7 +123,7 @@ RSpec.describe Clusters::Applications::ElasticStack do subject { elastic_stack.uninstall_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand) } it 'is initialized with elastic stack arguments' do expect(subject.name).to eq('elastic-stack') diff --git a/spec/models/clusters/applications/fluentd_spec.rb b/spec/models/clusters/applications/fluentd_spec.rb index 3bda3e99ec1..ccdf6b0e40d 100644 --- a/spec/models/clusters/applications/fluentd_spec.rb +++ b/spec/models/clusters/applications/fluentd_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Clusters::Applications::Fluentd do describe '#install_command' do subject { fluentd.install_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) } it 'is initialized with fluentd arguments' do expect(subject.name).to eq('fluentd') diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index 6d2ecaa6d47..ad1ebd4966a 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -56,7 +56,7 @@ RSpec.describe Clusters::Applications::Helm do subject { application.issue_client_cert } it 'returns a new cert' do - is_expected.to be_kind_of(Gitlab::Kubernetes::Helm::Certificate) + is_expected.to be_kind_of(Gitlab::Kubernetes::Helm::V2::Certificate) expect(subject.cert_string).not_to eq(application.ca_cert) expect(subject.key_string).not_to eq(application.ca_key) end @@ -67,7 +67,7 @@ RSpec.describe Clusters::Applications::Helm do subject { helm.install_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InitCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V2::InitCommand) } it 'is initialized with 1 arguments' do expect(subject.name).to eq('helm') @@ -104,7 +104,7 @@ RSpec.describe Clusters::Applications::Helm do subject { helm.uninstall_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::ResetCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V2::ResetCommand) } it 'has name' do expect(subject.name).to eq('helm') diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 196d57aff7b..1bc1a4343aa 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -131,7 +131,7 @@ RSpec.describe Clusters::Applications::Ingress do describe '#install_command' do subject { ingress.install_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) } it 'is initialized with ingress arguments' do expect(subject.name).to eq('ingress') diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb index 3cf24f1a9ef..e7de2d24334 100644 --- a/spec/models/clusters/applications/jupyter_spec.rb +++ b/spec/models/clusters/applications/jupyter_spec.rb @@ -52,7 +52,7 @@ RSpec.describe Clusters::Applications::Jupyter do subject { jupyter.install_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) } it 'is initialized with 4 arguments' do expect(subject.name).to eq('jupyter') diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index b14161ce8e6..41b4ec86233 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -119,7 +119,7 @@ RSpec.describe Clusters::Applications::Knative do shared_examples 'a command' do it 'is an instance of Helm::InstallCommand' do - expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) + expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) end it 'is initialized with knative arguments' do @@ -171,7 +171,7 @@ RSpec.describe Clusters::Applications::Knative do describe '#uninstall_command' do subject { knative.uninstall_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand) } it "removes knative deployed services before uninstallation" do 2.times do |i| diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index b450900bee6..032de6aa7c2 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -148,7 +148,7 @@ RSpec.describe Clusters::Applications::Prometheus do subject { prometheus.install_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) } it 'is initialized with 3 arguments' do expect(subject.name).to eq('prometheus') @@ -195,7 +195,7 @@ RSpec.describe Clusters::Applications::Prometheus do subject { prometheus.uninstall_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand) } it 'has the application name' do expect(subject.name).to eq('prometheus') @@ -236,7 +236,7 @@ RSpec.describe Clusters::Applications::Prometheus do let(:prometheus) { build(:clusters_applications_prometheus) } let(:values) { prometheus.values } - it { is_expected.to be_an_instance_of(::Gitlab::Kubernetes::Helm::PatchCommand) } + it { is_expected.to be_an_instance_of(::Gitlab::Kubernetes::Helm::V3::PatchCommand) } it 'is initialized with 3 arguments' do expect(patch_command.name).to eq('prometheus') diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index ef916c73e0b..43e2eab3b9d 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Clusters::Applications::Runner do subject { gitlab_runner.install_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) } it 'is initialized with 4 arguments' do expect(subject.name).to eq('runner') diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index dd9b96f39ad..ed74a841044 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -540,6 +540,27 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do end end end + + describe 'helm_major_version can only be 2 or 3' do + using RSpec::Parameterized::TableSyntax + + where(:helm_major_version, :expect_valid) do + 2 | true + 3 | true + 4 | false + -1 | false + end + + with_them do + let(:cluster) { build(:cluster, helm_major_version: helm_major_version) } + + it { is_expected.to eq(expect_valid) } + end + end + end + + it 'has default helm_major_version 3' do + expect(create(:cluster).helm_major_version).to eq(3) end describe '.ancestor_clusters_for_clusterable' do diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 877188097fd..9824eb91bc7 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -493,104 +493,49 @@ RSpec.describe CommitStatus do end end - context 'with the one_dimensional_matrix feature flag disabled' do - describe '#group_name' do - before do - stub_feature_flags(one_dimensional_matrix: false) - end - - let(:commit_status) do - build(:commit_status, pipeline: pipeline, stage: 'test') - end - - subject { commit_status.group_name } - - tests = { - 'rspec:windows' => 'rspec:windows', - 'rspec:windows 0' => 'rspec:windows 0', - 'rspec:windows 0 test' => 'rspec:windows 0 test', - 'rspec:windows 0 1' => 'rspec:windows', - 'rspec:windows 0 1 name' => 'rspec:windows name', - 'rspec:windows 0/1' => 'rspec:windows', - 'rspec:windows 0/1 name' => 'rspec:windows name', - 'rspec:windows 0:1' => 'rspec:windows', - 'rspec:windows 0:1 name' => 'rspec:windows name', - 'rspec:windows 10000 20000' => 'rspec:windows', - 'rspec:windows 0 : / 1' => 'rspec:windows', - 'rspec:windows 0 : / 1 name' => 'rspec:windows name', - '0 1 name ruby' => 'name ruby', - '0 :/ 1 name ruby' => 'name ruby', - 'rspec: [aws]' => 'rspec: [aws]', - 'rspec: [aws] 0/1' => 'rspec: [aws]', - 'rspec: [aws, max memory]' => 'rspec', - 'rspec:linux: [aws, max memory, data]' => 'rspec:linux', - 'rspec: [inception: [something, other thing], value]' => 'rspec', - 'rspec:windows 0/1: [name, other]' => 'rspec:windows', - 'rspec:windows: [name, other] 0/1' => 'rspec:windows', - 'rspec:windows: [name, 0/1] 0/1' => 'rspec:windows', - 'rspec:windows: [0/1, name]' => 'rspec:windows', - 'rspec:windows: [, ]' => 'rspec:windows', - 'rspec:windows: [name]' => 'rspec:windows: [name]', - 'rspec:windows: [name,other]' => 'rspec:windows: [name,other]' - } - - tests.each do |name, group_name| - it "'#{name}' puts in '#{group_name}'" do - commit_status.name = name - - is_expected.to eq(group_name) - end - end - end - end + describe '#group_name' do + using RSpec::Parameterized::TableSyntax - context 'with one_dimensional_matrix feature flag enabled' do - describe '#group_name' do - before do - stub_feature_flags(one_dimensional_matrix: true) - end + let(:commit_status) do + build(:commit_status, pipeline: pipeline, stage: 'test') + end + + subject { commit_status.group_name } + + where(:name, :group_name) do + 'rspec:windows' | 'rspec:windows' + 'rspec:windows 0' | 'rspec:windows 0' + 'rspec:windows 0 test' | 'rspec:windows 0 test' + 'rspec:windows 0 1' | 'rspec:windows' + 'rspec:windows 0 1 name' | 'rspec:windows name' + 'rspec:windows 0/1' | 'rspec:windows' + 'rspec:windows 0/1 name' | 'rspec:windows name' + 'rspec:windows 0:1' | 'rspec:windows' + 'rspec:windows 0:1 name' | 'rspec:windows name' + 'rspec:windows 10000 20000' | 'rspec:windows' + 'rspec:windows 0 : / 1' | 'rspec:windows' + 'rspec:windows 0 : / 1 name' | 'rspec:windows name' + '0 1 name ruby' | 'name ruby' + '0 :/ 1 name ruby' | 'name ruby' + 'rspec: [aws]' | 'rspec' + 'rspec: [aws] 0/1' | 'rspec' + 'rspec: [aws, max memory]' | 'rspec' + 'rspec:linux: [aws, max memory, data]' | 'rspec:linux' + 'rspec: [inception: [something, other thing], value]' | 'rspec' + 'rspec:windows 0/1: [name, other]' | 'rspec:windows' + 'rspec:windows: [name, other] 0/1' | 'rspec:windows' + 'rspec:windows: [name, 0/1] 0/1' | 'rspec:windows' + 'rspec:windows: [0/1, name]' | 'rspec:windows' + 'rspec:windows: [, ]' | 'rspec:windows' + 'rspec:windows: [name]' | 'rspec:windows' + 'rspec:windows: [name,other]' | 'rspec:windows' + end + + with_them do + it "#{params[:name]} puts in #{params[:group_name]}" do + commit_status.name = name - let(:commit_status) do - build(:commit_status, pipeline: pipeline, stage: 'test') - end - - subject { commit_status.group_name } - - tests = { - 'rspec:windows' => 'rspec:windows', - 'rspec:windows 0' => 'rspec:windows 0', - 'rspec:windows 0 test' => 'rspec:windows 0 test', - 'rspec:windows 0 1' => 'rspec:windows', - 'rspec:windows 0 1 name' => 'rspec:windows name', - 'rspec:windows 0/1' => 'rspec:windows', - 'rspec:windows 0/1 name' => 'rspec:windows name', - 'rspec:windows 0:1' => 'rspec:windows', - 'rspec:windows 0:1 name' => 'rspec:windows name', - 'rspec:windows 10000 20000' => 'rspec:windows', - 'rspec:windows 0 : / 1' => 'rspec:windows', - 'rspec:windows 0 : / 1 name' => 'rspec:windows name', - '0 1 name ruby' => 'name ruby', - '0 :/ 1 name ruby' => 'name ruby', - 'rspec: [aws]' => 'rspec', - 'rspec: [aws] 0/1' => 'rspec', - 'rspec: [aws, max memory]' => 'rspec', - 'rspec:linux: [aws, max memory, data]' => 'rspec:linux', - 'rspec: [inception: [something, other thing], value]' => 'rspec', - 'rspec:windows 0/1: [name, other]' => 'rspec:windows', - 'rspec:windows: [name, other] 0/1' => 'rspec:windows', - 'rspec:windows: [name, 0/1] 0/1' => 'rspec:windows', - 'rspec:windows: [0/1, name]' => 'rspec:windows', - 'rspec:windows: [, ]' => 'rspec:windows', - 'rspec:windows: [name]' => 'rspec:windows', - 'rspec:windows: [name,other]' => 'rspec:windows' - } - - tests.each do |name, group_name| - it "'#{name}' puts in '#{group_name}'" do - commit_status.name = name - - is_expected.to eq(group_name) - end + is_expected.to eq(group_name) end end end diff --git a/spec/models/concerns/atomic_internal_id_spec.rb b/spec/models/concerns/atomic_internal_id_spec.rb index 8c3537f1dcc..5ee3c012dc9 100644 --- a/spec/models/concerns/atomic_internal_id_spec.rb +++ b/spec/models/concerns/atomic_internal_id_spec.rb @@ -86,4 +86,20 @@ RSpec.describe AtomicInternalId do expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i) end end + + describe '.with_project_iid_supply' do + let(:iid) { 100 } + + it 'wraps generate and track_greatest in a concurrency-safe lock' do + expect_next_instance_of(InternalId::InternalIdGenerator) do |g| + expect(g).to receive(:with_lock).and_call_original + expect(g.record).to receive(:last_value).and_return(iid) + expect(g).to receive(:track_greatest).with(iid + 4) + end + + ::Milestone.with_project_iid_supply(milestone.project) do |supply| + 4.times { supply.next_value } + end + end + end end diff --git a/spec/models/concerns/from_union_spec.rb b/spec/models/concerns/from_union_spec.rb index bd2893090a8..4f4d948fe48 100644 --- a/spec/models/concerns/from_union_spec.rb +++ b/spec/models/concerns/from_union_spec.rb @@ -3,13 +3,5 @@ require 'spec_helper' RSpec.describe FromUnion do - [true, false].each do |sql_set_operator| - context "when sql-set-operators feature flag is #{sql_set_operator}" do - before do - stub_feature_flags(sql_set_operators: sql_set_operator) - end - - it_behaves_like 'from set operator', Gitlab::SQL::Union - end - end + it_behaves_like 'from set operator', Gitlab::SQL::Union end diff --git a/spec/models/concerns/optionally_search_spec.rb b/spec/models/concerns/optionally_search_spec.rb index c8e2e6da51f..8067ad50322 100644 --- a/spec/models/concerns/optionally_search_spec.rb +++ b/spec/models/concerns/optionally_search_spec.rb @@ -32,7 +32,7 @@ RSpec.describe OptionallySearch do it 'delegates to the search method' do expect(model) .to receive(:search) - .with('foo', {}) + .with('foo') .and_call_original expect(model.optionally_search('foo')).to eq(['foo', {}]) diff --git a/spec/models/container_expiration_policy_spec.rb b/spec/models/container_expiration_policy_spec.rb index 1d9dbe8a867..32ec5ed161a 100644 --- a/spec/models/container_expiration_policy_spec.rb +++ b/spec/models/container_expiration_policy_spec.rb @@ -38,10 +38,43 @@ RSpec.describe ContainerExpirationPolicy, type: :model do it { is_expected.not_to allow_value('foo').for(:keep_n) } end + describe '#disable!' do + let_it_be(:policy) { create(:container_expiration_policy) } + + subject { policy.disable! } + + it 'disables the container expiration policy' do + expect { subject }.to change { policy.reload.enabled }.from(true).to(false) + end + end + + describe '#policy_params' do + let_it_be(:policy) { create(:container_expiration_policy) } + + let(:expected) do + { + 'older_than' => policy.older_than, + 'keep_n' => policy.keep_n, + 'name_regex' => policy.name_regex, + 'name_regex_keep' => policy.name_regex_keep + } + end + + subject { policy.policy_params } + + it { is_expected.to eq(expected) } + end + context 'with a set of regexps' do + let_it_be(:container_expiration_policy) { create(:container_expiration_policy) } + + subject { container_expiration_policy } + valid_regexps = %w[master .* v.+ v10.1.* (?:v.+|master|release)] invalid_regexps = ['[', '(?:v.+|master|release'] + it { is_expected.to validate_presence_of(:name_regex) } + valid_regexps.each do |valid_regexp| it { is_expected.to allow_value(valid_regexp).for(:name_regex) } it { is_expected.to allow_value(valid_regexp).for(:name_regex_keep) } @@ -57,6 +90,8 @@ RSpec.describe ContainerExpirationPolicy, type: :model do subject { container_expiration_policy } + it { is_expected.not_to validate_presence_of(:name_regex) } + valid_regexps.each do |valid_regexp| it { is_expected.to allow_value(valid_regexp).for(:name_regex) } it { is_expected.to allow_value(valid_regexp).for(:name_regex_keep) } @@ -104,25 +139,15 @@ RSpec.describe ContainerExpirationPolicy, type: :model do end end - describe '.executable' do - subject { described_class.executable } + describe '.with_container_repositories' do + subject { described_class.with_container_repositories } - let_it_be(:policy1) { create(:container_expiration_policy, :runnable) } + let_it_be(:policy1) { create(:container_expiration_policy) } let_it_be(:container_repository1) { create(:container_repository, project: policy1.project) } - let_it_be(:policy2) { create(:container_expiration_policy, :runnable) } + let_it_be(:policy2) { create(:container_expiration_policy) } let_it_be(:container_repository2) { create(:container_repository, project: policy2.project) } - let_it_be(:policy3) { create(:container_expiration_policy, :runnable) } + let_it_be(:policy3) { create(:container_expiration_policy) } it { is_expected.to contain_exactly(policy1, policy2) } end - - describe '#disable!' do - let_it_be(:container_expiration_policy) { create(:container_expiration_policy) } - - subject { container_expiration_policy.disable! } - - it 'disables the container expiration policy' do - expect { subject }.to change { container_expiration_policy.reload.enabled }.from(true).to(false) - end - end end diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb index 2a7aaed5204..2adceb1c960 100644 --- a/spec/models/container_repository_spec.rb +++ b/spec/models/container_repository_spec.rb @@ -352,4 +352,20 @@ RSpec.describe ContainerRepository do it { is_expected.to contain_exactly(repository) } end + + describe '.for_project_id' do + subject { described_class.for_project_id(project.id) } + + it { is_expected.to contain_exactly(repository) } + 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) } + let_it_be(:repository_cleanup_ongoing) { create(:container_repository, :cleanup_ongoing) } + + subject { described_class.waiting_for_cleanup } + + it { is_expected.to contain_exactly(repository_cleanup_scheduled, repository_cleanup_unfinished) } + end end diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb index 836c4139107..62380299ea0 100644 --- a/spec/models/custom_emoji_spec.rb +++ b/spec/models/custom_emoji_spec.rb @@ -13,20 +13,28 @@ RSpec.describe CustomEmoji do describe 'exclusion of duplicated emoji' do let(:emoji_name) { Gitlab::Emoji.emojis_names.sample } + let(:group) { create(:group, :private) } it 'disallows emoji names of built-in emoji' do - new_emoji = build(:custom_emoji, name: emoji_name) + new_emoji = build(:custom_emoji, name: emoji_name, group: group) expect(new_emoji).not_to be_valid expect(new_emoji.errors.messages).to eq(name: ["#{emoji_name} is already being used for another emoji"]) end it 'disallows duplicate custom emoji names within namespace' do - old_emoji = create(:custom_emoji) - new_emoji = build(:custom_emoji, name: old_emoji.name, namespace: old_emoji.namespace) + old_emoji = create(:custom_emoji, group: group) + 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 eq(name: ["has already been taken"]) end + + it 'disallows non http and https file value' do + emoji = build(:custom_emoji, name: 'new-name', group: group, file: 'ftp://some-url.in') + + expect(emoji).not_to be_valid + expect(emoji.errors.messages).to eq(file: ["is blocked: Only allowed schemes are http, https"]) + end end end diff --git a/spec/models/dependency_proxy/blob_spec.rb b/spec/models/dependency_proxy/blob_spec.rb new file mode 100644 index 00000000000..7c8a1eb95e8 --- /dev/null +++ b/spec/models/dependency_proxy/blob_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe DependencyProxy::Blob, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:group) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:group) } + it { is_expected.to validate_presence_of(:file) } + it { is_expected.to validate_presence_of(:file_name) } + end + + describe '.total_size' do + it 'returns 0 if no files' do + expect(described_class.total_size).to eq(0) + end + + it 'returns a correct sum of all files sizes' do + create(:dependency_proxy_blob, size: 10) + create(:dependency_proxy_blob, size: 20) + + expect(described_class.total_size).to eq(30) + end + end + + describe '.find_or_build' do + let!(:blob) { create(:dependency_proxy_blob) } + + it 'builds new instance if not found' do + expect(described_class.find_or_build('foo.gz')).not_to be_persisted + end + + it 'finds an existing blob' do + expect(described_class.find_or_build(blob.file_name)).to eq(blob) + end + end + + describe 'file is being stored' do + subject { create(:dependency_proxy_blob) } + + context 'when existing object has local store' do + it_behaves_like 'mounted file in local store' + end + + context 'when direct upload is enabled' do + before do + stub_dependency_proxy_object_storage(direct_upload: true) + end + + it_behaves_like 'mounted file in object store' + end + end +end diff --git a/spec/models/dependency_proxy/group_setting_spec.rb b/spec/models/dependency_proxy/group_setting_spec.rb new file mode 100644 index 00000000000..c4c4a877d50 --- /dev/null +++ b/spec/models/dependency_proxy/group_setting_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe DependencyProxy::GroupSetting, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:group) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:group) } + end +end diff --git a/spec/models/dependency_proxy/registry_spec.rb b/spec/models/dependency_proxy/registry_spec.rb new file mode 100644 index 00000000000..5bfa75a2eed --- /dev/null +++ b/spec/models/dependency_proxy/registry_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe DependencyProxy::Registry, type: :model do + let(:tag) { '2.3.5-alpine' } + let(:blob_sha) { '40bd001563085fc35165329ea1ff5c5ecbdbbeef' } + + context 'image name without namespace' do + let(:image) { 'ruby' } + + describe '#auth_url' do + it 'returns a correct auth url' do + expect(described_class.auth_url(image)) + .to eq('https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/ruby:pull') + end + end + + describe '#manifest_url' do + it 'returns a correct manifest url' do + expect(described_class.manifest_url(image, tag)) + .to eq('https://registry-1.docker.io/v2/library/ruby/manifests/2.3.5-alpine') + end + end + + describe '#blob_url' do + it 'returns a correct blob url' do + expect(described_class.blob_url(image, blob_sha)) + .to eq('https://registry-1.docker.io/v2/library/ruby/blobs/40bd001563085fc35165329ea1ff5c5ecbdbbeef') + end + end + end + + context 'image name with namespace' do + let(:image) { 'foo/ruby' } + + describe '#auth_url' do + it 'returns a correct auth url' do + expect(described_class.auth_url(image)) + .to eq('https://auth.docker.io/token?service=registry.docker.io&scope=repository:foo/ruby:pull') + end + end + + describe '#manifest_url' do + it 'returns a correct manifest url' do + expect(described_class.manifest_url(image, tag)) + .to eq('https://registry-1.docker.io/v2/foo/ruby/manifests/2.3.5-alpine') + end + end + + describe '#blob_url' do + it 'returns a correct blob url' do + expect(described_class.blob_url(image, blob_sha)) + .to eq('https://registry-1.docker.io/v2/foo/ruby/blobs/40bd001563085fc35165329ea1ff5c5ecbdbbeef') + end + end + end +end diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb index 00114a94b56..d4ccaa6a10e 100644 --- a/spec/models/deploy_key_spec.rb +++ b/spec/models/deploy_key_spec.rb @@ -6,6 +6,7 @@ RSpec.describe DeployKey, :mailer do describe "Associations" do it { is_expected.to have_many(:deploy_keys_projects) } it { is_expected.to have_many(:projects) } + it { is_expected.to have_many(:protected_branch_push_access_levels) } end describe 'notification' do @@ -40,4 +41,56 @@ RSpec.describe DeployKey, :mailer do end end end + + describe '.with_write_access_for_project' do + let_it_be(:project) { create(:project, :private) } + + subject { described_class.with_write_access_for_project(project) } + + context 'when no project is passed in' do + let(:project) { nil } + + it { is_expected.to be_empty } + end + + context 'when a project is passed in' do + let_it_be(:deploy_keys_project) { create(:deploy_keys_project, :write_access, project: project) } + let_it_be(:deploy_key) { deploy_keys_project.deploy_key } + + it 'only returns deploy keys with write access' do + create(:deploy_keys_project, project: project) + + is_expected.to contain_exactly(deploy_key) + end + + it 'returns deploy keys only for this project' do + other_project = create(:project) + create(:deploy_keys_project, :write_access, project: other_project) + + is_expected.to contain_exactly(deploy_key) + end + + context 'and a specific deploy key is passed in' do + subject { described_class.with_write_access_for_project(project, deploy_key: specific_deploy_key) } + + context 'and this deploy key is not linked to the project' do + let(:specific_deploy_key) { create(:deploy_key) } + + it { is_expected.to be_empty } + end + + context 'and this deploy key has not write access to the project' do + let(:specific_deploy_key) { create(:deploy_key, deploy_keys_projects: [create(:deploy_keys_project, project: project)]) } + + it { is_expected.to be_empty } + end + + context 'and this deploy key has write access to the project' do + let(:specific_deploy_key) { create(:deploy_key, deploy_keys_projects: [create(:deploy_keys_project, :write_access, project: project)]) } + + it { is_expected.to contain_exactly(specific_deploy_key) } + end + end + end + end end diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb index 7dd4d3129de..ccc2c64e02c 100644 --- a/spec/models/deploy_keys_project_spec.rb +++ b/spec/models/deploy_keys_project_spec.rb @@ -13,21 +13,6 @@ RSpec.describe DeployKeysProject do it { is_expected.to validate_presence_of(:deploy_key) } end - describe '.with_deploy_keys' do - subject(:scoped_query) { described_class.with_deploy_keys.last } - - it 'includes deploy_keys in query' do - project = create(:project) - create(:deploy_keys_project, project: project, deploy_key: create(:deploy_key)) - - includes_query_count = ActiveRecord::QueryRecorder.new { scoped_query }.count - deploy_key_query_count = ActiveRecord::QueryRecorder.new { scoped_query.deploy_key }.count - - expect(includes_query_count).to eq(2) - expect(deploy_key_query_count).to eq(0) - end - end - describe "Destroying" do let(:project) { create(:project) } subject { create(:deploy_keys_project, project: project) } diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb index 60a3e3fc0e2..c7e1d5fc0d5 100644 --- a/spec/models/deploy_token_spec.rb +++ b/spec/models/deploy_token_spec.rb @@ -124,6 +124,39 @@ RSpec.describe DeployToken do end end + # override the default PolicyActor implementation that always returns false + describe "#deactivated?" do + context "when it has been revoked" do + it 'returns true' do + deploy_token.revoke! + + expect(deploy_token.deactivated?).to be_truthy + end + end + + context "when it hasn't been revoked and is not expired" do + it 'returns false' do + expect(deploy_token.deactivated?).to be_falsy + end + end + + context "when it hasn't been revoked and is expired" do + it 'returns false' do + deploy_token.update_attribute(:expires_at, Date.today - 5.days) + + expect(deploy_token.deactivated?).to be_truthy + end + end + + context "when it hasn't been revoked and has no expiry" do + let(:deploy_token) { create(:deploy_token, expires_at: nil) } + + it 'returns false' do + expect(deploy_token.deactivated?).to be_falsy + end + end + end + describe '#username' do context 'persisted records' do it 'returns a default username if none is set' do diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 3e855584c38..9afacd518af 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -114,14 +114,6 @@ RSpec.describe Deployment do deployment.run! end - it 'does not execute Deployments::ExecuteHooksWorker when feature is disabled' do - stub_feature_flags(ci_send_deployment_hook_when_start: false) - expect(Deployments::ExecuteHooksWorker) - .not_to receive(:perform_async).with(deployment.id) - - deployment.run! - end - it 'executes Deployments::DropOlderDeploymentsWorker asynchronously' do expect(Deployments::DropOlderDeploymentsWorker) .to receive(:perform_async).once.with(deployment.id) diff --git a/spec/models/design_management/design_at_version_spec.rb b/spec/models/design_management/design_at_version_spec.rb index 220de80a52a..a7cf6a9652b 100644 --- a/spec/models/design_management/design_at_version_spec.rb +++ b/spec/models/design_management/design_at_version_spec.rb @@ -185,7 +185,7 @@ RSpec.describe DesignManagement::DesignAtVersion do end describe 'validations' do - subject(:design_at_version) { build(:design_at_version) } + subject(:design_at_version) { build_stubbed(:design_at_version) } it { is_expected.to be_valid } diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb index 2ce9f00a056..d3ce2f2d48f 100644 --- a/spec/models/design_management/design_spec.rb +++ b/spec/models/design_management/design_spec.rb @@ -11,6 +11,14 @@ RSpec.describe DesignManagement::Design do let_it_be(:design3) { create(:design, :with_versions, issue: issue, versions_count: 1) } let_it_be(:deleted_design) { create(:design, :with_versions, deleted: true) } + it_behaves_like 'AtomicInternalId', validate_presence: true do + let(:internal_id_attribute) { :iid } + let(:instance) { build(:design, issue: issue) } + let(:scope) { :project } + let(:scope_attrs) { { project: instance.project } } + let(:usage) { :design_management_designs } + end + it_behaves_like 'a class that supports relative positioning' do let_it_be(:relative_parent) { create(:issue) } @@ -23,8 +31,20 @@ RSpec.describe DesignManagement::Design do it { is_expected.to belong_to(:issue) } it { is_expected.to have_many(:actions) } it { is_expected.to have_many(:versions) } + it { is_expected.to have_many(:authors) } it { is_expected.to have_many(:notes).dependent(:delete_all) } it { is_expected.to have_many(:user_mentions) } + + describe '#authors' do + it 'returns unique version authors', :aggregate_failures do + author = create(:user) + create_list(:design_version, 2, designs: [design1], author: author) + version_authors = design1.versions.map(&:author) + + expect(version_authors).to contain_exactly(issue.author, author, author) + expect(design1.authors).to contain_exactly(issue.author, author) + end + end end describe 'validations' do @@ -326,6 +346,38 @@ RSpec.describe DesignManagement::Design do end end + describe '#participants' do + let_it_be_with_refind(:design) { create(:design, issue: issue) } + let_it_be(:current_user) { create(:user) } + let_it_be(:version_author) { create(:user) } + let_it_be(:note_author) { create(:user) } + let_it_be(:mentioned_user) { create(:user) } + let_it_be(:design_version) { create(:design_version, :committed, designs: [design], author: version_author) } + let_it_be(:note) do + create(:diff_note_on_design, + noteable: design, + issue: issue, + project: issue.project, + author: note_author, + note: mentioned_user.to_reference + ) + end + + subject { design.participants(current_user) } + + it { is_expected.to be_empty } + + context 'when participants can read the project' do + before do + design.project.add_guest(version_author) + design.project.add_guest(note_author) + design.project.add_guest(mentioned_user) + end + + it { is_expected.to contain_exactly(version_author, note_author, mentioned_user) } + end + end + describe "#new_design?" do let(:design) { design1 } diff --git a/spec/models/design_management/version_spec.rb b/spec/models/design_management/version_spec.rb index cd52f4129dc..e004ad024bc 100644 --- a/spec/models/design_management/version_spec.rb +++ b/spec/models/design_management/version_spec.rb @@ -31,7 +31,7 @@ RSpec.describe DesignManagement::Version do it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:sha) } it { is_expected.to validate_presence_of(:designs) } - it { is_expected.to validate_presence_of(:issue_id) } + it { is_expected.to validate_presence_of(:issue) } it { is_expected.to validate_uniqueness_of(:sha).scoped_to(:issue_id).case_insensitive } end diff --git a/spec/models/diff_viewer/image_spec.rb b/spec/models/diff_viewer/image_spec.rb new file mode 100644 index 00000000000..e959a7d5eb2 --- /dev/null +++ b/spec/models/diff_viewer/image_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe DiffViewer::Image do + describe '.can_render?' do + let(:diff_file) { double(Gitlab::Diff::File) } + let(:blob) { double(Gitlab::Git::Blob, binary_in_repo?: true, extension: 'png') } + + subject { described_class.can_render?(diff_file, verify_binary: false) } + + it 'returns false if both old and new blob are absent' do + allow(diff_file).to receive(:old_blob) { nil } + allow(diff_file).to receive(:new_blob) { nil } + + is_expected.to be_falsy + end + + it 'returns true if the old blob is present' do + allow(diff_file).to receive(:old_blob) { blob } + allow(diff_file).to receive(:new_blob) { nil } + + is_expected.to be_truthy + end + + it 'returns true if the new blob is present' do + allow(diff_file).to receive(:old_blob) { nil } + allow(diff_file).to receive(:new_blob) { blob } + + is_expected.to be_truthy + end + + it 'returns true if both old and new blobs are present' do + allow(diff_file).to receive(:old_blob) { blob } + allow(diff_file).to receive(:new_blob) { blob } + + is_expected.to be_truthy + end + end +end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 06d3e9da286..179f2a1b0e0 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -982,6 +982,22 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do end end + describe '#has_running_deployments?' do + subject { environment.has_running_deployments? } + + it 'return false when no deployments exist' do + is_expected.to eq(false) + end + + context 'when deployment is running on the environment' do + let!(:deployment) { create(:deployment, :running, environment: environment) } + + it 'return true' do + is_expected.to eq(true) + end + end + end + describe '#metrics' do let(:project) { create(:prometheus_project) } diff --git a/spec/models/experiment_spec.rb b/spec/models/experiment_spec.rb index 64cd2da4621..587f410c9be 100644 --- a/spec/models/experiment_spec.rb +++ b/spec/models/experiment_spec.rb @@ -7,32 +7,6 @@ RSpec.describe Experiment do describe 'associations' do it { is_expected.to have_many(:experiment_users) } - it { is_expected.to have_many(:users) } - it { is_expected.to have_many(:control_group_users) } - it { is_expected.to have_many(:experimental_group_users) } - - describe 'control_group_users and experimental_group_users' do - let(:experiment) { create(:experiment) } - let(:control_group_user) { build(:user) } - let(:experimental_group_user) { build(:user) } - - before do - experiment.control_group_users << control_group_user - experiment.experimental_group_users << experimental_group_user - end - - describe 'control_group_users' do - subject { experiment.control_group_users } - - it { is_expected.to contain_exactly(control_group_user) } - end - - describe 'experimental_group_users' do - subject { experiment.experimental_group_users } - - it { is_expected.to contain_exactly(experimental_group_user) } - end - end end describe 'validations' do @@ -42,71 +16,83 @@ RSpec.describe Experiment do end describe '.add_user' do - let(:name) { :experiment_key } - let(:user) { build(:user) } + let_it_be(:experiment_name) { :experiment_key } + let_it_be(:user) { 'a user' } + let_it_be(:group) { 'a group' } - let!(:experiment) { create(:experiment, name: name) } + subject(:add_user) { described_class.add_user(experiment_name, group, user) } - subject { described_class.add_user(name, :control, user) } - - describe 'creating a new experiment record' do - context 'an experiment with the provided name already exists' do - it 'does not create a new experiment record' do - expect { subject }.not_to change(Experiment, :count) + context 'when an experiment with the provided name does not exist' do + it 'creates a new experiment record' do + allow_next_instance_of(described_class) do |experiment| + allow(experiment).to receive(:record_user_and_group).with(user, group) end + expect { add_user }.to change(described_class, :count).by(1) end - context 'an experiment with the provided name does not exist yet' do - let(:experiment) { nil } - - it 'creates a new experiment record' do - expect { subject }.to change(Experiment, :count).by(1) + it 'forwards the user and group_type to the instance' do + expect_next_instance_of(described_class) do |experiment| + expect(experiment).to receive(:record_user_and_group).with(user, group) end + add_user end end - describe 'creating a new experiment_user record' do - context 'an experiment_user record for this experiment already exists' do - before do - subject - end + context 'when an experiment with the provided name already exists' do + let_it_be(:experiment) { create(:experiment, name: experiment_name) } - it 'does not create a new experiment_user record' do - expect { subject }.not_to change(ExperimentUser, :count) + it 'does not create a new experiment record' do + allow_next_found_instance_of(described_class) do |experiment| + allow(experiment).to receive(:record_user_and_group).with(user, group) end + expect { add_user }.not_to change(described_class, :count) end - context 'an experiment_user record for this experiment does not exist yet' do - it 'creates a new experiment_user record' do - expect { subject }.to change(ExperimentUser, :count).by(1) - end - - it 'assigns the correct group_type to the experiment_user' do - expect { subject }.to change { experiment.control_group_users.count }.by(1) + it 'forwards the user and group_type to the instance' do + expect_next_found_instance_of(described_class) do |experiment| + expect(experiment).to receive(:record_user_and_group).with(user, group) end + add_user end end end - describe '#add_control_user' do - let(:experiment) { create(:experiment) } - let(:user) { build(:user) } + describe '#record_user_and_group' do + let_it_be(:experiment) { create(:experiment) } + let_it_be(:user) { create(:user) } - subject { experiment.add_control_user(user) } + let(:group) { :control } - it 'creates a new experiment_user record and assigns the correct group_type' do - expect { subject }.to change { experiment.control_group_users.count }.by(1) + subject(:record_user_and_group) { experiment.record_user_and_group(user, group) } + + context 'when an experiment_user does not yet exist for the given user' do + it 'creates a new experiment_user record' do + expect { record_user_and_group }.to change(ExperimentUser, :count).by(1) + end + + it 'assigns the correct group_type to the experiment_user' do + record_user_and_group + expect(ExperimentUser.last.group_type).to eq('control') + end end - end - describe '#add_experimental_user' do - let(:experiment) { create(:experiment) } - let(:user) { build(:user) } + context 'when an experiment_user already exists for the given user' do + before do + # Create an existing experiment_user for this experiment and the :control group + experiment.record_user_and_group(user, :control) + end + + it 'does not create a new experiment_user record' do + expect { record_user_and_group }.not_to change(ExperimentUser, :count) + end - subject { experiment.add_experimental_user(user) } + context 'but the group_type has changed' do + let(:group) { :experimental } - it 'creates a new experiment_user record and assigns the correct group_type' do - expect { subject }.to change { experiment.experimental_group_users.count }.by(1) + it 'updates the existing experiment_user record' do + expect { record_user_and_group }.to change { ExperimentUser.last.group_type } + end + end end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index cc29e20710a..dd1faf999b3 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -28,6 +28,8 @@ RSpec.describe Group do it { is_expected.to have_many(:iterations) } it { is_expected.to have_many(:group_deploy_keys) } it { is_expected.to have_many(:services) } + it { is_expected.to have_one(:dependency_proxy_setting) } + it { is_expected.to have_many(:dependency_proxy_blobs) } describe '#members & #requesters' do let(:requester) { create(:user) } @@ -308,8 +310,10 @@ RSpec.describe Group do end describe 'scopes' do - let!(:private_group) { create(:group, :private) } - let!(:internal_group) { create(:group, :internal) } + let_it_be(:private_group) { create(:group, :private) } + let_it_be(:internal_group) { create(:group, :internal) } + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } describe 'public_only' do subject { described_class.public_only.to_a } @@ -328,6 +332,27 @@ RSpec.describe Group do it { is_expected.to match_array([private_group, internal_group]) } 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) } + + it do + result = described_class.for_authorized_group_members([user1.id, user2.id]) + + expect(result).to match_array([private_group]) + end + end + + describe 'for_authorized_project_members' do + let_it_be(:project) { create(:project, group: internal_group) } + let_it_be(:project_member1) { create(:project_member, source: project, user_id: user1.id, access_level: Gitlab::Access::DEVELOPER) } + + it do + result = described_class.for_authorized_project_members([user1.id, user2.id]) + + expect(result).to match_array([internal_group]) + end + end end describe '#to_reference' do @@ -944,23 +969,72 @@ RSpec.describe Group do context 'expanded group members' do let(:indirect_user) { create(:user) } - it 'enables two_factor_requirement for subgroup member' do - subgroup = create(:group, :nested, parent: group) - subgroup.add_user(indirect_user, GroupMember::OWNER) + context 'two_factor_requirement is enabled' do + context 'two_factor_requirement is also enabled for ancestor group' do + it 'enables two_factor_requirement for subgroup member' do + subgroup = create(:group, :nested, parent: group) + subgroup.add_user(indirect_user, GroupMember::OWNER) - group.update!(require_two_factor_authentication: true) + group.update!(require_two_factor_authentication: true) + + expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy + end + end + + context 'two_factor_requirement is disabled for ancestor group' do + it 'enables two_factor_requirement for subgroup member' do + subgroup = create(:group, :nested, parent: group, require_two_factor_authentication: true) + subgroup.add_user(indirect_user, GroupMember::OWNER) + + group.update!(require_two_factor_authentication: false) + + expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy + end + + it 'enable two_factor_requirement for ancestor group member' do + ancestor_group = create(:group) + ancestor_group.add_user(indirect_user, GroupMember::OWNER) + group.update!(parent: ancestor_group) + + group.update!(require_two_factor_authentication: true) - expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy + expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy + end + end end - it 'does not enable two_factor_requirement for ancestor group member' do - ancestor_group = create(:group) - ancestor_group.add_user(indirect_user, GroupMember::OWNER) - group.update!(parent: ancestor_group) + context 'two_factor_requirement is disabled' do + context 'two_factor_requirement is enabled for ancestor group' do + it 'enables two_factor_requirement for subgroup member' do + subgroup = create(:group, :nested, parent: group) + subgroup.add_user(indirect_user, GroupMember::OWNER) - group.update!(require_two_factor_authentication: true) + group.update!(require_two_factor_authentication: true) + + expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy + end + end + + context 'two_factor_requirement is also disabled for ancestor group' do + it 'disables two_factor_requirement for subgroup member' do + subgroup = create(:group, :nested, parent: group) + subgroup.add_user(indirect_user, GroupMember::OWNER) - expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_falsey + group.update!(require_two_factor_authentication: false) + + expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_falsey + end + + it 'disables two_factor_requirement for ancestor group member' do + ancestor_group = create(:group, require_two_factor_authentication: false) + indirect_user.update!(require_two_factor_authentication_from_group: true) + ancestor_group.add_user(indirect_user, GroupMember::OWNER) + + group.update!(require_two_factor_authentication: false) + + expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_falsey + end + end end end @@ -1591,4 +1665,47 @@ RSpec.describe Group do end end end + + describe 'has_project_with_service_desk_enabled?' do + let_it_be(:group) { create(:group, :private) } + + subject { group.has_project_with_service_desk_enabled? } + + before do + allow(Gitlab::ServiceDesk).to receive(:supported?).and_return(true) + end + + context 'when service desk is enabled' do + context 'for top level group' do + let_it_be(:project) { create(:project, group: group, service_desk_enabled: true) } + + it { is_expected.to eq(true) } + + context 'when service desk is not supported' do + before do + allow(Gitlab::ServiceDesk).to receive(:supported?).and_return(false) + end + + it { is_expected.to eq(false) } + end + end + + context 'for subgroup project' do + let_it_be(:subgroup) { create(:group, :private, parent: group)} + let_it_be(:project) { create(:project, group: subgroup, service_desk_enabled: true) } + + it { is_expected.to eq(true) } + end + end + + context 'when none of group child projects has service desk enabled' do + let_it_be(:project) { create(:project, group: group, service_desk_enabled: false) } + + before do + project.update(service_desk_enabled: false) + end + + it { is_expected.to eq(false) } + end + end end diff --git a/spec/models/instance_metadata_spec.rb b/spec/models/instance_metadata_spec.rb new file mode 100644 index 00000000000..1835dc8a9af --- /dev/null +++ b/spec/models/instance_metadata_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe InstanceMetadata do + it 'has the correct properties' do + expect(subject).to have_attributes( + version: Gitlab::VERSION, + revision: Gitlab.revision + ) + end +end diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb index 751e8724872..07f62b9de55 100644 --- a/spec/models/internal_id_spec.rb +++ b/spec/models/internal_id_spec.rb @@ -6,8 +6,9 @@ RSpec.describe InternalId do let(:project) { create(:project) } let(:usage) { :issues } let(:issue) { build(:issue, project: project) } + let(:id_subject) { issue } let(:scope) { { project: project } } - let(:init) { ->(s) { s.project.issues.size } } + let(:init) { ->(issue, scope) { issue&.project&.issues&.size || Issue.where(**scope).count } } it_behaves_like 'having unique enum values' @@ -39,7 +40,7 @@ RSpec.describe InternalId do end describe '.generate_next' do - subject { described_class.generate_next(issue, scope, usage, init) } + subject { described_class.generate_next(id_subject, scope, usage, init) } context 'in the absence of a record' do it 'creates a record if not yet present' do @@ -88,6 +89,14 @@ RSpec.describe InternalId do expect(normalized).to eq((0..seq.size - 1).to_a) end + + context 'there are no instances to pass in' do + let(:id_subject) { Issue } + + it 'accepts classes instead' do + expect(subject).to eq(1) + end + end end describe '.reset' do @@ -130,7 +139,7 @@ RSpec.describe InternalId do describe '.track_greatest' do let(:value) { 9001 } - subject { described_class.track_greatest(issue, scope, usage, value, init) } + subject { described_class.track_greatest(id_subject, scope, usage, value, init) } context 'in the absence of a record' do it 'creates a record if not yet present' do @@ -166,6 +175,14 @@ RSpec.describe InternalId do expect(subject).to eq 10_001 end end + + context 'there are no instances to pass in' do + let(:id_subject) { Issue } + + it 'accepts classes instead' do + expect(subject).to eq(value) + end + end end describe '#increment_and_save!' do diff --git a/spec/models/issue_link_spec.rb b/spec/models/issue_link_spec.rb index 00791d4a48b..ef41108ebea 100644 --- a/spec/models/issue_link_spec.rb +++ b/spec/models/issue_link_spec.rb @@ -27,7 +27,14 @@ RSpec.describe IssueLink do .with_message(/already related/) end - context 'self relation' do + it 'is not valid if an opposite link already exists' do + issue_link = build(:issue_link, source: subject.target, target: subject.source) + + expect(issue_link).to be_invalid + expect(issue_link.errors[:source]).to include('is already related to this issue') + end + + context 'when it relates to itself' do let(:issue) { create :issue } context 'cannot be validated' do diff --git a/spec/models/issues/csv_import_spec.rb b/spec/models/issues/csv_import_spec.rb new file mode 100644 index 00000000000..2911a79e505 --- /dev/null +++ b/spec/models/issues/csv_import_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Issues::CsvImport, type: :model do + describe 'associations' do + it { is_expected.to belong_to(:project).required } + it { is_expected.to belong_to(:user).required } + end +end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 1e14864676c..3d33a39d353 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -108,7 +108,7 @@ RSpec.describe Key, :mailer do expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid end - where(:factory, :chars, :expected_sections) do + where(:factory, :characters, :expected_sections) do [ [:key, ["\n", "\r\n"], 3], [:key, [' ', ' '], 3], @@ -122,7 +122,7 @@ RSpec.describe Key, :mailer do let!(:original_fingerprint_sha256) { key.fingerprint_sha256 } it 'accepts a key with blank space characters after stripping them' do - modified_key = key.key.insert(100, chars.first).insert(40, chars.last) + modified_key = key.key.insert(100, characters.first).insert(40, characters.last) _, content = modified_key.split key.update!(key: modified_key) diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 118b1492cd6..1a791820f1b 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -252,12 +252,17 @@ RSpec.describe Member do end describe '.last_ten_days_excluding_today' do - let_it_be(:created_today) { create(:group_member, created_at: Date.today.beginning_of_day) } - let_it_be(:created_yesterday) { create(:group_member, created_at: 1.day.ago) } - let_it_be(:created_eleven_days_ago) { create(:group_member, created_at: 11.days.ago) } + let_it_be(:now) { Time.current } + let_it_be(:created_today) { create(:group_member, created_at: now.beginning_of_day) } + let_it_be(:created_yesterday) { create(:group_member, created_at: now - 1.day) } + let_it_be(:created_eleven_days_ago) { create(:group_member, created_at: now - 11.days) } subject { described_class.last_ten_days_excluding_today } + before do + travel_to now + end + it { is_expected.to include(created_yesterday) } it { is_expected.not_to include(created_today, created_eleven_days_ago) } end diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 9af620e70a5..2b24e2d6455 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -4,9 +4,10 @@ require 'spec_helper' RSpec.describe GroupMember do context 'scopes' do + let_it_be(:user_1) { create(:user) } + let_it_be(:user_2) { create(:user) } + it 'counts users by group ID' do - user_1 = create(:user) - user_2 = create(:user) group_1 = create(:group) group_2 = create(:group) @@ -25,6 +26,15 @@ RSpec.describe GroupMember do expect(described_class.of_ldap_type).to eq([group_member]) end end + + describe '.with_user' do + it 'returns requested user' do + group_member = create(:group_member, user: user_2) + create(:group_member, user: user_1) + + expect(described_class.with_user(user_2)).to eq([group_member]) + end + end end describe '.access_level_roles' do diff --git a/spec/models/merge_request/cleanup_schedule_spec.rb b/spec/models/merge_request/cleanup_schedule_spec.rb new file mode 100644 index 00000000000..925d287088b --- /dev/null +++ b/spec/models/merge_request/cleanup_schedule_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequest::CleanupSchedule do + describe 'associations' do + it { is_expected.to belong_to(:merge_request) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:scheduled_at) } + end + + describe '.scheduled_merge_request_ids' do + let_it_be(:mr_cleanup_schedule_1) { create(:merge_request_cleanup_schedule, scheduled_at: 2.days.ago) } + let_it_be(:mr_cleanup_schedule_2) { create(:merge_request_cleanup_schedule, scheduled_at: 1.day.ago) } + let_it_be(:mr_cleanup_schedule_3) { create(:merge_request_cleanup_schedule, scheduled_at: 1.day.ago, completed_at: Time.current) } + let_it_be(:mr_cleanup_schedule_4) { create(:merge_request_cleanup_schedule, scheduled_at: 4.days.ago) } + let_it_be(:mr_cleanup_schedule_5) { create(:merge_request_cleanup_schedule, scheduled_at: 3.days.ago) } + let_it_be(:mr_cleanup_schedule_6) { create(:merge_request_cleanup_schedule, scheduled_at: 1.day.from_now) } + let_it_be(:mr_cleanup_schedule_7) { create(:merge_request_cleanup_schedule, scheduled_at: 5.days.ago) } + + it 'only includes incomplete schedule within the specified limit' do + expect(described_class.scheduled_merge_request_ids(4)).to eq([ + mr_cleanup_schedule_2.merge_request_id, + mr_cleanup_schedule_1.merge_request_id, + mr_cleanup_schedule_5.merge_request_id, + mr_cleanup_schedule_4.merge_request_id + ]) + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index ddb3ffdda2f..9574c57e46c 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -30,6 +30,7 @@ RSpec.describe MergeRequest, factory_default: :keep do it { is_expected.to have_many(:resource_state_events) } it { is_expected.to have_many(:draft_notes) } it { is_expected.to have_many(:reviews).inverse_of(:merge_request) } + it { is_expected.to have_one(:cleanup_schedule).inverse_of(:merge_request) } context 'for forks' do let!(:project) { create(:project) } @@ -79,6 +80,18 @@ RSpec.describe MergeRequest, factory_default: :keep do end end + describe '.with_jira_issue_keys' do + let_it_be(:mr_with_jira_title) { create(:merge_request, :unique_branches, title: 'Fix TEST-123') } + let_it_be(:mr_with_jira_description) { create(:merge_request, :unique_branches, description: 'this closes TEST-321') } + let_it_be(:mr_without_jira_reference) { create(:merge_request, :unique_branches) } + + subject { described_class.with_jira_issue_keys } + + it { is_expected.to contain_exactly(mr_with_jira_title, mr_with_jira_description) } + + it { is_expected.not_to include(mr_without_jira_reference) } + end + describe '#squash_in_progress?' do let(:repo_path) do Gitlab::GitalyClient::StorageSettings.allow_disk_access do diff --git a/spec/models/namespace/root_storage_statistics_spec.rb b/spec/models/namespace/root_storage_statistics_spec.rb index 92a8d17a2a8..b725d2366a1 100644 --- a/spec/models/namespace/root_storage_statistics_spec.rb +++ b/spec/models/namespace/root_storage_statistics_spec.rb @@ -45,6 +45,7 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do total_storage_size = stat1.storage_size + stat2.storage_size total_snippets_size = stat1.snippets_size + stat2.snippets_size total_pipeline_artifacts_size = stat1.pipeline_artifacts_size + stat2.pipeline_artifacts_size + total_uploads_size = stat1.uploads_size + stat2.uploads_size expect(root_storage_statistics.repository_size).to eq(total_repository_size) expect(root_storage_statistics.wiki_size).to eq(total_wiki_size) @@ -54,6 +55,7 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do expect(root_storage_statistics.storage_size).to eq(total_storage_size) expect(root_storage_statistics.snippets_size).to eq(total_snippets_size) expect(root_storage_statistics.pipeline_artifacts_size).to eq(total_pipeline_artifacts_size) + expect(root_storage_statistics.uploads_size).to eq(total_uploads_size) end it 'works when there are no projects' do diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb index c6e8d5b129c..59b7510051f 100644 --- a/spec/models/namespace_setting_spec.rb +++ b/spec/models/namespace_setting_spec.rb @@ -36,13 +36,10 @@ RSpec.describe NamespaceSetting, type: :model do context "when an empty string" do before do - namespace_settings.default_branch_name = '' + namespace_settings.default_branch_name = "" end - it "returns an error" do - expect(namespace_settings.valid?).to be_falsey - expect(namespace_settings.errors.full_messages).not_to be_empty - end + it_behaves_like "doesn't return an error" end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 91b18f346c6..85f9005052e 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -147,30 +147,45 @@ RSpec.describe Namespace do end describe '.search' do - let(:namespace) { create(:namespace) } + let_it_be(:first_namespace) { build(:namespace, name: 'my first namespace', path: 'old-path').tap(&:save!) } + let_it_be(:parent_namespace) { build(:namespace, name: 'my parent namespace', path: 'parent-path').tap(&:save!) } + let_it_be(:second_namespace) { build(:namespace, name: 'my second namespace', path: 'new-path', parent: parent_namespace).tap(&:save!) } + let_it_be(:project_with_same_path) { create(:project, id: second_namespace.id, path: first_namespace.path) } it 'returns namespaces with a matching name' do - expect(described_class.search(namespace.name)).to eq([namespace]) + expect(described_class.search('my first namespace')).to eq([first_namespace]) end it 'returns namespaces with a partially matching name' do - expect(described_class.search(namespace.name[0..2])).to eq([namespace]) + expect(described_class.search('first')).to eq([first_namespace]) end it 'returns namespaces with a matching name regardless of the casing' do - expect(described_class.search(namespace.name.upcase)).to eq([namespace]) + expect(described_class.search('MY FIRST NAMESPACE')).to eq([first_namespace]) end it 'returns namespaces with a matching path' do - expect(described_class.search(namespace.path)).to eq([namespace]) + expect(described_class.search('old-path')).to eq([first_namespace]) end it 'returns namespaces with a partially matching path' do - expect(described_class.search(namespace.path[0..2])).to eq([namespace]) + expect(described_class.search('old')).to eq([first_namespace]) end it 'returns namespaces with a matching path regardless of the casing' do - expect(described_class.search(namespace.path.upcase)).to eq([namespace]) + expect(described_class.search('OLD-PATH')).to eq([first_namespace]) + end + + it 'returns namespaces with a matching route path' do + expect(described_class.search('parent-path/new-path', include_parents: true)).to eq([second_namespace]) + end + + it 'returns namespaces with a partially matching route path' do + expect(described_class.search('parent-path/new', include_parents: true)).to eq([second_namespace]) + end + + it 'returns namespaces with a matching route path regardless of the casing' do + expect(described_class.search('PARENT-PATH/NEW-PATH', include_parents: true)).to eq([second_namespace]) end end @@ -672,7 +687,7 @@ RSpec.describe Namespace do let!(:project) { create(:project_empty_repo, namespace: namespace) } it 'has no repositories base directories to remove' do - allow(GitlabShellWorker).to receive(:perform_in) + expect(GitlabShellWorker).not_to receive(:perform_in) expect(File.exist?(path_in_dir)).to be(false) @@ -855,8 +870,8 @@ RSpec.describe Namespace do end describe '#all_projects' do - shared_examples 'all projects for a namespace' do - let(:namespace) { create(:namespace) } + shared_examples 'all projects for a group' do + let(:namespace) { create(:group) } let(:child) { create(:group, parent: namespace) } let!(:project1) { create(:project_empty_repo, namespace: namespace) } let!(:project2) { create(:project_empty_repo, namespace: child) } @@ -865,30 +880,34 @@ RSpec.describe Namespace do it { expect(child.all_projects.to_a).to match_array([project2]) } end - shared_examples 'all project examples' do - include_examples 'all projects for a namespace' + shared_examples 'all projects for personal namespace' do + let_it_be(:user) { create(:user) } + let_it_be(:user_namespace) { create(:namespace, owner: user) } + let_it_be(:project) { create(:project, namespace: user_namespace) } + + it { expect(user_namespace.all_projects.to_a).to match_array([project]) } + end + context 'with recursive approach' do context 'when namespace is a group' do - let_it_be(:namespace) { create(:group) } + include_examples 'all projects for a group' + + it 'queries for the namespace and its descendants' do + expect(Project).to receive(:where).with(namespace: [namespace, child]) - include_examples 'all projects for a namespace' + namespace.all_projects + end end context 'when namespace is a user namespace' do - let_it_be(:user) { create(:user) } - let_it_be(:user_namespace) { create(:namespace, owner: user) } - let_it_be(:project) { create(:project, namespace: user_namespace) } + include_examples 'all projects for personal namespace' - it { expect(user_namespace.all_projects.to_a).to match_array([project]) } - end - end + it 'only queries for the namespace itself' do + expect(Project).to receive(:where).with(namespace: user_namespace) - context 'with recursive approach' do - before do - stub_feature_flags(recursive_approach_for_all_projects: true) + user_namespace.all_projects + end end - - include_examples 'all project examples' end context 'with route path wildcard approach' do @@ -896,7 +915,13 @@ RSpec.describe Namespace do stub_feature_flags(recursive_approach_for_all_projects: false) end - include_examples 'all project examples' + context 'when namespace is a group' do + include_examples 'all projects for a group' + end + + context 'when namespace is a user namespace' do + include_examples 'all projects for personal namespace' + end end end @@ -1246,24 +1271,6 @@ RSpec.describe Namespace do expect(virtual_domain.lookup_paths).not_to be_empty end end - - it 'preloads project_feature and route' do - project2 = create(:project, namespace: namespace) - project3 = create(:project, namespace: namespace) - - project.mark_pages_as_deployed - project2.mark_pages_as_deployed - project3.mark_pages_as_deployed - - virtual_domain = namespace.pages_virtual_domain - - queries = ActiveRecord::QueryRecorder.new { virtual_domain.lookup_paths } - - # 1 to load projects - # 1 to preload project features - # 1 to load routes - expect(queries.count).to eq(3) - end end end diff --git a/spec/models/operations/feature_flag_spec.rb b/spec/models/operations/feature_flag_spec.rb index b4e941f2856..93dd7d4f0bb 100644 --- a/spec/models/operations/feature_flag_spec.rb +++ b/spec/models/operations/feature_flag_spec.rb @@ -261,4 +261,38 @@ RSpec.describe Operations::FeatureFlag do expect(flags.map(&:id)).to eq([feature_flag.id, feature_flag_b.id]) end end + + describe '#hook_attrs' do + it 'includes expected attributes' do + hook_attrs = { + id: subject.id, + name: subject.name, + description: subject.description, + active: subject.active + } + expect(subject.hook_attrs).to eq(hook_attrs) + end + end + + describe "#execute_hooks" do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) } + + it 'does not execute the hook when feature_flag event is disabled' do + create(:project_hook, project: project, feature_flag_events: false) + expect(WebHookWorker).not_to receive(:perform_async) + + feature_flag.execute_hooks(user) + feature_flag.touch + end + + it 'executes hook when feature_flag event is enabled' do + hook = create(:project_hook, project: project, feature_flag_events: true) + expect(WebHookWorker).to receive(:perform_async).with(hook.id, an_instance_of(Hash), 'feature_flag_hooks') + + feature_flag.execute_hooks(user) + feature_flag.touch + end + end end diff --git a/spec/models/operations/feature_flags/user_list_spec.rb b/spec/models/operations/feature_flags/user_list_spec.rb index 020416aa7bc..3a48d3389a3 100644 --- a/spec/models/operations/feature_flags/user_list_spec.rb +++ b/spec/models/operations/feature_flags/user_list_spec.rb @@ -92,6 +92,25 @@ RSpec.describe Operations::FeatureFlags::UserList do end end + describe '.for_name_like' do + let_it_be(:project) { create(:project) } + let_it_be(:user_list_one) { create(:operations_feature_flag_user_list, project: project, name: 'one') } + let_it_be(:user_list_two) { create(:operations_feature_flag_user_list, project: project, name: 'list_two') } + let_it_be(:user_list_three) { create(:operations_feature_flag_user_list, project: project, name: 'list_three') } + + it 'returns a found name' do + lists = project.operations_feature_flags_user_lists.for_name_like('list') + + expect(lists).to contain_exactly(user_list_two, user_list_three) + end + + it 'returns an empty array when no lists match the query' do + lists = project.operations_feature_flags_user_lists.for_name_like('no match') + + expect(lists).to be_empty + end + end + it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } let(:instance) { build(:operations_feature_flag_user_list) } diff --git a/spec/models/packages/build_info_spec.rb b/spec/models/packages/build_info_spec.rb new file mode 100644 index 00000000000..a4369c56fe2 --- /dev/null +++ b/spec/models/packages/build_info_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::BuildInfo, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:package) } + it { is_expected.to belong_to(:pipeline) } + end +end diff --git a/spec/models/packages/package_file_build_info_spec.rb b/spec/models/packages/package_file_build_info_spec.rb new file mode 100644 index 00000000000..18d6e720bf8 --- /dev/null +++ b/spec/models/packages/package_file_build_info_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::PackageFileBuildInfo, type: :model do + describe 'relationships' do + it { is_expected.to belong_to(:package_file) } + it { is_expected.to belong_to(:pipeline) } + end +end diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb index 6b992dbc2a5..ef09fb037e9 100644 --- a/spec/models/packages/package_file_spec.rb +++ b/spec/models/packages/package_file_spec.rb @@ -5,6 +5,8 @@ RSpec.describe Packages::PackageFile, type: :model do 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) } end describe 'validations' do diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index 41a731b87e9..705a1991845 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -10,6 +10,8 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.to have_many(:package_files).dependent(:destroy) } it { is_expected.to have_many(:dependency_links).inverse_of(:package) } it { is_expected.to have_many(:tags).inverse_of(:package) } + it { is_expected.to have_many(:build_infos).inverse_of(:package) } + it { is_expected.to have_many(:pipelines).through(:build_infos) } it { is_expected.to have_one(:conan_metadatum).inverse_of(:package) } it { is_expected.to have_one(:maven_metadatum).inverse_of(:package) } it { is_expected.to have_one(:nuget_metadatum).inverse_of(:package) } @@ -171,6 +173,13 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) } end + context 'composer package' do + it_behaves_like 'validating version to be SemVer compliant for', :composer_package + + it { is_expected.to allow_value('dev-master').for(:version) } + it { is_expected.to allow_value('2.x-dev').for(:version) } + end + context 'maven package' do subject { build_stubbed(:maven_package) } @@ -573,7 +582,7 @@ RSpec.describe Packages::Package, type: :model do end describe '#pipeline' do - let_it_be(:package) { create(:maven_package) } + let_it_be_with_refind(:package) { create(:maven_package) } context 'package without pipeline' do it 'returns nil if there is no pipeline' do @@ -585,7 +594,7 @@ RSpec.describe Packages::Package, type: :model do let_it_be(:pipeline) { create(:ci_pipeline) } before do - package.create_build_info!(pipeline: pipeline) + package.build_infos.create!(pipeline: pipeline) end it 'returns the pipeline' do @@ -630,4 +639,23 @@ RSpec.describe Packages::Package, type: :model do end end end + + describe '#original_build_info' do + let_it_be_with_refind(:package) { create(:npm_package) } + + context 'without build_infos' do + it 'returns nil' do + expect(package.original_build_info).to be_nil + end + end + + context 'with build_infos' do + let_it_be(:first_build_info) { create(:package_build_info, :with_pipeline, package: package) } + let_it_be(:second_build_info) { create(:package_build_info, :with_pipeline, package: package) } + + it 'returns the first build info' do + expect(package.original_build_info).to eq(first_build_info) + end + end + end end diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb index cb1938a0113..f8ebc237577 100644 --- a/spec/models/pages/lookup_path_spec.rb +++ b/spec/models/pages/lookup_path_spec.rb @@ -3,15 +3,14 @@ require 'spec_helper' RSpec.describe Pages::LookupPath do - let_it_be(:project) do - create(:project, :pages_private, pages_https_only: true) - end + let(:project) { create(:project, :pages_private, pages_https_only: true) } subject(:lookup_path) { described_class.new(project) } before do stub_pages_setting(access_control: true, external_https: ["1.1.1.1:443"]) stub_artifacts_object_storage + stub_pages_object_storage(::Pages::DeploymentUploader) end describe '#project_id' do @@ -47,18 +46,78 @@ RSpec.describe Pages::LookupPath do end describe '#source' do - shared_examples 'uses disk storage' do - it 'sets the source type to "file"' do - expect(lookup_path.source[:type]).to eq('file') - end + let(:source) { lookup_path.source } - it 'sets the source path to the project full path suffixed with "public/' do - expect(lookup_path.source[:path]).to eq(project.full_path + "/public/") + 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 end include_examples 'uses disk storage' + context 'when there is pages deployment' do + let(:deployment) { create(:pages_deployment, project: project) } + + before do + project.mark_pages_as_deployed + project.pages_metadatum.update!(pages_deployment: deployment) + end + + it 'uses deployment from object storage' do + Timecop.freeze do + expect(source).to( + eq({ + type: 'zip', + path: deployment.file.url(expire_at: 1.day.from_now), + global_id: "gid://gitlab/PagesDeployment/#{deployment.id}", + sha256: deployment.file_sha256, + file_size: deployment.size, + file_count: deployment.file_count + }) + ) + end + end + + context 'when deployment is in the local storage' do + before do + deployment.file.migrate!(::ObjectStorage::Store::LOCAL) + end + + it 'uses file protocol' do + Timecop.freeze do + expect(source).to( + eq({ + type: 'zip', + path: 'file://' + deployment.file.path, + global_id: "gid://gitlab/PagesDeployment/#{deployment.id}", + sha256: deployment.file_sha256, + file_size: deployment.size, + file_count: deployment.file_count + }) + ) + 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 pages_serve_from_deployments feature flag is disabled' do + before do + stub_feature_flags(pages_serve_from_deployments: false) + end + + include_examples 'uses disk storage' + end + end + context 'when artifact_id from build job is present in pages metadata' do let(:artifacts_archive) { create(:ci_job_artifact, :zip, :remote_store, project: project) } @@ -66,26 +125,51 @@ RSpec.describe Pages::LookupPath do project.mark_pages_as_deployed(artifacts_archive: artifacts_archive) end - it 'sets the source type to "zip"' do - expect(lookup_path.source[:type]).to eq('zip') - end - - it 'sets the source path to the artifacts archive URL' do + it 'uses artifacts object storage' do Timecop.freeze do - expect(lookup_path.source[:path]).to eq(artifacts_archive.file.url(expire_at: 1.day.from_now)) - expect(lookup_path.source[:path]).to include("Expires=86400") + expect(source).to( + eq({ + type: 'zip', + path: artifacts_archive.file.url(expire_at: 1.day.from_now), + global_id: "gid://gitlab/Ci::JobArtifact/#{artifacts_archive.id}", + sha256: artifacts_archive.file_sha256, + file_size: artifacts_archive.size, + file_count: nil + }) + ) end end context 'when artifact is not uploaded to object storage' do let(:artifacts_archive) { create(:ci_job_artifact, :zip) } - include_examples 'uses disk storage' + it 'uses file protocol', :aggregate_failures do + Timecop.freeze do + expect(source).to( + eq({ + type: 'zip', + path: 'file://' + artifacts_archive.file.path, + global_id: "gid://gitlab/Ci::JobArtifact/#{artifacts_archive.id}", + sha256: artifacts_archive.file_sha256, + file_size: artifacts_archive.size, + file_count: nil + }) + ) + 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 feature flag is disabled' do before do - stub_feature_flags(pages_artifacts_archive: false) + stub_feature_flags(pages_serve_from_artifacts_archive: false) end include_examples 'uses disk storage' diff --git a/spec/models/pages_deployment_spec.rb b/spec/models/pages_deployment_spec.rb index 5d26ade740e..e83cbc15004 100644 --- a/spec/models/pages_deployment_spec.rb +++ b/spec/models/pages_deployment_spec.rb @@ -10,8 +10,15 @@ RSpec.describe PagesDeployment do describe 'validations' do it { is_expected.to validate_presence_of(:file) } + it { is_expected.to validate_presence_of(:size) } it { is_expected.to validate_numericality_of(:size).only_integer.is_greater_than(0) } + + it { is_expected.to validate_presence_of(:file_count) } + it { is_expected.to validate_numericality_of(:file_count).only_integer.is_greater_than_or_equal_to(0) } + + it { is_expected.to validate_presence_of(:file_sha256) } + it { is_expected.to validate_inclusion_of(:file_store).in_array(ObjectStorage::SUPPORTED_STORES) } it 'is valid when created from the factory' do @@ -20,14 +27,26 @@ RSpec.describe PagesDeployment do end describe 'default for file_store' do + let(:project) { create(:project) } + let(:deployment) do + filepath = Rails.root.join("spec/fixtures/pages.zip") + + described_class.create!( + project: project, + file: fixture_file_upload(filepath), + file_sha256: Digest::SHA256.file(filepath).hexdigest, + file_count: 3 + ) + end + it 'uses local store when object storage is not enabled' do - expect(build(:pages_deployment).file_store).to eq(ObjectStorage::Store::LOCAL) + expect(deployment.file_store).to eq(ObjectStorage::Store::LOCAL) end it 'uses remote store when object storage is enabled' do stub_pages_object_storage(::Pages::DeploymentUploader) - expect(build(:pages_deployment).file_store).to eq(ObjectStorage::Store::REMOTE) + expect(deployment.file_store).to eq(ObjectStorage::Store::REMOTE) end end @@ -35,4 +54,17 @@ RSpec.describe PagesDeployment do deployment = create(:pages_deployment) expect(deployment.size).to eq(deployment.file.size) end + + describe '.older_than' do + it 'returns deployments with lower id' do + old_deployments = create_list(:pages_deployment, 2) + + deployment = create(:pages_deployment) + + # new deployment + create(:pages_deployment) + + expect(PagesDeployment.older_than(deployment.id)).to eq(old_deployments) + end + end end diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index 9e80d0e0886..67ecbe13c1a 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -31,6 +31,18 @@ RSpec.describe PersonalAccessToken do expect(described_class.for_user(user_1)).to contain_exactly(token_of_user_1) end end + + describe '.for_users' do + it 'returns personal access tokens for the specified users only' do + user_1 = create(:user) + user_2 = create(:user) + token_of_user_1 = create(:personal_access_token, user: user_1) + token_of_user_2 = create(:personal_access_token, user: user_2) + create_list(:personal_access_token, 3) + + expect(described_class.for_users([user_1, user_2])).to contain_exactly(token_of_user_1, token_of_user_2) + end + end end describe ".active?" do diff --git a/spec/models/personal_snippet_spec.rb b/spec/models/personal_snippet_spec.rb index 234f6e4b4b5..212605445ff 100644 --- a/spec/models/personal_snippet_spec.rb +++ b/spec/models/personal_snippet_spec.rb @@ -20,9 +20,8 @@ RSpec.describe PersonalSnippet do it_behaves_like 'model with repository' do let_it_be(:container) { create(:personal_snippet, :repository) } let(:stubbed_container) { build_stubbed(:personal_snippet) } - let(:expected_full_path) { "@snippets/#{container.id}" } + let(:expected_full_path) { "snippets/#{container.id}" } let(:expected_web_url_path) { "-/snippets/#{container.id}" } - let(:expected_repo_url_path) { "snippets/#{container.id}" } end describe '#parent_user' do diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index 3bcbf6b9e1b..3d1c87771f3 100644 --- a/spec/models/project_snippet_spec.rb +++ b/spec/models/project_snippet_spec.rb @@ -36,8 +36,7 @@ RSpec.describe ProjectSnippet do it_behaves_like 'model with repository' do let_it_be(:container) { create(:project_snippet, :repository) } let(:stubbed_container) { build_stubbed(:project_snippet) } - let(:expected_full_path) { "#{container.project.full_path}/@snippets/#{container.id}" } + let(:expected_full_path) { "#{container.project.full_path}/snippets/#{container.id}" } let(:expected_web_url_path) { "#{container.project.full_path}/-/snippets/#{container.id}" } - let(:expected_repo_url_path) { "#{container.project.full_path}/snippets/#{container.id}" } end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 53a213891e9..c8b96963d5d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2,12 +2,14 @@ require 'spec_helper' -RSpec.describe Project do +RSpec.describe Project, factory_default: :keep do include ProjectForksHelper include GitHelpers include ExternalAuthorizationServiceHelpers using RSpec::Parameterized::TableSyntax + let_it_be(:namespace) { create_default(:namespace) } + it_behaves_like 'having unique enum values' describe 'associations' do @@ -3003,14 +3005,23 @@ RSpec.describe Project do describe '#set_repository_read_only!' do let(:project) { create(:project) } - it 'returns true when there is no existing git transfer in progress' do - expect(project.set_repository_read_only!).to be_truthy + it 'makes the repository read-only' do + expect { project.set_repository_read_only! } + .to change(project, :repository_read_only?) + .from(false) + .to(true) end - it 'returns false when there is an existing git transfer in progress' do + it 'raises an error if the project is already read-only' do + project.set_repository_read_only! + + expect { project.set_repository_read_only! }.to raise_error(described_class::RepositoryReadOnlyError, /already read-only/) + end + + it 'raises an error when there is an existing git transfer in progress' do allow(project).to receive(:git_transfer_in_progress?) { true } - expect(project.set_repository_read_only!).to be_falsey + expect { project.set_repository_read_only! }.to raise_error(described_class::RepositoryReadOnlyError, /in progress/) end end @@ -3657,7 +3668,7 @@ RSpec.describe Project do let(:project) { create(:project) } before do - project.namespace_id = 7 + project.namespace_id = project.namespace_id + 1 end it { expect(project.parent_changed?).to be_truthy } @@ -3985,8 +3996,16 @@ RSpec.describe Project do context 'when feature is private' do let(:project) { create(:project, :public, :merge_requests_private) } - it 'returns projects with the project feature private' do - is_expected.to include(project) + context 'when admin mode is enabled', :enable_admin_mode do + it 'returns projects with the project feature private' do + is_expected.to include(project) + end + end + + context 'when admin mode is disabled' do + it 'does not return projects with the project feature private' do + is_expected.not_to include(project) + end end end end @@ -4009,7 +4028,7 @@ RSpec.describe Project do end end - describe '.filter_by_feature_visibility', :enable_admin_mode do + describe '.filter_by_feature_visibility' do include_context 'ProjectPolicyTable context' include ProjectHelpers using RSpec::Parameterized::TableSyntax @@ -4021,12 +4040,13 @@ RSpec.describe Project do context 'reporter level access' do let(:feature) { MergeRequest } - where(:project_level, :feature_access_level, :membership, :expected_count) do + where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do permission_table_for_reporter_feature_access end with_them do it "respects visibility" do + enable_admin_mode!(user) if admin_mode update_feature_access_level(project, feature_access_level) expected_objects = expected_count == 1 ? [project] : [] @@ -4041,12 +4061,13 @@ RSpec.describe Project do context 'issues' do let(:feature) { Issue } - where(:project_level, :feature_access_level, :membership, :expected_count) do + where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do permission_table_for_guest_feature_access end with_them do it "respects visibility" do + enable_admin_mode!(user) if admin_mode update_feature_access_level(project, feature_access_level) expected_objects = expected_count == 1 ? [project] : [] @@ -4061,12 +4082,13 @@ RSpec.describe Project do context 'wiki' do let(:feature) { :wiki } - where(:project_level, :feature_access_level, :membership, :expected_count) do + where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do permission_table_for_guest_feature_access end with_them do it "respects visibility" do + enable_admin_mode!(user) if admin_mode update_feature_access_level(project, feature_access_level) expected_objects = expected_count == 1 ? [project] : [] @@ -4081,12 +4103,13 @@ RSpec.describe Project do context 'code' do let(:feature) { :repository } - where(:project_level, :feature_access_level, :membership, :expected_count) do + where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do permission_table_for_guest_feature_access_and_non_private_project_only end with_them do it "respects visibility" do + enable_admin_mode!(user) if admin_mode update_feature_access_level(project, feature_access_level) expected_objects = expected_count == 1 ? [project] : [] @@ -4208,6 +4231,27 @@ RSpec.describe Project do expect { project.destroy }.not_to raise_error end + + context 'when there is an old pages deployment' do + let!(:old_deployment_from_another_project) { create(:pages_deployment) } + let!(:old_deployment) { create(:pages_deployment, project: project) } + + it 'schedules a destruction of pages deployments' do + expect(DestroyPagesDeploymentsWorker).to( + receive(:perform_async).with(project.id) + ) + + project.remove_pages + end + + it 'removes pages deployments', :sidekiq_inline do + expect do + project.remove_pages + end.to change { PagesDeployment.count }.by(-1) + + expect(PagesDeployment.find_by_id(old_deployment.id)).to be_nil + end + end end describe '#remove_export' do @@ -5507,15 +5551,16 @@ RSpec.describe Project do end describe '#find_or_initialize_services' do - it 'returns only enabled services' do + before do allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity]) - allow(Service).to receive(:project_specific_services_names).and_return(%w[asana]) allow(subject).to receive(:disabled_services).and_return(%w[prometheus]) + end + it 'returns only enabled services' do services = subject.find_or_initialize_services - expect(services.count).to eq(3) - expect(services.map(&:title)).to eq(['Asana', 'JetBrains TeamCity CI', 'Pushover']) + expect(services.count).to eq(2) + expect(services.map(&:title)).to eq(['JetBrains TeamCity CI', 'Pushover']) end end @@ -5895,6 +5940,26 @@ RSpec.describe Project do end end + describe '#update_pages_deployment!' do + let(:project) { create(:project) } + let(:deployment) { create(:pages_deployment, project: project) } + + it "creates new metadata record if none exists yet and sets deployment" do + project.pages_metadatum.destroy! + project.reload + + project.update_pages_deployment!(deployment) + + expect(project.pages_metadatum.pages_deployment).to eq(deployment) + end + + it "updates the existing metadara record with deployment" do + expect do + project.update_pages_deployment!(deployment) + end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil).to(deployment) + end + end + describe '#has_pool_repsitory?' do it 'returns false when it does not have a pool repository' do subject = create(:project, :repository) diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb index 9f40dbb3401..2d283766edb 100644 --- a/spec/models/project_statistics_spec.rb +++ b/spec/models/project_statistics_spec.rb @@ -34,7 +34,8 @@ RSpec.describe ProjectStatistics do lfs_objects_size: 2.exabytes, build_artifacts_size: 1.exabyte, snippets_size: 1.exabyte, - pipeline_artifacts_size: 1.exabyte - 1 + pipeline_artifacts_size: 512.petabytes - 1, + uploads_size: 512.petabytes ) statistics.reload @@ -46,7 +47,8 @@ RSpec.describe ProjectStatistics do expect(statistics.build_artifacts_size).to eq(1.exabyte) expect(statistics.storage_size).to eq(8.exabytes - 1) expect(statistics.snippets_size).to eq(1.exabyte) - expect(statistics.pipeline_artifacts_size).to eq(1.exabyte - 1) + expect(statistics.pipeline_artifacts_size).to eq(512.petabytes - 1) + expect(statistics.uploads_size).to eq(512.petabytes) end end @@ -57,6 +59,7 @@ RSpec.describe ProjectStatistics do statistics.lfs_objects_size = 3 statistics.build_artifacts_size = 4 statistics.snippets_size = 5 + statistics.uploads_size = 3 expect(statistics.total_repository_size).to eq 5 end @@ -98,6 +101,7 @@ RSpec.describe ProjectStatistics do allow(statistics).to receive(:update_lfs_objects_size) allow(statistics).to receive(:update_snippets_size) allow(statistics).to receive(:update_storage_size) + allow(statistics).to receive(:update_uploads_size) end context "without arguments" do @@ -111,6 +115,7 @@ RSpec.describe ProjectStatistics do expect(statistics).to have_received(:update_wiki_size) expect(statistics).to have_received(:update_lfs_objects_size) expect(statistics).to have_received(:update_snippets_size) + expect(statistics).to have_received(:update_uploads_size) end end @@ -125,6 +130,7 @@ RSpec.describe ProjectStatistics do expect(statistics).not_to have_received(:update_repository_size) expect(statistics).not_to have_received(:update_wiki_size) expect(statistics).not_to have_received(:update_snippets_size) + expect(statistics).not_to have_received(:update_uploads_size) end end @@ -139,10 +145,12 @@ RSpec.describe ProjectStatistics do expect(statistics).to have_received(:update_repository_size) expect(statistics).to have_received(:update_wiki_size) expect(statistics).to have_received(:update_snippets_size) + expect(statistics).to have_received(:update_uploads_size) expect(statistics.repository_size).to eq(0) expect(statistics.commit_count).to eq(0) expect(statistics.wiki_size).to eq(0) expect(statistics.snippets_size).to eq(0) + expect(statistics.uploads_size).to eq(0) end end @@ -163,10 +171,12 @@ RSpec.describe ProjectStatistics do expect(statistics).to have_received(:update_repository_size) expect(statistics).to have_received(:update_wiki_size) expect(statistics).to have_received(:update_snippets_size) + expect(statistics).to have_received(:update_uploads_size) expect(statistics.repository_size).to eq(0) expect(statistics.commit_count).to eq(0) expect(statistics.wiki_size).to eq(0) expect(statistics.snippets_size).to eq(0) + expect(statistics.uploads_size).to eq(0) end end @@ -211,6 +221,7 @@ RSpec.describe ProjectStatistics do expect(statistics).not_to receive(:update_wiki_size) expect(statistics).not_to receive(:update_lfs_objects_size) expect(statistics).not_to receive(:update_snippets_size) + expect(statistics).not_to receive(:update_uploads_size) expect(statistics).not_to receive(:save!) expect(Namespaces::ScheduleAggregationWorker) .not_to receive(:perform_async) @@ -295,6 +306,26 @@ RSpec.describe ProjectStatistics do end end + describe '#update_uploads_size' do + let!(:upload1) { create(:upload, model: project, size: 1.megabyte) } + let!(:upload2) { create(:upload, model: project, size: 2.megabytes) } + + it 'stores the size of related uploaded files' do + expect(statistics.update_uploads_size).to eq(3.megabytes) + end + + context 'with feature flag disabled' do + before do + statistics.update_columns(uploads_size: 0) + stub_feature_flags(count_uploads_size_in_storage_stats: false) + end + + it 'does not store the size of related uploaded files' do + expect(statistics.update_uploads_size).to eq(0) + end + end + end + describe '#update_storage_size' do it "sums all storage counters" do statistics.update!( @@ -302,12 +333,13 @@ RSpec.describe ProjectStatistics do wiki_size: 4, lfs_objects_size: 3, snippets_size: 2, - pipeline_artifacts_size: 3 + pipeline_artifacts_size: 3, + uploads_size: 5 ) statistics.reload - expect(statistics.storage_size).to eq 14 + expect(statistics.storage_size).to eq 19 end it 'works during wiki_size backfill' do diff --git a/spec/models/protected_branch/push_access_level_spec.rb b/spec/models/protected_branch/push_access_level_spec.rb index 77fe9814c86..0aba51ea567 100644 --- a/spec/models/protected_branch/push_access_level_spec.rb +++ b/spec/models/protected_branch/push_access_level_spec.rb @@ -4,4 +4,34 @@ require 'spec_helper' RSpec.describe ProtectedBranch::PushAccessLevel do it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MAINTAINER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) } + + describe 'associations' do + it { is_expected.to belong_to(:deploy_key) } + end + + describe 'validations' do + it 'is not valid when a record exists with the same access level' do + protected_branch = create(:protected_branch) + create(:protected_branch_push_access_level, protected_branch: protected_branch) + level = build(:protected_branch_push_access_level, protected_branch: protected_branch) + + expect(level).to be_invalid + end + + it 'is not valid when a record exists with the same access level' do + protected_branch = create(:protected_branch) + deploy_key = create(:deploy_key, projects: [protected_branch.project]) + create(:protected_branch_push_access_level, protected_branch: protected_branch, deploy_key: deploy_key) + level = build(:protected_branch_push_access_level, protected_branch: protected_branch, deploy_key: deploy_key) + + expect(level).to be_invalid + end + + it 'checks that a deploy key is enabled for the same project as the protected branch\'s' do + level = build(:protected_branch_push_access_level, deploy_key: create(:deploy_key)) + + expect { level.save! }.to raise_error + expect(level.errors.full_messages).to contain_exactly('Deploy key is not enabled for this project') + end + end end diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index 0f1637016d6..eb81db95cd3 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -62,6 +62,15 @@ RSpec.describe Route do end end + describe '.for_routable_type' do + let!(:nested_group) { create(:group, path: 'foo', name: 'foo', parent: group) } + let!(:project) { create(:project, path: 'other-project') } + + it 'returns correct routes' do + expect(described_class.for_routable_type(Project.name)).to match_array([project.route]) + end + end + describe '#rename_descendants' do let!(:nested_group) { create(:group, path: 'test', name: 'test', parent: group) } let!(:deep_nested_group) { create(:group, path: 'foo', name: 'foo', parent: nested_group) } diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index db3cf19a03f..402c1a3d19b 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -3,6 +3,8 @@ 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) } @@ -15,8 +17,6 @@ RSpec.describe Service do end describe 'validations' do - using RSpec::Parameterized::TableSyntax - it { is_expected.to validate_presence_of(:type) } where(:project_id, :group_id, :template, :instance, :valid) do @@ -208,27 +208,27 @@ RSpec.describe Service do end end - describe '.find_or_initialize_integration' do + 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_integration('jira', group_id: group)).to eq(service1) + 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_integration('redmine', group_id: group) }.not_to change { Service.count } + 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' do + 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(Service.for_instance).pluck(:type)).to match_array(Service.available_services_types) + 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(Service.for_instance) }.not_to change { Service.count } + expect { Service.find_or_initialize_all_non_project_specific(Service.for_instance) }.not_to change { Service.count } end end @@ -237,7 +237,7 @@ RSpec.describe Service do context 'with all existing instances' do before do Service.insert_all( - Service.available_services_types.map { |type| { instance: true, type: type } } + Service.available_services_types(include_project_specific: false).map { |type| { instance: true, type: type } } ) end @@ -265,13 +265,13 @@ RSpec.describe Service do 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) + 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.size) + 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' @@ -279,7 +279,7 @@ RSpec.describe Service do context 'with all existing templates' do before do Service.insert_all( - Service.available_services_types.map { |type| { template: true, type: type } } + Service.available_services_types(include_project_specific: false).map { |type| { template: true, type: type } } ) end @@ -305,7 +305,7 @@ RSpec.describe Service do 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.size) + 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' @@ -599,6 +599,23 @@ RSpec.describe Service do 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( @@ -826,5 +843,78 @@ RSpec.describe Service do 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_issue_tracker?' do + where(:category, :active, :result) do + :issue_tracker | true | true + :issue_tracker | false | false + :common | true | false + end + + with_them do + it 'returns the right result' do + expect(build(:service, category: category, active: active).external_issue_tracker?).to eq(result) + 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 end end diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb index 608c5bdf03a..ca8fe4cca3b 100644 --- a/spec/models/terraform/state_spec.rb +++ b/spec/models/terraform/state_spec.rb @@ -97,10 +97,11 @@ RSpec.describe Terraform::State do end describe '#update_file!' do - let(:version) { 3 } - let(:data) { Hash[terraform_version: '0.12.21'].to_json } + let_it_be(:build) { create(:ci_build) } + let_it_be(:version) { 3 } + let_it_be(:data) { Hash[terraform_version: '0.12.21'].to_json } - subject { terraform_state.update_file!(CarrierWaveStringFile.new(data), version: version) } + subject { terraform_state.update_file!(CarrierWaveStringFile.new(data), version: version, build: build) } context 'versioning is enabled' do let(:terraform_state) { create(:terraform_state) } @@ -109,6 +110,7 @@ RSpec.describe Terraform::State do expect { subject }.to change { Terraform::StateVersion.count } expect(terraform_state.latest_version.version).to eq(version) + expect(terraform_state.latest_version.build).to eq(build) expect(terraform_state.latest_version.file.read).to eq(data) end end diff --git a/spec/models/terraform/state_version_spec.rb b/spec/models/terraform/state_version_spec.rb index cc5ea87159d..97ac77d5e7b 100644 --- a/spec/models/terraform/state_version_spec.rb +++ b/spec/models/terraform/state_version_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Terraform::StateVersion do it { is_expected.to belong_to(:terraform_state).required } it { is_expected.to belong_to(:created_by_user).class_name('User').optional } + it { is_expected.to belong_to(:build).class_name('Ci::Build').optional } describe 'scopes' do describe '.ordered_by_version_desc' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 64bff5d00aa..19a6a3ce3c4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -319,7 +319,7 @@ RSpec.describe User do expect(subject).to validate_presence_of(:username) end - it 'rejects blacklisted names' do + it 'rejects denied names' do user = build(:user, username: 'dashboard') expect(user).not_to be_valid @@ -442,9 +442,9 @@ RSpec.describe User do end describe 'email' do - context 'when no signup domains whitelisted' do + context 'when no signup domains allowed' do before do - allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return([]) + allow_any_instance_of(ApplicationSetting).to receive(:domain_allowlist).and_return([]) end it 'accepts any email' do @@ -455,7 +455,7 @@ RSpec.describe User do context 'bad regex' do before do - allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['([a-zA-Z0-9]+)+\.com']) + allow_any_instance_of(ApplicationSetting).to receive(:domain_allowlist).and_return(['([a-zA-Z0-9]+)+\.com']) end it 'does not hang on evil input' do @@ -467,9 +467,9 @@ RSpec.describe User do end end - context 'when a signup domain is whitelisted and subdomains are allowed' do + context 'when a signup domain is allowed and subdomains are allowed' do before do - allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['example.com', '*.example.com']) + allow_any_instance_of(ApplicationSetting).to receive(:domain_allowlist).and_return(['example.com', '*.example.com']) end it 'accepts info@example.com' do @@ -488,9 +488,9 @@ RSpec.describe User do end end - context 'when a signup domain is whitelisted and subdomains are not allowed' do + context 'when a signup domain is allowed and subdomains are not allowed' do before do - allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['example.com']) + allow_any_instance_of(ApplicationSetting).to receive(:domain_allowlist).and_return(['example.com']) end it 'accepts info@example.com' do @@ -514,15 +514,15 @@ RSpec.describe User do end end - context 'domain blacklist' do + context 'domain denylist' do before do - allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist_enabled?).and_return(true) - allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['example.com']) + allow_any_instance_of(ApplicationSetting).to receive(:domain_denylist_enabled?).and_return(true) + allow_any_instance_of(ApplicationSetting).to receive(:domain_denylist).and_return(['example.com']) end context 'bad regex' do before do - allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['([a-zA-Z0-9]+)+\.com']) + allow_any_instance_of(ApplicationSetting).to receive(:domain_denylist).and_return(['([a-zA-Z0-9]+)+\.com']) end it 'does not hang on evil input' do @@ -534,7 +534,7 @@ RSpec.describe User do end end - context 'when a signup domain is blacklisted' do + context 'when a signup domain is denied' do it 'accepts info@test.com' do user = build(:user, email: 'info@test.com') expect(user).to be_valid @@ -551,13 +551,13 @@ RSpec.describe User do end end - context 'when a signup domain is blacklisted but a wildcard subdomain is allowed' do + context 'when a signup domain is denied but a wildcard subdomain is allowed' do before do - allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['test.example.com']) - allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['*.example.com']) + allow_any_instance_of(ApplicationSetting).to receive(:domain_denylist).and_return(['test.example.com']) + allow_any_instance_of(ApplicationSetting).to receive(:domain_allowlist).and_return(['*.example.com']) end - it 'gives priority to whitelist and allow info@test.example.com' do + it 'gives priority to allowlist and allow info@test.example.com' do user = build(:user, email: 'info@test.example.com') expect(user).to be_valid end @@ -565,7 +565,7 @@ RSpec.describe User do context 'with both lists containing a domain' do before do - allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['test.com']) + allow_any_instance_of(ApplicationSetting).to receive(:domain_allowlist).and_return(['test.com']) end it 'accepts info@test.com' do @@ -1740,6 +1740,16 @@ RSpec.describe User do end end + describe '.instance_access_request_approvers_to_be_notified' do + let_it_be(:admin_list) { create_list(:user, 12, :admin, :with_sign_ins) } + + it 'returns up to the ten most recently active instance admins' do + active_admins_in_recent_sign_in_desc_order = User.admins.active.order_recent_sign_in.limit(10) + + expect(User.instance_access_request_approvers_to_be_notified).to eq(active_admins_in_recent_sign_in_desc_order) + end + end + describe '.filter_items' do let(:user) { double } @@ -2906,6 +2916,34 @@ RSpec.describe User do subject { user.authorized_groups } it { is_expected.to contain_exactly private_group, project_group } + + context 'with shared memberships' do + let!(:shared_group) { create(:group) } + let!(:other_group) { create(:group) } + + before do + create(:group_group_link, shared_group: shared_group, shared_with_group: private_group) + create(:group_group_link, shared_group: private_group, shared_with_group: other_group) + end + + context 'when shared_group_membership_auth is enabled' do + before do + stub_feature_flags(shared_group_membership_auth: user) + end + + it { is_expected.to include shared_group } + it { is_expected.not_to include other_group } + end + + context 'when shared_group_membership_auth is disabled' do + before do + stub_feature_flags(shared_group_membership_auth: false) + end + + it { is_expected.not_to include shared_group } + it { is_expected.not_to include other_group } + end + end end describe '#membership_groups' do @@ -3637,9 +3675,9 @@ RSpec.describe User do end end - context 'when a domain whitelist is in place' do + context 'when a domain allowlist is in place' do before do - stub_application_setting(domain_whitelist: ['gitlab.com']) + stub_application_setting(domain_allowlist: ['gitlab.com']) end it 'creates a ghost user' do |