summaryrefslogtreecommitdiff
path: root/spec/models
diff options
context:
space:
mode:
Diffstat (limited to 'spec/models')
-rw-r--r--spec/models/ability_spec.rb4
-rw-r--r--spec/models/acts_as_taggable_on/tag_spec.rb16
-rw-r--r--spec/models/acts_as_taggable_on/tagging_spec.rb16
-rw-r--r--spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb9
-rw-r--r--spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb9
-rw-r--r--spec/models/blob_viewer/package_json_spec.rb52
-rw-r--r--spec/models/bulk_imports/entity_spec.rb9
-rw-r--r--spec/models/bulk_imports/file_transfer/project_config_spec.rb2
-rw-r--r--spec/models/chat_name_spec.rb8
-rw-r--r--spec/models/ci/bridge_spec.rb2
-rw-r--r--spec/models/ci/build_metadata_spec.rb12
-rw-r--r--spec/models/ci/build_spec.rb267
-rw-r--r--spec/models/ci/job_artifact_spec.rb62
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb2
-rw-r--r--spec/models/ci/pipeline_spec.rb18
-rw-r--r--spec/models/ci/runner_spec.rb53
-rw-r--r--spec/models/ci/trigger_spec.rb4
-rw-r--r--spec/models/clusters/applications/runner_spec.rb6
-rw-r--r--spec/models/clusters/cluster_spec.rb6
-rw-r--r--spec/models/commit_status_spec.rb16
-rw-r--r--spec/models/concerns/bulk_insert_safe_spec.rb24
-rw-r--r--spec/models/concerns/bulk_insertable_associations_spec.rb32
-rw-r--r--spec/models/concerns/cascading_namespace_setting_attribute_spec.rb15
-rw-r--r--spec/models/concerns/clusters/agents/authorization_config_scopes_spec.rb21
-rw-r--r--spec/models/concerns/database_reflection_spec.rb18
-rw-r--r--spec/models/concerns/has_integrations_spec.rb25
-rw-r--r--spec/models/concerns/legacy_bulk_insert_spec.rb103
-rw-r--r--spec/models/concerns/loaded_in_group_list_spec.rb58
-rw-r--r--spec/models/concerns/loose_foreign_key_spec.rb29
-rw-r--r--spec/models/concerns/noteable_spec.rb64
-rw-r--r--spec/models/concerns/prometheus_adapter_spec.rb8
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb2
-rw-r--r--spec/models/concerns/sha256_attribute_spec.rb2
-rw-r--r--spec/models/concerns/sha_attribute_spec.rb2
-rw-r--r--spec/models/concerns/where_composite_spec.rb2
-rw-r--r--spec/models/concerns/x509_serial_number_attribute_spec.rb2
-rw-r--r--spec/models/custom_emoji_spec.rb2
-rw-r--r--spec/models/customer_relations/contact_spec.rb3
-rw-r--r--spec/models/customer_relations/issue_contact_spec.rb48
-rw-r--r--spec/models/data_list_spec.rb31
-rw-r--r--spec/models/dependency_proxy/manifest_spec.rb21
-rw-r--r--spec/models/deploy_key_spec.rb11
-rw-r--r--spec/models/deployment_spec.rb81
-rw-r--r--spec/models/design_management/version_spec.rb2
-rw-r--r--spec/models/email_spec.rb11
-rw-r--r--spec/models/environment_spec.rb44
-rw-r--r--spec/models/error_tracking/error_event_spec.rb20
-rw-r--r--spec/models/error_tracking/error_spec.rb4
-rw-r--r--spec/models/event_spec.rb12
-rw-r--r--spec/models/fork_network_spec.rb6
-rw-r--r--spec/models/generic_commit_status_spec.rb2
-rw-r--r--spec/models/grafana_integration_spec.rb6
-rw-r--r--spec/models/group_spec.rb55
-rw-r--r--spec/models/hooks/project_hook_spec.rb2
-rw-r--r--spec/models/identity_spec.rb10
-rw-r--r--spec/models/integration_spec.rb18
-rw-r--r--spec/models/integrations/jira_spec.rb148
-rw-r--r--spec/models/integrations/pipelines_email_spec.rb38
-rw-r--r--spec/models/integrations/shimo_spec.rb41
-rw-r--r--spec/models/integrations/zentao_spec.rb6
-rw-r--r--spec/models/issue_spec.rb25
-rw-r--r--spec/models/jira_import_state_spec.rb4
-rw-r--r--spec/models/key_spec.rb4
-rw-r--r--spec/models/loose_foreign_keys/deleted_record_spec.rb35
-rw-r--r--spec/models/loose_foreign_keys/modification_tracker_spec.rb93
-rw-r--r--spec/models/member_spec.rb14
-rw-r--r--spec/models/members/member_task_spec.rb124
-rw-r--r--spec/models/members/project_member_spec.rb54
-rw-r--r--spec/models/merge_request_assignee_spec.rb4
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb16
-rw-r--r--spec/models/merge_request_diff_spec.rb20
-rw-r--r--spec/models/merge_request_reviewer_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb22
-rw-r--r--spec/models/namespace_spec.rb295
-rw-r--r--spec/models/namespaces/project_namespace_spec.rb2
-rw-r--r--spec/models/note_spec.rb22
-rw-r--r--spec/models/notification_setting_spec.rb12
-rw-r--r--spec/models/operations/feature_flags/strategy_spec.rb269
-rw-r--r--spec/models/operations/feature_flags/user_list_spec.rb21
-rw-r--r--spec/models/packages/npm/metadatum_spec.rb50
-rw-r--r--spec/models/packages/package_file_spec.rb23
-rw-r--r--spec/models/packages/package_spec.rb24
-rw-r--r--spec/models/pages_domain_spec.rb4
-rw-r--r--spec/models/preloaders/group_policy_preloader_spec.rb45
-rw-r--r--spec/models/preloaders/group_root_ancestor_preloader_spec.rb63
-rw-r--r--spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb49
-rw-r--r--spec/models/project_authorization_spec.rb21
-rw-r--r--spec/models/project_spec.rb113
-rw-r--r--spec/models/project_statistics_spec.rb4
-rw-r--r--spec/models/project_team_spec.rb14
-rw-r--r--spec/models/protectable_dropdown_spec.rb2
-rw-r--r--spec/models/redirect_route_spec.rb10
-rw-r--r--spec/models/release_spec.rb14
-rw-r--r--spec/models/remote_mirror_spec.rb10
-rw-r--r--spec/models/repository_spec.rb79
-rw-r--r--spec/models/route_spec.rb14
-rw-r--r--spec/models/sentry_issue_spec.rb2
-rw-r--r--spec/models/snippet_spec.rb6
-rw-r--r--spec/models/suggestion_spec.rb8
-rw-r--r--spec/models/u2f_registration_spec.rb28
-rw-r--r--spec/models/upload_spec.rb18
-rw-r--r--spec/models/uploads/fog_spec.rb27
-rw-r--r--spec/models/user_spec.rb138
-rw-r--r--spec/models/users/credit_card_validation_spec.rb18
-rw-r--r--spec/models/users/in_product_marketing_email_spec.rb3
-rw-r--r--spec/models/users/merge_request_interaction_spec.rb5
-rw-r--r--spec/models/users_statistics_spec.rb16
-rw-r--r--spec/models/webauthn_registration_spec.rb23
108 files changed, 2675 insertions, 798 deletions
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index e131661602e..bb8d476f257 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -425,9 +425,9 @@ RSpec.describe Ability do
expect(keys).to include(
:administrator,
'admin',
- "/dp/condition/BasePolicy/admin/#{user_b.id}"
+ "/dp/condition/BasePolicy/admin/User:#{user_b.id}"
)
- expect(keys).not_to include("/dp/condition/BasePolicy/admin/#{user_a.id}")
+ expect(keys).not_to include("/dp/condition/BasePolicy/admin/User:#{user_a.id}")
end
# regression spec for re-entrant admin condition checks
diff --git a/spec/models/acts_as_taggable_on/tag_spec.rb b/spec/models/acts_as_taggable_on/tag_spec.rb
new file mode 100644
index 00000000000..4b390bbd0bb
--- /dev/null
+++ b/spec/models/acts_as_taggable_on/tag_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActsAsTaggableOn::Tag do
+ it 'has the same connection as Ci::ApplicationRecord' do
+ query = 'select current_database()'
+
+ expect(described_class.connection.execute(query).first).to eq(Ci::ApplicationRecord.connection.execute(query).first)
+ expect(described_class.retrieve_connection.execute(query).first).to eq(Ci::ApplicationRecord.retrieve_connection.execute(query).first)
+ end
+
+ it 'has the same sticking as Ci::ApplicationRecord' do
+ expect(described_class.sticking).to eq(Ci::ApplicationRecord.sticking)
+ end
+end
diff --git a/spec/models/acts_as_taggable_on/tagging_spec.rb b/spec/models/acts_as_taggable_on/tagging_spec.rb
new file mode 100644
index 00000000000..4520a0aaf70
--- /dev/null
+++ b/spec/models/acts_as_taggable_on/tagging_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActsAsTaggableOn::Tagging do
+ it 'has the same connection as Ci::ApplicationRecord' do
+ query = 'select current_database()'
+
+ expect(described_class.connection.execute(query).first).to eq(Ci::ApplicationRecord.connection.execute(query).first)
+ expect(described_class.retrieve_connection.execute(query).first).to eq(Ci::ApplicationRecord.retrieve_connection.execute(query).first)
+ end
+
+ it 'has the same sticking as Ci::ApplicationRecord' do
+ expect(described_class.sticking).to eq(Ci::ApplicationRecord.sticking)
+ end
+end
diff --git a/spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb b/spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb
index c0d5b9203b8..ac17271ff99 100644
--- a/spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb
+++ b/spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb
@@ -9,5 +9,12 @@ RSpec.describe Analytics::CycleAnalytics::IssueStageEvent do
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:start_event_timestamp) }
- it_behaves_like 'StageEventModel'
+ it 'has state enum' do
+ expect(described_class.states).to eq(Issue.available_states)
+ end
+
+ it_behaves_like 'StageEventModel' do
+ let_it_be(:stage_event_factory) { :cycle_analytics_issue_stage_event }
+ let_it_be(:issuable_factory) { :issue }
+ end
end
diff --git a/spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb b/spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb
index 82a7e66d62a..bccc485d3f9 100644
--- a/spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb
+++ b/spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb
@@ -9,5 +9,12 @@ RSpec.describe Analytics::CycleAnalytics::MergeRequestStageEvent do
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:start_event_timestamp) }
- it_behaves_like 'StageEventModel'
+ it 'has state enum' do
+ expect(described_class.states).to eq(MergeRequest.available_states)
+ end
+
+ it_behaves_like 'StageEventModel' do
+ let_it_be(:stage_event_factory) { :cycle_analytics_merge_request_stage_event }
+ let_it_be(:issuable_factory) { :merge_request }
+ end
end
diff --git a/spec/models/blob_viewer/package_json_spec.rb b/spec/models/blob_viewer/package_json_spec.rb
index 8a394a7334f..1dcba3bcb4f 100644
--- a/spec/models/blob_viewer/package_json_spec.rb
+++ b/spec/models/blob_viewer/package_json_spec.rb
@@ -27,11 +27,55 @@ RSpec.describe BlobViewer::PackageJson do
end
end
- describe '#package_url' do
- it 'returns the package URL' do
- expect(subject).to receive(:prepare!)
+ context 'yarn' do
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ {
+ "name": "module-name",
+ "version": "10.3.1",
+ "engines": {
+ "yarn": "^2.4.0"
+ }
+ }
+ SPEC
+ end
+
+ let(:blob) { fake_blob(path: 'package.json', data: data) }
+
+ subject { described_class.new(blob) }
+
+ describe '#package_url' do
+ it 'returns the package URL', :aggregate_failures do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_url).to eq("https://yarnpkg.com/package/#{subject.package_name}")
+ end
+ end
- expect(subject.package_url).to eq("https://www.npmjs.com/package/#{subject.package_name}")
+ describe '#manager_url' do
+ it 'returns the manager URL', :aggregate_failures do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.manager_url).to eq("https://yarnpkg.com/")
+ end
+ end
+ end
+
+ context 'npm' do
+ describe '#package_url' do
+ it 'returns the package URL', :aggregate_failures do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_url).to eq("https://www.npmjs.com/package/#{subject.package_name}")
+ end
+ end
+
+ describe '#manager_url' do
+ it 'returns the manager URL', :aggregate_failures do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.manager_url).to eq("https://www.npmjs.com/")
+ end
end
end
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index 278d7f4bc56..cc66572cd6f 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -243,4 +243,13 @@ RSpec.describe BulkImports::Entity, type: :model do
end
end
end
+
+ describe '#relation_download_url_path' do
+ it 'returns export relations url with download query string' do
+ entity = build(:bulk_import_entity)
+
+ expect(entity.relation_download_url_path('test'))
+ .to eq("/groups/#{entity.encoded_source_full_path}/export_relations/download?relation=test")
+ end
+ end
end
diff --git a/spec/models/bulk_imports/file_transfer/project_config_spec.rb b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
index 3bd79333f0c..02151da583e 100644
--- a/spec/models/bulk_imports/file_transfer/project_config_spec.rb
+++ b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe BulkImports::FileTransfer::ProjectConfig do
describe '#tree_relation_definition_for' do
it 'returns relation definition' do
- expected = { service_desk_setting: { except: [:outgoing_name, :file_template_project_id], include: [] } }
+ expected = { service_desk_setting: { except: [:outgoing_name, :file_template_project_id], include: [], only: %i[project_id issue_template_key project_key] } }
expect(subject.tree_relation_definition_for('service_desk_setting')).to eq(expected)
end
diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb
index 9ed00003ac1..67e0f98d147 100644
--- a/spec/models/chat_name_spec.rb
+++ b/spec/models/chat_name_spec.rb
@@ -43,4 +43,12 @@ RSpec.describe ChatName do
expect(subject.last_used_at).to eq(time)
end
end
+
+ it_behaves_like 'it has loose foreign keys' do
+ let(:factory_name) { :chat_name }
+
+ before do
+ Ci::PipelineChatData # ensure that the referenced model is loaded
+ end
+ end
end
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index 8f1ae9c5f02..6fde55103f8 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -17,8 +17,6 @@ RSpec.describe Ci::Bridge do
{ trigger: { project: 'my/project', branch: 'master' } }
end
- it { is_expected.to respond_to(:runner_features) }
-
it 'has many sourced pipelines' do
expect(bridge).to have_many(:sourced_pipelines)
end
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index 069864fa765..b2ffb34da1d 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -121,4 +121,16 @@ RSpec.describe Ci::BuildMetadata do
end
end
end
+
+ describe 'set_cancel_gracefully' do
+ it 'sets cancel_gracefully' do
+ build.set_cancel_gracefully
+
+ expect(build.cancel_gracefully?).to be true
+ end
+
+ it 'returns false' do
+ expect(build.cancel_gracefully?).to be false
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 2ebf75a1d8a..b7de8ca4337 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -35,7 +35,8 @@ RSpec.describe Ci::Build do
it { is_expected.to respond_to(:has_trace?) }
it { is_expected.to respond_to(:trace) }
- it { is_expected.to respond_to(:runner_features) }
+ it { is_expected.to respond_to(:set_cancel_gracefully) }
+ it { is_expected.to respond_to(:cancel_gracefully?) }
it { is_expected.to delegate_method(:merge_request?).to(:pipeline) }
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
@@ -214,6 +215,26 @@ RSpec.describe Ci::Build do
end
end
+ describe '.license_management_jobs' do
+ subject { described_class.license_management_jobs }
+
+ let!(:management_build) { create(:ci_build, :success, name: :license_management) }
+ let!(:scanning_build) { create(:ci_build, :success, name: :license_scanning) }
+ let!(:another_build) { create(:ci_build, :success, name: :another_type) }
+
+ it 'returns license_scanning jobs' do
+ is_expected.to include(scanning_build)
+ end
+
+ it 'returns license_management jobs' do
+ is_expected.to include(management_build)
+ end
+
+ it 'doesnt return filtered out jobs' do
+ is_expected.not_to include(another_build)
+ end
+ end
+
describe '.finished_before' do
subject { described_class.finished_before(date) }
@@ -350,7 +371,7 @@ RSpec.describe Ci::Build do
it 'sticks the build if the status changed' do
job = create(:ci_build, :pending)
- expect(ApplicationRecord.sticking).to receive(:stick)
+ expect(described_class.sticking).to receive(:stick)
.with(:build, job.id)
job.update!(status: :running)
@@ -1290,7 +1311,7 @@ RSpec.describe Ci::Build do
end
end
- shared_examples_for 'state transition as a deployable' do
+ describe 'state transition as a deployable' do
subject { build.send(event) }
let!(:build) { create(:ci_build, :with_deployment, :start_review_app, project: project, pipeline: pipeline) }
@@ -1332,6 +1353,22 @@ RSpec.describe Ci::Build do
expect(deployment).to be_running
end
+
+ context 'when deployment is already running state' do
+ before do
+ build.deployment.success!
+ end
+
+ it 'does not change deployment status and tracks an error' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception).with(
+ instance_of(Deployment::StatusSyncError), deployment_id: deployment.id, build_id: build.id)
+
+ with_cross_database_modification_prevented do
+ expect { subject }.not_to change { deployment.reload.status }
+ end
+ end
+ end
end
context 'when transits to success' do
@@ -1399,36 +1436,6 @@ RSpec.describe Ci::Build do
end
end
- it_behaves_like 'state transition as a deployable' do
- context 'when transits to running' do
- let(:event) { :run! }
-
- context 'when deployment is already running state' do
- before do
- build.deployment.success!
- end
-
- it 'does not change deployment status and tracks an error' do
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception).with(
- instance_of(Deployment::StatusSyncError), deployment_id: deployment.id, build_id: build.id)
-
- with_cross_database_modification_prevented do
- expect { subject }.not_to change { deployment.reload.status }
- end
- end
- end
- end
- end
-
- context 'when update_deployment_after_transaction_commit feature flag is disabled' do
- before do
- stub_feature_flags(update_deployment_after_transaction_commit: false)
- end
-
- it_behaves_like 'state transition as a deployable'
- end
-
describe '#on_stop' do
subject { build.on_stop }
@@ -2759,7 +2766,10 @@ RSpec.describe Ci::Build do
let(:job_dependency_var) { { key: 'job_dependency', value: 'value', public: true, masked: false } }
before do
- allow(build).to receive(:predefined_variables) { [build_pre_var] }
+ allow_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder|
+ allow(builder).to receive(:predefined_variables) { [build_pre_var] }
+ end
+
allow(build).to receive(:yaml_variables) { [build_yaml_var] }
allow(build).to receive(:persisted_variables) { [] }
allow(build).to receive(:job_jwt_variables) { [job_jwt_var] }
@@ -3411,75 +3421,122 @@ RSpec.describe Ci::Build do
end
describe '#scoped_variables' do
- context 'when build has not been persisted yet' do
- let(:build) do
- described_class.new(
- name: 'rspec',
- stage: 'test',
- ref: 'feature',
- project: project,
- pipeline: pipeline,
- scheduling_type: :stage
- )
- end
+ before do
+ pipeline.clear_memoization(:predefined_vars_in_builder_enabled)
+ end
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') }
+ it 'records a prometheus metric' do
+ histogram = double(:histogram)
+ expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_builder_scoped_variables_histogram)
+ .and_return(histogram)
- it 'does not persist the build' do
- expect(build).to be_valid
- expect(build).not_to be_persisted
+ expect(histogram).to receive(:observe)
+ .with({}, a_kind_of(ActiveSupport::Duration))
- build.scoped_variables
+ build.scoped_variables
+ end
- expect(build).not_to be_persisted
- end
+ shared_examples 'calculates scoped_variables' do
+ context 'when build has not been persisted yet' do
+ let(:build) do
+ described_class.new(
+ name: 'rspec',
+ stage: 'test',
+ ref: 'feature',
+ project: project,
+ pipeline: pipeline,
+ scheduling_type: :stage
+ )
+ end
- it 'returns static predefined variables' do
- keys = %w[CI_JOB_NAME
- CI_COMMIT_SHA
- CI_COMMIT_SHORT_SHA
- CI_COMMIT_REF_NAME
- CI_COMMIT_REF_SLUG
- CI_JOB_STAGE]
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') }
- variables = build.scoped_variables
+ it 'does not persist the build' do
+ expect(build).to be_valid
+ expect(build).not_to be_persisted
- variables.map { |env| env[:key] }.tap do |names|
- expect(names).to include(*keys)
+ build.scoped_variables
+
+ expect(build).not_to be_persisted
end
- expect(variables)
- .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true, masked: false)
+ it 'returns static predefined variables' do
+ keys = %w[CI_JOB_NAME
+ CI_COMMIT_SHA
+ CI_COMMIT_SHORT_SHA
+ CI_COMMIT_REF_NAME
+ CI_COMMIT_REF_SLUG
+ CI_JOB_STAGE]
+
+ variables = build.scoped_variables
+
+ variables.map { |env| env[:key] }.tap do |names|
+ expect(names).to include(*keys)
+ end
+
+ expect(variables)
+ .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true, masked: false)
+ end
+
+ it 'does not return prohibited variables' do
+ keys = %w[CI_JOB_ID
+ CI_JOB_URL
+ CI_JOB_TOKEN
+ CI_BUILD_ID
+ CI_BUILD_TOKEN
+ CI_REGISTRY_USER
+ CI_REGISTRY_PASSWORD
+ CI_REPOSITORY_URL
+ CI_ENVIRONMENT_URL
+ CI_DEPLOY_USER
+ CI_DEPLOY_PASSWORD]
+
+ build.scoped_variables.map { |env| env[:key] }.tap do |names|
+ expect(names).not_to include(*keys)
+ end
+ end
end
- it 'does not return prohibited variables' do
- keys = %w[CI_JOB_ID
- CI_JOB_URL
- CI_JOB_TOKEN
- CI_BUILD_ID
- CI_BUILD_TOKEN
- CI_REGISTRY_USER
- CI_REGISTRY_PASSWORD
- CI_REPOSITORY_URL
- CI_ENVIRONMENT_URL
- CI_DEPLOY_USER
- CI_DEPLOY_PASSWORD]
+ context 'with dependency variables' do
+ let!(:prepare) { create(:ci_build, name: 'prepare', pipeline: pipeline, stage_idx: 0) }
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare'] }) }
+
+ let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) }
- build.scoped_variables.map { |env| env[:key] }.tap do |names|
- expect(names).not_to include(*keys)
+ it 'inherits dependent variables' do
+ expect(build.scoped_variables.to_hash).to include(job_variable.key => job_variable.value)
end
end
end
- context 'with dependency variables' do
- let!(:prepare) { create(:ci_build, name: 'prepare', pipeline: pipeline, stage_idx: 0) }
- let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare'] }) }
+ it_behaves_like 'calculates scoped_variables'
- let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) }
+ it 'delegates to the variable builders' do
+ expect_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder|
+ expect(builder)
+ .to receive(:scoped_variables).with(build, hash_including(:environment, :dependencies))
+ .and_call_original
- it 'inherits dependent variables' do
- expect(build.scoped_variables.to_hash).to include(job_variable.key => job_variable.value)
+ expect(builder).to receive(:predefined_variables).and_call_original
end
+
+ build.scoped_variables
+ end
+
+ context 'when ci builder feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_predefined_vars_in_builder: false)
+ end
+
+ it 'does not delegate to the variable builders' do
+ expect_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder|
+ expect(builder).not_to receive(:predefined_variables)
+ end
+
+ build.scoped_variables
+ end
+
+ it_behaves_like 'calculates scoped_variables'
end
end
@@ -3569,6 +3626,27 @@ RSpec.describe Ci::Build do
include_examples "secret CI variables"
end
+ describe '#kubernetes_variables' do
+ let(:build) { create(:ci_build) }
+ let(:service) { double(execute: template) }
+ let(:template) { double(to_yaml: 'example-kubeconfig', valid?: template_valid) }
+ let(:template_valid) { true }
+
+ subject { build.kubernetes_variables }
+
+ before do
+ allow(Ci::GenerateKubeconfigService).to receive(:new).with(build).and_return(service)
+ end
+
+ it { is_expected.to include(key: 'KUBECONFIG', value: 'example-kubeconfig', public: false, file: true) }
+
+ context 'generated config is invalid' do
+ let(:template_valid) { false }
+
+ it { is_expected.not_to include(key: 'KUBECONFIG', value: 'example-kubeconfig', public: false, file: true) }
+ end
+ end
+
describe '#deployment_variables' do
let(:build) { create(:ci_build, environment: environment) }
let(:environment) { 'production' }
@@ -3728,7 +3806,7 @@ RSpec.describe Ci::Build do
it 'ensures that it is not run in database transaction' do
expect(job.pipeline.persistent_ref).to receive(:create) do
- expect(Gitlab::Database.main).not_to be_inside_transaction
+ expect(ApplicationRecord).not_to be_inside_transaction
end
run_job_without_exception
@@ -5326,4 +5404,23 @@ RSpec.describe Ci::Build do
create(:ci_build)
end
end
+
+ describe '#runner_features' do
+ subject do
+ build.save!
+ build.cancel_gracefully?
+ end
+
+ let_it_be(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'cannot cancel gracefully' do
+ expect(subject).to be false
+ end
+
+ it 'can cancel gracefully' do
+ build.set_cancel_gracefully
+
+ expect(subject).to be true
+ end
+ end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index a94a1dd284a..d63f87e8943 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -351,6 +351,21 @@ RSpec.describe Ci::JobArtifact do
end
end
+ context 'when updating any field except the file' do
+ let(:artifact) { create(:ci_job_artifact, :unarchived_trace_artifact, file_store: 2) }
+
+ before do
+ stub_artifacts_object_storage(direct_upload: true)
+ artifact.file.object_store = 1
+ end
+
+ it 'the `after_commit` hook does not update `file_store`' do
+ artifact.update!(expire_at: Time.current)
+
+ expect(artifact.file_store).to be(2)
+ end
+ end
+
describe 'validates file format' do
subject { artifact }
@@ -507,6 +522,53 @@ RSpec.describe Ci::JobArtifact do
end
end
+ describe '#store_after_commit?' do
+ let(:file_type) { :archive }
+ let(:artifact) { build(:ci_job_artifact, file_type) }
+
+ context 'when direct upload is enabled' do
+ before do
+ stub_artifacts_object_storage(direct_upload: true)
+ end
+
+ context 'when the artifact is a trace' do
+ let(:file_type) { :trace }
+
+ context 'when ci_store_trace_outside_transaction is enabled' do
+ it 'returns true' do
+ expect(artifact.store_after_commit?).to be_truthy
+ end
+ end
+
+ context 'when ci_store_trace_outside_transaction is disabled' do
+ before do
+ stub_feature_flags(ci_store_trace_outside_transaction: false)
+ end
+
+ it 'returns false' do
+ expect(artifact.store_after_commit?).to be_falsey
+ end
+ end
+ end
+
+ context 'when the artifact is not a trace' do
+ it 'returns false' do
+ expect(artifact.store_after_commit?).to be_falsey
+ end
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ stub_artifacts_object_storage(direct_upload: false)
+ end
+
+ it 'returns false' do
+ expect(artifact.store_after_commit?).to be_falsey
+ end
+ end
+ end
+
describe 'file is being stored' do
subject { create(:ci_job_artifact, :archive) }
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index c7e1fe91b1e..fee74f8f674 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::PipelineSchedule do
- let_it_be(:project) { create_default(:project) }
+ let_it_be_with_reload(:project) { create_default(:project) }
subject { build(:ci_pipeline_schedule) }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 5f3aad0ab24..e573a6ef780 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -3361,7 +3361,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
shared_examples 'sending a notification' do
it 'sends an email', :sidekiq_might_not_need_inline do
- should_only_email(pipeline.user, kind: :bcc)
+ should_only_email(pipeline.user)
end
end
@@ -4595,4 +4595,20 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
end
+
+ describe '#authorized_cluster_agents' do
+ let(:pipeline) { create(:ci_empty_pipeline, :created) }
+ let(:agent) { instance_double(Clusters::Agent) }
+ let(:authorization) { instance_double(Clusters::Agents::GroupAuthorization, agent: agent) }
+ let(:finder) { double(execute: [authorization]) }
+
+ it 'retrieves agent records from the finder and caches the result' do
+ expect(Clusters::AgentAuthorizationsFinder).to receive(:new).once
+ .with(pipeline.project)
+ .and_return(finder)
+
+ expect(pipeline.authorized_cluster_agents).to contain_exactly(agent)
+ expect(pipeline.authorized_cluster_agents).to contain_exactly(agent) # cached
+ end
+ end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 826332268c5..2e79159cc60 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -5,6 +5,14 @@ require 'spec_helper'
RSpec.describe Ci::Runner do
it_behaves_like 'having unique enum values'
+ it_behaves_like 'it has loose foreign keys' do
+ let(:factory_name) { :ci_runner }
+
+ before do
+ Clusters::Applications::Runner # ensure that the referenced model is loaded
+ end
+ end
+
describe 'groups association' do
# Due to other assoctions such as projects this whole spec is allowed to
# generate cross-database queries. So we have this temporary spec to
@@ -44,7 +52,7 @@ RSpec.describe Ci::Runner do
let(:runner) { create(:ci_runner, :group, groups: [group]) }
it 'disallows assigning group if already assigned to a group' do
- runner.groups << build(:group)
+ runner.runner_namespaces << build(:ci_runner_namespace)
expect(runner).not_to be_valid
expect(runner.errors.full_messages).to include('Runner needs to be assigned to exactly one group')
@@ -397,7 +405,7 @@ RSpec.describe Ci::Runner do
it 'sticks the runner to the primary and calls the original method' do
runner = create(:ci_runner)
- expect(ApplicationRecord.sticking).to receive(:stick)
+ expect(described_class.sticking).to receive(:stick)
.with(:runner, runner.id)
expect(Gitlab::Workhorse).to receive(:set_key_and_notify)
@@ -618,7 +626,7 @@ RSpec.describe Ci::Runner do
end
describe '#status' do
- let(:runner) { create(:ci_runner, :instance, contacted_at: 1.second.ago) }
+ let(:runner) { build(:ci_runner, :instance) }
subject { runner.status }
@@ -630,6 +638,45 @@ RSpec.describe Ci::Runner do
it { is_expected.to eq(:not_connected) }
end
+ context 'inactive but online' do
+ before do
+ runner.contacted_at = 1.second.ago
+ runner.active = false
+ end
+
+ it { is_expected.to eq(:online) }
+ end
+
+ context 'contacted 1s ago' do
+ before do
+ runner.contacted_at = 1.second.ago
+ end
+
+ it { is_expected.to eq(:online) }
+ end
+
+ context 'contacted long time ago' do
+ before do
+ runner.contacted_at = 1.year.ago
+ end
+
+ it { is_expected.to eq(:offline) }
+ end
+ end
+
+ describe '#deprecated_rest_status' do
+ let(:runner) { build(:ci_runner, :instance, contacted_at: 1.second.ago) }
+
+ subject { runner.deprecated_rest_status }
+
+ context 'never connected' do
+ before do
+ runner.contacted_at = nil
+ end
+
+ it { is_expected.to eq(:not_connected) }
+ end
+
context 'contacted 1s ago' do
before do
runner.contacted_at = 1.second.ago
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 4ba6c6e50f7..c254279a32f 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -57,4 +57,8 @@ RSpec.describe Ci::Trigger do
it { is_expected.to eq(false) }
end
end
+
+ it_behaves_like 'includes Limitable concern' do
+ subject { build(:ci_trigger, owner: project.owner, project: project) }
+ end
end
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 788430d53d3..806c60d5aff 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -112,7 +112,7 @@ RSpec.describe Clusters::Applications::Runner do
subject
expect(runner).to be_group_type
- expect(runner.groups).to eq [group]
+ expect(runner.runner_namespaces.pluck(:namespace_id)).to match_array [group.id]
end
end
@@ -162,12 +162,12 @@ RSpec.describe Clusters::Applications::Runner do
it 'pauses associated runner' do
active_runner = create(:ci_runner, contacted_at: 1.second.ago)
- expect(active_runner.status).to eq(:online)
+ expect(active_runner.active).to be_truthy
application_runner = create(:clusters_applications_runner, :scheduled, runner: active_runner)
application_runner.prepare_uninstall
- expect(active_runner.status).to eq(:paused)
+ expect(active_runner.active).to be_falsey
end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 9d305e31bad..d61bed80aaa 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -178,13 +178,13 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
- describe '.with_application_prometheus' do
- subject { described_class.with_application_prometheus }
+ describe '.with_integration_prometheus' do
+ subject { described_class.with_integration_prometheus }
let!(:cluster) { create(:cluster) }
context 'cluster has prometheus application' do
- let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let!(:application) { create(:clusters_integrations_prometheus, cluster: cluster) }
it { is_expected.to include(cluster) }
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 20afddd8470..59d14574c02 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -379,6 +379,22 @@ RSpec.describe CommitStatus do
end
end
+ describe '.retried_ordered' do
+ subject { described_class.retried_ordered.to_a }
+
+ let!(:statuses) do
+ [create_status(name: 'aa', ref: 'bb', status: 'running', retried: true),
+ create_status(name: 'cc', ref: 'cc', status: 'pending', retried: true),
+ create_status(name: 'aa', ref: 'cc', status: 'success', retried: true),
+ create_status(name: 'cc', ref: 'bb', status: 'success'),
+ create_status(name: 'aa', ref: 'bb', status: 'success')]
+ end
+
+ it 'returns retried statuses in order' do
+ is_expected.to eq(statuses.values_at(2, 0, 1))
+ end
+ end
+
describe '.running_or_pending' do
subject { described_class.running_or_pending.order(:id) }
diff --git a/spec/models/concerns/bulk_insert_safe_spec.rb b/spec/models/concerns/bulk_insert_safe_spec.rb
index 172986c142c..e6b197f34ca 100644
--- a/spec/models/concerns/bulk_insert_safe_spec.rb
+++ b/spec/models/concerns/bulk_insert_safe_spec.rb
@@ -5,42 +5,42 @@ require 'spec_helper'
RSpec.describe BulkInsertSafe do
before(:all) do
ActiveRecord::Schema.define do
- create_table :bulk_insert_parent_items, force: true do |t|
+ create_table :_test_bulk_insert_parent_items, force: true do |t|
t.string :name, null: false
end
- create_table :bulk_insert_items, force: true do |t|
+ create_table :_test_bulk_insert_items, force: true do |t|
t.string :name, null: true
t.integer :enum_value, null: false
t.text :encrypted_secret_value, null: false
t.string :encrypted_secret_value_iv, null: false
t.binary :sha_value, null: false, limit: 20
t.jsonb :jsonb_value, null: false
- t.belongs_to :bulk_insert_parent_item, foreign_key: true, null: true
+ t.belongs_to :bulk_insert_parent_item, foreign_key: { to_table: :_test_bulk_insert_parent_items }, null: true
t.timestamps null: true
t.index :name, unique: true
end
- create_table :bulk_insert_items_with_composite_pk, id: false, force: true do |t|
+ create_table :_test_bulk_insert_items_with_composite_pk, id: false, force: true do |t|
t.integer :id, null: true
t.string :name, null: true
end
- execute("ALTER TABLE bulk_insert_items_with_composite_pk ADD PRIMARY KEY (id,name);")
+ execute("ALTER TABLE _test_bulk_insert_items_with_composite_pk ADD PRIMARY KEY (id,name);")
end
end
after(:all) do
ActiveRecord::Schema.define do
- drop_table :bulk_insert_items, force: true
- drop_table :bulk_insert_parent_items, force: true
- drop_table :bulk_insert_items_with_composite_pk, force: true
+ drop_table :_test_bulk_insert_items, force: true
+ drop_table :_test_bulk_insert_parent_items, force: true
+ drop_table :_test_bulk_insert_items_with_composite_pk, force: true
end
end
BulkInsertParentItem = Class.new(ActiveRecord::Base) do
- self.table_name = :bulk_insert_parent_items
+ self.table_name = :_test_bulk_insert_parent_items
self.inheritance_column = :_type_disabled
def self.name
@@ -54,7 +54,7 @@ RSpec.describe BulkInsertSafe do
let_it_be(:bulk_insert_item_class) do
Class.new(ActiveRecord::Base) do
- self.table_name = 'bulk_insert_items'
+ self.table_name = '_test_bulk_insert_items'
include BulkInsertSafe
include ShaAttribute
@@ -182,7 +182,7 @@ RSpec.describe BulkInsertSafe do
context 'with returns option set' do
let(:items) { bulk_insert_item_class.valid_list(1) }
- subject(:bulk_insert) { bulk_insert_item_class.bulk_insert!(items, returns: returns) }
+ subject(:legacy_bulk_insert) { bulk_insert_item_class.bulk_insert!(items, returns: returns) }
context 'when is set to :ids' do
let(:returns) { :ids }
@@ -247,7 +247,7 @@ RSpec.describe BulkInsertSafe do
context 'when a model with composite primary key is inserted' do
let_it_be(:bulk_insert_items_with_composite_pk_class) do
Class.new(ActiveRecord::Base) do
- self.table_name = 'bulk_insert_items_with_composite_pk'
+ self.table_name = '_test_bulk_insert_items_with_composite_pk'
include BulkInsertSafe
end
diff --git a/spec/models/concerns/bulk_insertable_associations_spec.rb b/spec/models/concerns/bulk_insertable_associations_spec.rb
index 25b13c8233d..9713f1ce9a4 100644
--- a/spec/models/concerns/bulk_insertable_associations_spec.rb
+++ b/spec/models/concerns/bulk_insertable_associations_spec.rb
@@ -6,42 +6,50 @@ RSpec.describe BulkInsertableAssociations do
class BulkFoo < ApplicationRecord
include BulkInsertSafe
+ self.table_name = '_test_bulk_foos'
+
validates :name, presence: true
end
class BulkBar < ApplicationRecord
include BulkInsertSafe
+
+ self.table_name = '_test_bulk_bars'
end
- SimpleBar = Class.new(ApplicationRecord)
+ SimpleBar = Class.new(ApplicationRecord) do
+ self.table_name = '_test_simple_bars'
+ end
class BulkParent < ApplicationRecord
include BulkInsertableAssociations
- has_many :bulk_foos
+ self.table_name = '_test_bulk_parents'
+
+ has_many :bulk_foos, class_name: 'BulkFoo'
has_many :bulk_hunks, class_name: 'BulkFoo'
- has_many :bulk_bars
- has_many :simple_bars # not `BulkInsertSafe`
+ has_many :bulk_bars, class_name: 'BulkBar'
+ has_many :simple_bars, class_name: 'SimpleBar' # not `BulkInsertSafe`
has_one :bulk_foo # not supported
end
before(:all) do
ActiveRecord::Schema.define do
- create_table :bulk_parents, force: true do |t|
+ create_table :_test_bulk_parents, force: true do |t|
t.string :name, null: true
end
- create_table :bulk_foos, force: true do |t|
+ create_table :_test_bulk_foos, force: true do |t|
t.string :name, null: true
t.belongs_to :bulk_parent, null: false
end
- create_table :bulk_bars, force: true do |t|
+ create_table :_test_bulk_bars, force: true do |t|
t.string :name, null: true
t.belongs_to :bulk_parent, null: false
end
- create_table :simple_bars, force: true do |t|
+ create_table :_test_simple_bars, force: true do |t|
t.string :name, null: true
t.belongs_to :bulk_parent, null: false
end
@@ -50,10 +58,10 @@ RSpec.describe BulkInsertableAssociations do
after(:all) do
ActiveRecord::Schema.define do
- drop_table :bulk_foos, force: true
- drop_table :bulk_bars, force: true
- drop_table :simple_bars, force: true
- drop_table :bulk_parents, force: true
+ drop_table :_test_bulk_foos, force: true
+ drop_table :_test_bulk_bars, force: true
+ drop_table :_test_simple_bars, force: true
+ drop_table :_test_bulk_parents, force: true
end
end
diff --git a/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb b/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb
index e8f2b18e662..6be6e3f048f 100644
--- a/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb
+++ b/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb
@@ -136,6 +136,21 @@ RSpec.describe NamespaceSetting, 'CascadingNamespaceSettingAttribute' do
.to raise_error(ActiveRecord::RecordInvalid, /Delayed project removal cannot be changed because it is locked by an ancestor/)
end
end
+
+ context 'when parent locked the attribute then the application settings locks it' do
+ before do
+ subgroup_settings.update!(delayed_project_removal: true)
+ group_settings.update!(lock_delayed_project_removal: true, delayed_project_removal: false)
+ stub_application_setting(lock_delayed_project_removal: true, delayed_project_removal: true)
+
+ subgroup_settings.clear_memoization(:delayed_project_removal)
+ subgroup_settings.clear_memoization(:delayed_project_removal_locked_ancestor)
+ end
+
+ it 'returns the application setting value' do
+ expect(delayed_project_removal).to eq(true)
+ end
+ end
end
describe '#delayed_project_removal?' do
diff --git a/spec/models/concerns/clusters/agents/authorization_config_scopes_spec.rb b/spec/models/concerns/clusters/agents/authorization_config_scopes_spec.rb
new file mode 100644
index 00000000000..a4d1a33b3d5
--- /dev/null
+++ b/spec/models/concerns/clusters/agents/authorization_config_scopes_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::AuthorizationConfigScopes do
+ describe '.with_available_ci_access_fields' do
+ let(:project) { create(:project) }
+
+ let!(:agent_authorization_0) { create(:agent_project_authorization, project: project) }
+ let!(:agent_authorization_1) { create(:agent_project_authorization, project: project, config: { access_as: {} }) }
+ let!(:agent_authorization_2) { create(:agent_project_authorization, project: project, config: { access_as: { agent: {} } }) }
+ let!(:impersonate_authorization) { create(:agent_project_authorization, project: project, config: { access_as: { impersonate: {} } }) }
+ let!(:ci_user_authorization) { create(:agent_project_authorization, project: project, config: { access_as: { ci_user: {} } }) }
+ let!(:ci_job_authorization) { create(:agent_project_authorization, project: project, config: { access_as: { ci_job: {} } }) }
+ let!(:unexpected_authorization) { create(:agent_project_authorization, project: project, config: { access_as: { unexpected: {} } }) }
+
+ subject { Clusters::Agents::ProjectAuthorization.with_available_ci_access_fields(project) }
+
+ it { is_expected.to contain_exactly(agent_authorization_0, agent_authorization_1, agent_authorization_2) }
+ end
+end
diff --git a/spec/models/concerns/database_reflection_spec.rb b/spec/models/concerns/database_reflection_spec.rb
new file mode 100644
index 00000000000..4111f29ea8d
--- /dev/null
+++ b/spec/models/concerns/database_reflection_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe DatabaseReflection do
+ describe '.reflect' do
+ it 'returns a Reflection instance' do
+ expect(User.database).to be_an_instance_of(Gitlab::Database::Reflection)
+ end
+
+ it 'memoizes the result' do
+ instance1 = User.database
+ instance2 = User.database
+
+ expect(instance1).to equal(instance2)
+ end
+ end
+end
diff --git a/spec/models/concerns/has_integrations_spec.rb b/spec/models/concerns/has_integrations_spec.rb
deleted file mode 100644
index ea6b0e69209..00000000000
--- a/spec/models/concerns/has_integrations_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe HasIntegrations do
- let_it_be(:project_1) { create(:project) }
- let_it_be(:project_2) { create(:project) }
- let_it_be(:project_3) { create(:project) }
- let_it_be(:project_4) { create(:project) }
- let_it_be(:instance_integration) { create(:jira_integration, :instance) }
-
- before do
- create(:jira_integration, project: project_1, inherit_from_id: instance_integration.id)
- create(:jira_integration, project: project_2, inherit_from_id: nil)
- create(:jira_integration, group: create(:group), project: nil, inherit_from_id: nil)
- create(:jira_integration, project: project_3, inherit_from_id: nil)
- create(:integrations_slack, project: project_4, inherit_from_id: nil)
- end
-
- describe '.without_integration' do
- it 'returns projects without integration' do
- expect(Project.without_integration(instance_integration)).to contain_exactly(project_4)
- end
- end
-end
diff --git a/spec/models/concerns/legacy_bulk_insert_spec.rb b/spec/models/concerns/legacy_bulk_insert_spec.rb
new file mode 100644
index 00000000000..0c6f84f391b
--- /dev/null
+++ b/spec/models/concerns/legacy_bulk_insert_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# rubocop: disable Gitlab/BulkInsert
+RSpec.describe LegacyBulkInsert do
+ let(:model) { ApplicationRecord }
+
+ describe '#bulk_insert' do
+ before do
+ allow(model).to receive(:connection).and_return(dummy_connection)
+ allow(dummy_connection).to receive(:quote_column_name, &:itself)
+ allow(dummy_connection).to receive(:quote, &:itself)
+ allow(dummy_connection).to receive(:execute)
+ end
+
+ let(:dummy_connection) { double(:connection) }
+
+ let(:rows) do
+ [
+ { a: 1, b: 2, c: 3 },
+ { c: 6, a: 4, b: 5 }
+ ]
+ end
+
+ it 'does nothing with empty rows' do
+ expect(dummy_connection).not_to receive(:execute)
+
+ model.legacy_bulk_insert('test', [])
+ end
+
+ it 'uses the ordering from the first row' do
+ expect(dummy_connection).to receive(:execute) do |sql|
+ expect(sql).to include('(1, 2, 3)')
+ expect(sql).to include('(4, 5, 6)')
+ end
+
+ model.legacy_bulk_insert('test', rows)
+ end
+
+ it 'quotes column names' do
+ expect(dummy_connection).to receive(:quote_column_name).with(:a)
+ expect(dummy_connection).to receive(:quote_column_name).with(:b)
+ expect(dummy_connection).to receive(:quote_column_name).with(:c)
+
+ model.legacy_bulk_insert('test', rows)
+ end
+
+ it 'quotes values' do
+ 1.upto(6) do |i|
+ expect(dummy_connection).to receive(:quote).with(i)
+ end
+
+ model.legacy_bulk_insert('test', rows)
+ end
+
+ it 'does not quote values of a column in the disable_quote option' do
+ [1, 2, 4, 5].each do |i|
+ expect(dummy_connection).to receive(:quote).with(i)
+ end
+
+ model.legacy_bulk_insert('test', rows, disable_quote: :c)
+ end
+
+ it 'does not quote values of columns in the disable_quote option' do
+ [2, 5].each do |i|
+ expect(dummy_connection).to receive(:quote).with(i)
+ end
+
+ model.legacy_bulk_insert('test', rows, disable_quote: [:a, :c])
+ end
+
+ it 'handles non-UTF-8 data' do
+ expect { model.legacy_bulk_insert('test', [{ a: "\255" }]) }.not_to raise_error
+ end
+
+ context 'when using PostgreSQL' do
+ it 'allows the returning of the IDs of the inserted rows' do
+ result = double(:result, values: [['10']])
+
+ expect(dummy_connection)
+ .to receive(:execute)
+ .with(/RETURNING id/)
+ .and_return(result)
+
+ ids = model
+ .legacy_bulk_insert('test', [{ number: 10 }], return_ids: true)
+
+ expect(ids).to eq([10])
+ end
+
+ it 'allows setting the upsert to do nothing' do
+ expect(dummy_connection)
+ .to receive(:execute)
+ .with(/ON CONFLICT DO NOTHING/)
+
+ model
+ .legacy_bulk_insert('test', [{ number: 10 }], on_conflict: :do_nothing)
+ end
+ end
+ end
+end
+# rubocop: enable Gitlab/BulkInsert
diff --git a/spec/models/concerns/loaded_in_group_list_spec.rb b/spec/models/concerns/loaded_in_group_list_spec.rb
index c37943022ba..d38e842c666 100644
--- a/spec/models/concerns/loaded_in_group_list_spec.rb
+++ b/spec/models/concerns/loaded_in_group_list_spec.rb
@@ -3,49 +3,67 @@
require 'spec_helper'
RSpec.describe LoadedInGroupList do
- let(:parent) { create(:group) }
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent) }
+ let_it_be(:project) { create(:project, namespace: parent) }
- subject(:found_group) { Group.with_selects_for_list.find_by(id: parent.id) }
+ let(:archived_parameter) { nil }
- describe '.with_selects_for_list' do
- it 'includes the preloaded counts for groups' do
- create(:group, parent: parent)
- create(:project, namespace: parent)
- parent.add_developer(create(:user))
+ before do
+ parent.add_developer(create(:user))
+ end
- found_group = Group.with_selects_for_list.find_by(id: parent.id)
+ subject(:found_group) { Group.with_selects_for_list(archived: archived_parameter).find_by(id: parent.id) }
+ describe '.with_selects_for_list' do
+ it 'includes the preloaded counts for groups' do
expect(found_group.preloaded_project_count).to eq(1)
expect(found_group.preloaded_subgroup_count).to eq(1)
expect(found_group.preloaded_member_count).to eq(1)
end
+ context 'with project namespaces' do
+ let_it_be(:group1) { create(:group, parent: parent) }
+ let_it_be(:group2) { create(:group, parent: parent) }
+ let_it_be(:project_namespace) { project.project_namespace }
+
+ it 'does not include project_namespaces in the count of subgroups' do
+ expect(found_group.preloaded_subgroup_count).to eq(3)
+ expect(parent.subgroup_count).to eq(3)
+ end
+ end
+
context 'with archived projects' do
- it 'counts including archived projects when `true` is passed' do
- create(:project, namespace: parent, archived: true)
- create(:project, namespace: parent)
+ let_it_be(:archived_project) { create(:project, namespace: parent, archived: true) }
- found_group = Group.with_selects_for_list(archived: 'true').find_by(id: parent.id)
+ let(:archived_parameter) { true }
+ it 'counts including archived projects when `true` is passed' do
expect(found_group.preloaded_project_count).to eq(2)
end
- it 'counts only archived projects when `only` is passed' do
- create_list(:project, 2, namespace: parent, archived: true)
- create(:project, namespace: parent)
+ context 'when not counting archived projects' do
+ let(:archived_parameter) { false }
+
+ it 'counts projects without archived ones' do
+ expect(found_group.preloaded_project_count).to eq(1)
+ end
+ end
+
+ context 'with archived only' do
+ let_it_be(:archived_project2) { create(:project, namespace: parent, archived: true) }
- found_group = Group.with_selects_for_list(archived: 'only').find_by(id: parent.id)
+ let(:archived_parameter) { 'only' }
- expect(found_group.preloaded_project_count).to eq(2)
+ it 'counts only archived projects when `only` is passed' do
+ expect(found_group.preloaded_project_count).to eq(2)
+ end
end
end
end
describe '#children_count' do
it 'counts groups and projects' do
- create(:group, parent: parent)
- create(:project, namespace: parent)
-
expect(found_group.children_count).to eq(2)
end
end
diff --git a/spec/models/concerns/loose_foreign_key_spec.rb b/spec/models/concerns/loose_foreign_key_spec.rb
index ce5e33261a9..42da69eb75e 100644
--- a/spec/models/concerns/loose_foreign_key_spec.rb
+++ b/spec/models/concerns/loose_foreign_key_spec.rb
@@ -9,8 +9,8 @@ RSpec.describe LooseForeignKey do
self.table_name = 'projects'
- loose_foreign_key :issues, :project_id, on_delete: :async_delete, gitlab_schema: :gitlab_main
- loose_foreign_key 'merge_requests', 'project_id', 'on_delete' => 'async_nullify', 'gitlab_schema' => :gitlab_main
+ loose_foreign_key :issues, :project_id, on_delete: :async_delete
+ loose_foreign_key 'merge_requests', 'project_id', 'on_delete' => 'async_nullify'
end
end
@@ -28,7 +28,6 @@ RSpec.describe LooseForeignKey do
expect(definition.to_table).to eq('merge_requests')
expect(definition.column).to eq('project_id')
expect(definition.on_delete).to eq(:async_nullify)
- expect(definition.options[:gitlab_schema]).to eq(:gitlab_main)
end
context 'validation' do
@@ -39,9 +38,9 @@ RSpec.describe LooseForeignKey do
self.table_name = 'projects'
- loose_foreign_key :issues, :project_id, on_delete: :async_delete, gitlab_schema: :gitlab_main
- loose_foreign_key :merge_requests, :project_id, on_delete: :async_nullify, gitlab_schema: :gitlab_main
- loose_foreign_key :merge_requests, :project_id, on_delete: :destroy, gitlab_schema: :gitlab_main
+ loose_foreign_key :issues, :project_id, on_delete: :async_delete
+ loose_foreign_key :merge_requests, :project_id, on_delete: :async_nullify
+ loose_foreign_key :merge_requests, :project_id, on_delete: :destroy
end
end
@@ -50,28 +49,12 @@ RSpec.describe LooseForeignKey do
end
end
- context 'gitlab_schema validation' do
- let(:invalid_class) do
- Class.new(ApplicationRecord) do
- include LooseForeignKey
-
- self.table_name = 'projects'
-
- loose_foreign_key :merge_requests, :project_id, on_delete: :async_nullify, gitlab_schema: :unknown
- end
- end
-
- it 'raises error when invalid `gitlab_schema` option was given' do
- expect { invalid_class }.to raise_error /Invalid gitlab_schema option given: unknown/
- end
- end
-
context 'inheritance validation' do
let(:inherited_project_class) do
Class.new(Project) do
include LooseForeignKey
- loose_foreign_key :issues, :project_id, on_delete: :async_delete, gitlab_schema: :gitlab_main
+ loose_foreign_key :issues, :project_id, on_delete: :async_delete
end
end
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index 38766d8decd..81ae30b7116 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -77,6 +77,70 @@ RSpec.describe Noteable do
end
end
+ describe '#discussion_root_note_ids' do
+ let!(:label_event) { create(:resource_label_event, merge_request: subject) }
+ let!(:system_note) { create(:system_note, project: project, noteable: subject) }
+ let!(:milestone_event) { create(:resource_milestone_event, merge_request: subject) }
+ let!(:state_event) { create(:resource_state_event, merge_request: subject) }
+
+ it 'returns ordered discussion_ids and synthetic note ids' do
+ discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:all_notes]).map do |n|
+ { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
+ end
+
+ expect(discussions).to match([
+ a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id),
+ a_hash_including(table_name: 'resource_label_events', id: label_event.id),
+ a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id),
+ a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id),
+ a_hash_including(table_name: 'resource_state_events', id: state_event.id)
+ ])
+ end
+
+ it 'filters by comments only' do
+ discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:only_comments]).map do |n|
+ { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
+ end
+
+ expect(discussions).to match([
+ a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id)
+ ])
+ end
+
+ it 'filters by system notes only' do
+ discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:only_activity]).map do |n|
+ { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
+ end
+
+ expect(discussions).to match([
+ a_hash_including(table_name: 'resource_label_events', id: label_event.id),
+ a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id),
+ a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id),
+ a_hash_including(table_name: 'resource_state_events', id: state_event.id)
+ ])
+ end
+ end
+
describe '#grouped_diff_discussions' do
let(:grouped_diff_discussions) { subject.grouped_diff_discussions }
diff --git a/spec/models/concerns/prometheus_adapter_spec.rb b/spec/models/concerns/prometheus_adapter_spec.rb
index 01c987a1d92..4158e8a0a4c 100644
--- a/spec/models/concerns/prometheus_adapter_spec.rb
+++ b/spec/models/concerns/prometheus_adapter_spec.rb
@@ -165,6 +165,14 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) }
end
end
+
+ context "when client raises Gitlab::PrometheusClient::ConnectionError" do
+ before do
+ stub_any_prometheus_request.to_raise(Gitlab::PrometheusClient::ConnectionError)
+ end
+
+ it { is_expected.to include(success: false, result: kind_of(String)) }
+ end
end
describe '#build_query_args' do
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index 7e031bdd263..4f3b95e43cd 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -375,7 +375,7 @@ RSpec.describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
describe 'classes including this concern' do
- it 'sets reactive_cache_work_type' do
+ it 'sets reactive_cache_work_type', :eager_load do
classes = ObjectSpace.each_object(Class).select do |klass|
klass < described_class && klass.name
end
diff --git a/spec/models/concerns/sha256_attribute_spec.rb b/spec/models/concerns/sha256_attribute_spec.rb
index c247865d77f..02947325bf4 100644
--- a/spec/models/concerns/sha256_attribute_spec.rb
+++ b/spec/models/concerns/sha256_attribute_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Sha256Attribute do
- let(:model) { Class.new { include Sha256Attribute } }
+ let(:model) { Class.new(ApplicationRecord) { include Sha256Attribute } }
before do
columns = [
diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb
index 3846dd9c231..220eadfab92 100644
--- a/spec/models/concerns/sha_attribute_spec.rb
+++ b/spec/models/concerns/sha_attribute_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe ShaAttribute do
- let(:model) { Class.new { include ShaAttribute } }
+ let(:model) { Class.new(ApplicationRecord) { include ShaAttribute } }
before do
columns = [
diff --git a/spec/models/concerns/where_composite_spec.rb b/spec/models/concerns/where_composite_spec.rb
index 5e67f2f5b65..6abdd12aac5 100644
--- a/spec/models/concerns/where_composite_spec.rb
+++ b/spec/models/concerns/where_composite_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe WhereComposite do
describe '.where_composite' do
- let_it_be(:test_table_name) { "test_table_#{SecureRandom.hex(10)}" }
+ let_it_be(:test_table_name) { "_test_table_#{SecureRandom.hex(10)}" }
let(:model) do
tbl_name = test_table_name
diff --git a/spec/models/concerns/x509_serial_number_attribute_spec.rb b/spec/models/concerns/x509_serial_number_attribute_spec.rb
index 88550823748..723e2ad07b6 100644
--- a/spec/models/concerns/x509_serial_number_attribute_spec.rb
+++ b/spec/models/concerns/x509_serial_number_attribute_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe X509SerialNumberAttribute do
- let(:model) { Class.new { include X509SerialNumberAttribute } }
+ let(:model) { Class.new(ApplicationRecord) { include X509SerialNumberAttribute } }
before do
columns = [
diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb
index 4a8b671bab7..01252a58681 100644
--- a/spec/models/custom_emoji_spec.rb
+++ b/spec/models/custom_emoji_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe CustomEmoji do
end
describe 'exclusion of duplicated emoji' do
- let(:emoji_name) { Gitlab::Emoji.emojis_names.sample }
+ let(:emoji_name) { TanukiEmoji.index.all.sample.name }
let(:group) { create(:group, :private) }
it 'disallows emoji names of built-in emoji' do
diff --git a/spec/models/customer_relations/contact_spec.rb b/spec/models/customer_relations/contact_spec.rb
index 298d5db3ab9..3a2d4e2d0ca 100644
--- a/spec/models/customer_relations/contact_spec.rb
+++ b/spec/models/customer_relations/contact_spec.rb
@@ -6,7 +6,8 @@ RSpec.describe CustomerRelations::Contact, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:organization).optional }
- it { is_expected.to have_and_belong_to_many(:issues) }
+ it { is_expected.to have_many(:issue_contacts) }
+ it { is_expected.to have_many(:issues) }
end
describe 'validations' do
diff --git a/spec/models/customer_relations/issue_contact_spec.rb b/spec/models/customer_relations/issue_contact_spec.rb
new file mode 100644
index 00000000000..3747d159833
--- /dev/null
+++ b/spec/models/customer_relations/issue_contact_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CustomerRelations::IssueContact do
+ let_it_be(:issue_contact, reload: true) { create(:issue_customer_relations_contact) }
+
+ subject { issue_contact }
+
+ it { expect(subject).to be_valid }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:issue).required }
+ it { is_expected.to belong_to(:contact).required }
+ end
+
+ describe 'factory' do
+ let(:built) { build(:issue_customer_relations_contact) }
+ let(:stubbed) { build_stubbed(:issue_customer_relations_contact) }
+ let(:created) { create(:issue_customer_relations_contact) }
+
+ let(:group) { build(:group) }
+ let(:project) { build(:project, group: group) }
+ let(:issue) { build(:issue, project: project) }
+ let(:contact) { build(:contact, group: group) }
+ let(:for_issue) { build(:issue_customer_relations_contact, :for_issue, issue: issue) }
+ let(:for_contact) { build(:issue_customer_relations_contact, :for_contact, contact: contact) }
+
+ it 'uses objects from the same group', :aggregate_failures do
+ expect(stubbed.contact.group).to eq(stubbed.issue.project.group)
+ expect(built.contact.group).to eq(built.issue.project.group)
+ expect(created.contact.group).to eq(created.issue.project.group)
+ end
+
+ it 'builds using the same group', :aggregate_failures do
+ expect(for_issue.contact.group).to eq(group)
+ expect(for_contact.issue.project.group).to eq(group)
+ end
+ end
+
+ describe 'validation' do
+ let(:built) { build(:issue_customer_relations_contact, issue: create(:issue), contact: create(:contact)) }
+
+ it 'fails when the contact group does not match the issue group' do
+ expect(built).not_to be_valid
+ end
+ end
+end
diff --git a/spec/models/data_list_spec.rb b/spec/models/data_list_spec.rb
new file mode 100644
index 00000000000..d2f15386808
--- /dev/null
+++ b/spec/models/data_list_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe DataList do
+ describe '#to_array' do
+ let(:jira_integration) { create(:jira_integration) }
+ let(:zentao_integration) { create(:zentao_integration) }
+ let(:cases) do
+ [
+ [jira_integration, 'Integrations::JiraTrackerData', 'service_id'],
+ [zentao_integration, 'Integrations::ZentaoTrackerData', 'integration_id']
+ ]
+ end
+
+ def data_list(integration)
+ DataList.new([integration], integration.to_data_fields_hash, integration.data_fields.class).to_array
+ end
+
+ it 'returns current data' do
+ cases.each do |integration, data_fields_class_name, foreign_key|
+ data_fields_klass, columns, values_items = data_list(integration)
+
+ expect(data_fields_klass.to_s).to eq data_fields_class_name
+ expect(columns.last).to eq foreign_key
+ values = values_items.first
+ expect(values.last).to eq integration.id
+ end
+ end
+ end
+end
diff --git a/spec/models/dependency_proxy/manifest_spec.rb b/spec/models/dependency_proxy/manifest_spec.rb
index e7f0889345a..59415096989 100644
--- a/spec/models/dependency_proxy/manifest_spec.rb
+++ b/spec/models/dependency_proxy/manifest_spec.rb
@@ -15,6 +15,17 @@ RSpec.describe DependencyProxy::Manifest, type: :model do
it { is_expected.to validate_presence_of(:digest) }
end
+ describe 'scopes' do
+ let_it_be(:manifest_one) { create(:dependency_proxy_manifest) }
+ let_it_be(:manifest_two) { create(:dependency_proxy_manifest) }
+ let_it_be(:manifests) { [manifest_one, manifest_two] }
+ let_it_be(:ids) { manifests.map(&:id) }
+
+ it 'order_id_desc' do
+ expect(described_class.where(id: ids).order_id_desc.to_a).to eq [manifest_two, manifest_one]
+ end
+ end
+
describe 'file is being stored' do
subject { create(:dependency_proxy_manifest) }
@@ -31,18 +42,14 @@ RSpec.describe DependencyProxy::Manifest, type: :model do
end
end
- describe '.find_or_initialize_by_file_name_or_digest' do
+ describe '.find_by_file_name_or_digest' do
let_it_be(:file_name) { 'foo' }
let_it_be(:digest) { 'bar' }
- subject { DependencyProxy::Manifest.find_or_initialize_by_file_name_or_digest(file_name: file_name, digest: digest) }
+ subject { DependencyProxy::Manifest.find_by_file_name_or_digest(file_name: file_name, digest: digest) }
context 'no manifest exists' do
- it 'initializes a manifest' do
- expect(DependencyProxy::Manifest).to receive(:new).with(file_name: file_name, digest: digest)
-
- subject
- end
+ it { is_expected.to be_nil }
end
context 'manifest exists and matches file_name' do
diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb
index fa78527e366..c22bad0e062 100644
--- a/spec/models/deploy_key_spec.rb
+++ b/spec/models/deploy_key_spec.rb
@@ -5,6 +5,17 @@ require 'spec_helper'
RSpec.describe DeployKey, :mailer do
describe "Associations" do
it { is_expected.to have_many(:deploy_keys_projects) }
+ it do
+ is_expected.to have_many(:deploy_keys_projects_with_write_access)
+ .conditions(can_push: true)
+ .class_name('DeployKeysProject')
+ end
+ it do
+ is_expected.to have_many(:projects_with_write_access)
+ .class_name('Project')
+ .through(:deploy_keys_projects_with_write_access)
+ .source(:project)
+ end
it { is_expected.to have_many(:projects) }
it { is_expected.to have_many(:protected_branch_push_access_levels) }
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index f9a05fbb06f..51e1e63da8d 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -385,6 +385,43 @@ RSpec.describe Deployment do
end
end
+ describe '.archivables_in' do
+ subject { described_class.archivables_in(project, limit: limit) }
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:deployment_1) { create(:deployment, project: project) }
+ let_it_be(:deployment_2) { create(:deployment, project: project) }
+ let_it_be(:deployment_3) { create(:deployment, project: project) }
+
+ let(:limit) { 100 }
+
+ context 'when there are no archivable deployments in the project' do
+ it 'returns nothing' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when there are archivable deployments in the project' do
+ before do
+ stub_const("::Deployment::ARCHIVABLE_OFFSET", 1)
+ end
+
+ it 'returns all archivable deployments' do
+ expect(subject.count).to eq(2)
+ expect(subject).to contain_exactly(deployment_1, deployment_2)
+ end
+
+ context 'with limit' do
+ let(:limit) { 1 }
+
+ it 'takes the limit into account' do
+ expect(subject.count).to eq(1)
+ expect(subject.take).to be_in([deployment_1, deployment_2])
+ end
+ end
+ end
+ end
+
describe 'scopes' do
describe 'last_for_environment' do
let(:production) { create(:environment) }
@@ -456,6 +493,17 @@ RSpec.describe Deployment do
end
end
+ describe '.ordered' do
+ let!(:deployment1) { create(:deployment, status: :running) }
+ let!(:deployment2) { create(:deployment, status: :success, finished_at: Time.current) }
+ let!(:deployment3) { create(:deployment, status: :canceled, finished_at: 1.day.ago) }
+ let!(:deployment4) { create(:deployment, status: :success, finished_at: 2.days.ago) }
+
+ it 'sorts by finished at' do
+ expect(described_class.ordered).to eq([deployment1, deployment2, deployment3, deployment4])
+ end
+ end
+
describe 'visible' do
subject { described_class.visible }
@@ -763,6 +811,7 @@ RSpec.describe Deployment do
it 'schedules workers when finishing a deploy' do
expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
+ expect(Deployments::ArchiveInProjectWorker).to receive(:perform_async)
expect(Deployments::HooksWorker).to receive(:perform_async)
expect(deploy.update_status('success')).to eq(true)
@@ -840,6 +889,12 @@ RSpec.describe Deployment do
context 'with created deployment' do
let(:deployment_status) { :created }
+ context 'with created build' do
+ let(:build_status) { :created }
+
+ it_behaves_like 'ignoring build'
+ end
+
context 'with running build' do
let(:build_status) { :running }
@@ -862,12 +917,16 @@ RSpec.describe Deployment do
context 'with running deployment' do
let(:deployment_status) { :running }
+ context 'with created build' do
+ let(:build_status) { :created }
+
+ it_behaves_like 'ignoring build'
+ end
+
context 'with running build' do
let(:build_status) { :running }
- it_behaves_like 'gracefully handling error' do
- let(:error_message) { %Q{Status cannot transition via \"run\"} }
- end
+ it_behaves_like 'ignoring build'
end
context 'with finished build' do
@@ -886,6 +945,12 @@ RSpec.describe Deployment do
context 'with finished deployment' do
let(:deployment_status) { :success }
+ context 'with created build' do
+ let(:build_status) { :created }
+
+ it_behaves_like 'ignoring build'
+ end
+
context 'with running build' do
let(:build_status) { :running }
@@ -897,9 +962,13 @@ RSpec.describe Deployment do
context 'with finished build' do
let(:build_status) { :success }
- it_behaves_like 'gracefully handling error' do
- let(:error_message) { %Q{Status cannot transition via \"succeed\"} }
- end
+ it_behaves_like 'ignoring build'
+ end
+
+ context 'with failed build' do
+ let(:build_status) { :failed }
+
+ it_behaves_like 'synchronizing deployment'
end
context 'with unrelated build' do
diff --git a/spec/models/design_management/version_spec.rb b/spec/models/design_management/version_spec.rb
index e004ad024bc..303bac61e1e 100644
--- a/spec/models/design_management/version_spec.rb
+++ b/spec/models/design_management/version_spec.rb
@@ -283,7 +283,7 @@ RSpec.describe DesignManagement::Version do
it 'retrieves author from the Commit if author_id is nil and version has been persisted' do
author = create(:user)
version = create(:design_version, :committed, author: author)
- author.destroy
+ author.destroy!
version.reload
commit = version.issue.project.design_repository.commit(version.sha)
commit_user = create(:user, email: commit.author_email, name: commit.author_name)
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index 2b09ee5c190..59299a507e4 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -13,6 +13,15 @@ RSpec.describe Email do
it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :email do
subject { build(:email) }
end
+
+ context 'when the email conflicts with the primary email of a different user' do
+ let(:user) { create(:user) }
+ let(:email) { build(:email, email: user.email) }
+
+ it 'is invalid' do
+ expect(email).to be_invalid
+ end
+ end
end
it 'normalize email value' do
@@ -33,7 +42,7 @@ RSpec.describe Email do
end
describe 'scopes' do
- let(:user) { create(:user) }
+ let(:user) { create(:user, :unconfirmed) }
it 'scopes confirmed emails' do
create(:email, :confirmed, user: user)
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 08c639957d3..9d9862aa3d3 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
it 'ensures environment tier when a new object is created' do
environment = build(:environment, name: 'gprd', tier: nil)
- expect { environment.save }.to change { environment.tier }.from(nil).to('production')
+ expect { environment.save! }.to change { environment.tier }.from(nil).to('production')
end
it 'ensures environment tier when an existing object is updated' do
@@ -418,7 +418,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
context 'not in the same branch' do
before do
- deployment.update(sha: project.commit('feature').id)
+ deployment.update!(sha: project.commit('feature').id)
end
it 'returns false' do
@@ -496,7 +496,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
context 'when no other actions' do
context 'environment is available' do
before do
- environment.update(state: :available)
+ environment.update!(state: :available)
end
it do
@@ -508,7 +508,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
context 'environment is already stopped' do
before do
- environment.update(state: :stopped)
+ environment.update!(state: :stopped)
end
it do
@@ -1502,7 +1502,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
deployment = create(:deployment, :success, environment: environment, project: project)
deployment.create_ref
- expect { environment.destroy }.to change { project.commit(deployment.ref_path) }.to(nil)
+ expect { environment.destroy! }.to change { project.commit(deployment.ref_path) }.to(nil)
end
end
@@ -1517,7 +1517,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
it 'returns the environments count grouped by state with zero value' do
- environment2.update(state: 'stopped')
+ environment2.update!(state: 'stopped')
expect(project.environments.count_by_state).to eq({ stopped: 3, available: 0 })
end
end
@@ -1710,4 +1710,36 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
subject
end
end
+
+ describe '#should_link_to_merge_requests?' do
+ subject { environment.should_link_to_merge_requests? }
+
+ context 'when environment is foldered' do
+ context 'when environment is production tier' do
+ let(:environment) { create(:environment, project: project, name: 'production/aws') }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when environment is development tier' do
+ let(:environment) { create(:environment, project: project, name: 'review/feature') }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when environment is unfoldered' do
+ context 'when environment is production tier' do
+ let(:environment) { create(:environment, project: project, name: 'production') }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when environment is development tier' do
+ let(:environment) { create(:environment, project: project, name: 'development') }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+ end
end
diff --git a/spec/models/error_tracking/error_event_spec.rb b/spec/models/error_tracking/error_event_spec.rb
index 8e20eb25353..9cf5a405e74 100644
--- a/spec/models/error_tracking/error_event_spec.rb
+++ b/spec/models/error_tracking/error_event_spec.rb
@@ -11,7 +11,10 @@ RSpec.describe ErrorTracking::ErrorEvent, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:description) }
+ it { is_expected.to validate_length_of(:description).is_at_most(1024) }
it { is_expected.to validate_presence_of(:occurred_at) }
+ it { is_expected.to validate_length_of(:level).is_at_most(255) }
+ it { is_expected.to validate_length_of(:environment).is_at_most(255) }
end
describe '#stacktrace' do
@@ -37,6 +40,23 @@ RSpec.describe ErrorTracking::ErrorEvent, type: :model do
expect(event.stacktrace).to be_kind_of(Array)
expect(event.stacktrace.first).to eq(expected_entry)
end
+
+ context 'error context is missing' do
+ let(:event) { create(:error_tracking_error_event, :browser) }
+
+ it 'generates a stacktrace without context' do
+ expected_entry = {
+ 'lineNo' => 6395,
+ 'context' => [],
+ 'filename' => 'webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js',
+ 'function' => 'hydrate',
+ 'colNo' => 0
+ }
+
+ expect(event.stacktrace).to be_kind_of(Array)
+ expect(event.stacktrace.first).to eq(expected_entry)
+ end
+ end
end
describe '#to_sentry_error_event' do
diff --git a/spec/models/error_tracking/error_spec.rb b/spec/models/error_tracking/error_spec.rb
index 9b8a81c6372..363cd197f3e 100644
--- a/spec/models/error_tracking/error_spec.rb
+++ b/spec/models/error_tracking/error_spec.rb
@@ -12,8 +12,12 @@ RSpec.describe ErrorTracking::Error, type: :model do
describe 'validations' do
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(:description) }
+ it { is_expected.to validate_length_of(:description).is_at_most(1024) }
it { is_expected.to validate_presence_of(:actor) }
+ it { is_expected.to validate_length_of(:actor).is_at_most(255) }
+ it { is_expected.to validate_length_of(:platform).is_at_most(255) }
end
describe '.report_error' do
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 41510b7aa1c..ee27eaf1d0b 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Event do
describe 'after_create :set_last_repository_updated_at' do
context 'with a push event' do
it 'updates the project last_repository_updated_at' do
- project.update(last_repository_updated_at: 1.year.ago)
+ project.update!(last_repository_updated_at: 1.year.ago)
create_push_event(project, project.owner)
@@ -44,7 +44,7 @@ RSpec.describe Event do
context 'without a push event' do
it 'does not update the project last_repository_updated_at' do
- project.update(last_repository_updated_at: 1.year.ago)
+ project.update!(last_repository_updated_at: 1.year.ago)
create(:closed_issue_event, project: project, author: project.owner)
@@ -58,7 +58,7 @@ RSpec.describe Event do
describe '#set_last_repository_updated_at' do
it 'only updates once every Event::REPOSITORY_UPDATED_AT_INTERVAL minutes' do
last_known_timestamp = (Event::REPOSITORY_UPDATED_AT_INTERVAL - 1.minute).ago
- project.update(last_repository_updated_at: last_known_timestamp)
+ project.update!(last_repository_updated_at: last_known_timestamp)
project.reload # a reload removes fractions of seconds
expect do
@@ -73,7 +73,7 @@ RSpec.describe Event do
it 'passes event to UserInteractedProject.track' do
expect(UserInteractedProject).to receive(:track).with(event)
- event.save
+ event.save!
end
end
end
@@ -824,7 +824,7 @@ RSpec.describe Event do
context 'when a project was updated less than 1 hour ago' do
it 'does not update the project' do
- project.update(last_activity_at: Time.current)
+ project.update!(last_activity_at: Time.current)
expect(project).not_to receive(:update_column)
.with(:last_activity_at, a_kind_of(Time))
@@ -835,7 +835,7 @@ RSpec.describe Event do
context 'when a project was updated more than 1 hour ago' do
it 'updates the project' do
- project.update(last_activity_at: 1.year.ago)
+ project.update!(last_activity_at: 1.year.ago)
create_push_event(project, project.owner)
diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb
index c2ef1fdcb5f..f2ec0ccb4fd 100644
--- a/spec/models/fork_network_spec.rb
+++ b/spec/models/fork_network_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe ForkNetwork do
describe '#add_root_as_member' do
it 'adds the root project as a member when creating a new root network' do
project = create(:project)
- fork_network = described_class.create(root_project: project)
+ fork_network = described_class.create!(root_project: project)
expect(fork_network.projects).to include(project)
end
@@ -54,8 +54,8 @@ RSpec.describe ForkNetwork do
first_fork = fork_project(first_project)
second_fork = fork_project(second_project)
- first_project.destroy
- second_project.destroy
+ first_project.destroy!
+ second_project.destroy!
expect(first_fork.fork_network).not_to be_nil
expect(first_fork.fork_network.root_project).to be_nil
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 6fe5a1407a9..9d70019734b 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -133,7 +133,7 @@ RSpec.describe GenericCommitStatus do
before do
generic_commit_status.context = nil
generic_commit_status.stage = nil
- generic_commit_status.save
+ generic_commit_status.save!
end
describe '#context' do
diff --git a/spec/models/grafana_integration_spec.rb b/spec/models/grafana_integration_spec.rb
index 79f102919ac..bb822187e0c 100644
--- a/spec/models/grafana_integration_spec.rb
+++ b/spec/models/grafana_integration_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe GrafanaIntegration do
context 'with grafana integration enabled' do
it 'returns nil' do
- grafana_integration.update(enabled: false)
+ grafana_integration.update!(enabled: false)
expect(grafana_integration.client).to be(nil)
end
@@ -81,8 +81,8 @@ RSpec.describe GrafanaIntegration do
end
it 'prevents overriding token value with its encrypted or masked version', :aggregate_failures do
- expect { grafana_integration.update(token: grafana_integration.encrypted_token) }.not_to change { grafana_integration.reload.send(:token) }
- expect { grafana_integration.update(token: grafana_integration.masked_token) }.not_to change { grafana_integration.reload.send(:token) }
+ expect { grafana_integration.update!(token: grafana_integration.encrypted_token) }.not_to change { grafana_integration.reload.send(:token) }
+ expect { grafana_integration.update!(token: grafana_integration.masked_token) }.not_to change { grafana_integration.reload.send(:token) }
end
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index e88abc21ef2..735aa4df2ba 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -37,6 +37,8 @@ RSpec.describe Group do
it { is_expected.to have_many(:daily_build_group_report_results).class_name('Ci::DailyBuildGroupReportResult') }
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout').with_foreign_key(:group_id) }
it { is_expected.to have_many(:bulk_import_exports).class_name('BulkImports::Export') }
+ it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') }
+ it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') }
describe '#members & #requesters' do
let(:requester) { create(:user) }
@@ -160,7 +162,7 @@ RSpec.describe Group do
context 'when sub group is deleted' do
it 'does not delete parent notification settings' do
expect do
- sub_group.destroy
+ sub_group.destroy!
end.to change { NotificationSetting.count }.by(-1)
end
end
@@ -404,7 +406,7 @@ RSpec.describe Group do
subject do
recorded_queries.record do
- group.update(parent: new_parent)
+ group.update!(parent: new_parent)
end
end
@@ -496,7 +498,7 @@ RSpec.describe Group do
let!(:group) { create(:group, parent: parent_group) }
before do
- parent_group.update(parent: new_grandparent)
+ parent_group.update!(parent: new_grandparent)
end
it 'updates traversal_ids for all descendants' do
@@ -563,6 +565,15 @@ RSpec.describe Group do
it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' }
end
end
+
+ context 'when project namespace exists in the group' do
+ let!(:project) { create(:project, group: group) }
+ let!(:project_namespace) { project.project_namespace }
+
+ it 'filters out project namespace' do
+ expect(group.descendants.find_by_id(project_namespace.id)).to be_nil
+ end
+ end
end
end
@@ -571,8 +582,8 @@ RSpec.describe Group do
let(:instance_integration) { build(:jira_integration, :instance) }
before do
- create(:jira_integration, group: group, project: nil)
- create(:integrations_slack, group: another_group, project: nil)
+ create(:jira_integration, :group, group: group)
+ create(:integrations_slack, :group, group: another_group)
end
it 'returns groups without integration' do
@@ -718,6 +729,22 @@ RSpec.describe Group do
expect(group.group_members.developers.map(&:user)).to include(user)
expect(group.group_members.guests.map(&:user)).not_to include(user)
end
+
+ context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
+ let!(:project) { create(:project, group: group) }
+
+ before do
+ stub_experiments(invite_members_for_task: true)
+ group.add_users([create(:user)], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: project.id)
+ end
+
+ it 'creates a member_task with the correct attributes', :aggregate_failures do
+ member = group.group_members.last
+
+ expect(member.tasks_to_be_done).to match_array([:ci, :code])
+ expect(member.member_task.project).to eq(project)
+ end
+ end
end
describe '#avatar_type' do
@@ -831,7 +858,7 @@ RSpec.describe Group do
before do
parent_group = create(:group)
create(:group_member, :owner, group: parent_group)
- group.update(parent: parent_group)
+ group.update!(parent: parent_group)
end
it { expect(group.last_owner?(@members[:owner])).to be_falsy }
@@ -888,7 +915,7 @@ RSpec.describe Group do
before do
parent_group = create(:group)
create(:group_member, :owner, group: parent_group)
- group.update(parent: parent_group)
+ group.update!(parent: parent_group)
end
it { expect(group.member_last_blocked_owner?(member)).to be(false) }
@@ -1936,7 +1963,7 @@ RSpec.describe Group do
let(:environment) { 'foo%bar/test' }
it 'matches literally for %' do
- ci_variable.update(environment_scope: 'foo%bar/*')
+ ci_variable.update_attribute(:environment_scope, 'foo%bar/*')
is_expected.to contain_exactly(ci_variable)
end
@@ -2077,7 +2104,7 @@ RSpec.describe Group do
let(:ancestor_group) { create(:group) }
before do
- group.update(parent: ancestor_group)
+ group.update!(parent: ancestor_group)
end
it 'returns all ancestor group ids' do
@@ -2594,7 +2621,7 @@ RSpec.describe Group do
let_it_be(:project) { create(:project, group: group, service_desk_enabled: false) }
before do
- project.update(service_desk_enabled: false)
+ project.update!(service_desk_enabled: false)
end
it { is_expected.to eq(false) }
@@ -2623,14 +2650,6 @@ RSpec.describe Group do
end
it_behaves_like 'returns namespaces with disabled email'
-
- context 'when feature flag :linear_group_ancestor_scopes is disabled' do
- before do
- stub_feature_flags(linear_group_ancestor_scopes: false)
- end
-
- it_behaves_like 'returns namespaces with disabled email'
- end
end
describe '.timelogs' do
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index d811f67d16b..f0ee9a613d8 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -32,8 +32,8 @@ RSpec.describe ProjectHook do
end
describe '#rate_limit' do
- let_it_be(:hook) { create(:project_hook) }
let_it_be(:plan_limits) { create(:plan_limits, :default_plan, web_hook_calls: 100) }
+ let_it_be(:hook) { create(:project_hook) }
it 'returns the default limit' do
expect(hook.rate_limit).to be(100)
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
index c0efb2dff56..124c54a2028 100644
--- a/spec/models/identity_spec.rb
+++ b/spec/models/identity_spec.rb
@@ -118,19 +118,19 @@ RSpec.describe Identity do
it 'if extern_uid changes' do
expect(ldap_identity).not_to receive(:ensure_normalized_extern_uid)
- ldap_identity.save
+ ldap_identity.save!
end
it 'if current_uid is nil' do
expect(ldap_identity).to receive(:ensure_normalized_extern_uid)
- ldap_identity.update(extern_uid: nil)
+ ldap_identity.update!(extern_uid: nil)
expect(ldap_identity.extern_uid).to be_nil
end
it 'if extern_uid changed and not nil' do
- ldap_identity.update(extern_uid: 'uid=john1,ou=PEOPLE,dc=example,dc=com')
+ ldap_identity.update!(extern_uid: 'uid=john1,ou=PEOPLE,dc=example,dc=com')
expect(ldap_identity.extern_uid).to eq 'uid=john1,ou=people,dc=example,dc=com'
end
@@ -150,7 +150,7 @@ RSpec.describe Identity do
expect(user.user_synced_attributes_metadata.provider).to eq 'ldapmain'
- ldap_identity.destroy
+ ldap_identity.destroy!
expect(user.reload.user_synced_attributes_metadata).to be_nil
end
@@ -162,7 +162,7 @@ RSpec.describe Identity do
expect(user.user_synced_attributes_metadata.provider).to eq 'other'
- ldap_identity.destroy
+ ldap_identity.destroy!
expect(user.reload.user_synced_attributes_metadata.provider).to eq 'other'
end
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index 1a83d948fcf..de47fb3839a 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -299,7 +299,7 @@ RSpec.describe Integration do
end
context 'when integration is a group-level integration' do
- let(:group_integration) { create(:jira_integration, group: group, project: nil) }
+ let(:group_integration) { create(:jira_integration, :group, group: group) }
it 'sets inherit_from_id from integration' do
integration = described_class.build_from_integration(group_integration, project_id: project.id)
@@ -458,7 +458,7 @@ RSpec.describe Integration do
end
context 'with an active group-level integration' do
- let!(:group_integration) { create(:prometheus_integration, group: group, project: nil, api_url: 'https://prometheus.group.com/') }
+ let!(:group_integration) { create(:prometheus_integration, :group, group: group, api_url: 'https://prometheus.group.com/') }
it 'creates an integration from the group-level integration' do
described_class.create_from_active_default_integrations(project, :project_id)
@@ -481,7 +481,7 @@ RSpec.describe Integration do
end
context 'with an active subgroup' do
- let!(:subgroup_integration) { create(:prometheus_integration, group: subgroup, project: nil, api_url: 'https://prometheus.subgroup.com/') }
+ let!(:subgroup_integration) { create(:prometheus_integration, :group, group: subgroup, api_url: 'https://prometheus.subgroup.com/') }
let!(:subgroup) { create(:group, parent: group) }
let(:project) { create(:project, group: subgroup) }
@@ -509,7 +509,7 @@ RSpec.describe Integration do
end
context 'having an integration inheriting settings' do
- let!(:subgroup_integration) { create(:prometheus_integration, group: subgroup, project: nil, inherit_from_id: group_integration.id, api_url: 'https://prometheus.subgroup.com/') }
+ let!(:subgroup_integration) { create(:prometheus_integration, :group, group: subgroup, inherit_from_id: group_integration.id, api_url: 'https://prometheus.subgroup.com/') }
it 'creates an integration from the group-level integration' do
described_class.create_from_active_default_integrations(sub_subgroup, :group_id)
@@ -552,11 +552,11 @@ RSpec.describe Integration do
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_integration, group: group, project: nil) }
- let_it_be(:subgroup_integration1) { create(:prometheus_integration, group: subgroup1, project: nil, inherit_from_id: group_integration.id) }
- let_it_be(:subgroup_integration2) { create(:prometheus_integration, group: subgroup2, project: nil) }
- let_it_be(:project_integration1) { create(:prometheus_integration, group: nil, project: project1, inherit_from_id: group_integration.id) }
- let_it_be(:project_integration2) { create(:prometheus_integration, group: nil, project: project2, inherit_from_id: subgroup_integration2.id) }
+ let_it_be(:group_integration) { create(:prometheus_integration, :group, group: group) }
+ let_it_be(:subgroup_integration1) { create(:prometheus_integration, :group, group: subgroup1, inherit_from_id: group_integration.id) }
+ let_it_be(:subgroup_integration2) { create(:prometheus_integration, :group, group: subgroup2) }
+ let_it_be(:project_integration1) { create(:prometheus_integration, project: project1, inherit_from_id: group_integration.id) }
+ let_it_be(:project_integration2) { create(:prometheus_integration, 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])
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index 0321b151633..1d81668f97d 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -495,6 +495,18 @@ RSpec.describe Integrations::Jira do
end
end
+ describe '#client' do
+ it 'uses the default GitLab::HTTP timeouts' do
+ timeouts = Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS
+ stub_request(:get, 'http://jira.example.com/foo')
+
+ expect(Gitlab::HTTP).to receive(:httparty_perform_request)
+ .with(Net::HTTP::Get, '/foo', hash_including(timeouts)).and_call_original
+
+ jira_integration.client.get('/foo')
+ end
+ end
+
describe '#find_issue' do
let(:issue_key) { 'JIRA-123' }
let(:issue_url) { "#{url}/rest/api/2/issue/#{issue_key}" }
@@ -503,7 +515,7 @@ RSpec.describe Integrations::Jira do
stub_request(:get, issue_url).with(basic_auth: [username, password])
end
- it 'call the Jira API to get the issue' do
+ it 'calls the Jira API to get the issue' do
jira_integration.find_issue(issue_key)
expect(WebMock).to have_requested(:get, issue_url)
@@ -845,10 +857,14 @@ RSpec.describe Integrations::Jira do
let_it_be(:user) { build_stubbed(:user) }
let(:jira_issue) { ExternalIssue.new('JIRA-123', project) }
+ let(:success_message) { 'SUCCESS: Successfully posted to http://jira.example.com.' }
+ let(:favicon_path) { "http://localhost/assets/#{find_asset('favicon.png').digest_path}" }
subject { jira_integration.create_cross_reference_note(jira_issue, resource, user) }
- shared_examples 'creates a comment on Jira' do
+ shared_examples 'handles cross-references' do
+ let(:resource_name) { jira_integration.send(:noteable_name, resource) }
+ let(:resource_url) { jira_integration.send(:build_entity_url, resource_name, resource.to_param) }
let(:issue_url) { "#{url}/rest/api/2/issue/JIRA-123" }
let(:comment_url) { "#{issue_url}/comment" }
let(:remote_link_url) { "#{issue_url}/remotelink" }
@@ -860,12 +876,77 @@ RSpec.describe Integrations::Jira do
stub_request(:post, remote_link_url).with(basic_auth: [username, password])
end
- it 'creates a comment on Jira' do
- subject
+ context 'when enabled' do
+ before do
+ allow(jira_integration).to receive(:can_cross_reference?) { true }
+ end
- expect(WebMock).to have_requested(:post, comment_url).with(
- body: /mentioned this issue in/
- ).once
+ it 'creates a comment and remote link' do
+ expect(subject).to eq(success_message)
+ expect(WebMock).to have_requested(:post, comment_url).with(body: comment_body).once
+ expect(WebMock).to have_requested(:post, remote_link_url).with(
+ body: hash_including(
+ GlobalID: 'GitLab',
+ relationship: 'mentioned on',
+ object: {
+ url: resource_url,
+ title: "#{resource.model_name.human} - #{resource.title}",
+ icon: { title: 'GitLab', url16x16: favicon_path },
+ status: { resolved: false }
+ }
+ )
+ ).once
+ end
+
+ context 'when comment already exists' do
+ before do
+ allow(jira_integration).to receive(:comment_exists?) { true }
+ end
+
+ it 'does not create a comment or remote link' do
+ expect(subject).to be_nil
+ expect(WebMock).not_to have_requested(:post, comment_url)
+ expect(WebMock).not_to have_requested(:post, remote_link_url)
+ end
+ end
+
+ context 'when remote link already exists' do
+ let(:link) { double(object: { 'url' => resource_url }) }
+
+ before do
+ allow(jira_integration).to receive(:find_remote_link).and_return(link)
+ end
+
+ it 'updates the remote link but does not create a comment' do
+ expect(link).to receive(:save!)
+ expect(subject).to eq(success_message)
+ expect(WebMock).not_to have_requested(:post, comment_url)
+ end
+ end
+ end
+
+ context 'when disabled' do
+ before do
+ allow(jira_integration).to receive(:can_cross_reference?) { false }
+ end
+
+ it 'does not create a comment or remote link' do
+ expect(subject).to eq("Events for #{resource_name.pluralize.humanize(capitalize: false)} are disabled.")
+ expect(WebMock).not_to have_requested(:post, comment_url)
+ expect(WebMock).not_to have_requested(:post, remote_link_url)
+ end
+ end
+
+ context 'with jira_use_first_ref_by_oid feature flag disabled' do
+ before do
+ stub_feature_flags(jira_use_first_ref_by_oid: false)
+ end
+
+ it 'creates a comment and remote link on Jira' do
+ expect(subject).to eq(success_message)
+ expect(WebMock).to have_requested(:post, comment_url).with(body: comment_body).once
+ expect(WebMock).to have_requested(:post, remote_link_url).once
+ end
end
it 'tracks usage' do
@@ -877,39 +958,38 @@ RSpec.describe Integrations::Jira do
end
end
- context 'when resource is a commit' do
- let(:resource) { project.commit('master') }
-
- context 'when disabled' do
- before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:commit_events) { false }
- end
- end
-
- it { is_expected.to eq('Events for commits are disabled.') }
+ context 'for commits' do
+ it_behaves_like 'handles cross-references' do
+ let(:resource) { project.commit('master') }
+ let(:comment_body) { /mentioned this issue in \[a commit\|.* on branch \[master\|/ }
end
+ end
- context 'when enabled' do
- it_behaves_like 'creates a comment on Jira'
+ context 'for issues' do
+ it_behaves_like 'handles cross-references' do
+ let(:resource) { build_stubbed(:issue, project: project) }
+ let(:comment_body) { /mentioned this issue in \[a issue\|/ }
end
end
- context 'when resource is a merge request' do
- let(:resource) { build_stubbed(:merge_request, source_project: project) }
-
- context 'when disabled' do
- before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:merge_requests_events) { false }
- end
- end
+ context 'for merge requests' do
+ it_behaves_like 'handles cross-references' do
+ let(:resource) { build_stubbed(:merge_request, source_project: project) }
+ let(:comment_body) { /mentioned this issue in \[a merge request\|.* on branch \[master\|/ }
+ end
+ end
- it { is_expected.to eq('Events for merge requests are disabled.') }
+ context 'for notes' do
+ it_behaves_like 'handles cross-references' do
+ let(:resource) { build_stubbed(:note, project: project) }
+ let(:comment_body) { /mentioned this issue in \[a note\|/ }
end
+ end
- context 'when enabled' do
- it_behaves_like 'creates a comment on Jira'
+ context 'for snippets' do
+ it_behaves_like 'handles cross-references' do
+ let(:resource) { build_stubbed(:snippet, project: project) }
+ let(:comment_body) { /mentioned this issue in \[a snippet\|/ }
end
end
end
@@ -946,7 +1026,9 @@ RSpec.describe Integrations::Jira do
expect(jira_integration).to receive(:log_error).with(
'Error sending message',
client_url: 'http://jira.example.com',
- error: error_message
+ 'exception.class' => anything,
+ 'exception.message' => error_message,
+ 'exception.backtrace' => anything
)
expect(jira_integration.test(nil)).to eq(success: false, result: error_message)
diff --git a/spec/models/integrations/pipelines_email_spec.rb b/spec/models/integrations/pipelines_email_spec.rb
index afd9d71ebc4..d70f104b965 100644
--- a/spec/models/integrations/pipelines_email_spec.rb
+++ b/spec/models/integrations/pipelines_email_spec.rb
@@ -35,6 +35,42 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do
it { is_expected.not_to validate_presence_of(:recipients) }
end
+
+ describe 'validates number of recipients' do
+ before do
+ stub_const("#{described_class}::RECIPIENTS_LIMIT", 2)
+ end
+
+ subject(:integration) { described_class.new(project: project, recipients: recipients, active: true) }
+
+ context 'valid number of recipients' do
+ let(:recipients) { 'foo@bar.com, , ' }
+
+ it 'does not count empty emails' do
+ is_expected.to be_valid
+ end
+ end
+
+ context 'invalid number of recipients' do
+ let(:recipients) { 'foo@bar.com bar@foo.com bob@gitlab.com' }
+
+ it { is_expected.not_to be_valid }
+
+ it 'adds an error message' do
+ integration.valid?
+
+ expect(integration.errors).to contain_exactly('Recipients can\'t exceed 2')
+ end
+
+ context 'when integration is not active' do
+ before do
+ integration.active = false
+ end
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
end
shared_examples 'sending email' do |branches_to_be_notified: nil|
@@ -50,7 +86,7 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do
it 'sends email' do
emails = receivers.map { |r| double(notification_email_or_default: r) }
- should_only_email(*emails, kind: :bcc)
+ should_only_email(*emails)
end
end
diff --git a/spec/models/integrations/shimo_spec.rb b/spec/models/integrations/shimo_spec.rb
new file mode 100644
index 00000000000..25df8d2b249
--- /dev/null
+++ b/spec/models/integrations/shimo_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Integrations::Shimo do
+ describe '#fields' do
+ let(:shimo_integration) { create(:shimo_integration) }
+
+ it 'returns custom fields' do
+ expect(shimo_integration.fields.pluck(:name)).to eq(%w[external_wiki_url])
+ end
+ end
+
+ describe '#create' do
+ let(:project) { create(:project, :repository) }
+ let(:external_wiki_url) { 'https://shimo.example.com/desktop' }
+ let(:params) { { active: true, project: project, external_wiki_url: external_wiki_url } }
+
+ context 'with valid params' do
+ it 'creates the Shimo integration' do
+ shimo = described_class.create!(params)
+
+ expect(shimo.valid?).to be true
+ expect(shimo.render?).to be true
+ expect(shimo.external_wiki_url).to eq(external_wiki_url)
+ end
+ end
+
+ context 'with invalid params' do
+ it 'cannot create the Shimo integration without external_wiki_url' do
+ params['external_wiki_url'] = nil
+ expect { described_class.create!(params) }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+
+ it 'cannot create the Shimo integration with invalid external_wiki_url' do
+ params['external_wiki_url'] = 'Fake Invalid URL'
+ expect { described_class.create!(params) }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/spec/models/integrations/zentao_spec.rb b/spec/models/integrations/zentao_spec.rb
index a1503ecc092..2b0532c7930 100644
--- a/spec/models/integrations/zentao_spec.rb
+++ b/spec/models/integrations/zentao_spec.rb
@@ -50,4 +50,10 @@ RSpec.describe Integrations::Zentao do
expect(zentao_integration.test).to eq(test_response)
end
end
+
+ describe '#help' do
+ it 'renders prompt information' do
+ expect(zentao_integration.help).not_to be_empty
+ end
+ end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 4319407706e..ba4429451d1 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -34,7 +34,8 @@ RSpec.describe Issue do
it { is_expected.to have_many(:issue_email_participants) }
it { is_expected.to have_many(:timelogs).autosave(true) }
it { is_expected.to have_one(:incident_management_issuable_escalation_status) }
- it { is_expected.to have_and_belong_to_many(:customer_relations_contacts) }
+ it { is_expected.to have_many(:issue_customer_relations_contacts) }
+ it { is_expected.to have_many(:customer_relations_contacts) }
describe 'versions.most_recent' do
it 'returns the most recent version' do
@@ -305,7 +306,7 @@ RSpec.describe Issue do
end
describe '#reopen' do
- let(:issue) { create(:issue, project: reusable_project, state: 'closed', closed_at: Time.current, closed_by: user) }
+ let_it_be_with_reload(:issue) { create(:issue, project: reusable_project, state: 'closed', closed_at: Time.current, closed_by: user) }
it 'sets closed_at to nil when an issue is reopened' do
expect { issue.reopen }.to change { issue.closed_at }.to(nil)
@@ -315,6 +316,22 @@ RSpec.describe Issue do
expect { issue.reopen }.to change { issue.closed_by }.from(user).to(nil)
end
+ it 'clears moved_to_id for moved issues' do
+ moved_issue = create(:issue)
+
+ issue.update!(moved_to_id: moved_issue.id)
+
+ expect { issue.reopen }.to change { issue.moved_to_id }.from(moved_issue.id).to(nil)
+ end
+
+ it 'clears duplicated_to_id for duplicated issues' do
+ duplicate_issue = create(:issue)
+
+ issue.update!(duplicated_to_id: duplicate_issue.id)
+
+ expect { issue.reopen }.to change { issue.duplicated_to_id }.from(duplicate_issue.id).to(nil)
+ end
+
it 'changes the state to opened' do
expect { issue.reopen }.to change { issue.state_id }.from(described_class.available_states[:closed]).to(described_class.available_states[:opened])
end
@@ -1218,7 +1235,7 @@ RSpec.describe Issue do
end
it 'returns public and hidden issues' do
- expect(described_class.public_only).to eq([public_issue, hidden_issue])
+ expect(described_class.public_only).to contain_exactly(public_issue, hidden_issue)
end
end
end
@@ -1247,7 +1264,7 @@ RSpec.describe Issue do
end
it 'returns public and hidden issues' do
- expect(described_class.without_hidden).to eq([public_issue, hidden_issue])
+ expect(described_class.without_hidden).to contain_exactly(public_issue, hidden_issue)
end
end
end
diff --git a/spec/models/jira_import_state_spec.rb b/spec/models/jira_import_state_spec.rb
index e982b7353ba..a272d001429 100644
--- a/spec/models/jira_import_state_spec.rb
+++ b/spec/models/jira_import_state_spec.rb
@@ -175,7 +175,7 @@ RSpec.describe JiraImportState do
let(:jira_import) { build(:jira_import_state, project: project)}
it 'does not run the callback', :aggregate_failures do
- expect { jira_import.save }.to change { JiraImportState.count }.by(1)
+ expect { jira_import.save! }.to change { JiraImportState.count }.by(1)
expect(jira_import.reload.error_message).to be_nil
end
end
@@ -184,7 +184,7 @@ RSpec.describe JiraImportState do
let(:jira_import) { build(:jira_import_state, project: project, error_message: 'error')}
it 'does not run the callback', :aggregate_failures do
- expect { jira_import.save }.to change { JiraImportState.count }.by(1)
+ expect { jira_import.save! }.to change { JiraImportState.count }.by(1)
expect(jira_import.reload.error_message).to eq('error')
end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 7468c1b9f0a..d41a1604211 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -85,9 +85,9 @@ RSpec.describe Key, :mailer do
let_it_be(:expiring_soon_notified) { create(:key, expires_at: 4.days.from_now, user: user, before_expiry_notification_delivered_at: Time.current) }
let_it_be(:future_expiry) { create(:key, expires_at: 1.month.from_now, user: user) }
- describe '.expired_and_not_notified' do
+ describe '.expired_today_and_not_notified' do
it 'returns keys that expire today and in the past' do
- expect(described_class.expired_and_not_notified).to contain_exactly(expired_today_not_notified, expired_yesterday)
+ expect(described_class.expired_today_and_not_notified).to contain_exactly(expired_today_not_notified)
end
end
diff --git a/spec/models/loose_foreign_keys/deleted_record_spec.rb b/spec/models/loose_foreign_keys/deleted_record_spec.rb
new file mode 100644
index 00000000000..cd5068bdb52
--- /dev/null
+++ b/spec/models/loose_foreign_keys/deleted_record_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe LooseForeignKeys::DeletedRecord, type: :model do
+ let_it_be(:table) { 'public.projects' }
+
+ let_it_be(:deleted_record_1) { described_class.create!(partition: 1, fully_qualified_table_name: table, primary_key_value: 5) }
+ let_it_be(:deleted_record_2) { described_class.create!(partition: 1, fully_qualified_table_name: table, primary_key_value: 1) }
+ let_it_be(:deleted_record_3) { described_class.create!(partition: 1, fully_qualified_table_name: 'public.other_table', primary_key_value: 3) }
+ let_it_be(:deleted_record_4) { described_class.create!(partition: 1, fully_qualified_table_name: table, primary_key_value: 1) } # duplicate
+
+ describe '.load_batch_for_table' do
+ it 'loads records and orders them by creation date' do
+ records = described_class.load_batch_for_table(table, 10)
+
+ expect(records).to eq([deleted_record_1, deleted_record_2, deleted_record_4])
+ end
+
+ it 'supports configurable batch size' do
+ records = described_class.load_batch_for_table(table, 2)
+
+ expect(records).to eq([deleted_record_1, deleted_record_2])
+ end
+ end
+
+ describe '.mark_records_processed' do
+ it 'updates all records' do
+ described_class.mark_records_processed([deleted_record_1, deleted_record_2, deleted_record_4])
+
+ expect(described_class.status_pending.count).to eq(1)
+ expect(described_class.status_processed.count).to eq(3)
+ end
+ end
+end
diff --git a/spec/models/loose_foreign_keys/modification_tracker_spec.rb b/spec/models/loose_foreign_keys/modification_tracker_spec.rb
new file mode 100644
index 00000000000..069ccf85141
--- /dev/null
+++ b/spec/models/loose_foreign_keys/modification_tracker_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe LooseForeignKeys::ModificationTracker do
+ subject(:tracker) { described_class.new }
+
+ describe '#over_limit?' do
+ it 'is true when deletion MAX_DELETES is exceeded' do
+ stub_const('LooseForeignKeys::ModificationTracker::MAX_DELETES', 5)
+
+ tracker.add_deletions('issues', 10)
+ expect(tracker).to be_over_limit
+ end
+
+ it 'is false when MAX_DELETES is not exceeded' do
+ tracker.add_deletions('issues', 3)
+
+ expect(tracker).not_to be_over_limit
+ end
+
+ it 'is true when deletion MAX_UPDATES is exceeded' do
+ stub_const('LooseForeignKeys::ModificationTracker::MAX_UPDATES', 5)
+
+ tracker.add_updates('issues', 3)
+ tracker.add_updates('issues', 4)
+
+ expect(tracker).to be_over_limit
+ end
+
+ it 'is false when MAX_UPDATES is not exceeded' do
+ tracker.add_updates('projects', 3)
+
+ expect(tracker).not_to be_over_limit
+ end
+
+ it 'is true when max runtime is exceeded' do
+ monotonic_time_before = 1 # this will be the start time
+ monotonic_time_after = described_class::MAX_RUNTIME.to_i + 1 # this will be returned when over_limit? is called
+
+ allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
+
+ tracker
+
+ expect(tracker).to be_over_limit
+ end
+
+ it 'is false when max runtime is not exceeded' do
+ expect(tracker).not_to be_over_limit
+ end
+ end
+
+ describe '#add_deletions' do
+ it 'increments a Prometheus counter' do
+ counter = Gitlab::Metrics.registry.get(:loose_foreign_key_deletions)
+
+ subject.add_deletions(:users, 4)
+
+ expect(counter.get(table: :users)).to eq(4)
+ end
+ end
+
+ describe '#add_updates' do
+ it 'increments a Prometheus counter' do
+ counter = Gitlab::Metrics.registry.get(:loose_foreign_key_updates)
+
+ subject.add_updates(:users, 4)
+
+ expect(counter.get(table: :users)).to eq(4)
+ end
+ end
+
+ describe '#stats' do
+ it 'exposes stats' do
+ freeze_time do
+ tracker
+ tracker.add_deletions('issues', 5)
+ tracker.add_deletions('issues', 2)
+ tracker.add_deletions('projects', 2)
+
+ tracker.add_updates('projects', 3)
+
+ expect(tracker.stats).to eq({
+ over_limit: false,
+ delete_count_by_table: { 'issues' => 7, 'projects' => 2 },
+ update_count_by_table: { 'projects' => 3 },
+ delete_count: 9,
+ update_count: 3
+ })
+ end
+ end
+ end
+end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index afe78adc547..abff1815f1a 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe Member do
describe 'Associations' do
it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_one(:member_task) }
end
describe 'Validation' do
@@ -678,6 +679,19 @@ RSpec.describe Member do
expect(member.invite_token).not_to be_nil
expect_any_instance_of(Member).not_to receive(:after_accept_invite)
end
+
+ it 'schedules a TasksToBeDone::CreateWorker task' do
+ stub_experiments(invite_members_for_task: true)
+
+ member_task = create(:member_task, member: member, project: member.project)
+
+ expect(TasksToBeDone::CreateWorker)
+ .to receive(:perform_async)
+ .with(member_task.id, member.created_by_id, [user.id])
+ .once
+
+ member.accept_invite!(user)
+ end
end
describe '#decline_invite!' do
diff --git a/spec/models/members/member_task_spec.rb b/spec/models/members/member_task_spec.rb
new file mode 100644
index 00000000000..b06aa05c255
--- /dev/null
+++ b/spec/models/members/member_task_spec.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MemberTask do
+ describe 'Associations' do
+ it { is_expected.to belong_to(:member) }
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'Validations' do
+ it { is_expected.to validate_presence_of(:member) }
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_inclusion_of(:tasks).in_array(MemberTask::TASKS.values) }
+
+ describe 'unique tasks validation' do
+ subject do
+ build(:member_task, tasks: [0, 0])
+ end
+
+ it 'expects the task values to be unique' do
+ expect(subject).to be_invalid
+ expect(subject.errors[:tasks]).to include('are not unique')
+ end
+ end
+
+ describe 'project validations' do
+ let_it_be(:project) { create(:project) }
+
+ subject do
+ build(:member_task, member: member, project: project, tasks_to_be_done: [:ci, :code])
+ end
+
+ context 'when the member source is a group' do
+ let_it_be(:member) { create(:group_member) }
+
+ it "expects the project to be part of the member's group projects" do
+ expect(subject).to be_invalid
+ expect(subject.errors[:project]).to include('is not in the member group')
+ end
+
+ context "when the project is part of the member's group projects" do
+ let_it_be(:project) { create(:project, namespace: member.source) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'when the member source is a project' do
+ let_it_be(:member) { create(:project_member) }
+
+ it "expects the project to be the member's project" do
+ expect(subject).to be_invalid
+ expect(subject.errors[:project]).to include('is not the member project')
+ end
+
+ context "when the project is the member's project" do
+ let_it_be(:project) { member.source }
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
+ end
+
+ describe '.for_members' do
+ it 'returns the member_tasks for multiple members' do
+ member1 = create(:group_member)
+ member_task1 = create(:member_task, member: member1)
+ create(:member_task)
+ expect(described_class.for_members([member1])).to match_array([member_task1])
+ end
+ end
+
+ describe '#tasks_to_be_done' do
+ subject { member_task.tasks_to_be_done }
+
+ let_it_be(:member_task) { build(:member_task) }
+
+ before do
+ member_task[:tasks] = [0, 1]
+ end
+
+ it 'returns an array of symbols for the corresponding integers' do
+ expect(subject).to match_array([:ci, :code])
+ end
+ end
+
+ describe '#tasks_to_be_done=' do
+ let_it_be(:member_task) { build(:member_task) }
+
+ context 'when passing valid values' do
+ subject { member_task[:tasks] }
+
+ before do
+ member_task.tasks_to_be_done = tasks
+ end
+
+ context 'when passing tasks as strings' do
+ let_it_be(:tasks) { %w(ci code) }
+
+ it 'sets an array of integers for the corresponding tasks' do
+ expect(subject).to match_array([0, 1])
+ end
+ end
+
+ context 'when passing a single task' do
+ let_it_be(:tasks) { :ci }
+
+ it 'sets an array of integers for the corresponding tasks' do
+ expect(subject).to match_array([1])
+ end
+ end
+
+ context 'when passing a task twice' do
+ let_it_be(:tasks) { %w(ci ci) }
+
+ it 'is set only once' do
+ expect(subject).to match_array([1])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index ca846cf9e8e..031caefbd43 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -256,59 +256,5 @@ RSpec.describe ProjectMember do
it_behaves_like 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations'
end
-
- context 'when the feature flag `specialized_service_for_project_member_auth_refresh` is disabled' do
- before do
- stub_feature_flags(specialized_service_for_project_member_auth_refresh: false)
- end
-
- shared_examples_for 'calls UserProjectAccessChangedService to recalculate authorizations' do
- it 'calls UserProjectAccessChangedService' do
- expect_next_instance_of(UserProjectAccessChangedService, user.id) do |service|
- expect(service).to receive(:execute)
- end
-
- action
- end
- end
-
- context 'on create' do
- let(:action) { project.add_user(user, Gitlab::Access::GUEST) }
-
- it 'changes access level' do
- expect { action }.to change { user.can?(:guest_access, project) }.from(false).to(true)
- end
-
- it_behaves_like 'calls UserProjectAccessChangedService to recalculate authorizations'
- end
-
- context 'on update' do
- let(:action) { project.members.find_by(user: user).update!(access_level: Gitlab::Access::DEVELOPER) }
-
- before do
- project.add_user(user, Gitlab::Access::GUEST)
- end
-
- it 'changes access level' do
- expect { action }.to change { user.can?(:developer_access, project) }.from(false).to(true)
- end
-
- it_behaves_like 'calls UserProjectAccessChangedService to recalculate authorizations'
- end
-
- context 'on destroy' do
- let(:action) { project.members.find_by(user: user).destroy! }
-
- before do
- project.add_user(user, Gitlab::Access::GUEST)
- end
-
- it 'changes access level', :sidekiq_inline do
- expect { action }.to change { user.can?(:guest_access, project) }.from(true).to(false)
- end
-
- it_behaves_like 'calls UserProjectAccessChangedService to recalculate authorizations'
- end
- end
end
end
diff --git a/spec/models/merge_request_assignee_spec.rb b/spec/models/merge_request_assignee_spec.rb
index d287392bf7f..5bb8e7184a3 100644
--- a/spec/models/merge_request_assignee_spec.rb
+++ b/spec/models/merge_request_assignee_spec.rb
@@ -37,4 +37,8 @@ RSpec.describe MergeRequestAssignee do
end
end
end
+
+ it_behaves_like 'having unique enum values'
+
+ it_behaves_like 'having reviewer state'
end
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
index adddec7ced8..25e5e40feb7 100644
--- a/spec/models/merge_request_diff_commit_spec.rb
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -46,11 +46,7 @@ RSpec.describe MergeRequestDiffCommit do
{
"message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
"authored_date": "2014-02-27T10:01:38.000+01:00".to_time,
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T10:01:38.000+01:00".to_time,
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author_id": an_instance_of(Integer),
"committer_id": an_instance_of(Integer),
"merge_request_diff_id": merge_request_diff_id,
@@ -61,11 +57,7 @@ RSpec.describe MergeRequestDiffCommit do
{
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
"authored_date": "2014-02-27T09:57:31.000+01:00".to_time,
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:57:31.000+01:00".to_time,
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author_id": an_instance_of(Integer),
"committer_id": an_instance_of(Integer),
"merge_request_diff_id": merge_request_diff_id,
@@ -79,7 +71,7 @@ RSpec.describe MergeRequestDiffCommit do
subject { described_class.create_bulk(merge_request_diff_id, commits) }
it 'inserts the commits into the database en masse' do
- expect(Gitlab::Database.main).to receive(:bulk_insert)
+ expect(ApplicationRecord).to receive(:legacy_bulk_insert)
.with(described_class.table_name, rows)
subject
@@ -111,11 +103,7 @@ RSpec.describe MergeRequestDiffCommit do
[{
"message": "Weird commit date\n",
"authored_date": timestamp,
- "author_name": "Alejandro Rodríguez",
- "author_email": "alejorro70@gmail.com",
"committed_date": timestamp,
- "committer_name": "Alejandro Rodríguez",
- "committer_email": "alejorro70@gmail.com",
"commit_author_id": an_instance_of(Integer),
"committer_id": an_instance_of(Integer),
"merge_request_diff_id": merge_request_diff_id,
@@ -126,7 +114,7 @@ RSpec.describe MergeRequestDiffCommit do
end
it 'uses a sanitized date' do
- expect(Gitlab::Database.main).to receive(:bulk_insert)
+ expect(ApplicationRecord).to receive(:legacy_bulk_insert)
.with(described_class.table_name, rows)
subject
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 5fff880c44e..afe7251f59a 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -240,8 +240,8 @@ RSpec.describe MergeRequestDiff do
stub_external_diffs_setting(enabled: true)
expect(diff).not_to receive(:save!)
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
+ expect(ApplicationRecord)
+ .to receive(:legacy_bulk_insert)
.with('merge_request_diff_files', anything)
.and_raise(ActiveRecord::Rollback)
@@ -1080,6 +1080,22 @@ RSpec.describe MergeRequestDiff do
end
end
+ describe '#commits' do
+ include ProjectForksHelper
+
+ let_it_be(:target) { create(:project, :test_repo) }
+ let_it_be(:forked) { fork_project(target, nil, repository: true) }
+ let_it_be(:mr) { create(:merge_request, source_project: forked, target_project: target) }
+
+ it 'returns a CommitCollection whose container points to the target project' do
+ expect(mr.merge_request_diff.commits.container).to eq(target)
+ end
+
+ it 'returns a non-empty CommitCollection' do
+ expect(mr.merge_request_diff.commits.commits.size).to be > 0
+ end
+ end
+
describe '.latest_diff_for_merge_requests' do
let_it_be(:merge_request_1) { create(:merge_request_without_merge_request_diff) }
let_it_be(:merge_request_1_diff_1) { create(:merge_request_diff, merge_request: merge_request_1, created_at: 3.days.ago) }
diff --git a/spec/models/merge_request_reviewer_spec.rb b/spec/models/merge_request_reviewer_spec.rb
index 76b44abca54..d69d60c94f0 100644
--- a/spec/models/merge_request_reviewer_spec.rb
+++ b/spec/models/merge_request_reviewer_spec.rb
@@ -7,6 +7,10 @@ RSpec.describe MergeRequestReviewer do
subject { merge_request.merge_request_reviewers.build(reviewer: create(:user)) }
+ it_behaves_like 'having unique enum values'
+
+ it_behaves_like 'having reviewer state'
+
describe 'associations' do
it { is_expected.to belong_to(:merge_request).class_name('MergeRequest') }
it { is_expected.to belong_to(:reviewer).class_name('User').inverse_of(:merge_request_reviewers) }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index d871453e062..5618fb06157 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1638,6 +1638,22 @@ RSpec.describe MergeRequest, factory_default: :keep do
expect(request.default_merge_commit_message)
.not_to match("By removing all code\n\n")
end
+
+ it 'uses template from target project' do
+ request = build(:merge_request, title: 'Fix everything')
+ subject.target_project.merge_commit_template = '%{title}'
+
+ expect(request.default_merge_commit_message)
+ .to eq('Fix everything')
+ end
+
+ it 'ignores template when include_description is true' do
+ request = build(:merge_request, title: 'Fix everything')
+ subject.target_project.merge_commit_template = '%{title}'
+
+ expect(request.default_merge_commit_message(include_description: true))
+ .to match("See merge request #{request.to_reference(full: true)}")
+ end
end
describe "#auto_merge_strategy" do
@@ -2904,6 +2920,8 @@ RSpec.describe MergeRequest, factory_default: :keep do
params = {}
merge_jid = 'hash-123'
+ allow(MergeWorker).to receive(:with_status).and_return(MergeWorker)
+
expect(merge_request).to receive(:expire_etag_cache)
expect(MergeWorker).to receive(:perform_async).with(merge_request.id, user_id, params) do
merge_jid
@@ -2922,6 +2940,10 @@ RSpec.describe MergeRequest, factory_default: :keep do
subject(:execute) { merge_request.rebase_async(user_id) }
+ before do
+ allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker)
+ end
+
it 'atomically enqueues a RebaseWorker job and updates rebase_jid' do
expect(RebaseWorker)
.to receive(:perform_async)
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index c201d89947e..8f5860c799c 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -28,6 +28,41 @@ RSpec.describe Namespace do
it { is_expected.to have_one :onboarding_progress }
it { is_expected.to have_one :admin_note }
it { is_expected.to have_many :pending_builds }
+
+ describe '#children' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:project_with_namespace) { create(:project, namespace: group) }
+
+ it 'excludes project namespaces' do
+ expect(project_with_namespace.project_namespace.parent).to eq(group)
+ expect(group.children).to match_array([subgroup])
+ end
+ end
+ end
+
+ shared_examples 'validations called by different namespace types' do |method|
+ using RSpec::Parameterized::TableSyntax
+
+ where(:namespace_type, :call_validation) do
+ :namespace | true
+ :group | true
+ :user_namespace | true
+ :project_namespace | false
+ end
+
+ with_them do
+ it 'conditionally runs given validation' do
+ namespace = build(namespace_type)
+ if call_validation
+ expect(namespace).to receive(method)
+ else
+ expect(namespace).not_to receive(method)
+ end
+
+ namespace.valid?
+ end
+ end
end
describe 'validations' do
@@ -50,10 +85,10 @@ RSpec.describe Namespace do
ref(:project_sti_name) | ref(:user_sti_name) | 'project namespace cannot be the parent of another namespace'
ref(:project_sti_name) | ref(:group_sti_name) | 'project namespace cannot be the parent of another namespace'
ref(:project_sti_name) | ref(:project_sti_name) | 'project namespace cannot be the parent of another namespace'
- ref(:group_sti_name) | ref(:user_sti_name) | 'cannot not be used for user namespace'
+ ref(:group_sti_name) | ref(:user_sti_name) | 'cannot be used for user namespace'
ref(:group_sti_name) | ref(:group_sti_name) | nil
ref(:group_sti_name) | ref(:project_sti_name) | nil
- ref(:user_sti_name) | ref(:user_sti_name) | 'cannot not be used for user namespace'
+ ref(:user_sti_name) | ref(:user_sti_name) | 'cannot be used for user namespace'
ref(:user_sti_name) | ref(:group_sti_name) | 'user namespace cannot be the parent of another namespace'
ref(:user_sti_name) | ref(:project_sti_name) | nil
end
@@ -102,14 +137,20 @@ RSpec.describe Namespace do
end
end
- it 'does not allow too deep nesting' do
- ancestors = (1..21).to_a
- group = build(:group)
+ describe '#nesting_level_allowed' do
+ context 'for a group' do
+ it 'does not allow too deep nesting' do
+ ancestors = (1..21).to_a
+ group = build(:group)
+
+ allow(group).to receive(:ancestors).and_return(ancestors)
- allow(group).to receive(:ancestors).and_return(ancestors)
+ expect(group).not_to be_valid
+ expect(group.errors[:parent_id].first).to eq('has too deep level of nesting')
+ end
+ end
- expect(group).not_to be_valid
- expect(group.errors[:parent_id].first).to eq('has too deep level of nesting')
+ it_behaves_like 'validations called by different namespace types', :nesting_level_allowed
end
describe 'reserved path validation' do
@@ -188,7 +229,7 @@ RSpec.describe Namespace do
expect(namespace.path).to eq('j')
- namespace.update(name: 'something new')
+ namespace.update!(name: 'something new')
expect(namespace).to be_valid
expect(namespace.name).to eq('something new')
@@ -199,8 +240,10 @@ RSpec.describe Namespace do
let(:namespace) { build(:project_namespace) }
it 'allows to update path to single char' do
- namespace = create(:project_namespace)
- namespace.update(path: 'j')
+ project = create(:project)
+ namespace = project.project_namespace
+
+ namespace.update!(path: 'j')
expect(namespace).to be_valid
end
@@ -244,7 +287,7 @@ RSpec.describe Namespace do
end
end
- context 'creating a default Namespace' do
+ context 'creating a Namespace with nil type' do
let(:namespace_type) { nil }
it 'is the correct type of namespace' do
@@ -255,7 +298,7 @@ RSpec.describe Namespace do
end
context 'creating an unknown Namespace type' do
- let(:namespace_type) { 'One' }
+ let(:namespace_type) { 'nonsense' }
it 'creates a default Namespace' do
expect(Namespace.find(namespace.id)).to be_a(Namespace)
@@ -273,8 +316,8 @@ RSpec.describe Namespace do
describe '.by_parent' do
it 'includes correct namespaces' do
- expect(described_class.by_parent(namespace1.id)).to eq([namespace1sub])
- expect(described_class.by_parent(namespace2.id)).to eq([namespace2sub])
+ expect(described_class.by_parent(namespace1.id)).to match_array([namespace1sub])
+ expect(described_class.by_parent(namespace2.id)).to match_array([namespace2sub])
expect(described_class.by_parent(nil)).to match_array([namespace, namespace1, namespace2])
end
end
@@ -302,9 +345,13 @@ RSpec.describe Namespace do
describe '.without_project_namespaces' do
let_it_be(:user_namespace) { create(:user_namespace) }
- let_it_be(:project_namespace) { create(:project_namespace) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project_namespace) { project.project_namespace }
it 'excludes project namespaces' do
+ expect(project_namespace).not_to be_nil
+ expect(project_namespace.parent).not_to be_nil
+ expect(described_class.all).to include(project_namespace)
expect(described_class.without_project_namespaces).to match_array([namespace, namespace1, namespace2, namespace1sub, namespace2sub, user_namespace, project_namespace.parent])
end
end
@@ -519,6 +566,25 @@ RSpec.describe Namespace do
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_group])
end
+
+ context 'with project namespaces' do
+ let_it_be(:project) { create(:project, namespace: parent_group, path: 'some-new-path') }
+ let_it_be(:project_namespace) { project.project_namespace }
+
+ it 'does not return project namespace' do
+ search_result = described_class.search('path')
+
+ expect(search_result).not_to include(project_namespace)
+ expect(search_result).to match_array([first_group, parent_group, second_group])
+ end
+
+ it 'does not return project namespace when including parents' do
+ search_result = described_class.search('path', include_parents: true)
+
+ expect(search_result).not_to include(project_namespace)
+ expect(search_result).to match_array([first_group, parent_group, second_group])
+ end
+ end
end
describe '.with_statistics' do
@@ -528,26 +594,30 @@ RSpec.describe Namespace do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
- namespace: namespace,
- repository_size: 101,
- wiki_size: 505,
- lfs_objects_size: 202,
- build_artifacts_size: 303,
- packages_size: 404,
- snippets_size: 605))
+ namespace: namespace,
+ repository_size: 101,
+ wiki_size: 505,
+ lfs_objects_size: 202,
+ build_artifacts_size: 303,
+ pipeline_artifacts_size: 707,
+ packages_size: 404,
+ snippets_size: 605,
+ uploads_size: 808))
end
let(:project2) do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
- namespace: namespace,
- repository_size: 10,
- wiki_size: 50,
- lfs_objects_size: 20,
- build_artifacts_size: 30,
- packages_size: 40,
- snippets_size: 60))
+ namespace: namespace,
+ repository_size: 10,
+ wiki_size: 50,
+ lfs_objects_size: 20,
+ build_artifacts_size: 30,
+ pipeline_artifacts_size: 70,
+ packages_size: 40,
+ snippets_size: 60,
+ uploads_size: 80))
end
it "sums all project storage counters in the namespace" do
@@ -555,13 +625,15 @@ RSpec.describe Namespace do
project2
statistics = described_class.with_statistics.find(namespace.id)
- expect(statistics.storage_size).to eq 2330
+ expect(statistics.storage_size).to eq 3995
expect(statistics.repository_size).to eq 111
expect(statistics.wiki_size).to eq 555
expect(statistics.lfs_objects_size).to eq 222
expect(statistics.build_artifacts_size).to eq 333
+ expect(statistics.pipeline_artifacts_size).to eq 777
expect(statistics.packages_size).to eq 444
expect(statistics.snippets_size).to eq 665
+ expect(statistics.uploads_size).to eq 888
end
it "correctly handles namespaces without projects" do
@@ -572,8 +644,10 @@ RSpec.describe Namespace do
expect(statistics.wiki_size).to eq 0
expect(statistics.lfs_objects_size).to eq 0
expect(statistics.build_artifacts_size).to eq 0
+ expect(statistics.pipeline_artifacts_size).to eq 0
expect(statistics.packages_size).to eq 0
expect(statistics.snippets_size).to eq 0
+ expect(statistics.uploads_size).to eq 0
end
end
@@ -673,7 +747,7 @@ RSpec.describe Namespace do
end
it "moves dir if path changed" do
- namespace.update(path: namespace.full_path + '_new')
+ namespace.update!(path: namespace.full_path + '_new')
expect(gitlab_shell.repository_exists?(project.repository_storage, "#{namespace.path}/#{project.path}.git")).to be_truthy
end
@@ -684,7 +758,7 @@ RSpec.describe Namespace do
expect(namespace).to receive(:write_projects_repository_config).and_raise('foo')
expect do
- namespace.update(path: namespace.full_path + '_new')
+ namespace.update!(path: namespace.full_path + '_new')
end.to raise_error('foo')
end
end
@@ -701,7 +775,7 @@ RSpec.describe Namespace do
end
expect(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false) # like prod
- namespace.update(path: namespace.full_path + '_new')
+ namespace.update!(path: namespace.full_path + '_new')
end
end
end
@@ -931,7 +1005,7 @@ RSpec.describe Namespace do
it "repository directory remains unchanged if path changed" do
before_disk_path = project.disk_path
- namespace.update(path: namespace.full_path + '_new')
+ namespace.update!(path: namespace.full_path + '_new')
expect(before_disk_path).to eq(project.disk_path)
expect(gitlab_shell.repository_exists?(project.repository_storage, "#{project.disk_path}.git")).to be_truthy
@@ -946,7 +1020,7 @@ RSpec.describe Namespace do
let!(:legacy_project_in_subgroup) { create(:project, :legacy_storage, :repository, namespace: subgroup, name: 'foo3') }
it 'updates project full path in .git/config' do
- parent.update(path: 'mygroup_new')
+ parent.update!(path: 'mygroup_new')
expect(project_rugged(project_in_parent_group).config['gitlab.fullpath']).to eq "mygroup_new/#{project_in_parent_group.path}"
expect(project_rugged(hashed_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{hashed_project_in_subgroup.path}"
@@ -958,7 +1032,7 @@ RSpec.describe Namespace do
repository_hashed_project_in_subgroup = hashed_project_in_subgroup.project_repository
repository_legacy_project_in_subgroup = legacy_project_in_subgroup.project_repository
- parent.update(path: 'mygroup_moved')
+ parent.update!(path: 'mygroup_moved')
expect(repository_project_in_parent_group.reload.disk_path).to eq "mygroup_moved/#{project_in_parent_group.path}"
expect(repository_hashed_project_in_subgroup.reload.disk_path).to eq hashed_project_in_subgroup.disk_path
@@ -992,7 +1066,7 @@ RSpec.describe Namespace do
it 'renames its dirs when deleted' do
allow(GitlabShellWorker).to receive(:perform_in)
- namespace.destroy
+ namespace.destroy!
expect(File.exist?(deleted_path_in_dir)).to be(true)
end
@@ -1000,7 +1074,7 @@ RSpec.describe Namespace do
it 'schedules the namespace for deletion' do
expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage, deleted_path)
- namespace.destroy
+ namespace.destroy!
end
context 'in sub-groups' do
@@ -1014,7 +1088,7 @@ RSpec.describe Namespace do
it 'renames its dirs when deleted' do
allow(GitlabShellWorker).to receive(:perform_in)
- child.destroy
+ child.destroy!
expect(File.exist?(deleted_path_in_dir)).to be(true)
end
@@ -1022,7 +1096,7 @@ RSpec.describe Namespace do
it 'schedules the namespace for deletion' do
expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage, deleted_path)
- child.destroy
+ child.destroy!
end
end
end
@@ -1035,7 +1109,7 @@ RSpec.describe Namespace do
expect(File.exist?(path_in_dir)).to be(false)
- namespace.destroy
+ namespace.destroy!
expect(File.exist?(deleted_path_in_dir)).to be(false)
end
@@ -1293,6 +1367,7 @@ RSpec.describe Namespace do
context 'refreshing project access on updating share_with_group_lock' do
let(:group) { create(:group, share_with_group_lock: false) }
let(:project) { create(:project, :private, group: group) }
+ let(:another_project) { create(:project, :private, group: group) }
let_it_be(:shared_with_group_one) { create(:group) }
let_it_be(:shared_with_group_two) { create(:group) }
@@ -1305,6 +1380,7 @@ RSpec.describe Namespace do
shared_with_group_one.add_developer(group_one_user)
shared_with_group_two.add_developer(group_two_user)
create(:project_group_link, group: shared_with_group_one, project: project)
+ create(:project_group_link, group: shared_with_group_one, project: another_project)
create(:project_group_link, group: shared_with_group_two, project: project)
end
@@ -1312,6 +1388,9 @@ RSpec.describe Namespace do
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
.to receive(:perform_async).with(project.id).once
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
+ .to receive(:perform_async).with(another_project.id).once
+
execute_update
end
@@ -1344,11 +1423,23 @@ RSpec.describe Namespace do
stub_feature_flags(specialized_worker_for_group_lock_update_auth_recalculation: false)
end
- it 'refreshes the permissions of the members of the old and new namespace' do
+ it 'updates authorizations leading to users from shared groups losing access', :sidekiq_inline do
expect { execute_update }
.to change { group_one_user.authorized_projects.include?(project) }.from(true).to(false)
.and change { group_two_user.authorized_projects.include?(project) }.from(true).to(false)
end
+
+ it 'updates the authorizations in a non-blocking manner' do
+ expect(AuthorizedProjectsWorker).to(
+ receive(:bulk_perform_async)
+ .with([[group_one_user.id]])).once
+
+ expect(AuthorizedProjectsWorker).to(
+ receive(:bulk_perform_async)
+ .with([[group_two_user.id]])).once
+
+ execute_update
+ end
end
end
@@ -1544,7 +1635,7 @@ RSpec.describe Namespace do
it 'returns the path before last save' do
group = create(:group)
- group.update(parent: nil)
+ group.update!(parent: nil)
expect(group.full_path_before_last_save).to eq(group.path_before_last_save)
end
@@ -1555,7 +1646,7 @@ RSpec.describe Namespace do
group = create(:group, parent: nil)
parent = create(:group)
- group.update(parent: parent)
+ group.update!(parent: parent)
expect(group.full_path_before_last_save).to eq("#{group.path_before_last_save}")
end
@@ -1566,7 +1657,7 @@ RSpec.describe Namespace do
parent = create(:group)
group = create(:group, parent: parent)
- group.update(parent: nil)
+ group.update!(parent: nil)
expect(group.full_path_before_last_save).to eq("#{parent.full_path}/#{group.path}")
end
@@ -1578,7 +1669,7 @@ RSpec.describe Namespace do
group = create(:group, parent: parent)
new_parent = create(:group)
- group.update(parent: new_parent)
+ group.update!(parent: new_parent)
expect(group.full_path_before_last_save).to eq("#{parent.full_path}/#{group.path}")
end
@@ -1845,87 +1936,95 @@ RSpec.describe Namespace do
end
context 'with a parent' do
- context 'when parent has shared runners disabled' do
- let(:parent) { create(:group, :shared_runners_disabled) }
- let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
-
- it 'is invalid' do
- expect(group).to be_invalid
- expect(group.errors[:shared_runners_enabled]).to include('cannot be enabled because parent group has shared Runners disabled')
+ context 'when namespace is a group' do
+ context 'when parent has shared runners disabled' do
+ let(:parent) { create(:group, :shared_runners_disabled) }
+ let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
+
+ it 'is invalid' do
+ expect(group).to be_invalid
+ expect(group.errors[:shared_runners_enabled]).to include('cannot be enabled because parent group has shared Runners disabled')
+ end
end
- end
- context 'when parent has shared runners disabled but allows override' do
- let(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
- let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
+ context 'when parent has shared runners disabled but allows override' do
+ let(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
+ let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
- it 'is valid' do
- expect(group).to be_valid
+ it 'is valid' do
+ expect(group).to be_valid
+ end
end
- end
- context 'when parent has shared runners enabled' do
- let(:parent) { create(:group, shared_runners_enabled: true) }
- let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
+ context 'when parent has shared runners enabled' do
+ let(:parent) { create(:group, shared_runners_enabled: true) }
+ let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
- it 'is valid' do
- expect(group).to be_valid
+ it 'is valid' do
+ expect(group).to be_valid
+ end
end
end
end
+
+ it_behaves_like 'validations called by different namespace types', :changing_shared_runners_enabled_is_allowed
end
describe 'validation #changing_allow_descendants_override_disabled_shared_runners_is_allowed' do
- context 'without a parent' do
- context 'with shared runners disabled' do
- let(:namespace) { build(:namespace, :allow_descendants_override_disabled_shared_runners, :shared_runners_disabled) }
+ context 'when namespace is a group' do
+ context 'without a parent' do
+ context 'with shared runners disabled' do
+ let(:namespace) { build(:group, :allow_descendants_override_disabled_shared_runners, :shared_runners_disabled) }
- it 'is valid' do
- expect(namespace).to be_valid
+ it 'is valid' do
+ expect(namespace).to be_valid
+ end
end
- end
- context 'with shared runners enabled' do
- let(:namespace) { create(:namespace) }
+ context 'with shared runners enabled' do
+ let(:namespace) { create(:namespace) }
- it 'is invalid' do
- namespace.allow_descendants_override_disabled_shared_runners = true
+ it 'is invalid' do
+ namespace.allow_descendants_override_disabled_shared_runners = true
- expect(namespace).to be_invalid
- expect(namespace.errors[:allow_descendants_override_disabled_shared_runners]).to include('cannot be changed if shared runners are enabled')
+ expect(namespace).to be_invalid
+ expect(namespace.errors[:allow_descendants_override_disabled_shared_runners]).to include('cannot be changed if shared runners are enabled')
+ end
end
end
- end
- context 'with a parent' do
- context 'when parent does not allow shared runners' do
- let(:parent) { create(:group, :shared_runners_disabled) }
- let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) }
+ context 'with a parent' do
+ context 'when parent does not allow shared runners' do
+ let(:parent) { create(:group, :shared_runners_disabled) }
+ let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) }
- it 'is invalid' do
- expect(group).to be_invalid
- expect(group.errors[:allow_descendants_override_disabled_shared_runners]).to include('cannot be enabled because parent group does not allow it')
+ it 'is invalid' do
+ expect(group).to be_invalid
+ expect(group.errors[:allow_descendants_override_disabled_shared_runners]).to include('cannot be enabled because parent group does not allow it')
+ end
end
- end
- context 'when parent allows shared runners and setting to true' do
- let(:parent) { create(:group, shared_runners_enabled: true) }
- let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) }
+ context 'when parent allows shared runners and setting to true' do
+ let(:parent) { create(:group, shared_runners_enabled: true) }
+ let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) }
- it 'is valid' do
- expect(group).to be_valid
+ it 'is valid' do
+ expect(group).to be_valid
+ end
end
- end
- context 'when parent allows shared runners and setting to false' do
- let(:parent) { create(:group, shared_runners_enabled: true) }
- let(:group) { build(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent_id: parent.id) }
+ context 'when parent allows shared runners and setting to false' do
+ let(:parent) { create(:group, shared_runners_enabled: true) }
+ let(:group) { build(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent_id: parent.id) }
- it 'is valid' do
- expect(group).to be_valid
+ it 'is valid' do
+ expect(group).to be_valid
+ end
end
end
end
+
+ it_behaves_like 'validations called by different namespace types', :changing_allow_descendants_override_disabled_shared_runners_is_allowed
end
describe '#root?' do
diff --git a/spec/models/namespaces/project_namespace_spec.rb b/spec/models/namespaces/project_namespace_spec.rb
index f38e8aa85d0..4416c49f1bf 100644
--- a/spec/models/namespaces/project_namespace_spec.rb
+++ b/spec/models/namespaces/project_namespace_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Namespaces::ProjectNamespace, type: :model do
# using delete rather than destroy due to `delete` skipping AR hooks/callbacks
# so it's ensured to work at the DB level. Uses ON DELETE CASCADE on foreign key
let_it_be(:project) { create(:project) }
- let_it_be(:project_namespace) { create(:project_namespace, project: project) }
+ let_it_be(:project_namespace) { project.project_namespace }
it 'also deletes the associated project' do
project_namespace.delete
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 0dd77967f25..9d9cca0678a 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -155,7 +155,7 @@ RSpec.describe Note do
expect(note).to receive(:notify_after_destroy).and_call_original
expect(note.noteable).to receive(:after_note_destroyed).with(note)
- note.destroy
+ note.destroy!
end
it 'does not error if noteable is nil' do
@@ -163,7 +163,7 @@ RSpec.describe Note do
expect(note).to receive(:notify_after_destroy).and_call_original
expect(note).to receive(:noteable).at_least(:once).and_return(nil)
- expect { note.destroy }.not_to raise_error
+ expect { note.destroy! }.not_to raise_error
end
end
end
@@ -226,8 +226,8 @@ RSpec.describe Note do
describe 'read' do
before do
- @p1.project_members.create(user: @u2, access_level: ProjectMember::GUEST)
- @p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST)
+ @p1.project_members.create!(user: @u2, access_level: ProjectMember::GUEST)
+ @p2.project_members.create!(user: @u3, access_level: ProjectMember::GUEST)
end
it { expect(Ability.allowed?(@u1, :read_note, @p1)).to be_falsey }
@@ -237,8 +237,8 @@ RSpec.describe Note do
describe 'write' do
before do
- @p1.project_members.create(user: @u2, access_level: ProjectMember::DEVELOPER)
- @p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER)
+ @p1.project_members.create!(user: @u2, access_level: ProjectMember::DEVELOPER)
+ @p2.project_members.create!(user: @u3, access_level: ProjectMember::DEVELOPER)
end
it { expect(Ability.allowed?(@u1, :create_note, @p1)).to be_falsey }
@@ -248,9 +248,9 @@ RSpec.describe Note do
describe 'admin' do
before do
- @p1.project_members.create(user: @u1, access_level: ProjectMember::REPORTER)
- @p1.project_members.create(user: @u2, access_level: ProjectMember::MAINTAINER)
- @p2.project_members.create(user: @u3, access_level: ProjectMember::MAINTAINER)
+ @p1.project_members.create!(user: @u1, access_level: ProjectMember::REPORTER)
+ @p1.project_members.create!(user: @u2, access_level: ProjectMember::MAINTAINER)
+ @p2.project_members.create!(user: @u3, access_level: ProjectMember::MAINTAINER)
end
it { expect(Ability.allowed?(@u1, :admin_note, @p1)).to be_falsey }
@@ -1468,7 +1468,7 @@ RSpec.describe Note do
shared_examples 'assignee check' do
context 'when the provided user is one of the assignees' do
before do
- note.noteable.update(assignees: [user, create(:user)])
+ note.noteable.update!(assignees: [user, create(:user)])
end
it 'returns true' do
@@ -1480,7 +1480,7 @@ RSpec.describe Note do
shared_examples 'author check' do
context 'when the provided user is the author' do
before do
- note.noteable.update(author: user)
+ note.noteable.update!(author: user)
end
it 'returns true' do
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 3f1684327e7..cc601fb30c2 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe NotificationSetting do
notification_setting.merge_merge_request = "t"
notification_setting.close_merge_request = "nil"
notification_setting.reopen_merge_request = "false"
- notification_setting.save
+ notification_setting.save!
end
it "parses boolean before saving" do
@@ -52,12 +52,12 @@ RSpec.describe NotificationSetting do
context 'notification_email' do
let_it_be(:user) { create(:user) }
- subject { described_class.new(source_id: 1, source_type: 'Project', user_id: user.id) }
+ subject { build(:notification_setting, user_id: user.id) }
it 'allows to change email to verified one' do
email = create(:email, :confirmed, user: user)
- subject.update(notification_email: email.email)
+ subject.notification_email = email.email
expect(subject).to be_valid
end
@@ -65,13 +65,13 @@ RSpec.describe NotificationSetting do
it 'does not allow to change email to not verified one' do
email = create(:email, user: user)
- subject.update(notification_email: email.email)
+ subject.notification_email = email.email
expect(subject).to be_invalid
end
it 'allows to change email to empty one' do
- subject.update(notification_email: '')
+ subject.notification_email = ''
expect(subject).to be_valid
end
@@ -85,7 +85,7 @@ RSpec.describe NotificationSetting do
1.upto(4) do |i|
setting = create(:notification_setting, user: user)
- setting.project.update(pending_delete: true) if i.even?
+ setting.project.update!(pending_delete: true) if i.even?
end
end
diff --git a/spec/models/operations/feature_flags/strategy_spec.rb b/spec/models/operations/feature_flags/strategy_spec.rb
index 9289e3beab5..de1b9d2c855 100644
--- a/spec/models/operations/feature_flags/strategy_spec.rb
+++ b/spec/models/operations/feature_flags/strategy_spec.rb
@@ -20,8 +20,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'skips parameters validation' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: invalid_name, parameters: { bad: 'params' })
+ strategy = build(:operations_strategy,
+ feature_flag: feature_flag,
+ name: invalid_name,
+ parameters: { bad: 'params' })
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:name]).to eq(['strategy name is invalid'])
expect(strategy.errors[:parameters]).to be_empty
@@ -36,19 +40,24 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must have valid parameters for the strategy' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId', parameters: invalid_parameters)
+ strategy = build(:operations_strategy,
+ :gradual_rollout,
+ feature_flag: feature_flag,
+ parameters: invalid_parameters)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
end
end
it 'allows the parameters in any order' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- parameters: { percentage: '10', groupId: 'mygroup' })
+ strategy = build(:operations_strategy,
+ :gradual_rollout,
+ feature_flag: feature_flag,
+ parameters: { percentage: '10', groupId: 'mygroup' })
- expect(strategy.errors[:parameters]).to be_empty
+ expect(strategy).to be_valid
end
describe 'percentage' do
@@ -59,9 +68,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- parameters: { groupId: 'mygroup', percentage: invalid_value })
+ strategy = build(:operations_strategy,
+ :gradual_rollout,
+ feature_flag: feature_flag,
+ parameters: { groupId: 'mygroup', percentage: invalid_value })
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['percentage must be a string between 0 and 100 inclusive'])
end
@@ -72,11 +84,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- parameters: { groupId: 'mygroup', percentage: valid_value })
+ strategy = build(:operations_strategy,
+ :gradual_rollout,
+ feature_flag: feature_flag,
+ parameters: { groupId: 'mygroup', percentage: valid_value })
- expect(strategy.errors[:parameters]).to eq([])
+ expect(strategy).to be_valid
end
end
end
@@ -88,9 +101,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must be a string value of up to 32 lowercase characters' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- parameters: { groupId: invalid_value, percentage: '40' })
+ strategy = build(:operations_strategy,
+ :gradual_rollout,
+ feature_flag: feature_flag,
+ parameters: { groupId: invalid_value, percentage: '40' })
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['groupId parameter is invalid'])
end
@@ -101,11 +117,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must be a string value of up to 32 lowercase characters' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- parameters: { groupId: valid_value, percentage: '40' })
+ strategy = build(:operations_strategy,
+ :gradual_rollout,
+ feature_flag: feature_flag,
+ parameters: { groupId: valid_value, percentage: '40' })
- expect(strategy.errors[:parameters]).to eq([])
+ expect(strategy).to be_valid
end
end
end
@@ -123,9 +140,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
])
with_them do
it 'must have valid parameters for the strategy' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: invalid_parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: invalid_parameters)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
end
@@ -137,11 +157,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
[:groupId, 'mygroup']
].permutation(3).each do |parameters|
it "allows the parameters in the order #{parameters.map { |p| p.first }.join(', ')}" do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: Hash[parameters])
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: Hash[parameters])
- expect(strategy.errors[:parameters]).to be_empty
+ expect(strategy).to be_valid
end
end
@@ -152,9 +173,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
with_them do
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
parameters = { stickiness: 'default', groupId: 'mygroup', rollout: invalid_value }
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: parameters)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq([
'rollout must be a string between 0 and 100 inclusive'
@@ -166,11 +190,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
with_them do
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
parameters = { stickiness: 'default', groupId: 'mygroup', rollout: valid_value }
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: parameters)
- expect(strategy.errors[:parameters]).to eq([])
+ expect(strategy).to be_valid
end
end
end
@@ -181,9 +206,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
with_them do
it 'must be a string value of up to 32 lowercase characters' do
parameters = { stickiness: 'default', groupId: invalid_value, rollout: '40' }
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: parameters)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['groupId parameter is invalid'])
end
@@ -193,11 +221,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
with_them do
it 'must be a string value of up to 32 lowercase characters' do
parameters = { stickiness: 'default', groupId: valid_value, rollout: '40' }
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: parameters)
- expect(strategy.errors[:parameters]).to eq([])
+ expect(strategy).to be_valid
end
end
end
@@ -207,9 +236,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
with_them do
it 'must be a string representing a supported stickiness setting' do
parameters = { stickiness: invalid_value, groupId: 'mygroup', rollout: '40' }
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: parameters)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq([
'stickiness parameter must be default, userId, sessionId, or random'
@@ -221,11 +253,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
with_them do
it 'must be a string representing a supported stickiness setting' do
parameters = { stickiness: valid_value, groupId: 'mygroup', rollout: '40' }
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: parameters)
- expect(strategy.errors[:parameters]).to eq([])
+ expect(strategy).to be_valid
end
end
end
@@ -237,8 +270,11 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must have valid parameters for the strategy' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'userWithId', parameters: invalid_parameters)
+ strategy = build(:operations_strategy,
+ feature_flag: feature_flag,
+ name: 'userWithId', parameters: invalid_parameters)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
end
@@ -253,10 +289,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'is valid with a string of comma separated values' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: valid_value })
+ strategy = build(:operations_strategy,
+ feature_flag: feature_flag,
+ name: 'userWithId',
+ parameters: { userIds: valid_value })
- expect(strategy.errors[:parameters]).to be_empty
+ expect(strategy).to be_valid
end
end
@@ -267,8 +305,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'is invalid' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: invalid_value })
+ strategy = build(:operations_strategy,
+ feature_flag: feature_flag,
+ name: 'userWithId',
+ parameters: { userIds: invalid_value })
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to include(
'userIds must be a string of unique comma separated values each 256 characters or less'
@@ -284,43 +326,48 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must be empty' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'default',
- parameters: invalid_value)
+ strategy = build(:operations_strategy, :default, feature_flag: feature_flag, parameters: invalid_value)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
end
end
it 'must be empty' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'default',
- parameters: {})
+ strategy = build(:operations_strategy, :default, feature_flag: feature_flag)
- expect(strategy.errors[:parameters]).to be_empty
+ expect(strategy).to be_valid
end
end
context 'when the strategy name is gitlabUserList' do
+ let_it_be(:user_list) { create(:operations_feature_flag_user_list, project: project) }
+
where(:invalid_value) do
[{ groupId: "default", percentage: "7" }, "", "nothing", 7, nil, [], 2.5, { userIds: 'user1' }]
end
with_them do
- it 'must be empty' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gitlabUserList',
- parameters: invalid_value)
+ it 'is invalid' do
+ strategy = build(:operations_strategy,
+ :gitlab_userlist,
+ user_list: user_list,
+ feature_flag: feature_flag,
+ parameters: invalid_value)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
end
end
- it 'must be empty' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gitlabUserList',
- parameters: {})
+ it 'is valid' do
+ strategy = build(:operations_strategy,
+ :gitlab_userlist,
+ user_list: user_list,
+ feature_flag: feature_flag)
- expect(strategy.errors[:parameters]).to be_empty
+ expect(strategy).to be_valid
end
end
end
@@ -329,18 +376,15 @@ RSpec.describe Operations::FeatureFlags::Strategy do
context 'when name is gitlabUserList' do
it 'is valid when associated with a user list' do
user_list = create(:operations_feature_flag_user_list, project: project)
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gitlabUserList',
- user_list: user_list,
- parameters: {})
+ strategy = build(:operations_strategy, :gitlab_userlist, feature_flag: feature_flag, user_list: user_list)
- expect(strategy.errors[:user_list]).to be_empty
+ expect(strategy).to be_valid
end
it 'is invalid without a user list' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gitlabUserList',
- parameters: {})
+ strategy = build(:operations_strategy, :gitlab_userlist, feature_flag: feature_flag, user_list: nil)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:user_list]).to eq(["can't be blank"])
end
@@ -348,10 +392,9 @@ RSpec.describe Operations::FeatureFlags::Strategy do
it 'is invalid when associated with a user list from another project' do
other_project = create(:project)
user_list = create(:operations_feature_flag_user_list, project: other_project)
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gitlabUserList',
- user_list: user_list,
- parameters: {})
+ strategy = build(:operations_strategy, :gitlab_userlist, feature_flag: feature_flag, user_list: user_list)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:user_list]).to eq(['must belong to the same project'])
end
@@ -360,84 +403,68 @@ RSpec.describe Operations::FeatureFlags::Strategy do
context 'when name is default' do
it 'is invalid when associated with a user list' do
user_list = create(:operations_feature_flag_user_list, project: project)
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'default',
- user_list: user_list,
- parameters: {})
+ strategy = build(:operations_strategy, :default, feature_flag: feature_flag, user_list: user_list)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:user_list]).to eq(['must be blank'])
end
it 'is valid without a user list' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'default',
- parameters: {})
+ strategy = build(:operations_strategy, :default, feature_flag: feature_flag)
- expect(strategy.errors[:user_list]).to be_empty
+ expect(strategy).to be_valid
end
end
context 'when name is userWithId' do
it 'is invalid when associated with a user list' do
user_list = create(:operations_feature_flag_user_list, project: project)
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'userWithId',
- user_list: user_list,
- parameters: { userIds: 'user1' })
+ strategy = build(:operations_strategy, :userwithid, feature_flag: feature_flag, user_list: user_list)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:user_list]).to eq(['must be blank'])
end
it 'is valid without a user list' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'userWithId',
- parameters: { userIds: 'user1' })
+ strategy = build(:operations_strategy, :userwithid, feature_flag: feature_flag)
- expect(strategy.errors[:user_list]).to be_empty
+ expect(strategy).to be_valid
end
end
context 'when name is gradualRolloutUserId' do
it 'is invalid when associated with a user list' do
user_list = create(:operations_feature_flag_user_list, project: project)
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- user_list: user_list,
- parameters: { groupId: 'default', percentage: '10' })
+ strategy = build(:operations_strategy, :gradual_rollout, feature_flag: feature_flag, user_list: user_list)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:user_list]).to eq(['must be blank'])
end
it 'is valid without a user list' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- parameters: { groupId: 'default', percentage: '10' })
+ strategy = build(:operations_strategy, :gradual_rollout, feature_flag: feature_flag)
- expect(strategy.errors[:user_list]).to be_empty
+ expect(strategy).to be_valid
end
end
context 'when name is flexibleRollout' do
it 'is invalid when associated with a user list' do
user_list = create(:operations_feature_flag_user_list, project: project)
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- user_list: user_list,
- parameters: { groupId: 'default',
- rollout: '10',
- stickiness: 'default' })
+ strategy = build(:operations_strategy, :flexible_rollout, feature_flag: feature_flag, user_list: user_list)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:user_list]).to eq(['must be blank'])
end
it 'is valid without a user list' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: { groupId: 'default',
- rollout: '10',
- stickiness: 'default' })
+ strategy = build(:operations_strategy, :flexible_rollout, feature_flag: feature_flag)
- expect(strategy.errors[:user_list]).to be_empty
+ expect(strategy).to be_valid
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 3a48d3389a3..b2dbebb2c0d 100644
--- a/spec/models/operations/feature_flags/user_list_spec.rb
+++ b/spec/models/operations/feature_flags/user_list_spec.rb
@@ -20,9 +20,9 @@ RSpec.describe Operations::FeatureFlags::UserList do
end
with_them do
it 'is valid with a string of comma separated values' do
- user_list = described_class.create(user_xids: valid_value)
+ user_list = build(:operations_feature_flag_user_list, user_xids: valid_value)
- expect(user_list.errors[:user_xids]).to be_empty
+ expect(user_list).to be_valid
end
end
@@ -31,9 +31,10 @@ RSpec.describe Operations::FeatureFlags::UserList do
end
with_them do
it 'automatically casts values of other types' do
- user_list = described_class.create(user_xids: typecast_value)
+ user_list = build(:operations_feature_flag_user_list, user_xids: typecast_value)
+
+ expect(user_list).to be_valid
- expect(user_list.errors[:user_xids]).to be_empty
expect(user_list.user_xids).to eq(typecast_value.to_s)
end
end
@@ -45,7 +46,9 @@ RSpec.describe Operations::FeatureFlags::UserList do
end
with_them do
it 'is invalid' do
- user_list = described_class.create(user_xids: invalid_value)
+ user_list = build(:operations_feature_flag_user_list, user_xids: invalid_value)
+
+ expect(user_list).to be_invalid
expect(user_list.errors[:user_xids]).to include(
'user_xids must be a string of unique comma separated values each 256 characters or less'
@@ -70,20 +73,20 @@ RSpec.describe Operations::FeatureFlags::UserList do
describe '#destroy' do
it 'deletes the model if it is not associated with any feature flag strategies' do
project = create(:project)
- user_list = described_class.create(project: project, name: 'My User List', user_xids: 'user1,user2')
+ user_list = described_class.create!(project: project, name: 'My User List', user_xids: 'user1,user2')
- user_list.destroy
+ user_list.destroy!
expect(described_class.count).to eq(0)
end
it 'does not delete the model if it is associated with a feature flag strategy' do
project = create(:project)
- user_list = described_class.create(project: project, name: 'My User List', user_xids: 'user1,user2')
+ user_list = described_class.create!(project: project, name: 'My User List', user_xids: 'user1,user2')
feature_flag = create(:operations_feature_flag, :new_version_flag, project: project)
strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'gitlabUserList', user_list: user_list)
- user_list.destroy
+ user_list.destroy # rubocop:disable Rails/SaveBang
expect(described_class.count).to eq(1)
expect(::Operations::FeatureFlags::StrategyUserList.count).to eq(1)
diff --git a/spec/models/packages/npm/metadatum_spec.rb b/spec/models/packages/npm/metadatum_spec.rb
new file mode 100644
index 00000000000..ff8cce5310e
--- /dev/null
+++ b/spec/models/packages/npm/metadatum_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Npm::Metadatum, type: :model do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:package).inverse_of(:npm_metadatum) }
+ end
+
+ describe 'validations' do
+ describe 'package', :aggregate_failures do
+ it { is_expected.to validate_presence_of(:package) }
+
+ it 'ensure npm package type' do
+ metadatum = build(:npm_metadatum)
+
+ metadatum.package = build(:nuget_package)
+
+ expect(metadatum).not_to be_valid
+ expect(metadatum.errors).to contain_exactly('Package type must be NPM')
+ end
+ end
+
+ describe 'package_json', :aggregate_failures do
+ let(:valid_json) { { 'name' => 'foo', 'version' => 'v1.0', 'dist' => { 'tarball' => 'x', 'shasum' => 'x' } } }
+
+ it { is_expected.to allow_value(valid_json).for(:package_json) }
+ it { is_expected.to allow_value(valid_json.merge('extra-field': { 'foo': 'bar' })).for(:package_json) }
+ it { is_expected.to allow_value(with_dist { |dist| dist.merge('extra-field': 'x') }).for(:package_json) }
+
+ %w[name version dist].each do |field|
+ it { is_expected.not_to allow_value(valid_json.except(field)).for(:package_json) }
+ end
+
+ %w[tarball shasum].each do |field|
+ it { is_expected.not_to allow_value(with_dist { |dist| dist.except(field) }).for(:package_json) }
+ end
+
+ it { is_expected.not_to allow_value({}).for(:package_json) }
+
+ it { is_expected.not_to allow_value(test: 'test' * 10000).for(:package_json) }
+
+ def with_dist
+ valid_json.tap do |h|
+ h['dist'] = yield(h['dist'])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index 450656e3e9c..8617793f41d 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -14,7 +14,6 @@ RSpec.describe Packages::PackageFile, type: :model do
it { is_expected.to belong_to(:package) }
it { is_expected.to have_one(:conan_file_metadatum) }
it { is_expected.to have_many(:package_file_build_infos).inverse_of(:package_file) }
- it { is_expected.to have_many(:pipelines).through(:package_file_build_infos) }
it { is_expected.to have_one(:debian_file_metadatum).inverse_of(:package_file).class_name('Packages::Debian::FileMetadatum') }
it { is_expected.to have_one(:helm_file_metadatum).inverse_of(:package_file).class_name('Packages::Helm::FileMetadatum') }
end
@@ -206,6 +205,28 @@ RSpec.describe Packages::PackageFile, type: :model do
end
end
+ describe '#pipelines' do
+ let_it_be_with_refind(:package_file) { create(:package_file) }
+
+ subject { package_file.pipelines }
+
+ context 'package_file without pipeline' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'package_file with pipeline' do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+ let_it_be(:pipeline2) { create(:ci_pipeline) }
+
+ before do
+ package_file.package_file_build_infos.create!(pipeline: pipeline)
+ package_file.package_file_build_infos.create!(pipeline: pipeline2)
+ end
+
+ it { is_expected.to contain_exactly(pipeline, pipeline2) }
+ end
+ end
+
describe '#update_file_store callback' do
let_it_be(:package_file) { build(:package_file, :nuget, size: nil) }
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 2573c01d686..6ee5219819c 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -14,13 +14,13 @@ RSpec.describe Packages::Package, type: :model do
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(:debian_publication).inverse_of(:package).class_name('Packages::Debian::Publication') }
it { is_expected.to have_one(:debian_distribution).through(:debian_publication).source(:distribution).inverse_of(:packages).class_name('Packages::Debian::ProjectDistribution') }
it { is_expected.to have_one(:nuget_metadatum).inverse_of(:package) }
it { is_expected.to have_one(:rubygems_metadatum).inverse_of(:package) }
+ it { is_expected.to have_one(:npm_metadatum).inverse_of(:package) }
end
describe '.with_debian_codename' do
@@ -999,6 +999,28 @@ RSpec.describe Packages::Package, type: :model do
end
end
+ describe '#pipelines' do
+ let_it_be_with_refind(:package) { create(:maven_package) }
+
+ subject { package.pipelines }
+
+ context 'package without pipeline' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'package with pipeline' do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+ let_it_be(:pipeline2) { create(:ci_pipeline) }
+
+ before do
+ package.build_infos.create!(pipeline: pipeline)
+ package.build_infos.create!(pipeline: pipeline2)
+ end
+
+ it { is_expected.to contain_exactly(pipeline, pipeline2) }
+ end
+ end
+
describe '#tag_names' do
let_it_be(:package) { create(:nuget_package) }
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 2b6ed9a9927..d476e18a72c 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe PagesDomain do
let(:domain) { build(:pages_domain) }
it 'saves validity time' do
- domain.save
+ domain.save!
expect(domain.certificate_valid_not_before).to be_like_time(Time.zone.parse("2020-03-16 14:20:34 UTC"))
expect(domain.certificate_valid_not_after).to be_like_time(Time.zone.parse("2220-01-28 14:20:34 UTC"))
@@ -161,7 +161,7 @@ RSpec.describe PagesDomain do
context 'when certificate is already saved' do
it "doesn't add error to certificate" do
- domain.save(validate: false)
+ domain.save!(validate: false)
domain.valid?
diff --git a/spec/models/preloaders/group_policy_preloader_spec.rb b/spec/models/preloaders/group_policy_preloader_spec.rb
new file mode 100644
index 00000000000..f6e40d1f033
--- /dev/null
+++ b/spec/models/preloaders/group_policy_preloader_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Preloaders::GroupPolicyPreloader do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:root_parent) { create(:group, :private, name: 'root-1', path: 'root-1') }
+ let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
+ let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer', parent: root_parent) }
+ let_it_be(:private_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
+ let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer') }
+
+ let(:base_groups) { [guest_group, private_maintainer_group, private_developer_group, public_maintainer_group] }
+
+ before_all do
+ guest_group.add_guest(user)
+ private_maintainer_group.add_maintainer(user)
+ private_developer_group.add_developer(user)
+ public_maintainer_group.add_maintainer(user)
+ end
+
+ it 'avoids N+1 queries when authorizing a list of groups', :request_store do
+ preload_groups_for_policy(user)
+ control = ActiveRecord::QueryRecorder.new { authorize_all_groups(user) }
+
+ new_group1 = create(:group, :private).tap { |group| group.add_maintainer(user) }
+ new_group2 = create(:group, :private, parent: private_maintainer_group)
+
+ another_root = create(:group, :private, name: 'root-3', path: 'root-3')
+ new_group3 = create(:group, :private, parent: another_root).tap { |group| group.add_maintainer(user) }
+
+ pristine_groups = Group.where(id: base_groups + [new_group1, new_group2, new_group3])
+
+ preload_groups_for_policy(user, pristine_groups)
+ expect { authorize_all_groups(user, pristine_groups) }.not_to exceed_query_limit(control)
+ end
+
+ def authorize_all_groups(current_user, group_list = base_groups)
+ group_list.each { |group| current_user.can?(:read_group, group) }
+ end
+
+ def preload_groups_for_policy(current_user, group_list = base_groups)
+ described_class.new(group_list, current_user).execute
+ end
+end
diff --git a/spec/models/preloaders/group_root_ancestor_preloader_spec.rb b/spec/models/preloaders/group_root_ancestor_preloader_spec.rb
new file mode 100644
index 00000000000..0d622e84ef1
--- /dev/null
+++ b/spec/models/preloaders/group_root_ancestor_preloader_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Preloaders::GroupRootAncestorPreloader do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:root_parent1) { create(:group, :private, name: 'root-1', path: 'root-1') }
+ let_it_be(:root_parent2) { create(:group, :private, name: 'root-2', path: 'root-2') }
+ let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
+ let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer', parent: root_parent1) }
+ let_it_be(:private_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
+ let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer', parent: root_parent2) }
+
+ let(:root_query_regex) { /\ASELECT.+FROM "namespaces" WHERE "namespaces"."id" = \d+/ }
+ let(:additional_preloads) { [] }
+ let(:groups) { [guest_group, private_maintainer_group, private_developer_group, public_maintainer_group] }
+ let(:pristine_groups) { Group.where(id: groups) }
+
+ shared_examples 'executes N matching DB queries' do |expected_query_count, query_method = nil|
+ it 'executes the specified root_ancestor queries' do
+ expect do
+ pristine_groups.each do |group|
+ root_ancestor = group.root_ancestor
+
+ root_ancestor.public_send(query_method) if query_method.present?
+ end
+ end.to make_queries_matching(root_query_regex, expected_query_count)
+ end
+
+ it 'strong_memoizes the correct root_ancestor' do
+ pristine_groups.each do |group|
+ expected_parent_id = group.root_ancestor.id == group.id ? nil : group.root_ancestor.id
+
+ expect(group.parent_id).to eq(expected_parent_id)
+ end
+ end
+ end
+
+ context 'when the preloader is used' do
+ before do
+ preload_ancestors
+ end
+
+ context 'when no additional preloads are provided' do
+ it_behaves_like 'executes N matching DB queries', 0
+ end
+
+ context 'when additional preloads are provided' do
+ let(:additional_preloads) { [:route] }
+ let(:root_query_regex) { /\ASELECT.+FROM "routes" WHERE "routes"."source_id" = \d+/ }
+
+ it_behaves_like 'executes N matching DB queries', 0, :full_path
+ end
+ end
+
+ context 'when the preloader is not used' do
+ it_behaves_like 'executes N matching DB queries', 2
+ end
+
+ def preload_ancestors
+ described_class.new(pristine_groups, additional_preloads).execute
+ end
+end
diff --git a/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb b/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
index 8144e1ad233..5fc7bfb1f62 100644
--- a/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
+++ b/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
@@ -13,32 +13,47 @@ RSpec.describe Preloaders::UserMaxAccessLevelInGroupsPreloader do
shared_examples 'executes N max member permission queries to the DB' do
it 'executes the specified max membership queries' do
- queries = ActiveRecord::QueryRecorder.new do
- groups.each { |group| user.can?(:read_group, group) }
- end
+ expect { groups.each { |group| user.can?(:read_group, group) } }.to make_queries_matching(max_query_regex, expected_query_count)
+ end
- max_queries = queries.log.grep(max_query_regex)
+ it 'caches the correct access_level for each group' do
+ groups.each do |group|
+ access_level_from_db = group.members_with_parents.where(user_id: user.id).group(:user_id).maximum(:access_level)[user.id] || Gitlab::Access::NO_ACCESS
+ cached_access_level = group.max_member_access_for_user(user)
- expect(max_queries.count).to eq(expected_query_count)
+ expect(cached_access_level).to eq(access_level_from_db)
+ end
end
end
context 'when the preloader is used', :request_store do
- before do
- described_class.new(groups, user).execute
- end
+ context 'when user has indirect access to groups' do
+ let_it_be(:child_maintainer) { create(:group, :private, parent: group1).tap {|g| g.add_maintainer(user)} }
+ let_it_be(:child_indirect_access) { create(:group, :private, parent: group1) }
- it_behaves_like 'executes N max member permission queries to the DB' do
- # Will query all groups where the user is not already a member
- let(:expected_query_count) { 1 }
- end
+ let(:groups) { [group1, group2, group3, child_maintainer, child_indirect_access] }
+
+ context 'when traversal_ids feature flag is disabled' do
+ it_behaves_like 'executes N max member permission queries to the DB' do
+ before do
+ stub_feature_flags(use_traversal_ids: false)
+ described_class.new(groups, user).execute
+ end
+
+ # One query for group with no access and another one per group where the user is not a direct member
+ let(:expected_query_count) { 2 }
+ end
+ end
- context 'when user has access but is not a direct member of the group' do
- let(:groups) { [group1, group2, group3, create(:group, :private, parent: group1)] }
+ context 'when traversal_ids feature flag is enabled' do
+ it_behaves_like 'executes N max member permission queries to the DB' do
+ before do
+ stub_feature_flags(use_traversal_ids: true)
+ described_class.new(groups, user).execute
+ end
- it_behaves_like 'executes N max member permission queries to the DB' do
- # One query for group with no access and another one where the user is not a direct member
- let(:expected_query_count) { 2 }
+ let(:expected_query_count) { 0 }
+ end
end
end
end
diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb
index c517fc8be55..58c0ff48b46 100644
--- a/spec/models/project_authorization_spec.rb
+++ b/spec/models/project_authorization_spec.rb
@@ -3,9 +3,10 @@
require 'spec_helper'
RSpec.describe ProjectAuthorization do
- let(:user) { create(:user) }
- let(:project1) { create(:project) }
- let(:project2) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:project3) { create(:project) }
describe '.insert_authorizations' do
it 'inserts the authorizations' do
@@ -23,5 +24,19 @@ RSpec.describe ProjectAuthorization do
expect(user.project_authorizations.count).to eq(2)
end
+
+ it 'skips duplicates and inserts the remaining rows without error' do
+ create(:project_authorization, user: user, project: project1, access_level: Gitlab::Access::MAINTAINER)
+
+ rows = [
+ [user.id, project1.id, Gitlab::Access::MAINTAINER],
+ [user.id, project2.id, Gitlab::Access::MAINTAINER],
+ [user.id, project3.id, Gitlab::Access::MAINTAINER]
+ ]
+
+ described_class.insert_authorizations(rows)
+
+ expect(user.project_authorizations.pluck(:user_id, :project_id, :access_level)).to match_array(rows)
+ end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 2e5c5af4eb0..3a8768ff463 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Project, factory_default: :keep do
describe 'associations' do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:namespace) }
- it { is_expected.to belong_to(:project_namespace).class_name('Namespaces::ProjectNamespace').with_foreign_key('project_namespace_id').inverse_of(:project) }
+ it { is_expected.to belong_to(:project_namespace).class_name('Namespaces::ProjectNamespace').with_foreign_key('project_namespace_id') }
it { is_expected.to belong_to(:creator).class_name('User') }
it { is_expected.to belong_to(:pool_repository) }
it { is_expected.to have_many(:users) }
@@ -191,7 +191,7 @@ RSpec.describe Project, factory_default: :keep do
# using delete rather than destroy due to `delete` skipping AR hooks/callbacks
# so it's ensured to work at the DB level. Uses AFTER DELETE trigger.
let_it_be(:project) { create(:project) }
- let_it_be(:project_namespace) { create(:project_namespace, project: project) }
+ let_it_be(:project_namespace) { project.project_namespace }
it 'also deletes the associated ProjectNamespace' do
project.delete
@@ -233,6 +233,58 @@ RSpec.describe Project, factory_default: :keep do
expect(project.project_setting).to be_an_instance_of(ProjectSetting)
expect(project.project_setting).to be_new_record
end
+
+ context 'with project namespaces' do
+ it 'automatically creates a project namespace' do
+ project = build(:project, path: 'hopefully-valid-path1')
+ project.save!
+
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_persisted
+ expect(project.project_namespace).to be_in_sync_with_project(project)
+ end
+
+ context 'with FF disabled' do
+ before do
+ stub_feature_flags(create_project_namespace_on_project_create: false)
+ end
+
+ it 'does not create a project namespace' do
+ project = build(:project, path: 'hopefully-valid-path2')
+ project.save!
+
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_nil
+ end
+ end
+ end
+ end
+
+ context 'updating a project' do
+ context 'with project namespaces' do
+ it 'keeps project namespace in sync with project' do
+ project = create(:project)
+ project.update!(path: 'hopefully-valid-path1')
+
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_persisted
+ expect(project.project_namespace).to be_in_sync_with_project(project)
+ end
+
+ context 'with FF disabled' do
+ before do
+ stub_feature_flags(create_project_namespace_on_project_create: false)
+ end
+
+ it 'does not create a project namespace when project is updated' do
+ project = create(:project)
+ project.update!(path: 'hopefully-valid-path1')
+
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_nil
+ end
+ end
+ end
end
context 'updating cd_cd_settings' do
@@ -294,6 +346,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to validate_presence_of(:namespace) }
it { is_expected.to validate_presence_of(:repository_storage) }
it { is_expected.to validate_numericality_of(:max_artifacts_size).only_integer.is_greater_than(0) }
+ it { is_expected.to validate_length_of(:suggestion_commit_message).is_at_most(255) }
it 'validates build timeout constraints' do
is_expected.to validate_numericality_of(:build_timeout)
@@ -322,6 +375,18 @@ RSpec.describe Project, factory_default: :keep do
create(:project)
end
+ context 'validates project namespace creation' do
+ it 'does not create project namespace if project is not created' do
+ project = build(:project, path: 'tree')
+
+ project.valid?
+
+ expect(project).not_to be_valid
+ expect(project).to be_new_record
+ expect(project.project_namespace).to be_new_record
+ end
+ end
+
context 'repository storages inclusion' do
let(:project2) { build(:project, repository_storage: 'missing') }
@@ -424,8 +489,9 @@ RSpec.describe Project, factory_default: :keep do
end
include_context 'invalid urls'
+ include_context 'valid urls with CRLF'
- it 'does not allow urls with CR or LF characters' do
+ it 'does not allow URLs with unencoded CR or LF characters' do
project = build(:project)
aggregate_failures do
@@ -437,6 +503,19 @@ RSpec.describe Project, factory_default: :keep do
end
end
end
+
+ it 'allow URLs with CR or LF characters' do
+ project = build(:project)
+
+ aggregate_failures do
+ valid_urls_with_CRLF.each do |url|
+ project.import_url = url
+
+ expect(project).to be_valid
+ expect(project.errors).to be_empty
+ end
+ end
+ end
end
describe 'project pending deletion' do
@@ -1714,13 +1793,19 @@ RSpec.describe Project, factory_default: :keep do
allow(::Gitlab::ServiceDeskEmail).to receive(:config).and_return(config)
end
- it 'returns custom address when project_key is set' do
- create(:service_desk_setting, project: project, project_key: 'key1')
+ context 'when project_key is set' do
+ it 'returns custom address including the project_key' do
+ create(:service_desk_setting, project: project, project_key: 'key1')
- expect(subject).to eq("foo+#{project.full_path_slug}-key1@bar.com")
+ expect(subject).to eq("foo+#{project.full_path_slug}-key1@bar.com")
+ end
end
- it_behaves_like 'with incoming email address'
+ context 'when project_key is not set' do
+ it 'returns custom address including the project full path' do
+ expect(subject).to eq("foo+#{project.full_path_slug}-#{project.project_id}-issue-@bar.com")
+ end
+ end
end
end
@@ -1780,6 +1865,20 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '.without_integration' do
+ it 'returns projects without the integration' do
+ project_1, project_2, project_3, project_4 = create_list(:project, 4)
+ instance_integration = create(:jira_integration, :instance)
+ create(:jira_integration, project: project_1, inherit_from_id: instance_integration.id)
+ create(:jira_integration, project: project_2, inherit_from_id: nil)
+ create(:jira_integration, group: create(:group), project: nil, inherit_from_id: nil)
+ create(:jira_integration, project: project_3, inherit_from_id: nil)
+ create(:integrations_slack, project: project_4, inherit_from_id: nil)
+
+ expect(Project.without_integration(instance_integration)).to contain_exactly(project_4)
+ end
+ end
+
context 'repository storage by default' do
let(:project) { build(:project) }
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index ead6238b2f4..5fbf1a9c502 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -325,12 +325,14 @@ RSpec.describe ProjectStatistics do
lfs_objects_size: 3,
snippets_size: 2,
pipeline_artifacts_size: 3,
+ build_artifacts_size: 3,
+ packages_size: 6,
uploads_size: 5
)
statistics.reload
- expect(statistics.storage_size).to eq 19
+ expect(statistics.storage_size).to eq 28
end
it 'works during wiki_size backfill' do
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 8eab50abd8c..a6a56180ce1 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -234,6 +234,20 @@ RSpec.describe ProjectTeam do
expect(project.team.reporter?(user1)).to be(true)
expect(project.team.reporter?(user2)).to be(true)
end
+
+ context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
+ before do
+ stub_experiments(invite_members_for_task: true)
+ project.team.add_users([user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: project.id)
+ end
+
+ it 'creates a member_task with the correct attributes', :aggregate_failures do
+ member = project.project_members.last
+
+ expect(member.tasks_to_be_done).to match_array([:ci, :code])
+ expect(member.member_task.project).to eq(project)
+ end
+ end
end
describe '#add_user' do
diff --git a/spec/models/protectable_dropdown_spec.rb b/spec/models/protectable_dropdown_spec.rb
index 918c3078405..ab3f455fe63 100644
--- a/spec/models/protectable_dropdown_spec.rb
+++ b/spec/models/protectable_dropdown_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe ProtectableDropdown do
describe '#protectable_ref_names' do
context 'when project repository is not empty' do
before do
- project.protected_branches.create(name: 'master')
+ create(:protected_branch, project: project, name: 'master')
end
it { expect(subject.protectable_ref_names).to include('feature') }
diff --git a/spec/models/redirect_route_spec.rb b/spec/models/redirect_route_spec.rb
index c6e35923b89..2de662fd4b4 100644
--- a/spec/models/redirect_route_spec.rb
+++ b/spec/models/redirect_route_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe RedirectRoute do
let(:group) { create(:group) }
- let!(:redirect_route) { group.redirect_routes.create(path: 'gitlabb') }
+ let!(:redirect_route) { group.redirect_routes.create!(path: 'gitlabb') }
describe 'relationships' do
it { is_expected.to belong_to(:source) }
@@ -17,10 +17,10 @@ RSpec.describe RedirectRoute do
end
describe '.matching_path_and_descendants' do
- let!(:redirect2) { group.redirect_routes.create(path: 'gitlabb/test') }
- let!(:redirect3) { group.redirect_routes.create(path: 'gitlabb/test/foo') }
- let!(:redirect4) { group.redirect_routes.create(path: 'gitlabb/test/foo/bar') }
- let!(:redirect5) { group.redirect_routes.create(path: 'gitlabb/test/baz') }
+ let!(:redirect2) { group.redirect_routes.create!(path: 'gitlabb/test') }
+ let!(:redirect3) { group.redirect_routes.create!(path: 'gitlabb/test/foo') }
+ let!(:redirect4) { group.redirect_routes.create!(path: 'gitlabb/test/foo/bar') }
+ let!(:redirect5) { group.redirect_routes.create!(path: 'gitlabb/test/baz') }
context 'when the redirect route matches with same casing' do
it 'returns correct routes' do
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index b88813b3328..125fec61d72 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -26,10 +26,10 @@ RSpec.describe Release do
context 'when a release exists in the database without a name' do
it 'does not require name' do
existing_release_without_name = build(:release, project: project, author: user, name: nil)
- existing_release_without_name.save(validate: false)
+ existing_release_without_name.save!(validate: false)
existing_release_without_name.description = "change"
- existing_release_without_name.save
+ existing_release_without_name.save!
existing_release_without_name.reload
expect(existing_release_without_name).to be_valid
@@ -88,7 +88,7 @@ RSpec.describe Release do
describe '.create' do
it "fills released_at using created_at if it's not set" do
- release = described_class.create(project: project, author: user)
+ release = create(:release, project: project, author: user, released_at: nil)
expect(release.released_at).to eq(release.created_at)
end
@@ -96,14 +96,14 @@ RSpec.describe Release do
it "does not change released_at if it's set explicitly" do
released_at = Time.zone.parse('2018-10-20T18:00:00Z')
- release = described_class.create(project: project, author: user, released_at: released_at)
+ release = create(:release, project: project, author: user, released_at: released_at)
expect(release.released_at).to eq(released_at)
end
end
describe '#update' do
- subject { release.update(params) }
+ subject { release.update!(params) }
context 'when links do not exist' do
context 'when params are specified for creation' do
@@ -182,7 +182,7 @@ RSpec.describe Release do
it 'also deletes the associated evidence' do
release_with_evidence
- expect { release_with_evidence.destroy }.to change(Releases::Evidence, :count).by(-1)
+ expect { release_with_evidence.destroy! }.to change(Releases::Evidence, :count).by(-1)
end
end
end
@@ -190,7 +190,7 @@ RSpec.describe Release do
describe '#name' do
context 'name is nil' do
before do
- release.update(name: nil)
+ release.update!(name: nil)
end
it 'returns tag' do
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index 382359ccb17..9f1d1c84da3 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -289,7 +289,7 @@ RSpec.describe RemoteMirror, :mailer do
context 'with remote mirroring disabled' do
it 'returns nil' do
- remote_mirror.update(enabled: false)
+ remote_mirror.update!(enabled: false)
expect(remote_mirror.sync).to be_nil
end
@@ -354,7 +354,7 @@ RSpec.describe RemoteMirror, :mailer do
let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first }
it 'resets all the columns when URL changes' do
- remote_mirror.update(last_error: Time.current,
+ remote_mirror.update!(last_error: Time.current,
last_update_at: Time.current,
last_successful_update_at: Time.current,
update_status: 'started',
@@ -378,7 +378,7 @@ RSpec.describe RemoteMirror, :mailer do
end
before do
- remote_mirror.update(last_update_started_at: Time.current)
+ remote_mirror.update!(last_update_started_at: Time.current)
end
context 'when remote mirror does not have status failed' do
@@ -393,7 +393,7 @@ RSpec.describe RemoteMirror, :mailer do
context 'when remote mirror has status failed' do
it 'returns false when last update started after the timestamp' do
- remote_mirror.update(update_status: 'failed')
+ remote_mirror.update!(update_status: 'failed')
expect(remote_mirror.updated_since?(timestamp)).to be false
end
@@ -409,7 +409,7 @@ RSpec.describe RemoteMirror, :mailer do
updated_at: 25.hours.ago)
project = mirror.project
project.pending_delete = true
- project.save
+ project.save!
mirror.reload
expect(mirror.sync).to be_nil
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 7bad907cf90..d50c60774b4 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -66,35 +66,58 @@ RSpec.describe Repository do
it { is_expected.not_to include('v1.0.0') }
end
- describe 'tags_sorted_by' do
+ describe '#tags_sorted_by' do
let(:tags_to_compare) { %w[v1.0.0 v1.1.0] }
- let(:feature_flag) { true }
-
- before do
- stub_feature_flags(tags_finder_gitaly: feature_flag)
- end
context 'name_desc' do
subject { repository.tags_sorted_by('name_desc').map(&:name) & tags_to_compare }
it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
-
- context 'when feature flag is disabled' do
- let(:feature_flag) { false }
-
- it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
- end
end
context 'name_asc' do
- subject { repository.tags_sorted_by('name_asc').map(&:name) & tags_to_compare }
+ subject { repository.tags_sorted_by('name_asc', pagination_params).map(&:name) & tags_to_compare }
+
+ let(:pagination_params) { nil }
it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
- context 'when feature flag is disabled' do
- let(:feature_flag) { false }
+ context 'with pagination' do
+ context 'with limit' do
+ let(:pagination_params) { { limit: 1 } }
+
+ it { is_expected.to eq(['v1.0.0']) }
+ end
+
+ context 'with page token and limit' do
+ let(:pagination_params) { { page_token: 'refs/tags/v1.0.0', limit: 1 } }
+
+ it { is_expected.to eq(['v1.1.0']) }
+ end
- it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
+ context 'with page token only' do
+ let(:pagination_params) { { page_token: 'refs/tags/v1.0.0' } }
+
+ it 'raises an ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'with negative limit' do
+ let(:pagination_params) { { limit: -1 } }
+
+ it 'returns all tags' do
+ is_expected.to eq(['v1.0.0', 'v1.1.0'])
+ end
+ end
+
+ context 'with unknown token' do
+ let(:pagination_params) { { page_token: 'unknown' } }
+
+ it 'raises an ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
end
end
@@ -113,24 +136,12 @@ RSpec.describe Repository do
subject { repository.tags_sorted_by('updated_desc').map(&:name) & (tags_to_compare + [latest_tag]) }
it { is_expected.to eq([latest_tag, 'v1.1.0', 'v1.0.0']) }
-
- context 'when feature flag is disabled' do
- let(:feature_flag) { false }
-
- it { is_expected.to eq([latest_tag, 'v1.1.0', 'v1.0.0']) }
- end
end
context 'asc' do
subject { repository.tags_sorted_by('updated_asc').map(&:name) & (tags_to_compare + [latest_tag]) }
it { is_expected.to eq(['v1.0.0', 'v1.1.0', latest_tag]) }
-
- context 'when feature flag is disabled' do
- let(:feature_flag) { false }
-
- it { is_expected.to eq(['v1.0.0', 'v1.1.0', latest_tag]) }
- end
end
context 'annotated tag pointing to a blob' do
@@ -147,12 +158,6 @@ RSpec.describe Repository do
it { is_expected.to eq(['v1.0.0', 'v1.1.0', annotated_tag_name]) }
- context 'when feature flag is disabled' do
- let(:feature_flag) { false }
-
- it { is_expected.to eq(['v1.0.0', 'v1.1.0', annotated_tag_name]) }
- end
-
after do
rugged_repo(repository).tags.delete(annotated_tag_name)
end
@@ -163,12 +168,6 @@ RSpec.describe Repository do
subject { repository.tags_sorted_by('unknown_desc').map(&:name) & tags_to_compare }
it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
-
- context 'when feature flag is disabled' do
- let(:feature_flag) { false }
-
- it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
- end
end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index eb81db95cd3..b2fa9c24535 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -31,18 +31,18 @@ RSpec.describe Route do
context 'after update' do
it 'calls #create_redirect_for_old_path' do
expect(route).to receive(:create_redirect_for_old_path)
- route.update(path: 'foo')
+ route.update!(path: 'foo')
end
it 'calls #delete_conflicting_redirects' do
expect(route).to receive(:delete_conflicting_redirects)
- route.update(path: 'foo')
+ route.update!(path: 'foo')
end
end
context 'after create' do
it 'calls #delete_conflicting_redirects' do
- route.destroy
+ route.destroy!
new_route = described_class.new(source: group, path: group.path)
expect(new_route).to receive(:delete_conflicting_redirects)
new_route.save!
@@ -81,7 +81,7 @@ RSpec.describe Route do
context 'path update' do
context 'when route name is set' do
before do
- route.update(path: 'bar')
+ route.update!(path: 'bar')
end
it 'updates children routes with new path' do
@@ -111,7 +111,7 @@ RSpec.describe Route do
let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') }
it 'deletes the conflicting redirects' do
- route.update(path: 'bar')
+ route.update!(path: 'bar')
expect(RedirectRoute.exists?(path: 'bar/test')).to be_falsey
expect(RedirectRoute.exists?(path: 'bar/test/foo')).to be_falsey
@@ -122,7 +122,7 @@ RSpec.describe Route do
context 'name update' do
it 'updates children routes with new path' do
- route.update(name: 'bar')
+ route.update!(name: 'bar')
expect(described_class.exists?(name: 'bar')).to be_truthy
expect(described_class.exists?(name: 'bar / test')).to be_truthy
@@ -134,7 +134,7 @@ RSpec.describe Route do
# Note: using `update_columns` to skip all validation and callbacks
route.update_columns(name: nil)
- expect { route.update(name: 'bar') }
+ expect { route.update!(name: 'bar') }
.to change { route.name }.from(nil).to('bar')
end
end
diff --git a/spec/models/sentry_issue_spec.rb b/spec/models/sentry_issue_spec.rb
index c24350d7067..09b23b6fd0d 100644
--- a/spec/models/sentry_issue_spec.rb
+++ b/spec/models/sentry_issue_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe SentryIssue do
create(:sentry_issue)
project = sentry_issue.issue.project
sentry_issue_3 = build(:sentry_issue, issue: create(:issue, project: project), sentry_issue_identifier: sentry_issue.sentry_issue_identifier)
- sentry_issue_3.save(validate: false)
+ sentry_issue_3.save!(validate: false)
result = described_class.for_project_and_identifier(project, sentry_issue.sentry_issue_identifier)
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 4e20a83f18e..e24dd910c39 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -98,7 +98,7 @@ RSpec.describe Snippet do
snippet = build(:snippet)
expect(snippet.statistics).to be_nil
- snippet.save
+ snippet.save!
expect(snippet.statistics).to be_persisted
end
@@ -289,7 +289,7 @@ RSpec.describe Snippet do
let(:access_level) { ProjectFeature::ENABLED }
before do
- project.project_feature.update(snippets_access_level: access_level)
+ project.project_feature.update!(snippets_access_level: access_level)
end
it 'includes snippets for projects with snippets enabled' do
@@ -623,7 +623,7 @@ RSpec.describe Snippet do
context 'when snippet_repository does not exist' do
it 'creates a snippet_repository' do
- snippet.snippet_repository.destroy
+ snippet.snippet_repository.destroy!
snippet.reload
expect do
diff --git a/spec/models/suggestion_spec.rb b/spec/models/suggestion_spec.rb
index 9a7624c253a..4f91908264f 100644
--- a/spec/models/suggestion_spec.rb
+++ b/spec/models/suggestion_spec.rb
@@ -154,6 +154,14 @@ RSpec.describe Suggestion do
it { is_expected.to eq("This suggestion already matches its content.") }
end
+ context 'when file is .ipynb' do
+ before do
+ allow(suggestion).to receive(:file_path).and_return("example.ipynb")
+ end
+
+ it { is_expected.to eq(_("This file was modified for readability, and can't accept suggestions. Edit it directly.")) }
+ end
+
context 'when applicable' do
it { is_expected.to be_nil }
end
diff --git a/spec/models/u2f_registration_spec.rb b/spec/models/u2f_registration_spec.rb
index aba2f27d104..7a70cf69566 100644
--- a/spec/models/u2f_registration_spec.rb
+++ b/spec/models/u2f_registration_spec.rb
@@ -5,9 +5,11 @@ require 'spec_helper'
RSpec.describe U2fRegistration do
let_it_be(:user) { create(:user) }
+ let(:u2f_registration_name) { 'u2f_device' }
+
let(:u2f_registration) do
device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
- create(:u2f_registration, name: 'u2f_device',
+ create(:u2f_registration, name: u2f_registration_name,
user: user,
certificate: Base64.strict_encode64(device.cert_raw),
key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
@@ -16,11 +18,27 @@ RSpec.describe U2fRegistration do
describe 'callbacks' do
describe '#create_webauthn_registration' do
- it 'creates webauthn registration' do
- u2f_registration.save!
+ shared_examples_for 'creates webauthn registration' do
+ it 'creates webauthn registration' do
+ u2f_registration.save!
+
+ webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id)
+ expect(webauthn_registration).to exist
+ end
+ end
+
+ it_behaves_like 'creates webauthn registration'
+
+ context 'when the u2f_registration has a blank name' do
+ let(:u2f_registration_name) { '' }
+
+ it_behaves_like 'creates webauthn registration'
+ end
+
+ context 'when the u2f_registration has the name as `nil`' do
+ let(:u2f_registration_name) { nil }
- webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id)
- expect(webauthn_registration).to exist
+ it_behaves_like 'creates webauthn registration'
end
it 'logs error' do
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index 0ac684cd04c..cdf73b203af 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Upload do
it 'schedules checksum calculation' do
stub_const('UploadChecksumWorker', spy)
- upload = described_class.create(
+ upload = described_class.create!(
path: __FILE__,
size: described_class::CHECKSUM_THRESHOLD + 1.kilobyte,
model: build_stubbed(:user),
@@ -42,7 +42,7 @@ RSpec.describe Upload do
store: ObjectStorage::Store::LOCAL
)
- expect { upload.save }
+ expect { upload.save! }
.to change { upload.checksum }.from(nil)
.to(a_string_matching(/\A\h{64}\z/))
end
@@ -55,7 +55,7 @@ RSpec.describe Upload do
it 'calls delete_file!' do
is_expected.to receive(:delete_file!)
- subject.destroy
+ subject.destroy!
end
end
end
@@ -82,6 +82,18 @@ RSpec.describe Upload do
end
end
+ describe '#relative_path' do
+ it "delegates to the uploader's relative_path method" do
+ uploader = spy('FakeUploader')
+ upload = described_class.new(path: '/tmp/secret/file.jpg', store: ObjectStorage::Store::LOCAL)
+ expect(upload).to receive(:uploader_class).and_return(uploader)
+
+ upload.relative_path
+
+ expect(uploader).to have_received(:relative_path).with(upload)
+ end
+ end
+
describe '#calculate_checksum!' do
let(:upload) do
described_class.new(path: __FILE__,
diff --git a/spec/models/uploads/fog_spec.rb b/spec/models/uploads/fog_spec.rb
index 899e6f2064c..1ffe7c6c43b 100644
--- a/spec/models/uploads/fog_spec.rb
+++ b/spec/models/uploads/fog_spec.rb
@@ -40,7 +40,9 @@ RSpec.describe Uploads::Fog do
end
describe '#delete_keys' do
+ let(:connection) { ::Fog::Storage.new(FileUploader.object_store_credentials) }
let(:keys) { data_store.keys(relation) }
+ let(:paths) { relation.pluck(:path) }
let!(:uploads) { create_list(:upload, 2, :with_file, :issuable_upload, model: project) }
subject { data_store.delete_keys(keys) }
@@ -50,17 +52,32 @@ RSpec.describe Uploads::Fog do
end
it 'deletes multiple data' do
- paths = relation.pluck(:path)
+ paths.each do |path|
+ expect(connection.get_object('uploads', path)[:body]).not_to be_nil
+ end
+
+ subject
+
+ paths.each do |path|
+ expect { connection.get_object('uploads', path)[:body] }.to raise_error(Excon::Error::NotFound)
+ end
+ end
- ::Fog::Storage.new(FileUploader.object_store_credentials).tap do |connection|
+ context 'when one of keys is missing' do
+ let(:keys) { ['unknown'] + super() }
+
+ it 'deletes only existing keys' do
paths.each do |path|
expect(connection.get_object('uploads', path)[:body]).not_to be_nil
end
- end
- subject
+ expect_next_instance_of(::Fog::Storage) do |storage|
+ allow(storage).to receive(:delete_object).and_call_original
+ expect(storage).to receive(:delete_object).with('uploads', keys.first).and_raise(::Google::Apis::ClientError, 'NotFound')
+ end
+
+ subject
- ::Fog::Storage.new(FileUploader.object_store_credentials).tap do |connection|
paths.each do |path|
expect { connection.get_object('uploads', path)[:body] }.to raise_error(Excon::Error::NotFound)
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 21c5aea514a..b5d4614d206 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe User do
include ProjectForksHelper
include TermsHelper
include ExclusiveLeaseHelpers
+ include LdapHelpers
it_behaves_like 'having unique enum values'
@@ -98,7 +99,7 @@ RSpec.describe User do
it { is_expected.to have_many(:group_members) }
it { is_expected.to have_many(:groups) }
it { is_expected.to have_many(:keys).dependent(:destroy) }
- it { is_expected.to have_many(:expired_and_unnotified_keys) }
+ it { is_expected.to have_many(:expired_today_and_unnotified_keys) }
it { is_expected.to have_many(:deploy_keys).dependent(:nullify) }
it { is_expected.to have_many(:group_deploy_keys) }
it { is_expected.to have_many(:events).dependent(:delete_all) }
@@ -1123,7 +1124,7 @@ RSpec.describe User do
end
describe 'after commit hook' do
- describe '#update_emails_with_primary_email' do
+ describe 'when the primary email is updated' do
before do
@user = create(:user, email: 'primary@example.com').tap do |user|
user.skip_reconfirmation!
@@ -1132,13 +1133,7 @@ RSpec.describe User do
@user.reload
end
- it 'gets called when email updated' do
- expect(@user).to receive(:update_emails_with_primary_email)
-
- @user.update!(email: 'new_primary@example.com')
- end
-
- it 'adds old primary to secondary emails when secondary is a new email' do
+ it 'keeps old primary to secondary emails when secondary is a new email' do
@user.update!(email: 'new_primary@example.com')
@user.reload
@@ -1146,22 +1141,6 @@ RSpec.describe User do
expect(@user.emails.pluck(:email)).to match_array([@secondary.email, 'primary@example.com'])
end
- it 'adds old primary to secondary emails if secondary is becoming a primary' do
- @user.update!(email: @secondary.email)
- @user.reload
-
- expect(@user.emails.count).to eq 1
- expect(@user.emails.first.email).to eq 'primary@example.com'
- end
-
- it 'transfers old confirmation values into new secondary' do
- @user.update!(email: @secondary.email)
- @user.reload
-
- expect(@user.emails.count).to eq 1
- expect(@user.emails.first.confirmed_at).not_to eq nil
- end
-
context 'when the first email was unconfirmed and the second email gets confirmed' do
let(:user) { create(:user, :unconfirmed, email: 'should-be-unconfirmed@test.com') }
@@ -1178,11 +1157,8 @@ RSpec.describe User do
expect(user).to be_confirmed
end
- it 'keeps the unconfirmed email unconfirmed' do
- email = user.emails.first
-
- expect(email.email).to eq('should-be-unconfirmed@test.com')
- expect(email).not_to be_confirmed
+ it 'does not add unconfirmed email to secondary' do
+ expect(user.emails.map(&:email)).not_to include('should-be-unconfirmed@test.com')
end
it 'has only one email association' do
@@ -1244,7 +1220,7 @@ RSpec.describe User do
expect(user.email).to eq(confirmed_email)
end
- it 'moves the old email' do
+ it 'keeps the old email' do
email = user.reload.emails.first
expect(email.email).to eq(old_confirmed_email)
@@ -1499,7 +1475,7 @@ RSpec.describe User do
allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true)
end
- let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'test@gitlab.com') }
+ let(:user) { create(:user, :unconfirmed, unconfirmed_email: 'test@gitlab.com') }
it 'returns unconfirmed' do
expect(user.confirmed?).to be_falsey
@@ -1509,6 +1485,22 @@ RSpec.describe User do
user.confirm
expect(user.confirmed?).to be_truthy
end
+
+ it 'adds the confirmed primary email to emails' do
+ expect(user.emails.confirmed.map(&:email)).not_to include(user.email)
+
+ user.confirm
+
+ expect(user.emails.confirmed.map(&:email)).to include(user.email)
+ end
+ end
+
+ context 'if the user is created with confirmed_at set to a time' do
+ let!(:user) { create(:user, email: 'test@gitlab.com', confirmed_at: Time.now.utc) }
+
+ it 'adds the confirmed primary email to emails upon creation' do
+ expect(user.emails.confirmed.map(&:email)).to include(user.email)
+ end
end
describe '#to_reference' do
@@ -2216,7 +2208,7 @@ RSpec.describe User do
end
context 'primary email not confirmed' do
- let(:user) { create(:user, confirmed_at: nil) }
+ let(:user) { create(:user, :unconfirmed) }
let!(:email) { create(:email, :confirmed, user: user, email: 'foo@example.com') }
it 'finds user respecting the confirmed flag' do
@@ -2231,7 +2223,7 @@ RSpec.describe User do
end
it 'returns nil when user is not confirmed' do
- user = create(:user, email: 'foo@example.com', confirmed_at: nil)
+ user = create(:user, :unconfirmed, email: 'foo@example.com')
expect(described_class.find_by_any_email(user.email, confirmed: false)).to eq(user)
expect(described_class.find_by_any_email(user.email, confirmed: true)).to be_nil
@@ -4155,6 +4147,23 @@ RSpec.describe User do
end
end
+ describe '#remove_project_authorizations' do
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:project3) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ it 'removes the project authorizations of the user, in specified projects' do
+ create(:project_authorization, user: user, project: project1)
+ create(:project_authorization, user: user, project: project2)
+ create(:project_authorization, user: user, project: project3)
+
+ user.remove_project_authorizations([project1.id, project2.id])
+
+ expect(user.project_authorizations.pluck(:project_id)).to match_array([project3.id])
+ end
+ end
+
describe '#access_level=' do
let(:user) { build(:user) }
@@ -5817,7 +5826,7 @@ RSpec.describe User do
end
describe '#active_for_authentication?' do
- subject { user.active_for_authentication? }
+ subject(:active_for_authentication?) { user.active_for_authentication? }
let(:user) { create(:user) }
@@ -5827,6 +5836,14 @@ RSpec.describe User do
end
it { is_expected.to be false }
+
+ it 'does not check if LDAP is allowed' do
+ stub_ldap_setting(enabled: true)
+
+ expect(Gitlab::Auth::Ldap::Access).not_to receive(:allowed?)
+
+ active_for_authentication?
+ end
end
context 'when user is a ghost user' do
@@ -5837,6 +5854,28 @@ RSpec.describe User do
it { is_expected.to be false }
end
+ context 'when user is ldap_blocked' do
+ before do
+ user.ldap_block
+ end
+
+ it 'rechecks if LDAP is allowed when LDAP is enabled' do
+ stub_ldap_setting(enabled: true)
+
+ expect(Gitlab::Auth::Ldap::Access).to receive(:allowed?)
+
+ active_for_authentication?
+ end
+
+ it 'does not check if LDAP is allowed when LDAP is not enabled' do
+ stub_ldap_setting(enabled: false)
+
+ expect(Gitlab::Auth::Ldap::Access).not_to receive(:allowed?)
+
+ active_for_authentication?
+ end
+ end
+
context 'based on user type' do
using RSpec::Parameterized::TableSyntax
@@ -6011,7 +6050,7 @@ RSpec.describe User do
subject { user.confirmation_required_on_sign_in? }
context 'when user is confirmed' do
- let(:user) { build_stubbed(:user) }
+ let(:user) { create(:user) }
it 'is falsey' do
expect(user.confirmed?).to be_truthy
@@ -6203,4 +6242,31 @@ RSpec.describe User do
expect(described_class.get_ids_by_username([user_name])).to match_array([user_id])
end
end
+
+ describe 'user_project' do
+ it 'returns users project matched by username and public visibility' do
+ user = create(:user)
+ public_project = create(:project, :public, path: user.username, namespace: user.namespace)
+ create(:project, namespace: user.namespace)
+
+ expect(user.user_project).to eq(public_project)
+ end
+ end
+
+ describe 'user_readme' do
+ it 'returns readme from user project' do
+ user = create(:user)
+ create(:project, :repository, :public, path: user.username, namespace: user.namespace)
+
+ expect(user.user_readme.name).to eq('README.md')
+ expect(user.user_readme.data).to include('testme')
+ end
+
+ it 'returns nil if project is private' do
+ user = create(:user)
+ create(:project, :repository, :private, path: user.username, namespace: user.namespace)
+
+ expect(user.user_readme).to be(nil)
+ end
+ end
end
diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb
index d2b4f5ebd65..43edf7ed093 100644
--- a/spec/models/users/credit_card_validation_spec.rb
+++ b/spec/models/users/credit_card_validation_spec.rb
@@ -6,19 +6,25 @@ RSpec.describe Users::CreditCardValidation do
it { is_expected.to belong_to(:user) }
it { is_expected.to validate_length_of(:holder_name).is_at_most(26) }
+ it { is_expected.to validate_length_of(:network).is_at_most(32) }
it { is_expected.to validate_numericality_of(:last_digits).is_less_than_or_equal_to(9999) }
describe '.similar_records' do
- let(:card_details) { subject.attributes.slice(:expiration_date, :last_digits, :holder_name) }
+ let(:card_details) do
+ subject.attributes.with_indifferent_access.slice(:expiration_date, :last_digits, :network, :holder_name)
+ end
- subject(:credit_card_validation) { create(:credit_card_validation) }
+ subject!(:credit_card_validation) { create(:credit_card_validation, holder_name: 'Alice') }
let!(:match1) { create(:credit_card_validation, card_details) }
- let!(:other1) { create(:credit_card_validation, card_details.merge(last_digits: 9)) }
- let!(:match2) { create(:credit_card_validation, card_details) }
- let!(:other2) { create(:credit_card_validation, card_details.merge(holder_name: 'foo bar')) }
+ let!(:match2) { create(:credit_card_validation, card_details.merge(holder_name: 'Bob')) }
+ let!(:non_match1) { create(:credit_card_validation, card_details.merge(last_digits: 9)) }
+ let!(:non_match2) { create(:credit_card_validation, card_details.merge(network: 'unknown')) }
+ let!(:non_match3) do
+ create(:credit_card_validation, card_details.dup.tap { |h| h[:expiration_date] += 1.year })
+ end
- it 'returns records with matching credit card, ordered by credit_card_validated_at' do
+ it 'returns matches with the same last_digits, expiration and network, ordered by credit_card_validated_at' do
expect(subject.similar_records).to eq([match2, match1, subject])
end
end
diff --git a/spec/models/users/in_product_marketing_email_spec.rb b/spec/models/users/in_product_marketing_email_spec.rb
index a9ddd86677c..cf08cf7ceed 100644
--- a/spec/models/users/in_product_marketing_email_spec.rb
+++ b/spec/models/users/in_product_marketing_email_spec.rb
@@ -21,7 +21,8 @@ RSpec.describe Users::InProductMarketingEmail, type: :model do
describe '.tracks' do
it 'has an entry for every track' do
- expect(Namespaces::InProductMarketingEmailsService::TRACKS.keys).to match_array(described_class.tracks.keys.map(&:to_sym))
+ tracks = [Namespaces::InviteTeamEmailService::TRACK, Namespaces::InProductMarketingEmailsService::TRACKS.keys].flatten
+ expect(tracks).to match_array(described_class.tracks.keys.map(&:to_sym))
end
end
diff --git a/spec/models/users/merge_request_interaction_spec.rb b/spec/models/users/merge_request_interaction_spec.rb
index d333577fa1a..12c7fa43a60 100644
--- a/spec/models/users/merge_request_interaction_spec.rb
+++ b/spec/models/users/merge_request_interaction_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe ::Users::MergeRequestInteraction do
merge_request.reviewers << user
end
- it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['UNREVIEWED'].value) }
+ it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['ATTENTION_REQUESTED'].value) }
it 'implies not reviewed' do
expect(interaction).not_to be_reviewed
@@ -70,7 +70,8 @@ RSpec.describe ::Users::MergeRequestInteraction do
context 'when the user has provided a review' do
before do
- merge_request.merge_request_reviewers.create!(reviewer: user, state: MergeRequestReviewer.states['reviewed'])
+ reviewer = merge_request.merge_request_reviewers.create!(reviewer: user)
+ reviewer.update!(state: MergeRequestReviewer.states['reviewed'])
end
it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['REVIEWED'].value) }
diff --git a/spec/models/users_statistics_spec.rb b/spec/models/users_statistics_spec.rb
index b4b7ddb7c63..8553d0bfdb0 100644
--- a/spec/models/users_statistics_spec.rb
+++ b/spec/models/users_statistics_spec.rb
@@ -34,11 +34,11 @@ RSpec.describe UsersStatistics do
describe '.create_current_stats!' do
before do
- create_list(:user_highest_role, 4)
+ create_list(:user_highest_role, 1)
create_list(:user_highest_role, 2, :guest)
- create_list(:user_highest_role, 3, :reporter)
- create_list(:user_highest_role, 4, :developer)
- create_list(:user_highest_role, 3, :maintainer)
+ create_list(:user_highest_role, 2, :reporter)
+ create_list(:user_highest_role, 2, :developer)
+ create_list(:user_highest_role, 2, :maintainer)
create_list(:user_highest_role, 2, :owner)
create_list(:user, 2, :bot)
create_list(:user, 1, :blocked)
@@ -49,11 +49,11 @@ RSpec.describe UsersStatistics do
context 'when successful' do
it 'creates an entry with the current statistics values' do
expect(described_class.create_current_stats!).to have_attributes(
- without_groups_and_projects: 4,
+ without_groups_and_projects: 1,
with_highest_role_guest: 2,
- with_highest_role_reporter: 3,
- with_highest_role_developer: 4,
- with_highest_role_maintainer: 3,
+ with_highest_role_reporter: 2,
+ with_highest_role_developer: 2,
+ with_highest_role_maintainer: 2,
with_highest_role_owner: 2,
bots: 2,
blocked: 1
diff --git a/spec/models/webauthn_registration_spec.rb b/spec/models/webauthn_registration_spec.rb
new file mode 100644
index 00000000000..6813854bf6c
--- /dev/null
+++ b/spec/models/webauthn_registration_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WebauthnRegistration do
+ describe 'relations' do
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:credential_xid) }
+ it { is_expected.to validate_presence_of(:public_key) }
+ it { is_expected.to validate_presence_of(:counter) }
+ it { is_expected.to validate_length_of(:name).is_at_least(0) }
+ it { is_expected.not_to allow_value(nil).for(:name) }
+ it do
+ is_expected.to validate_numericality_of(:counter)
+ .only_integer
+ .is_greater_than_or_equal_to(0)
+ .is_less_than_or_equal_to(4294967295)
+ end
+ end
+end