Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-08-20 21:42:06 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-08-20 21:42:06 +0300
commit6e4e1050d9dba2b7b2523fdd1768823ab85feef4 (patch)
tree78be5963ec075d80116a932011d695dd33910b4e /spec/models
parent1ce776de4ae122aba3f349c02c17cebeaa8ecf07 (diff)
Add latest changes from gitlab-org/gitlab@13-3-stable-ee
Diffstat (limited to 'spec/models')
-rw-r--r--spec/models/ability_spec.rb1
-rw-r--r--spec/models/alert_management/alert_spec.rb151
-rw-r--r--spec/models/analytics/cycle_analytics/project_stage_spec.rb2
-rw-r--r--spec/models/application_record_spec.rb23
-rw-r--r--spec/models/application_setting_spec.rb6
-rw-r--r--spec/models/audit_event_partitioned_spec.rb41
-rw-r--r--spec/models/audit_event_spec.rb18
-rw-r--r--spec/models/blob_viewer/composer_json_spec.rb1
-rw-r--r--spec/models/blob_viewer/gemspec_spec.rb1
-rw-r--r--spec/models/blob_viewer/go_mod_spec.rb1
-rw-r--r--spec/models/blob_viewer/package_json_spec.rb2
-rw-r--r--spec/models/blob_viewer/podspec_json_spec.rb1
-rw-r--r--spec/models/blob_viewer/podspec_spec.rb1
-rw-r--r--spec/models/blob_viewer/route_map_spec.rb1
-rw-r--r--spec/models/ci/build_spec.rb248
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb14
-rw-r--r--spec/models/ci/build_trace_chunks/database_spec.rb18
-rw-r--r--spec/models/ci/build_trace_chunks/fog_spec.rb28
-rw-r--r--spec/models/ci/build_trace_chunks/redis_spec.rb98
-rw-r--r--spec/models/ci/daily_build_group_report_result_spec.rb1
-rw-r--r--spec/models/ci/group_spec.rb20
-rw-r--r--spec/models/ci/instance_variable_spec.rb29
-rw-r--r--spec/models/ci/job_artifact_spec.rb76
-rw-r--r--spec/models/ci/pipeline_artifact_spec.rb82
-rw-r--r--spec/models/ci/pipeline_spec.rb311
-rw-r--r--spec/models/ci/ref_spec.rb71
-rw-r--r--spec/models/clusters/agent_spec.rb35
-rw-r--r--spec/models/clusters/agent_token_spec.rb18
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb4
-rw-r--r--spec/models/clusters/cluster_spec.rb83
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb1
-rw-r--r--spec/models/commit_collection_spec.rb29
-rw-r--r--spec/models/commit_status_spec.rb56
-rw-r--r--spec/models/commit_with_pipeline_spec.rb1
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb31
-rw-r--r--spec/models/concerns/ci/artifactable_spec.rb21
-rw-r--r--spec/models/concerns/ci/has_status_spec.rb36
-rw-r--r--spec/models/concerns/counter_attribute_spec.rb52
-rw-r--r--spec/models/concerns/featurable_spec.rb1
-rw-r--r--spec/models/concerns/issuable_spec.rb2
-rw-r--r--spec/models/concerns/manual_inverse_association_spec.rb2
-rw-r--r--spec/models/concerns/milestoneable_spec.rb18
-rw-r--r--spec/models/concerns/sha_attribute_spec.rb8
-rw-r--r--spec/models/custom_emoji_spec.rb1
-rw-r--r--spec/models/design_management/design_at_version_spec.rb9
-rw-r--r--spec/models/design_management/design_collection_spec.rb9
-rw-r--r--spec/models/design_management/design_spec.rb134
-rw-r--r--spec/models/environment_spec.rb24
-rw-r--r--spec/models/event_spec.rb39
-rw-r--r--spec/models/experiment_spec.rb112
-rw-r--r--spec/models/experiment_user_spec.rb14
-rw-r--r--spec/models/group_deploy_key_spec.rb74
-rw-r--r--spec/models/group_deploy_keys_group_spec.rb15
-rw-r--r--spec/models/group_spec.rb45
-rw-r--r--spec/models/issue_spec.rb84
-rw-r--r--spec/models/iteration_spec.rb100
-rw-r--r--spec/models/lfs_object_spec.rb16
-rw-r--r--spec/models/member_spec.rb23
-rw-r--r--spec/models/merge_request/metrics_spec.rb40
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb2
-rw-r--r--spec/models/merge_request_diff_spec.rb24
-rw-r--r--spec/models/merge_request_spec.rb32
-rw-r--r--spec/models/namespace_spec.rb1
-rw-r--r--spec/models/note_spec.rb32
-rw-r--r--spec/models/notification_setting_spec.rb43
-rw-r--r--spec/models/packages/package_file_spec.rb27
-rw-r--r--spec/models/pages_domain_spec.rb27
-rw-r--r--spec/models/personal_access_token_spec.rb12
-rw-r--r--spec/models/personal_snippet_spec.rb12
-rw-r--r--spec/models/plan_limits_spec.rb4
-rw-r--r--spec/models/product_analytics_event_spec.rb14
-rw-r--r--spec/models/project_repository_storage_move_spec.rb4
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb20
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb54
-rw-r--r--spec/models/project_services/jira_service_spec.rb2
-rw-r--r--spec/models/project_services/jira_tracker_data_spec.rb4
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb1
-rw-r--r--spec/models/project_snippet_spec.rb4
-rw-r--r--spec/models/project_spec.rb217
-rw-r--r--spec/models/project_statistics_spec.rb4
-rw-r--r--spec/models/prometheus_alert_spec.rb37
-rw-r--r--spec/models/prometheus_metric_spec.rb4
-rw-r--r--spec/models/raw_usage_data_spec.rb44
-rw-r--r--spec/models/release_spec.rb16
-rw-r--r--spec/models/repository_spec.rb48
-rw-r--r--spec/models/resource_iteration_event_spec.rb17
-rw-r--r--spec/models/resource_milestone_event_spec.rb69
-rw-r--r--spec/models/service_spec.rb48
-rw-r--r--spec/models/snippet_repository_spec.rb1
-rw-r--r--spec/models/suggestion_spec.rb52
-rw-r--r--spec/models/terraform/state_spec.rb8
-rw-r--r--spec/models/user_spec.rb35
-rw-r--r--spec/models/wiki_page_spec.rb118
-rw-r--r--spec/models/wiki_spec.rb14
94 files changed, 2429 insertions, 1005 deletions
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 9206f14fd37..3418d7d39ad 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -194,6 +194,7 @@ RSpec.describe Ability do
let(:cross_project_merge_request) do
create(:merge_request, source_project: create(:project, :public))
end
+
let(:other_merge_request) { create(:merge_request) }
let(:all_merge_requests) do
[merge_request, cross_project_merge_request, other_merge_request]
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index becc5475c15..f937a879400 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -3,6 +3,13 @@
require 'spec_helper'
RSpec.describe AlertManagement::Alert do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:triggered_alert, reload: true) { create(:alert_management_alert, :triggered, project: project) }
+ let_it_be(:acknowledged_alert, reload: true) { create(:alert_management_alert, :acknowledged, project: project) }
+ let_it_be(:resolved_alert, reload: true) { create(:alert_management_alert, :resolved, project: project2) }
+ let_it_be(:ignored_alert, reload: true) { create(:alert_management_alert, :ignored, project: project2) }
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:issue).optional }
@@ -27,63 +34,70 @@ RSpec.describe AlertManagement::Alert do
it { is_expected.to validate_length_of(:monitoring_tool).is_at_most(100) }
context 'when status is triggered' do
- context 'when ended_at is blank' do
- subject { build(:alert_management_alert) }
+ subject { triggered_alert }
+ context 'when ended_at is blank' do
it { is_expected.to be_valid }
end
context 'when ended_at is present' do
- subject { build(:alert_management_alert, ended_at: Time.current) }
+ before do
+ triggered_alert.ended_at = Time.current
+ end
it { is_expected.to be_invalid }
end
end
context 'when status is acknowledged' do
- context 'when ended_at is blank' do
- subject { build(:alert_management_alert, :acknowledged) }
+ subject { acknowledged_alert }
+ context 'when ended_at is blank' do
it { is_expected.to be_valid }
end
context 'when ended_at is present' do
- subject { build(:alert_management_alert, :acknowledged, ended_at: Time.current) }
+ before do
+ acknowledged_alert.ended_at = Time.current
+ end
it { is_expected.to be_invalid }
end
end
context 'when status is resolved' do
+ subject { resolved_alert }
+
context 'when ended_at is blank' do
- subject { build(:alert_management_alert, :resolved, ended_at: nil) }
+ before do
+ resolved_alert.ended_at = nil
+ end
it { is_expected.to be_invalid }
end
context 'when ended_at is present' do
- subject { build(:alert_management_alert, :resolved, ended_at: Time.current) }
-
it { is_expected.to be_valid }
end
end
context 'when status is ignored' do
- context 'when ended_at is blank' do
- subject { build(:alert_management_alert, :ignored) }
+ subject { ignored_alert }
+ context 'when ended_at is blank' do
it { is_expected.to be_valid }
end
context 'when ended_at is present' do
- subject { build(:alert_management_alert, :ignored, ended_at: Time.current) }
+ before do
+ ignored_alert.ended_at = Time.current
+ end
it { is_expected.to be_invalid }
end
end
describe 'fingerprint' do
- let_it_be(:project) { create(:project) }
let_it_be(:fingerprint) { 'fingerprint' }
let(:new_alert) { build(:alert_management_alert, fingerprint: fingerprint, project: project) }
@@ -93,6 +107,8 @@ RSpec.describe AlertManagement::Alert do
context 'same project, various states' do
using RSpec::Parameterized::TableSyntax
+ let_it_be(:existing_alert) { create(:alert_management_alert, fingerprint: fingerprint, project: project) }
+
# We are only validating uniqueness for non-resolved alerts
where(:existing_status, :new_status, :valid) do
:resolved | :triggered | true
@@ -114,9 +130,12 @@ RSpec.describe AlertManagement::Alert do
end
with_them do
- let!(:existing_alert) { create(:alert_management_alert, existing_status, fingerprint: fingerprint, project: project) }
let(:new_alert) { build(:alert_management_alert, new_status, fingerprint: fingerprint, project: project) }
+ before do
+ existing_alert.public_send(described_class::STATUS_EVENTS[existing_status])
+ end
+
if params[:valid]
it { is_expected.to be_valid }
else
@@ -126,7 +145,7 @@ RSpec.describe AlertManagement::Alert do
end
context 'different project' do
- let!(:existing_alert) { create(:alert_management_alert, fingerprint: fingerprint) }
+ let_it_be(:existing_alert) { create(:alert_management_alert, fingerprint: fingerprint, project: project2) }
it { is_expected.to be_valid }
end
@@ -134,7 +153,11 @@ RSpec.describe AlertManagement::Alert do
end
describe 'hosts' do
- subject(:alert) { build(:alert_management_alert, hosts: hosts) }
+ subject(:alert) { triggered_alert }
+
+ before do
+ triggered_alert.hosts = hosts
+ end
context 'over 255 total chars' do
let(:hosts) { ['111.111.111.111'] * 18 }
@@ -159,13 +182,8 @@ RSpec.describe AlertManagement::Alert do
end
describe 'scopes' do
- let_it_be(:project) { create(:project) }
- let_it_be(:triggered_alert) { create(:alert_management_alert, project: project) }
- let_it_be(:resolved_alert) { create(:alert_management_alert, :resolved, project: project) }
- let_it_be(:ignored_alert) { create(:alert_management_alert, :ignored, project: project) }
-
describe '.for_iid' do
- subject { AlertManagement::Alert.for_iid(triggered_alert.iid) }
+ subject { project.alert_management_alerts.for_iid(triggered_alert.iid) }
it { is_expected.to match_array(triggered_alert) }
end
@@ -185,30 +203,51 @@ RSpec.describe AlertManagement::Alert do
end
describe '.for_fingerprint' do
- let_it_be(:fingerprint) { SecureRandom.hex }
- let_it_be(:alert_with_fingerprint) { create(:alert_management_alert, project: project, fingerprint: fingerprint) }
- let_it_be(:unrelated_alert_with_finger_print) { create(:alert_management_alert, fingerprint: fingerprint) }
+ let(:fingerprint) { SecureRandom.hex }
+ let(:alert_with_fingerprint) { triggered_alert }
+ let(:unrelated_alert_with_finger_print) { resolved_alert }
subject { described_class.for_fingerprint(project, fingerprint) }
+ before do
+ alert_with_fingerprint.update!(fingerprint: fingerprint)
+ unrelated_alert_with_finger_print.update!(fingerprint: fingerprint)
+ end
+
it { is_expected.to contain_exactly(alert_with_fingerprint) }
end
describe '.for_environment' do
let(:environment) { create(:environment, project: project) }
- let!(:env_alert) { create(:alert_management_alert, project: project, environment: environment) }
+ let(:env_alert) { triggered_alert }
subject { described_class.for_environment(environment) }
+ before do
+ triggered_alert.update!(environment: environment)
+ end
+
it { is_expected.to match_array(env_alert) }
end
+ describe '.order_severity_with_open_prometheus_alert' do
+ subject { described_class.where(project: alert_project).order_severity_with_open_prometheus_alert }
+
+ let_it_be(:alert_project) { create(:project) }
+ let_it_be(:resolved_critical_alert) { create(:alert_management_alert, :resolved, :critical, project: alert_project) }
+ let_it_be(:triggered_critical_alert) { create(:alert_management_alert, :triggered, :critical, project: alert_project) }
+ let_it_be(:triggered_high_alert) { create(:alert_management_alert, :triggered, :high, project: alert_project) }
+
+ it { is_expected.to eq([triggered_critical_alert, triggered_high_alert]) }
+ end
+
describe '.counts_by_status' do
subject { described_class.counts_by_status }
it do
is_expected.to eq(
triggered_alert.status => 1,
+ acknowledged_alert.status => 1,
resolved_alert.status => 1,
ignored_alert.status => 1
)
@@ -218,12 +257,10 @@ RSpec.describe AlertManagement::Alert do
describe '.counts_by_project_id' do
subject { described_class.counts_by_project_id }
- let!(:alert_other_project) { create(:alert_management_alert) }
-
it do
is_expected.to eq(
- project.id => 3,
- alert_other_project.project.id => 1
+ project.id => 2,
+ project2.id => 2
)
end
end
@@ -231,16 +268,12 @@ RSpec.describe AlertManagement::Alert do
describe '.open' do
subject { described_class.open }
- let!(:acknowledged_alert) { create(:alert_management_alert, :acknowledged, project: project)}
-
it { is_expected.to contain_exactly(acknowledged_alert, triggered_alert) }
end
describe '.not_resolved' do
subject { described_class.not_resolved }
- let!(:acknowledged_alert) { create(:alert_management_alert, :acknowledged, project: project) }
-
it { is_expected.to contain_exactly(acknowledged_alert, triggered_alert, ignored_alert) }
end
end
@@ -248,27 +281,22 @@ RSpec.describe AlertManagement::Alert do
describe '.last_prometheus_alert_by_project_id' do
subject { described_class.last_prometheus_alert_by_project_id }
- let(:project_1) { create(:project) }
- let!(:alert_1) { create(:alert_management_alert, project: project_1) }
- let!(:alert_2) { create(:alert_management_alert, project: project_1) }
+ let!(:p1_alert_1) { triggered_alert }
+ let!(:p1_alert_2) { acknowledged_alert }
- let(:project_2) { create(:project) }
- let!(:alert_3) { create(:alert_management_alert, project: project_2) }
- let!(:alert_4) { create(:alert_management_alert, project: project_2) }
+ let!(:p2_alert_1) { resolved_alert }
+ let!(:p2_alert_2) { ignored_alert }
it 'returns the latest alert for each project' do
- expect(subject).to contain_exactly(alert_2, alert_4)
+ expect(subject).to contain_exactly(p1_alert_2, p2_alert_2)
end
end
describe '.search' do
- let_it_be(:alert) do
- create(:alert_management_alert,
- title: 'Title',
- description: 'Desc',
- service: 'Service',
- monitoring_tool: 'Monitor'
- )
+ let(:alert) { triggered_alert }
+
+ before do
+ alert.update!(title: 'Title', description: 'Desc', service: 'Service', monitoring_tool: 'Monitor')
end
subject { AlertManagement::Alert.search(query) }
@@ -318,7 +346,8 @@ RSpec.describe AlertManagement::Alert do
}
}
end
- let(:alert) { build(:alert_management_alert, title: 'Details title', payload: payload) }
+
+ let(:alert) { build(:alert_management_alert, project: project, title: 'Details title', payload: payload) }
subject { alert.details }
@@ -331,16 +360,14 @@ RSpec.describe AlertManagement::Alert do
end
describe '#to_reference' do
- let(:alert) { build(:alert_management_alert) }
-
- it { expect(alert.to_reference).to eq('') }
+ it { expect(triggered_alert.to_reference).to eq('') }
end
describe '#trigger' do
subject { alert.trigger }
context 'when alert is in triggered state' do
- let(:alert) { create(:alert_management_alert) }
+ let(:alert) { triggered_alert }
it 'does not change the alert status' do
expect { subject }.not_to change { alert.reload.status }
@@ -348,7 +375,7 @@ RSpec.describe AlertManagement::Alert do
end
context 'when alert not in triggered state' do
- let(:alert) { create(:alert_management_alert, :resolved) }
+ let(:alert) { resolved_alert }
it 'changes the alert status to triggered' do
expect { subject }.to change { alert.triggered? }.to(true)
@@ -363,7 +390,7 @@ RSpec.describe AlertManagement::Alert do
describe '#acknowledge' do
subject { alert.acknowledge }
- let(:alert) { create(:alert_management_alert, :resolved) }
+ let(:alert) { resolved_alert }
it 'changes the alert status to acknowledged' do
expect { subject }.to change { alert.acknowledged? }.to(true)
@@ -383,15 +410,15 @@ RSpec.describe AlertManagement::Alert do
end
context 'when alert already resolved' do
- let(:alert) { create(:alert_management_alert, :resolved) }
+ let(:alert) { resolved_alert }
it 'does not change the alert status' do
- expect { subject }.not_to change { alert.reload.status }
+ expect { subject }.not_to change { resolved_alert.reload.status }
end
end
context 'when alert is not resolved' do
- let(:alert) { create(:alert_management_alert) }
+ let(:alert) { triggered_alert }
it 'changes alert status to "resolved"' do
expect { subject }.to change { alert.resolved? }.to(true)
@@ -402,7 +429,7 @@ RSpec.describe AlertManagement::Alert do
describe '#ignore' do
subject { alert.ignore }
- let(:alert) { create(:alert_management_alert, :resolved) }
+ let(:alert) { resolved_alert }
it 'changes the alert status to ignored' do
expect { subject }.to change { alert.ignored? }.to(true)
@@ -416,7 +443,7 @@ RSpec.describe AlertManagement::Alert do
describe '#register_new_event!' do
subject { alert.register_new_event! }
- let(:alert) { create(:alert_management_alert) }
+ let(:alert) { triggered_alert }
it 'increments the events count by 1' do
expect { subject }.to change { alert.events }.by(1)
@@ -425,7 +452,7 @@ RSpec.describe AlertManagement::Alert do
describe '#present' do
context 'when alert is generic' do
- let(:alert) { build(:alert_management_alert) }
+ let(:alert) { triggered_alert }
it 'uses generic alert presenter' do
expect(alert.present).to be_kind_of(AlertManagement::AlertPresenter)
@@ -433,7 +460,7 @@ RSpec.describe AlertManagement::Alert do
end
context 'when alert is Prometheus specific' do
- let(:alert) { build(:alert_management_alert, :prometheus) }
+ let(:alert) { build(:alert_management_alert, :prometheus, project: project) }
it 'uses Prometheus Alert presenter' do
expect(alert.present).to be_kind_of(AlertManagement::PrometheusAlertPresenter)
diff --git a/spec/models/analytics/cycle_analytics/project_stage_spec.rb b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
index 2e024011553..4675f037957 100644
--- a/spec/models/analytics/cycle_analytics/project_stage_spec.rb
+++ b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Analytics::CycleAnalytics::ProjectStage do
context 'relative positioning' do
it_behaves_like 'a class that supports relative positioning' do
- let(:project) { build(:project) }
+ let_it_be(:project) { create(:project) }
let(:factory) { :cycle_analytics_project_stage }
let(:default_params) { { project: project } }
end
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index cc314d9077d..d9ab326505b 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -38,6 +38,21 @@ RSpec.describe ApplicationRecord do
expect { Suggestion.safe_find_or_create_by(build(:suggestion).attributes) }
.to change { Suggestion.count }.by(1)
end
+
+ it 'passes a block to find_or_create_by' do
+ attributes = build(:suggestion).attributes
+
+ expect do |block|
+ Suggestion.safe_find_or_create_by(attributes, &block)
+ end.to yield_with_args(an_object_having_attributes(attributes))
+ end
+
+ it 'does not create a record when is not valid' do
+ raw_usage_data = RawUsageData.safe_find_or_create_by({ recorded_at: nil })
+
+ expect(raw_usage_data.id).to be_nil
+ expect(raw_usage_data).not_to be_valid
+ end
end
describe '.safe_find_or_create_by!' do
@@ -51,6 +66,14 @@ RSpec.describe ApplicationRecord do
it 'raises a validation error if the record was not persisted' do
expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid)
end
+
+ it 'passes a block to find_or_create_by' do
+ attributes = build(:suggestion).attributes
+
+ expect do |block|
+ Suggestion.safe_find_or_create_by!(attributes, &block)
+ end.to yield_with_args(an_object_having_attributes(attributes))
+ end
end
describe '.underscore' do
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 5723b0d0729..bcd8eccd68f 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -72,6 +72,7 @@ RSpec.describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) }
it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) }
+ it { is_expected.to validate_numericality_of(:wiki_page_max_content_bytes).only_integer.is_greater_than_or_equal_to(1024) }
it { is_expected.to validate_presence_of(:max_artifacts_size) }
it { is_expected.to validate_numericality_of(:max_artifacts_size).only_integer.is_greater_than(0) }
it { is_expected.to validate_presence_of(:max_pages_size) }
@@ -86,11 +87,6 @@ RSpec.describe ApplicationSetting do
it { is_expected.not_to allow_value('abc').for(:minimum_password_length) }
it { is_expected.to allow_value(10).for(:minimum_password_length) }
- it { is_expected.to allow_value(0).for(:namespace_storage_size_limit) }
- it { is_expected.to allow_value(1).for(:namespace_storage_size_limit) }
- it { is_expected.not_to allow_value(nil).for(:namespace_storage_size_limit) }
- it { is_expected.not_to allow_value(-1).for(:namespace_storage_size_limit) }
-
it { is_expected.to allow_value(300).for(:issues_create_limit) }
it { is_expected.not_to allow_value('three').for(:issues_create_limit) }
it { is_expected.not_to allow_value(nil).for(:issues_create_limit) }
diff --git a/spec/models/audit_event_partitioned_spec.rb b/spec/models/audit_event_partitioned_spec.rb
new file mode 100644
index 00000000000..fe69f0083b7
--- /dev/null
+++ b/spec/models/audit_event_partitioned_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AuditEventPartitioned do
+ let(:source_table) { AuditEvent }
+ let(:partitioned_table) { described_class }
+
+ it 'has the same columns as the source table' do
+ expect(partitioned_table.column_names).to match_array(source_table.column_names)
+ end
+
+ it 'has the same null constraints as the source table' do
+ constraints_from_source_table = null_constraints(source_table)
+ constraints_from_partitioned_table = null_constraints(partitioned_table)
+
+ expect(constraints_from_partitioned_table.to_a).to match_array(constraints_from_source_table.to_a)
+ end
+
+ it 'inserts the same record as the one in the source table', :aggregate_failures do
+ expect { create(:audit_event) }.to change { partitioned_table.count }.by(1)
+
+ event_from_source_table = source_table.connection.select_one(
+ "SELECT * FROM #{source_table.table_name} ORDER BY created_at desc LIMIT 1"
+ )
+ event_from_partitioned_table = partitioned_table.connection.select_one(
+ "SELECT * FROM #{partitioned_table.table_name} ORDER BY created_at desc LIMIT 1"
+ )
+
+ expect(event_from_partitioned_table).to eq(event_from_source_table)
+ end
+
+ def null_constraints(table)
+ table.connection.select_all(<<~SQL)
+ SELECT c.column_name, c.is_nullable
+ FROM information_schema.columns c
+ WHERE c.table_name = '#{table.table_name}'
+ AND c.column_name != 'created_at'
+ SQL
+ end
+end
diff --git a/spec/models/audit_event_spec.rb b/spec/models/audit_event_spec.rb
new file mode 100644
index 00000000000..a1ed48c57f4
--- /dev/null
+++ b/spec/models/audit_event_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AuditEvent do
+ let_it_be(:audit_event) { create(:project_audit_event) }
+ subject { audit_event }
+
+ describe '#as_json' do
+ context 'ip_address' do
+ subject { build(:group_audit_event, ip_address: '192.168.1.1').as_json }
+
+ it 'overrides the ip_address with its string value' do
+ expect(subject['ip_address']).to eq('192.168.1.1')
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/composer_json_spec.rb b/spec/models/blob_viewer/composer_json_spec.rb
index 8d66e9e951f..5af58f3d6c7 100644
--- a/spec/models/blob_viewer/composer_json_spec.rb
+++ b/spec/models/blob_viewer/composer_json_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe BlobViewer::ComposerJson do
}
SPEC
end
+
let(:blob) { fake_blob(path: 'composer.json', data: data) }
subject { described_class.new(blob) }
diff --git a/spec/models/blob_viewer/gemspec_spec.rb b/spec/models/blob_viewer/gemspec_spec.rb
index b6f3e059c7e..43c63050c90 100644
--- a/spec/models/blob_viewer/gemspec_spec.rb
+++ b/spec/models/blob_viewer/gemspec_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe BlobViewer::Gemspec do
end
SPEC
end
+
let(:blob) { fake_blob(path: 'activerecord.gemspec', data: data) }
subject { described_class.new(blob) }
diff --git a/spec/models/blob_viewer/go_mod_spec.rb b/spec/models/blob_viewer/go_mod_spec.rb
index 21e84d39a54..3249e86fb03 100644
--- a/spec/models/blob_viewer/go_mod_spec.rb
+++ b/spec/models/blob_viewer/go_mod_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe BlobViewer::GoMod do
module #{Settings.build_gitlab_go_url}/#{project.full_path}
SPEC
end
+
let(:blob) { fake_blob(path: 'go.mod', data: data) }
subject { described_class.new(blob) }
diff --git a/spec/models/blob_viewer/package_json_spec.rb b/spec/models/blob_viewer/package_json_spec.rb
index d2e8ab6575f..8a394a7334f 100644
--- a/spec/models/blob_viewer/package_json_spec.rb
+++ b/spec/models/blob_viewer/package_json_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe BlobViewer::PackageJson do
}
SPEC
end
+
let(:blob) { fake_blob(path: 'package.json', data: data) }
subject { described_class.new(blob) }
@@ -54,6 +55,7 @@ RSpec.describe BlobViewer::PackageJson do
}
SPEC
end
+
let(:blob) { fake_blob(path: 'package.json', data: data) }
subject { described_class.new(blob) }
diff --git a/spec/models/blob_viewer/podspec_json_spec.rb b/spec/models/blob_viewer/podspec_json_spec.rb
index 61d2602c413..cdeea4e8744 100644
--- a/spec/models/blob_viewer/podspec_json_spec.rb
+++ b/spec/models/blob_viewer/podspec_json_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe BlobViewer::PodspecJson do
}
SPEC
end
+
let(:blob) { fake_blob(path: 'AFNetworking.podspec.json', data: data) }
subject { described_class.new(blob) }
diff --git a/spec/models/blob_viewer/podspec_spec.rb b/spec/models/blob_viewer/podspec_spec.rb
index 0a0fbcaebd4..c2828067f22 100644
--- a/spec/models/blob_viewer/podspec_spec.rb
+++ b/spec/models/blob_viewer/podspec_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe BlobViewer::Podspec do
end
SPEC
end
+
let(:blob) { fake_blob(path: 'Reachability.podspec', data: data) }
subject { described_class.new(blob) }
diff --git a/spec/models/blob_viewer/route_map_spec.rb b/spec/models/blob_viewer/route_map_spec.rb
index bb0284d7868..c412afaac4e 100644
--- a/spec/models/blob_viewer/route_map_spec.rb
+++ b/spec/models/blob_viewer/route_map_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe BlobViewer::RouteMap do
public: 'team/'
MAP
end
+
let(:blob) { fake_blob(path: '.gitlab/route-map.yml', data: data) }
subject { described_class.new(blob) }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 857b238981b..069ac23c5a4 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -612,6 +612,62 @@ RSpec.describe Ci::Build do
end
end
+ describe '#available_artifacts?' do
+ let(:build) { create(:ci_build) }
+
+ subject { build.available_artifacts? }
+
+ context 'when artifacts are not expired' do
+ before do
+ build.artifacts_expire_at = Date.tomorrow
+ end
+
+ context 'when artifacts exist' do
+ before do
+ create(:ci_job_artifact, :archive, job: build)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when artifacts do not exist' do
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when artifacts are expired' do
+ before do
+ build.artifacts_expire_at = Date.yesterday
+ end
+
+ context 'when artifacts are not locked' do
+ before do
+ build.pipeline.locked = :unlocked
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when artifacts are locked' do
+ before do
+ build.pipeline.locked = :artifacts_locked
+ end
+
+ context 'when artifacts exist' do
+ before do
+ create(:ci_job_artifact, :archive, job: build)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when artifacts do not exist' do
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+ end
+
describe '#browsable_artifacts?' do
subject { build.browsable_artifacts? }
@@ -1195,18 +1251,6 @@ RSpec.describe Ci::Build do
is_expected.to eq('review/host')
end
-
- context 'when ci_persisted_expanded_environment_name feature flag is disabled' do
- before do
- stub_feature_flags(ci_persisted_expanded_environment_name: false)
- end
-
- it 'returns an expanded environment name with a list of variables' do
- expect(build).to receive(:simple_variables).once.and_call_original
-
- is_expected.to eq('review/host')
- end
- end
end
end
@@ -1703,112 +1747,6 @@ RSpec.describe Ci::Build do
end
end
end
-
- describe '#options_retry_max' do
- context 'with retries max config option' do
- subject { create(:ci_build, options: { retry: { max: 1 } }) }
-
- context 'when build_metadata_config is set' do
- before do
- stub_feature_flags(ci_build_metadata_config: true)
- end
-
- it 'returns the number of configured max retries' do
- expect(subject.options_retry_max).to eq 1
- end
- end
-
- context 'when build_metadata_config is not set' do
- before do
- stub_feature_flags(ci_build_metadata_config: false)
- end
-
- it 'returns the number of configured max retries' do
- expect(subject.options_retry_max).to eq 1
- end
- end
- end
-
- context 'without retries max config option' do
- subject { create(:ci_build) }
-
- it 'returns nil' do
- expect(subject.options_retry_max).to be_nil
- end
- end
-
- context 'when build is degenerated' do
- subject { create(:ci_build, :degenerated) }
-
- it 'returns nil' do
- expect(subject.options_retry_max).to be_nil
- end
- end
-
- context 'with integer only config option' do
- subject { create(:ci_build, options: { retry: 1 }) }
-
- it 'returns the number of configured max retries' do
- expect(subject.options_retry_max).to eq 1
- end
- end
- end
-
- describe '#options_retry_when' do
- context 'with retries when config option' do
- subject { create(:ci_build, options: { retry: { when: ['some_reason'] } }) }
-
- it 'returns the configured when' do
- expect(subject.options_retry_when).to eq ['some_reason']
- end
- end
-
- context 'without retries when config option' do
- subject { create(:ci_build) }
-
- it 'returns always array' do
- expect(subject.options_retry_when).to eq ['always']
- end
- end
-
- context 'with integer only config option' do
- subject { create(:ci_build, options: { retry: 1 }) }
-
- it 'returns always array' do
- expect(subject.options_retry_when).to eq ['always']
- end
- end
- end
-
- describe '#retry_failure?' do
- using RSpec::Parameterized::TableSyntax
-
- let(:build) { create(:ci_build) }
-
- subject { build.retry_failure? }
-
- where(:description, :retry_count, :options, :failure_reason, :result) do
- "retries are disabled" | 0 | { max: 0 } | nil | false
- "max equals count" | 2 | { max: 2 } | nil | false
- "max is higher than count" | 1 | { max: 2 } | nil | true
- "matching failure reason" | 0 | { when: %w[api_failure], max: 2 } | :api_failure | true
- "not matching with always" | 0 | { when: %w[always], max: 2 } | :api_failure | true
- "not matching reason" | 0 | { when: %w[script_error], max: 2 } | :api_failure | false
- "scheduler failure override" | 1 | { when: %w[scheduler_failure], max: 1 } | :scheduler_failure | false
- "default for scheduler failure" | 1 | {} | :scheduler_failure | true
- end
-
- with_them do
- before do
- allow(build).to receive(:retries_count) { retry_count }
-
- build.options[:retry] = options
- build.failure_reason = failure_reason
- end
-
- it { is_expected.to eq(result) }
- end
- end
end
describe '.keep_artifacts!' do
@@ -2115,23 +2053,13 @@ RSpec.describe Ci::Build do
it { is_expected.to be_nil }
end
- context 'when build has a start environment' do
- let(:build) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }
-
- it 'does not expand environment name' do
- expect(build).not_to receive(:expanded_environment_name)
-
- subject
- end
- end
-
context 'when build has a stop environment' do
- let(:build) { create(:ci_build, :stop_review_app, pipeline: pipeline) }
+ let(:build) { create(:ci_build, :stop_review_app, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
it 'expands environment name' do
- expect(build).to receive(:expanded_environment_name)
+ expect(build).to receive(:expanded_environment_name).and_call_original
- subject
+ is_expected.to eq(environment)
end
end
end
@@ -2925,6 +2853,7 @@ RSpec.describe Ci::Build do
let(:ci_registry) do
{ key: 'CI_REGISTRY', value: 'registry.example.com', public: true, masked: false }
end
+
let(:ci_registry_image) do
{ key: 'CI_REGISTRY_IMAGE', value: project.container_registry_url, public: true, masked: false }
end
@@ -3007,25 +2936,46 @@ RSpec.describe Ci::Build do
end
context 'when build is parallelized' do
- let(:total) { 5 }
- let(:index) { 3 }
+ shared_examples 'parallelized jobs config' do
+ let(:index) { 3 }
+ let(:total) { 5 }
- before do
- build.options[:parallel] = total
- build.options[:instance] = index
- build.name = "#{build.name} #{index}/#{total}"
+ before do
+ build.options[:parallel] = config
+ build.options[:instance] = index
+ end
+
+ it 'includes CI_NODE_INDEX' do
+ is_expected.to include(
+ { key: 'CI_NODE_INDEX', value: index.to_s, public: true, masked: false }
+ )
+ end
+
+ it 'includes correct CI_NODE_TOTAL' do
+ is_expected.to include(
+ { key: 'CI_NODE_TOTAL', value: total.to_s, public: true, masked: false }
+ )
+ end
end
- it 'includes CI_NODE_INDEX' do
- is_expected.to include(
- { key: 'CI_NODE_INDEX', value: index.to_s, public: true, masked: false }
- )
+ context 'when parallel is a number' do
+ let(:config) { 5 }
+
+ it_behaves_like 'parallelized jobs config'
end
- it 'includes correct CI_NODE_TOTAL' do
- is_expected.to include(
- { key: 'CI_NODE_TOTAL', value: total.to_s, public: true, masked: false }
- )
+ context 'when parallel is hash with the total key' do
+ let(:config) { { total: 5 } }
+
+ it_behaves_like 'parallelized jobs config'
+ end
+
+ context 'when parallel is nil' do
+ let(:config) {}
+
+ it_behaves_like 'parallelized jobs config' do
+ let(:total) { 1 }
+ end
end
end
@@ -3161,6 +3111,14 @@ RSpec.describe Ci::Build do
end
end
+ describe '#simple_variables_without_dependencies' do
+ it 'does not load dependencies' do
+ expect(build).not_to receive(:dependency_variables)
+
+ build.simple_variables_without_dependencies
+ end
+ end
+
shared_examples "secret CI variables" do
context 'when ref is branch' do
let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) }
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index dab523f67ab..a6362d46449 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -262,6 +262,12 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(build_trace_chunk.data).to be_empty
end
+ it 'does not read data when appending' do
+ expect(build_trace_chunk).not_to receive(:data)
+
+ build_trace_chunk.append(new_data, offset)
+ end
+
it_behaves_like 'Appending correctly'
it_behaves_like 'Scheduling sidekiq worker to flush data to persist store'
end
@@ -486,7 +492,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(build_trace_chunk.redis?).to be_truthy
expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data)
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
- expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil
subject
@@ -508,7 +514,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(build_trace_chunk.redis?).to be_truthy
expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data)
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
- expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil
end
end
end
@@ -535,7 +541,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(build_trace_chunk.database?).to be_truthy
expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data)
- expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil
subject
@@ -557,7 +563,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(build_trace_chunk.database?).to be_truthy
expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data)
- expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil
end
end
end
diff --git a/spec/models/ci/build_trace_chunks/database_spec.rb b/spec/models/ci/build_trace_chunks/database_spec.rb
index 245625b8046..313328ac037 100644
--- a/spec/models/ci/build_trace_chunks/database_spec.rb
+++ b/spec/models/ci/build_trace_chunks/database_spec.rb
@@ -89,6 +89,24 @@ RSpec.describe Ci::BuildTraceChunks::Database do
end
end
+ describe '#size' do
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :database_with_data, initial_data: 'üabcdef') }
+
+ it 'returns data bytesize correctly' do
+ expect(data_store.size(model)).to eq 8
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :database_without_data) }
+
+ it 'returns zero' do
+ expect(data_store.size(model)).to be_zero
+ end
+ end
+ end
+
describe '#keys' do
subject { data_store.keys(relation) }
diff --git a/spec/models/ci/build_trace_chunks/fog_spec.rb b/spec/models/ci/build_trace_chunks/fog_spec.rb
index 7ef3018d87b..a44ae58dfd2 100644
--- a/spec/models/ci/build_trace_chunks/fog_spec.rb
+++ b/spec/models/ci/build_trace_chunks/fog_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe Ci::BuildTraceChunks::Fog do
let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
it 'returns nil' do
- expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+ expect(data_store.data(model)).to be_nil
end
end
end
@@ -66,7 +66,7 @@ RSpec.describe Ci::BuildTraceChunks::Fog do
let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
it 'sets new data' do
- expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+ expect(data_store.data(model)).to be_nil
subject
@@ -86,7 +86,7 @@ RSpec.describe Ci::BuildTraceChunks::Fog do
subject
- expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+ expect(data_store.data(model)).to be_nil
end
end
@@ -94,11 +94,29 @@ RSpec.describe Ci::BuildTraceChunks::Fog do
let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
it 'does nothing' do
- expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+ expect(data_store.data(model)).to be_nil
subject
- expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+ expect(data_store.data(model)).to be_nil
+ end
+ end
+ end
+
+ describe '#size' do
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'üabcd') }
+
+ it 'returns data bytesize correctly' do
+ expect(data_store.size(model)).to eq 6
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
+
+ it 'returns zero' do
+ expect(data_store.size(model)).to be_zero
end
end
end
diff --git a/spec/models/ci/build_trace_chunks/redis_spec.rb b/spec/models/ci/build_trace_chunks/redis_spec.rb
index c37b8697a4d..cb0b6baadeb 100644
--- a/spec/models/ci/build_trace_chunks/redis_spec.rb
+++ b/spec/models/ci/build_trace_chunks/redis_spec.rb
@@ -61,6 +61,86 @@ RSpec.describe Ci::BuildTraceChunks::Redis, :clean_gitlab_redis_shared_state do
end
end
+ describe '#append_data' do
+ context 'when valid offset is used with existing data' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'abcd') }
+
+ it 'appends data' do
+ expect(data_store.data(model)).to eq('abcd')
+
+ length = data_store.append_data(model, '12345', 4)
+
+ expect(length).to eq 9
+ expect(data_store.data(model)).to eq('abcd12345')
+ end
+ end
+
+ context 'when data does not exist yet' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
+
+ it 'sets new data' do
+ expect(data_store.data(model)).to be_nil
+
+ length = data_store.append_data(model, 'abc', 0)
+
+ expect(length).to eq 3
+ expect(data_store.data(model)).to eq('abc')
+ end
+ end
+
+ context 'when data needs to be truncated' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: '12345678') }
+
+ it 'appends data and truncates stored value' do
+ expect(data_store.data(model)).to eq('12345678')
+
+ length = data_store.append_data(model, 'ab', 4)
+
+ expect(length).to eq 6
+ expect(data_store.data(model)).to eq('1234ab')
+ end
+ end
+
+ context 'when invalid offset is provided' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'abc') }
+
+ it 'raises an exception' do
+ length = data_store.append_data(model, '12345', 4)
+
+ expect(length).to be_negative
+ end
+ end
+
+ context 'when trace contains multi-byte UTF8 characters' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'aüc') }
+
+ it 'appends data' do
+ length = data_store.append_data(model, '1234', 4)
+
+ data_store.data(model).then do |new_data|
+ expect(new_data.bytesize).to eq 8
+ expect(new_data).to eq 'aüc1234'
+ end
+
+ expect(length).to eq 8
+ end
+ end
+
+ context 'when trace contains non-UTF8 characters' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: "a\255c") }
+
+ it 'appends data' do
+ length = data_store.append_data(model, '1234', 3)
+
+ data_store.data(model).then do |new_data|
+ expect(new_data.bytesize).to eq 7
+ end
+
+ expect(length).to eq 7
+ end
+ end
+ end
+
describe '#delete_data' do
subject { data_store.delete_data(model) }
@@ -89,6 +169,24 @@ RSpec.describe Ci::BuildTraceChunks::Redis, :clean_gitlab_redis_shared_state do
end
end
+ describe '#size' do
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'üabcd') }
+
+ it 'returns data bytesize correctly' do
+ expect(data_store.size(model)).to eq 6
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
+
+ it 'returns zero' do
+ expect(data_store.size(model)).to be_zero
+ end
+ end
+ end
+
describe '#keys' do
subject { data_store.keys(relation) }
diff --git a/spec/models/ci/daily_build_group_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb
index 059a5b76b9a..326366666cb 100644
--- a/spec/models/ci/daily_build_group_report_result_spec.rb
+++ b/spec/models/ci/daily_build_group_report_result_spec.rb
@@ -36,6 +36,7 @@ RSpec.describe Ci::DailyBuildGroupReportResult do
data: { coverage: 71.2 }
)
end
+
let!(:new_pipeline) { create(:ci_pipeline) }
it 'creates or updates matching report results' do
diff --git a/spec/models/ci/group_spec.rb b/spec/models/ci/group_spec.rb
index dc9aee906ea..c20b7e61044 100644
--- a/spec/models/ci/group_spec.rb
+++ b/spec/models/ci/group_spec.rb
@@ -29,24 +29,8 @@ RSpec.describe Ci::Group do
[create(:ci_build, :failed)]
end
- context 'when ci_composite_status is enabled' do
- before do
- stub_feature_flags(ci_composite_status: true)
- end
-
- it 'returns a failed status' do
- expect(subject.status).to eq('failed')
- end
- end
-
- context 'when ci_composite_status is disabled' do
- before do
- stub_feature_flags(ci_composite_status: false)
- end
-
- it 'returns a failed status' do
- expect(subject.status).to eq('failed')
- end
+ it 'returns a failed status' do
+ expect(subject.status).to eq('failed')
end
end
diff --git a/spec/models/ci/instance_variable_spec.rb b/spec/models/ci/instance_variable_spec.rb
index 344ba5bfafd..15d0c911bc4 100644
--- a/spec/models/ci/instance_variable_spec.rb
+++ b/spec/models/ci/instance_variable_spec.rb
@@ -9,12 +9,39 @@ RSpec.describe Ci::InstanceVariable do
it { is_expected.to include_module(Ci::Maskable) }
it { is_expected.to validate_uniqueness_of(:key).with_message(/\(\w+\) has already been taken/) }
- it { is_expected.to validate_length_of(:encrypted_value).is_at_most(1024).with_message(/Variables over 700 characters risk exceeding the limit/) }
+ it { is_expected.to validate_length_of(:value).is_at_most(10_000).with_message(/The value of the provided variable exceeds the 10000 character limit/) }
it_behaves_like 'includes Limitable concern' do
subject { build(:ci_instance_variable) }
end
+ describe '#value' do
+ context 'without application limit' do
+ # Ensures breakage if encryption algorithm changes
+ let(:variable) { build(:ci_instance_variable, key: 'too_long', value: value) }
+
+ before do
+ allow(variable).to receive(:valid?).and_return(true)
+ end
+
+ context 'when value is over the limit' do
+ let(:value) { SecureRandom.alphanumeric(10_002) }
+
+ it 'raises a database level error' do
+ expect { variable.save }.to raise_error(ActiveRecord::StatementInvalid)
+ end
+ end
+
+ context 'when value is under the limit' do
+ let(:value) { SecureRandom.alphanumeric(10_000) }
+
+ it 'does not raise database level error' do
+ expect { variable.save }.not_to raise_error
+ end
+ end
+ end
+ end
+
describe '.unprotected' do
subject { described_class.unprotected }
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index b5f9128b7c5..91a669aa3f4 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -483,11 +483,7 @@ RSpec.describe Ci::JobArtifact do
subject { create(:ci_job_artifact, :archive) }
context 'when existing object has local store' do
- it 'is stored locally' do
- expect(subject.file_store).to be(ObjectStorage::Store::LOCAL)
- expect(subject.file).to be_file_storage
- expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
- end
+ it_behaves_like 'mounted file in local store'
end
context 'when direct upload is enabled' do
@@ -496,11 +492,7 @@ RSpec.describe Ci::JobArtifact do
end
context 'when file is stored' do
- it 'is stored remotely' do
- expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE)
- expect(subject.file).not_to be_file_storage
- expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE)
- end
+ it_behaves_like 'mounted file in object store'
end
end
end
@@ -529,11 +521,9 @@ RSpec.describe Ci::JobArtifact do
context 'when file type is supported' do
let(:project_closest_setting) { 1024 }
let(:artifact_type) { 'junit' }
+ let(:limit_name) { "#{described_class::PLAN_LIMIT_PREFIX}#{artifact_type}" }
- before do
- stub_feature_flags(ci_max_artifact_size_per_type: flag_enabled)
- allow(build.project).to receive(:closest_setting).with(:max_artifacts_size).and_return(project_closest_setting)
- end
+ let!(:plan_limits) { create(:plan_limits, :default_plan) }
shared_examples_for 'basing off the project closest setting' do
it { is_expected.to eq(project_closest_setting.megabytes.to_i) }
@@ -543,49 +533,40 @@ RSpec.describe Ci::JobArtifact do
it { is_expected.to eq(max_size_for_type.megabytes.to_i) }
end
- context 'and feature flag for custom max size per type is enabled' do
- let(:flag_enabled) { true }
- let(:limit_name) { "#{described_class::PLAN_LIMIT_PREFIX}#{artifact_type}" }
-
- let!(:plan_limits) { create(:plan_limits, :default_plan) }
+ before do
+ allow(build.project).to receive(:closest_setting).with(:max_artifacts_size).and_return(project_closest_setting)
+ end
- context 'and plan limit is disabled for the given artifact type' do
- before do
- plan_limits.update!(limit_name => 0)
- end
+ context 'and plan limit is disabled for the given artifact type' do
+ before do
+ plan_limits.update!(limit_name => 0)
+ end
- it_behaves_like 'basing off the project closest setting'
+ it_behaves_like 'basing off the project closest setting'
- context 'and project closest setting results to zero' do
- let(:project_closest_setting) { 0 }
+ context 'and project closest setting results to zero' do
+ let(:project_closest_setting) { 0 }
- it { is_expected.to eq(0) }
- end
+ it { is_expected.to eq(0) }
end
+ end
- context 'and plan limit is enabled for the given artifact type' do
- before do
- plan_limits.update!(limit_name => max_size_for_type)
- end
-
- context 'and plan limit is smaller than project setting' do
- let(:max_size_for_type) { project_closest_setting - 1 }
-
- it_behaves_like 'basing off the plan limit'
- end
+ context 'and plan limit is enabled for the given artifact type' do
+ before do
+ plan_limits.update!(limit_name => max_size_for_type)
+ end
- context 'and plan limit is smaller than project setting' do
- let(:max_size_for_type) { project_closest_setting + 1 }
+ context 'and plan limit is smaller than project setting' do
+ let(:max_size_for_type) { project_closest_setting - 1 }
- it_behaves_like 'basing off the project closest setting'
- end
+ it_behaves_like 'basing off the plan limit'
end
- end
- context 'and feature flag for custom max size per type is disabled' do
- let(:flag_enabled) { false }
+ context 'and plan limit is larger than project setting' do
+ let(:max_size_for_type) { project_closest_setting + 1 }
- it_behaves_like 'basing off the project closest setting'
+ it_behaves_like 'basing off the project closest setting'
+ end
end
end
end
@@ -597,7 +578,8 @@ RSpec.describe Ci::JobArtifact do
Please refer to https://docs.gitlab.com/ee/development/application_limits.html on how to add new plan limit columns.
Take note that while existing max size plan limits default to 0, succeeding new limits are recommended to have
- non-zero default values.
+ non-zero default values. Also, remember to update the plan limits documentation (doc/administration/instance_limits.md)
+ when changes or new entries are made.
MSG
end
end
diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb
new file mode 100644
index 00000000000..9d63d74a6cc
--- /dev/null
+++ b/spec/models/ci/pipeline_artifact_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineArtifact, type: :model do
+ let_it_be(:coverage_report) { create(:ci_pipeline_artifact) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:pipeline) }
+ it { is_expected.to belong_to(:project) }
+ end
+
+ it_behaves_like 'having unique enum values'
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:pipeline) }
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:file_type) }
+ it { is_expected.to validate_presence_of(:file_format) }
+ it { is_expected.to validate_presence_of(:size) }
+ it { is_expected.to validate_presence_of(:file) }
+
+ context 'when attributes are valid' do
+ it 'returns no errors' do
+ expect(coverage_report).to be_valid
+ end
+ end
+
+ context 'when file_store is invalid' do
+ it 'returns errors' do
+ coverage_report.file_store = 0
+
+ expect(coverage_report).to be_invalid
+ expect(coverage_report.errors.full_messages).to eq(["File store is not included in the list"])
+ end
+ end
+
+ context 'when size is over 10 megabytes' do
+ it 'returns errors' do
+ coverage_report.size = 11.megabytes
+
+ expect(coverage_report).to be_invalid
+ end
+ end
+ end
+
+ describe '#set_size' do
+ subject { create(:ci_pipeline_artifact) }
+
+ context 'when file is being created' do
+ it 'sets the size' do
+ expect(subject.size).to eq(85)
+ end
+ end
+
+ context 'when file is being updated' do
+ it 'updates the size' do
+ subject.update!(file: fixture_file_upload('spec/fixtures/dk.png'))
+
+ expect(subject.size).to eq(1062)
+ end
+ end
+ end
+
+ describe 'file is being stored' do
+ subject { create(:ci_pipeline_artifact) }
+
+ context 'when existing object has local store' do
+ it_behaves_like 'mounted file in local store'
+ end
+
+ context 'when direct upload is enabled' do
+ before do
+ stub_artifacts_object_storage(Ci::PipelineArtifactUploader, direct_upload: true)
+ end
+
+ context 'when file is stored' do
+ it_behaves_like 'mounted file in object store'
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index ed2466d6413..b4e80fa7588 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -46,6 +46,7 @@ RSpec.describe Ci::Pipeline, :mailer do
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
it { is_expected.to delegate_method(:full_path).to(:project).with_prefix }
+ it { is_expected.to have_many(:pipeline_artifacts) }
describe 'associations' do
it 'has a bidirectional relationship with projects' do
@@ -813,6 +814,8 @@ RSpec.describe Ci::Pipeline, :mailer do
expect(subject.to_hash)
.to include(
'CI_EXTERNAL_PULL_REQUEST_IID' => pull_request.pull_request_iid.to_s,
+ 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY' => pull_request.source_repository,
+ 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY' => pull_request.target_repository,
'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA' => pull_request.source_sha,
'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA' => pull_request.target_sha,
'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME' => pull_request.source_branch,
@@ -936,69 +939,59 @@ RSpec.describe Ci::Pipeline, :mailer do
subject { pipeline.legacy_stages }
- where(:ci_composite_status) do
- [false, true]
+ context 'stages list' do
+ it 'returns ordered list of stages' do
+ expect(subject.map(&:name)).to eq(%w[build test deploy])
+ end
end
- with_them do
- before do
- stub_feature_flags(ci_composite_status: ci_composite_status)
+ context 'stages with statuses' do
+ let(:statuses) do
+ subject.map { |stage| [stage.name, stage.status] }
end
- context 'stages list' do
- it 'returns ordered list of stages' do
- expect(subject.map(&:name)).to eq(%w[build test deploy])
- end
+ it 'returns list of stages with correct statuses' do
+ expect(statuses).to eq([%w(build failed),
+ %w(test success),
+ %w(deploy running)])
end
- context 'stages with statuses' do
- let(:statuses) do
- subject.map { |stage| [stage.name, stage.status] }
+ context 'when commit status is retried' do
+ before do
+ create(:commit_status, pipeline: pipeline,
+ stage: 'build',
+ name: 'mac',
+ stage_idx: 0,
+ status: 'success')
+
+ Ci::ProcessPipelineService
+ .new(pipeline)
+ .execute
end
- it 'returns list of stages with correct statuses' do
- expect(statuses).to eq([%w(build failed),
+ it 'ignores the previous state' do
+ expect(statuses).to eq([%w(build success),
%w(test success),
%w(deploy running)])
end
-
- context 'when commit status is retried' do
- before do
- create(:commit_status, pipeline: pipeline,
- stage: 'build',
- name: 'mac',
- stage_idx: 0,
- status: 'success')
-
- Ci::ProcessPipelineService
- .new(pipeline)
- .execute
- end
-
- it 'ignores the previous state' do
- expect(statuses).to eq([%w(build success),
- %w(test success),
- %w(deploy running)])
- end
- end
end
+ end
- context 'when there is a stage with warnings' do
- before do
- create(:commit_status, pipeline: pipeline,
- stage: 'deploy',
- name: 'prod:2',
- stage_idx: 2,
- status: 'failed',
- allow_failure: true)
- end
+ context 'when there is a stage with warnings' do
+ before do
+ create(:commit_status, pipeline: pipeline,
+ stage: 'deploy',
+ name: 'prod:2',
+ stage_idx: 2,
+ status: 'failed',
+ allow_failure: true)
+ end
- it 'populates stage with correct number of warnings' do
- deploy_stage = pipeline.legacy_stages.third
+ it 'populates stage with correct number of warnings' do
+ deploy_stage = pipeline.legacy_stages.third
- expect(deploy_stage).not_to receive(:statuses)
- expect(deploy_stage).to have_warnings
- end
+ expect(deploy_stage).not_to receive(:statuses)
+ expect(deploy_stage).to have_warnings
end
end
end
@@ -1044,19 +1037,6 @@ RSpec.describe Ci::Pipeline, :mailer do
before do
create(:ci_stage_entity, project: project,
pipeline: pipeline,
- name: 'build')
- end
-
- it 'returns persisted stages' do
- expect(pipeline.stages).not_to be_empty
- expect(pipeline.stages).to all(be_persisted)
- end
- end
-
- describe '#ordered_stages' do
- before do
- create(:ci_stage_entity, project: project,
- pipeline: pipeline,
position: 4,
name: 'deploy')
@@ -1083,60 +1063,25 @@ RSpec.describe Ci::Pipeline, :mailer do
name: 'cleanup')
end
- subject { pipeline.ordered_stages }
+ subject { pipeline.stages }
- context 'when using atomic processing' do
- before do
- stub_feature_flags(
- ci_atomic_processing: true
- )
- end
-
- context 'when pipelines is not complete' do
- it 'returns stages in valid order' do
- expect(subject).to all(be_a Ci::Stage)
- expect(subject.map(&:name))
- .to eq %w[sanity build test deploy cleanup]
- end
- end
-
- context 'when pipeline is complete' do
- before do
- pipeline.succeed!
- end
-
- it 'returns stages in valid order' do
- expect(subject).to all(be_a Ci::Stage)
- expect(subject.map(&:name))
- .to eq %w[sanity build test deploy cleanup]
- end
+ context 'when pipelines is not complete' do
+ it 'returns stages in valid order' do
+ expect(subject).to all(be_a Ci::Stage)
+ expect(subject.map(&:name))
+ .to eq %w[sanity build test deploy cleanup]
end
end
- context 'when using persisted stages' do
+ context 'when pipeline is complete' do
before do
- stub_feature_flags(
- ci_atomic_processing: false
- )
+ pipeline.succeed!
end
- context 'when pipelines is not complete' do
- it 'still returns legacy stages' do
- expect(subject).to all(be_a Ci::LegacyStage)
- expect(subject.map(&:name)).to eq %w[build test]
- end
- end
-
- context 'when pipeline is complete' do
- before do
- pipeline.succeed!
- end
-
- it 'returns stages in valid order' do
- expect(subject).to all(be_a Ci::Stage)
- expect(subject.map(&:name))
- .to eq %w[sanity build test deploy cleanup]
- end
+ it 'returns stages in valid order' do
+ expect(subject).to all(be_a Ci::Stage)
+ expect(subject.map(&:name))
+ .to eq %w[sanity build test deploy cleanup]
end
end
end
@@ -1932,6 +1877,7 @@ RSpec.describe Ci::Pipeline, :mailer do
project: project
)
end
+
let!(:commit_123_ref_develop) do
create(
:ci_empty_pipeline,
@@ -1941,6 +1887,7 @@ RSpec.describe Ci::Pipeline, :mailer do
project: project
)
end
+
let!(:commit_456_ref_test) do
create(
:ci_empty_pipeline,
@@ -2139,58 +2086,6 @@ RSpec.describe Ci::Pipeline, :mailer do
end
end
- describe '#update_status' do
- context 'when pipeline is empty' do
- it 'updates does not change pipeline status' do
- expect(pipeline.statuses.latest.slow_composite_status(project: project)).to be_nil
-
- expect { pipeline.update_legacy_status }
- .to change { pipeline.reload.status }
- .from('created')
- .to('skipped')
- end
- end
-
- context 'when updating status to pending' do
- before do
- create(:ci_build, pipeline: pipeline, status: :running)
- end
-
- it 'updates pipeline status to running' do
- expect { pipeline.update_legacy_status }
- .to change { pipeline.reload.status }
- .from('created')
- .to('running')
- end
- end
-
- context 'when updating status to scheduled' do
- before do
- create(:ci_build, pipeline: pipeline, status: :scheduled)
- end
-
- it 'updates pipeline status to scheduled' do
- expect { pipeline.update_legacy_status }
- .to change { pipeline.reload.status }
- .from('created')
- .to('scheduled')
- end
- end
-
- context 'when statuses status was not recognized' do
- before do
- allow(pipeline)
- .to receive(:latest_builds_status)
- .and_return(:unknown)
- end
-
- it 'raises an exception' do
- expect { pipeline.update_legacy_status }
- .to raise_error(Ci::HasStatus::UnknownStatusError)
- end
- end
- end
-
describe '#detailed_status' do
subject { pipeline.detailed_status(user) }
@@ -2918,25 +2813,9 @@ RSpec.describe Ci::Pipeline, :mailer do
describe '#ensure_ci_ref!' do
subject { pipeline.ensure_ci_ref! }
- shared_examples_for 'protected by feature flag' do
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ci_pipeline_fixed_notifications: false)
- end
-
- it 'does not do anything' do
- expect(Ci::Ref).not_to receive(:ensure_for)
-
- subject
- end
- end
- end
-
context 'when ci_ref does not exist yet' do
let!(:pipeline) { create(:ci_pipeline, ci_ref_presence: false) }
- it_behaves_like 'protected by feature flag'
-
it 'creates a new ci_ref and assigns it' do
expect { subject }.to change { Ci::Ref.count }.by(1)
@@ -2947,8 +2826,6 @@ RSpec.describe Ci::Pipeline, :mailer do
context 'when ci_ref already exists' do
let!(:pipeline) { create(:ci_pipeline) }
- it_behaves_like 'protected by feature flag'
-
it 'fetches a new ci_ref and assigns it' do
expect { subject }.not_to change { Ci::Ref.count }
@@ -3082,24 +2959,14 @@ RSpec.describe Ci::Pipeline, :mailer do
create(:ci_build, :success, :report_results, name: 'java', pipeline: pipeline, project: project)
end
- it 'returns test report summary with collected data', :aggregate_failures do
- expect(subject.total_time).to be(0.84)
- expect(subject.total_count).to be(4)
- expect(subject.success_count).to be(0)
- expect(subject.failed_count).to be(0)
- expect(subject.error_count).to be(4)
- expect(subject.skipped_count).to be(0)
+ it 'returns test report summary with collected data' do
+ expect(subject.total).to include(time: 0.84, count: 4, success: 0, failed: 0, skipped: 0, error: 4)
end
end
context 'when pipeline does not have any builds with report results' do
- it 'returns empty test report sumary', :aggregate_failures do
- expect(subject.total_time).to be(0)
- expect(subject.total_count).to be(0)
- expect(subject.success_count).to be(0)
- expect(subject.failed_count).to be(0)
- expect(subject.error_count).to be(0)
- expect(subject.skipped_count).to be(0)
+ it 'returns empty test report summary' do
+ expect(subject.total).to include(time: 0, count: 0, success: 0, failed: 0, skipped: 0, error: 0)
end
end
end
@@ -3141,40 +3008,6 @@ RSpec.describe Ci::Pipeline, :mailer do
end
end
- describe '#test_reports_count', :use_clean_rails_memory_store_caching do
- subject { pipeline.test_reports }
-
- context 'when pipeline has multiple builds with test reports' do
- let!(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline, project: project) }
- let!(:build_java) { create(:ci_build, :success, name: 'java', pipeline: pipeline, project: project) }
-
- before do
- create(:ci_job_artifact, :junit, job: build_rspec, project: project)
- create(:ci_job_artifact, :junit_with_ant, job: build_java, project: project)
- end
-
- it 'returns test report count equal to test reports total_count' do
- expect(subject.total_count).to eq(7)
- expect(subject.total_count).to eq(pipeline.test_reports_count)
- end
-
- it 'reads from cache when records are cached' do
- expect(Rails.cache.fetch(['project', project.id, 'pipeline', pipeline.id, 'test_reports_count'], force: false)).to be_nil
-
- pipeline.test_reports_count
-
- expect(ActiveRecord::QueryRecorder.new { pipeline.test_reports_count }.count).to eq(0)
- end
- end
-
- context 'when pipeline does not have any builds with test reports' do
- it 'returns empty test report count' do
- expect(subject.total_count).to eq(0)
- expect(subject.total_count).to eq(pipeline.test_reports_count)
- end
- end
- end
-
describe '#accessibility_reports' do
subject { pipeline.accessibility_reports }
@@ -3282,32 +3115,6 @@ RSpec.describe Ci::Pipeline, :mailer do
end
end
end
-
- context 'when transitioning to success' do
- context 'when feature is enabled' do
- before do
- stub_feature_flags(keep_latest_artifacts_for_ref: true)
- end
-
- it 'calls the PipelineSuccessUnlockArtifactsWorker' do
- expect(Ci::PipelineSuccessUnlockArtifactsWorker).to receive(:perform_async).with(pipeline.id)
-
- pipeline.succeed!
- end
- end
-
- context 'when feature is disabled' do
- before do
- stub_feature_flags(keep_latest_artifacts_for_ref: false)
- end
-
- it 'does not call the PipelineSuccessUnlockArtifactsWorker' do
- expect(Ci::PipelineSuccessUnlockArtifactsWorker).not_to receive(:perform_async)
-
- pipeline.succeed!
- end
- end
- end
end
describe '#default_branch?' do
diff --git a/spec/models/ci/ref_spec.rb b/spec/models/ci/ref_spec.rb
index fd4742a8ad2..8bce3c10d8c 100644
--- a/spec/models/ci/ref_spec.rb
+++ b/spec/models/ci/ref_spec.rb
@@ -3,8 +3,69 @@
require 'spec_helper'
RSpec.describe Ci::Ref do
+ using RSpec::Parameterized::TableSyntax
+
it { is_expected.to belong_to(:project) }
+ describe 'state machine transitions' do
+ context 'unlock artifacts transition' do
+ let(:ci_ref) { create(:ci_ref) }
+ let(:unlock_artifacts_worker_spy) { class_spy(::Ci::PipelineSuccessUnlockArtifactsWorker) }
+
+ before do
+ stub_const('Ci::PipelineSuccessUnlockArtifactsWorker', unlock_artifacts_worker_spy)
+ end
+
+ context 'when keep latest artifact feature is enabled' do
+ before do
+ stub_feature_flags(keep_latest_artifacts_for_ref: true)
+ end
+
+ where(:initial_state, :action, :count) do
+ :unknown | :succeed! | 1
+ :unknown | :do_fail! | 0
+ :success | :succeed! | 1
+ :success | :do_fail! | 0
+ :failed | :succeed! | 1
+ :failed | :do_fail! | 0
+ :fixed | :succeed! | 1
+ :fixed | :do_fail! | 0
+ :broken | :succeed! | 1
+ :broken | :do_fail! | 0
+ :still_failing | :succeed | 1
+ :still_failing | :do_fail | 0
+ end
+
+ with_them do
+ context "when transitioning states" do
+ before do
+ status_value = Ci::Ref.state_machines[:status].states[initial_state].value
+ ci_ref.update!(status: status_value)
+ end
+
+ it 'calls unlock artifacts service' do
+ ci_ref.send(action)
+
+ expect(unlock_artifacts_worker_spy).to have_received(:perform_async).exactly(count).times
+ end
+ end
+ end
+ end
+
+ context 'when keep latest artifact feature is not enabled' do
+ before do
+ stub_feature_flags(keep_latest_artifacts_for_ref: false)
+ end
+
+ it 'does not call unlock artifacts service' do
+ ci_ref.succeed!
+
+ expect(unlock_artifacts_worker_spy).not_to have_received(:perform_async)
+ end
+ end
+ end
+ end
+
describe '.ensure_for' do
let_it_be(:project) { create(:project, :repository) }
@@ -161,16 +222,6 @@ RSpec.describe Ci::Ref do
it_behaves_like 'no-op'
end
- context 'when feature flag is disabled' do
- let(:pipeline) { create(:ci_pipeline, :success, ci_ref: ci_ref) }
-
- before do
- stub_feature_flags(ci_pipeline_fixed_notifications: false)
- end
-
- it_behaves_like 'no-op'
- end
-
context 'when pipeline is not the latest pipeline' do
let!(:pipeline) { create(:ci_pipeline, :success, ci_ref: ci_ref) }
let!(:latest_pipeline) { create(:ci_pipeline, :success, ci_ref: ci_ref) }
diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb
new file mode 100644
index 00000000000..bb1fc021e66
--- /dev/null
+++ b/spec/models/clusters/agent_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agent do
+ subject { create(:cluster_agent) }
+
+ it { is_expected.to belong_to(:project).class_name('::Project') }
+ it { is_expected.to have_many(:agent_tokens).class_name('Clusters::AgentToken') }
+
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_length_of(:name).is_at_most(63) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
+
+ describe 'validation' do
+ describe 'name validation' do
+ it 'rejects names that do not conform to RFC 1123', :aggregate_failures do
+ %w[Agent agentA agentAagain gent- -agent agent.a agent/a agent>a].each do |name|
+ agent = build(:cluster_agent, name: name)
+
+ expect(agent).not_to be_valid
+ expect(agent.errors[:name]).to eq(["can contain only lowercase letters, digits, and '-', but cannot start or end with '-'"])
+ end
+ end
+
+ it 'accepts valid names', :aggregate_failures do
+ %w[agent agent123 agent-123].each do |name|
+ agent = build(:cluster_agent, name: name)
+
+ expect(agent).to be_valid
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/clusters/agent_token_spec.rb b/spec/models/clusters/agent_token_spec.rb
new file mode 100644
index 00000000000..ad9dd11b24e
--- /dev/null
+++ b/spec/models/clusters/agent_token_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::AgentToken do
+ it { is_expected.to belong_to(:agent).class_name('Clusters::Agent') }
+
+ describe '#token' do
+ it 'is generated on save' do
+ agent_token = build(:cluster_agent_token, token_encrypted: nil)
+ expect(agent_token.token).to be_nil
+
+ agent_token.save!
+
+ expect(agent_token.token).to be_present
+ end
+ end
+end
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index d1138f5fa2d..e029283326f 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe Clusters::Applications::Ingress do
it 'is initialized with ingress arguments' do
expect(subject.name).to eq('ingress')
expect(subject.chart).to eq('stable/nginx-ingress')
- expect(subject.version).to eq('1.29.7')
+ expect(subject.version).to eq('1.40.2')
expect(subject).to be_rbac
expect(subject.files).to eq(ingress.files)
end
@@ -153,7 +153,7 @@ RSpec.describe Clusters::Applications::Ingress do
let(:ingress) { create(:clusters_applications_ingress, :errored, version: 'nginx') }
it 'is initialized with the locked version' do
- expect(subject.version).to eq('1.29.7')
+ expect(subject.version).to eq('1.40.2')
end
end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 4807957152c..2d0b5af0e77 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -1153,6 +1153,57 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
+ describe '#connection_error' do
+ let(:cluster) { create(:cluster) }
+ let(:error) { :unknown_error }
+
+ subject { cluster.connection_error }
+
+ it { is_expected.to be_nil }
+
+ context 'with a cached status' do
+ before do
+ stub_reactive_cache(cluster, connection_error: error)
+ end
+
+ it { is_expected.to eq(error) }
+ end
+ end
+
+ describe '#node_connection_error' do
+ let(:cluster) { create(:cluster) }
+ let(:error) { :unknown_error }
+
+ subject { cluster.node_connection_error }
+
+ it { is_expected.to be_nil }
+
+ context 'with a cached status' do
+ before do
+ stub_reactive_cache(cluster, node_connection_error: error)
+ end
+
+ it { is_expected.to eq(error) }
+ end
+ end
+
+ describe '#metrics_connection_error' do
+ let(:cluster) { create(:cluster) }
+ let(:error) { :unknown_error }
+
+ subject { cluster.metrics_connection_error }
+
+ it { is_expected.to be_nil }
+
+ context 'with a cached status' do
+ before do
+ stub_reactive_cache(cluster, metrics_connection_error: error)
+ end
+
+ it { is_expected.to eq(error) }
+ end
+ end
+
describe '#nodes' do
let(:cluster) { create(:cluster) }
@@ -1186,43 +1237,49 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
context 'cluster is enabled' do
let(:cluster) { create(:cluster, :provided_by_user, :group) }
let(:gl_k8s_node_double) { double(Gitlab::Kubernetes::Node) }
- let(:expected_nodes) { nil }
+ let(:expected_nodes) { {} }
before do
stub_kubeclient_discover(cluster.platform.api_url)
allow(Gitlab::Kubernetes::Node).to receive(:new).with(cluster).and_return(gl_k8s_node_double)
- allow(gl_k8s_node_double).to receive(:all).and_return([])
+ allow(gl_k8s_node_double).to receive(:all).and_return(expected_nodes)
end
context 'connection to the cluster is successful' do
+ let(:expected_nodes) { { nodes: [kube_node.merge(kube_node_metrics)] } }
+ let(:connection_status) { { connection_status: :connected } }
+
before do
allow(gl_k8s_node_double).to receive(:all).and_return(expected_nodes)
end
- let(:expected_nodes) { [kube_node.merge(kube_node_metrics)] }
-
- it { is_expected.to eq(connection_status: :connected, nodes: expected_nodes) }
+ it { is_expected.to eq(**connection_status, **expected_nodes) }
end
context 'cluster cannot be reached' do
+ let(:connection_status) { { connection_status: :unreachable, connection_error: :connection_error } }
+
before do
allow(cluster.kubeclient.core_client).to receive(:discover)
.and_raise(SocketError)
end
- it { is_expected.to eq(connection_status: :unreachable, nodes: expected_nodes) }
+ it { is_expected.to eq(**connection_status, **expected_nodes) }
end
context 'cluster cannot be authenticated to' do
+ let(:connection_status) { { connection_status: :authentication_failure, connection_error: :authentication_error } }
+
before do
allow(cluster.kubeclient.core_client).to receive(:discover)
.and_raise(OpenSSL::X509::CertificateError.new("Certificate error"))
end
- it { is_expected.to eq(connection_status: :authentication_failure, nodes: expected_nodes) }
+ it { is_expected.to eq(**connection_status, **expected_nodes) }
end
describe 'Kubeclient::HttpError' do
+ let(:connection_status) { { connection_status: :authentication_failure, connection_error: :http_error } }
let(:error_code) { 403 }
let(:error_message) { "Forbidden" }
@@ -1231,28 +1288,32 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
.and_raise(Kubeclient::HttpError.new(error_code, error_message, nil))
end
- it { is_expected.to eq(connection_status: :authentication_failure, nodes: expected_nodes) }
+ it { is_expected.to eq(**connection_status, **expected_nodes) }
context 'generic timeout' do
+ let(:connection_status) { { connection_status: :unreachable, connection_error: :http_error } }
let(:error_message) { 'Timed out connecting to server'}
- it { is_expected.to eq(connection_status: :unreachable, nodes: expected_nodes) }
+ it { is_expected.to eq(**connection_status, **expected_nodes) }
end
context 'gateway timeout' do
+ let(:connection_status) { { connection_status: :unreachable, connection_error: :http_error } }
let(:error_message) { '504 Gateway Timeout for GET https://kubernetes.example.com/api/v1'}
- it { is_expected.to eq(connection_status: :unreachable, nodes: expected_nodes) }
+ it { is_expected.to eq(**connection_status, **expected_nodes) }
end
end
context 'an uncategorised error is raised' do
+ let(:connection_status) { { connection_status: :unknown_failure, connection_error: :unknown_error } }
+
before do
allow(cluster.kubeclient.core_client).to receive(:discover)
.and_raise(StandardError)
end
- it { is_expected.to eq(connection_status: :unknown_failure, nodes: expected_nodes) }
+ it { is_expected.to eq(**connection_status, **expected_nodes) }
it 'notifies Sentry' do
expect(Gitlab::ErrorTracking).to receive(:track_exception)
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index adccc72d13d..c6a2b67a008 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -410,6 +410,7 @@ RSpec.describe Clusters::Platforms::Kubernetes do
let(:expected_pod_cached_data) do
kube_pod.tap { |kp| kp['metadata'].delete('namespace') }
end
+
let(:namespace) { "project-namespace" }
let(:environment) { instance_double(Environment, deployment_namespace: namespace) }
diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb
index f4e86f3292b..de9b72c1da2 100644
--- a/spec/models/commit_collection_spec.rb
+++ b/spec/models/commit_collection_spec.rb
@@ -52,27 +52,38 @@ RSpec.describe CommitCollection do
end
describe '#with_latest_pipeline' do
+ let(:another_commit) { project.commit("60ecb67744cb56576c30214ff52294f8ce2def98") }
+
let!(:pipeline) do
- create(
- :ci_empty_pipeline,
- ref: 'master',
- sha: commit.id,
- status: 'success',
- project: project
- )
+ create(:ci_empty_pipeline, ref: 'master', sha: commit.id, status: 'success', project: project)
+ end
+
+ let!(:another_pipeline) do
+ create(:ci_empty_pipeline, ref: 'master', sha: another_commit.id, status: 'success', project: project)
end
- let(:collection) { described_class.new(project, [commit]) }
+
+ let(:collection) { described_class.new(project, [commit, another_commit]) }
it 'sets the latest pipeline for every commit so no additional queries are necessary' do
commits = collection.with_latest_pipeline('master')
recorder = ActiveRecord::QueryRecorder.new do
expect(commits.map { |c| c.latest_pipeline('master') })
- .to eq([pipeline])
+ .to eq([pipeline, another_pipeline])
end
expect(recorder.count).to be_zero
end
+
+ it 'performs a single query to fetch pipeline warnings' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ collection.with_latest_pipeline('master').each do |c|
+ c.latest_pipeline('master').number_of_warnings.itself
+ end
+ end
+
+ expect(recorder.count).to eq(2) # 1 for pipelines, 1 for warnings counts
+ end
end
describe '#with_markdown_cache' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index cd0110a787b..7f893d6a100 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -66,51 +66,35 @@ RSpec.describe CommitStatus do
describe '#processed' do
subject { commit_status.processed }
- context 'when ci_atomic_processing is disabled' do
+ context 'status is latest' do
before do
- stub_feature_flags(ci_atomic_processing: false)
-
- commit_status.save!
+ commit_status.update!(retried: false, status: :pending)
end
- it { is_expected.to be_nil }
+ it { is_expected.to be_falsey }
end
- context 'when ci_atomic_processing is enabled' do
+ context 'status is retried' do
before do
- stub_feature_flags(ci_atomic_processing: true)
- end
-
- context 'status is latest' do
- before do
- commit_status.update!(retried: false, status: :pending)
- end
-
- it { is_expected.to be_falsey }
+ commit_status.update!(retried: true, status: :pending)
end
- context 'status is retried' do
- before do
- commit_status.update!(retried: true, status: :pending)
- end
-
- it { is_expected.to be_truthy }
- end
+ it { is_expected.to be_truthy }
+ end
- it "processed state is always persisted" do
- commit_status.update!(retried: false, status: :pending)
+ it "processed state is always persisted" do
+ commit_status.update!(retried: false, status: :pending)
- # another process does mark object as processed
- CommitStatus.find(commit_status.id).update_column(:processed, true)
+ # another process does mark object as processed
+ CommitStatus.find(commit_status.id).update_column(:processed, true)
- # subsequent status transitions on the same instance
- # always saves processed=false to DB even though
- # the current value did not change
- commit_status.update!(retried: false, status: :running)
+ # subsequent status transitions on the same instance
+ # always saves processed=false to DB even though
+ # the current value did not change
+ commit_status.update!(retried: false, status: :running)
- # we look at a persisted state in DB
- expect(CommitStatus.find(commit_status.id).processed).to eq(false)
- end
+ # we look at a persisted state in DB
+ expect(CommitStatus.find(commit_status.id).processed).to eq(false)
end
end
@@ -438,7 +422,7 @@ RSpec.describe CommitStatus do
end
it 'returns a correct compound status' do
- expect(described_class.all.slow_composite_status(project: project)).to eq 'running'
+ expect(described_class.all.composite_status).to eq 'running'
end
end
@@ -448,7 +432,7 @@ RSpec.describe CommitStatus do
end
it 'returns status that indicates success' do
- expect(described_class.all.slow_composite_status(project: project)).to eq 'success'
+ expect(described_class.all.composite_status).to eq 'success'
end
end
@@ -459,7 +443,7 @@ RSpec.describe CommitStatus do
end
it 'returns status according to the scope' do
- expect(described_class.latest.slow_composite_status(project: project)).to eq 'success'
+ expect(described_class.latest.composite_status).to eq 'success'
end
end
end
diff --git a/spec/models/commit_with_pipeline_spec.rb b/spec/models/commit_with_pipeline_spec.rb
index ff451527929..c4b6deebae0 100644
--- a/spec/models/commit_with_pipeline_spec.rb
+++ b/spec/models/commit_with_pipeline_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe CommitWithPipeline do
sha: commit.sha,
status: 'success')
end
+
let!(:second_pipeline) do
create(:ci_empty_pipeline,
project: project,
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 5f8c65e429e..440943171c3 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe CacheMarkdownField, :clean_gitlab_redis_cache do
@title, @description, @cached_markdown_version = args[:title], args[:description], args[:cached_markdown_version]
@title_html, @description_html = args[:title_html], args[:description_html]
@author, @project = args[:author], args[:project]
+ @parent_user = args[:parent_user]
end
attr_accessor :title, :description, :cached_markdown_version
@@ -41,8 +42,8 @@ RSpec.describe CacheMarkdownField, :clean_gitlab_redis_cache do
let(:cache_version) { Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 }
- def thing_subclass(klass, extra_attribute)
- Class.new(klass) { attr_accessor(extra_attribute) }
+ def thing_subclass(klass, *extra_attributes)
+ Class.new(klass) { attr_accessor(*extra_attributes) }
end
shared_examples 'a class with cached markdown fields' do
@@ -192,11 +193,33 @@ RSpec.describe CacheMarkdownField, :clean_gitlab_redis_cache do
end
context 'with an author' do
- let(:thing) { thing_subclass(klass, :author).new(title: markdown, title_html: html, author: :author_value) }
+ let(:user) { build(:user) }
+ let(:thing) { thing_subclass(klass, :author).new(title: markdown, title_html: html, author: user) }
it 'sets the author in the context' do
is_expected.to have_key(:author)
- expect(context[:author]).to eq(:author_value)
+ expect(context[:author]).to eq(user)
+ end
+ end
+
+ context 'with a parent_user' do
+ let(:user) { build(:user) }
+ let(:thing) { thing_subclass(klass, :author, :parent_user).new(title: markdown, title_html: html, parent_user: user, author: user) }
+
+ it 'sets the user in the context' do
+ is_expected.to have_key(:user)
+ expect(context[:user]).to eq(user)
+ end
+
+ context 'when the personal_snippet_reference_filters flag is disabled' do
+ before do
+ stub_feature_flags(personal_snippet_reference_filters: false)
+ end
+
+ it 'does not set the user in the context' do
+ is_expected.not_to have_key(:user)
+ expect(context[:user]).to be_nil
+ end
end
end
end
diff --git a/spec/models/concerns/ci/artifactable_spec.rb b/spec/models/concerns/ci/artifactable_spec.rb
new file mode 100644
index 00000000000..13c2ff5efe5
--- /dev/null
+++ b/spec/models/concerns/ci/artifactable_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Artifactable do
+ let(:ci_job_artifact) { build(:ci_job_artifact) }
+
+ describe 'artifact properties are included' do
+ context 'when enum is defined' do
+ subject { ci_job_artifact }
+
+ it { is_expected.to define_enum_for(:file_format).with_values(raw: 1, zip: 2, gzip: 3).with_suffix }
+ end
+
+ context 'when const is defined' do
+ subject { ci_job_artifact.class }
+
+ it { is_expected.to be_const_defined(:FILE_FORMAT_ADAPTERS) }
+ end
+ end
+end
diff --git a/spec/models/concerns/ci/has_status_spec.rb b/spec/models/concerns/ci/has_status_spec.rb
index fe46b63781d..b16420bc658 100644
--- a/spec/models/concerns/ci/has_status_spec.rb
+++ b/spec/models/concerns/ci/has_status_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
RSpec.describe Ci::HasStatus do
- describe '.slow_composite_status' do
+ describe '.composite_status' do
using RSpec::Parameterized::TableSyntax
- subject { CommitStatus.slow_composite_status(project: nil) }
+ subject { CommitStatus.composite_status }
shared_examples 'build status summary' do
context 'all successful' do
@@ -184,26 +184,16 @@ RSpec.describe Ci::HasStatus do
end
end
- where(:ci_composite_status) do
- [false, true]
- end
-
- with_them do
- before do
- stub_feature_flags(ci_composite_status: ci_composite_status)
- end
+ context 'ci build statuses' do
+ let(:type) { :ci_build }
- context 'ci build statuses' do
- let(:type) { :ci_build }
-
- it_behaves_like 'build status summary'
- end
+ it_behaves_like 'build status summary'
+ end
- context 'generic commit statuses' do
- let(:type) { :generic_commit_status }
+ context 'generic commit statuses' do
+ let(:type) { :generic_commit_status }
- it_behaves_like 'build status summary'
- end
+ it_behaves_like 'build status summary'
end
end
@@ -400,12 +390,4 @@ RSpec.describe Ci::HasStatus do
end
end
end
-
- describe '.legacy_status_sql' do
- subject { Ci::Build.legacy_status_sql }
-
- it 'returns SQL' do
- puts subject
- end
- end
end
diff --git a/spec/models/concerns/counter_attribute_spec.rb b/spec/models/concerns/counter_attribute_spec.rb
new file mode 100644
index 00000000000..f23865a5dbb
--- /dev/null
+++ b/spec/models/concerns/counter_attribute_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CounterAttribute, :counter_attribute, :clean_gitlab_redis_shared_state do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:project_statistics) { create(:project_statistics) }
+ let(:model) { CounterAttributeModel.find(project_statistics.id) }
+
+ it_behaves_like CounterAttribute, [:build_artifacts_size, :commit_count] do
+ let(:model) { CounterAttributeModel.find(project_statistics.id) }
+ end
+
+ describe '.steal_increments' do
+ let(:increment_key) { 'counters:Model:123:attribute' }
+ let(:flushed_key) { 'counter:Model:123:attribute:flushed' }
+
+ subject { model.send(:steal_increments, increment_key, flushed_key) }
+
+ where(:increment, :flushed, :result, :flushed_key_present) do
+ nil | nil | 0 | false
+ nil | 0 | 0 | false
+ 0 | 0 | 0 | false
+ 1 | 0 | 1 | true
+ 1 | nil | 1 | true
+ 1 | 1 | 2 | true
+ 1 | -2 | -1 | true
+ -1 | 1 | 0 | false
+ end
+
+ with_them do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(increment_key, increment) if increment
+ redis.set(flushed_key, flushed) if flushed
+ end
+ end
+
+ it { is_expected.to eq(result) }
+
+ it 'drops the increment key and creates the flushed key if it does not exist' do
+ subject
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.exists(increment_key)).to be_falsey
+ expect(redis.exists(flushed_key)).to eq(flushed_key_present)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/featurable_spec.rb b/spec/models/concerns/featurable_spec.rb
index cc01820cc97..31186b5fc77 100644
--- a/spec/models/concerns/featurable_spec.rb
+++ b/spec/models/concerns/featurable_spec.rb
@@ -42,6 +42,7 @@ RSpec.describe Featurable do
end
end
end
+
let!(:instance) { klass.new }
it { expect(klass.available_features).to eq [:feature1, :feature2] }
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 96d3e2b7b1b..0824b5c7834 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Issuable do
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
it { is_expected.to have_many(:labels) }
+ it { is_expected.to have_many(:note_authors).through(:notes) }
context 'Notes' do
let!(:note) { create(:note, noteable: issue, project: issue.project) }
@@ -149,6 +150,7 @@ RSpec.describe Issuable do
let!(:searchable_issue) do
create(:issue, title: "Searchable awesome issue", description: 'Many cute kittens')
end
+
let!(:searchable_issue2) { create(:issue, title: "Aw", description: "Cu") }
it 'returns issues with a matching title' do
diff --git a/spec/models/concerns/manual_inverse_association_spec.rb b/spec/models/concerns/manual_inverse_association_spec.rb
index 1349d2cc680..0d56d06c624 100644
--- a/spec/models/concerns/manual_inverse_association_spec.rb
+++ b/spec/models/concerns/manual_inverse_association_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe ManualInverseAssociation do
stub_const("#{described_class}::Model", model)
end
- let(:instance) { create(:merge_request).becomes(model) }
+ let(:instance) { create(:merge_request).becomes(model) } # rubocop: disable Cop/AvoidBecomes
describe '.manual_inverse_association' do
context 'when the relation exists' do
diff --git a/spec/models/concerns/milestoneable_spec.rb b/spec/models/concerns/milestoneable_spec.rb
index 15352a1453c..3dd6f1450c7 100644
--- a/spec/models/concerns/milestoneable_spec.rb
+++ b/spec/models/concerns/milestoneable_spec.rb
@@ -103,7 +103,7 @@ RSpec.describe Milestoneable do
end
describe 'release scopes' do
- let_it_be(:project) { create(:project) }
+ let_it_be(:project) { create(:project, :repository) }
let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
@@ -126,6 +126,22 @@ RSpec.describe Milestoneable do
let_it_be(:items) { Issue.all }
+ describe '#any_milestone' do
+ context 'when milestone filter is present and related closing issues are joined' do
+ let_it_be(:merge_request_1) { create(:merge_request, source_project: project, source_branch: 'feature-1') }
+ let_it_be(:merge_request_2) { create(:merge_request, source_project: project, source_branch: 'feature-2') }
+
+ let_it_be(:mrc_issue_1) { create(:merge_requests_closing_issues, issue: issue_1, merge_request: merge_request_1) }
+ let_it_be(:mrc_issue_2) { create(:merge_requests_closing_issues, issue: issue_2, merge_request: merge_request_2) }
+
+ it 'returns merge request closing issues of any milestone' do
+ relation = items.joins(merge_requests_closing_issues: :issue).any_milestone
+
+ expect(relation).to contain_exactly(issue_1, issue_2)
+ end
+ end
+ end
+
describe '#without_release' do
it 'returns the issues not tied to any milestone and the ones tied to milestone with no release' do
expect(items.without_release).to contain_exactly(issue_5, issue_6)
diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb
index 50748efcda4..3846dd9c231 100644
--- a/spec/models/concerns/sha_attribute_spec.rb
+++ b/spec/models/concerns/sha_attribute_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe ShaAttribute do
end
describe '#sha_attribute' do
- context 'when in non-production' do
+ context 'when in development' do
before do
stub_rails_env('development')
end
@@ -38,24 +38,22 @@ RSpec.describe ShaAttribute do
end
context 'when the table does not exist' do
- it 'allows the attribute to be added and issues a warning' do
+ it 'allows the attribute to be added' do
allow(model).to receive(:table_exists?).and_return(false)
expect(model).not_to receive(:columns)
expect(model).to receive(:attribute)
- expect(model).to receive(:warn)
model.sha_attribute(:name)
end
end
context 'when the column does not exist' do
- it 'allows the attribute to be added and issues a warning' do
+ it 'allows the attribute to be added' do
allow(model).to receive(:table_exists?).and_return(true)
expect(model).to receive(:columns)
expect(model).to receive(:attribute)
- expect(model).to receive(:warn)
model.sha_attribute(:no_name)
end
diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb
index 2b569b6097d..836c4139107 100644
--- a/spec/models/custom_emoji_spec.rb
+++ b/spec/models/custom_emoji_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe CustomEmoji do
it { is_expected.to have_db_column(:file) }
it { is_expected.to validate_length_of(:name).is_at_most(36) }
it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to have_db_column(:external) }
end
describe 'exclusion of duplicated emoji' do
diff --git a/spec/models/design_management/design_at_version_spec.rb b/spec/models/design_management/design_at_version_spec.rb
index 2c640ee5c2c..3c1ff45c53f 100644
--- a/spec/models/design_management/design_at_version_spec.rb
+++ b/spec/models/design_management/design_at_version_spec.rb
@@ -78,18 +78,23 @@ RSpec.describe DesignManagement::DesignAtVersion do
let!(:version_a) do
create(:design_version, designs: [design_a])
end
+
let!(:version_b) do
create(:design_version, designs: [design_b])
end
+
let!(:version_mod) do
create(:design_version, modified_designs: [design_a, design_b])
end
+
let!(:version_c) do
create(:design_version, deleted_designs: [design_a])
end
+
let!(:version_d) do
create(:design_version, deleted_designs: [design_b])
end
+
let!(:version_e) do
create(:design_version, designs: [design_a])
end
@@ -296,9 +301,11 @@ RSpec.describe DesignManagement::DesignAtVersion do
let!(:version_a) do
create(:design_version, designs: create_list(:design, 3, issue: issue))
end
+
let!(:version_b) do
create(:design_version, designs: create_list(:design, 1, issue: issue))
end
+
let!(:version_c) do
create(:design_version, designs: create_list(:design, 1, issue: issue_b))
end
@@ -346,10 +353,12 @@ RSpec.describe DesignManagement::DesignAtVersion do
let!(:version_a) do
create(:design_version, designs: create_list(:design, 3, issue: issue))
end
+
let!(:version_b) do
create(:design_version, designs: create_list(:design, 2, issue: issue))
end
# 1 version, with 3 designs on issue B, so 1*3 = 3
+
let!(:version_c) do
create(:design_version, designs: create_list(:design, 3, issue: issue_b))
end
diff --git a/spec/models/design_management/design_collection_spec.rb b/spec/models/design_management/design_collection_spec.rb
index c5e290da759..de766d5ce09 100644
--- a/spec/models/design_management/design_collection_spec.rb
+++ b/spec/models/design_management/design_collection_spec.rb
@@ -34,6 +34,15 @@ RSpec.describe DesignManagement::DesignCollection do
collection.find_or_create_design!(filename: 'world.jpg')
end.not_to exceed_query_limit(1)
end
+
+ it 'inserts the design after any existing designs' do
+ design1 = collection.find_or_create_design!(filename: 'design1.jpg')
+ design1.update!(relative_position: 100)
+
+ design2 = collection.find_or_create_design!(filename: 'design2.jpg')
+
+ expect(collection.designs.ordered(issue.project)).to eq([design1, design2])
+ end
end
describe "#versions" do
diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb
index 345147390c0..2c129f883b9 100644
--- a/spec/models/design_management/design_spec.rb
+++ b/spec/models/design_management/design_spec.rb
@@ -11,6 +11,11 @@ RSpec.describe DesignManagement::Design do
let_it_be(:design3) { create(:design, :with_versions, issue: issue, versions_count: 1) }
let_it_be(:deleted_design) { create(:design, :with_versions, deleted: true) }
+ it_behaves_like 'a class that supports relative positioning' do
+ let(:factory) { :design }
+ let(:default_params) { { issue: issue } }
+ end
+
describe 'relations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:issue) }
@@ -21,7 +26,7 @@ RSpec.describe DesignManagement::Design do
end
describe 'validations' do
- subject(:design) { build(:design) }
+ subject(:design) { build(:design, issue: issue) }
it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:project) }
@@ -147,6 +152,45 @@ RSpec.describe DesignManagement::Design do
end
end
+ describe '.ordered' do
+ before_all do
+ design1.update!(relative_position: 2)
+ design2.update!(relative_position: 1)
+ design3.update!(relative_position: nil)
+ deleted_design.update!(relative_position: nil)
+ end
+
+ it 'sorts by relative position and ID in ascending order' do
+ expect(described_class.ordered(issue.project)).to eq([design2, design1, design3, deleted_design])
+ end
+
+ context 'when the :reorder_designs feature is enabled for the project' do
+ before do
+ stub_feature_flags(reorder_designs: issue.project)
+ end
+
+ it 'sorts by relative position and ID in ascending order' do
+ expect(described_class.ordered(issue.project)).to eq([design2, design1, design3, deleted_design])
+ end
+ end
+
+ context 'when the :reorder_designs feature is disabled' do
+ before do
+ stub_feature_flags(reorder_designs: false)
+ end
+
+ it 'sorts by ID in ascending order' do
+ expect(described_class.ordered(issue.project)).to eq([design1, design2, design3, deleted_design])
+ end
+ end
+ end
+
+ describe '.in_creation_order' do
+ it 'sorts by ID in ascending order' do
+ expect(described_class.in_creation_order).to eq([design1, design2, design3, deleted_design])
+ end
+ end
+
describe '.with_filename' do
it 'returns correct design when passed a single filename' do
expect(described_class.with_filename(design1.filename)).to eq([design1])
@@ -181,7 +225,7 @@ RSpec.describe DesignManagement::Design do
end
describe '#visible_in?' do
- let_it_be(:issue) { create(:issue) }
+ let_it_be(:issue) { create(:issue, project: issue.project) }
# It is expensive to re-create complex histories, so we do it once, and then
# assert that we can establish visibility at any given version.
@@ -237,7 +281,7 @@ RSpec.describe DesignManagement::Design do
describe '#status' do
context 'the design is new' do
- subject { build(:design) }
+ subject { build(:design, issue: issue) }
it { is_expected.to have_attributes(status: :new) }
end
@@ -257,7 +301,7 @@ RSpec.describe DesignManagement::Design do
describe '#deleted?' do
context 'the design is new' do
- let(:design) { build(:design) }
+ let(:design) { build(:design, issue: issue) }
it 'is falsy' do
expect(design).not_to be_deleted
@@ -281,7 +325,7 @@ RSpec.describe DesignManagement::Design do
end
context 'the design has been deleted, but was then re-created' do
- let(:design) { create(:design, :with_versions, versions_count: 1, deleted: true) }
+ let(:design) { create(:design, :with_versions, issue: issue, versions_count: 1, deleted: true) }
it 'is falsy' do
restore_designs(design)
@@ -299,7 +343,7 @@ RSpec.describe DesignManagement::Design do
end
it "is true when there are no versions" do
- expect(build(:design)).to be_new_design
+ expect(build(:design, issue: issue)).to be_new_design
end
it 'is false for deleted designs' do
@@ -336,7 +380,7 @@ RSpec.describe DesignManagement::Design do
describe "#full_path" do
it "builds the full path for a design" do
- design = build(:design, filename: "hello.jpg")
+ design = build(:design, issue: issue, filename: "hello.jpg")
expected_path = "#{DesignManagement.designs_directory}/issue-#{design.issue.iid}/hello.jpg"
expect(design.full_path).to eq(expected_path)
@@ -359,15 +403,13 @@ RSpec.describe DesignManagement::Design do
let(:versions_count) { 1 }
it 'builds diff refs based on the empty tree if there was only one version' do
- design = create(:design, :with_file, versions_count: 1)
-
expect(design.diff_refs.base_sha).to eq(Gitlab::Git::BLANK_SHA)
expect(design.diff_refs.head_sha).to eq(design.diff_refs.head_sha)
end
end
it 'has no diff ref if new' do
- design = build(:design)
+ design = build(:design, issue: issue)
expect(design.diff_refs).to be_nil
end
@@ -375,7 +417,7 @@ RSpec.describe DesignManagement::Design do
describe '#repository' do
it 'is a design repository' do
- design = build(:design)
+ design = build(:design, issue: issue)
expect(design.repository).to be_a(DesignManagement::Repository)
end
@@ -383,7 +425,7 @@ RSpec.describe DesignManagement::Design do
describe '#note_etag_key' do
it 'returns a correct etag key' do
- design = create(:design)
+ design = design1
expect(design.note_etag_key).to eq(
::Gitlab::Routing.url_helpers.designs_project_issue_path(design.project, design.issue, { vueroute: design.filename })
@@ -392,47 +434,26 @@ RSpec.describe DesignManagement::Design do
end
describe '#user_notes_count', :use_clean_rails_memory_store_caching do
- let_it_be(:design) { create(:design, :with_file) }
-
- subject { design.user_notes_count }
-
# Note: Cache invalidation tests are in `design_user_notes_count_service_spec.rb`
-
it 'returns a count of user-generated notes' do
- create(:diff_note_on_design, noteable: design)
-
- is_expected.to eq(1)
- end
-
- it 'does not count notes on other designs' do
- second_design = create(:design, :with_file)
- create(:diff_note_on_design, noteable: second_design)
+ common_attrs = { issue: issue, project: issue.project, author: issue.project.creator }
+ design, second_design = create_list(:design, 2, :with_file, issue: issue)
+ create(:diff_note_on_design, **common_attrs, noteable: design)
+ create(:diff_note_on_design, **common_attrs, system: true, noteable: design)
+ create(:diff_note_on_design, **common_attrs, noteable: second_design)
- is_expected.to eq(0)
- end
-
- it 'does not count system notes' do
- create(:diff_note_on_design, system: true, noteable: design)
-
- is_expected.to eq(0)
+ expect(design.user_notes_count).to eq(1)
end
end
describe '#after_note_changed' do
- subject { build(:design) }
+ it 'calls #delete_cache on DesignUserNotesCountService for non-system notes' do
+ design = design1
- it 'calls #delete_cache on DesignUserNotesCountService' do
- expect_next_instance_of(DesignManagement::DesignUserNotesCountService) do |service|
- expect(service).to receive(:delete_cache)
- end
+ expect(design.send(:user_notes_count_service)).to receive(:delete_cache).once
- subject.after_note_changed(build(:note))
- end
-
- it 'does not call #delete_cache on DesignUserNotesCountService when passed a system note' do
- expect(DesignManagement::DesignUserNotesCountService).not_to receive(:new)
-
- subject.after_note_changed(build(:note, :system))
+ design.after_note_changed(build(:note, project: issue.project))
+ design.after_note_changed(build(:note, :system, project: issue.project))
end
end
@@ -516,14 +537,14 @@ RSpec.describe DesignManagement::Design do
with_them do
let(:filename) { "my-file.#{ext}" }
- let(:design) { build(:design, filename: filename) }
+ let(:design) { build(:design, issue: issue, filename: filename) }
let(:url) { url_for_design(design) }
let(:captures) { described_class.link_reference_pattern.match(url)&.named_captures }
it 'matches the URL' do
expect(captures).to include(
'url_filename' => filename,
- 'issue' => design.issue.iid.to_s,
+ 'issue' => issue.iid.to_s,
'namespace' => design.project.namespace.to_param,
'project' => design.project.name
)
@@ -565,4 +586,25 @@ RSpec.describe DesignManagement::Design do
end
end
end
+
+ describe '#immediately_before' do
+ let_it_be(:design) { create(:design, issue: issue, relative_position: 100) }
+ let_it_be(:next_design) { create(:design, issue: issue, relative_position: 200) }
+
+ it 'is true when there is no element positioned between this item and the next' do
+ expect(design.immediately_before?(next_design)).to be true
+ end
+
+ it 'is false when there is an element positioned between this item and the next' do
+ create(:design, issue: issue, relative_position: 150)
+
+ expect(design.immediately_before?(next_design)).to be false
+ end
+
+ it 'is false when the next design is to the left of this design' do
+ further_left = create(:design, issue: issue, relative_position: 50)
+
+ expect(design.immediately_before?(further_left)).to be false
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index c449a3c3c47..2696d144db4 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
it { is_expected.to have_many(:deployments) }
it { is_expected.to have_many(:metrics_dashboard_annotations) }
it { is_expected.to have_many(:alert_management_alerts) }
+ it { is_expected.to have_one(:latest_opened_most_severe_alert) }
it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) }
@@ -1347,4 +1348,27 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
expect(project.environments.count_by_state).to eq({ stopped: 0, available: 0 })
end
end
+
+ describe '#has_opened_alert?' do
+ subject { environment.has_opened_alert? }
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:environment, reload: true) { create(:environment, project: project) }
+
+ context 'when environment has an triggered alert' do
+ let!(:alert) { create(:alert_management_alert, :triggered, project: project, environment: environment) }
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when environment has an resolved alert' do
+ let!(:alert) { create(:alert_management_alert, :resolved, project: project, environment: environment) }
+
+ it { is_expected.to be(false) }
+ end
+
+ context 'when environment does not have an alert' do
+ it { is_expected.to be(false) }
+ end
+ end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 96baeab6809..015a86cb28b 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -111,6 +111,45 @@ RSpec.describe Event do
expect(found).not_to include(false_positive)
end
end
+
+ describe '.for_fingerprint' do
+ let_it_be(:with_fingerprint) { create(:event, fingerprint: 'aaa') }
+
+ before_all do
+ create(:event)
+ create(:event, fingerprint: 'bbb')
+ end
+
+ it 'returns none if there is no fingerprint' do
+ expect(described_class.for_fingerprint(nil)).to be_empty
+ expect(described_class.for_fingerprint('')).to be_empty
+ end
+
+ it 'returns none if there is no match' do
+ expect(described_class.for_fingerprint('not-found')).to be_empty
+ end
+
+ it 'can find a given event' do
+ expect(described_class.for_fingerprint(with_fingerprint.fingerprint))
+ .to contain_exactly(with_fingerprint)
+ end
+ end
+ end
+
+ describe '#fingerprint' do
+ it 'is unique scoped to target' do
+ issue = create(:issue)
+ mr = create(:merge_request)
+
+ expect { create_list(:event, 2, target: issue, fingerprint: '1234') }
+ .to raise_error(include('fingerprint'))
+
+ expect do
+ create(:event, target: mr, fingerprint: 'abcd')
+ create(:event, target: issue, fingerprint: 'abcd')
+ create(:event, target: issue, fingerprint: 'efgh')
+ end.not_to raise_error
+ end
end
describe "Push event" do
diff --git a/spec/models/experiment_spec.rb b/spec/models/experiment_spec.rb
new file mode 100644
index 00000000000..64cd2da4621
--- /dev/null
+++ b/spec/models/experiment_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Experiment do
+ subject { build(:experiment) }
+
+ describe 'associations' do
+ it { is_expected.to have_many(:experiment_users) }
+ it { is_expected.to have_many(:users) }
+ it { is_expected.to have_many(:control_group_users) }
+ it { is_expected.to have_many(:experimental_group_users) }
+
+ describe 'control_group_users and experimental_group_users' do
+ let(:experiment) { create(:experiment) }
+ let(:control_group_user) { build(:user) }
+ let(:experimental_group_user) { build(:user) }
+
+ before do
+ experiment.control_group_users << control_group_user
+ experiment.experimental_group_users << experimental_group_user
+ end
+
+ describe 'control_group_users' do
+ subject { experiment.control_group_users }
+
+ it { is_expected.to contain_exactly(control_group_user) }
+ end
+
+ describe 'experimental_group_users' do
+ subject { experiment.experimental_group_users }
+
+ it { is_expected.to contain_exactly(experimental_group_user) }
+ end
+ end
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name) }
+ it { is_expected.to validate_length_of(:name).is_at_most(255) }
+ end
+
+ describe '.add_user' do
+ let(:name) { :experiment_key }
+ let(:user) { build(:user) }
+
+ let!(:experiment) { create(:experiment, name: name) }
+
+ subject { described_class.add_user(name, :control, user) }
+
+ describe 'creating a new experiment record' do
+ context 'an experiment with the provided name already exists' do
+ it 'does not create a new experiment record' do
+ expect { subject }.not_to change(Experiment, :count)
+ end
+ end
+
+ context 'an experiment with the provided name does not exist yet' do
+ let(:experiment) { nil }
+
+ it 'creates a new experiment record' do
+ expect { subject }.to change(Experiment, :count).by(1)
+ end
+ end
+ end
+
+ describe 'creating a new experiment_user record' do
+ context 'an experiment_user record for this experiment already exists' do
+ before do
+ subject
+ end
+
+ it 'does not create a new experiment_user record' do
+ expect { subject }.not_to change(ExperimentUser, :count)
+ end
+ end
+
+ context 'an experiment_user record for this experiment does not exist yet' do
+ it 'creates a new experiment_user record' do
+ expect { subject }.to change(ExperimentUser, :count).by(1)
+ end
+
+ it 'assigns the correct group_type to the experiment_user' do
+ expect { subject }.to change { experiment.control_group_users.count }.by(1)
+ end
+ end
+ end
+ end
+
+ describe '#add_control_user' do
+ let(:experiment) { create(:experiment) }
+ let(:user) { build(:user) }
+
+ subject { experiment.add_control_user(user) }
+
+ it 'creates a new experiment_user record and assigns the correct group_type' do
+ expect { subject }.to change { experiment.control_group_users.count }.by(1)
+ end
+ end
+
+ describe '#add_experimental_user' do
+ let(:experiment) { create(:experiment) }
+ let(:user) { build(:user) }
+
+ subject { experiment.add_experimental_user(user) }
+
+ it 'creates a new experiment_user record and assigns the correct group_type' do
+ expect { subject }.to change { experiment.experimental_group_users.count }.by(1)
+ end
+ end
+end
diff --git a/spec/models/experiment_user_spec.rb b/spec/models/experiment_user_spec.rb
new file mode 100644
index 00000000000..9201529b145
--- /dev/null
+++ b/spec/models/experiment_user_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ExperimentUser do
+ describe 'Associations' do
+ it { is_expected.to belong_to(:experiment) }
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'Validations' do
+ it { is_expected.to validate_presence_of(:group_type) }
+ end
+end
diff --git a/spec/models/group_deploy_key_spec.rb b/spec/models/group_deploy_key_spec.rb
index 3fe71cc4699..6757c5534ce 100644
--- a/spec/models/group_deploy_key_spec.rb
+++ b/spec/models/group_deploy_key_spec.rb
@@ -4,8 +4,82 @@ require 'spec_helper'
RSpec.describe GroupDeployKey do
it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_many(:groups) }
+
+ let_it_be(:group_deploy_key) { create(:group_deploy_key) }
+ let_it_be(:group) { create(:group) }
it 'is of type DeployKey' do
expect(build(:group_deploy_key).type).to eq('DeployKey')
end
+
+ describe '#group_deploy_keys_group_for' do
+ subject { group_deploy_key.group_deploy_keys_group_for(group) }
+
+ context 'when this group deploy key is linked to a given group' do
+ it 'returns the relevant group_deploy_keys_group association' do
+ group_deploy_keys_group = create(:group_deploy_keys_group, group: group, group_deploy_key: group_deploy_key)
+
+ expect(subject).to eq(group_deploy_keys_group)
+ end
+ end
+
+ context 'when this group deploy key is not linked to a given group' do
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#can_be_edited_for' do
+ let_it_be(:user) { create(:user) }
+
+ subject { group_deploy_key.can_be_edited_for?(user, group) }
+
+ context 'when a given user has the :update_group_deploy_key permission for that key' do
+ it 'is true' do
+ allow(Ability).to receive(:allowed?).with(user, :update_group_deploy_key, group_deploy_key).and_return(true)
+
+ expect(subject).to be_truthy
+ end
+ end
+
+ context 'when a given user does not have the :update_group_deploy_key permission for that key' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :update_group_deploy_key, group_deploy_key).and_return(false)
+ end
+
+ it 'is true when this user has the :update_group_deploy_key_for_group permission for this group' do
+ allow(Ability).to receive(:allowed?).with(user, :update_group_deploy_key_for_group, group_deploy_key.group_deploy_keys_group_for(group)).and_return(true)
+
+ expect(subject).to be_truthy
+ end
+
+ it 'is false when this user does not have the :update_group_deploy_key_for_group permission for this group' do
+ allow(Ability).to receive(:allowed?).with(user, :update_group_deploy_key_for_group, group_deploy_key.group_deploy_keys_group_for(group)).and_return(false)
+
+ expect(subject).to be_falsey
+ end
+ end
+ end
+
+ describe '#group_deploy_keys_groups_for_user' do
+ let_it_be(:user) { create(:user) }
+
+ context 'when a group has a group deploy key' do
+ let_it_be(:expected_association) { create(:group_deploy_keys_group, group: group, group_deploy_key: group_deploy_key) }
+
+ it 'returns the related group_deploy_keys_group association when the user can read the group' do
+ allow(Ability).to receive(:allowed?).with(user, :read_group, group).and_return(true)
+
+ expect(group_deploy_key.group_deploy_keys_groups_for_user(user))
+ .to contain_exactly(expected_association)
+ end
+
+ it 'does not return the related group_deploy_keys_group association when the user cannot read the group' do
+ allow(Ability).to receive(:allowed?).with(user, :read_group, group).and_return(false)
+
+ expect(group_deploy_key.group_deploy_keys_groups_for_user(user)).to be_empty
+ end
+ end
+ end
end
diff --git a/spec/models/group_deploy_keys_group_spec.rb b/spec/models/group_deploy_keys_group_spec.rb
new file mode 100644
index 00000000000..31e5627a093
--- /dev/null
+++ b/spec/models/group_deploy_keys_group_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GroupDeployKeysGroup do
+ describe "Associations" do
+ it { is_expected.to belong_to(:group_deploy_key) }
+ it { is_expected.to belong_to(:group) }
+ end
+
+ describe "Validation" do
+ it { is_expected.to validate_presence_of(:group_id) }
+ it { is_expected.to validate_presence_of(:group_deploy_key) }
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 4184f2d07cc..3eb74da09e1 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -26,6 +26,7 @@ RSpec.describe Group do
it { is_expected.to have_many(:container_repositories) }
it { is_expected.to have_many(:milestones) }
it { is_expected.to have_many(:iterations) }
+ it { is_expected.to have_many(:group_deploy_keys) }
describe '#members & #requesters' do
let(:requester) { create(:user) }
@@ -1540,4 +1541,48 @@ RSpec.describe Group do
end
end
end
+
+ describe '#default_owner' do
+ let(:group) { build(:group) }
+
+ context 'the group has owners' do
+ before do
+ group.add_owner(create(:user))
+ group.add_owner(create(:user))
+ end
+
+ it 'is the first owner' do
+ expect(group.default_owner)
+ .to eq(group.owners.first)
+ .and be_a(User)
+ end
+ end
+
+ context 'the group has a parent' do
+ let(:parent) { build(:group) }
+
+ before do
+ group.parent = parent
+ parent.add_owner(create(:user))
+ end
+
+ it 'is the first owner of the parent' do
+ expect(group.default_owner)
+ .to eq(parent.default_owner)
+ .and be_a(User)
+ end
+ end
+
+ context 'we fallback to group.owner' do
+ before do
+ group.owner = build(:user)
+ end
+
+ it 'is the group.owner' do
+ expect(group.default_owner)
+ .to eq(group.owner)
+ .and be_a(User)
+ end
+ end
+ end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 80041d2e859..59634524e74 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -5,10 +5,14 @@ require 'spec_helper'
RSpec.describe Issue do
include ExternalAuthorizationServiceHelpers
+ let_it_be(:user) { create(:user) }
+ let_it_be(:reusable_project) { create(:project) }
+
describe "Associations" do
it { is_expected.to belong_to(:milestone) }
it { is_expected.to belong_to(:iteration) }
it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_one(:namespace).through(:project) }
it { is_expected.to belong_to(:moved_to).class_name('Issue') }
it { is_expected.to have_one(:moved_from).class_name('Issue') }
it { is_expected.to belong_to(:duplicated_to).class_name('Issue') }
@@ -55,6 +59,26 @@ RSpec.describe Issue do
end
end
+ describe 'validations' do
+ subject { issue.valid? }
+
+ describe 'issue_type' do
+ let(:issue) { build(:issue, issue_type: issue_type) }
+
+ context 'when a valid type' do
+ let(:issue_type) { :issue }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'empty type' do
+ let(:issue_type) { nil }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
subject { create(:issue) }
describe 'callbacks' do
@@ -105,14 +129,30 @@ RSpec.describe Issue do
end
end
+ describe '.with_issue_type' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:incident) { create(:incident, project: project) }
+
+ it 'gives issues with the given issue type' do
+ expect(described_class.with_issue_type('issue'))
+ .to contain_exactly(issue)
+ end
+
+ it 'gives issues with the given issue type' do
+ expect(described_class.with_issue_type(%w(issue incident)))
+ .to contain_exactly(issue, incident)
+ end
+ end
+
describe '#order_by_position_and_priority' do
- let(:project) { create :project }
+ let(:project) { reusable_project }
let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let!(:issue1) { create(:labeled_issue, project: project, labels: [p1]) }
let!(:issue2) { create(:labeled_issue, project: project, labels: [p2]) }
- let!(:issue3) { create(:issue, project: project, relative_position: 100) }
- let!(:issue4) { create(:issue, project: project, relative_position: 200) }
+ let!(:issue3) { create(:issue, project: project, relative_position: -200) }
+ let!(:issue4) { create(:issue, project: project, relative_position: -100) }
it 'returns ordered list' do
expect(project.issues.order_by_position_and_priority)
@@ -121,10 +161,10 @@ RSpec.describe Issue do
end
describe '#sort' do
- let(:project) { create(:project) }
+ let(:project) { reusable_project }
context "by relative_position" do
- let!(:issue) { create(:issue, project: project) }
+ let!(:issue) { create(:issue, project: project, relative_position: nil) }
let!(:issue2) { create(:issue, project: project, relative_position: 2) }
let!(:issue3) { create(:issue, project: project, relative_position: 1) }
@@ -166,39 +206,9 @@ RSpec.describe Issue do
expect { issue.close }.to change { issue.state_id }.from(open_state).to(closed_state)
end
-
- context 'when there is an associated Alert Management Alert' do
- context 'when alert can be resolved' do
- let!(:alert) { create(:alert_management_alert, project: issue.project, issue: issue) }
-
- it 'resolves an alert' do
- expect { issue.close }.to change { alert.reload.resolved? }.to(true)
- end
- end
-
- context 'when alert cannot be resolved' do
- let!(:alert) { create(:alert_management_alert, :with_validation_errors, project: issue.project, issue: issue) }
-
- before do
- allow(Gitlab::AppLogger).to receive(:warn).and_call_original
- end
-
- it 'writes a warning into the log' do
- issue.close
-
- expect(Gitlab::AppLogger).to have_received(:warn).with(
- message: 'Cannot resolve an associated Alert Management alert',
- issue_id: issue.id,
- alert_id: alert.id,
- alert_errors: { hosts: ['hosts array is over 255 chars'] }
- )
- end
- end
- end
end
describe '#reopen' do
- let(:user) { create(:user) }
let(:issue) { create(:issue, state: 'closed', closed_at: Time.current, closed_by: user) }
it 'sets closed_at to nil when an issue is reopend' do
@@ -282,7 +292,6 @@ RSpec.describe Issue do
end
describe '#assignee_or_author?' do
- let(:user) { create(:user) }
let(:issue) { create(:issue) }
it 'returns true for a user that is assigned to an issue' do
@@ -303,7 +312,6 @@ RSpec.describe Issue do
end
describe '#can_move?' do
- let(:user) { create(:user) }
let(:issue) { create(:issue) }
subject { issue.can_move?(user) }
@@ -1020,7 +1028,7 @@ RSpec.describe Issue do
context "relative positioning" do
it_behaves_like "a class that supports relative positioning" do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
let(:factory) { :issue }
let(:default_params) { { project: project } }
end
diff --git a/spec/models/iteration_spec.rb b/spec/models/iteration_spec.rb
index ef638330208..5c684fa9771 100644
--- a/spec/models/iteration_spec.rb
+++ b/spec/models/iteration_spec.rb
@@ -8,11 +8,11 @@ RSpec.describe Iteration do
describe "#iid" do
it "is properly scoped on project and group" do
- iteration1 = create(:iteration, project: project)
- iteration2 = create(:iteration, project: project)
+ iteration1 = create(:iteration, :skip_project_validation, project: project)
+ iteration2 = create(:iteration, :skip_project_validation, project: project)
iteration3 = create(:iteration, group: group)
iteration4 = create(:iteration, group: group)
- iteration5 = create(:iteration, project: project)
+ iteration5 = create(:iteration, :skip_project_validation, project: project)
want = {
iteration1: 1,
@@ -35,6 +35,15 @@ RSpec.describe Iteration do
context 'Validations' do
subject { build(:iteration, group: group, start_date: start_date, due_date: due_date) }
+ describe '#not_belonging_to_project' do
+ subject { build(:iteration, project: project, start_date: Time.current, due_date: 1.day.from_now) }
+
+ it 'is invalid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:project_id]).to include('is not allowed. We do not currently support project-level iterations')
+ end
+ end
+
describe '#dates_do_not_overlap' do
let_it_be(:existing_iteration) { create(:iteration, group: group, start_date: 4.days.from_now, due_date: 1.week.from_now) }
@@ -54,7 +63,10 @@ RSpec.describe Iteration do
end
context 'when dates overlap' do
- context 'same group' do
+ let(:start_date) { 5.days.from_now }
+ let(:due_date) { 6.days.from_now }
+
+ shared_examples_for 'overlapping dates' do
context 'when start_date is in range' do
let(:start_date) { 5.days.from_now }
let(:due_date) { 3.weeks.from_now }
@@ -63,6 +75,11 @@ RSpec.describe Iteration do
expect(subject).not_to be_valid
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
end
+
+ it 'is not valid even if forced' do
+ subject.validate # to generate iid/etc
+ expect { subject.save!(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/)
+ end
end
context 'when end_date is in range' do
@@ -73,25 +90,72 @@ RSpec.describe Iteration do
expect(subject).not_to be_valid
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
end
+
+ it 'is not valid even if forced' do
+ subject.validate # to generate iid/etc
+ expect { subject.save!(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/)
+ end
end
context 'when both overlap' do
- let(:start_date) { 5.days.from_now }
- let(:due_date) { 6.days.from_now }
-
it 'is not valid' do
expect(subject).not_to be_valid
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
end
+
+ it 'is not valid even if forced' do
+ subject.validate # to generate iid/etc
+ expect { subject.save!(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/)
+ end
end
end
- context 'different group' do
- let(:start_date) { 5.days.from_now }
- let(:due_date) { 6.days.from_now }
- let(:group) { create(:group) }
+ context 'group' do
+ it_behaves_like 'overlapping dates' do
+ let(:constraint_name) { 'iteration_start_and_due_daterange_group_id_constraint' }
+ end
+
+ context 'different group' do
+ let(:group) { create(:group) }
- it { is_expected.to be_valid }
+ it { is_expected.to be_valid }
+
+ it 'does not trigger exclusion constraints' do
+ expect { subject.save! }.not_to raise_exception
+ end
+ end
+ end
+
+ context 'project' do
+ let_it_be(:existing_iteration) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) }
+
+ subject { build(:iteration, :skip_project_validation, project: project, start_date: start_date, due_date: due_date) }
+
+ it_behaves_like 'overlapping dates' do
+ let(:constraint_name) { 'iteration_start_and_due_daterange_project_id_constraint' }
+ end
+
+ context 'different project' do
+ let(:project) { create(:project) }
+
+ it { is_expected.to be_valid }
+
+ it 'does not trigger exclusion constraints' do
+ expect { subject.save! }.not_to raise_exception
+ end
+ end
+
+ context 'in a group' do
+ let(:group) { create(:group) }
+
+ subject { build(:iteration, group: group, start_date: start_date, due_date: due_date) }
+
+ it { is_expected.to be_valid }
+
+ it 'does not trigger exclusion constraints' do
+ expect { subject.save! }.not_to raise_exception
+ end
+ end
end
end
end
@@ -148,9 +212,9 @@ RSpec.describe Iteration do
context 'time scopes' do
let_it_be(:project) { create(:project, :empty_repo) }
- let_it_be(:iteration_1) { create(:iteration, :skip_future_date_validation, project: project, start_date: 3.days.ago, due_date: 1.day.from_now) }
- let_it_be(:iteration_2) { create(:iteration, :skip_future_date_validation, project: project, start_date: 10.days.ago, due_date: 4.days.ago) }
- let_it_be(:iteration_3) { create(:iteration, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) }
+ let_it_be(:iteration_1) { create(:iteration, :skip_future_date_validation, :skip_project_validation, project: project, start_date: 3.days.ago, due_date: 1.day.from_now) }
+ let_it_be(:iteration_2) { create(:iteration, :skip_future_date_validation, :skip_project_validation, project: project, start_date: 10.days.ago, due_date: 4.days.ago) }
+ let_it_be(:iteration_3) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) }
describe 'start_date_passed' do
it 'returns iterations where start_date is in the past but due_date is in the future' do
@@ -168,9 +232,9 @@ RSpec.describe Iteration do
describe '.within_timeframe' do
let_it_be(:now) { Time.current }
let_it_be(:project) { create(:project, :empty_repo) }
- let_it_be(:iteration_1) { create(:iteration, project: project, start_date: now, due_date: 1.day.from_now) }
- let_it_be(:iteration_2) { create(:iteration, project: project, start_date: 2.days.from_now, due_date: 3.days.from_now) }
- let_it_be(:iteration_3) { create(:iteration, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) }
+ let_it_be(:iteration_1) { create(:iteration, :skip_project_validation, project: project, start_date: now, due_date: 1.day.from_now) }
+ let_it_be(:iteration_2) { create(:iteration, :skip_project_validation, project: project, start_date: 2.days.from_now, due_date: 3.days.from_now) }
+ let_it_be(:iteration_3) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) }
it 'returns iterations with start_date and/or end_date between timeframe' do
iterations = described_class.within_timeframe(2.days.from_now, 3.days.from_now)
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index 36d45f17392..a0f633218b0 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -152,14 +152,10 @@ RSpec.describe LfsObject do
end
describe 'file is being stored' do
- let(:lfs_object) { create(:lfs_object, :with_file) }
+ subject { create(:lfs_object, :with_file) }
context 'when existing object has local store' do
- it 'is stored locally' do
- expect(lfs_object.file_store).to be(ObjectStorage::Store::LOCAL)
- expect(lfs_object.file).to be_file_storage
- expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL)
- end
+ it_behaves_like 'mounted file in local store'
end
context 'when direct upload is enabled' do
@@ -167,13 +163,7 @@ RSpec.describe LfsObject do
stub_lfs_object_storage(direct_upload: true)
end
- context 'when file is stored' do
- it 'is stored remotely' do
- expect(lfs_object.file_store).to eq(ObjectStorage::Store::REMOTE)
- expect(lfs_object.file).not_to be_file_storage
- expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::REMOTE)
- end
- end
+ it_behaves_like 'mounted file in object store'
end
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index f155c240fb2..a3ed39abfb3 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -113,9 +113,10 @@ RSpec.describe Member do
end
describe 'Scopes & finders' do
- before do
- project = create(:project, :public)
- group = create(:group)
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:group) { create(:group) }
+
+ before_all do
@owner_user = create(:user).tap { |u| group.add_owner(u) }
@owner = group.members.find_by(user_id: @owner_user.id)
@@ -252,9 +253,9 @@ RSpec.describe Member do
describe '.add_user' do
%w[project group].each do |source_type|
context "when source is a #{source_type}" do
- let!(:source) { create(source_type, :public) }
- let!(:user) { create(:user) }
- let!(:admin) { create(:admin) }
+ let_it_be(:source, reload: true) { create(source_type, :public) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
it 'returns a <Source>Member object' do
member = described_class.add_user(source, user, :maintainer)
@@ -322,7 +323,7 @@ RSpec.describe Member do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
- described_class.add_user(source, 42, :maintainer)
+ described_class.add_user(source, non_existing_record_id, :maintainer)
expect(source.users.reload).not_to include(user)
end
@@ -482,10 +483,10 @@ RSpec.describe Member do
describe '.add_users' do
%w[project group].each do |source_type|
context "when source is a #{source_type}" do
- let!(:source) { create(source_type, :public) }
- let!(:admin) { create(:admin) }
- let(:user1) { create(:user) }
- let(:user2) { create(:user) }
+ let_it_be(:source) { create(source_type, :public) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
it 'returns a <Source>Member objects' do
members = described_class.add_users(source, [user1, user2], :maintainer)
diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb
index 4d9e768ecc6..82402b95597 100644
--- a/spec/models/merge_request/metrics_spec.rb
+++ b/spec/models/merge_request/metrics_spec.rb
@@ -8,4 +8,44 @@ RSpec.describe MergeRequest::Metrics do
it { is_expected.to belong_to(:latest_closed_by).class_name('User') }
it { is_expected.to belong_to(:merged_by).class_name('User') }
end
+
+ it 'sets `target_project_id` before save' do
+ merge_request = create(:merge_request)
+ metrics = merge_request.metrics
+
+ metrics.update_column(:target_project_id, nil)
+
+ metrics.save!
+
+ expect(metrics.target_project_id).to eq(merge_request.target_project_id)
+ end
+
+ describe 'scopes' do
+ let_it_be(:metrics_1) { create(:merge_request).metrics.tap { |m| m.update!(merged_at: 10.days.ago) } }
+ let_it_be(:metrics_2) { create(:merge_request).metrics.tap { |m| m.update!(merged_at: 5.days.ago) } }
+
+ describe '.merged_after' do
+ subject { described_class.merged_after(7.days.ago) }
+
+ it 'finds the record' do
+ is_expected.to eq([metrics_2])
+ end
+
+ it "doesn't include record outside of the filter" do
+ is_expected.not_to include([metrics_1])
+ end
+ end
+
+ describe '.merged_before' do
+ subject { described_class.merged_before(7.days.ago) }
+
+ it 'finds the record' do
+ is_expected.to eq([metrics_1])
+ end
+
+ it "doesn't include record outside of the filter" do
+ is_expected.not_to include([metrics_2])
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
index 5ea0145e60f..84fdfae1ed1 100644
--- a/spec/models/merge_request_diff_commit_spec.rb
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -30,6 +30,7 @@ RSpec.describe MergeRequestDiffCommit do
project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
]
end
+
let(:rows) do
[
{
@@ -73,6 +74,7 @@ RSpec.describe MergeRequestDiffCommit do
# This commit's date is "Sun Aug 17 07:12:55 292278994 +0000"
[project.commit('ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69')]
end
+
let(:timestamp) { Time.zone.at((1 << 31) - 1) }
let(:rows) do
[{
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index d153ccedf8c..e02c71a1c6f 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -103,6 +103,8 @@ RSpec.describe MergeRequestDiff do
it 'ignores diffs with 0 files' do
MergeRequestDiffFile.where(merge_request_diff_id: [closed_recently.id, merged_recently.id]).delete_all
+ closed_recently.update!(files_count: 0)
+ merged_recently.update!(files_count: 0)
is_expected.to contain_exactly(outdated.id, latest.id, closed.id, merged.id)
end
@@ -672,6 +674,12 @@ RSpec.describe MergeRequestDiff do
end
end
+ describe '#files_count' do
+ it 'returns number of diff files' do
+ expect(diff_with_commits.files_count).to eq(diff_with_commits.merge_request_diff_files.count)
+ end
+ end
+
describe '#first_commit' do
it 'returns first commit' do
expect(diff_with_commits.first_commit.sha).to eq(diff_with_commits.merge_request_diff_commits.last.sha)
@@ -721,10 +729,12 @@ RSpec.describe MergeRequestDiff do
describe '#modified_paths' do
subject do
- diff = create(:merge_request_diff)
- create(:merge_request_diff_file, :new_file, merge_request_diff: diff)
- create(:merge_request_diff_file, :renamed_file, merge_request_diff: diff)
- diff
+ create(:merge_request_diff).tap do |diff|
+ create(:merge_request_diff_file, :new_file, merge_request_diff: diff)
+ create(:merge_request_diff_file, :renamed_file, merge_request_diff: diff)
+
+ diff.merge_request_diff_files.reset
+ end
end
it 'returns affected file paths' do
@@ -735,12 +745,6 @@ RSpec.describe MergeRequestDiff do
let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
let(:diff) { merge_request.merge_request_diff }
- # before do
- # # Temporarily unstub diff.modified_paths in favor of original code
- # #
- # allow(diff).to receive(:modified_paths).and_call_original
- # end
-
context "when the merge_request_diff is overflowed" do
before do
expect(diff).to receive(:overflow?).and_return(true)
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 06febddef0c..6edef54b153 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -63,6 +63,7 @@ RSpec.describe MergeRequest do
subject.source_project.repository.path
end
end
+
let(:squash_path) { File.join(repo_path, "gitlab-worktree", "squash-#{subject.id}") }
before do
@@ -247,24 +248,20 @@ RSpec.describe MergeRequest do
describe 'callbacks' do
describe '#ensure_merge_request_metrics' do
- it 'creates metrics after saving' do
- merge_request = create(:merge_request)
+ let(:merge_request) { create(:merge_request) }
+ it 'creates metrics after saving' do
expect(merge_request.metrics).to be_persisted
expect(MergeRequest::Metrics.count).to eq(1)
end
it 'does not duplicate metrics for a merge request' do
- merge_request = create(:merge_request)
-
merge_request.mark_as_merged!
expect(MergeRequest::Metrics.count).to eq(1)
end
it 'does not create duplicated metrics records when MR is concurrently updated' do
- merge_request = create(:merge_request)
-
merge_request.metrics.destroy
instance1 = MergeRequest.find(merge_request.id)
@@ -276,6 +273,27 @@ RSpec.describe MergeRequest do
metrics_records = MergeRequest::Metrics.where(merge_request_id: merge_request.id)
expect(metrics_records.size).to eq(1)
end
+
+ it 'syncs the `target_project_id` to the metrics record' do
+ project = create(:project)
+
+ merge_request.update!(target_project: project, state: :closed)
+
+ expect(merge_request.target_project_id).to eq(project.id)
+ expect(merge_request.target_project_id).to eq(merge_request.metrics.target_project_id)
+ end
+
+ context 'when metrics record already exists with NULL target_project_id' do
+ before do
+ merge_request.metrics.update_column(:target_project_id, nil)
+ end
+
+ it 'returns the metrics record' do
+ metrics_record = merge_request.ensure_metrics
+
+ expect(metrics_record).to be_persisted
+ end
+ end
end
end
@@ -725,6 +743,7 @@ RSpec.describe MergeRequest do
let!(:diff_note) do
create(:diff_note_on_merge_request, project: project, noteable: merge_request)
end
+
let!(:draft_note) do
create(:draft_note_on_text_diff, author: user, merge_request: merge_request)
end
@@ -3696,6 +3715,7 @@ RSpec.describe MergeRequest do
source_branch: 'fixes',
target_project: target_project)
end
+
let(:user) { create(:user) }
before do
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index ad4c8448745..4ef2ddd218a 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -507,6 +507,7 @@ RSpec.describe Namespace do
Gitlab.config.repositories.storages.default.legacy_disk_path
end
end
+
let(:path_in_dir) { File.join(repository_storage_path, namespace.full_path) }
let(:deleted_path) { namespace.full_path.gsub(namespace.path, "#{namespace.full_path}+#{namespace.id}+deleted") }
let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) }
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index e6e6a8c35cf..7edd7849bbe 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -471,6 +471,7 @@ RSpec.describe Note do
note: "added label #{private_label.to_reference(ext_proj)}",
system: true
end
+
let!(:system_note_metadata) { create(:system_note_metadata, note: note, action: :label) }
it_behaves_like "checks references"
@@ -1372,11 +1373,11 @@ RSpec.describe Note do
describe 'banzai_render_context' do
let(:project) { build(:project_empty_repo) }
+ subject(:context) { noteable.banzai_render_context(:title) }
+
context 'when noteable is a merge request' do
let(:noteable) { build :merge_request, target_project: project, source_project: project }
- subject(:context) { noteable.banzai_render_context(:title) }
-
it 'sets the label_url_method in the context' do
expect(context[:label_url_method]).to eq(:project_merge_requests_url)
end
@@ -1385,11 +1386,34 @@ RSpec.describe Note do
context 'when noteable is an issue' do
let(:noteable) { build :issue, project: project }
- subject(:context) { noteable.banzai_render_context(:title) }
-
it 'sets the label_url_method in the context' do
expect(context[:label_url_method]).to eq(:project_issues_url)
end
end
+
+ context 'when noteable is a personal snippet' do
+ let(:noteable) { build(:personal_snippet) }
+
+ it 'sets the parent user in the context' do
+ expect(context[:user]).to eq(noteable.author)
+ end
+ end
+ end
+
+ describe '#parent_user' do
+ it 'returns the author of a personal snippet' do
+ note = build(:note_on_personal_snippet)
+ expect(note.parent_user).to eq(note.noteable.author)
+ end
+
+ it 'returns nil for project snippet' do
+ note = build(:note_on_project_snippet)
+ expect(note.parent_user).to be_nil
+ end
+
+ it 'returns nil when noteable is not a snippet' do
+ note = build(:note_on_issue)
+ expect(note.parent_user).to be_nil
+ end
end
end
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 67738eaec20..0f765d6b09b 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -118,6 +118,46 @@ RSpec.describe NotificationSetting do
expect(subject.event_enabled?(:foo_event)).to be(false)
end
end
+
+ describe 'for failed_pipeline' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:column, :expected) do
+ nil | true
+ true | true
+ false | false
+ end
+
+ with_them do
+ before do
+ subject.update!(failed_pipeline: column)
+ end
+
+ it do
+ expect(subject.event_enabled?(:failed_pipeline)).to eq(expected)
+ end
+ end
+ end
+
+ describe 'for fixed_pipeline' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:column, :expected) do
+ nil | true
+ true | true
+ false | false
+ end
+
+ with_them do
+ before do
+ subject.update!(fixed_pipeline: column)
+ end
+
+ it do
+ expect(subject.event_enabled?(:fixed_pipeline)).to eq(expected)
+ end
+ end
+ end
end
describe '.email_events' do
@@ -138,7 +178,8 @@ RSpec.describe NotificationSetting do
:merge_merge_request,
:failed_pipeline,
:success_pipeline,
- :fixed_pipeline
+ :fixed_pipeline,
+ :moved_project
)
end
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index 7758ed4a500..7cc8e13d449 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -32,11 +32,17 @@ RSpec.describe Packages::PackageFile, type: :model do
end
end
- it_behaves_like 'UpdateProjectStatistics' do
- subject { build(:package_file, :jar, size: 42) }
+ context 'updating project statistics' do
+ context 'when the package file has an explicit size' do
+ it_behaves_like 'UpdateProjectStatistics' do
+ subject { build(:package_file, :jar, size: 42) }
+ end
+ end
- before do
- allow_any_instance_of(Packages::PackageFileUploader).to receive(:size).and_return(42)
+ context 'when the package file does not have a size' do
+ it_behaves_like 'UpdateProjectStatistics' do
+ subject { build(:package_file, size: nil) }
+ end
end
end
@@ -52,7 +58,7 @@ RSpec.describe Packages::PackageFile, type: :model do
end
describe '#update_file_metadata callback' do
- let_it_be(:package_file) { build(:package_file, :nuget, file_store: nil, size: nil) }
+ let_it_be(:package_file) { build(:package_file, :nuget, size: nil) }
subject { package_file.save! }
@@ -61,9 +67,14 @@ RSpec.describe Packages::PackageFile, type: :model do
.to receive(:update_file_metadata)
.and_call_original
- expect { subject }
- .to change { package_file.file_store }.from(nil).to(::Packages::PackageFileUploader::Store::LOCAL)
- .and change { package_file.size }.from(nil).to(3513)
+ # This expectation uses a stub because we can no longer test a change from
+ # `nil` to `1`, because the field is no longer nullable, and it defaults
+ # to `1`.
+ expect(package_file)
+ .to receive(:update_column)
+ .with(:file_store, ::Packages::PackageFileUploader::Store::LOCAL)
+
+ expect { subject }.to change { package_file.size }.from(nil).to(3513)
end
end
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index d283389e29e..78980f8cdab 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -328,9 +328,11 @@ RSpec.describe PagesDomain do
end
describe '#update_daemon' do
+ let_it_be(:project) { create(:project).tap(&:mark_pages_as_deployed) }
+
context 'when usage is serverless' do
it 'does not call the UpdatePagesConfigurationService' do
- expect(Projects::UpdatePagesConfigurationService).not_to receive(:new)
+ expect(PagesUpdateConfigurationWorker).not_to receive(:perform_async)
create(:pages_domain, usage: :serverless)
end
@@ -352,12 +354,30 @@ RSpec.describe PagesDomain do
domain.destroy!
end
- it 'delegates to Projects::UpdatePagesConfigurationService' do
+ it 'delegates to Projects::UpdatePagesConfigurationService when not running async' do
+ stub_feature_flags(async_update_pages_config: false)
+
service = instance_double('Projects::UpdatePagesConfigurationService')
expect(Projects::UpdatePagesConfigurationService).to receive(:new) { service }
expect(service).to receive(:execute)
- create(:pages_domain)
+ create(:pages_domain, project: project)
+ end
+
+ it "schedules a PagesUpdateConfigurationWorker" do
+ expect(PagesUpdateConfigurationWorker).to receive(:perform_async).with(project.id)
+
+ create(:pages_domain, project: project)
+ end
+
+ context "when the pages aren't deployed" do
+ let_it_be(:project) { create(:project).tap(&:mark_pages_as_not_deployed) }
+
+ it "does not schedule a PagesUpdateConfigurationWorker" do
+ expect(PagesUpdateConfigurationWorker).not_to receive(:perform_async).with(project.id)
+
+ create(:pages_domain, project: project)
+ end
end
context 'configuration updates when attributes change' do
@@ -611,6 +631,7 @@ RSpec.describe PagesDomain do
let!(:domain_with_expired_user_provided_certificate) do
create(:pages_domain, :with_expired_certificate)
end
+
let!(:domain_with_user_provided_certificate_and_auto_ssl) do
create(:pages_domain, auto_ssl_enabled: true)
end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index a39a37b605f..9e80d0e0886 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -180,6 +180,18 @@ RSpec.describe PersonalAccessToken do
end
end
+ describe '.expired_today_and_not_notified' do
+ let_it_be(:active) { create(:personal_access_token) }
+ let_it_be(:expired_yesterday) { create(:personal_access_token, expires_at: Date.yesterday) }
+ let_it_be(:revoked_token) { create(:personal_access_token, expires_at: Date.current, revoked: true) }
+ let_it_be(:expired_today) { create(:personal_access_token, expires_at: Date.current) }
+ let_it_be(:expired_today_and_notified) { create(:personal_access_token, expires_at: Date.current, after_expiry_notification_delivered: true) }
+
+ it 'returns tokens that have expired today' do
+ expect(described_class.expired_today_and_not_notified).to contain_exactly(expired_today)
+ end
+ end
+
describe '.without_impersonation' do
let_it_be(:impersonation_token) { create(:personal_access_token, :impersonation) }
let_it_be(:personal_access_token) { create(:personal_access_token) }
diff --git a/spec/models/personal_snippet_spec.rb b/spec/models/personal_snippet_spec.rb
index 10d70fed1ee..234f6e4b4b5 100644
--- a/spec/models/personal_snippet_spec.rb
+++ b/spec/models/personal_snippet_spec.rb
@@ -21,7 +21,15 @@ RSpec.describe PersonalSnippet do
let_it_be(:container) { create(:personal_snippet, :repository) }
let(:stubbed_container) { build_stubbed(:personal_snippet) }
let(:expected_full_path) { "@snippets/#{container.id}" }
- let(:expected_web_url_path) { "snippets/#{container.id}" }
- let(:expected_repo_url_path) { expected_web_url_path }
+ let(:expected_web_url_path) { "-/snippets/#{container.id}" }
+ let(:expected_repo_url_path) { "snippets/#{container.id}" }
+ end
+
+ describe '#parent_user' do
+ it 'returns the snippet author' do
+ snippet = build(:personal_snippet)
+
+ expect(snippet.parent_user).to eq(snippet.author)
+ end
end
end
diff --git a/spec/models/plan_limits_spec.rb b/spec/models/plan_limits_spec.rb
index 831fd0dcbc3..bc6398de9a4 100644
--- a/spec/models/plan_limits_spec.rb
+++ b/spec/models/plan_limits_spec.rb
@@ -183,12 +183,9 @@ RSpec.describe PlanLimits do
ci_max_artifact_size_trace
ci_max_artifact_size_junit
ci_max_artifact_size_sast
- ci_max_artifact_size_dependency_scanning
- ci_max_artifact_size_container_scanning
ci_max_artifact_size_dast
ci_max_artifact_size_codequality
ci_max_artifact_size_license_management
- ci_max_artifact_size_license_scanning
ci_max_artifact_size_performance
ci_max_artifact_size_browser_performance
ci_max_artifact_size_load_performance
@@ -197,7 +194,6 @@ RSpec.describe PlanLimits do
ci_max_artifact_size_network_referee
ci_max_artifact_size_dotenv
ci_max_artifact_size_cobertura
- ci_max_artifact_size_terraform
ci_max_artifact_size_accessibility
ci_max_artifact_size_cluster_applications
ci_max_artifact_size_secret_detection
diff --git a/spec/models/product_analytics_event_spec.rb b/spec/models/product_analytics_event_spec.rb
index 6058df9fa13..afdb5b690f8 100644
--- a/spec/models/product_analytics_event_spec.rb
+++ b/spec/models/product_analytics_event_spec.rb
@@ -21,4 +21,18 @@ RSpec.describe ProductAnalyticsEvent, type: :model do
it { expect(described_class.timerange(7.days)).to match_array([event_1, event_2]) }
it { expect(described_class.timerange(30.days)).to match_array([event_1, event_2, event_3]) }
end
+
+ describe '.count_by_graph' do
+ let_it_be(:events) do
+ [
+ create(:product_analytics_event, platform: 'web'),
+ create(:product_analytics_event, platform: 'web'),
+ create(:product_analytics_event, platform: 'app'),
+ create(:product_analytics_event, platform: 'mobile', collector_tstamp: Time.zone.now - 10.days)
+ ]
+ end
+
+ it { expect(described_class.count_by_graph('platform', 7.days)).to eq({ 'app' => 1, 'web' => 2 }) }
+ it { expect(described_class.count_by_graph('platform', 30.days)).to eq({ 'app' => 1, 'mobile' => 1, 'web' => 2 }) }
+ end
end
diff --git a/spec/models/project_repository_storage_move_spec.rb b/spec/models/project_repository_storage_move_spec.rb
index 83711085c92..3e679c8af4d 100644
--- a/spec/models/project_repository_storage_move_spec.rb
+++ b/spec/models/project_repository_storage_move_spec.rb
@@ -74,9 +74,9 @@ RSpec.describe ProjectRepositoryStorageMove, type: :model do
context 'when started' do
subject(:storage_move) { create(:project_repository_storage_move, :started, project: project, destination_storage_name: 'test_second_storage') }
- context 'and transits to finished' do
+ context 'and transits to replicated' do
it 'sets the repository storage and marks the project as writable' do
- storage_move.finish!
+ storage_move.finish_replication!
expect(project.repository_storage).to eq('test_second_storage')
expect(project).not_to be_repository_read_only
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index ff717a59e7b..3d0c2cc1006 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe BuildkiteService, :use_clean_rails_memory_store_caching do
project: project,
properties: {
service_hook: true,
- project_url: 'https://buildkite.com/account-name/example-project',
+ project_url: 'https://buildkite.com/organization-name/example-pipeline',
token: 'secret-sauce-webhook-token:secret-sauce-status-token'
}
)
@@ -45,11 +45,27 @@ RSpec.describe BuildkiteService, :use_clean_rails_memory_store_caching do
end
end
+ describe '.supported_events' do
+ it 'supports push, merge_request, and tag_push events' do
+ expect(service.supported_events).to eq %w(push merge_request tag_push)
+ end
+ end
+
describe 'commits methods' do
before do
allow(project).to receive(:default_branch).and_return('default-brancho')
end
+ it 'always activates SSL verification after saved' do
+ service.create_service_hook(enable_ssl_verification: false)
+
+ service.enable_ssl_verification = false
+ service.active = true
+
+ expect { service.save! }
+ .to change { service.service_hook.enable_ssl_verification }.from(false).to(true)
+ end
+
describe '#webhook_url' do
it 'returns the webhook url' do
expect(service.webhook_url).to eq(
@@ -69,7 +85,7 @@ RSpec.describe BuildkiteService, :use_clean_rails_memory_store_caching do
describe '#build_page' do
it 'returns the correct build page' do
expect(service.build_page('2ab7834c', nil)).to eq(
- 'https://buildkite.com/account-name/example-project/builds?commit=2ab7834c'
+ 'https://buildkite.com/organization-name/example-pipeline/builds?commit=2ab7834c'
)
end
end
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
deleted file mode 100644
index a6b7cb05836..00000000000
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe GitlabIssueTrackerService do
- describe "Associations" do
- it { is_expected.to belong_to :project }
- it { is_expected.to have_one :service_hook }
- end
-
- describe 'Validations' do
- context 'when service is active' do
- subject { described_class.new(project: create(:project), active: true) }
-
- it { is_expected.to validate_presence_of(:issues_url) }
- it_behaves_like 'issue tracker service URL attribute', :issues_url
- end
-
- context 'when service is inactive' do
- subject { described_class.new(project: create(:project), active: false) }
-
- it { is_expected.not_to validate_presence_of(:issues_url) }
- end
- end
-
- describe 'project and issue urls' do
- let(:project) { create(:project) }
- let(:service) { project.create_gitlab_issue_tracker_service(active: true) }
-
- context 'with absolute urls' do
- before do
- allow(described_class).to receive(:default_url_options).and_return(script_name: "/gitlab/root")
- end
-
- it 'gives the correct path' do
- expect(service.project_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.full_path}/-/issues")
- expect(service.new_issue_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.full_path}/-/issues/new")
- expect(service.issue_url(432)).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.full_path}/-/issues/432")
- end
- end
-
- context 'with relative urls' do
- before do
- allow(described_class).to receive(:default_url_options).and_return(script_name: "/gitlab/root")
- end
-
- it 'gives the correct path' do
- expect(service.issue_tracker_path).to eq("/gitlab/root/#{project.full_path}/-/issues")
- expect(service.new_issue_path).to eq("/gitlab/root/#{project.full_path}/-/issues/new")
- expect(service.issue_path(432)).to eq("/gitlab/root/#{project.full_path}/-/issues/432")
- end
- end
- end
-end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index cfc2c920cd2..28bba893be4 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -121,6 +121,7 @@ RSpec.describe JiraService do
{ url: url, api_url: api_url, username: username, password: password,
jira_issue_transition_id: transition_id }
end
+
let(:data_params) do
{
url: url, api_url: api_url,
@@ -562,6 +563,7 @@ RSpec.describe JiraService do
password: password
)
end
+
let(:jira_issue) { ExternalIssue.new('JIRA-123', project) }
subject { jira_service.create_cross_reference_note(jira_issue, resource, user) }
diff --git a/spec/models/project_services/jira_tracker_data_spec.rb b/spec/models/project_services/jira_tracker_data_spec.rb
index 9e38bced46c..f2e2fa65e93 100644
--- a/spec/models/project_services/jira_tracker_data_spec.rb
+++ b/spec/models/project_services/jira_tracker_data_spec.rb
@@ -8,4 +8,8 @@ RSpec.describe JiraTrackerData do
describe 'Associations' do
it { is_expected.to belong_to(:service) }
end
+
+ describe 'deployment_type' do
+ it { is_expected.to define_enum_for(:deployment_type).with_values([:unknown, :server, :cloud]).with_prefix(:deployment) }
+ end
end
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index 610feb52827..53ab63ef030 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -121,6 +121,7 @@ RSpec.describe MicrosoftTeamsService do
message: "user created page: Awesome wiki_page"
}
end
+
let(:wiki_page) { create(:wiki_page, wiki: project.wiki, **opts) }
let(:wiki_page_sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') }
diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb
index 464b9b1da84..3bcbf6b9e1b 100644
--- a/spec/models/project_snippet_spec.rb
+++ b/spec/models/project_snippet_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe ProjectSnippet do
let_it_be(:container) { create(:project_snippet, :repository) }
let(:stubbed_container) { build_stubbed(:project_snippet) }
let(:expected_full_path) { "#{container.project.full_path}/@snippets/#{container.id}" }
- let(:expected_web_url_path) { "#{container.project.full_path}/snippets/#{container.id}" }
- let(:expected_repo_url_path) { expected_web_url_path }
+ let(:expected_web_url_path) { "#{container.project.full_path}/-/snippets/#{container.id}" }
+ let(:expected_repo_url_path) { "#{container.project.full_path}/snippets/#{container.id}" }
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 8fdda241719..f589589af8f 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -61,7 +61,6 @@ RSpec.describe Project do
it { is_expected.to have_one(:youtrack_service) }
it { is_expected.to have_one(:custom_issue_tracker_service) }
it { is_expected.to have_one(:bugzilla_service) }
- it { is_expected.to have_one(:gitlab_issue_tracker_service) }
it { is_expected.to have_one(:external_wiki_service) }
it { is_expected.to have_one(:confluence_service) }
it { is_expected.to have_one(:project_feature) }
@@ -104,6 +103,7 @@ RSpec.describe Project do
it { is_expected.to have_many(:clusters) }
it { is_expected.to have_many(:management_clusters).class_name('Clusters::Cluster') }
it { is_expected.to have_many(:kubernetes_namespaces) }
+ it { is_expected.to have_many(:cluster_agents).class_name('Clusters::Agent') }
it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') }
it { is_expected.to have_many(:lfs_file_locks) }
@@ -122,6 +122,7 @@ RSpec.describe Project do
it { is_expected.to have_many(:reviews).inverse_of(:project) }
it { is_expected.to have_many(:packages).class_name('Packages::Package') }
it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile') }
+ it { is_expected.to have_many(:pipeline_artifacts) }
it_behaves_like 'model with repository' do
let_it_be(:container) { create(:project, :repository, path: 'somewhere') }
@@ -400,6 +401,7 @@ RSpec.describe Project do
create(:project,
pending_delete: true)
end
+
let(:new_project) do
build(:project,
name: project_pending_deletion.name,
@@ -474,6 +476,46 @@ RSpec.describe Project do
end
end
+ describe '#has_packages?' do
+ let(:project) { create(:project, :public) }
+
+ subject { project.has_packages?(package_type) }
+
+ shared_examples 'returning true examples' do
+ let!(:package) { create("#{package_type}_package", project: project) }
+
+ it { is_expected.to be true }
+ end
+
+ shared_examples 'returning false examples' do
+ it { is_expected.to be false }
+ end
+
+ context 'with maven packages' do
+ it_behaves_like 'returning true examples' do
+ let(:package_type) { :maven }
+ end
+ end
+
+ context 'with npm packages' do
+ it_behaves_like 'returning true examples' do
+ let(:package_type) { :npm }
+ end
+ end
+
+ context 'with conan packages' do
+ it_behaves_like 'returning true examples' do
+ let(:package_type) { :conan }
+ end
+ end
+
+ context 'with no package type' do
+ it_behaves_like 'returning false examples' do
+ let(:package_type) { nil }
+ end
+ end
+ end
+
describe '#ci_pipelines' do
let(:project) { create(:project) }
@@ -638,6 +680,12 @@ RSpec.describe Project do
end
end
end
+
+ context 'when argument is a user' do
+ it 'returns full path to the project' do
+ expect(project.to_reference_base(owner)).to eq 'sample-namespace/sample-project'
+ end
+ end
end
describe '#to_human_reference' do
@@ -1042,6 +1090,30 @@ RSpec.describe Project do
end
end
+ describe '#default_owner' do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:namespace) { create(:namespace, owner: owner) }
+
+ context 'the project does not have a group' do
+ let(:project) { build(:project, namespace: namespace) }
+
+ it 'is the namespace owner' do
+ expect(project.default_owner).to eq(owner)
+ end
+ end
+
+ context 'the project is in a group' do
+ let(:group) { build(:group) }
+ let(:project) { build(:project, group: group, namespace: namespace) }
+
+ it 'is the group owner' do
+ allow(group).to receive(:default_owner).and_return(Object.new)
+
+ expect(project.default_owner).to eq(group.default_owner)
+ end
+ end
+ end
+
describe '#external_wiki' do
let(:project) { create(:project) }
@@ -1408,16 +1480,69 @@ RSpec.describe Project do
end
describe '#service_desk_address' do
- let_it_be(:project) { create(:project, service_desk_enabled: true) }
+ let_it_be(:project, reload: true) { create(:project, service_desk_enabled: true) }
- before do
- allow(Gitlab::ServiceDesk).to receive(:enabled?).and_return(true)
- allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
- allow(Gitlab.config.incoming_email).to receive(:address).and_return("test+%{key}@mail.com")
+ subject { project.service_desk_address }
+
+ shared_examples 'with incoming email address' do
+ context 'when incoming email is enabled' do
+ before do
+ config = double(enabled: true, address: 'test+%{key}@mail.com')
+ allow(::Gitlab.config).to receive(:incoming_email).and_return(config)
+ end
+
+ it 'uses project full path as service desk address key' do
+ expect(project.service_desk_address).to eq("test+#{project.full_path_slug}-#{project.project_id}-issue-@mail.com")
+ end
+ end
+
+ context 'when incoming email is disabled' do
+ before do
+ config = double(enabled: false)
+ allow(::Gitlab.config).to receive(:incoming_email).and_return(config)
+ end
+
+ it 'uses project full path as service desk address key' do
+ expect(project.service_desk_address).to be_nil
+ end
+ end
+ end
+
+ context 'when service_desk_email is disabled' do
+ before do
+ allow(::Gitlab::ServiceDeskEmail).to receive(:enabled?).and_return(false)
+ end
+
+ it_behaves_like 'with incoming email address'
end
- it 'uses project full path as service desk address key' do
- expect(project.service_desk_address).to eq("test+#{project.full_path_slug}-#{project.project_id}-issue-@mail.com")
+ context 'when service_desk_email is enabled' do
+ before do
+ config = double(enabled: true, address: 'foo+%{key}@bar.com')
+ allow(::Gitlab::ServiceDeskEmail).to receive(:config).and_return(config)
+ end
+
+ context 'when service_desk_custom_address flag is enabled' do
+ before do
+ stub_feature_flags(service_desk_custom_address: true)
+ end
+
+ it 'returns custom address when project_key is set' do
+ create(:service_desk_setting, project: project, project_key: 'key1')
+
+ expect(subject).to eq("foo+#{project.full_path_slug}-key1@bar.com")
+ end
+
+ it_behaves_like 'with incoming email address'
+ end
+
+ context 'when service_desk_custom_address flag is disabled' do
+ before do
+ stub_feature_flags(service_desk_custom_address: false)
+ end
+
+ it_behaves_like 'with incoming email address'
+ end
end
end
@@ -1657,9 +1782,9 @@ RSpec.describe Project do
subject { project.pages_deployed? }
- context 'if public folder does exist' do
+ context 'if pages are deployed' do
before do
- allow(Dir).to receive(:exist?).with(project.public_pages_path).and_return(true)
+ project.pages_metadatum.update_column(:deployed, true)
end
it { is_expected.to be_truthy }
@@ -2221,6 +2346,7 @@ RSpec.describe Project do
create(:ci_empty_pipeline, project: project, sha: project.commit.id,
ref: project.default_branch)
end
+
let!(:pipeline_for_second_branch) do
create(:ci_empty_pipeline, project: project, sha: second_branch.target,
ref: second_branch.name)
@@ -3488,6 +3614,7 @@ RSpec.describe Project do
public: '\\1'
MAP
end
+
let(:sha) { project.commit.id }
context 'when there is a route map' do
@@ -4085,7 +4212,6 @@ RSpec.describe Project do
end
it 'removes the pages directory and marks the project as not having pages deployed' do
- expect_any_instance_of(Projects::UpdatePagesConfigurationService).to receive(:execute)
expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return(true)
expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, namespace.full_path, anything)
@@ -5105,6 +5231,7 @@ RSpec.describe Project do
allow_collaboration: true
)
end
+
let!(:merge_request) do
create(
:merge_request,
@@ -5455,6 +5582,32 @@ RSpec.describe Project do
end
end
+ describe '.for_repository_storage' do
+ it 'returns the projects for a given repository storage' do
+ stub_storage_settings('test_second_storage' => {
+ 'path' => TestEnv::SECOND_STORAGE_PATH,
+ 'gitaly_address' => Gitlab.config.repositories.storages.default.gitaly_address
+ })
+ expected_project = create(:project, repository_storage: 'default')
+ create(:project, repository_storage: 'test_second_storage')
+
+ expect(described_class.for_repository_storage('default')).to eq([expected_project])
+ end
+ end
+
+ describe '.excluding_repository_storage' do
+ it 'returns the projects excluding the given repository storage' do
+ stub_storage_settings('test_second_storage' => {
+ 'path' => TestEnv::SECOND_STORAGE_PATH,
+ 'gitaly_address' => Gitlab.config.repositories.storages.default.gitaly_address
+ })
+ expected_project = create(:project, repository_storage: 'test_second_storage')
+ create(:project, repository_storage: 'default')
+
+ expect(described_class.excluding_repository_storage('default')).to eq([expected_project])
+ end
+ end
+
describe '.deployments' do
subject { project.deployments }
@@ -6154,6 +6307,48 @@ RSpec.describe Project do
end
end
+ describe '#has_packages?' do
+ let(:project) { create(:project, :public) }
+
+ subject { project.has_packages?(package_type) }
+
+ shared_examples 'has_package' do
+ context 'package of package_type exists' do
+ let!(:package) { create("#{package_type}_package", project: project) }
+
+ it { is_expected.to be true }
+ end
+
+ context 'package of package_type does not exist' do
+ it { is_expected.to be false }
+ end
+ end
+
+ context 'with maven packages' do
+ it_behaves_like 'has_package' do
+ let(:package_type) { :maven }
+ end
+ end
+
+ context 'with npm packages' do
+ it_behaves_like 'has_package' do
+ let(:package_type) { :npm }
+ end
+ end
+
+ context 'with conan packages' do
+ it_behaves_like 'has_package' do
+ let(:package_type) { :conan }
+ end
+ end
+
+ context 'calling has_package? with nil' do
+ let(:package_type) { nil }
+
+ it { is_expected.to be false }
+ end
+ end
+
describe '#environments_for_scope' do
let_it_be(:project, reload: true) { create(:project) }
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 3659e6b973e..5f66de3a63c 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -328,8 +328,8 @@ RSpec.describe ProjectStatistics do
it 'increases also storage size by that amount' do
expect { described_class.increment_statistic(project.id, stat, 20) }
- .to change { statistics.reload.storage_size }
- .by(20)
+ .to change { statistics.reload.storage_size }
+ .by(20)
end
end
diff --git a/spec/models/prometheus_alert_spec.rb b/spec/models/prometheus_alert_spec.rb
index 7169a34d96f..8e517e1764e 100644
--- a/spec/models/prometheus_alert_spec.rb
+++ b/spec/models/prometheus_alert_spec.rb
@@ -50,6 +50,8 @@ RSpec.describe PrometheusAlert do
it { is_expected.to validate_presence_of(:environment) }
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:prometheus_metric) }
+ it { is_expected.to validate_presence_of(:operator) }
+ it { is_expected.to validate_presence_of(:threshold) }
context 'when environment and metric belongs same project' do
it { is_expected.to be_valid }
@@ -74,6 +76,34 @@ RSpec.describe PrometheusAlert do
end
end
+ describe 'runbook validations' do
+ it 'disallow invalid urls' do
+ unsafe_url = %{https://replaceme.com/'><script>alert(document.cookie)</script>}
+ non_ascii_url = 'http://gitlab.com/user/project1/wiki/something€'
+ excessively_long_url = 'https://gitla' + 'b' * 1024 + '.com'
+
+ is_expected.not_to allow_values(
+ unsafe_url,
+ non_ascii_url,
+ excessively_long_url
+ ).for(:runbook_url)
+ end
+
+ it 'allow valid urls' do
+ external_url = 'http://runbook.gitlab.com/'
+ internal_url = 'http://192.168.1.1'
+ blank_url = ''
+ nil_url = nil
+
+ is_expected.to allow_value(
+ external_url,
+ internal_url,
+ blank_url,
+ nil_url
+ ).for(:runbook_url)
+ end
+ end
+
describe '#full_query' do
before do
subject.operator = "gt"
@@ -91,6 +121,7 @@ RSpec.describe PrometheusAlert do
subject.operator = "gt"
subject.threshold = 1
subject.prometheus_metric = metric
+ subject.runbook_url = 'runbook'
end
it 'returns the params of the prometheus alert' do
@@ -102,7 +133,11 @@ RSpec.describe PrometheusAlert do
"gitlab" => "hook",
"gitlab_alert_id" => metric.id,
"gitlab_prometheus_alert_id" => subject.id
- })
+ },
+ "annotations" => {
+ "runbook" => "runbook"
+ }
+ )
end
end
end
diff --git a/spec/models/prometheus_metric_spec.rb b/spec/models/prometheus_metric_spec.rb
index f284102b4a9..9588167bbcc 100644
--- a/spec/models/prometheus_metric_spec.rb
+++ b/spec/models/prometheus_metric_spec.rb
@@ -138,10 +138,6 @@ RSpec.describe PrometheusMetric do
expect(subject.to_query_metric.required_metrics).to eq([])
end
- it 'queryable metric has weight 0' do
- expect(subject.to_query_metric.weight).to eq(0)
- end
-
it 'queryable metrics has query description' do
queries = [
{
diff --git a/spec/models/raw_usage_data_spec.rb b/spec/models/raw_usage_data_spec.rb
new file mode 100644
index 00000000000..c10db63da56
--- /dev/null
+++ b/spec/models/raw_usage_data_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe RawUsageData do
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:payload) }
+ it { is_expected.to validate_presence_of(:recorded_at) }
+
+ context 'uniqueness validation' do
+ let!(:existing_record) { create(:raw_usage_data) }
+
+ it { is_expected.to validate_uniqueness_of(:recorded_at) }
+ end
+
+ describe '#update_sent_at!' do
+ let(:raw_usage_data) { create(:raw_usage_data) }
+
+ context 'with save_raw_usage_data feature enabled' do
+ before do
+ stub_feature_flags(save_raw_usage_data: true)
+ end
+
+ it 'updates sent_at' do
+ raw_usage_data.update_sent_at!
+
+ expect(raw_usage_data.sent_at).not_to be_nil
+ end
+ end
+
+ context 'with save_raw_usage_data feature disabled' do
+ before do
+ stub_feature_flags(save_raw_usage_data: false)
+ end
+
+ it 'updates sent_at' do
+ raw_usage_data.update_sent_at!
+
+ expect(raw_usage_data.sent_at).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index 716e7dc786e..fea15ea00c8 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -73,6 +73,22 @@ RSpec.describe Release do
end
end
+ describe '.create' do
+ it "fills released_at using created_at if it's not set" do
+ release = described_class.create(project: project, author: user)
+
+ expect(release.released_at).to eq(release.created_at)
+ end
+
+ 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)
+
+ expect(release.released_at).to eq(released_at)
+ end
+ end
+
describe '#sources' do
subject { release.sources }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 964cc5a13ca..a6b79e55f02 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -587,15 +587,19 @@ RSpec.describe Repository do
end
it "is expired when the branches caches are expired" do
- expect(cache).to receive(:delete).with(:merged_branch_names).at_least(:once)
+ expect(cache).to receive(:delete) do |*args|
+ expect(args).to include(:merged_branch_names)
+ end
- repository.send(:expire_branches_cache)
+ repository.expire_branches_cache
end
it "is expired when the repository caches are expired" do
- expect(cache).to receive(:delete).with(:merged_branch_names).at_least(:once)
+ expect(cache).to receive(:delete) do |*args|
+ expect(args).to include(:merged_branch_names)
+ end
- repository.send(:expire_all_method_caches)
+ repository.expire_all_method_caches
end
end
@@ -1245,6 +1249,32 @@ RSpec.describe Repository do
end
end
+ describe '#has_ambiguous_refs?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:branch_names, :tag_names, :result) do
+ nil | nil | false
+ %w() | %w() | false
+ %w(a b) | %w() | false
+ %w() | %w(c d) | false
+ %w(a b) | %w(c d) | false
+ %w(a/b) | %w(c/d) | false
+ %w(a b) | %w(c d a/z) | true
+ %w(a b c/z) | %w(c d) | true
+ %w(a/b/z) | %w(a/b) | false # we only consider refs ambiguous before the first slash
+ %w(a/b/z) | %w(a/b a) | true
+ end
+
+ with_them do
+ it do
+ allow(repository).to receive(:branch_names).and_return(branch_names)
+ allow(repository).to receive(:tag_names).and_return(tag_names)
+
+ expect(repository.has_ambiguous_refs?).to eq(result)
+ end
+ end
+ end
+
describe '#expand_ref' do
let(:ref) { 'ref' }
@@ -1926,8 +1956,9 @@ RSpec.describe Repository do
:has_visible_content?,
:issue_template_names,
:merge_request_template_names,
- :metrics_dashboard_paths,
- :xcode_project?
+ :user_defined_metrics_dashboard_paths,
+ :xcode_project?,
+ :has_ambiguous_refs?
])
repository.after_change_head
@@ -2072,7 +2103,7 @@ RSpec.describe Repository do
describe '#expire_branches_cache' do
it 'expires the cache' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(branch_names merged_branch_names branch_count has_visible_content?))
+ .with(%i(branch_names merged_branch_names branch_count has_visible_content? has_ambiguous_refs?))
.and_call_original
repository.expire_branches_cache
@@ -2082,7 +2113,7 @@ RSpec.describe Repository do
describe '#expire_tags_cache' do
it 'expires the cache' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(tag_names tag_count))
+ .with(%i(tag_names tag_count has_ambiguous_refs?))
.and_call_original
repository.expire_tags_cache
@@ -2673,6 +2704,7 @@ RSpec.describe Repository do
build(:commit, author: author_c),
build(:commit, author: author_c)]
end
+
let(:order_by) { nil }
let(:sort) { nil }
diff --git a/spec/models/resource_iteration_event_spec.rb b/spec/models/resource_iteration_event_spec.rb
new file mode 100644
index 00000000000..fe1310d7bf1
--- /dev/null
+++ b/spec/models/resource_iteration_event_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ResourceIterationEvent, type: :model do
+ it_behaves_like 'a resource event'
+ it_behaves_like 'a resource event for issues'
+ it_behaves_like 'a resource event for merge requests'
+
+ it_behaves_like 'having unique enum values'
+ it_behaves_like 'timebox resource event validations'
+ it_behaves_like 'timebox resource event actions'
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:iteration) }
+ end
+end
diff --git a/spec/models/resource_milestone_event_spec.rb b/spec/models/resource_milestone_event_spec.rb
index 76ffb358d80..0a5292b2d16 100644
--- a/spec/models/resource_milestone_event_spec.rb
+++ b/spec/models/resource_milestone_event_spec.rb
@@ -8,77 +8,14 @@ RSpec.describe ResourceMilestoneEvent, type: :model do
it_behaves_like 'a resource event for merge requests'
it_behaves_like 'having unique enum values'
+ it_behaves_like 'timebox resource event validations'
+ it_behaves_like 'timebox resource event states'
+ it_behaves_like 'timebox resource event actions'
describe 'associations' do
it { is_expected.to belong_to(:milestone) }
end
- describe 'validations' do
- context 'when issue and merge_request are both nil' do
- subject { build(described_class.name.underscore.to_sym, issue: nil, merge_request: nil) }
-
- it { is_expected.not_to be_valid }
- end
-
- context 'when issue and merge_request are both set' do
- subject { build(described_class.name.underscore.to_sym, issue: build(:issue), merge_request: build(:merge_request)) }
-
- it { is_expected.not_to be_valid }
- end
-
- context 'when issue is set' do
- subject { create(described_class.name.underscore.to_sym, issue: create(:issue), merge_request: nil) }
-
- it { is_expected.to be_valid }
- end
-
- context 'when merge_request is set' do
- subject { create(described_class.name.underscore.to_sym, issue: nil, merge_request: create(:merge_request)) }
-
- it { is_expected.to be_valid }
- end
- end
-
- describe 'states' do
- [Issue, MergeRequest].each do |klass|
- klass.available_states.each do |state|
- it "supports state #{state.first} for #{klass.name.underscore}" do
- model = create(klass.name.underscore, state: state[0])
- key = model.class.name.underscore
- event = build(described_class.name.underscore.to_sym, key => model, state: model.state)
-
- expect(event.state).to eq(state[0])
- end
- end
- end
- end
-
- shared_examples 'a milestone action queryable resource event' do |expected_results_for_actions|
- [Issue, MergeRequest].each do |klass|
- expected_results_for_actions.each do |action, expected_result|
- it "is #{expected_result} for action #{action} on #{klass.name.underscore}" do
- model = create(klass.name.underscore)
- key = model.class.name.underscore
- event = build(described_class.name.underscore.to_sym, key => model, action: action)
-
- expect(event.send(query_method)).to eq(expected_result)
- end
- end
- end
- end
-
- describe '#added?' do
- it_behaves_like 'a milestone action queryable resource event', { add: true, remove: false } do
- let(:query_method) { :add? }
- end
- end
-
- describe '#removed?' do
- it_behaves_like 'a milestone action queryable resource event', { add: false, remove: true } do
- let(:query_method) { :remove? }
- end
- end
-
describe '#milestone_title' do
let(:milestone) { create(:milestone, title: 'v2.3') }
let(:event) { create(:resource_milestone_event, milestone: milestone) }
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 75bbb074526..c4a9c0329c7 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -11,30 +11,31 @@ RSpec.describe Service do
end
describe 'validations' do
- it { is_expected.to validate_presence_of(:type) }
-
- it 'validates presence of project_id if not template', :aggregate_failures do
- expect(build(:service, project_id: nil, template: true)).to be_valid
- expect(build(:service, project_id: nil, template: false)).to be_invalid
- end
+ using RSpec::Parameterized::TableSyntax
- it 'validates presence of project_id if not instance', :aggregate_failures do
- expect(build(:service, project_id: nil, instance: true)).to be_valid
- expect(build(:service, project_id: nil, instance: false)).to be_invalid
- end
+ let(:group) { create(:group) }
+ let(:project) { create(:project) }
- it 'validates absence of project_id if instance', :aggregate_failures do
- expect(build(:service, project_id: nil, instance: true)).to be_valid
- expect(build(:service, instance: true)).to be_invalid
- end
+ it { is_expected.to validate_presence_of(:type) }
- it 'validates absence of project_id if template', :aggregate_failures do
- expect(build(:service, template: true)).to validate_absence_of(:project_id)
- expect(build(:service, template: false)).not_to validate_absence_of(:project_id)
+ where(:project_id, :group_id, :template, :instance, :valid) do
+ 1 | nil | false | false | true
+ nil | 1 | false | false | true
+ nil | nil | true | false | true
+ nil | nil | false | true | true
+ nil | nil | false | false | false
+ nil | nil | true | true | false
+ 1 | 1 | false | false | false
+ 1 | nil | true | false | false
+ 1 | nil | false | true | false
+ nil | 1 | true | false | false
+ nil | 1 | false | true | false
end
- it 'validates service is template or instance' do
- expect(build(:service, project_id: nil, template: true, instance: true)).to be_invalid
+ with_them do
+ it 'validates the service' do
+ expect(build(:service, project_id: project_id, group_id: group_id, template: template, instance: instance).valid?).to eq(valid)
+ end
end
context 'with an existing service template' do
@@ -58,12 +59,15 @@ RSpec.describe Service do
end
it 'validates uniqueness of type and project_id on create' do
- project = create(:project)
-
expect(create(:service, project: project, type: 'Service')).to be_valid
expect(build(:service, project: project, type: 'Service').valid?(:create)).to eq(false)
expect(build(:service, project: project, type: 'Service').valid?(:update)).to eq(true)
end
+
+ it 'validates uniqueness of type and group_id' do
+ expect(create(:service, group_id: group.id, project_id: nil, type: 'Service')).to be_valid
+ expect(build(:service, group_id: group.id, project_id: nil, type: 'Service')).to be_invalid
+ end
end
describe 'Scopes' do
@@ -535,7 +539,7 @@ RSpec.describe Service do
describe 'initialize service with no properties' do
let(:service) do
- GitlabIssueTrackerService.create(
+ BugzillaService.create(
project: create(:project),
project_url: 'http://gitlab.example.com'
)
diff --git a/spec/models/snippet_repository_spec.rb b/spec/models/snippet_repository_spec.rb
index 8c25d713c0a..0f5e0bfc75c 100644
--- a/spec/models/snippet_repository_spec.rb
+++ b/spec/models/snippet_repository_spec.rb
@@ -102,6 +102,7 @@ RSpec.describe SnippetRepository do
{ action: :move }.merge(move_file),
{ action: :update }.merge(update_file)]
end
+
let(:repo) { double }
before do
diff --git a/spec/models/suggestion_spec.rb b/spec/models/suggestion_spec.rb
index 6c30bc39c1d..e88fc13ecee 100644
--- a/spec/models/suggestion_spec.rb
+++ b/spec/models/suggestion_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe Suggestion do
end
context 'when inapplicable_reason is not nil' do
- let(:inapplicable_reason) { :applied }
+ let(:inapplicable_reason) { "Can't apply this suggestion." }
it { is_expected.to be_falsey }
end
@@ -77,7 +77,7 @@ RSpec.describe Suggestion do
context 'when suggestion is already applied' do
let(:suggestion) { build(:suggestion, :applied, note: note) }
- it { is_expected.to eq(:applied) }
+ it { is_expected.to eq("Can't apply this suggestion.") }
end
context 'when merge request was merged' do
@@ -85,7 +85,7 @@ RSpec.describe Suggestion do
merge_request.mark_as_merged!
end
- it { is_expected.to eq(:merge_request_merged) }
+ it { is_expected.to eq("This merge request was merged. To apply this suggestion, edit this file directly.") }
end
context 'when merge request is closed' do
@@ -93,7 +93,7 @@ RSpec.describe Suggestion do
merge_request.close!
end
- it { is_expected.to eq(:merge_request_closed) }
+ it { is_expected.to eq("This merge request is closed. To apply this suggestion, edit this file directly.") }
end
context 'when source branch is deleted' do
@@ -101,23 +101,51 @@ RSpec.describe Suggestion do
merge_request.project.repository.rm_branch(merge_request.author, merge_request.source_branch)
end
- it { is_expected.to eq(:source_branch_deleted) }
+ it { is_expected.to eq("Can't apply as the source branch was deleted.") }
end
- context 'when content is outdated' do
- before do
- allow(suggestion).to receive(:outdated?).and_return(true)
+ context 'when outdated' do
+ shared_examples_for 'outdated suggestion' do
+ before do
+ allow(suggestion).to receive(:single_line?).and_return(single_line)
+ end
+
+ context 'and suggestion is for a single line' do
+ let(:single_line) { true }
+
+ it { is_expected.to eq("Can't apply as this line was changed in a more recent version.") }
+ end
+
+ context 'and suggestion is for multiple lines' do
+ let(:single_line) { false }
+
+ it { is_expected.to eq("Can't apply as these lines were changed in a more recent version.") }
+ end
end
- it { is_expected.to eq(:outdated) }
+ context 'and content is outdated' do
+ before do
+ allow(suggestion).to receive(:outdated?).and_return(true)
+ end
+
+ it_behaves_like 'outdated suggestion'
+ end
+
+ context 'and note is outdated' do
+ before do
+ allow(note).to receive(:active?).and_return(false)
+ end
+
+ it_behaves_like 'outdated suggestion'
+ end
end
- context 'when note is outdated' do
+ context 'when suggestion has the same content' do
before do
- allow(note).to receive(:active?).and_return(false)
+ allow(suggestion).to receive(:different_content?).and_return(false)
end
- it { is_expected.to eq(:outdated) }
+ it { is_expected.to eq("This suggestion already matches its content.") }
end
context 'when applicable' do
diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb
index 00e67ad70db..68bb86bfa49 100644
--- a/spec/models/terraform/state_spec.rb
+++ b/spec/models/terraform/state_spec.rb
@@ -45,9 +45,7 @@ RSpec.describe Terraform::State do
describe '#update_file_store' do
context 'when file is stored in object storage' do
- it 'sets file_store to remote' do
- expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE)
- end
+ it_behaves_like 'mounted file in object store'
end
context 'when file is stored locally' do
@@ -55,9 +53,7 @@ RSpec.describe Terraform::State do
stub_terraform_state_object_storage(Terraform::StateUploader, enabled: false)
end
- it 'sets file_store to local' do
- expect(subject.file_store).to eq(ObjectStorage::Store::LOCAL)
- end
+ it_behaves_like 'mounted file in local store'
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index fa2e4b63648..f9b819e22cd 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -76,6 +76,7 @@ RSpec.describe User do
it { is_expected.to have_many(:groups) }
it { is_expected.to have_many(:keys).dependent(:destroy) }
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) }
it { is_expected.to have_many(:issues).dependent(:destroy) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
@@ -241,6 +242,22 @@ RSpec.describe User do
it { is_expected.to validate_length_of(:last_name).is_at_most(127) }
end
+ describe 'preferred_language' do
+ context 'when its value is nil in the database' do
+ let(:user) { build(:user, preferred_language: nil) }
+
+ it 'falls back to I18n.default_locale when empty in the database' do
+ expect(user.preferred_language).to eq I18n.default_locale.to_s
+ end
+
+ it 'falls back to english when I18n.default_locale is not an available language' do
+ I18n.default_locale = :kl
+
+ expect(user.preferred_language).to eq 'en'
+ end
+ end
+ end
+
describe 'username' do
it 'validates presence' do
expect(subject).to validate_presence_of(:username)
@@ -839,6 +856,24 @@ RSpec.describe User do
end
end
+ describe '.with_personal_access_tokens_expired_today' do
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:expired_today) { create(:personal_access_token, user: user1, expires_at: Date.current) }
+
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:revoked_token) { create(:personal_access_token, user: user2, expires_at: Date.current, revoked: true) }
+
+ let_it_be(:user3) { create(:user) }
+ let_it_be(:impersonated_token) { create(:personal_access_token, user: user3, expires_at: Date.current, impersonation: true) }
+
+ let_it_be(:user4) { create(:user) }
+ let_it_be(:already_notified) { create(:personal_access_token, user: user4, expires_at: Date.current, after_expiry_notification_delivered: true) }
+
+ it 'returns users whose token has expired today' do
+ expect(described_class.with_personal_access_tokens_expired_today).to contain_exactly(user1)
+ end
+ end
+
describe '.active_without_ghosts' do
let_it_be(:user1) { create(:user, :external) }
let_it_be(:user2) { create(:user, state: 'blocked') }
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index a2ca6441f28..aa8b9ce58b9 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -7,7 +7,11 @@ RSpec.describe WikiPage do
let(:container) { create(:project, :wiki_repo) }
let(:wiki) { Wiki.for_container(container, user) }
let(:new_page) { build(:wiki_page, wiki: wiki, title: 'test page', content: 'test content') }
- let(:existing_page) { create(:wiki_page, wiki: wiki, title: 'test page', content: 'test content', message: 'test commit') }
+
+ let(:existing_page) do
+ create(:wiki_page, wiki: wiki, title: 'test page', content: 'test content', message: 'test commit')
+ wiki.find_page('test page')
+ end
subject { new_page }
@@ -45,9 +49,11 @@ RSpec.describe WikiPage do
let(:dir_1) do
WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')])
end
+
let(:dir_1_1) do
WikiDirectory.new('dir_1/dir_1_1', [wiki.find_page('dir_1/dir_1_1/page_3')])
end
+
let(:dir_2) do
pages = [wiki.find_page('dir_2/page_5'),
wiki.find_page('dir_2/page_4')]
@@ -257,14 +263,68 @@ RSpec.describe WikiPage do
subject.attributes.delete(:title)
expect(subject).not_to be_valid
- expect(subject.errors.keys).to contain_exactly(:title)
+ expect(subject.errors.messages).to eq(title: ["can't be blank"])
end
it "validates presence of content" do
subject.attributes.delete(:content)
expect(subject).not_to be_valid
- expect(subject.errors.keys).to contain_exactly(:content)
+ expect(subject.errors.messages).to eq(content: ["can't be blank"])
+ end
+
+ describe '#validate_content_size_limit' do
+ context 'with a new page' do
+ before do
+ stub_application_setting(wiki_page_max_content_bytes: 10)
+ end
+
+ it 'accepts content below the limit' do
+ subject.attributes[:content] = 'a' * 10
+
+ expect(subject).to be_valid
+ end
+
+ it 'rejects content exceeding the limit' do
+ subject.attributes[:content] = 'a' * 11
+
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages).to eq(
+ content: ['is too long (11 Bytes). The maximum size is 10 Bytes.']
+ )
+ end
+
+ it 'counts content size in bytes rather than characters' do
+ subject.attributes[:content] = '💩💩💩'
+
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages).to eq(
+ content: ['is too long (12 Bytes). The maximum size is 10 Bytes.']
+ )
+ end
+ end
+
+ context 'with an existing page exceeding the limit' do
+ subject { existing_page }
+
+ before do
+ subject
+ stub_application_setting(wiki_page_max_content_bytes: 11)
+ end
+
+ it 'accepts content when it has not changed' do
+ expect(subject).to be_valid
+ end
+
+ it 'rejects content when it has changed' do
+ subject.attributes[:content] = 'a' * 12
+
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages).to eq(
+ content: ['is too long (12 Bytes). The maximum size is 11 Bytes.']
+ )
+ end
+ end
end
describe '#validate_path_limits' do
@@ -702,6 +762,58 @@ RSpec.describe WikiPage do
end
end
+ describe '#content_changed?' do
+ context 'with a new page' do
+ subject { new_page }
+
+ it 'returns true if content is set' do
+ subject.attributes[:content] = 'new'
+
+ expect(subject.content_changed?).to be(true)
+ end
+
+ it 'returns false if content is blank' do
+ subject.attributes[:content] = ' '
+
+ expect(subject.content_changed?).to be(false)
+ end
+ end
+
+ context 'with an existing page' do
+ subject { existing_page }
+
+ it 'returns false' do
+ expect(subject.content_changed?).to be(false)
+ end
+
+ it 'returns false if content is set to the same value' do
+ subject.attributes[:content] = 'test content'
+
+ expect(subject.content_changed?).to be(false)
+ end
+
+ it 'returns true if content is changed' do
+ subject.attributes[:content] = 'new'
+
+ expect(subject.content_changed?).to be(true)
+ end
+
+ it 'returns true if content is changed to a blank string' do
+ subject.attributes[:content] = ' '
+
+ expect(subject.content_changed?).to be(true)
+ end
+
+ it 'returns false if only the newline format has changed' do
+ expect(subject.page).to receive(:text_data).and_return("foo\nbar")
+
+ subject.attributes[:content] = "foo\r\nbar"
+
+ expect(subject.content_changed?).to be(false)
+ end
+ end
+ end
+
describe '#path' do
it 'returns the path when persisted' do
expect(existing_page.path).to eq('test-page.md')
diff --git a/spec/models/wiki_spec.rb b/spec/models/wiki_spec.rb
new file mode 100644
index 00000000000..8dd510a0b98
--- /dev/null
+++ b/spec/models/wiki_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Wiki do
+ describe '.new' do
+ it 'verifies that the user is a User' do
+ expect { described_class.new(double, 1) }.to raise_error(ArgumentError)
+ expect { described_class.new(double, build(:group)) }.to raise_error(ArgumentError)
+ expect { described_class.new(double, build(:user)) }.not_to raise_error
+ expect { described_class.new(double, nil) }.not_to raise_error
+ end
+ end
+end