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-09-19 04:45:44 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 04:45:44 +0300
commit85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch)
tree9160f299afd8c80c038f08e1545be119f5e3f1e1 /spec/models
parent15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff)
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'spec/models')
-rw-r--r--spec/models/alert_management/alert_spec.rb72
-rw-r--r--spec/models/analytics/instance_statistics/measurement_spec.rb45
-rw-r--r--spec/models/application_record_spec.rb68
-rw-r--r--spec/models/application_setting_spec.rb29
-rw-r--r--spec/models/atlassian/identity_spec.rb34
-rw-r--r--spec/models/audit_event_partitioned_spec.rb13
-rw-r--r--spec/models/authentication_event_spec.rb15
-rw-r--r--spec/models/badges/project_badge_spec.rb4
-rw-r--r--spec/models/blob_viewer/metrics_dashboard_yml_spec.rb253
-rw-r--r--spec/models/board_group_recent_visit_spec.rb2
-rw-r--r--spec/models/board_project_recent_visit_spec.rb2
-rw-r--r--spec/models/ci/bridge_spec.rb1
-rw-r--r--spec/models/ci/build_metadata_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb145
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb123
-rw-r--r--spec/models/ci/build_trace_chunks/fog_spec.rb12
-rw-r--r--spec/models/ci/instance_variable_spec.rb6
-rw-r--r--spec/models/ci/job_artifact_spec.rb40
-rw-r--r--spec/models/ci/legacy_stage_spec.rb2
-rw-r--r--spec/models/ci/persistent_ref_spec.rb8
-rw-r--r--spec/models/ci/pipeline_artifact_spec.rb78
-rw-r--r--spec/models/ci/pipeline_spec.rb473
-rw-r--r--spec/models/ci/ref_spec.rb72
-rw-r--r--spec/models/ci/runner_spec.rb6
-rw-r--r--spec/models/ci_platform_metric_spec.rb94
-rw-r--r--spec/models/clusters/agent_spec.rb11
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb17
-rw-r--r--spec/models/clusters/cluster_spec.rb1
-rw-r--r--spec/models/clusters/kubernetes_namespace_spec.rb3
-rw-r--r--spec/models/commit_range_spec.rb23
-rw-r--r--spec/models/commit_status_spec.rb18
-rw-r--r--spec/models/concerns/ci/artifactable_spec.rb36
-rw-r--r--spec/models/concerns/from_except_spec.rb7
-rw-r--r--spec/models/concerns/from_intersect_spec.rb7
-rw-r--r--spec/models/concerns/from_set_operator_spec.rb20
-rw-r--r--spec/models/concerns/from_union_spec.rb35
-rw-r--r--spec/models/concerns/id_in_ordered_spec.rb33
-rw-r--r--spec/models/concerns/issuable_spec.rb178
-rw-r--r--spec/models/concerns/milestoneable_spec.rb8
-rw-r--r--spec/models/concerns/prometheus_adapter_spec.rb10
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb8
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb6
-rw-r--r--spec/models/cycle_analytics/production_spec.rb53
-rw-r--r--spec/models/deployment_spec.rb18
-rw-r--r--spec/models/design_management/design_collection_spec.rb60
-rw-r--r--spec/models/design_management/design_spec.rb26
-rw-r--r--spec/models/dev_ops_report/metric_spec.rb (renamed from spec/models/dev_ops_score/metric_spec.rb)4
-rw-r--r--spec/models/diff_note_spec.rb4
-rw-r--r--spec/models/draft_note_spec.rb8
-rw-r--r--spec/models/environment_spec.rb8
-rw-r--r--spec/models/environment_status_spec.rb20
-rw-r--r--spec/models/event_spec.rb16
-rw-r--r--spec/models/group_deploy_key_spec.rb21
-rw-r--r--spec/models/group_spec.rb15
-rw-r--r--spec/models/issuable_severity_spec.rb25
-rw-r--r--spec/models/issue_link_spec.rb53
-rw-r--r--spec/models/issue_spec.rb78
-rw-r--r--spec/models/iteration_spec.rb53
-rw-r--r--spec/models/jira_connect_installation_spec.rb45
-rw-r--r--spec/models/jira_connect_subscription_spec.rb15
-rw-r--r--spec/models/member_spec.rb70
-rw-r--r--spec/models/merge_request/metrics_spec.rb11
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb7
-rw-r--r--spec/models/merge_request_diff_file_spec.rb15
-rw-r--r--spec/models/merge_request_diff_spec.rb36
-rw-r--r--spec/models/merge_request_reviewer_spec.rb14
-rw-r--r--spec/models/merge_request_spec.rb382
-rw-r--r--spec/models/metrics/dashboard/annotation_spec.rb2
-rw-r--r--spec/models/milestone_spec.rb10
-rw-r--r--spec/models/namespace/root_storage_statistics_spec.rb3
-rw-r--r--spec/models/namespace_spec.rb187
-rw-r--r--spec/models/note_spec.rb100
-rw-r--r--spec/models/operations/feature_flag_scope_spec.rb391
-rw-r--r--spec/models/operations/feature_flag_spec.rb258
-rw-r--r--spec/models/operations/feature_flags/strategy_spec.rb323
-rw-r--r--spec/models/operations/feature_flags/user_list_spec.rb102
-rw-r--r--spec/models/operations/feature_flags_client_spec.rb21
-rw-r--r--spec/models/packages/package_file_spec.rb6
-rw-r--r--spec/models/packages/package_spec.rb118
-rw-r--r--spec/models/pages/lookup_path_spec.rb61
-rw-r--r--spec/models/pages_deployment_spec.rb21
-rw-r--r--spec/models/pages_domain_spec.rb10
-rw-r--r--spec/models/performance_monitoring/prometheus_dashboard_spec.rb93
-rw-r--r--spec/models/product_analytics_event_spec.rb25
-rw-r--r--spec/models/project_feature_usage_spec.rb59
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb10
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/merge_message_spec.rb69
-rw-r--r--spec/models/project_services/ewm_service_spec.rb61
-rw-r--r--spec/models/project_services/jira_service_spec.rb147
-rw-r--r--spec/models/project_services/packagist_service_spec.rb30
-rw-r--r--spec/models/project_services/pipelines_email_service_spec.rb22
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb10
-rw-r--r--spec/models/project_spec.rb526
-rw-r--r--spec/models/project_statistics_spec.rb19
-rw-r--r--spec/models/project_team_spec.rb95
-rw-r--r--spec/models/project_wiki_spec.rb13
-rw-r--r--spec/models/remote_mirror_spec.rb24
-rw-r--r--spec/models/repository_spec.rb1
-rw-r--r--spec/models/resource_iteration_event_spec.rb17
-rw-r--r--spec/models/resource_label_event_spec.rb8
-rw-r--r--spec/models/resource_state_event_spec.rb28
-rw-r--r--spec/models/service_spec.rb148
-rw-r--r--spec/models/snippet_input_action_spec.rb6
-rw-r--r--spec/models/snippet_repository_spec.rb5
-rw-r--r--spec/models/snippet_spec.rb215
-rw-r--r--spec/models/snippet_statistics_spec.rb2
-rw-r--r--spec/models/terraform/state_spec.rb62
-rw-r--r--spec/models/terraform/state_version_spec.rb76
-rw-r--r--spec/models/user_agent_detail_spec.rb4
-rw-r--r--spec/models/user_interacted_project_spec.rb7
-rw-r--r--spec/models/user_spec.rb188
-rw-r--r--spec/models/x509_certificate_spec.rb18
113 files changed, 5344 insertions, 1336 deletions
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index f937a879400..eb9dcca842d 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -332,35 +332,39 @@ RSpec.describe AlertManagement::Alert do
end
end
- describe '#details' do
- let(:payload) do
- {
- 'title' => 'Details title',
- 'custom' => {
- 'alert' => {
- 'fields' => %w[one two]
- }
- },
- 'yet' => {
- 'another' => 'field'
- }
- }
- end
-
- let(:alert) { build(:alert_management_alert, project: project, title: 'Details title', payload: payload) }
-
- subject { alert.details }
-
- it 'renders the payload as inline hash' do
- is_expected.to eq(
- 'custom.alert.fields' => %w[one two],
- 'yet.another' => 'field'
- )
+ describe '.reference_pattern' do
+ subject { described_class.reference_pattern }
+
+ it { is_expected.to match('gitlab-org/gitlab^alert#123') }
+ end
+
+ describe '.link_reference_pattern' do
+ subject { described_class.link_reference_pattern }
+
+ it { is_expected.to match(triggered_alert.details_url) }
+ it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab/alert_management/123") }
+ it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab/issues/123") }
+ it { is_expected.not_to match("gitlab-org/gitlab/-/alert_management/123") }
+ end
+
+ describe '.reference_valid?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ref, :result) do
+ '123456' | true
+ '1' | true
+ '-1' | false
+ nil | false
+ '123456891012345678901234567890' | false
+ end
+
+ with_them do
+ it { expect(described_class.reference_valid?(ref)).to eq(result) }
end
end
describe '#to_reference' do
- it { expect(triggered_alert.to_reference).to eq('') }
+ it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") }
end
describe '#trigger' do
@@ -449,22 +453,4 @@ RSpec.describe AlertManagement::Alert do
expect { subject }.to change { alert.events }.by(1)
end
end
-
- describe '#present' do
- context 'when alert is generic' do
- let(:alert) { triggered_alert }
-
- it 'uses generic alert presenter' do
- expect(alert.present).to be_kind_of(AlertManagement::AlertPresenter)
- end
- end
-
- context 'when alert is Prometheus specific' do
- let(:alert) { build(:alert_management_alert, :prometheus, project: project) }
-
- it 'uses Prometheus Alert presenter' do
- expect(alert.present).to be_kind_of(AlertManagement::PrometheusAlertPresenter)
- end
- end
- end
end
diff --git a/spec/models/analytics/instance_statistics/measurement_spec.rb b/spec/models/analytics/instance_statistics/measurement_spec.rb
new file mode 100644
index 00000000000..4df847ea524
--- /dev/null
+++ b/spec/models/analytics/instance_statistics/measurement_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Analytics::InstanceStatistics::Measurement, type: :model do
+ describe 'validation' do
+ let!(:measurement) { create(:instance_statistics_measurement) }
+
+ it { is_expected.to validate_presence_of(:recorded_at) }
+ it { is_expected.to validate_presence_of(:identifier) }
+ it { is_expected.to validate_presence_of(:count) }
+ it { is_expected.to validate_uniqueness_of(:recorded_at).scoped_to(:identifier) }
+ end
+
+ describe 'identifiers enum' do
+ it 'maps to the correct values' do
+ expect(described_class.identifiers).to eq({
+ projects: 1,
+ users: 2,
+ issues: 3,
+ merge_requests: 4,
+ groups: 5,
+ pipelines: 6
+ }.with_indifferent_access)
+ end
+ end
+
+ describe 'scopes' do
+ let_it_be(:measurement_1) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago) }
+ let_it_be(:measurement_2) { create(:instance_statistics_measurement, :project_count, recorded_at: 2.days.ago) }
+ let_it_be(:measurement_3) { create(:instance_statistics_measurement, :group_count, recorded_at: 5.days.ago) }
+
+ describe '.order_by_latest' do
+ subject { described_class.order_by_latest }
+
+ it { is_expected.to eq([measurement_2, measurement_3, measurement_1]) }
+ end
+
+ describe '.with_identifier' do
+ subject { described_class.with_identifier(:projects) }
+
+ it { is_expected.to match_array([measurement_1, measurement_2]) }
+ end
+ end
+end
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index d9ab326505b..5ea1907543a 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -30,49 +30,51 @@ RSpec.describe ApplicationRecord do
end
end
- describe '.safe_find_or_create_by' do
- it 'creates the user avoiding race conditions' do
- expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
- allow(Suggestion).to receive(:find_or_create_by).and_call_original
+ context 'safe find or create methods' do
+ let_it_be(:note) { create(:diff_note_on_merge_request) }
- expect { Suggestion.safe_find_or_create_by(build(:suggestion).attributes) }
- .to change { Suggestion.count }.by(1)
- end
+ let(:suggestion_attributes) { attributes_for(:suggestion).merge!(note_id: note.id) }
- it 'passes a block to find_or_create_by' do
- attributes = build(:suggestion).attributes
+ describe '.safe_find_or_create_by' do
+ it 'creates the suggestion avoiding race conditions' do
+ expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
+ allow(Suggestion).to receive(:find_or_create_by).and_call_original
- expect do |block|
- Suggestion.safe_find_or_create_by(attributes, &block)
- end.to yield_with_args(an_object_having_attributes(attributes))
- end
+ expect { Suggestion.safe_find_or_create_by(suggestion_attributes) }
+ .to change { Suggestion.count }.by(1)
+ end
- it 'does not create a record when is not valid' do
- raw_usage_data = RawUsageData.safe_find_or_create_by({ recorded_at: nil })
+ it 'passes a block to find_or_create_by' do
+ expect do |block|
+ Suggestion.safe_find_or_create_by(suggestion_attributes, &block)
+ end.to yield_with_args(an_object_having_attributes(suggestion_attributes))
+ end
- expect(raw_usage_data.id).to be_nil
- expect(raw_usage_data).not_to be_valid
- end
- end
+ it 'does not create a record when is not valid' do
+ raw_usage_data = RawUsageData.safe_find_or_create_by({ recorded_at: nil })
- describe '.safe_find_or_create_by!' do
- it 'creates a record using safe_find_or_create_by' do
- expect(Suggestion).to receive(:find_or_create_by).and_call_original
-
- expect(Suggestion.safe_find_or_create_by!(build(:suggestion).attributes))
- .to be_a(Suggestion)
+ expect(raw_usage_data.id).to be_nil
+ expect(raw_usage_data).not_to be_valid
+ end
end
- it 'raises a validation error if the record was not persisted' do
- expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid)
- end
+ describe '.safe_find_or_create_by!' do
+ it 'creates a record using safe_find_or_create_by' do
+ expect(Suggestion).to receive(:find_or_create_by).and_call_original
+
+ expect(Suggestion.safe_find_or_create_by!(suggestion_attributes))
+ .to be_a(Suggestion)
+ end
- it 'passes a block to find_or_create_by' do
- attributes = build(:suggestion).attributes
+ 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
- expect do |block|
- Suggestion.safe_find_or_create_by!(attributes, &block)
- end.to yield_with_args(an_object_having_attributes(attributes))
+ it 'passes a block to find_or_create_by' do
+ expect do |block|
+ Suggestion.safe_find_or_create_by!(suggestion_attributes, &block)
+ end.to yield_with_args(an_object_having_attributes(suggestion_attributes))
+ end
end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index ab25608e2f0..9f76fb3330d 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -71,6 +71,8 @@ RSpec.describe ApplicationSetting do
it { is_expected.not_to allow_value('three').for(:push_event_activities_limit) }
it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) }
+ it { is_expected.to validate_numericality_of(:container_registry_delete_tags_service_timeout).only_integer.is_greater_than_or_equal_to(0) }
+
it { is_expected.to validate_numericality_of(: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) }
@@ -189,14 +191,10 @@ RSpec.describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:snowplow_collector_hostname) }
it { is_expected.to allow_value("snowplow.gitlab.com").for(:snowplow_collector_hostname) }
it { is_expected.not_to allow_value('/example').for(:snowplow_collector_hostname) }
- it { is_expected.to allow_value('https://example.org').for(:snowplow_iglu_registry_url) }
- it { is_expected.not_to allow_value('not-a-url').for(:snowplow_iglu_registry_url) }
- it { is_expected.to allow_value(nil).for(:snowplow_iglu_registry_url) }
end
context 'when snowplow is not enabled' do
it { is_expected.to allow_value(nil).for(:snowplow_collector_hostname) }
- it { is_expected.to allow_value(nil).for(:snowplow_iglu_registry_url) }
end
context "when user accepted let's encrypt terms of service" do
@@ -640,6 +638,29 @@ RSpec.describe ApplicationSetting do
is_expected.to be_invalid
end
end
+
+ context 'gitpod settings' do
+ it 'is invalid if gitpod is enabled and no url is provided' do
+ allow(subject).to receive(:gitpod_enabled).and_return(true)
+ allow(subject).to receive(:gitpod_url).and_return(nil)
+
+ is_expected.to be_invalid
+ end
+
+ it 'is invalid if gitpod is enabled and an empty url is provided' do
+ allow(subject).to receive(:gitpod_enabled).and_return(true)
+ allow(subject).to receive(:gitpod_url).and_return('')
+
+ is_expected.to be_invalid
+ end
+
+ it 'is invalid if gitpod is enabled and an invalid url is provided' do
+ allow(subject).to receive(:gitpod_enabled).and_return(true)
+ allow(subject).to receive(:gitpod_url).and_return('javascript:alert("test")//')
+
+ is_expected.to be_invalid
+ end
+ end
end
context 'restrict creating duplicates' do
diff --git a/spec/models/atlassian/identity_spec.rb b/spec/models/atlassian/identity_spec.rb
new file mode 100644
index 00000000000..a1dfe5b0e51
--- /dev/null
+++ b/spec/models/atlassian/identity_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Atlassian::Identity do
+ describe 'associations' do
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'validations' do
+ subject { create(:atlassian_identity) }
+
+ it { is_expected.to validate_presence_of(:extern_uid) }
+ it { is_expected.to validate_uniqueness_of(:extern_uid) }
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_uniqueness_of(:user) }
+ end
+
+ describe 'encrypted tokens' do
+ let(:token) { SecureRandom.alphanumeric(1254) }
+ let(:refresh_token) { SecureRandom.alphanumeric(45) }
+ let(:identity) { create(:atlassian_identity, token: token, refresh_token: refresh_token) }
+
+ it 'saves the encrypted token, refresh token and corresponding ivs' do
+ expect(identity.encrypted_token).not_to be_nil
+ expect(identity.encrypted_token_iv).not_to be_nil
+ expect(identity.encrypted_refresh_token).not_to be_nil
+ expect(identity.encrypted_refresh_token_iv).not_to be_nil
+
+ expect(identity.token).to eq(token)
+ expect(identity.refresh_token).to eq(refresh_token)
+ end
+ end
+end
diff --git a/spec/models/audit_event_partitioned_spec.rb b/spec/models/audit_event_partitioned_spec.rb
index fe69f0083b7..ab48e291f78 100644
--- a/spec/models/audit_event_partitioned_spec.rb
+++ b/spec/models/audit_event_partitioned_spec.rb
@@ -7,7 +7,10 @@ RSpec.describe AuditEventPartitioned do
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)
+ column_names_from_source_table = column_names(source_table)
+ column_names_from_partioned_table = column_names(partitioned_table)
+
+ expect(column_names_from_partioned_table).to match_array(column_names_from_source_table)
end
it 'has the same null constraints as the source table' do
@@ -30,6 +33,14 @@ RSpec.describe AuditEventPartitioned do
expect(event_from_partitioned_table).to eq(event_from_source_table)
end
+ def column_names(table)
+ table.connection.select_all(<<~SQL)
+ SELECT c.column_name
+ FROM information_schema.columns c
+ WHERE c.table_name = '#{table.table_name}'
+ SQL
+ end
+
def null_constraints(table)
table.connection.select_all(<<~SQL)
SELECT c.column_name, c.is_nullable
diff --git a/spec/models/authentication_event_spec.rb b/spec/models/authentication_event_spec.rb
new file mode 100644
index 00000000000..56b0111f2c7
--- /dev/null
+++ b/spec/models/authentication_event_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AuthenticationEvent do
+ describe 'associations' do
+ it { is_expected.to belong_to(:user).optional }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:provider) }
+ it { is_expected.to validate_presence_of(:user_name) }
+ it { is_expected.to validate_presence_of(:result) }
+ end
+end
diff --git a/spec/models/badges/project_badge_spec.rb b/spec/models/badges/project_badge_spec.rb
index 9b9836129a6..78bfd4e18c0 100644
--- a/spec/models/badges/project_badge_spec.rb
+++ b/spec/models/badges/project_badge_spec.rb
@@ -27,8 +27,8 @@ RSpec.describe ProjectBadge do
end
context 'methods' do
- let(:badge) { build(:project_badge, link_url: placeholder_url, image_url: placeholder_url) }
- let!(:project) { badge.project }
+ let(:badge) { build_stubbed(:project_badge, link_url: placeholder_url, image_url: placeholder_url) }
+ let(:project) { badge.project }
describe '#rendered_link_url' do
let(:method) { :link_url }
diff --git a/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
index 057f0f32158..84dfc5186a8 100644
--- a/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
+++ b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
@@ -9,119 +9,228 @@ RSpec.describe BlobViewer::MetricsDashboardYml do
let_it_be(:project) { create(:project, :repository) }
let(:blob) { fake_blob(path: '.gitlab/dashboards/custom-dashboard.yml', data: data) }
let(:sha) { sample_commit.id }
+ let(:data) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
subject(:viewer) { described_class.new(blob) }
- context 'when the definition is valid' do
- let(:data) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
+ context 'with metrics_dashboard_exhaustive_validations feature flag on' do
+ before do
+ stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
+ end
+
+ context 'when the definition is valid' do
+ before do
+ allow(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).and_return([])
+ end
+
+ describe '#valid?' do
+ it 'calls prepare! on the viewer' do
+ expect(viewer).to receive(:prepare!)
+
+ viewer.valid?
+ end
+
+ it 'processes dashboard yaml and returns true', :aggregate_failures do
+ yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw!
+
+ expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader|
+ expect(loader).to receive(:load_raw!).and_call_original
+ end
+ expect(Gitlab::Metrics::Dashboard::Validator)
+ .to receive(:errors)
+ .with(yml, dashboard_path: '.gitlab/dashboards/custom-dashboard.yml', project: project)
+ .and_call_original
+ expect(viewer.valid?).to be true
+ end
+ end
+
+ describe '#errors' do
+ it 'returns empty array' do
+ expect(viewer.errors).to eq []
+ end
+ end
+ end
+
+ context 'when definition is invalid' do
+ let(:error) { ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new }
+ let(:data) do
+ <<~YAML
+ dashboard:
+ YAML
+ end
+
+ before do
+ allow(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).and_return([error])
+ end
- describe '#valid?' do
- it 'calls prepare! on the viewer' do
- allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json)
+ describe '#valid?' do
+ it 'returns false' do
+ expect(viewer.valid?).to be false
+ end
+ end
- expect(viewer).to receive(:prepare!)
+ describe '#errors' do
+ it 'returns validation errors' do
+ expect(viewer.errors).to eq ["Dashboard failed schema validation"]
+ end
+ end
+ end
- viewer.valid?
+ context 'when YAML syntax is invalid' do
+ let(:data) do
+ <<~YAML
+ dashboard: 'empty metrics'
+ panel_groups:
+ - group: 'Group Title'
+ YAML
end
- it 'returns true', :aggregate_failures do
- yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw!
+ describe '#valid?' do
+ it 'returns false' do
+ expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors)
+ expect(viewer.valid?).to be false
+ end
+ end
- expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader|
- expect(loader).to receive(:load_raw!).and_call_original
+ describe '#errors' do
+ it 'returns validation errors' do
+ expect(viewer.errors).to all be_kind_of String
end
- expect(PerformanceMonitoring::PrometheusDashboard)
- .to receive(:from_json)
- .with(yml)
- .and_call_original
- expect(viewer.valid?).to be_truthy
end
end
- describe '#errors' do
- it 'returns nil' do
- allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json)
+ context 'when YAML loader raises error' do
+ let(:data) do
+ <<~YAML
+ large yaml file
+ YAML
+ end
+
+ before do
+ allow(::Gitlab::Config::Loader::Yaml).to receive(:new)
+ .and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big')
+ end
- expect(viewer.errors).to be nil
+ it 'is invalid' do
+ expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors)
+ expect(viewer.valid?).to be false
+ end
+
+ it 'returns validation errors' do
+ expect(viewer.errors).to eq ['The parsed YAML is too big']
end
end
end
- context 'when definition is invalid' do
- let(:error) { ActiveModel::ValidationError.new(PerformanceMonitoring::PrometheusDashboard.new.tap(&:validate)) }
- let(:data) do
- <<~YAML
- dashboard:
- YAML
+ context 'with metrics_dashboard_exhaustive_validations feature flag off' do
+ before do
+ stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
end
- describe '#valid?' do
- it 'returns false' do
- expect(PerformanceMonitoring::PrometheusDashboard)
- .to receive(:from_json).and_raise(error)
+ context 'when the definition is valid' do
+ describe '#valid?' do
+ before do
+ allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json)
+ end
+
+ it 'calls prepare! on the viewer' do
+ expect(viewer).to receive(:prepare!)
- expect(viewer.valid?).to be_falsey
+ viewer.valid?
+ end
+
+ it 'processes dashboard yaml and returns true', :aggregate_failures do
+ yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw!
+
+ expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader|
+ expect(loader).to receive(:load_raw!).and_call_original
+ end
+ expect(PerformanceMonitoring::PrometheusDashboard)
+ .to receive(:from_json)
+ .with(yml)
+ .and_call_original
+ expect(viewer.valid?).to be true
+ end
+ end
+
+ describe '#errors' do
+ it 'returns empty array' do
+ expect(viewer.errors).to eq []
+ end
end
end
- describe '#errors' do
- it 'returns validation errors' do
- allow(PerformanceMonitoring::PrometheusDashboard)
- .to receive(:from_json).and_raise(error)
+ context 'when definition is invalid' do
+ let(:error) { ActiveModel::ValidationError.new(PerformanceMonitoring::PrometheusDashboard.new.tap(&:validate)) }
+ let(:data) do
+ <<~YAML
+ dashboard:
+ YAML
+ end
+
+ describe '#valid?' do
+ it 'returns false' do
+ expect(PerformanceMonitoring::PrometheusDashboard)
+ .to receive(:from_json).and_raise(error)
- expect(viewer.errors).to be error.model.errors
+ expect(viewer.valid?).to be false
+ end
+ end
+
+ describe '#errors' do
+ it 'returns validation errors' do
+ allow(PerformanceMonitoring::PrometheusDashboard)
+ .to receive(:from_json).and_raise(error)
+
+ expect(viewer.errors).to eq error.model.errors.messages.map { |messages| messages.join(': ') }
+ end
end
end
- end
- context 'when YAML syntax is invalid' do
- let(:data) do
- <<~YAML
+ context 'when YAML syntax is invalid' do
+ let(:data) do
+ <<~YAML
dashboard: 'empty metrics'
panel_groups:
- group: 'Group Title'
- YAML
- end
-
- describe '#valid?' do
- it 'returns false' do
- expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
- expect(viewer.valid?).to be_falsey
+ YAML
end
- end
- describe '#errors' do
- it 'returns validation errors' do
- yaml_wrapped_errors = { 'YAML syntax': ["(<unknown>): did not find expected key while parsing a block mapping at line 1 column 1"] }
+ describe '#valid?' do
+ it 'returns false' do
+ expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
+ expect(viewer.valid?).to be false
+ end
+ end
- expect(viewer.errors).to be_kind_of ActiveModel::Errors
- expect(viewer.errors.messages).to eql(yaml_wrapped_errors)
+ describe '#errors' do
+ it 'returns validation errors' do
+ expect(viewer.errors).to eq ["YAML syntax: (<unknown>): did not find expected key while parsing a block mapping at line 1 column 1"]
+ end
end
end
- end
- context 'when YAML loader raises error' do
- let(:data) do
- <<~YAML
+ context 'when YAML loader raises error' do
+ let(:data) do
+ <<~YAML
large yaml file
- YAML
- end
-
- before do
- allow(::Gitlab::Config::Loader::Yaml).to receive(:new)
- .and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big')
- end
+ YAML
+ end
- it 'is invalid' do
- expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
- expect(viewer.valid?).to be(false)
- end
+ before do
+ allow(::Gitlab::Config::Loader::Yaml).to(
+ receive(:new).and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big')
+ )
+ end
- it 'returns validation errors' do
- yaml_wrapped_errors = { 'YAML syntax': ["The parsed YAML is too big"] }
+ it 'is invalid' do
+ expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
+ expect(viewer.valid?).to be false
+ end
- expect(viewer.errors).to be_kind_of(ActiveModel::Errors)
- expect(viewer.errors.messages).to eq(yaml_wrapped_errors)
+ it 'returns validation errors' do
+ expect(viewer.errors).to eq ["YAML syntax: The parsed YAML is too big"]
+ end
end
end
end
diff --git a/spec/models/board_group_recent_visit_spec.rb b/spec/models/board_group_recent_visit_spec.rb
index 4d16e1ff839..c6fbd263072 100644
--- a/spec/models/board_group_recent_visit_spec.rb
+++ b/spec/models/board_group_recent_visit_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe BoardGroupRecentVisit do
let!(:visit) { create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago }
it 'updates the timestamp' do
- Timecop.freeze do
+ freeze_time do
described_class.visited!(user, board)
expect(described_class.count).to eq 1
diff --git a/spec/models/board_project_recent_visit_spec.rb b/spec/models/board_project_recent_visit_spec.rb
index 8e74405fd8c..145a4f5b1a7 100644
--- a/spec/models/board_project_recent_visit_spec.rb
+++ b/spec/models/board_project_recent_visit_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe BoardProjectRecentVisit do
let!(:visit) { create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago }
it 'updates the timestamp' do
- Timecop.freeze do
+ freeze_time do
described_class.visited!(user, board)
expect(described_class.count).to eq 1
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index 3a459e5897a..850fc1ec6e6 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -50,6 +50,7 @@ RSpec.describe Ci::Bridge do
CI_PROJECT_PATH_SLUG CI_PROJECT_NAMESPACE CI_PROJECT_ROOT_NAMESPACE
CI_PIPELINE_IID CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE
CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION CI_COMMIT_REF_PROTECTED
+ CI_COMMIT_TIMESTAMP
]
expect(bridge.scoped_variables_hash.keys).to include(*variables)
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index e4d71632957..069864fa765 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe Ci::BuildMetadata do
context 'when both runner and job timeouts are set' do
before do
- build.update(runner: runner)
+ build.update!(runner: runner)
end
context 'when job timeout is higher than runner timeout' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 069ac23c5a4..1e551d9ee33 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -25,6 +25,7 @@ RSpec.describe Ci::Build do
it { is_expected.to have_many(:sourced_pipelines) }
it { is_expected.to have_many(:job_variables) }
it { is_expected.to have_many(:report_results) }
+ it { is_expected.to have_many(:pages_deployments) }
it { is_expected.to have_one(:deployment) }
it { is_expected.to have_one(:runner_session) }
@@ -448,7 +449,7 @@ RSpec.describe Ci::Build do
end
it 'schedules BuildScheduleWorker at the right time' do
- Timecop.freeze do
+ freeze_time do
expect(Ci::BuildScheduleWorker)
.to receive(:perform_at).with(be_like_time(1.minute.since), build.id)
@@ -496,7 +497,7 @@ RSpec.describe Ci::Build do
let(:option) { { start_in: '1 day' } }
it 'returns date after 1 day' do
- Timecop.freeze do
+ freeze_time do
is_expected.to eq(1.day.since)
end
end
@@ -506,7 +507,7 @@ RSpec.describe Ci::Build do
let(:option) { { start_in: '1 week' } }
it 'returns date after 1 week' do
- Timecop.freeze do
+ freeze_time do
is_expected.to eq(1.week.since)
end
end
@@ -566,18 +567,18 @@ RSpec.describe Ci::Build do
let(:runner) { create(:ci_runner, :project, projects: [build.project]) }
before do
- runner.update(contacted_at: 1.second.ago)
+ runner.update!(contacted_at: 1.second.ago)
end
it { is_expected.to be_truthy }
it 'that is inactive' do
- runner.update(active: false)
+ runner.update!(active: false)
is_expected.to be_falsey
end
it 'that is not online' do
- runner.update(contacted_at: nil)
+ runner.update!(contacted_at: nil)
is_expected.to be_falsey
end
@@ -612,6 +613,46 @@ RSpec.describe Ci::Build do
end
end
+ describe '#locked_artifacts?' do
+ subject(:locked_artifacts) { build.locked_artifacts? }
+
+ context 'when pipeline is artifacts_locked' do
+ before do
+ build.pipeline.artifacts_locked!
+ end
+
+ context 'artifacts archive does not exist' do
+ let(:build) { create(:ci_build) }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'artifacts archive exists' do
+ let(:build) { create(:ci_build, :artifacts) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when pipeline is unlocked' do
+ before do
+ build.pipeline.unlocked!
+ end
+
+ context 'artifacts archive does not exist' do
+ let(:build) { create(:ci_build) }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'artifacts archive exists' do
+ let(:build) { create(:ci_build, :artifacts) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+ end
+
describe '#available_artifacts?' do
let(:build) { create(:ci_build) }
@@ -683,7 +724,7 @@ RSpec.describe Ci::Build do
context 'is expired' do
before do
- build.update(artifacts_expire_at: Time.current - 7.days)
+ build.update!(artifacts_expire_at: Time.current - 7.days)
end
it { is_expected.to be_truthy }
@@ -691,7 +732,7 @@ RSpec.describe Ci::Build do
context 'is not expired' do
before do
- build.update(artifacts_expire_at: Time.current + 7.days)
+ build.update!(artifacts_expire_at: Time.current + 7.days)
end
it { is_expected.to be_falsey }
@@ -1012,18 +1053,53 @@ RSpec.describe Ci::Build do
end
describe '#hide_secrets' do
+ let(:metrics) { spy('metrics') }
let(:subject) { build.hide_secrets(data) }
context 'hide runners token' do
let(:data) { "new #{project.runners_token} data"}
it { is_expected.to match(/^new x+ data$/) }
+
+ it 'increments trace mutation metric' do
+ build.hide_secrets(data, metrics)
+
+ expect(metrics)
+ .to have_received(:increment_trace_operation)
+ .with(operation: :mutated)
+ end
end
context 'hide build token' do
let(:data) { "new #{build.token} data"}
it { is_expected.to match(/^new x+ data$/) }
+
+ it 'increments trace mutation metric' do
+ build.hide_secrets(data, metrics)
+
+ expect(metrics)
+ .to have_received(:increment_trace_operation)
+ .with(operation: :mutated)
+ end
+ end
+
+ context 'when build does not include secrets' do
+ let(:data) { 'my build log' }
+
+ it 'does not mutate trace' do
+ trace = build.hide_secrets(data)
+
+ expect(trace).to eq data
+ end
+
+ it 'does not increment trace mutation metric' do
+ build.hide_secrets(data, metrics)
+
+ expect(metrics)
+ .not_to have_received(:increment_trace_operation)
+ .with(operation: :mutated)
+ end
end
end
@@ -1200,7 +1276,7 @@ RSpec.describe Ci::Build do
context 'when environment is defined' do
before do
- build.update(environment: 'review')
+ build.update!(environment: 'review')
end
it { is_expected.to be_truthy }
@@ -1208,7 +1284,7 @@ RSpec.describe Ci::Build do
context 'when environment is not defined' do
before do
- build.update(environment: nil)
+ build.update!(environment: nil)
end
it { is_expected.to be_falsey }
@@ -1316,7 +1392,7 @@ RSpec.describe Ci::Build do
context 'when environment is defined' do
before do
- build.update(environment: 'review')
+ build.update!(environment: 'review')
end
context 'no action is defined' do
@@ -1325,7 +1401,7 @@ RSpec.describe Ci::Build do
context 'and start action is defined' do
before do
- build.update(options: { environment: { action: 'start' } } )
+ build.update!(options: { environment: { action: 'start' } } )
end
it { is_expected.to be_truthy }
@@ -1334,7 +1410,7 @@ RSpec.describe Ci::Build do
context 'when environment is not defined' do
before do
- build.update(environment: nil)
+ build.update!(environment: nil)
end
it { is_expected.to be_falsey }
@@ -1346,7 +1422,7 @@ RSpec.describe Ci::Build do
context 'when environment is defined' do
before do
- build.update(environment: 'review')
+ build.update!(environment: 'review')
end
context 'no action is defined' do
@@ -1355,7 +1431,7 @@ RSpec.describe Ci::Build do
context 'and stop action is defined' do
before do
- build.update(options: { environment: { action: 'stop' } } )
+ build.update!(options: { environment: { action: 'stop' } } )
end
it { is_expected.to be_truthy }
@@ -1364,7 +1440,7 @@ RSpec.describe Ci::Build do
context 'when environment is not defined' do
before do
- build.update(environment: nil)
+ build.update!(environment: nil)
end
it { is_expected.to be_falsey }
@@ -1687,7 +1763,7 @@ RSpec.describe Ci::Build do
describe '#action?' do
before do
- build.update(when: value)
+ build.update!(when: value)
end
subject { build.action? }
@@ -2232,7 +2308,7 @@ RSpec.describe Ci::Build do
describe '#has_expiring_archive_artifacts?' do
context 'when artifacts have expiration date set' do
before do
- build.update(artifacts_expire_at: 1.day.from_now)
+ build.update!(artifacts_expire_at: 1.day.from_now)
end
context 'and job artifacts archive record exists' do
@@ -2252,7 +2328,7 @@ RSpec.describe Ci::Build do
context 'when artifacts do not have expiration date set' do
before do
- build.update(artifacts_expire_at: nil)
+ build.update!(artifacts_expire_at: nil)
end
it 'does not have expiring artifacts' do
@@ -2329,6 +2405,7 @@ RSpec.describe Ci::Build do
{ key: 'CI_COMMIT_TITLE', value: pipeline.git_commit_title, public: true, masked: false },
{ key: 'CI_COMMIT_DESCRIPTION', value: pipeline.git_commit_description, public: true, masked: false },
{ key: 'CI_COMMIT_REF_PROTECTED', value: (!!pipeline.protected_ref?).to_s, public: true, masked: false },
+ { key: 'CI_COMMIT_TIMESTAMP', value: pipeline.git_commit_timestamp, public: true, masked: false },
{ key: 'CI_BUILD_REF', value: build.sha, public: true, masked: false },
{ key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true, masked: false },
{ key: 'CI_BUILD_REF_NAME', value: build.ref, public: true, masked: false },
@@ -2526,7 +2603,7 @@ RSpec.describe Ci::Build do
end
before do
- build.update(user: user)
+ build.update!(user: user)
end
it { user_variables.each { |v| is_expected.to include(v) } }
@@ -2562,7 +2639,7 @@ RSpec.describe Ci::Build do
end
before do
- build.update(environment: 'production')
+ build.update!(environment: 'production')
end
shared_examples 'containing environment variables' do
@@ -2589,7 +2666,7 @@ RSpec.describe Ci::Build do
context 'when the URL was set from the job' do
before do
- build.update(options: { environment: { url: url } })
+ build.update!(options: { environment: { url: url } })
end
it_behaves_like 'containing environment variables'
@@ -2607,7 +2684,7 @@ RSpec.describe Ci::Build do
context 'when the URL was not set from the job, but environment' do
before do
- environment.update(external_url: url)
+ environment.update!(external_url: url)
end
it_behaves_like 'containing environment variables'
@@ -2643,7 +2720,7 @@ RSpec.describe Ci::Build do
context 'when build started manually' do
before do
- build.update(when: :manual)
+ build.update!(when: :manual)
end
let(:manual_variable) do
@@ -2669,8 +2746,8 @@ RSpec.describe Ci::Build do
end
before do
- build.update(tag: false)
- pipeline.update(tag: false)
+ build.update!(tag: false)
+ pipeline.update!(tag: false)
end
it { is_expected.to include(branch_variable) }
@@ -2682,8 +2759,8 @@ RSpec.describe Ci::Build do
end
before do
- build.update(tag: true)
- pipeline.update(tag: true)
+ build.update!(tag: true)
+ pipeline.update!(tag: true)
end
it { is_expected.to include(tag_variable) }
@@ -2860,7 +2937,7 @@ RSpec.describe Ci::Build do
context 'and is disabled for project' do
before do
- project.update(container_registry_enabled: false)
+ project.update!(container_registry_enabled: false)
end
it { is_expected.to include(ci_registry) }
@@ -2869,7 +2946,7 @@ RSpec.describe Ci::Build do
context 'and is enabled for project' do
before do
- project.update(container_registry_enabled: true)
+ project.update!(container_registry_enabled: true)
end
it { is_expected.to include(ci_registry) }
@@ -2881,7 +2958,7 @@ RSpec.describe Ci::Build do
let(:runner) { create(:ci_runner, description: 'description', tag_list: %w(docker linux)) }
before do
- build.update(runner: runner)
+ build.update!(runner: runner)
end
it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true, masked: false }) }
@@ -3685,7 +3762,7 @@ RSpec.describe Ci::Build do
subject { described_class.where(id: build).matches_tag_ids(tag_ids) }
before do
- build.update(tag_list: build_tag_list)
+ build.update!(tag_list: build_tag_list)
end
context 'when have different tags' do
@@ -3731,7 +3808,7 @@ RSpec.describe Ci::Build do
subject { described_class.where(id: build).with_any_tags }
before do
- build.update(tag_list: tag_list)
+ build.update!(tag_list: tag_list)
end
context 'when does have tags' do
@@ -4087,7 +4164,7 @@ RSpec.describe Ci::Build do
let(:path) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' }
around do |example|
- Timecop.freeze { example.run }
+ freeze_time { example.run }
end
before do
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index a6362d46449..fefe5e3bfca 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -21,6 +21,25 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
stub_artifacts_object_storage
end
+ describe 'chunk creation' do
+ let(:metrics) { spy('metrics') }
+
+ before do
+ allow(::Gitlab::Ci::Trace::Metrics)
+ .to receive(:new)
+ .and_return(metrics)
+ end
+
+ it 'increments trace operation chunked metric' do
+ build_trace_chunk.save!
+
+ expect(metrics)
+ .to have_received(:increment_trace_operation)
+ .with(operation: :chunked)
+ .once
+ end
+ end
+
context 'FastDestroyAll' do
let(:parent) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: parent) }
@@ -32,7 +51,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(external_data_counter).to be > 0
expect(subjects.count).to be > 0
- expect { subjects.first.destroy }.to raise_error('`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`')
+ expect { subjects.first.destroy! }.to raise_error('`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`')
expect { subjects.destroy_all }.to raise_error('`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`') # rubocop: disable Cop/DestroyAll
expect(subjects.count).to be > 0
@@ -57,7 +76,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(external_data_counter).to be > 0
expect(subjects.count).to be > 0
- expect { parent.destroy }.not_to raise_error
+ expect { parent.destroy! }.not_to raise_error
expect(subjects.count).to eq(0)
expect(external_data_counter).to eq(0)
@@ -222,6 +241,8 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
subject
build_trace_chunk.reload
+
+ expect(build_trace_chunk.checksum).to be_present
expect(build_trace_chunk.fog?).to be_truthy
expect(build_trace_chunk.data).to eq(new_data)
end
@@ -344,6 +365,24 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
it_behaves_like 'Scheduling no sidekiq worker'
end
end
+
+ describe 'append metrics' do
+ let(:metrics) { spy('metrics') }
+
+ before do
+ allow(::Gitlab::Ci::Trace::Metrics)
+ .to receive(:new)
+ .and_return(metrics)
+ end
+
+ it 'increments trace operation appended metric' do
+ build_trace_chunk.append('123456', 0)
+
+ expect(metrics)
+ .to have_received(:increment_trace_operation)
+ .with(operation: :appended)
+ end
+ end
end
describe '#truncate' do
@@ -461,6 +500,8 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
describe '#persist_data!' do
+ let(:build) { create(:ci_build, :running) }
+
subject { build_trace_chunk.persist_data! }
shared_examples_for 'Atomic operation' do
@@ -502,6 +543,12 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
end
+ it 'calculates CRC32 checksum' do
+ subject
+
+ expect(build_trace_chunk.reload.checksum).to eq '3398914352'
+ end
+
it_behaves_like 'Atomic operation'
end
@@ -516,6 +563,18 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil
end
+
+ context 'when chunk is a final one' do
+ before do
+ create(:ci_build_pending_state, build: build)
+ end
+
+ it 'persists the data' do
+ subject
+
+ expect(build_trace_chunk.fog?).to be_truthy
+ end
+ end
end
end
@@ -565,6 +624,18 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data)
expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil
end
+
+ context 'when chunk is a final one' do
+ before do
+ create(:ci_build_pending_state, build: build)
+ end
+
+ it 'persists the data' do
+ subject
+
+ expect(build_trace_chunk.fog?).to be_truthy
+ end
+ end
end
end
@@ -614,6 +685,54 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
end
+ describe 'final?' do
+ let(:build) { create(:ci_build, :running) }
+
+ context 'when build pending state exists' do
+ before do
+ create(:ci_build_pending_state, build: build)
+ end
+
+ context 'when chunks is not the last one' do
+ before do
+ create(:ci_build_trace_chunk, chunk_index: 1, build: build)
+ end
+
+ it 'is not a final chunk' do
+ expect(build.reload.pending_state).to be_present
+ expect(build_trace_chunk).not_to be_final
+ end
+ end
+
+ context 'when chunks is the last one' do
+ it 'is a final chunk' do
+ expect(build.reload.pending_state).to be_present
+ expect(build_trace_chunk).to be_final
+ end
+ end
+ end
+
+ context 'when build pending state does not exist' do
+ context 'when chunks is not the last one' do
+ before do
+ create(:ci_build_trace_chunk, chunk_index: 1, build: build)
+ end
+
+ it 'is not a final chunk' do
+ expect(build.reload.pending_state).not_to be_present
+ expect(build_trace_chunk).not_to be_final
+ end
+ end
+
+ context 'when chunks is the last one' do
+ it 'is not a final chunk' do
+ expect(build.reload.pending_state).not_to be_present
+ expect(build_trace_chunk).not_to be_final
+ end
+ end
+ end
+ end
+
describe 'deletes data in redis after a parent record destroyed' do
let(:project) { create(:project) }
diff --git a/spec/models/ci/build_trace_chunks/fog_spec.rb b/spec/models/ci/build_trace_chunks/fog_spec.rb
index a44ae58dfd2..b7e9adab04a 100644
--- a/spec/models/ci/build_trace_chunks/fog_spec.rb
+++ b/spec/models/ci/build_trace_chunks/fog_spec.rb
@@ -46,9 +46,7 @@ RSpec.describe Ci::BuildTraceChunks::Fog do
end
describe '#set_data' do
- subject { data_store.set_data(model, data) }
-
- let(:data) { 'abc123' }
+ let(:new_data) { 'abc123' }
context 'when data exists' do
let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') }
@@ -56,9 +54,9 @@ RSpec.describe Ci::BuildTraceChunks::Fog do
it 'overwrites data' do
expect(data_store.data(model)).to eq('sample data in fog')
- subject
+ data_store.set_data(model, new_data)
- expect(data_store.data(model)).to eq('abc123')
+ expect(data_store.data(model)).to eq new_data
end
end
@@ -68,9 +66,9 @@ RSpec.describe Ci::BuildTraceChunks::Fog do
it 'sets new data' do
expect(data_store.data(model)).to be_nil
- subject
+ data_store.set_data(model, new_data)
- expect(data_store.data(model)).to eq('abc123')
+ expect(data_store.data(model)).to eq new_data
end
end
end
diff --git a/spec/models/ci/instance_variable_spec.rb b/spec/models/ci/instance_variable_spec.rb
index 15d0c911bc4..0ef1dfbd55c 100644
--- a/spec/models/ci/instance_variable_spec.rb
+++ b/spec/models/ci/instance_variable_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Ci::InstanceVariable do
let(:value) { SecureRandom.alphanumeric(10_002) }
it 'raises a database level error' do
- expect { variable.save }.to raise_error(ActiveRecord::StatementInvalid)
+ expect { variable.save! }.to raise_error(ActiveRecord::StatementInvalid)
end
end
@@ -36,7 +36,7 @@ RSpec.describe Ci::InstanceVariable do
let(:value) { SecureRandom.alphanumeric(10_000) }
it 'does not raise database level error' do
- expect { variable.save }.not_to raise_error
+ expect { variable.save! }.not_to raise_error
end
end
end
@@ -85,7 +85,7 @@ RSpec.describe Ci::InstanceVariable do
it 'resets the cache when records are deleted' do
expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
- protected_variable.destroy
+ protected_variable.destroy!
expect(described_class.all_cached).to contain_exactly(unprotected_variable)
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 91a669aa3f4..779839df670 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -20,7 +20,9 @@ RSpec.describe Ci::JobArtifact do
it_behaves_like 'having unique enum values'
it_behaves_like 'UpdateProjectStatistics' do
- subject { build(:ci_job_artifact, :archive, size: 107464) }
+ let_it_be(:job, reload: true) { create(:ci_build) }
+
+ subject { build(:ci_job_artifact, :archive, job: job, size: 107464) }
end
describe '.not_expired' do
@@ -343,42 +345,6 @@ RSpec.describe Ci::JobArtifact do
end
end
- describe '#each_blob' do
- context 'when file format is gzip' do
- context 'when gzip file contains one file' do
- let(:artifact) { build(:ci_job_artifact, :junit) }
-
- it 'iterates blob once' do
- expect { |b| artifact.each_blob(&b) }.to yield_control.once
- end
- end
-
- context 'when gzip file contains three files' do
- let(:artifact) { build(:ci_job_artifact, :junit_with_three_testsuites) }
-
- it 'iterates blob three times' do
- expect { |b| artifact.each_blob(&b) }.to yield_control.exactly(3).times
- end
- end
- end
-
- context 'when file format is raw' do
- let(:artifact) { build(:ci_job_artifact, :codequality, file_format: :raw) }
-
- it 'iterates blob once' do
- expect { |b| artifact.each_blob(&b) }.to yield_control.once
- end
- end
-
- context 'when there are no adapters for the file format' do
- let(:artifact) { build(:ci_job_artifact, :junit, file_format: :zip) }
-
- it 'raises an error' do
- expect { |b| artifact.each_blob(&b) }.to raise_error(described_class::NotSupportedAdapterError)
- end
- end
- end
-
describe 'expired?' do
subject { artifact.expired? }
diff --git a/spec/models/ci/legacy_stage_spec.rb b/spec/models/ci/legacy_stage_spec.rb
index c53f6abb037..2487ad85889 100644
--- a/spec/models/ci/legacy_stage_spec.rb
+++ b/spec/models/ci/legacy_stage_spec.rb
@@ -116,7 +116,7 @@ RSpec.describe Ci::LegacyStage do
let!(:new_build) { create_job(:ci_build, status: :success) }
before do
- stage_build.update(retried: true)
+ stage_build.update!(retried: true)
end
it "returns status of latest build" do
diff --git a/spec/models/ci/persistent_ref_spec.rb b/spec/models/ci/persistent_ref_spec.rb
index 18552317025..e488580ae7b 100644
--- a/spec/models/ci/persistent_ref_spec.rb
+++ b/spec/models/ci/persistent_ref_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Ci::PersistentRef do
context 'when a persistent ref exists' do
before do
- pipeline.persistent_ref.create
+ pipeline.persistent_ref.create # rubocop: disable Rails/SaveBang
end
it { is_expected.to eq(true) }
@@ -32,7 +32,7 @@ RSpec.describe Ci::PersistentRef do
end
describe '#create' do
- subject { pipeline.persistent_ref.create }
+ subject { pipeline.persistent_ref.create } # rubocop: disable Rails/SaveBang
let(:pipeline) { create(:ci_pipeline, sha: sha, project: project) }
let(:project) { create(:project, :repository) }
@@ -58,7 +58,7 @@ RSpec.describe Ci::PersistentRef do
context 'when a persistent ref already exists' do
before do
- pipeline.persistent_ref.create
+ pipeline.persistent_ref.create # rubocop: disable Rails/SaveBang
end
it 'overwrites a persistent ref' do
@@ -78,7 +78,7 @@ RSpec.describe Ci::PersistentRef do
context 'when a persistent ref exists' do
before do
- pipeline.persistent_ref.create
+ pipeline.persistent_ref.create # rubocop: disable Rails/SaveBang
end
it 'deletes the ref' do
diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb
index 9d63d74a6cc..8cbace845a9 100644
--- a/spec/models/ci/pipeline_artifact_spec.rb
+++ b/spec/models/ci/pipeline_artifact_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::PipelineArtifact, type: :model do
- let_it_be(:coverage_report) { create(:ci_pipeline_artifact) }
+ let(:coverage_report) { create(:ci_pipeline_artifact) }
describe 'associations' do
it { is_expected.to belong_to(:pipeline) }
@@ -12,6 +12,12 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
it_behaves_like 'having unique enum values'
+ it_behaves_like 'UpdateProjectStatistics' do
+ let_it_be(:pipeline, reload: true) { create(:ci_pipeline) }
+
+ subject { build(:ci_pipeline_artifact, pipeline: pipeline) }
+ end
+
describe 'validations' do
it { is_expected.to validate_presence_of(:pipeline) }
it { is_expected.to validate_presence_of(:project) }
@@ -44,38 +50,74 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
end
end
- describe '#set_size' do
+ describe 'file is being stored' do
subject { create(:ci_pipeline_artifact) }
- context 'when file is being created' do
- it 'sets the size' do
- expect(subject.size).to eq(85)
+ 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
- context 'when file is being updated' do
- it 'updates the size' do
- subject.update!(file: fixture_file_upload('spec/fixtures/dk.png'))
+ context 'when file contains multi-byte characters' do
+ let(:coverage_report_multibyte) { create(:ci_pipeline_artifact, :with_multibyte_characters) }
- expect(subject.size).to eq(1062)
+ it 'sets the size in bytesize' do
+ expect(coverage_report_multibyte.size).to eq(14)
end
end
end
- describe 'file is being stored' do
- subject { create(:ci_pipeline_artifact) }
+ describe '.has_code_coverage?' do
+ subject { Ci::PipelineArtifact.has_code_coverage? }
- context 'when existing object has local store' do
- it_behaves_like 'mounted file in local store'
+ context 'when pipeline artifact has a code coverage' do
+ let!(:pipeline_artifact) { create(:ci_pipeline_artifact) }
+
+ it 'returns true' do
+ expect(subject).to be_truthy
+ end
end
- context 'when direct upload is enabled' do
- before do
- stub_artifacts_object_storage(Ci::PipelineArtifactUploader, direct_upload: true)
+ context 'when pipeline artifact does not have a code coverage' do
+ it 'returns false' do
+ expect(subject).to be_falsey
end
+ end
+ end
- context 'when file is stored' do
- it_behaves_like 'mounted file in object store'
+ describe '.find_with_code_coverage' do
+ subject { Ci::PipelineArtifact.find_with_code_coverage }
+
+ context 'when pipeline artifact has a coverage report' do
+ let!(:coverage_report) { create(:ci_pipeline_artifact) }
+
+ it 'returns a pipeline artifact with a code coverage' do
+ expect(subject.file_type).to eq('code_coverage')
+ end
+ end
+
+ context 'when pipeline artifact does not have a coverage report' do
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+ end
+
+ describe '#present' do
+ subject { coverage_report.present }
+
+ context 'when file_type is code_coverage' do
+ it 'uses code coverage presenter' do
+ expect(subject.present).to be_kind_of(Ci::PipelineArtifacts::CodeCoveragePresenter)
end
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index b4e80fa7588..228a1e8f7a2 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -2,12 +2,14 @@
require 'spec_helper'
-RSpec.describe Ci::Pipeline, :mailer do
+RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
include ProjectForksHelper
include StubRequests
+ include Ci::SourcePipelineHelpers
- let(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:namespace) { create_default(:namespace) }
+ let_it_be(:project) { create_default(:project, :repository) }
let(:pipeline) do
create(:ci_empty_pipeline, status: :created, project: project)
@@ -220,6 +222,26 @@ RSpec.describe Ci::Pipeline, :mailer do
end
end
+ describe '.ci_sources' do
+ subject { described_class.ci_sources }
+
+ let!(:push_pipeline) { create(:ci_pipeline, source: :push) }
+ let!(:web_pipeline) { create(:ci_pipeline, source: :web) }
+ let!(:api_pipeline) { create(:ci_pipeline, source: :api) }
+ let!(:webide_pipeline) { create(:ci_pipeline, source: :webide) }
+ let!(:child_pipeline) { create(:ci_pipeline, source: :parent_pipeline) }
+
+ it 'contains pipelines having CI only sources' do
+ expect(subject).to contain_exactly(push_pipeline, web_pipeline, api_pipeline)
+ end
+
+ it 'filters on expected sources' do
+ expect(::Enums::Ci::Pipeline.ci_sources.keys).to contain_exactly(
+ *%i[unknown push web trigger schedule api external pipeline chat
+ merge_request_event external_pull_request_event])
+ end
+ end
+
describe '.outside_pipeline_family' do
subject(:outside_pipeline_family) { described_class.outside_pipeline_family(upstream_pipeline) }
@@ -714,6 +736,7 @@ RSpec.describe Ci::Pipeline, :mailer do
CI_COMMIT_TITLE
CI_COMMIT_DESCRIPTION
CI_COMMIT_REF_PROTECTED
+ CI_COMMIT_TIMESTAMP
CI_BUILD_REF
CI_BUILD_BEFORE_SHA
CI_BUILD_REF_NAME
@@ -879,7 +902,7 @@ RSpec.describe Ci::Pipeline, :mailer do
context 'when there is auto_canceled_by' do
before do
- pipeline.update(auto_canceled_by: create(:ci_empty_pipeline))
+ pipeline.update!(auto_canceled_by: create(:ci_empty_pipeline))
end
it 'is auto canceled' do
@@ -1237,14 +1260,42 @@ RSpec.describe Ci::Pipeline, :mailer do
end
describe 'pipeline caching' do
- before do
- pipeline.config_source = 'repository_source'
+ context 'if pipeline is cacheable' do
+ before do
+ pipeline.source = 'push'
+ end
+
+ it 'performs ExpirePipelinesCacheWorker' do
+ expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id)
+
+ pipeline.cancel
+ end
end
- it 'performs ExpirePipelinesCacheWorker' do
- expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id)
+ context 'if pipeline is not cacheable' do
+ before do
+ pipeline.source = 'webide'
+ end
- pipeline.cancel
+ it 'deos not perform ExpirePipelinesCacheWorker' do
+ expect(ExpirePipelineCacheWorker).not_to receive(:perform_async)
+
+ pipeline.cancel
+ end
+ end
+ end
+
+ describe '#dangling?' do
+ it 'returns true if pipeline comes from any dangling sources' do
+ pipeline.source = Enums::Ci::Pipeline.dangling_sources.each_key.first
+
+ expect(pipeline).to be_dangling
+ end
+
+ it 'returns true if pipeline comes from any CI sources' do
+ pipeline.source = Enums::Ci::Pipeline.ci_sources.each_key.first
+
+ expect(pipeline).not_to be_dangling
end
end
@@ -1309,34 +1360,126 @@ RSpec.describe Ci::Pipeline, :mailer do
end
end
- context 'when pipeline is bridge triggered' do
- before do
- pipeline.source_bridge = create(:ci_bridge)
- end
+ def auto_devops_pipelines_completed_total(status)
+ Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines').get(status: status)
+ end
+ end
+
+ describe 'bridge triggered pipeline' do
+ shared_examples 'upstream downstream pipeline' do
+ let!(:source_pipeline) { create(:ci_sources_pipeline, pipeline: downstream_pipeline, source_job: bridge) }
+ let!(:job) { downstream_pipeline.builds.first }
context 'when source bridge is dependent on pipeline status' do
- before do
- allow(pipeline.source_bridge).to receive(:dependent?).and_return(true)
- end
+ let!(:bridge) { create(:ci_bridge, :strategy_depend, pipeline: upstream_pipeline) }
it 'schedules the pipeline bridge worker' do
- expect(::Ci::PipelineBridgeStatusWorker).to receive(:perform_async)
+ expect(::Ci::PipelineBridgeStatusWorker).to receive(:perform_async).with(downstream_pipeline.id)
+
+ downstream_pipeline.succeed!
+ end
+
+ context 'when the downstream pipeline first fails then retries and succeeds' do
+ it 'makes the upstream pipeline successful' do
+ Sidekiq::Testing.inline! { job.drop! }
+
+ expect(downstream_pipeline.reload).to be_failed
+ expect(upstream_pipeline.reload).to be_failed
+
+ Sidekiq::Testing.inline! do
+ new_job = Ci::Build.retry(job, project.users.first)
+
+ expect(downstream_pipeline.reload).to be_running
+ expect(upstream_pipeline.reload).to be_running
+
+ new_job.success!
+ end
+
+ expect(downstream_pipeline.reload).to be_success
+ expect(upstream_pipeline.reload).to be_success
+ end
+ end
+
+ context 'when the downstream pipeline first succeeds then retries and fails' do
+ it 'makes the upstream pipeline failed' do
+ Sidekiq::Testing.inline! { job.success! }
+
+ expect(downstream_pipeline.reload).to be_success
+ expect(upstream_pipeline.reload).to be_success
+
+ Sidekiq::Testing.inline! do
+ new_job = Ci::Build.retry(job, project.users.first)
+
+ expect(downstream_pipeline.reload).to be_running
+ expect(upstream_pipeline.reload).to be_running
+
+ new_job.drop!
+ end
+
+ expect(downstream_pipeline.reload).to be_failed
+ expect(upstream_pipeline.reload).to be_failed
+ end
+ end
+
+ context 'when the upstream pipeline has another dependent upstream pipeline' do
+ let!(:upstream_of_upstream_pipeline) { create(:ci_pipeline) }
+
+ before do
+ upstream_bridge = create(:ci_bridge, :strategy_depend, pipeline: upstream_of_upstream_pipeline)
+ create(:ci_sources_pipeline, pipeline: upstream_pipeline,
+ source_job: upstream_bridge)
+ end
+
+ context 'when the downstream pipeline first fails then retries and succeeds' do
+ it 'makes upstream pipelines successful' do
+ Sidekiq::Testing.inline! { job.drop! }
+
+ expect(downstream_pipeline.reload).to be_failed
+ expect(upstream_pipeline.reload).to be_failed
+ expect(upstream_of_upstream_pipeline.reload).to be_failed
- pipeline.succeed!
+ Sidekiq::Testing.inline! do
+ new_job = Ci::Build.retry(job, project.users.first)
+
+ expect(downstream_pipeline.reload).to be_running
+ expect(upstream_pipeline.reload).to be_running
+ expect(upstream_of_upstream_pipeline.reload).to be_running
+
+ new_job.success!
+ end
+
+ expect(downstream_pipeline.reload).to be_success
+ expect(upstream_pipeline.reload).to be_success
+ expect(upstream_of_upstream_pipeline.reload).to be_success
+ end
+ end
end
end
context 'when source bridge is not dependent on pipeline status' do
+ let!(:bridge) { create(:ci_bridge, pipeline: upstream_pipeline) }
+
it 'does not schedule the pipeline bridge worker' do
expect(::Ci::PipelineBridgeStatusWorker).not_to receive(:perform_async)
- pipeline.succeed!
+ downstream_pipeline.succeed!
end
end
end
- def auto_devops_pipelines_completed_total(status)
- Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines').get(status: status)
+ context 'multi-project pipelines' do
+ let!(:downstream_project) { create(:project, :repository) }
+ let!(:upstream_pipeline) { create(:ci_pipeline, project: project) }
+ let!(:downstream_pipeline) { create(:ci_pipeline, :with_job, project: downstream_project) }
+
+ it_behaves_like 'upstream downstream pipeline'
+ end
+
+ context 'parent-child pipelines' do
+ let!(:upstream_pipeline) { create(:ci_pipeline, project: project) }
+ let!(:downstream_pipeline) { create(:ci_pipeline, :with_job, project: project) }
+
+ it_behaves_like 'upstream downstream pipeline'
end
end
@@ -1436,8 +1579,6 @@ RSpec.describe Ci::Pipeline, :mailer do
context 'when repository exists' do
using RSpec::Parameterized::TableSyntax
- let(:project) { create(:project, :repository) }
-
where(:tag, :ref, :result) do
false | 'master' | true
false | 'non-existent-branch' | false
@@ -1457,6 +1598,7 @@ RSpec.describe Ci::Pipeline, :mailer do
end
context 'when repository does not exist' do
+ let(:project) { create(:project) }
let(:pipeline) do
create(:ci_empty_pipeline, project: project, ref: 'master')
end
@@ -1468,8 +1610,6 @@ RSpec.describe Ci::Pipeline, :mailer do
end
context 'with non-empty project' do
- let(:project) { create(:project, :repository) }
-
let(:pipeline) do
create(:ci_pipeline,
project: project,
@@ -1524,7 +1664,7 @@ RSpec.describe Ci::Pipeline, :mailer do
it 'looks up a commit for a tag' do
expect(project.repository.branch_names).not_to include 'v1.0.0'
- pipeline.update(sha: project.commit('v1.0.0').sha, ref: 'v1.0.0', tag: true)
+ pipeline.update!(sha: project.commit('v1.0.0').sha, ref: 'v1.0.0', tag: true)
expect(pipeline).to be_tag
expect(pipeline).to be_latest
@@ -1533,7 +1673,7 @@ RSpec.describe Ci::Pipeline, :mailer do
context 'with not latest sha' do
before do
- pipeline.update(sha: project.commit("#{project.default_branch}~1").sha)
+ pipeline.update!(sha: project.commit("#{project.default_branch}~1").sha)
end
it 'returns false' do
@@ -1561,7 +1701,7 @@ RSpec.describe Ci::Pipeline, :mailer do
let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }
before do
- manual.update(retried: true)
+ manual.update!(retried: true)
end
it 'returns latest one' do
@@ -1596,10 +1736,8 @@ RSpec.describe Ci::Pipeline, :mailer do
describe '#modified_paths' do
context 'when old and new revisions are set' do
- let(:project) { create(:project, :repository) }
-
before do
- pipeline.update(before_sha: '1234abcd', sha: '2345bcde')
+ pipeline.update!(before_sha: '1234abcd', sha: '2345bcde')
end
it 'fetches stats for changes between commits' do
@@ -1866,8 +2004,6 @@ RSpec.describe Ci::Pipeline, :mailer do
end
describe '.latest_pipeline_per_commit' do
- let(:project) { create(:project) }
-
let!(:commit_123_ref_master) do
create(
:ci_empty_pipeline,
@@ -1962,15 +2098,14 @@ RSpec.describe Ci::Pipeline, :mailer do
end
describe '.last_finished_for_ref_id' do
- let(:project) { create(:project, :repository) }
let(:branch) { project.default_branch }
let(:ref) { project.ci_refs.take }
- let(:config_source) { Ci::PipelineEnums.config_sources[:parameter_source] }
+ let(:dangling_source) { Enums::Ci::Pipeline.sources[:ondemand_dast_scan] }
let!(:pipeline1) { create(:ci_pipeline, :success, project: project, ref: branch) }
let!(:pipeline2) { create(:ci_pipeline, :success, project: project, ref: branch) }
let!(:pipeline3) { create(:ci_pipeline, :failed, project: project, ref: branch) }
let!(:pipeline4) { create(:ci_pipeline, :success, project: project, ref: branch) }
- let!(:pipeline5) { create(:ci_pipeline, :success, project: project, ref: branch, config_source: config_source) }
+ let!(:pipeline5) { create(:ci_pipeline, :success, project: project, ref: branch, source: dangling_source) }
it 'returns the expected pipeline' do
result = described_class.last_finished_for_ref_id(ref.id)
@@ -2452,7 +2587,6 @@ RSpec.describe Ci::Pipeline, :mailer do
end
describe "#merge_requests_as_head_pipeline" do
- let(:project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: 'a288a022a53a5a944fae87bcec6efc87b7061808') }
it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do
@@ -2575,11 +2709,11 @@ RSpec.describe Ci::Pipeline, :mailer do
end
describe '#same_family_pipeline_ids' do
- subject(:same_family_pipeline_ids) { pipeline.same_family_pipeline_ids }
+ subject { pipeline.same_family_pipeline_ids.map(&:id) }
context 'when pipeline is not child nor parent' do
it 'returns just the pipeline id' do
- expect(same_family_pipeline_ids).to contain_exactly(pipeline.id)
+ expect(subject).to contain_exactly(pipeline.id)
end
end
@@ -2588,21 +2722,12 @@ RSpec.describe Ci::Pipeline, :mailer do
let(:sibling) { create(:ci_pipeline, project: pipeline.project) }
before do
- create(:ci_sources_pipeline,
- source_job: create(:ci_build, pipeline: parent),
- source_project: parent.project,
- pipeline: pipeline,
- project: pipeline.project)
-
- create(:ci_sources_pipeline,
- source_job: create(:ci_build, pipeline: parent),
- source_project: parent.project,
- pipeline: sibling,
- project: sibling.project)
+ create_source_pipeline(parent, pipeline)
+ create_source_pipeline(parent, sibling)
end
it 'returns parent sibling and self ids' do
- expect(same_family_pipeline_ids).to contain_exactly(parent.id, pipeline.id, sibling.id)
+ expect(subject).to contain_exactly(parent.id, pipeline.id, sibling.id)
end
end
@@ -2610,15 +2735,43 @@ RSpec.describe Ci::Pipeline, :mailer do
let(:child) { create(:ci_pipeline, project: pipeline.project) }
before do
- create(:ci_sources_pipeline,
- source_job: create(:ci_build, pipeline: pipeline),
- source_project: pipeline.project,
- pipeline: child,
- project: child.project)
+ create_source_pipeline(pipeline, child)
end
it 'returns self and child ids' do
- expect(same_family_pipeline_ids).to contain_exactly(pipeline.id, child.id)
+ expect(subject).to contain_exactly(pipeline.id, child.id)
+ end
+ end
+
+ context 'when pipeline is a child of a child pipeline' do
+ let(:ancestor) { create(:ci_pipeline, project: pipeline.project) }
+ let(:parent) { create(:ci_pipeline, project: pipeline.project) }
+ let(:cousin_parent) { create(:ci_pipeline, project: pipeline.project) }
+ let(:cousin) { create(:ci_pipeline, project: pipeline.project) }
+
+ before do
+ create_source_pipeline(ancestor, parent)
+ create_source_pipeline(ancestor, cousin_parent)
+ create_source_pipeline(parent, pipeline)
+ create_source_pipeline(cousin_parent, cousin)
+ end
+
+ it 'returns all family ids' do
+ expect(subject).to contain_exactly(
+ ancestor.id, parent.id, cousin_parent.id, cousin.id, pipeline.id
+ )
+ end
+ end
+
+ context 'when pipeline is a triggered pipeline' do
+ let(:upstream) { create(:ci_pipeline, project: create(:project)) }
+
+ before do
+ create_source_pipeline(upstream, pipeline)
+ end
+
+ it 'returns self id' do
+ expect(subject).to contain_exactly(pipeline.id)
end
end
end
@@ -2685,7 +2838,8 @@ RSpec.describe Ci::Pipeline, :mailer do
end
describe 'notifications when pipeline success or failed' do
- let(:project) { create(:project, :repository) }
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:project, :repository, namespace: namespace) }
let(:pipeline) do
create(:ci_pipeline,
@@ -2698,7 +2852,7 @@ RSpec.describe Ci::Pipeline, :mailer do
project.add_developer(pipeline.user)
pipeline.user.global_notification_setting
- .update(level: 'custom', failed_pipeline: true, success_pipeline: true)
+ .update!(level: 'custom', failed_pipeline: true, success_pipeline: true)
perform_enqueued_jobs do
pipeline.enqueue
@@ -2948,6 +3102,54 @@ RSpec.describe Ci::Pipeline, :mailer do
end
end
+ describe '#has_coverage_reports?' do
+ subject { pipeline.has_coverage_reports? }
+
+ context 'when pipeline has a code coverage artifact' do
+ let(:pipeline) { create(:ci_pipeline, :with_coverage_report_artifact, :running, project: project) }
+
+ it { expect(subject).to be_truthy }
+ end
+
+ context 'when pipeline does not have a code coverage artifact' do
+ let(:pipeline) { create(:ci_pipeline, :success, project: project) }
+
+ it { expect(subject).to be_falsey }
+ end
+ end
+
+ describe '#can_generate_coverage_reports?' do
+ subject { pipeline.can_generate_coverage_reports? }
+
+ context 'when pipeline has builds with coverage reports' do
+ before do
+ create(:ci_build, :coverage_reports, pipeline: pipeline, project: project)
+ end
+
+ context 'when pipeline status is running' do
+ let(:pipeline) { create(:ci_pipeline, :running, project: project) }
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when pipeline status is success' do
+ let(:pipeline) { create(:ci_pipeline, :success, project: project) }
+
+ it { expect(subject).to be_truthy }
+ end
+ end
+
+ context 'when pipeline does not have builds with coverage reports' do
+ before do
+ create(:ci_build, :artifacts, pipeline: pipeline, project: project)
+ end
+
+ let(:pipeline) { create(:ci_pipeline, :success, project: project) }
+
+ it { expect(subject).to be_falsey }
+ end
+ end
+
describe '#test_report_summary' do
subject { pipeline.test_report_summary }
@@ -3228,7 +3430,8 @@ RSpec.describe Ci::Pipeline, :mailer do
end
describe '#parent_pipeline' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
+
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when pipeline is triggered by a pipeline from the same project' do
@@ -3283,7 +3486,7 @@ RSpec.describe Ci::Pipeline, :mailer do
end
describe '#child_pipelines' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when pipeline triggered other pipelines on same project' do
@@ -3413,4 +3616,152 @@ RSpec.describe Ci::Pipeline, :mailer do
it { is_expected.to eq(Gitlab::Git::TAG_REF_PREFIX + pipeline.source_ref.to_s) }
end
end
+
+ describe "#builds_with_coverage" do
+ it 'returns builds with coverage only' do
+ rspec = create(:ci_build, name: 'rspec', coverage: 97.1, pipeline: pipeline)
+ jest = create(:ci_build, name: 'jest', coverage: 94.1, pipeline: pipeline)
+ karma = create(:ci_build, name: 'karma', coverage: nil, pipeline: pipeline)
+
+ builds = pipeline.builds_with_coverage
+
+ expect(builds).to include(rspec, jest)
+ expect(builds).not_to include(karma)
+ end
+ end
+
+ describe '#base_and_ancestors' do
+ let(:same_project) { false }
+
+ subject { pipeline.base_and_ancestors(same_project: same_project) }
+
+ context 'when pipeline is not child nor parent' do
+ it 'returns just the pipeline itself' do
+ expect(subject).to contain_exactly(pipeline)
+ end
+ end
+
+ context 'when pipeline is child' do
+ let(:parent) { create(:ci_pipeline, project: pipeline.project) }
+ let(:sibling) { create(:ci_pipeline, project: pipeline.project) }
+
+ before do
+ create_source_pipeline(parent, pipeline)
+ create_source_pipeline(parent, sibling)
+ end
+
+ it 'returns parent and self' do
+ expect(subject).to contain_exactly(parent, pipeline)
+ end
+ end
+
+ context 'when pipeline is parent' do
+ let(:child) { create(:ci_pipeline, project: pipeline.project) }
+
+ before do
+ create_source_pipeline(pipeline, child)
+ end
+
+ it 'returns self' do
+ expect(subject).to contain_exactly(pipeline)
+ end
+ end
+
+ context 'when pipeline is a child of a child pipeline' do
+ let(:ancestor) { create(:ci_pipeline, project: pipeline.project) }
+ let(:parent) { create(:ci_pipeline, project: pipeline.project) }
+
+ before do
+ create_source_pipeline(ancestor, parent)
+ create_source_pipeline(parent, pipeline)
+ end
+
+ it 'returns self, parent and ancestor' do
+ expect(subject).to contain_exactly(ancestor, parent, pipeline)
+ end
+ end
+
+ context 'when pipeline is a triggered pipeline' do
+ let(:upstream) { create(:ci_pipeline, project: create(:project)) }
+
+ before do
+ create_source_pipeline(upstream, pipeline)
+ end
+
+ context 'same_project: false' do
+ it 'returns upstream and self' do
+ expect(subject).to contain_exactly(pipeline, upstream)
+ end
+ end
+
+ context 'same_project: true' do
+ let(:same_project) { true }
+
+ it 'returns self' do
+ expect(subject).to contain_exactly(pipeline)
+ end
+ end
+ end
+ end
+
+ describe 'reset_ancestor_bridges!' do
+ context 'when the pipeline is a child pipeline and the bridge is depended' do
+ let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
+ let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) }
+
+ it 'marks source bridge as pending' do
+ pipeline.reset_ancestor_bridges!
+
+ expect(bridge.reload).to be_pending
+ end
+
+ context 'when the parent pipeline has a dependent upstream pipeline' do
+ let!(:upstream_bridge) do
+ create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true)
+ end
+
+ it 'marks all source bridges as pending' do
+ pipeline.reset_ancestor_bridges!
+
+ expect(bridge.reload).to be_pending
+ expect(upstream_bridge.reload).to be_pending
+ end
+ end
+ end
+
+ context 'when the pipeline is a child pipeline and the bridge is not depended' do
+ let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
+ let!(:bridge) { create_bridge(parent_pipeline, pipeline, false) }
+
+ it 'does not touch source bridge' do
+ pipeline.reset_ancestor_bridges!
+
+ expect(bridge.reload).to be_success
+ end
+
+ context 'when the parent pipeline has a dependent upstream pipeline' do
+ let!(:upstream_bridge) do
+ create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true)
+ end
+
+ it 'does not touch any source bridge' do
+ pipeline.reset_ancestor_bridges!
+
+ expect(bridge.reload).to be_success
+ expect(upstream_bridge.reload).to be_success
+ end
+ end
+ end
+
+ private
+
+ def create_bridge(upstream, downstream, depend = false)
+ options = depend ? { trigger: { strategy: 'depend' } } : {}
+
+ bridge = create(:ci_bridge, pipeline: upstream, status: 'success', options: options)
+ create(:ci_sources_pipeline, pipeline: downstream, source_job: bridge)
+
+ bridge
+ end
+ end
end
diff --git a/spec/models/ci/ref_spec.rb b/spec/models/ci/ref_spec.rb
index 8bce3c10d8c..cb62646532c 100644
--- a/spec/models/ci/ref_spec.rb
+++ b/spec/models/ci/ref_spec.rb
@@ -16,51 +16,33 @@ RSpec.describe Ci::Ref 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
+ 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
- context 'when keep latest artifact feature is not enabled' do
- before do
- stub_feature_flags(keep_latest_artifacts_for_ref: false)
- 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 'does not call unlock artifacts service' do
- ci_ref.succeed!
+ it 'calls unlock artifacts service' do
+ ci_ref.send(action)
- expect(unlock_artifacts_worker_spy).not_to have_received(:perform_async)
+ expect(unlock_artifacts_worker_spy).to have_received(:perform_async).exactly(count).times
+ end
end
end
end
@@ -125,8 +107,8 @@ RSpec.describe Ci::Ref do
describe '#last_finished_pipeline_id' do
let(:pipeline_status) { :running }
- let(:config_source) { Ci::PipelineEnums.config_sources[:repository_source] }
- let(:pipeline) { create(:ci_pipeline, pipeline_status, config_source: config_source) }
+ let(:pipeline_source) { Enums::Ci::Pipeline.sources[:push] }
+ let(:pipeline) { create(:ci_pipeline, pipeline_status, source: pipeline_source) }
let(:ci_ref) { pipeline.ci_ref }
context 'when there are no finished pipelines' do
@@ -142,8 +124,8 @@ RSpec.describe Ci::Ref do
expect(ci_ref.last_finished_pipeline_id).to eq(pipeline.id)
end
- context 'when the pipeline is not a ci_source' do
- let(:config_source) { Ci::PipelineEnums.config_sources[:parameter_source] }
+ context 'when the pipeline a dangling pipeline' do
+ let(:pipeline_source) { Enums::Ci::Pipeline.sources[:ondemand_dast_scan] }
it 'returns nil' do
expect(ci_ref.last_finished_pipeline_id).to be_nil
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 8247ebf1144..3e5d068d780 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -570,7 +570,7 @@ RSpec.describe Ci::Runner do
let!(:last_update) { runner.ensure_runner_queue_value }
before do
- Ci::UpdateRunnerService.new(runner).update(description: 'new runner')
+ Ci::UpdateRunnerService.new(runner).update(description: 'new runner') # rubocop: disable Rails/SaveBang
end
it 'sets a new last_update value' do
@@ -660,7 +660,7 @@ RSpec.describe Ci::Runner do
before do
runner.tick_runner_queue
- runner.destroy
+ runner.destroy!
end
it 'cleans up the queue' do
@@ -878,7 +878,7 @@ RSpec.describe Ci::Runner do
it 'can be destroyed' do
subject
- expect { subject.destroy }.to change { described_class.count }.by(-1)
+ expect { subject.destroy! }.to change { described_class.count }.by(-1)
end
end
diff --git a/spec/models/ci_platform_metric_spec.rb b/spec/models/ci_platform_metric_spec.rb
new file mode 100644
index 00000000000..0b00875df43
--- /dev/null
+++ b/spec/models/ci_platform_metric_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CiPlatformMetric do
+ subject { build(:ci_platform_metric) }
+
+ it_behaves_like 'a BulkInsertSafe model', CiPlatformMetric do
+ let(:valid_items_for_bulk_insertion) { build_list(:ci_platform_metric, 10) }
+ let(:invalid_items_for_bulk_insertion) { [] } # class does not have any non-constraint validations defined
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:recorded_at) }
+ it { is_expected.to validate_presence_of(:count) }
+ it { is_expected.to validate_numericality_of(:count).only_integer.is_greater_than(0) }
+ it { is_expected.to allow_values('').for(:platform_target) }
+ it { is_expected.not_to allow_values(nil).for(:platform_target) }
+ it { is_expected.to validate_length_of(:platform_target).is_at_most(255) }
+ end
+
+ describe '.insert_auto_devops_platform_targets!' do
+ def platform_target_counts_by_day
+ report = Hash.new { |hash, key| hash[key] = {} }
+ described_class.all.each do |metric|
+ date = metric.recorded_at.to_date
+ report[date][metric.platform_target] = metric.count
+ end
+ report
+ end
+
+ context 'when there is already existing metrics data' do
+ let!(:metric_1) { create(:ci_platform_metric) }
+ let!(:metric_2) { create(:ci_platform_metric) }
+
+ it 'does not erase any existing data' do
+ described_class.insert_auto_devops_platform_targets!
+
+ expect(described_class.all.to_a).to contain_exactly(metric_1, metric_2)
+ end
+ end
+
+ context 'when there are multiple platform target variables' do
+ let(:today) { Time.zone.local(1982, 4, 24) }
+ let(:tomorrow) { today + 1.day }
+
+ it 'inserts platform target counts for that day' do
+ Timecop.freeze(today) do
+ create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'ECS')
+ create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'ECS')
+ create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FARGATE')
+ create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FARGATE')
+ create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FARGATE')
+ described_class.insert_auto_devops_platform_targets!
+ end
+ Timecop.freeze(tomorrow) do
+ create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FARGATE')
+ described_class.insert_auto_devops_platform_targets!
+ end
+
+ expect(platform_target_counts_by_day).to eq({
+ today.to_date => { 'ECS' => 2, 'FARGATE' => 3 },
+ tomorrow.to_date => { 'ECS' => 2, 'FARGATE' => 4 }
+ })
+ end
+ end
+
+ context 'when there are invalid ci variable values for platform_target' do
+ let(:today) { Time.zone.local(1982, 4, 24) }
+
+ it 'ignores those values' do
+ Timecop.freeze(today) do
+ create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'ECS')
+ create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FOO')
+ create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'BAR')
+ described_class.insert_auto_devops_platform_targets!
+ end
+
+ expect(platform_target_counts_by_day).to eq({
+ today.to_date => { 'ECS' => 1 }
+ })
+ end
+ end
+
+ context 'when there are no platform target variables' do
+ it 'does not generate any new platform metrics' do
+ create(:ci_variable, key: 'KEY_WHATEVER', value: 'ECS')
+ described_class.insert_auto_devops_platform_targets!
+
+ expect(platform_target_counts_by_day).to eq({})
+ end
+ end
+ end
+end
diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb
index bb1fc021e66..99de0d1ddf7 100644
--- a/spec/models/clusters/agent_spec.rb
+++ b/spec/models/clusters/agent_spec.rb
@@ -12,6 +12,17 @@ RSpec.describe Clusters::Agent do
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 'scopes' do
+ describe '.with_name' do
+ let!(:matching_name) { create(:cluster_agent, name: 'matching-name') }
+ let!(:other_name) { create(:cluster_agent, name: 'other-name') }
+
+ subject { described_class.with_name(matching_name.name) }
+
+ it { is_expected.to contain_exactly(matching_name) }
+ end
+ end
+
describe 'validation' do
describe 'name validation' do
it 'rejects names that do not conform to RFC 1123', :aggregate_failures do
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index 1215b38a9a2..82971596176 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe Clusters::Applications::Prometheus do
subject { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
it 'sets last_update_started_at to now' do
- Timecop.freeze do
+ freeze_time do
expect { subject.make_updating }.to change { subject.reload.last_update_started_at }.to be_within(1.second).of(Time.current)
end
end
@@ -109,10 +109,13 @@ RSpec.describe Clusters::Applications::Prometheus do
expect(subject.prometheus_client).to be_instance_of(Gitlab::PrometheusClient)
end
- it 'copies proxy_url, options and headers from kube client to prometheus_client' do
+ it 'merges proxy_url, options and headers from kube client with prometheus_client options' do
expect(Gitlab::PrometheusClient)
.to(receive(:new))
- .with(a_valid_url, kube_client.rest_client.options.merge(headers: kube_client.headers))
+ .with(a_valid_url, kube_client.rest_client.options.merge({
+ headers: kube_client.headers,
+ timeout: PrometheusAdapter::DEFAULT_PROMETHEUS_REQUEST_TIMEOUT_SEC
+ }))
subject.prometheus_client
end
@@ -150,7 +153,7 @@ RSpec.describe Clusters::Applications::Prometheus do
it 'is initialized with 3 arguments' do
expect(subject.name).to eq('prometheus')
expect(subject.chart).to eq('stable/prometheus')
- expect(subject.version).to eq('9.5.2')
+ expect(subject.version).to eq('10.4.1')
expect(subject).to be_rbac
expect(subject.files).to eq(prometheus.files)
end
@@ -167,7 +170,7 @@ RSpec.describe Clusters::Applications::Prometheus do
let(:prometheus) { create(:clusters_applications_prometheus, :errored, version: '2.0.0') }
it 'is initialized with the locked version' do
- expect(subject.version).to eq('9.5.2')
+ expect(subject.version).to eq('10.4.1')
end
end
@@ -238,7 +241,7 @@ RSpec.describe Clusters::Applications::Prometheus do
it 'is initialized with 3 arguments' do
expect(patch_command.name).to eq('prometheus')
expect(patch_command.chart).to eq('stable/prometheus')
- expect(patch_command.version).to eq('9.5.2')
+ expect(patch_command.version).to eq('10.4.1')
expect(patch_command.files).to eq(prometheus.files)
end
end
@@ -350,7 +353,7 @@ RSpec.describe Clusters::Applications::Prometheus do
let(:timestamp) { Time.current - 5.minutes }
around do |example|
- Timecop.freeze { example.run }
+ freeze_time { example.run }
end
before do
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 2d0b5af0e77..024539e34ec 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -42,6 +42,7 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it { is_expected.to delegate_method(:available?).to(:application_ingress).with_prefix }
it { is_expected.to delegate_method(:available?).to(:application_prometheus).with_prefix }
it { is_expected.to delegate_method(:available?).to(:application_knative).with_prefix }
+ it { is_expected.to delegate_method(:available?).to(:application_elastic_stack).with_prefix }
it { is_expected.to delegate_method(:external_ip).to(:application_ingress).with_prefix }
it { is_expected.to delegate_method(:external_hostname).to(:application_ingress).with_prefix }
diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb
index 2920bbf2b58..3b903fe34f9 100644
--- a/spec/models/clusters/kubernetes_namespace_spec.rb
+++ b/spec/models/clusters/kubernetes_namespace_spec.rb
@@ -61,7 +61,8 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
end
describe 'namespace uniqueness validation' do
- let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, namespace: 'my-namespace') }
+ let_it_be(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, cluster: cluster, namespace: 'my-namespace') }
subject { kubernetes_namespace }
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index 3fb8708c884..334833e884b 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -3,25 +3,22 @@
require 'spec_helper'
RSpec.describe CommitRange do
+ let(:range2) { described_class.new("#{sha_from}..#{sha_to}", project) }
+ let(:range) { described_class.new("#{sha_from}...#{sha_to}", project) }
+ let(:full_sha_to) { commit2.id }
+ let(:full_sha_from) { commit1.id }
+ let(:sha_to) { commit2.short_id }
+ let(:sha_from) { commit1.short_id }
+ let!(:commit2) { project.commit }
+ let!(:commit1) { project.commit("HEAD~2") }
+ let!(:project) { create(:project, :public, :repository) }
+
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Referable) }
end
- let!(:project) { create(:project, :public, :repository) }
- let!(:commit1) { project.commit("HEAD~2") }
- let!(:commit2) { project.commit }
-
- let(:sha_from) { commit1.short_id }
- let(:sha_to) { commit2.short_id }
-
- let(:full_sha_from) { commit1.id }
- let(:full_sha_to) { commit2.id }
-
- let(:range) { described_class.new("#{sha_from}...#{sha_to}", project) }
- let(:range2) { described_class.new("#{sha_from}..#{sha_to}", project) }
-
it 'raises ArgumentError when given an invalid range string' do
expect { described_class.new("Foo", project) }.to raise_error(ArgumentError)
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 7f893d6a100..6e23f95af03 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -494,6 +494,10 @@ RSpec.describe CommitStatus do
end
describe '#group_name' do
+ let(:commit_status) do
+ build(:commit_status, pipeline: pipeline, stage: 'test')
+ end
+
subject { commit_status.group_name }
tests = {
@@ -510,7 +514,19 @@ RSpec.describe CommitStatus do
'rspec:windows 0 : / 1' => 'rspec:windows',
'rspec:windows 0 : / 1 name' => 'rspec:windows name',
'0 1 name ruby' => 'name ruby',
- '0 :/ 1 name ruby' => 'name ruby'
+ '0 :/ 1 name ruby' => 'name ruby',
+ 'rspec: [aws]' => 'rspec: [aws]',
+ 'rspec: [aws] 0/1' => 'rspec: [aws]',
+ 'rspec: [aws, max memory]' => 'rspec',
+ 'rspec:linux: [aws, max memory, data]' => 'rspec:linux',
+ 'rspec: [inception: [something, other thing], value]' => 'rspec',
+ 'rspec:windows 0/1: [name, other]' => 'rspec:windows',
+ 'rspec:windows: [name, other] 0/1' => 'rspec:windows',
+ 'rspec:windows: [name, 0/1] 0/1' => 'rspec:windows',
+ 'rspec:windows: [0/1, name]' => 'rspec:windows',
+ 'rspec:windows: [, ]' => 'rspec:windows',
+ 'rspec:windows: [name]' => 'rspec:windows: [name]',
+ 'rspec:windows: [name,other]' => 'rspec:windows: [name,other]'
}
tests.each do |name, group_name|
diff --git a/spec/models/concerns/ci/artifactable_spec.rb b/spec/models/concerns/ci/artifactable_spec.rb
index 13c2ff5efe5..f05189abdd2 100644
--- a/spec/models/concerns/ci/artifactable_spec.rb
+++ b/spec/models/concerns/ci/artifactable_spec.rb
@@ -18,4 +18,40 @@ RSpec.describe Ci::Artifactable do
it { is_expected.to be_const_defined(:FILE_FORMAT_ADAPTERS) }
end
end
+
+ describe '#each_blob' do
+ context 'when file format is gzip' do
+ context 'when gzip file contains one file' do
+ let(:artifact) { build(:ci_job_artifact, :junit) }
+
+ it 'iterates blob once' do
+ expect { |b| artifact.each_blob(&b) }.to yield_control.once
+ end
+ end
+
+ context 'when gzip file contains three files' do
+ let(:artifact) { build(:ci_job_artifact, :junit_with_three_testsuites) }
+
+ it 'iterates blob three times' do
+ expect { |b| artifact.each_blob(&b) }.to yield_control.exactly(3).times
+ end
+ end
+ end
+
+ context 'when file format is raw' do
+ let(:artifact) { build(:ci_job_artifact, :codequality, file_format: :raw) }
+
+ it 'iterates blob once' do
+ expect { |b| artifact.each_blob(&b) }.to yield_control.once
+ end
+ end
+
+ context 'when there are no adapters for the file format' do
+ let(:artifact) { build(:ci_job_artifact, :junit, file_format: :zip) }
+
+ it 'raises an error' do
+ expect { |b| artifact.each_blob(&b) }.to raise_error(described_class::NotSupportedAdapterError)
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/from_except_spec.rb b/spec/models/concerns/from_except_spec.rb
new file mode 100644
index 00000000000..3b081b4f572
--- /dev/null
+++ b/spec/models/concerns/from_except_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe FromExcept do
+ it_behaves_like 'from set operator', Gitlab::SQL::Except
+end
diff --git a/spec/models/concerns/from_intersect_spec.rb b/spec/models/concerns/from_intersect_spec.rb
new file mode 100644
index 00000000000..78884f8ae56
--- /dev/null
+++ b/spec/models/concerns/from_intersect_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe FromIntersect do
+ it_behaves_like 'from set operator', Gitlab::SQL::Intersect
+end
diff --git a/spec/models/concerns/from_set_operator_spec.rb b/spec/models/concerns/from_set_operator_spec.rb
new file mode 100644
index 00000000000..8ebbb5550c9
--- /dev/null
+++ b/spec/models/concerns/from_set_operator_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe FromSetOperator do
+ describe 'when set operator method already exists' do
+ let(:redefine_method) do
+ Class.new do
+ def self.from_union
+ # This method intentionally left blank.
+ end
+
+ extend FromSetOperator
+ define_set_operator Gitlab::SQL::Union
+ end
+ end
+
+ it { expect { redefine_method }.to raise_exception(RuntimeError) }
+ end
+end
diff --git a/spec/models/concerns/from_union_spec.rb b/spec/models/concerns/from_union_spec.rb
index 9819a6ec3de..bd2893090a8 100644
--- a/spec/models/concerns/from_union_spec.rb
+++ b/spec/models/concerns/from_union_spec.rb
@@ -3,38 +3,13 @@
require 'spec_helper'
RSpec.describe FromUnion do
- describe '.from_union' do
- let(:model) do
- Class.new(ActiveRecord::Base) do
- self.table_name = 'users'
-
- include FromUnion
+ [true, false].each do |sql_set_operator|
+ context "when sql-set-operators feature flag is #{sql_set_operator}" do
+ before do
+ stub_feature_flags(sql_set_operators: sql_set_operator)
end
- end
-
- it 'selects from the results of the UNION' do
- query = model.from_union([model.where(id: 1), model.where(id: 2)])
-
- expect(query.to_sql).to match(/FROM \(\(SELECT.+\)\nUNION\n\(SELECT.+\)\) users/m)
- end
-
- it 'supports the use of a custom alias for the sub query' do
- query = model.from_union(
- [model.where(id: 1), model.where(id: 2)],
- alias_as: 'kittens'
- )
-
- expect(query.to_sql).to match(/FROM \(\(SELECT.+\)\nUNION\n\(SELECT.+\)\) kittens/m)
- end
-
- it 'supports keeping duplicate rows' do
- query = model.from_union(
- [model.where(id: 1), model.where(id: 2)],
- remove_duplicates: false
- )
- expect(query.to_sql)
- .to match(/FROM \(\(SELECT.+\)\nUNION ALL\n\(SELECT.+\)\) users/m)
+ it_behaves_like 'from set operator', Gitlab::SQL::Union
end
end
end
diff --git a/spec/models/concerns/id_in_ordered_spec.rb b/spec/models/concerns/id_in_ordered_spec.rb
new file mode 100644
index 00000000000..a3b434caac6
--- /dev/null
+++ b/spec/models/concerns/id_in_ordered_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IdInOrdered do
+ describe 'Issue' do
+ describe '.id_in_ordered' do
+ it 'returns issues matching the ids in the same order as the ids' do
+ issue1 = create(:issue)
+ issue2 = create(:issue)
+ issue3 = create(:issue)
+ issue4 = create(:issue)
+ issue5 = create(:issue)
+
+ expect(Issue.id_in_ordered([issue3.id, issue1.id, issue4.id, issue5.id, issue2.id])).to eq([
+ issue3, issue1, issue4, issue5, issue2
+ ])
+ end
+
+ context 'when the ids are not an array of integers' do
+ it 'raises ArgumentError' do
+ expect { Issue.id_in_ordered([1, 'no SQL injection']) }.to raise_error(ArgumentError, "ids must be an array of integers")
+ end
+ end
+
+ context 'when an empty array is given' do
+ it 'does not raise error' do
+ expect(Issue.id_in_ordered([]).to_a).to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 0824b5c7834..44561e2e55a 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -295,20 +295,14 @@ RSpec.describe Issuable do
end
describe "#new?" do
- it "returns true when created today and record hasn't been updated" do
- allow(issue).to receive(:today?).and_return(true)
- expect(issue.new?).to be_truthy
- end
-
- it "returns false when not created today" do
- allow(issue).to receive(:today?).and_return(false)
+ it "returns false when created 30 hours ago" do
+ allow(issue).to receive(:created_at).and_return(Time.current - 30.hours)
expect(issue.new?).to be_falsey
end
- it "returns false when record has been updated" do
- allow(issue).to receive(:today?).and_return(true)
- issue.update_attribute(:updated_at, 1.hour.ago)
- expect(issue.new?).to be_falsey
+ it "returns true when created 20 hours ago" do
+ allow(issue).to receive(:created_at).and_return(Time.current - 20.hours)
+ expect(issue.new?).to be_truthy
end
end
@@ -824,4 +818,166 @@ RSpec.describe Issuable do
it_behaves_like 'matches_cross_reference_regex? fails fast'
end
end
+
+ describe '#supports_time_tracking?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:issuable_type, :supports_time_tracking) do
+ :issue | true
+ :incident | false
+ :merge_request | true
+ end
+
+ with_them do
+ let(:issuable) { build_stubbed(issuable_type) }
+
+ subject { issuable.supports_time_tracking? }
+
+ it { is_expected.to eq(supports_time_tracking) }
+ end
+ end
+
+ describe '#supports_severity?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:issuable_type, :supports_severity) do
+ :issue | false
+ :incident | true
+ :merge_request | false
+ end
+
+ with_them do
+ let(:issuable) { build_stubbed(issuable_type) }
+
+ subject { issuable.supports_severity? }
+
+ it { is_expected.to eq(supports_severity) }
+ end
+ end
+
+ describe '#incident?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:issuable_type, :incident) do
+ :issue | false
+ :incident | true
+ :merge_request | false
+ end
+
+ with_them do
+ let(:issuable) { build_stubbed(issuable_type) }
+
+ subject { issuable.incident? }
+
+ it { is_expected.to eq(incident) }
+ end
+ end
+
+ describe '#supports_issue_type?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:issuable_type, :supports_issue_type) do
+ :issue | true
+ :merge_request | false
+ end
+
+ with_them do
+ let(:issuable) { build_stubbed(issuable_type) }
+
+ subject { issuable.supports_issue_type? }
+
+ it { is_expected.to eq(supports_issue_type) }
+ end
+ end
+
+ describe '#severity' do
+ subject { issuable.severity }
+
+ context 'when issuable is not an incident' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:issuable_type, :severity) do
+ :issue | 'unknown'
+ :merge_request | 'unknown'
+ end
+
+ with_them do
+ let(:issuable) { build_stubbed(issuable_type) }
+
+ it { is_expected.to eq(severity) }
+ end
+ end
+
+ context 'when issuable type is an incident' do
+ let!(:issuable) { build_stubbed(:incident) }
+
+ context 'when incident does not have issuable_severity' do
+ it 'returns default serverity' do
+ is_expected.to eq(IssuableSeverity::DEFAULT)
+ end
+ end
+
+ context 'when incident has issuable_severity' do
+ let!(:issuable_severity) { build_stubbed(:issuable_severity, issue: issuable, severity: 'critical') }
+
+ it 'returns issuable serverity' do
+ is_expected.to eq('critical')
+ end
+ end
+ end
+ end
+
+ describe '#update_severity' do
+ let(:severity) { 'low' }
+
+ subject(:update_severity) { issuable.update_severity(severity) }
+
+ context 'when issuable not an incident' do
+ %i(issue merge_request).each do |issuable_type|
+ let(:issuable) { build_stubbed(issuable_type) }
+
+ it { is_expected.to be_nil }
+
+ it 'does not set severity' do
+ expect { subject }.not_to change(IssuableSeverity, :count)
+ end
+ end
+ end
+
+ context 'when issuable is an incident' do
+ let!(:issuable) { create(:incident) }
+
+ context 'when issuable does not have issuable severity yet' do
+ it 'creates new record' do
+ expect { update_severity }.to change { IssuableSeverity.where(issue: issuable).count }.to(1)
+ end
+
+ it 'sets severity to specified value' do
+ expect { update_severity }.to change { issuable.severity }.to('low')
+ end
+ end
+
+ context 'when issuable has an issuable severity' do
+ let!(:issuable_severity) { create(:issuable_severity, issue: issuable, severity: 'medium') }
+
+ it 'does not create new record' do
+ expect { update_severity }.not_to change(IssuableSeverity, :count)
+ end
+
+ it 'updates existing issuable severity' do
+ expect { update_severity }.to change { issuable_severity.severity }.to(severity)
+ end
+ end
+
+ context 'when severity value is unsupported' do
+ let(:severity) { 'unsupported-severity' }
+
+ it 'sets the severity to default value' do
+ update_severity
+
+ expect(issuable.issuable_severity.severity).to eq(IssuableSeverity::DEFAULT)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/milestoneable_spec.rb b/spec/models/concerns/milestoneable_spec.rb
index 3dd6f1450c7..f5b82e42ad4 100644
--- a/spec/models/concerns/milestoneable_spec.rb
+++ b/spec/models/concerns/milestoneable_spec.rb
@@ -100,6 +100,14 @@ RSpec.describe Milestoneable do
expect(merge_request.supports_milestone?).to be_truthy
end
end
+
+ context "for incidents" do
+ let(:incident) { build(:incident) }
+
+ it 'returns false' do
+ expect(incident.supports_milestone?).to be_falsy
+ end
+ end
end
describe 'release scopes' do
diff --git a/spec/models/concerns/prometheus_adapter_spec.rb b/spec/models/concerns/prometheus_adapter_spec.rb
index e795e2b06cb..235e505c6e9 100644
--- a/spec/models/concerns/prometheus_adapter_spec.rb
+++ b/spec/models/concerns/prometheus_adapter_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
let(:validation_respone) { { data: { valid: true } } }
around do |example|
- Timecop.freeze { example.run }
+ freeze_time { example.run }
end
context 'with valid data' do
@@ -45,7 +45,7 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
around do |example|
- Timecop.freeze { example.run }
+ freeze_time { example.run }
end
context 'with valid data' do
@@ -85,7 +85,7 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
let(:deployment_query) { Gitlab::Prometheus::Queries::DeploymentQuery }
around do |example|
- Timecop.freeze { example.run }
+ freeze_time { example.run }
end
context 'with valid data' do
@@ -107,7 +107,7 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
let(:time_window) { [1552642245.067, 1552642095.831] }
around do |example|
- Timecop.freeze { example.run }
+ freeze_time { example.run }
end
context 'with valid data' do
@@ -137,7 +137,7 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
end
around do |example|
- Timecop.freeze { example.run }
+ freeze_time { example.run }
end
context 'when service is inactive' do
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
index 9372ef5f0e6..5857365ceab 100644
--- a/spec/models/cycle_analytics/issue_spec.rb
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -15,17 +15,17 @@ RSpec.describe 'CycleAnalytics#issue' do
generate_cycle_analytics_spec(
phase: :issue,
data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
- start_time_conditions: [["issue created", -> (context, data) { data[:issue].save }]],
+ start_time_conditions: [["issue created", -> (context, data) { data[:issue].save! }]],
end_time_conditions: [["issue associated with a milestone",
-> (context, data) do
if data[:issue].persisted?
- data[:issue].update(milestone: context.create(:milestone, project: context.project))
+ data[:issue].update!(milestone: context.create(:milestone, project: context.project))
end
end],
["list label added to issue",
-> (context, data) do
if data[:issue].persisted?
- data[:issue].update(label_ids: [context.create(:list).label_id])
+ data[:issue].update!(label_ids: [context.create(:list).label_id])
end
end]],
post_fn: -> (context, data) do
@@ -35,7 +35,7 @@ RSpec.describe 'CycleAnalytics#issue' do
it "returns nil" do
regular_label = create(:label)
issue = create(:issue, project: project)
- issue.update(label_ids: [regular_label.id])
+ issue.update!(label_ids: [regular_label.id])
create_merge_request_closing_issue(user, project, issue)
merge_merge_requests_closing_issue(user, project, issue)
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
index 364694a11e1..2b9be64da2b 100644
--- a/spec/models/cycle_analytics/plan_spec.rb
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -22,11 +22,11 @@ RSpec.describe 'CycleAnalytics#plan' do
end,
start_time_conditions: [["issue associated with a milestone",
-> (context, data) do
- data[:issue].update(milestone: context.create(:milestone, project: context.project))
+ data[:issue].update!(milestone: context.create(:milestone, project: context.project))
end],
["list label added to issue",
-> (context, data) do
- data[:issue].update(label_ids: [context.create(:list).label_id])
+ data[:issue].update!(label_ids: [context.create(:list).label_id])
end]],
end_time_conditions: [["issue mentioned in a commit",
-> (context, data) do
@@ -40,7 +40,7 @@ RSpec.describe 'CycleAnalytics#plan' do
branch_name = generate(:branch)
label = create(:label)
issue = create(:issue, project: project)
- issue.update(label_ids: [label.id])
+ issue.update!(label_ids: [label.id])
create_commit_referencing_issue(issue, branch_name: branch_name)
create_merge_request_closing_issue(user, project, issue, source_branch: branch_name)
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
deleted file mode 100644
index cf4d57d6b73..00000000000
--- a/spec/models/cycle_analytics/production_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'CycleAnalytics#production' do
- extend CycleAnalyticsHelpers::TestGeneration
-
- let_it_be(:project) { create(:project, :repository) }
- let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { project.owner }
- let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
-
- subject { project_level }
-
- generate_cycle_analytics_spec(
- phase: :production,
- data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
- start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save }]],
- before_end_fn: lambda do |context, data|
- context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
- context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
- end,
- end_time_conditions:
- [["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master(context.user, context.project) }],
- ["production deploy happens after merge request is merged (along with other changes)",
- lambda do |context, data|
- # Make other changes on master
- context.project.repository.commit("sha_that_does_not_matter")
-
- context.deploy_master(context.user, context.project)
- end]])
-
- context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
- it "returns nil" do
- merge_request = create(:merge_request)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master(user, project)
-
- expect(subject[:production].project_median).to be_nil
- end
- end
-
- context "when the deployment happens to a non-production environment" do
- it "returns nil" do
- issue = build(:issue, project: project)
- merge_request = create_merge_request_closing_issue(user, project, issue)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master(user, project, environment: 'staging')
-
- expect(subject[:production].project_median).to be_nil
- end
- end
-end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index b320390711e..1c7b11257ce 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -44,10 +44,14 @@ RSpec.describe Deployment do
describe 'modules' do
it_behaves_like 'AtomicInternalId' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:deployable) { create(:ci_build, project: project) }
+ let_it_be(:environment) { create(:environment, project: project) }
+
let(:internal_id_attribute) { :iid }
- let(:instance) { build(:deployment) }
+ let(:instance) { build(:deployment, deployable: deployable, environment: environment) }
let(:scope) { :project }
- let(:scope_attrs) { { project: instance.project } }
+ let(:scope_attrs) { { project: project } }
let(:usage) { :deployments }
end
end
@@ -99,7 +103,7 @@ RSpec.describe Deployment do
end
it 'starts running' do
- Timecop.freeze do
+ freeze_time do
expect(deployment).to be_running
expect(deployment.finished_at).to be_nil
end
@@ -110,7 +114,7 @@ RSpec.describe Deployment do
let(:deployment) { create(:deployment, :running) }
it 'has correct status' do
- Timecop.freeze do
+ freeze_time do
deployment.succeed!
expect(deployment).to be_success
@@ -137,7 +141,7 @@ RSpec.describe Deployment do
let(:deployment) { create(:deployment, :running) }
it 'has correct status' do
- Timecop.freeze do
+ freeze_time do
deployment.drop!
expect(deployment).to be_failed
@@ -157,7 +161,7 @@ RSpec.describe Deployment do
let(:deployment) { create(:deployment, :running) }
it 'has correct status' do
- Timecop.freeze do
+ freeze_time do
deployment.cancel!
expect(deployment).to be_canceled
@@ -584,7 +588,7 @@ RSpec.describe Deployment do
end
it 'updates finished_at when transitioning to a finished status' do
- Timecop.freeze do
+ freeze_time do
deploy.update_status('success')
expect(deploy.read_attribute(:finished_at)).to eq(Time.current)
diff --git a/spec/models/design_management/design_collection_spec.rb b/spec/models/design_management/design_collection_spec.rb
index de766d5ce09..8575cc80b5b 100644
--- a/spec/models/design_management/design_collection_spec.rb
+++ b/spec/models/design_management/design_collection_spec.rb
@@ -3,8 +3,9 @@ require 'spec_helper'
RSpec.describe DesignManagement::DesignCollection do
include DesignManagementTestHelpers
+ using RSpec::Parameterized::TableSyntax
- let_it_be(:issue, reload: true) { create(:issue) }
+ let_it_be(:issue, refind: true) { create(:issue) }
subject(:collection) { described_class.new(issue) }
@@ -41,7 +42,62 @@ RSpec.describe DesignManagement::DesignCollection do
design2 = collection.find_or_create_design!(filename: 'design2.jpg')
- expect(collection.designs.ordered(issue.project)).to eq([design1, design2])
+ expect(collection.designs.ordered).to eq([design1, design2])
+ end
+ end
+
+ describe "#copy_state", :clean_gitlab_redis_shared_state do
+ it "defaults to ready" do
+ expect(collection).to be_copy_ready
+ end
+
+ it "persists its state changes between initializations" do
+ collection.start_copy!
+
+ expect(described_class.new(issue)).to be_copy_in_progress
+ end
+
+ where(:state, :can_start, :can_end, :can_error, :can_reset) do
+ "ready" | true | false | true | true
+ "in_progress" | false | true | true | true
+ "error" | false | false | false | true
+ end
+
+ with_them do
+ it "maintains state machine transition rules", :aggregate_failures do
+ collection.copy_state = state
+
+ expect(collection.can_start_copy?).to eq(can_start)
+ expect(collection.can_end_copy?).to eq(can_end)
+ end
+ end
+
+ describe "clearing the redis cached state when state changes back to ready" do
+ def redis_copy_state
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(collection.send(:copy_state_cache_key))
+ end
+ end
+
+ def fire_state_events(*events)
+ events.each do |event|
+ collection.fire_copy_state_event(event)
+ end
+ end
+
+ it "clears the cached state on end_copy!", :aggregate_failures do
+ fire_state_events(:start)
+
+ expect { collection.end_copy! }.to change { redis_copy_state }.from("in_progress").to(nil)
+ expect(collection).to be_copy_ready
+ end
+
+ it "clears the cached state on reset_copy!", :aggregate_failures do
+ fire_state_events(:start, :error)
+
+ expect { collection.reset_copy! }.to change { redis_copy_state }.from("error").to(nil)
+ expect(collection).to be_copy_ready
+ end
end
end
diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb
index 2c129f883b9..d4adc0d42d0 100644
--- a/spec/models/design_management/design_spec.rb
+++ b/spec/models/design_management/design_spec.rb
@@ -12,8 +12,10 @@ RSpec.describe DesignManagement::Design do
let_it_be(:deleted_design) { create(:design, :with_versions, deleted: true) }
it_behaves_like 'a class that supports relative positioning' do
+ let_it_be(:relative_parent) { create(:issue) }
+
let(:factory) { :design }
- let(:default_params) { { issue: issue } }
+ let(:default_params) { { issue: relative_parent } }
end
describe 'relations' do
@@ -161,27 +163,7 @@ RSpec.describe DesignManagement::Design do
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
+ expect(described_class.ordered).to eq([design2, design1, design3, deleted_design])
end
end
diff --git a/spec/models/dev_ops_score/metric_spec.rb b/spec/models/dev_ops_report/metric_spec.rb
index 60001d0667d..191692f43a4 100644
--- a/spec/models/dev_ops_score/metric_spec.rb
+++ b/spec/models/dev_ops_report/metric_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe DevOpsScore::Metric do
- let(:conv_dev_index) { create(:dev_ops_score_metric) }
+RSpec.describe DevOpsReport::Metric do
+ let(:conv_dev_index) { create(:dev_ops_report_metric) }
describe '#percentage_score' do
it 'returns stored percentage score' do
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 8a6176bf045..f7ce44f7281 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -35,7 +35,9 @@ RSpec.describe DiffNote do
subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
describe 'validations' do
- it_behaves_like 'a valid diff positionable note', :diff_note_on_commit
+ it_behaves_like 'a valid diff positionable note' do
+ subject { build(:diff_note_on_commit, project: project, commit_id: commit_id, position: position) }
+ end
end
describe "#position=" do
diff --git a/spec/models/draft_note_spec.rb b/spec/models/draft_note_spec.rb
index 64b06bf5c8f..580a588ae1d 100644
--- a/spec/models/draft_note_spec.rb
+++ b/spec/models/draft_note_spec.rb
@@ -5,11 +5,13 @@ require 'spec_helper'
RSpec.describe DraftNote do
include RepoHelpers
- let(:project) { create(:project, :repository) }
- let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
describe 'validations' do
- it_behaves_like 'a valid diff positionable note', :draft_note
+ it_behaves_like 'a valid diff positionable note' do
+ subject { build(:draft_note, merge_request: merge_request, commit_id: commit_id, position: position) }
+ end
end
describe 'delegations' do
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 2696d144db4..433ede97b82 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -854,8 +854,8 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
it 'overrides reactive_cache_limit_enabled? with a FF' do
- environment_with_enabled_ff = FactoryBot.build(:environment)
- environment_with_disabled_ff = FactoryBot.build(:environment)
+ environment_with_enabled_ff = build(:environment, project: create(:project))
+ environment_with_disabled_ff = build(:environment, project: create(:project))
stub_feature_flags(reactive_caching_limit_environment: environment_with_enabled_ff.project)
@@ -1222,7 +1222,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
let(:environment) { build(:environment, :will_auto_stop) }
it 'returns when it will expire' do
- Timecop.freeze { is_expected.to eq(1.day.to_i) }
+ freeze_time { is_expected.to eq(1.day.to_i) }
end
end
@@ -1248,7 +1248,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
with_them do
it 'sets correct auto_stop_in' do
- Timecop.freeze do
+ freeze_time do
if expected_result.is_a?(Integer) || expected_result.nil?
subject
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index 7eefb8f714a..a6954fb5d56 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -90,22 +90,6 @@ RSpec.describe EnvironmentStatus do
end
end
- describe '.after_merge_request' do
- let(:admin) { create(:admin) }
- let(:pipeline) { create(:ci_pipeline, sha: sha) }
-
- before do
- merge_request.mark_as_merged!
- end
-
- it 'is based on merge_request.merge_commit_sha' do
- expect(merge_request).to receive(:merge_commit_sha)
- expect(merge_request).not_to receive(:diff_head_sha)
-
- described_class.after_merge_request(merge_request, admin)
- end
- end
-
describe '.for_deployed_merge_request' do
context 'when a merge request has no explicitly linked deployments' do
it 'returns the statuses based on the CI pipelines' do
@@ -191,7 +175,7 @@ RSpec.describe EnvironmentStatus do
let(:environment) { build.deployment.environment }
let(:user) { project.owner }
- context 'when environment is created on a forked project' do
+ context 'when environment is created on a forked project', :sidekiq_inline do
let(:project) { create(:project, :repository) }
let(:forked) { fork_project(project, user, repository: true) }
let(:sha) { forked.commit.sha }
@@ -205,7 +189,7 @@ RSpec.describe EnvironmentStatus do
head_pipeline: pipeline)
end
- it 'returns environment status', :sidekiq_might_not_need_inline do
+ it 'returns environment status' do
expect(subject.count).to eq(1)
expect(subject[0].environment).to eq(environment)
expect(subject[0].merge_request).to eq(merge_request)
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 015a86cb28b..bafcb7a3741 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -81,6 +81,8 @@ RSpec.describe Event do
describe 'validations' do
describe 'action' do
context 'for a design' do
+ let_it_be(:author) { create(:user) }
+
where(:action, :valid) do
valid = described_class::DESIGN_ACTIONS.map(&:to_s).to_set
@@ -90,7 +92,7 @@ RSpec.describe Event do
end
with_them do
- let(:event) { build(:design_event, action: action) }
+ let(:event) { build(:design_event, author: author, action: action) }
specify { expect(event.valid?).to eq(valid) }
end
@@ -722,9 +724,17 @@ RSpec.describe Event do
note_on_commit: true
}
valid_target_factories.map do |kind, needs_project|
- extra_data = needs_project ? { project: project } : {}
+ extra_data = if kind == :merge_request
+ { source_project: project }
+ elsif needs_project
+ { project: project }
+ else
+ {}
+ end
+
target = kind == :project ? nil : build(kind, **extra_data)
- [kind, build(:event, :created, project: project, target: target)]
+
+ [kind, build(:event, :created, author: project.owner, project: project, target: target)]
end.to_h
end
diff --git a/spec/models/group_deploy_key_spec.rb b/spec/models/group_deploy_key_spec.rb
index 6757c5534ce..dfb4fee593f 100644
--- a/spec/models/group_deploy_key_spec.rb
+++ b/spec/models/group_deploy_key_spec.rb
@@ -82,4 +82,25 @@ RSpec.describe GroupDeployKey do
end
end
end
+
+ describe '.for_groups' do
+ context 'when group deploy keys are enabled for some groups' do
+ let_it_be(:group1) { create(:group) }
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:group3) { create(:group) }
+ let_it_be(:gdk1) { create(:group_deploy_key) }
+ let_it_be(:gdk2) { create(:group_deploy_key) }
+ let_it_be(:gdk3) { create(:group_deploy_key) }
+
+ it 'returns these group deploy keys' do
+ gdk1.groups << group1
+ gdk1.groups << group2
+ gdk2.groups << group3
+ gdk3.groups << group2
+
+ expect(described_class.for_groups([group1.id, group3.id])).to contain_exactly(gdk1, gdk2)
+ expect(described_class.for_groups([group2.id])).to contain_exactly(gdk1, gdk3)
+ end
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 3eb74da09e1..15972f66fd6 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -27,6 +27,7 @@ RSpec.describe Group do
it { is_expected.to have_many(:milestones) }
it { is_expected.to have_many(:iterations) }
it { is_expected.to have_many(:group_deploy_keys) }
+ it { is_expected.to have_many(:services) }
describe '#members & #requesters' do
let(:requester) { create(:user) }
@@ -652,6 +653,19 @@ RSpec.describe Group do
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::MAINTAINER)
end
end
+
+ context 'evaluating admin access level' do
+ let_it_be(:admin) { create(:admin) }
+
+ it 'returns OWNER by default' do
+ expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::OWNER)
+ end
+
+ it 'returns NO_ACCESS when only concrete membership should be considered' do
+ expect(group.max_member_access_for_user(admin, only_concrete_membership: true))
+ .to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
end
describe '#members_with_parents' do
@@ -692,6 +706,7 @@ RSpec.describe Group do
before do
create(:group_member, user: user, group: group_parent, access_level: parent_group_access_level)
create(:group_member, user: user, group: group, access_level: group_access_level)
+ create(:group_member, :minimal_access, user: create(:user), source: group)
create(:group_member, user: user, group: group_child, access_level: child_group_access_level)
end
diff --git a/spec/models/issuable_severity_spec.rb b/spec/models/issuable_severity_spec.rb
new file mode 100644
index 00000000000..4dfd19756cb
--- /dev/null
+++ b/spec/models/issuable_severity_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IssuableSeverity, type: :model do
+ let_it_be(:issuable_severity) { create(:issuable_severity) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:issue) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:severity) }
+ it { is_expected.to validate_presence_of(:issue) }
+ it { is_expected.to validate_uniqueness_of(:issue) }
+ end
+
+ describe 'enums' do
+ let(:severity_values) do
+ { unknown: 0, low: 1, medium: 2, high: 3, critical: 4 }
+ end
+
+ it { is_expected.to define_enum_for(:severity).with_values(severity_values) }
+ end
+end
diff --git a/spec/models/issue_link_spec.rb b/spec/models/issue_link_spec.rb
new file mode 100644
index 00000000000..00791d4a48b
--- /dev/null
+++ b/spec/models/issue_link_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IssueLink do
+ describe 'Associations' do
+ it { is_expected.to belong_to(:source).class_name('Issue') }
+ it { is_expected.to belong_to(:target).class_name('Issue') }
+ end
+
+ describe 'link_type' do
+ it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1, is_blocked_by: 2) }
+
+ it 'provides the "related" as default link_type' do
+ expect(create(:issue_link).link_type).to eq 'relates_to'
+ end
+ end
+
+ describe 'Validation' do
+ subject { create :issue_link }
+
+ it { is_expected.to validate_presence_of(:source) }
+ it { is_expected.to validate_presence_of(:target) }
+ it do
+ is_expected.to validate_uniqueness_of(:source)
+ .scoped_to(:target_id)
+ .with_message(/already related/)
+ end
+
+ context 'self relation' do
+ let(:issue) { create :issue }
+
+ context 'cannot be validated' do
+ it 'does not invalidate object with self relation error' do
+ issue_link = build :issue_link, source: issue, target: nil
+
+ issue_link.valid?
+
+ expect(issue_link.errors[:source]).to be_empty
+ end
+ end
+
+ context 'can be invalidated' do
+ it 'invalidates object' do
+ issue_link = build :issue_link, source: issue, target: issue
+
+ expect(issue_link).to be_invalid
+ expect(issue_link.errors[:source]).to include('cannot be related to itself')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 59634524e74..283d945157b 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -133,6 +133,7 @@ RSpec.describe Issue do
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:incident) { create(:incident, project: project) }
+ let_it_be(:test_case) { create(:quality_test_case, project: project) }
it 'gives issues with the given issue type' do
expect(described_class.with_issue_type('issue'))
@@ -140,8 +141,8 @@ RSpec.describe Issue do
end
it 'gives issues with the given issue type' do
- expect(described_class.with_issue_type(%w(issue incident)))
- .to contain_exactly(issue, incident)
+ expect(described_class.with_issue_type(%w(issue incident test_case)))
+ .to contain_exactly(issue, incident, test_case)
end
end
@@ -311,6 +312,50 @@ RSpec.describe Issue do
end
end
+ describe '#related_issues' do
+ let(:user) { create(:user) }
+ let(:authorized_project) { create(:project) }
+ let(:authorized_project2) { create(:project) }
+ let(:unauthorized_project) { create(:project) }
+
+ let(:authorized_issue_a) { create(:issue, project: authorized_project) }
+ let(:authorized_issue_b) { create(:issue, project: authorized_project) }
+ let(:authorized_issue_c) { create(:issue, project: authorized_project2) }
+
+ let(:unauthorized_issue) { create(:issue, project: unauthorized_project) }
+
+ let!(:issue_link_a) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_b) }
+ let!(:issue_link_b) { create(:issue_link, source: authorized_issue_a, target: unauthorized_issue) }
+ let!(:issue_link_c) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_c) }
+
+ before do
+ authorized_project.add_developer(user)
+ authorized_project2.add_developer(user)
+ end
+
+ it 'returns only authorized related issues for given user' do
+ expect(authorized_issue_a.related_issues(user))
+ .to contain_exactly(authorized_issue_b, authorized_issue_c)
+ end
+
+ it 'returns issues with valid issue_link_type' do
+ link_types = authorized_issue_a.related_issues(user).map(&:issue_link_type)
+
+ expect(link_types).not_to be_empty
+ expect(link_types).not_to include(nil)
+ end
+
+ describe 'when a user cannot read cross project' do
+ it 'only returns issues within the same project' do
+ expect(Ability).to receive(:allowed?).with(user, :read_all_resources, :global).at_least(:once).and_call_original
+ expect(Ability).to receive(:allowed?).with(user, :read_cross_project).and_return(false)
+
+ expect(authorized_issue_a.related_issues(user))
+ .to contain_exactly(authorized_issue_b)
+ end
+ end
+ end
+
describe '#can_move?' do
let(:issue) { create(:issue) }
@@ -1139,4 +1184,33 @@ RSpec.describe Issue do
expect(context[:label_url_method]).to eq(:project_issues_url)
end
end
+
+ describe 'scheduling rebalancing' do
+ before do
+ allow_next_instance_of(RelativePositioning::Mover) do |mover|
+ allow(mover).to receive(:move) { raise ActiveRecord::QueryCanceled }
+ end
+ end
+
+ let(:project) { build_stubbed(:project_empty_repo) }
+ let(:issue) { build_stubbed(:issue, relative_position: 100, project: project) }
+
+ it 'schedules rebalancing if we time-out when moving' do
+ lhs = build_stubbed(:issue, relative_position: 99, project: project)
+ to_move = build(:issue, project: project)
+ expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
+
+ expect { to_move.move_between(lhs, issue) }.to raise_error(ActiveRecord::QueryCanceled)
+ end
+ end
+
+ describe '#allows_reviewers?' do
+ it 'returns false as issues do not support reviewers feature' do
+ stub_feature_flags(merge_request_reviewers: true)
+
+ issue = build_stubbed(:issue)
+
+ expect(issue.allows_reviewers?).to be(false)
+ end
+ end
end
diff --git a/spec/models/iteration_spec.rb b/spec/models/iteration_spec.rb
index 5c684fa9771..19a1625aad3 100644
--- a/spec/models/iteration_spec.rb
+++ b/spec/models/iteration_spec.rb
@@ -32,6 +32,59 @@ RSpec.describe Iteration do
end
end
+ describe '.filter_by_state' do
+ let_it_be(:closed_iteration) { create(:iteration, :closed, :skip_future_date_validation, group: group, start_date: 8.days.ago, due_date: 2.days.ago) }
+ let_it_be(:started_iteration) { create(:iteration, :started, :skip_future_date_validation, group: group, start_date: 1.day.ago, due_date: 6.days.from_now) }
+ let_it_be(:upcoming_iteration) { create(:iteration, :upcoming, group: group, start_date: 1.week.from_now, due_date: 2.weeks.from_now) }
+
+ shared_examples_for 'filter_by_state' do
+ it 'filters by the given state' do
+ expect(described_class.filter_by_state(Iteration.all, state)).to match(expected_iterations)
+ end
+ end
+
+ context 'filtering by closed iterations' do
+ it_behaves_like 'filter_by_state' do
+ let(:state) { 'closed' }
+ let(:expected_iterations) { [closed_iteration] }
+ end
+ end
+
+ context 'filtering by started iterations' do
+ it_behaves_like 'filter_by_state' do
+ let(:state) { 'started' }
+ let(:expected_iterations) { [started_iteration] }
+ end
+ end
+
+ context 'filtering by opened iterations' do
+ it_behaves_like 'filter_by_state' do
+ let(:state) { 'opened' }
+ let(:expected_iterations) { [started_iteration, upcoming_iteration] }
+ end
+ end
+
+ context 'filtering by upcoming iterations' do
+ it_behaves_like 'filter_by_state' do
+ let(:state) { 'upcoming' }
+ let(:expected_iterations) { [upcoming_iteration] }
+ end
+ end
+
+ context 'filtering by "all"' do
+ it_behaves_like 'filter_by_state' do
+ let(:state) { 'all' }
+ let(:expected_iterations) { [closed_iteration, started_iteration, upcoming_iteration] }
+ end
+ end
+
+ context 'filtering by nonexistent filter' do
+ it 'raises ArgumentError' do
+ expect { described_class.filter_by_state(Iteration.none, 'unknown') }.to raise_error(ArgumentError, 'Unknown state filter: unknown')
+ end
+ end
+ end
+
context 'Validations' do
subject { build(:iteration, group: group, start_date: start_date, due_date: due_date) }
diff --git a/spec/models/jira_connect_installation_spec.rb b/spec/models/jira_connect_installation_spec.rb
new file mode 100644
index 00000000000..8ef96114c45
--- /dev/null
+++ b/spec/models/jira_connect_installation_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe JiraConnectInstallation do
+ describe 'associations' do
+ it { is_expected.to have_many(:subscriptions).class_name('JiraConnectSubscription') }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:client_key) }
+ it { is_expected.to validate_uniqueness_of(:client_key) }
+ it { is_expected.to validate_presence_of(:shared_secret) }
+ it { is_expected.to validate_presence_of(:base_url) }
+
+ it { is_expected.to allow_value('https://test.atlassian.net').for(:base_url) }
+ it { is_expected.not_to allow_value('not/a/url').for(:base_url) }
+ end
+
+ describe '.for_project' do
+ let(:other_group) { create(:group) }
+ let(:parent_group) { create(:group) }
+ let(:group) { create(:group, parent: parent_group) }
+ let(:project) { create(:project, group: group) }
+
+ subject { described_class.for_project(project) }
+
+ it 'returns installations with subscriptions for project' do
+ sub_on_project_namespace = create(:jira_connect_subscription, namespace: group)
+ sub_on_ancestor_namespace = create(:jira_connect_subscription, namespace: parent_group)
+
+ # Subscription on other group that shouldn't be returned
+ create(:jira_connect_subscription, namespace: other_group)
+
+ expect(subject).to contain_exactly(sub_on_project_namespace.installation, sub_on_ancestor_namespace.installation)
+ end
+
+ it 'returns distinct installations' do
+ subscription = create(:jira_connect_subscription, namespace: group)
+ create(:jira_connect_subscription, namespace: parent_group, installation: subscription.installation)
+
+ expect(subject).to contain_exactly(subscription.installation)
+ end
+ end
+end
diff --git a/spec/models/jira_connect_subscription_spec.rb b/spec/models/jira_connect_subscription_spec.rb
new file mode 100644
index 00000000000..548c030f4c4
--- /dev/null
+++ b/spec/models/jira_connect_subscription_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe JiraConnectSubscription do
+ describe 'associations' do
+ it { is_expected.to belong_to(:installation).class_name('JiraConnectInstallation') }
+ it { is_expected.to belong_to(:namespace).class_name('Namespace') }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:installation) }
+ it { is_expected.to validate_presence_of(:namespace) }
+ end
+end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 52f47f0a211..39807747cc0 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -16,14 +16,14 @@ RSpec.describe Member do
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:source) }
- it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.all_values) }
it_behaves_like 'an object with email-formated attributes', :invite_email do
subject { build(:project_member) }
end
context "when an invite email is provided" do
- let(:member) { build(:project_member, invite_email: "user@example.com", user: nil) }
+ let_it_be(:project) { create(:project) }
+ let(:member) { build(:project_member, source: project, invite_email: "user@example.com", user: nil) }
it "doesn't require a user" do
expect(member).to be_valid
@@ -149,6 +149,7 @@ RSpec.describe Member do
accepted_request_user = create(:user).tap { |u| project.request_access(u) }
@accepted_request_member = project.requesters.find_by(user_id: accepted_request_user.id).tap { |m| m.accept_request }
+ @member_with_minimal_access = create(:group_member, :minimal_access, source: group)
end
describe '.access_for_user_ids' do
@@ -179,6 +180,15 @@ RSpec.describe Member do
it { expect(described_class.non_invite).to include @accepted_request_member }
end
+ describe '.non_minimal_access' do
+ it { expect(described_class.non_minimal_access).to include @maintainer }
+ it { expect(described_class.non_minimal_access).to include @invited_member }
+ it { expect(described_class.non_minimal_access).to include @accepted_invite_member }
+ it { expect(described_class.non_minimal_access).to include @requested_member }
+ it { expect(described_class.non_minimal_access).to include @accepted_request_member }
+ it { expect(described_class.non_minimal_access).not_to include @member_with_minimal_access }
+ end
+
describe '.request' do
it { expect(described_class.request).not_to include @maintainer }
it { expect(described_class.request).not_to include @invited_member }
@@ -256,6 +266,34 @@ RSpec.describe Member do
it { is_expected.not_to include @blocked_maintainer }
it { is_expected.not_to include @blocked_developer }
end
+
+ describe '.active' do
+ subject { described_class.active.to_a }
+
+ it { is_expected.to include @owner }
+ it { is_expected.to include @maintainer }
+ it { is_expected.to include @invited_member }
+ it { is_expected.to include @accepted_invite_member }
+ it { is_expected.not_to include @requested_member }
+ it { is_expected.to include @accepted_request_member }
+ it { is_expected.not_to include @blocked_maintainer }
+ it { is_expected.not_to include @blocked_developer }
+ it { is_expected.not_to include @member_with_minimal_access }
+ end
+
+ describe '.active_without_invites_and_requests' do
+ subject { described_class.active_without_invites_and_requests.to_a }
+
+ it { is_expected.to include @owner }
+ it { is_expected.to include @maintainer }
+ it { is_expected.not_to include @invited_member }
+ it { is_expected.to include @accepted_invite_member }
+ it { is_expected.not_to include @requested_member }
+ it { is_expected.to include @accepted_request_member }
+ it { is_expected.not_to include @blocked_maintainer }
+ it { is_expected.not_to include @blocked_developer }
+ it { is_expected.not_to include @member_with_minimal_access }
+ end
end
describe "Delegate methods" do
@@ -630,6 +668,32 @@ RSpec.describe Member do
end
end
+ describe '.find_by_invite_token' do
+ let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
+
+ it 'finds the member' do
+ expect(described_class.find_by_invite_token(member.raw_invite_token)).to eq member
+ end
+ end
+
+ describe "#invite_to_unknown_user?" do
+ subject { member.invite_to_unknown_user? }
+
+ let(:member) { create(:project_member, invite_email: "user@example.com", invite_token: '1234', user: user) }
+
+ context 'when user is nil' do
+ let(:user) { nil }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when user is set' do
+ let(:user) { build(:user) }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
describe "destroying a record", :delete do
it "refreshes user's authorized projects" do
project = create(:project, :private)
@@ -655,7 +719,7 @@ RSpec.describe Member do
describe 'create member' do
let!(:source) { create(source_type) }
- subject { create(member_type, :guest, user: user, source_type => source) }
+ subject { create(member_type, :guest, user: user, source: source) }
include_examples 'update highest role with exclusive lease'
end
diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb
index 82402b95597..760eaf1ac7f 100644
--- a/spec/models/merge_request/metrics_spec.rb
+++ b/spec/models/merge_request/metrics_spec.rb
@@ -9,17 +9,6 @@ RSpec.describe MergeRequest::Metrics do
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) } }
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
index 84fdfae1ed1..2edf44ecdc4 100644
--- a/spec/models/merge_request_diff_commit_spec.rb
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -7,7 +7,12 @@ RSpec.describe MergeRequestDiffCommit do
let(:project) { merge_request.project }
it_behaves_like 'a BulkInsertSafe model', MergeRequestDiffCommit do
- let(:valid_items_for_bulk_insertion) { build_list(:merge_request_diff_commit, 10) }
+ let(:valid_items_for_bulk_insertion) do
+ build_list(:merge_request_diff_commit, 10) do |mr_diff_commit|
+ mr_diff_commit.merge_request_diff = create(:merge_request_diff)
+ end
+ end
+
let(:invalid_items_for_bulk_insertion) { [] } # class does not have any validations defined
end
diff --git a/spec/models/merge_request_diff_file_spec.rb b/spec/models/merge_request_diff_file_spec.rb
index 25971f63338..5a48438adab 100644
--- a/spec/models/merge_request_diff_file_spec.rb
+++ b/spec/models/merge_request_diff_file_spec.rb
@@ -4,7 +4,12 @@ require 'spec_helper'
RSpec.describe MergeRequestDiffFile do
it_behaves_like 'a BulkInsertSafe model', MergeRequestDiffFile do
- let(:valid_items_for_bulk_insertion) { build_list(:merge_request_diff_file, 10) }
+ let(:valid_items_for_bulk_insertion) do
+ build_list(:merge_request_diff_file, 10) do |mr_diff_file|
+ mr_diff_file.merge_request_diff = create(:merge_request_diff)
+ end
+ end
+
let(:invalid_items_for_bulk_insertion) { [] } # class does not have any validations defined
end
@@ -25,6 +30,14 @@ RSpec.describe MergeRequestDiffFile do
it 'unpacks from base 64' do
expect(subject.diff).to eq(unpacked)
end
+
+ context 'invalid base64' do
+ let(:packed) { '---/dev/null' }
+
+ it 'returns the raw diff' do
+ expect(subject.diff).to eq(packed)
+ end
+ end
end
context 'when the diff is not marked as binary' do
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index e02c71a1c6f..2c64201e84d 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -293,6 +293,7 @@ RSpec.describe MergeRequestDiff do
it 'does nothing with an empty diff' do
stub_external_diffs_setting(enabled: true)
MergeRequestDiffFile.where(merge_request_diff_id: diff.id).delete_all
+ diff.save! # update files_count
expect(diff).not_to receive(:update!)
@@ -675,8 +676,39 @@ RSpec.describe MergeRequestDiff do
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)
+ let_it_be(:merge_request) { create(:merge_request) }
+
+ let(:diff) { merge_request.merge_request_diff }
+ let(:actual_count) { diff.merge_request_diff_files.count }
+
+ it 'is set by default' do
+ expect(diff.read_attribute(:files_count)).to eq(actual_count)
+ end
+
+ it 'is set to the sentinel value if the actual value exceeds it' do
+ stub_const("#{described_class}::FILES_COUNT_SENTINEL", actual_count - 1)
+
+ diff.save! # update the files_count column with the stub in place
+
+ expect(diff.read_attribute(:files_count)).to eq(described_class::FILES_COUNT_SENTINEL)
+ end
+
+ it 'uses the cached count if present' do
+ diff.update_columns(files_count: actual_count + 1)
+
+ expect(diff.files_count).to eq(actual_count + 1)
+ end
+
+ it 'uses the actual count if nil' do
+ diff.update_columns(files_count: nil)
+
+ expect(diff.files_count).to eq(actual_count)
+ end
+
+ it 'uses the actual count if overflown' do
+ diff.update_columns(files_count: described_class::FILES_COUNT_SENTINEL)
+
+ expect(diff.files_count).to eq(actual_count)
end
end
diff --git a/spec/models/merge_request_reviewer_spec.rb b/spec/models/merge_request_reviewer_spec.rb
new file mode 100644
index 00000000000..55199cd96ad
--- /dev/null
+++ b/spec/models/merge_request_reviewer_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequestReviewer do
+ let(:merge_request) { create(:merge_request) }
+
+ subject { merge_request.merge_request_reviewers.build(reviewer: create(:user)) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:merge_request).class_name('MergeRequest') }
+ it { is_expected.to belong_to(:reviewer).class_name('User') }
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 6edef54b153..98f709a0610 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2,13 +2,16 @@
require 'spec_helper'
-RSpec.describe MergeRequest do
+RSpec.describe MergeRequest, factory_default: :keep do
include RepoHelpers
include ProjectForksHelper
include ReactiveCachingHelpers
using RSpec::Parameterized::TableSyntax
+ let_it_be(:namespace) { create_default(:namespace) }
+ let_it_be(:project, refind: true) { create_default(:project, :repository) }
+
subject { create(:merge_request) }
describe 'associations' do
@@ -18,6 +21,7 @@ RSpec.describe MergeRequest do
it { is_expected.to belong_to(:source_project).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
it { is_expected.to have_many(:assignees).through(:merge_request_assignees) }
+ it { is_expected.to have_many(:reviewers).through(:merge_request_reviewers) }
it { is_expected.to have_many(:merge_request_diffs) }
it { is_expected.to have_many(:user_mentions).class_name("MergeRequestUserMention") }
it { is_expected.to belong_to(:milestone) }
@@ -57,6 +61,24 @@ RSpec.describe MergeRequest do
end
end
+ describe '.order_merged_at_asc' do
+ let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) }
+ let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) }
+
+ it 'returns MRs ordered by merged_at ascending' do
+ expect(described_class.order_merged_at_asc).to eq([older_mr, newer_mr])
+ end
+ end
+
+ describe '.order_merged_at_desc' do
+ let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) }
+ let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) }
+
+ it 'returns MRs ordered by merged_at descending' do
+ expect(described_class.order_merged_at_desc).to eq([newer_mr, older_mr])
+ end
+ end
+
describe '#squash_in_progress?' do
let(:repo_path) do
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
@@ -282,18 +304,6 @@ RSpec.describe MergeRequest do
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
@@ -359,7 +369,7 @@ RSpec.describe MergeRequest do
it 'returns merge requests that match the given merge commit' do
note = create(:track_mr_picking_note, commit_id: '456abc')
- create(:track_mr_picking_note, commit_id: '456def')
+ create(:track_mr_picking_note, project: create(:project), commit_id: '456def')
expect(described_class.by_cherry_pick_sha('456abc')).to eq([note.noteable])
end
@@ -427,6 +437,23 @@ RSpec.describe MergeRequest do
end
end
+ describe '.sort_by_attribute' do
+ context 'merged_at' do
+ let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) }
+ let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) }
+
+ it 'sorts asc' do
+ merge_requests = described_class.sort_by_attribute(:merged_at_asc)
+ expect(merge_requests).to eq([older_mr, newer_mr])
+ end
+
+ it 'sorts desc' do
+ merge_requests = described_class.sort_by_attribute(:merged_at_desc)
+ expect(merge_requests).to eq([newer_mr, older_mr])
+ end
+ end
+ end
+
describe '#target_branch_sha' do
let(:project) { create(:project, :repository) }
@@ -831,7 +858,7 @@ RSpec.describe MergeRequest do
end
context 'with commit diff note' do
- let(:other_merge_request) { create(:merge_request) }
+ let(:other_merge_request) { create(:merge_request, source_project: create(:project, :repository)) }
let!(:diff_note) do
create(:diff_note_on_commit, project: merge_request.project)
@@ -854,8 +881,10 @@ RSpec.describe MergeRequest do
end
describe '#diff_size' do
+ let_it_be(:project) { create(:project, :repository) }
+
let(:merge_request) do
- build(:merge_request, source_branch: 'expand-collapse-files', target_branch: 'master')
+ build(:merge_request, source_project: project, source_branch: 'expand-collapse-files', target_branch: 'master')
end
context 'when there are MR diffs' do
@@ -1030,6 +1059,8 @@ RSpec.describe MergeRequest do
end
describe '#closes_issues' do
+ let(:project) { create(:project) }
+
let(:issue0) { create :issue, project: subject.project }
let(:issue1) { create :issue, project: subject.project }
@@ -1037,6 +1068,8 @@ RSpec.describe MergeRequest do
let(:commit1) { double('commit1', safe_message: "Fixes #{issue0.to_reference}") }
let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") }
+ subject { create(:merge_request, source_project: project) }
+
before do
subject.project.add_developer(subject.author)
allow(subject).to receive(:commits).and_return([commit0, commit1, commit2])
@@ -1087,6 +1120,8 @@ RSpec.describe MergeRequest do
end
context 'when the project has an external issue tracker' do
+ subject { create(:merge_request, source_project: create(:project, :repository)) }
+
before do
subject.project.add_developer(subject.author)
commit = double(:commit, safe_message: 'Fixes TEST-3')
@@ -1253,14 +1288,13 @@ RSpec.describe MergeRequest do
end
describe "#source_branch_exists?" do
- let(:merge_request) { subject }
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
let(:repository) { merge_request.source_project.repository }
context 'when the source project is set' do
- it 'memoizes the value and returns the result' do
- expect(repository).to receive(:branch_exists?).once.with(merge_request.source_branch).and_return(true)
-
- 2.times { expect(merge_request.source_branch_exists?).to eq(true) }
+ it 'returns true when the branch exists' do
+ expect(merge_request.source_branch_exists?).to eq(true)
end
end
@@ -1480,7 +1514,9 @@ RSpec.describe MergeRequest do
end
context 'new merge request' do
- subject { build(:merge_request) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ subject { build(:merge_request, source_project: project) }
context 'compare commits' do
before do
@@ -1575,11 +1611,36 @@ RSpec.describe MergeRequest do
before do
subject.mark_as_merged!
- subject.update_attribute(:merge_commit_sha, pipeline.sha)
end
- it 'returns the post-merge pipeline' do
- expect(subject.merge_pipeline).to eq(pipeline)
+ context 'and there is a merge commit' do
+ before do
+ subject.update_attribute(:merge_commit_sha, pipeline.sha)
+ end
+
+ it 'returns the pipeline associated with that merge request' do
+ expect(subject.merge_pipeline).to eq(pipeline)
+ end
+ end
+
+ context 'and there is no merge commit, but there is a diff head' do
+ before do
+ allow(subject).to receive(:diff_head_sha).and_return(pipeline.sha)
+ end
+
+ it 'returns the pipeline associated with that merge request' do
+ expect(subject.merge_pipeline).to eq(pipeline)
+ end
+ end
+
+ context 'and there is no merge commit, but there is a squash commit' do
+ before do
+ subject.update_attribute(:squash_commit_sha, pipeline.sha)
+ end
+
+ it 'returns the pipeline associated with that merge request' do
+ expect(subject.merge_pipeline).to eq(pipeline)
+ end
end
end
end
@@ -1706,16 +1767,14 @@ RSpec.describe MergeRequest do
describe '#has_test_reports?' do
subject { merge_request.has_test_reports? }
- let(:project) { create(:project, :repository) }
-
context 'when head pipeline has test reports' do
- let(:merge_request) { create(:merge_request, :with_test_reports, source_project: project) }
+ let(:merge_request) { create(:merge_request, :with_test_reports) }
it { is_expected.to be_truthy }
end
context 'when head pipeline does not have test reports' do
- let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:merge_request) { create(:merge_request) }
it { is_expected.to be_falsey }
end
@@ -1724,16 +1783,14 @@ RSpec.describe MergeRequest do
describe '#has_accessibility_reports?' do
subject { merge_request.has_accessibility_reports? }
- let(:project) { create(:project, :repository) }
-
context 'when head pipeline has an accessibility reports' do
- let(:merge_request) { create(:merge_request, :with_accessibility_reports, source_project: project) }
+ let(:merge_request) { create(:merge_request, :with_accessibility_reports) }
it { is_expected.to be_truthy }
end
context 'when head pipeline does not have accessibility reports' do
- let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:merge_request) { create(:merge_request) }
it { is_expected.to be_falsey }
end
@@ -1742,27 +1799,23 @@ RSpec.describe MergeRequest do
describe '#has_coverage_reports?' do
subject { merge_request.has_coverage_reports? }
- let(:project) { create(:project, :repository) }
-
context 'when head pipeline has coverage reports' do
- let(:merge_request) { create(:merge_request, :with_coverage_reports, source_project: project) }
+ let(:merge_request) { create(:merge_request, :with_coverage_reports) }
it { is_expected.to be_truthy }
end
context 'when head pipeline does not have coverage reports' do
- let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:merge_request) { create(:merge_request) }
it { is_expected.to be_falsey }
end
end
describe '#has_terraform_reports?' do
- let_it_be(:project) { create(:project, :repository) }
-
context 'when head pipeline has terraform reports' do
it 'returns true' do
- merge_request = create(:merge_request, :with_terraform_reports, source_project: project)
+ merge_request = create(:merge_request, :with_terraform_reports)
expect(merge_request.has_terraform_reports?).to be_truthy
end
@@ -1770,7 +1823,7 @@ RSpec.describe MergeRequest do
context 'when head pipeline does not have terraform reports' do
it 'returns false' do
- merge_request = create(:merge_request, source_project: project)
+ merge_request = create(:merge_request)
expect(merge_request.has_terraform_reports?).to be_falsey
end
@@ -1778,8 +1831,7 @@ RSpec.describe MergeRequest do
end
describe '#calculate_reactive_cache' do
- let(:project) { create(:project, :repository) }
- let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:merge_request) { create(:merge_request) }
subject { merge_request.calculate_reactive_cache(service_class_name) }
@@ -1867,12 +1919,6 @@ RSpec.describe MergeRequest do
subject { merge_request.find_coverage_reports }
context 'when head pipeline has coverage reports' do
- let!(:job) do
- create(:ci_build, options: { artifacts: { reports: { cobertura: ['cobertura-coverage.xml'] } } }, pipeline: pipeline)
- end
-
- let!(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) }
-
context 'when reactive cache worker is parsing results asynchronously' do
it 'returns status' do
expect(subject[:status]).to eq(:parsing)
@@ -2074,11 +2120,13 @@ RSpec.describe MergeRequest do
end
context 'when merge request is not persisted' do
+ let_it_be(:project) { create(:project, :repository) }
+
context 'when compare commits are set in the service' do
let(:commit) { spy('commit') }
subject do
- build(:merge_request, compare_commits: [commit, commit])
+ build(:merge_request, source_project: project, compare_commits: [commit, commit])
end
it 'returns commits from compare commits temporary data' do
@@ -2087,7 +2135,7 @@ RSpec.describe MergeRequest do
end
context 'when compare commits are not set in the service' do
- subject { build(:merge_request) }
+ subject { build(:merge_request, source_project: project) }
it 'returns array with diff head sha element only' do
expect(subject.all_commit_shas).to eq [subject.diff_head_sha]
@@ -2112,7 +2160,63 @@ RSpec.describe MergeRequest do
end
end
+ describe '#merged_commit_sha' do
+ it 'returns nil when not merged' do
+ expect(subject.merged_commit_sha).to be_nil
+ end
+
+ context 'when the MR is merged' do
+ let(:sha) { 'f7ce827c314c9340b075657fd61c789fb01cf74d' }
+
+ before do
+ subject.mark_as_merged!
+ end
+
+ it 'returns merge_commit_sha when there is a merge_commit_sha' do
+ subject.update_attribute(:merge_commit_sha, sha)
+
+ expect(subject.merged_commit_sha).to eq(sha)
+ end
+
+ it 'returns squash_commit_sha when there is a squash_commit_sha' do
+ subject.update_attribute(:squash_commit_sha, sha)
+
+ expect(subject.merged_commit_sha).to eq(sha)
+ end
+
+ it 'returns diff_head_sha when there are no merge_commit_sha and squash_commit_sha' do
+ allow(subject).to receive(:diff_head_sha).and_return(sha)
+
+ expect(subject.merged_commit_sha).to eq(sha)
+ end
+ end
+ end
+
+ describe '#short_merged_commit_sha' do
+ context 'when merged_commit_sha is nil' do
+ before do
+ allow(subject).to receive(:merged_commit_sha).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(subject.short_merged_commit_sha).to be_nil
+ end
+ end
+
+ context 'when merged_commit_sha is present' do
+ before do
+ allow(subject).to receive(:merged_commit_sha).and_return('f7ce827c314c9340b075657fd61c789fb01cf74d')
+ end
+
+ it 'returns shortened merged_commit_sha' do
+ expect(subject.short_merged_commit_sha).to eq('f7ce827c')
+ end
+ end
+ end
+
describe '#can_be_reverted?' do
+ subject { create(:merge_request, source_project: create(:project, :repository)) }
+
context 'when there is no merge_commit for the MR' do
before do
subject.metrics.update!(merged_at: Time.current.utc)
@@ -2301,8 +2405,6 @@ RSpec.describe MergeRequest do
end
describe '#participants' do
- let(:project) { create(:project, :public) }
-
let(:mr) do
create(:merge_request, source_project: project, target_project: project)
end
@@ -2410,9 +2512,7 @@ RSpec.describe MergeRequest do
end
describe '#mergeable?' do
- let(:project) { create(:project) }
-
- subject { create(:merge_request, source_project: project) }
+ subject { build_stubbed(:merge_request) }
it 'returns false if #mergeable_state? is false' do
expect(subject).to receive(:mergeable_state?) { false }
@@ -2427,6 +2527,57 @@ RSpec.describe MergeRequest do
expect(subject.mergeable?).to be_truthy
end
+
+ context 'with skip_ci_check option' do
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ allow(subject).to receive_messages(check_mergeability: nil,
+ can_be_merged?: true,
+ broken?: false)
+ end
+
+ where(:mergeable_ci_state, :skip_ci_check, :expected_mergeable) do
+ false | false | false
+ false | true | true
+ true | false | true
+ true | true | true
+ end
+
+ with_them do
+ it 'overrides mergeable_ci_state?' do
+ allow(subject).to receive(:mergeable_ci_state?) { mergeable_ci_state }
+
+ expect(subject.mergeable?(skip_ci_check: skip_ci_check)).to eq(expected_mergeable)
+ end
+ end
+ end
+
+ context 'with skip_discussions_check option' do
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ allow(subject).to receive_messages(mergeable_ci_state?: true,
+ check_mergeability: nil,
+ can_be_merged?: true,
+ broken?: false)
+ end
+
+ where(:mergeable_discussions_state, :skip_discussions_check, :expected_mergeable) do
+ false | false | false
+ false | true | true
+ true | false | true
+ true | true | true
+ end
+
+ with_them do
+ it 'overrides mergeable_discussions_state?' do
+ allow(subject).to receive(:mergeable_discussions_state?) { mergeable_discussions_state }
+
+ expect(subject.mergeable?(skip_discussions_check: skip_discussions_check)).to eq(expected_mergeable)
+ end
+ end
+ end
end
describe '#check_mergeability' do
@@ -2482,9 +2633,7 @@ RSpec.describe MergeRequest do
end
describe '#mergeable_state?' do
- let(:project) { create(:project, :repository) }
-
- subject { create(:merge_request, source_project: project) }
+ subject { create(:merge_request) }
it 'checks if merge request can be merged' do
allow(subject).to receive(:mergeable_ci_state?) { true }
@@ -2532,6 +2681,10 @@ RSpec.describe MergeRequest do
it 'returns false' do
expect(subject.mergeable_state?).to be_falsey
end
+
+ it 'returns true when skipping ci check' do
+ expect(subject.mergeable_state?(skip_ci_check: true)).to be(true)
+ end
end
context 'when #mergeable_discussions_state? is false' do
@@ -2599,9 +2752,9 @@ RSpec.describe MergeRequest do
let(:pipeline) { create(:ci_empty_pipeline) }
context 'when it is only allowed to merge when build is green' do
- let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true) }
+ let_it_be(:project) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true) }
- subject { build(:merge_request, target_project: project) }
+ subject { build(:merge_request, source_project: project) }
context 'and a failed pipeline is associated' do
before do
@@ -2640,9 +2793,9 @@ RSpec.describe MergeRequest do
end
context 'when it is only allowed to merge when build is green or skipped' do
- let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true, allow_merge_on_skipped_pipeline: true) }
+ let_it_be(:project) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true, allow_merge_on_skipped_pipeline: true) }
- subject { build(:merge_request, target_project: project) }
+ subject { build(:merge_request, source_project: project) }
context 'and a failed pipeline is associated' do
before do
@@ -2681,9 +2834,9 @@ RSpec.describe MergeRequest do
end
context 'when merges are not restricted to green builds' do
- let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: false) }
+ let_it_be(:project) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: false) }
- subject { build(:merge_request, target_project: project) }
+ subject { build(:merge_request, source_project: project) }
context 'and a failed pipeline is associated' do
before do
@@ -2725,7 +2878,7 @@ RSpec.describe MergeRequest do
let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) }
context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do
- let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
+ let_it_be(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
context 'with all discussions resolved' do
before do
@@ -2974,6 +3127,10 @@ RSpec.describe MergeRequest do
end
describe '#branch_merge_base_commit' do
+ let(:project) { create(:project, :repository) }
+
+ subject { create(:merge_request, :with_diffs, source_project: project) }
+
context 'source and target branch exist' do
it { expect(subject.branch_merge_base_commit.sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
it { expect(subject.branch_merge_base_commit).to be_a(Commit) }
@@ -2993,7 +3150,9 @@ RSpec.describe MergeRequest do
describe "#diff_refs" do
context "with diffs" do
- subject { create(:merge_request, :with_diffs) }
+ let(:project) { create(:project, :repository) }
+
+ subject { create(:merge_request, :with_diffs, source_project: project) }
let(:expected_diff_refs) do
Gitlab::Diff::DiffRefs.new(
@@ -3195,7 +3354,8 @@ RSpec.describe MergeRequest do
pipeline
end
- let(:project) { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) }
+ let_it_be(:project) { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) }
+
let(:developer) { create(:user) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -3289,8 +3449,7 @@ RSpec.describe MergeRequest do
end
describe '#pipeline_coverage_delta' do
- let!(:project) { create(:project, :repository) }
- let!(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:merge_request) { create(:merge_request) }
let!(:source_pipeline) do
create(:ci_pipeline,
@@ -3396,7 +3555,9 @@ RSpec.describe MergeRequest do
end
describe '#merge_request_diff_for' do
- subject { create(:merge_request, importing: true) }
+ let(:project) { create(:project, :repository) }
+
+ subject { create(:merge_request, importing: true, source_project: project) }
let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
@@ -3427,9 +3588,10 @@ RSpec.describe MergeRequest do
end
describe '#version_params_for' do
- subject { create(:merge_request, importing: true) }
+ let(:project) { create(:project, :repository) }
+
+ subject { create(:merge_request, importing: true, source_project: project) }
- let(:project) { subject.project }
let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
@@ -3460,6 +3622,10 @@ RSpec.describe MergeRequest do
end
describe '#fetch_ref!' do
+ let(:project) { create(:project, :repository) }
+
+ subject { create(:merge_request, :with_diffs, source_project: project) }
+
it 'fetches the ref correctly' do
expect { subject.target_project.repository.delete_refs(subject.ref_path) }.not_to raise_error
@@ -3482,8 +3648,10 @@ RSpec.describe MergeRequest do
end
context 'state machine transitions' do
+ let(:project) { create(:project, :repository) }
+
describe '#unlock_mr' do
- subject { create(:merge_request, state: 'locked', merge_jid: 123) }
+ subject { create(:merge_request, state: 'locked', source_project: project, merge_jid: 123) }
it 'updates merge request head pipeline and sets merge_jid to nil', :sidekiq_might_not_need_inline do
pipeline = create(:ci_empty_pipeline, project: subject.project, ref: subject.source_branch, sha: subject.source_branch_sha)
@@ -3500,7 +3668,7 @@ RSpec.describe MergeRequest do
let(:notification_service) { double(:notification_service) }
let(:todo_service) { double(:todo_service) }
- subject { create(:merge_request, state, merge_status: :unchecked) }
+ subject { create(:merge_request, state, source_project: project, merge_status: :unchecked) }
before do
allow(NotificationService).to receive(:new).and_return(notification_service)
@@ -3589,7 +3757,7 @@ RSpec.describe MergeRequest do
end
context 'source branch is missing' do
- subject { create(:merge_request, :invalid, :opened, merge_status: :unchecked, target_branch: 'master') }
+ subject { create(:merge_request, :invalid, :opened, source_project: project, merge_status: :unchecked, target_branch: 'master') }
before do
allow(subject.project.repository).to receive(:can_be_merged?).and_call_original
@@ -3622,10 +3790,8 @@ RSpec.describe MergeRequest do
end
describe '#should_be_rebased?' do
- let(:project) { create(:project, :repository) }
-
it 'returns false for the same source and target branches' do
- merge_request = create(:merge_request, source_project: project, target_project: project)
+ merge_request = build_stubbed(:merge_request, source_project: project, target_project: project)
expect(merge_request.should_be_rebased?).to be_falsey
end
@@ -3640,7 +3806,7 @@ RSpec.describe MergeRequest do
end
with_them do
- let(:merge_request) { create(:merge_request) }
+ let(:merge_request) { build_stubbed(:merge_request) }
subject { merge_request.rebase_in_progress? }
@@ -3863,7 +4029,7 @@ RSpec.describe MergeRequest do
describe '#cleanup_refs' do
subject { merge_request.cleanup_refs(only: only) }
- let(:merge_request) { build(:merge_request) }
+ let(:merge_request) { build(:merge_request, source_project: create(:project, :repository)) }
context 'when removing all refs' do
let(:only) { :all }
@@ -4084,4 +4250,58 @@ RSpec.describe MergeRequest do
expect(context[:label_url_method]).to eq(:project_merge_requests_url)
end
end
+
+ describe '#head_pipeline_builds_with_coverage' do
+ it 'delegates to head_pipeline' do
+ expect(subject)
+ .to delegate_method(:builds_with_coverage)
+ .to(:head_pipeline)
+ .with_prefix
+ .with_arguments(allow_nil: true)
+ end
+ end
+
+ describe '#allows_reviewers?' do
+ it 'returns false without merge_request_reviewers feature' do
+ stub_feature_flags(merge_request_reviewers: false)
+
+ merge_request = build_stubbed(:merge_request)
+
+ expect(merge_request.allows_reviewers?).to be(false)
+ end
+
+ it 'returns true with merge_request_reviewers feature' do
+ stub_feature_flags(merge_request_reviewers: true)
+
+ merge_request = build_stubbed(:merge_request)
+
+ expect(merge_request.allows_reviewers?).to be(true)
+ end
+ end
+
+ describe '#merge_ref_head' do
+ let(:merge_request) { create(:merge_request) }
+
+ context 'when merge_ref_sha is not present' do
+ let!(:result) do
+ MergeRequests::MergeToRefService
+ .new(merge_request.project, merge_request.author)
+ .execute(merge_request)
+ end
+
+ it 'returns the commit based on merge ref path' do
+ expect(merge_request.merge_ref_head.id).to eq(result[:commit_id])
+ end
+ end
+
+ context 'when merge_ref_sha is present' do
+ before do
+ merge_request.update!(merge_ref_sha: merge_request.project.repository.commit.id)
+ end
+
+ it 'returns the commit based on cached merge_ref_sha' do
+ expect(merge_request.merge_ref_head.id).to eq(merge_request.merge_ref_sha)
+ end
+ end
+ end
end
diff --git a/spec/models/metrics/dashboard/annotation_spec.rb b/spec/models/metrics/dashboard/annotation_spec.rb
index bd4baeb8851..4b7492016f3 100644
--- a/spec/models/metrics/dashboard/annotation_spec.rb
+++ b/spec/models/metrics/dashboard/annotation_spec.rb
@@ -100,7 +100,7 @@ RSpec.describe Metrics::Dashboard::Annotation do
describe '#ending_before' do
it 'returns annotations only for appointed dashboard' do
- Timecop.freeze do
+ freeze_time do
twelve_minutes_old_annotation = create(:metrics_dashboard_annotation, starting_at: 15.minutes.ago, ending_at: 12.minutes.ago)
create(:metrics_dashboard_annotation, starting_at: 15.minutes.ago, ending_at: 11.minutes.ago)
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index b52b035e130..e611484f5ee 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -3,6 +3,11 @@
require 'spec_helper'
RSpec.describe Milestone do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:project) { create(:project, :public) }
+
it_behaves_like 'a timebox', :milestone
describe 'MilestoneStruct#serializable_hash' do
@@ -47,11 +52,6 @@ RSpec.describe Milestone do
it { is_expected.to have_many(:milestone_releases) }
end
- let(:project) { create(:project, :public) }
- let(:milestone) { create(:milestone, project: project) }
- let(:issue) { create(:issue, project: project) }
- let(:user) { create(:user) }
-
describe '.predefined_id?' do
it 'returns true for a predefined Milestone ID' do
expect(Milestone.predefined_id?(described_class::Upcoming.id)).to be true
diff --git a/spec/models/namespace/root_storage_statistics_spec.rb b/spec/models/namespace/root_storage_statistics_spec.rb
index ce6f875ee09..92a8d17a2a8 100644
--- a/spec/models/namespace/root_storage_statistics_spec.rb
+++ b/spec/models/namespace/root_storage_statistics_spec.rb
@@ -44,6 +44,7 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
total_packages_size = stat1.packages_size + stat2.packages_size
total_storage_size = stat1.storage_size + stat2.storage_size
total_snippets_size = stat1.snippets_size + stat2.snippets_size
+ total_pipeline_artifacts_size = stat1.pipeline_artifacts_size + stat2.pipeline_artifacts_size
expect(root_storage_statistics.repository_size).to eq(total_repository_size)
expect(root_storage_statistics.wiki_size).to eq(total_wiki_size)
@@ -52,6 +53,7 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
expect(root_storage_statistics.packages_size).to eq(total_packages_size)
expect(root_storage_statistics.storage_size).to eq(total_storage_size)
expect(root_storage_statistics.snippets_size).to eq(total_snippets_size)
+ expect(root_storage_statistics.pipeline_artifacts_size).to eq(total_pipeline_artifacts_size)
end
it 'works when there are no projects' do
@@ -67,6 +69,7 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
expect(root_storage_statistics.packages_size).to eq(0)
expect(root_storage_statistics.storage_size).to eq(0)
expect(root_storage_statistics.snippets_size).to eq(0)
+ expect(root_storage_statistics.pipeline_artifacts_size).to eq(0)
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 4ef2ddd218a..ca1f06370d4 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -175,12 +175,13 @@ RSpec.describe Namespace do
end
describe '.with_statistics' do
- let(:namespace) { create :namespace }
+ let_it_be(:namespace) { create(:namespace) }
let(:project1) do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
+ namespace: namespace,
repository_size: 101,
wiki_size: 505,
lfs_objects_size: 202,
@@ -193,6 +194,7 @@ RSpec.describe Namespace do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
+ namespace: namespace,
repository_size: 10,
wiki_size: 50,
lfs_objects_size: 20,
@@ -391,14 +393,14 @@ RSpec.describe Namespace do
let(:uploads_dir) { FileUploader.root }
let(:pages_dir) { File.join(TestEnv.pages_path) }
- def expect_project_directories_at(namespace_path)
+ def expect_project_directories_at(namespace_path, with_pages: true)
expected_repository_path = File.join(TestEnv.repos_path, namespace_path, 'the-project.git')
expected_upload_path = File.join(uploads_dir, namespace_path, 'the-project')
expected_pages_path = File.join(pages_dir, namespace_path, 'the-project')
expect(File.directory?(expected_repository_path)).to be_truthy
expect(File.directory?(expected_upload_path)).to be_truthy
- expect(File.directory?(expected_pages_path)).to be_truthy
+ expect(File.directory?(expected_pages_path)).to be(with_pages)
end
before do
@@ -407,43 +409,156 @@ RSpec.describe Namespace do
FileUtils.mkdir_p(File.join(pages_dir, project.full_path))
end
+ after do
+ FileUtils.remove_entry(File.join(TestEnv.repos_path, parent.full_path), true)
+ FileUtils.remove_entry(File.join(TestEnv.repos_path, new_parent.full_path), true)
+ FileUtils.remove_entry(File.join(TestEnv.repos_path, child.full_path), true)
+ FileUtils.remove_entry(File.join(uploads_dir, project.full_path), true)
+ FileUtils.remove_entry(pages_dir, true)
+ end
+
context 'renaming child' do
- it 'correctly moves the repository, uploads and pages' do
- child.update!(path: 'renamed')
+ context 'when no projects have pages deployed' do
+ it 'moves the repository and uploads', :sidekiq_inline do
+ project.pages_metadatum.update!(deployed: false)
+ child.update!(path: 'renamed')
- expect_project_directories_at('parent/renamed')
+ expect_project_directories_at('parent/renamed', with_pages: false)
+ end
+ end
+
+ context 'when the project has pages deployed' do
+ before do
+ project.pages_metadatum.update!(deployed: true)
+ end
+
+ it 'correctly moves the repository, uploads and pages', :sidekiq_inline do
+ child.update!(path: 'renamed')
+
+ expect_project_directories_at('parent/renamed')
+ end
+
+ it 'performs the move async of pages async' do
+ expect(PagesTransferWorker).to receive(:perform_async).with('rename_namespace', ['parent/child', 'parent/renamed'])
+
+ child.update!(path: 'renamed')
+ end
end
end
context 'renaming parent' do
- it 'correctly moves the repository, uploads and pages' do
- parent.update!(path: 'renamed')
+ context 'when no projects have pages deployed' do
+ it 'moves the repository and uploads', :sidekiq_inline do
+ project.pages_metadatum.update!(deployed: false)
+ parent.update!(path: 'renamed')
- expect_project_directories_at('renamed/child')
+ expect_project_directories_at('renamed/child', with_pages: false)
+ end
+ end
+
+ context 'when the project has pages deployed' do
+ before do
+ project.pages_metadatum.update!(deployed: true)
+ end
+
+ it 'correctly moves the repository, uploads and pages', :sidekiq_inline do
+ parent.update!(path: 'renamed')
+
+ expect_project_directories_at('renamed/child')
+ end
+
+ it 'performs the move async of pages async' do
+ expect(PagesTransferWorker).to receive(:perform_async).with('rename_namespace', %w(parent renamed))
+
+ parent.update!(path: 'renamed')
+ end
end
end
context 'moving from one parent to another' do
- it 'correctly moves the repository, uploads and pages' do
- child.update!(parent: new_parent)
+ context 'when no projects have pages deployed' do
+ it 'moves the repository and uploads', :sidekiq_inline do
+ project.pages_metadatum.update!(deployed: false)
+ child.update!(parent: new_parent)
+
+ expect_project_directories_at('new_parent/child', with_pages: false)
+ end
+ end
- expect_project_directories_at('new_parent/child')
+ context 'when the project has pages deployed' do
+ before do
+ project.pages_metadatum.update!(deployed: true)
+ end
+
+ it 'correctly moves the repository, uploads and pages', :sidekiq_inline do
+ child.update!(parent: new_parent)
+
+ expect_project_directories_at('new_parent/child')
+ end
+
+ it 'performs the move async of pages async' do
+ expect(PagesTransferWorker).to receive(:perform_async).with('move_namespace', %w(child parent new_parent))
+
+ child.update!(parent: new_parent)
+ end
end
end
context 'moving from having a parent to root' do
- it 'correctly moves the repository, uploads and pages' do
- child.update!(parent: nil)
+ context 'when no projects have pages deployed' do
+ it 'moves the repository and uploads', :sidekiq_inline do
+ project.pages_metadatum.update!(deployed: false)
+ child.update!(parent: nil)
+
+ expect_project_directories_at('child', with_pages: false)
+ end
+ end
+
+ context 'when the project has pages deployed' do
+ before do
+ project.pages_metadatum.update!(deployed: true)
+ end
+
+ it 'correctly moves the repository, uploads and pages', :sidekiq_inline do
+ child.update!(parent: nil)
- expect_project_directories_at('child')
+ expect_project_directories_at('child')
+ end
+
+ it 'performs the move async of pages async' do
+ expect(PagesTransferWorker).to receive(:perform_async).with('move_namespace', ['child', 'parent', nil])
+
+ child.update!(parent: nil)
+ end
end
end
context 'moving from root to having a parent' do
- it 'correctly moves the repository, uploads and pages' do
- parent.update!(parent: new_parent)
+ context 'when no projects have pages deployed' do
+ it 'moves the repository and uploads', :sidekiq_inline do
+ project.pages_metadatum.update!(deployed: false)
+ parent.update!(parent: new_parent)
- expect_project_directories_at('new_parent/parent/child')
+ expect_project_directories_at('new_parent/parent/child', with_pages: false)
+ end
+ end
+
+ context 'when the project has pages deployed' do
+ before do
+ project.pages_metadatum.update!(deployed: true)
+ end
+
+ it 'correctly moves the repository, uploads and pages', :sidekiq_inline do
+ parent.update!(parent: new_parent)
+
+ expect_project_directories_at('new_parent/parent/child')
+ end
+
+ it 'performs the move async of pages async' do
+ expect(PagesTransferWorker).to receive(:perform_async).with('move_namespace', ['parent', nil, 'new_parent'])
+
+ parent.update!(parent: new_parent)
+ end
end
end
end
@@ -588,6 +703,21 @@ RSpec.describe Namespace do
end
end
+ describe ".clean_name" do
+ context "when the name complies with the group name regex" do
+ it "returns the name as is" do
+ valid_name = "Hello - World _ (Hi.)"
+ expect(described_class.clean_name(valid_name)).to eq(valid_name)
+ end
+ end
+
+ context "when the name does not comply with the group name regex" do
+ it "sanitizes the name by replacing all invalid char sequences with a space" do
+ expect(described_class.clean_name("Green'! Test~~~")).to eq("Green Test")
+ end
+ end
+ end
+
describe "#default_branch_protection" do
let(:namespace) { create(:namespace) }
let(:default_branch_protection) { nil }
@@ -1101,6 +1231,27 @@ RSpec.describe Namespace do
end
end
+ describe '#any_project_with_pages_deployed?' do
+ it 'returns true if any project nested under the group has pages deployed' do
+ parent_1 = create(:group) # Three projects, one with pages
+ child_1_1 = create(:group, parent: parent_1) # Two projects, one with pages
+ child_1_2 = create(:group, parent: parent_1) # One project, no pages
+ parent_2 = create(:group) # No projects
+
+ create(:project, group: child_1_1).tap do |project|
+ project.pages_metadatum.update!(deployed: true)
+ end
+
+ create(:project, group: child_1_1)
+ create(:project, group: child_1_2)
+
+ expect(parent_1.any_project_with_pages_deployed?).to be(true)
+ expect(child_1_1.any_project_with_pages_deployed?).to be(true)
+ expect(child_1_2.any_project_with_pages_deployed?).to be(false)
+ expect(parent_2.any_project_with_pages_deployed?).to be(false)
+ end
+ end
+
describe '#has_parent?' do
it 'returns true when the group has a parent' do
group = create(:group, :nested)
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 7edd7849bbe..a3417ee5fc7 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe Note do
end
context 'when noteable is a personal snippet' do
- subject { build(:note_on_personal_snippet) }
+ subject { build(:note_on_personal_snippet, noteable: create(:personal_snippet)) }
it 'is valid without project' do
is_expected.to be_valid
@@ -109,7 +109,8 @@ RSpec.describe Note do
describe 'callbacks' do
describe '#notify_after_create' do
it 'calls #after_note_created on the noteable' do
- note = build(:note)
+ noteable = create(:issue)
+ note = build(:note, project: noteable.project, noteable: noteable)
expect(note).to receive(:notify_after_create).and_call_original
expect(note.noteable).to receive(:after_note_created).with(note)
@@ -285,6 +286,56 @@ RSpec.describe Note do
end
end
+ describe "noteable_author?" do
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+
+ context 'when note is on commit' do
+ let(:noteable) { create(:commit, project: project, author: user1) }
+
+ context 'if user is the noteable author' do
+ let(:note) { create(:discussion_note_on_commit, commit_id: noteable.id, project: project, author: user1) }
+ let(:diff_note) { create(:diff_note_on_commit, commit_id: noteable.id, project: project, author: user1) }
+
+ it 'returns true' do
+ expect(note.noteable_author?(noteable)).to be true
+ expect(diff_note.noteable_author?(noteable)).to be true
+ end
+ end
+
+ context 'if user is not the noteable author' do
+ let(:note) { create(:discussion_note_on_commit, commit_id: noteable.id, project: project, author: user2) }
+ let(:diff_note) { create(:diff_note_on_commit, commit_id: noteable.id, project: project, author: user2) }
+
+ it 'returns false' do
+ expect(note.noteable_author?(noteable)).to be false
+ expect(diff_note.noteable_author?(noteable)).to be false
+ end
+ end
+ end
+
+ context 'when note is on issue' do
+ let(:noteable) { create(:issue, project: project, author: user1) }
+
+ context 'if user is the noteable author' do
+ let(:note) { create(:note, noteable: noteable, author: user1, project: project) }
+
+ it 'returns true' do
+ expect(note.noteable_author?(noteable)).to be true
+ end
+ end
+
+ context 'if user is not the noteable author' do
+ let(:note) { create(:note, noteable: noteable, author: user2, project: project) }
+
+ it 'returns false' do
+ expect(note.noteable_author?(noteable)).to be false
+ end
+ end
+ end
+ end
+
describe "edited?" do
let(:note) { build(:note, updated_by_id: nil, created_at: Time.current, updated_at: Time.current + 5.hours) }
@@ -840,7 +891,8 @@ RSpec.describe Note do
let(:html) { '<p>some html</p>'}
context 'note for a project snippet' do
- let(:note) { build(:note_on_project_snippet) }
+ let(:snippet) { create(:project_snippet) }
+ let(:note) { build(:note_on_project_snippet, project: snippet.project, noteable: snippet) }
before do
expect(Banzai::Renderer).to receive(:cacheless_render_field)
@@ -855,7 +907,8 @@ RSpec.describe Note do
end
context 'note for a personal snippet' do
- let(:note) { build(:note_on_personal_snippet) }
+ let(:snippet) { create(:personal_snippet) }
+ let(:note) { build(:note_on_personal_snippet, noteable: snippet) }
before do
expect(Banzai::Renderer).to receive(:cacheless_render_field)
@@ -889,7 +942,7 @@ RSpec.describe Note do
context 'for a note on a commit' do
it 'returns true' do
- note = build(:note_on_commit)
+ note = build(:note_on_commit, project: create(:project, :repository))
expect(note.can_be_discussion_note?).to be_truthy
end
@@ -913,7 +966,7 @@ RSpec.describe Note do
context 'for a diff note on commit' do
it 'returns false' do
- note = build(:diff_note_on_commit)
+ note = build(:diff_note_on_commit, project: create(:project, :repository))
expect(note.can_be_discussion_note?).to be_falsey
end
@@ -1143,7 +1196,8 @@ RSpec.describe Note do
end
describe 'expiring ETag cache' do
- let(:note) { build(:note_on_issue) }
+ let_it_be(:issue) { create(:issue) }
+ let(:note) { build(:note, project: issue.project, noteable: issue) }
def expect_expiration(noteable)
expect_any_instance_of(Gitlab::EtagCaching::Store)
@@ -1224,22 +1278,6 @@ RSpec.describe Note do
end
end
- describe '#special_role=' do
- let(:role) { Note::SpecialRole::FIRST_TIME_CONTRIBUTOR }
-
- it 'assigns role' do
- subject.special_role = role
-
- expect(subject.special_role).to eq(role)
- end
-
- it 'does not assign unknown role' do
- expect { subject.special_role = :bogus }.to raise_error(/Role is undefined/)
-
- expect(subject.special_role).to be_nil
- end
- end
-
describe '#parent' do
it 'returns project for project notes' do
project = create(:project)
@@ -1416,4 +1454,20 @@ RSpec.describe Note do
expect(note.parent_user).to be_nil
end
end
+
+ describe '#skip_notification?' do
+ subject(:skip_notification?) { note.skip_notification? }
+
+ context 'when there is no review' do
+ let(:note) { build(:note) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when the review exists' do
+ let(:note) { build(:note, :with_review) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
end
diff --git a/spec/models/operations/feature_flag_scope_spec.rb b/spec/models/operations/feature_flag_scope_spec.rb
new file mode 100644
index 00000000000..29d338d8b29
--- /dev/null
+++ b/spec/models/operations/feature_flag_scope_spec.rb
@@ -0,0 +1,391 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Operations::FeatureFlagScope do
+ describe 'associations' do
+ it { is_expected.to belong_to(:feature_flag) }
+ end
+
+ describe 'validations' do
+ context 'when duplicate environment scope is going to be created' do
+ let!(:existing_feature_flag_scope) do
+ create(:operations_feature_flag_scope)
+ end
+
+ let(:new_feature_flag_scope) do
+ build(:operations_feature_flag_scope,
+ feature_flag: existing_feature_flag_scope.feature_flag,
+ environment_scope: existing_feature_flag_scope.environment_scope)
+ end
+
+ it 'validates uniqueness of environment scope' do
+ new_feature_flag_scope.save
+
+ expect(new_feature_flag_scope.errors[:environment_scope])
+ .to include("(#{existing_feature_flag_scope.environment_scope})" \
+ " has already been taken")
+ end
+ end
+
+ context 'when environment scope of a default scope is updated' do
+ let!(:feature_flag) { create(:operations_feature_flag) }
+ let!(:scope_default) { feature_flag.default_scope }
+
+ it 'keeps default scope intact' do
+ scope_default.update(environment_scope: 'review/*')
+
+ expect(scope_default.errors[:environment_scope])
+ .to include("cannot be changed from default scope")
+ end
+ end
+
+ context 'when a default scope is destroyed' do
+ let!(:feature_flag) { create(:operations_feature_flag) }
+ let!(:scope_default) { feature_flag.default_scope }
+
+ it 'prevents from destroying the default scope' do
+ expect { scope_default.destroy! }.to raise_error(ActiveRecord::ReadOnlyRecord)
+ end
+ end
+
+ describe 'strategy validations' do
+ it 'handles null strategies which can occur while adding the column during migration' do
+ scope = create(:operations_feature_flag_scope, active: true)
+ allow(scope).to receive(:strategies).and_return(nil)
+
+ scope.active = false
+ scope.save
+
+ expect(scope.errors[:strategies]).to be_empty
+ end
+
+ it 'validates multiple strategies' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: "default", parameters: {} },
+ { name: "invalid", parameters: {} }])
+
+ expect(scope.errors[:strategies]).not_to be_empty
+ end
+
+ where(:invalid_value) do
+ [{}, 600, "bad", [{ name: 'default', parameters: {} }, 300]]
+ end
+ with_them do
+ it 'must be an array of strategy hashes' do
+ scope = create(:operations_feature_flag_scope)
+
+ scope.strategies = invalid_value
+ scope.save
+
+ expect(scope.errors[:strategies]).to eq(['must be an array of strategy hashes'])
+ end
+ end
+
+ describe 'name' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:name, :params, :expected) do
+ 'default' | {} | []
+ 'gradualRolloutUserId' | { groupId: 'mygroup', percentage: '50' } | []
+ 'userWithId' | { userIds: 'sam' } | []
+ 5 | nil | ['strategy name is invalid']
+ nil | nil | ['strategy name is invalid']
+ "nothing" | nil | ['strategy name is invalid']
+ "" | nil | ['strategy name is invalid']
+ 40.0 | nil | ['strategy name is invalid']
+ {} | nil | ['strategy name is invalid']
+ [] | nil | ['strategy name is invalid']
+ end
+ with_them do
+ it 'must be one of "default", "gradualRolloutUserId", or "userWithId"' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: name, parameters: params }])
+
+ expect(scope.errors[:strategies]).to eq(expected)
+ end
+ end
+ end
+
+ describe 'parameters' do
+ context 'when the strategy name is gradualRolloutUserId' do
+ it 'must have parameters' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'gradualRolloutUserId' }])
+
+ expect(scope.errors[:strategies]).to eq(['parameters are invalid'])
+ end
+
+ where(:invalid_parameters) do
+ [nil, {}, { percentage: '40', groupId: 'mygroup', userIds: '4' }, { percentage: '40' },
+ { percentage: '40', groupId: 'mygroup', extra: nil }, { groupId: 'mygroup' }]
+ end
+ with_them do
+ it 'must have valid parameters for the strategy' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'gradualRolloutUserId',
+ parameters: invalid_parameters }])
+
+ expect(scope.errors[:strategies]).to eq(['parameters are invalid'])
+ end
+ end
+
+ it 'allows the parameters in any order' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'gradualRolloutUserId',
+ parameters: { percentage: '10', groupId: 'mygroup' } }])
+
+ expect(scope.errors[:strategies]).to be_empty
+ end
+
+ describe 'percentage' do
+ where(:invalid_value) do
+ [50, 40.0, { key: "value" }, "garbage", "00", "01", "101", "-1", "-10", "0100",
+ "1000", "10.0", "5%", "25%", "100hi", "e100", "30m", " ", "\r\n", "\n", "\t",
+ "\n10", "20\n", "\n100", "100\n", "\n ", nil]
+ end
+ with_them do
+ it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'gradualRolloutUserId',
+ parameters: { groupId: 'mygroup', percentage: invalid_value } }])
+
+ expect(scope.errors[:strategies]).to eq(['percentage must be a string between 0 and 100 inclusive'])
+ end
+ end
+
+ where(:valid_value) do
+ %w[0 1 10 38 100 93]
+ end
+ with_them do
+ it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'gradualRolloutUserId',
+ parameters: { groupId: 'mygroup', percentage: valid_value } }])
+
+ expect(scope.errors[:strategies]).to eq([])
+ end
+ end
+ end
+
+ describe 'groupId' do
+ where(:invalid_value) do
+ [nil, 4, 50.0, {}, 'spaces bad', 'bad$', '%bad', '<bad', 'bad>', '!bad',
+ '.bad', 'Bad', 'bad1', "", " ", "b" * 33, "ba_d", "ba\nd"]
+ end
+ with_them do
+ it 'must be a string value of up to 32 lowercase characters' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'gradualRolloutUserId',
+ parameters: { groupId: invalid_value, percentage: '40' } }])
+
+ expect(scope.errors[:strategies]).to eq(['groupId parameter is invalid'])
+ end
+ end
+
+ where(:valid_value) do
+ ["somegroup", "anothergroup", "okay", "g", "a" * 32]
+ end
+ with_them do
+ it 'must be a string value of up to 32 lowercase characters' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'gradualRolloutUserId',
+ parameters: { groupId: valid_value, percentage: '40' } }])
+
+ expect(scope.errors[:strategies]).to eq([])
+ end
+ end
+ end
+ end
+
+ context 'when the strategy name is userWithId' do
+ it 'must have parameters' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'userWithId' }])
+
+ expect(scope.errors[:strategies]).to eq(['parameters are invalid'])
+ end
+
+ where(:invalid_parameters) do
+ [nil, { userIds: 'sam', percentage: '40' }, { userIds: 'sam', some: 'param' }, { percentage: '40' }, {}]
+ end
+ with_them do
+ it 'must have valid parameters for the strategy' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'userWithId', parameters: invalid_parameters }])
+
+ expect(scope.errors[:strategies]).to eq(['parameters are invalid'])
+ end
+ end
+
+ describe 'userIds' do
+ where(:valid_value) do
+ ["", "sam", "1", "a", "uuid-of-some-kind", "sam,fred,tom,jane,joe,mike",
+ "gitlab@example.com", "123,4", "UPPER,Case,charActeRS", "0",
+ "$valid$email#2345#$%..{}+=-)?\\/@example.com", "spaces allowed",
+ "a" * 256, "a,#{'b' * 256},ccc", "many spaces"]
+ end
+ with_them do
+ it 'is valid with a string of comma separated values' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'userWithId', parameters: { userIds: valid_value } }])
+
+ expect(scope.errors[:strategies]).to be_empty
+ end
+ end
+
+ where(:invalid_value) do
+ [1, 2.5, {}, [], nil, "123\n456", "1,2,3,12\t3", "\n", "\n\r",
+ "joe\r,sam", "1,2,2", "1,,2", "1,2,,,,", "b" * 257, "1, ,2", "tim, ,7", " ",
+ " ", " ,1", "1, ", " leading,1", "1,trailing ", "1, both ,2"]
+ end
+ with_them do
+ it 'is invalid' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'userWithId', parameters: { userIds: invalid_value } }])
+
+ expect(scope.errors[:strategies]).to include(
+ 'userIds must be a string of unique comma separated values each 256 characters or less'
+ )
+ end
+ end
+ end
+ end
+
+ context 'when the strategy name is default' do
+ it 'must have parameters' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'default' }])
+
+ expect(scope.errors[:strategies]).to eq(['parameters are invalid'])
+ end
+
+ where(:invalid_value) do
+ [{ groupId: "hi", percentage: "7" }, "", "nothing", 7, nil, [], 2.5]
+ end
+ with_them do
+ it 'must be empty' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'default',
+ parameters: invalid_value }])
+
+ expect(scope.errors[:strategies]).to eq(['parameters are invalid'])
+ end
+ end
+
+ it 'must be empty' do
+ feature_flag = create(:operations_feature_flag)
+ scope = described_class.create(feature_flag: feature_flag,
+ environment_scope: 'production', active: true,
+ strategies: [{ name: 'default',
+ parameters: {} }])
+
+ expect(scope.errors[:strategies]).to be_empty
+ end
+ end
+ end
+ end
+ end
+
+ describe '.enabled' do
+ subject { described_class.enabled }
+
+ let!(:feature_flag_scope) do
+ create(:operations_feature_flag_scope, active: active)
+ end
+
+ context 'when scope is active' do
+ let(:active) { true }
+
+ it 'returns the scope' do
+ is_expected.to include(feature_flag_scope)
+ end
+ end
+
+ context 'when scope is inactive' do
+ let(:active) { false }
+
+ it 'returns an empty array' do
+ is_expected.not_to include(feature_flag_scope)
+ end
+ end
+ end
+
+ describe '.disabled' do
+ subject { described_class.disabled }
+
+ let!(:feature_flag_scope) do
+ create(:operations_feature_flag_scope, active: active)
+ end
+
+ context 'when scope is active' do
+ let(:active) { true }
+
+ it 'returns an empty array' do
+ is_expected.not_to include(feature_flag_scope)
+ end
+ end
+
+ context 'when scope is inactive' do
+ let(:active) { false }
+
+ it 'returns the scope' do
+ is_expected.to include(feature_flag_scope)
+ end
+ end
+ end
+
+ describe '.for_unleash_client' do
+ it 'returns scopes for the specified project' do
+ project1 = create(:project)
+ project2 = create(:project)
+ expected_feature_flag = create(:operations_feature_flag, project: project1)
+ create(:operations_feature_flag, project: project2)
+
+ scopes = described_class.for_unleash_client(project1, 'sandbox').to_a
+
+ expect(scopes).to contain_exactly(*expected_feature_flag.scopes)
+ end
+
+ it 'returns a scope that matches exactly over a match with a wild card' do
+ project = create(:project)
+ feature_flag = create(:operations_feature_flag, project: project)
+ create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'production*')
+ expected_scope = create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'production')
+
+ scopes = described_class.for_unleash_client(project, 'production').to_a
+
+ expect(scopes).to contain_exactly(expected_scope)
+ end
+ end
+end
diff --git a/spec/models/operations/feature_flag_spec.rb b/spec/models/operations/feature_flag_spec.rb
new file mode 100644
index 00000000000..83d6c6b95a3
--- /dev/null
+++ b/spec/models/operations/feature_flag_spec.rb
@@ -0,0 +1,258 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Operations::FeatureFlag do
+ include FeatureFlagHelpers
+
+ subject { create(:operations_feature_flag) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:scopes) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
+ it { is_expected.to define_enum_for(:version).with_values(legacy_flag: 1, new_version_flag: 2) }
+
+ context 'a version 1 feature flag' do
+ it 'is valid if associated with Operations::FeatureFlagScope models' do
+ project = create(:project)
+ feature_flag = described_class.create({ name: 'test', project: project, version: 1,
+ scopes_attributes: [{ environment_scope: '*', active: false }] })
+
+ expect(feature_flag).to be_valid
+ end
+
+ it 'is invalid if associated with Operations::FeatureFlags::Strategy models' do
+ project = create(:project)
+ feature_flag = described_class.create({ name: 'test', project: project, version: 1,
+ strategies_attributes: [{ name: 'default', parameters: {} }] })
+
+ expect(feature_flag.errors.messages).to eq({
+ version_associations: ["version 1 feature flags may not have strategies"]
+ })
+ end
+ end
+
+ context 'a version 2 feature flag' do
+ it 'is invalid if associated with Operations::FeatureFlagScope models' do
+ project = create(:project)
+ feature_flag = described_class.create({ name: 'test', project: project, version: 2,
+ scopes_attributes: [{ environment_scope: '*', active: false }] })
+
+ expect(feature_flag.errors.messages).to eq({
+ version_associations: ["version 2 feature flags may not have scopes"]
+ })
+ end
+
+ it 'is valid if associated with Operations::FeatureFlags::Strategy models' do
+ project = create(:project)
+ feature_flag = described_class.create({ name: 'test', project: project, version: 2,
+ strategies_attributes: [{ name: 'default', parameters: {} }] })
+
+ expect(feature_flag).to be_valid
+ end
+ end
+
+ it_behaves_like 'AtomicInternalId', validate_presence: true do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:operations_feature_flag) }
+ let(:scope) { :project }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { :operations_feature_flags }
+ end
+ end
+
+ describe 'feature flag version' do
+ it 'defaults to 1 if unspecified' do
+ project = create(:project)
+
+ feature_flag = described_class.create(name: 'my_flag', project: project, active: true)
+
+ expect(feature_flag).to be_valid
+ expect(feature_flag.version_before_type_cast).to eq(1)
+ end
+ end
+
+ describe 'Scope creation' do
+ subject { described_class.new(**params) }
+
+ let(:project) { create(:project) }
+
+ let(:params) do
+ { name: 'test', project: project, scopes_attributes: scopes_attributes }
+ end
+
+ let(:scopes_attributes) do
+ [{ environment_scope: '*', active: false },
+ { environment_scope: 'review/*', active: true }]
+ end
+
+ it { is_expected.to be_valid }
+
+ context 'when the first scope is not wildcard' do
+ let(:scopes_attributes) do
+ [{ environment_scope: 'review/*', active: true },
+ { environment_scope: '*', active: false }]
+ end
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ describe 'the default scope' do
+ let_it_be(:project) { create(:project) }
+
+ context 'with a version 1 feature flag' do
+ it 'creates a default scope' do
+ feature_flag = described_class.create({ name: 'test', project: project, scopes_attributes: [], version: 1 })
+
+ expect(feature_flag.scopes.count).to eq(1)
+ expect(feature_flag.scopes.first.environment_scope).to eq('*')
+ end
+
+ it 'allows specifying the default scope in the parameters' do
+ feature_flag = described_class.create({ name: 'test', project: project,
+ scopes_attributes: [{ environment_scope: '*', active: false },
+ { environment_scope: 'review/*', active: true }], version: 1 })
+
+ expect(feature_flag.scopes.count).to eq(2)
+ expect(feature_flag.scopes.first.environment_scope).to eq('*')
+ end
+ end
+
+ context 'with a version 2 feature flag' do
+ it 'does not create a default scope' do
+ feature_flag = described_class.create({ name: 'test', project: project, scopes_attributes: [], version: 2 })
+
+ expect(feature_flag.scopes).to eq([])
+ end
+ end
+ end
+
+ describe '.enabled' do
+ subject { described_class.enabled }
+
+ context 'when the feature flag is active' do
+ let!(:feature_flag) { create(:operations_feature_flag, active: true) }
+
+ it 'returns the flag' do
+ is_expected.to eq([feature_flag])
+ end
+ end
+
+ context 'when the feature flag is active and all scopes are inactive' do
+ let!(:feature_flag) { create(:operations_feature_flag, active: true) }
+
+ it 'returns the flag' do
+ feature_flag.default_scope.update!(active: false)
+
+ is_expected.to eq([feature_flag])
+ end
+ end
+
+ context 'when the feature flag is inactive' do
+ let!(:feature_flag) { create(:operations_feature_flag, active: false) }
+
+ it 'does not return the flag' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when the feature flag is inactive and all scopes are active' do
+ let!(:feature_flag) { create(:operations_feature_flag, active: false) }
+
+ it 'does not return the flag' do
+ feature_flag.default_scope.update!(active: true)
+
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.disabled' do
+ subject { described_class.disabled }
+
+ context 'when the feature flag is active' do
+ let!(:feature_flag) { create(:operations_feature_flag, active: true) }
+
+ it 'does not return the flag' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when the feature flag is active and all scopes are inactive' do
+ let!(:feature_flag) { create(:operations_feature_flag, active: true) }
+
+ it 'does not return the flag' do
+ feature_flag.default_scope.update!(active: false)
+
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when the feature flag is inactive' do
+ let!(:feature_flag) { create(:operations_feature_flag, active: false) }
+
+ it 'returns the flag' do
+ is_expected.to eq([feature_flag])
+ end
+ end
+
+ context 'when the feature flag is inactive and all scopes are active' do
+ let!(:feature_flag) { create(:operations_feature_flag, active: false) }
+
+ it 'returns the flag' do
+ feature_flag.default_scope.update!(active: true)
+
+ is_expected.to eq([feature_flag])
+ end
+ end
+ end
+
+ describe '.for_unleash_client' do
+ let_it_be(:project) { create(:project) }
+ let!(:feature_flag) do
+ create(:operations_feature_flag, project: project,
+ name: 'feature1', active: true, version: 2)
+ end
+
+ let!(:strategy) do
+ create(:operations_strategy, feature_flag: feature_flag,
+ name: 'default', parameters: {})
+ end
+
+ it 'matches wild cards in the scope' do
+ create(:operations_scope, strategy: strategy, environment_scope: 'review/*')
+
+ flags = described_class.for_unleash_client(project, 'review/feature-branch')
+
+ expect(flags).to eq([feature_flag])
+ end
+
+ it 'matches wild cards case sensitively' do
+ create(:operations_scope, strategy: strategy, environment_scope: 'Staging/*')
+
+ flags = described_class.for_unleash_client(project, 'staging/feature')
+
+ expect(flags).to eq([])
+ end
+
+ it 'returns feature flags ordered by id' do
+ create(:operations_scope, strategy: strategy, environment_scope: 'production')
+ feature_flag_b = create(:operations_feature_flag, project: project,
+ name: 'feature2', active: true, version: 2)
+ strategy_b = create(:operations_strategy, feature_flag: feature_flag_b,
+ name: 'default', parameters: {})
+ create(:operations_scope, strategy: strategy_b, environment_scope: '*')
+
+ flags = described_class.for_unleash_client(project, 'production')
+
+ expect(flags.map(&:id)).to eq([feature_flag.id, feature_flag_b.id])
+ end
+ end
+end
diff --git a/spec/models/operations/feature_flags/strategy_spec.rb b/spec/models/operations/feature_flags/strategy_spec.rb
new file mode 100644
index 00000000000..04e3ef26e9d
--- /dev/null
+++ b/spec/models/operations/feature_flags/strategy_spec.rb
@@ -0,0 +1,323 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Operations::FeatureFlags::Strategy do
+ let_it_be(:project) { create(:project) }
+
+ describe 'validations' do
+ it do
+ is_expected.to validate_inclusion_of(:name)
+ .in_array(%w[default gradualRolloutUserId userWithId gitlabUserList])
+ .with_message('strategy name is invalid')
+ end
+
+ describe 'parameters' do
+ context 'when the strategy name is invalid' do
+ where(:invalid_name) do
+ [nil, {}, [], 'nothing', 3]
+ end
+ with_them do
+ it 'skips parameters validation' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: invalid_name, parameters: { bad: 'params' })
+
+ expect(strategy.errors[:name]).to eq(['strategy name is invalid'])
+ expect(strategy.errors[:parameters]).to be_empty
+ end
+ end
+ end
+
+ context 'when the strategy name is gradualRolloutUserId' do
+ where(:invalid_parameters) do
+ [nil, {}, { percentage: '40', groupId: 'mygroup', userIds: '4' }, { percentage: '40' },
+ { percentage: '40', groupId: 'mygroup', extra: nil }, { groupId: 'mygroup' }]
+ end
+ with_them do
+ it 'must have valid parameters for the strategy' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'gradualRolloutUserId', parameters: invalid_parameters)
+
+ expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
+ end
+ end
+
+ it 'allows the parameters in any order' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'gradualRolloutUserId',
+ parameters: { percentage: '10', groupId: 'mygroup' })
+
+ expect(strategy.errors[:parameters]).to be_empty
+ end
+
+ describe 'percentage' do
+ where(:invalid_value) do
+ [50, 40.0, { key: "value" }, "garbage", "00", "01", "101", "-1", "-10", "0100",
+ "1000", "10.0", "5%", "25%", "100hi", "e100", "30m", " ", "\r\n", "\n", "\t",
+ "\n10", "20\n", "\n100", "100\n", "\n ", nil]
+ end
+ with_them do
+ it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'gradualRolloutUserId',
+ parameters: { groupId: 'mygroup', percentage: invalid_value })
+
+ expect(strategy.errors[:parameters]).to eq(['percentage must be a string between 0 and 100 inclusive'])
+ end
+ end
+
+ where(:valid_value) do
+ %w[0 1 10 38 100 93]
+ end
+ with_them do
+ it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'gradualRolloutUserId',
+ parameters: { groupId: 'mygroup', percentage: valid_value })
+
+ expect(strategy.errors[:parameters]).to eq([])
+ end
+ end
+ end
+
+ describe 'groupId' do
+ where(:invalid_value) do
+ [nil, 4, 50.0, {}, 'spaces bad', 'bad$', '%bad', '<bad', 'bad>', '!bad',
+ '.bad', 'Bad', 'bad1', "", " ", "b" * 33, "ba_d", "ba\nd"]
+ end
+ with_them do
+ it 'must be a string value of up to 32 lowercase characters' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'gradualRolloutUserId',
+ parameters: { groupId: invalid_value, percentage: '40' })
+
+ expect(strategy.errors[:parameters]).to eq(['groupId parameter is invalid'])
+ end
+ end
+
+ where(:valid_value) do
+ ["somegroup", "anothergroup", "okay", "g", "a" * 32]
+ end
+ with_them do
+ it 'must be a string value of up to 32 lowercase characters' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'gradualRolloutUserId',
+ parameters: { groupId: valid_value, percentage: '40' })
+
+ expect(strategy.errors[:parameters]).to eq([])
+ end
+ end
+ end
+ end
+
+ context 'when the strategy name is userWithId' do
+ where(:invalid_parameters) do
+ [nil, { userIds: 'sam', percentage: '40' }, { userIds: 'sam', some: 'param' }, { percentage: '40' }, {}]
+ end
+ with_them do
+ it 'must have valid parameters for the strategy' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'userWithId', parameters: invalid_parameters)
+
+ expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
+ end
+ end
+
+ describe 'userIds' do
+ where(:valid_value) do
+ ["", "sam", "1", "a", "uuid-of-some-kind", "sam,fred,tom,jane,joe,mike",
+ "gitlab@example.com", "123,4", "UPPER,Case,charActeRS", "0",
+ "$valid$email#2345#$%..{}+=-)?\\/@example.com", "spaces allowed",
+ "a" * 256, "a,#{'b' * 256},ccc", "many spaces"]
+ end
+ with_them do
+ it 'is valid with a string of comma separated values' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'userWithId', parameters: { userIds: valid_value })
+
+ expect(strategy.errors[:parameters]).to be_empty
+ end
+ end
+
+ where(:invalid_value) do
+ [1, 2.5, {}, [], nil, "123\n456", "1,2,3,12\t3", "\n", "\n\r",
+ "joe\r,sam", "1,2,2", "1,,2", "1,2,,,,", "b" * 257, "1, ,2", "tim, ,7", " ",
+ " ", " ,1", "1, ", " leading,1", "1,trailing ", "1, both ,2"]
+ end
+ with_them do
+ it 'is invalid' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'userWithId', parameters: { userIds: invalid_value })
+
+ expect(strategy.errors[:parameters]).to include(
+ 'userIds must be a string of unique comma separated values each 256 characters or less'
+ )
+ end
+ end
+ end
+ end
+
+ context 'when the strategy name is default' do
+ where(:invalid_value) do
+ [{ groupId: "hi", percentage: "7" }, "", "nothing", 7, nil, [], 2.5]
+ end
+ with_them do
+ it 'must be empty' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'default',
+ parameters: invalid_value)
+
+ expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
+ end
+ end
+
+ it 'must be empty' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'default',
+ parameters: {})
+
+ expect(strategy.errors[:parameters]).to be_empty
+ end
+ end
+
+ context 'when the strategy name is gitlabUserList' do
+ where(:invalid_value) do
+ [{ groupId: "default", percentage: "7" }, "", "nothing", 7, nil, [], 2.5, { userIds: 'user1' }]
+ end
+ with_them do
+ it 'must be empty' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'gitlabUserList',
+ parameters: invalid_value)
+
+ expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
+ end
+ end
+
+ it 'must be empty' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'gitlabUserList',
+ parameters: {})
+
+ expect(strategy.errors[:parameters]).to be_empty
+ end
+ end
+ end
+
+ describe 'associations' do
+ context 'when name is gitlabUserList' do
+ it 'is valid when associated with a user list' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ user_list = create(:operations_feature_flag_user_list, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'gitlabUserList',
+ user_list: user_list,
+ parameters: {})
+
+ expect(strategy.errors[:user_list]).to be_empty
+ end
+
+ it 'is invalid without a user list' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'gitlabUserList',
+ parameters: {})
+
+ expect(strategy.errors[:user_list]).to eq(["can't be blank"])
+ end
+
+ it 'is invalid when associated with a user list from another project' do
+ other_project = create(:project)
+ feature_flag = create(:operations_feature_flag, project: project)
+ user_list = create(:operations_feature_flag_user_list, project: other_project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'gitlabUserList',
+ user_list: user_list,
+ parameters: {})
+
+ expect(strategy.errors[:user_list]).to eq(['must belong to the same project'])
+ end
+ end
+
+ context 'when name is default' do
+ it 'is invalid when associated with a user list' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ user_list = create(:operations_feature_flag_user_list, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'default',
+ user_list: user_list,
+ parameters: {})
+
+ expect(strategy.errors[:user_list]).to eq(['must be blank'])
+ end
+
+ it 'is valid without a user list' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'default',
+ parameters: {})
+
+ expect(strategy.errors[:user_list]).to be_empty
+ end
+ end
+
+ context 'when name is userWithId' do
+ it 'is invalid when associated with a user list' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ user_list = create(:operations_feature_flag_user_list, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'userWithId',
+ user_list: user_list,
+ parameters: { userIds: 'user1' })
+
+ expect(strategy.errors[:user_list]).to eq(['must be blank'])
+ end
+
+ it 'is valid without a user list' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'userWithId',
+ parameters: { userIds: 'user1' })
+
+ expect(strategy.errors[:user_list]).to be_empty
+ end
+ end
+
+ context 'when name is gradualRolloutUserId' do
+ it 'is invalid when associated with a user list' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ user_list = create(:operations_feature_flag_user_list, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'gradualRolloutUserId',
+ user_list: user_list,
+ parameters: { groupId: 'default', percentage: '10' })
+
+ expect(strategy.errors[:user_list]).to eq(['must be blank'])
+ end
+
+ it 'is valid without a user list' do
+ feature_flag = create(:operations_feature_flag, project: project)
+ strategy = described_class.create(feature_flag: feature_flag,
+ name: 'gradualRolloutUserId',
+ parameters: { groupId: 'default', percentage: '10' })
+
+ expect(strategy.errors[:user_list]).to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/operations/feature_flags/user_list_spec.rb b/spec/models/operations/feature_flags/user_list_spec.rb
new file mode 100644
index 00000000000..020416aa7bc
--- /dev/null
+++ b/spec/models/operations/feature_flags/user_list_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Operations::FeatureFlags::UserList do
+ subject { create(:operations_feature_flag_user_list) }
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
+ it { is_expected.to validate_length_of(:name).is_at_least(1).is_at_most(255) }
+
+ describe 'user_xids' do
+ where(:valid_value) do
+ ["", "sam", "1", "a", "uuid-of-some-kind", "sam,fred,tom,jane,joe,mike",
+ "gitlab@example.com", "123,4", "UPPER,Case,charActeRS", "0",
+ "$valid$email#2345#$%..{}+=-)?\\/@example.com", "spaces allowed",
+ "a" * 256, "a,#{'b' * 256},ccc", "many spaces"]
+ end
+ with_them do
+ it 'is valid with a string of comma separated values' do
+ user_list = described_class.create(user_xids: valid_value)
+
+ expect(user_list.errors[:user_xids]).to be_empty
+ end
+ end
+
+ where(:typecast_value) do
+ [1, 2.5, {}, []]
+ end
+ with_them do
+ it 'automatically casts values of other types' do
+ user_list = described_class.create(user_xids: typecast_value)
+
+ expect(user_list.errors[:user_xids]).to be_empty
+ expect(user_list.user_xids).to eq(typecast_value.to_s)
+ end
+ end
+
+ where(:invalid_value) do
+ [nil, "123\n456", "1,2,3,12\t3", "\n", "\n\r",
+ "joe\r,sam", "1,2,2", "1,,2", "1,2,,,,", "b" * 257, "1, ,2", "tim, ,7", " ",
+ " ", " ,1", "1, ", " leading,1", "1,trailing ", "1, both ,2"]
+ end
+ with_them do
+ it 'is invalid' do
+ user_list = described_class.create(user_xids: invalid_value)
+
+ expect(user_list.errors[:user_xids]).to include(
+ 'user_xids must be a string of unique comma separated values each 256 characters or less'
+ )
+ end
+ end
+ end
+ end
+
+ describe 'url_helpers' do
+ it 'generates paths based on the internal id' do
+ create(:operations_feature_flag_user_list)
+ project_b = create(:project)
+ list_b = create(:operations_feature_flag_user_list, project: project_b)
+
+ path = ::Gitlab::Routing.url_helpers.project_feature_flags_user_list_path(project_b, list_b)
+
+ expect(path).to eq("/#{project_b.full_path}/-/feature_flags_user_lists/#{list_b.iid}")
+ end
+ end
+
+ describe '#destroy' do
+ it 'deletes the model if it is not associated with any feature flag strategies' do
+ project = create(:project)
+ user_list = described_class.create(project: project, name: 'My User List', user_xids: 'user1,user2')
+
+ user_list.destroy
+
+ expect(described_class.count).to eq(0)
+ end
+
+ it 'does not delete the model if it is associated with a feature flag strategy' do
+ project = create(:project)
+ user_list = described_class.create(project: project, name: 'My User List', user_xids: 'user1,user2')
+ feature_flag = create(:operations_feature_flag, :new_version_flag, project: project)
+ strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'gitlabUserList', user_list: user_list)
+
+ user_list.destroy
+
+ expect(described_class.count).to eq(1)
+ expect(::Operations::FeatureFlags::StrategyUserList.count).to eq(1)
+ expect(strategy.reload.user_list).to eq(user_list)
+ expect(strategy.valid?).to eq(true)
+ end
+ end
+
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:operations_feature_flag_user_list) }
+ let(:scope) { :project }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { :operations_user_lists }
+ end
+end
diff --git a/spec/models/operations/feature_flags_client_spec.rb b/spec/models/operations/feature_flags_client_spec.rb
new file mode 100644
index 00000000000..05988d676f3
--- /dev/null
+++ b/spec/models/operations/feature_flags_client_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Operations::FeatureFlagsClient do
+ subject { create(:operations_feature_flags_client) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ end
+
+ describe '#token' do
+ it "ensures that token is always set" do
+ expect(subject.token).not_to be_empty
+ end
+ end
+end
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index 7cc8e13d449..6b992dbc2a5 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -33,15 +33,17 @@ RSpec.describe Packages::PackageFile, type: :model do
end
context 'updating project statistics' do
+ let_it_be(:package, reload: true) { create(:package) }
+
context 'when the package file has an explicit size' do
it_behaves_like 'UpdateProjectStatistics' do
- subject { build(:package_file, :jar, size: 42) }
+ subject { build(:package_file, :jar, package: package, size: 42) }
end
end
context 'when the package file does not have a size' do
it_behaves_like 'UpdateProjectStatistics' do
- subject { build(:package_file, size: nil) }
+ subject { build(:package_file, package: package, size: nil) }
end
end
end
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 4170bf595f0..ea1f75d04e7 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Packages::Package, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:creator) }
it { is_expected.to have_many(:package_files).dependent(:destroy) }
it { is_expected.to have_many(:dependency_links).inverse_of(:package) }
it { is_expected.to have_many(:tags).inverse_of(:package) }
@@ -84,7 +85,7 @@ RSpec.describe Packages::Package, type: :model do
end
describe 'validations' do
- subject { create(:package) }
+ subject { build(:package) }
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id, :version, :package_type) }
@@ -95,7 +96,7 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.not_to allow_value("my(dom$$$ain)com.my-app").for(:name) }
context 'conan package' do
- subject { create(:conan_package) }
+ subject { build_stubbed(:conan_package) }
let(:fifty_one_characters) {'f_b' * 17}
@@ -112,7 +113,7 @@ RSpec.describe Packages::Package, type: :model do
describe '#version' do
RSpec.shared_examples 'validating version to be SemVer compliant for' do |factory_name|
context "for #{factory_name}" do
- subject { create(factory_name) }
+ subject { build_stubbed(factory_name) }
it { is_expected.to allow_value('1.2.3').for(:version) }
it { is_expected.to allow_value('1.2.3-beta').for(:version) }
@@ -126,7 +127,7 @@ RSpec.describe Packages::Package, type: :model do
end
context 'conan package' do
- subject { create(:conan_package) }
+ subject { build_stubbed(:conan_package) }
let(:fifty_one_characters) {'1.2' * 17}
@@ -142,7 +143,7 @@ RSpec.describe Packages::Package, type: :model do
end
context 'maven package' do
- subject { create(:maven_package) }
+ subject { build_stubbed(:maven_package) }
it { is_expected.to allow_value('0').for(:version) }
it { is_expected.to allow_value('1').for(:version) }
@@ -169,6 +170,92 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) }
end
+ context 'pypi package' do
+ subject { create(:pypi_package) }
+
+ it { is_expected.to allow_value('0.1').for(:version) }
+ it { is_expected.to allow_value('2.0').for(:version) }
+ it { is_expected.to allow_value('1.2.0').for(:version) }
+ it { is_expected.to allow_value('0100!0.0').for(:version) }
+ it { is_expected.to allow_value('00!1.2').for(:version) }
+ it { is_expected.to allow_value('1.0a').for(:version) }
+ it { is_expected.to allow_value('1.0-a').for(:version) }
+ it { is_expected.to allow_value('1.0.a1').for(:version) }
+ it { is_expected.to allow_value('1.0a1').for(:version) }
+ it { is_expected.to allow_value('1.0-a1').for(:version) }
+ it { is_expected.to allow_value('1.0alpha1').for(:version) }
+ it { is_expected.to allow_value('1.0b1').for(:version) }
+ it { is_expected.to allow_value('1.0beta1').for(:version) }
+ it { is_expected.to allow_value('1.0rc1').for(:version) }
+ it { is_expected.to allow_value('1.0pre1').for(:version) }
+ it { is_expected.to allow_value('1.0preview1').for(:version) }
+ it { is_expected.to allow_value('1.0.dev1').for(:version) }
+ it { is_expected.to allow_value('1.0.DEV1').for(:version) }
+ it { is_expected.to allow_value('1.0.post1').for(:version) }
+ it { is_expected.to allow_value('1.0.rev1').for(:version) }
+ it { is_expected.to allow_value('1.0.r1').for(:version) }
+ it { is_expected.to allow_value('1.0c2').for(:version) }
+ it { is_expected.to allow_value('2012.15').for(:version) }
+ it { is_expected.to allow_value('1.0+5').for(:version) }
+ it { is_expected.to allow_value('1.0+abc.5').for(:version) }
+ it { is_expected.to allow_value('1!1.1').for(:version) }
+ it { is_expected.to allow_value('1.0c3').for(:version) }
+ it { is_expected.to allow_value('1.0rc2').for(:version) }
+ it { is_expected.to allow_value('1.0c1').for(:version) }
+ it { is_expected.to allow_value('1.0b2-346').for(:version) }
+ it { is_expected.to allow_value('1.0b2.post345').for(:version) }
+ it { is_expected.to allow_value('1.0b2.post345.dev456').for(:version) }
+ it { is_expected.to allow_value('1.2.rev33+123456').for(:version) }
+ it { is_expected.to allow_value('1.1.dev1').for(:version) }
+ it { is_expected.to allow_value('1.0b1.dev456').for(:version) }
+ it { is_expected.to allow_value('1.0a12.dev456').for(:version) }
+ it { is_expected.to allow_value('1.0b2').for(:version) }
+ it { is_expected.to allow_value('1.0.dev456').for(:version) }
+ it { is_expected.to allow_value('1.0c1.dev456').for(:version) }
+ it { is_expected.to allow_value('1.0.post456').for(:version) }
+ it { is_expected.to allow_value('1.0.post456.dev34').for(:version) }
+ it { is_expected.to allow_value('1.2+123abc').for(:version) }
+ it { is_expected.to allow_value('1.2+abc').for(:version) }
+ it { is_expected.to allow_value('1.2+abc123').for(:version) }
+ it { is_expected.to allow_value('1.2+abc123def').for(:version) }
+ it { is_expected.to allow_value('1.2+1234.abc').for(:version) }
+ it { is_expected.to allow_value('1.2+123456').for(:version) }
+ it { is_expected.to allow_value('1.2.r32+123456').for(:version) }
+ it { is_expected.to allow_value('1!1.2.rev33+123456').for(:version) }
+ it { is_expected.to allow_value('1.0a12').for(:version) }
+ it { is_expected.to allow_value('1.2.3-45+abcdefgh').for(:version) }
+ it { is_expected.to allow_value('v1.2.3').for(:version) }
+ it { is_expected.not_to allow_value('1.2.3-45-abcdefgh').for(:version) }
+ it { is_expected.not_to allow_value('..1.2.3').for(:version) }
+ it { is_expected.not_to allow_value(' 1.2.3').for(:version) }
+ it { is_expected.not_to allow_value("1.2.3 \r\t").for(:version) }
+ it { is_expected.not_to allow_value("\r\t 1.2.3").for(:version) }
+ it { is_expected.not_to allow_value('1./2.3').for(:version) }
+ it { is_expected.not_to allow_value('1.2.3-4/../../').for(:version) }
+ it { is_expected.not_to allow_value('1.2.3-4%2e%2e%').for(:version) }
+ it { is_expected.not_to allow_value('../../../../../1.2.3').for(:version) }
+ it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) }
+ end
+
+ context 'generic package' do
+ subject { build_stubbed(:generic_package) }
+
+ it { is_expected.to validate_presence_of(:version) }
+ it { is_expected.to allow_value('1.2.3').for(:version) }
+ it { is_expected.to allow_value('1.3.350').for(:version) }
+ it { is_expected.not_to allow_value('1.3.350-20201230123456').for(:version) }
+ it { is_expected.not_to allow_value('..1.2.3').for(:version) }
+ it { is_expected.not_to allow_value(' 1.2.3').for(:version) }
+ it { is_expected.not_to allow_value("1.2.3 \r\t").for(:version) }
+ it { is_expected.not_to allow_value("\r\t 1.2.3").for(:version) }
+ it { is_expected.not_to allow_value('1.2.3-4/../../').for(:version) }
+ it { is_expected.not_to allow_value('1.2.3-4%2e%2e%').for(:version) }
+ it { is_expected.not_to allow_value('../../../../../1.2.3').for(:version) }
+ it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) }
+ it { is_expected.not_to allow_value('').for(:version) }
+ it { is_expected.not_to allow_value(nil).for(:version) }
+ end
+
it_behaves_like 'validating version to be SemVer compliant for', :npm_package
it_behaves_like 'validating version to be SemVer compliant for', :nuget_package
end
@@ -178,7 +265,7 @@ RSpec.describe Packages::Package, type: :model do
let!(:package) { create(:npm_package) }
it 'will not allow a package of the same name' do
- new_package = build(:npm_package, name: package.name)
+ new_package = build(:npm_package, project: create(:project), name: package.name)
expect(new_package).not_to be_valid
end
@@ -482,4 +569,23 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to contain_exactly(*tags) }
end
end
+
+ describe 'plan_limits' do
+ Packages::Package.package_types.keys.without('composer').each do |pt|
+ plan_limit_name = if pt == 'generic'
+ "#{pt}_packages_max_file_size"
+ else
+ "#{pt}_max_file_size"
+ end
+
+ context "File size limits for #{pt}" do
+ let(:package) { create("#{pt}_package") }
+
+ it "plan_limits includes column #{plan_limit_name}" do
+ expect { package.project.actual_limits.send(plan_limit_name) }
+ .not_to raise_error(NoMethodError)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index 38bd9b39a56..cb1938a0113 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -3,20 +3,20 @@
require 'spec_helper'
RSpec.describe Pages::LookupPath do
- let(:project) do
- instance_double(Project,
- id: 12345,
- private_pages?: true,
- pages_https_only?: true,
- full_path: 'the/full/path'
- )
+ let_it_be(:project) do
+ create(:project, :pages_private, pages_https_only: true)
end
subject(:lookup_path) { described_class.new(project) }
+ before do
+ stub_pages_setting(access_control: true, external_https: ["1.1.1.1:443"])
+ stub_artifacts_object_storage
+ end
+
describe '#project_id' do
it 'delegates to Project#id' do
- expect(lookup_path.project_id).to eq(12345)
+ expect(lookup_path.project_id).to eq(project.id)
end
end
@@ -47,12 +47,49 @@ RSpec.describe Pages::LookupPath do
end
describe '#source' do
- it 'sets the source type to "file"' do
- expect(lookup_path.source[:type]).to eq('file')
+ shared_examples 'uses disk storage' do
+ it 'sets the source type to "file"' do
+ expect(lookup_path.source[:type]).to eq('file')
+ end
+
+ it 'sets the source path to the project full path suffixed with "public/' do
+ expect(lookup_path.source[:path]).to eq(project.full_path + "/public/")
+ end
end
- it 'sets the source path to the project full path suffixed with "public/' do
- expect(lookup_path.source[:path]).to eq('the/full/path/public/')
+ include_examples 'uses disk storage'
+
+ context 'when artifact_id from build job is present in pages metadata' do
+ let(:artifacts_archive) { create(:ci_job_artifact, :zip, :remote_store, project: project) }
+
+ before do
+ project.mark_pages_as_deployed(artifacts_archive: artifacts_archive)
+ end
+
+ it 'sets the source type to "zip"' do
+ expect(lookup_path.source[:type]).to eq('zip')
+ end
+
+ it 'sets the source path to the artifacts archive URL' do
+ Timecop.freeze do
+ expect(lookup_path.source[:path]).to eq(artifacts_archive.file.url(expire_at: 1.day.from_now))
+ expect(lookup_path.source[:path]).to include("Expires=86400")
+ end
+ end
+
+ context 'when artifact is not uploaded to object storage' do
+ let(:artifacts_archive) { create(:ci_job_artifact, :zip) }
+
+ include_examples 'uses disk storage'
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_artifacts_archive: false)
+ end
+
+ include_examples 'uses disk storage'
+ end
end
end
diff --git a/spec/models/pages_deployment_spec.rb b/spec/models/pages_deployment_spec.rb
new file mode 100644
index 00000000000..eafff1ed59a
--- /dev/null
+++ b/spec/models/pages_deployment_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PagesDeployment do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project).required }
+ it { is_expected.to belong_to(:ci_build).optional }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:file) }
+ it { is_expected.to validate_presence_of(:size) }
+ it { is_expected.to validate_numericality_of(:size).only_integer.is_greater_than(0) }
+ it { is_expected.to validate_inclusion_of(:file_store).in_array(ObjectStorage::SUPPORTED_STORES) }
+
+ it 'is valid when created from the factory' do
+ expect(create(:pages_deployment)).to be_valid
+ end
+ end
+end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 78980f8cdab..1d5369e608e 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -354,16 +354,6 @@ RSpec.describe PagesDomain do
domain.destroy!
end
- 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, project: project)
- end
-
it "schedules a PagesUpdateConfigurationWorker" do
expect(PagesUpdateConfigurationWorker).to receive(:perform_async).with(project.id)
diff --git a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
index 61174a7d0c5..634690d5d0b 100644
--- a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
+++ b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
@@ -219,20 +219,93 @@ RSpec.describe PerformanceMonitoring::PrometheusDashboard do
end
describe '#schema_validation_warnings' do
- context 'when schema is valid' do
- it 'returns nil' do
- expect(described_class).to receive(:from_json)
- expect(described_class.new.schema_validation_warnings).to be_nil
+ let(:environment) { create(:environment, project: project) }
+ let(:path) { '.gitlab/dashboards/test.yml' }
+ let(:project) { create(:project, :repository, :custom_repo, files: { path => dashboard_schema.to_yaml }) }
+
+ subject(:schema_validation_warnings) { described_class.new(dashboard_schema.merge(path: path, environment: environment)).schema_validation_warnings }
+
+ before do
+ allow(Gitlab::Metrics::Dashboard::Finder).to receive(:find_raw).with(project, dashboard_path: path).and_call_original
+ end
+
+ context 'metrics_dashboard_exhaustive_validations is on' do
+ before do
+ stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
+ end
+
+ context 'when schema is valid' do
+ let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) }
+
+ it 'returns empty array' do
+ expect(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).with(dashboard_schema, dashboard_path: path, project: project).and_return([])
+
+ expect(schema_validation_warnings).to eq []
+ end
+ end
+
+ context 'when schema is invalid' do
+ let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) }
+
+ it 'returns array with errors messages' do
+ error = ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new
+
+ expect(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).with(dashboard_schema, dashboard_path: path, project: project).and_return([error])
+
+ expect(schema_validation_warnings).to eq [error.message]
+ end
+ end
+
+ context 'when YAML has wrong syntax' do
+ let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) }
+
+ subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings }
+
+ it 'returns array with errors messages' do
+ expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors)
+
+ expect(schema_validation_warnings).to eq ['Invalid yaml']
+ end
end
end
- context 'when schema is invalid' do
- it 'returns array with errors messages' do
- instance = described_class.new
- instance.errors.add(:test, 'test error')
+ context 'metrics_dashboard_exhaustive_validations is off' do
+ before do
+ stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
+ end
+
+ context 'when schema is valid' do
+ let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) }
+
+ it 'returns empty array' do
+ expect(described_class).to receive(:from_json).with(dashboard_schema)
+
+ expect(schema_validation_warnings).to eq []
+ end
+ end
+
+ context 'when schema is invalid' do
+ let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) }
+
+ it 'returns array with errors messages' do
+ instance = described_class.new
+ instance.errors.add(:test, 'test error')
+
+ expect(described_class).to receive(:from_json).and_raise(ActiveModel::ValidationError.new(instance))
+ expect(described_class.new.schema_validation_warnings).to eq ['test: test error']
+ end
+ end
- expect(described_class).to receive(:from_json).and_raise(ActiveModel::ValidationError.new(instance))
- expect(described_class.new.schema_validation_warnings).to eq ['test: test error']
+ context 'when YAML has wrong syntax' do
+ let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) }
+
+ subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings }
+
+ it 'returns array with errors messages' do
+ expect(described_class).not_to receive(:from_json)
+
+ expect(schema_validation_warnings).to eq ['Invalid yaml']
+ end
end
end
end
diff --git a/spec/models/product_analytics_event_spec.rb b/spec/models/product_analytics_event_spec.rb
index afdb5b690f8..286729b8398 100644
--- a/spec/models/product_analytics_event_spec.rb
+++ b/spec/models/product_analytics_event_spec.rb
@@ -35,4 +35,29 @@ RSpec.describe ProductAnalyticsEvent, type: :model do
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
+
+ describe '.by_category_and_action' do
+ let_it_be(:event) { create(:product_analytics_event, se_category: 'catA', se_action: 'actA') }
+
+ before do
+ create(:product_analytics_event, se_category: 'catA', se_action: 'actB')
+ create(:product_analytics_event, se_category: 'catB', se_action: 'actA')
+ end
+
+ it { expect(described_class.by_category_and_action('catA', 'actA')).to match_array([event]) }
+ end
+
+ describe '.count_collector_tstamp_by_day' do
+ let_it_be(:time_now) { Time.zone.now }
+ let_it_be(:time_ago) { Time.zone.now - 5.days }
+
+ let_it_be(:events) do
+ create_list(:product_analytics_event, 3, collector_tstamp: time_now) +
+ create_list(:product_analytics_event, 2, collector_tstamp: time_ago)
+ end
+
+ subject { described_class.count_collector_tstamp_by_day(7.days) }
+
+ it { is_expected.to eq({ time_now.beginning_of_day => 3, time_ago.beginning_of_day => 2 }) }
+ end
end
diff --git a/spec/models/project_feature_usage_spec.rb b/spec/models/project_feature_usage_spec.rb
new file mode 100644
index 00000000000..908b98ee9c2
--- /dev/null
+++ b/spec/models/project_feature_usage_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProjectFeatureUsage, type: :model do
+ describe '.jira_dvcs_integrations_enabled_count' do
+ it 'returns count of projects with Jira DVCS Cloud enabled' do
+ create(:project).feature_usage.log_jira_dvcs_integration_usage
+ create(:project).feature_usage.log_jira_dvcs_integration_usage
+
+ expect(described_class.with_jira_dvcs_integration_enabled.count).to eq(2)
+ end
+
+ it 'returns count of projects with Jira DVCS Server enabled' do
+ create(:project).feature_usage.log_jira_dvcs_integration_usage(cloud: false)
+ create(:project).feature_usage.log_jira_dvcs_integration_usage(cloud: false)
+
+ expect(described_class.with_jira_dvcs_integration_enabled(cloud: false).count).to eq(2)
+ end
+ end
+
+ describe '#log_jira_dvcs_integration_usage' do
+ let(:project) { create(:project) }
+
+ subject { project.feature_usage }
+
+ it 'logs Jira DVCS Cloud last sync' do
+ Timecop.freeze do
+ subject.log_jira_dvcs_integration_usage
+
+ expect(subject.jira_dvcs_server_last_sync_at).to be_nil
+ expect(subject.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.current)
+ end
+ end
+
+ it 'logs Jira DVCS Server last sync' do
+ Timecop.freeze do
+ subject.log_jira_dvcs_integration_usage(cloud: false)
+
+ expect(subject.jira_dvcs_server_last_sync_at).to be_like_time(Time.current)
+ expect(subject.jira_dvcs_cloud_last_sync_at).to be_nil
+ end
+ end
+
+ context 'when log_jira_dvcs_integration_usage is called simultaneously for the same project' do
+ it 'logs the latest call' do
+ feature_usage = project.feature_usage
+ feature_usage.log_jira_dvcs_integration_usage
+ first_logged_at = feature_usage.jira_dvcs_cloud_last_sync_at
+
+ Timecop.freeze(1.hour.from_now) do
+ ProjectFeatureUsage.new(project_id: project.id).log_jira_dvcs_integration_usage
+ end
+
+ expect(feature_usage.reload.jira_dvcs_cloud_last_sync_at).to be > first_logged_at
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index 4d2474cc56a..45afbcca96d 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do
let_it_be(:project) { create(:project) }
subject(:service) do
- described_class.create(
+ described_class.create!(
project: project,
properties: {
bamboo_url: bamboo_url,
@@ -85,7 +85,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do
bamboo_service = service
bamboo_service.bamboo_url = 'http://gitlab1.com'
- bamboo_service.save
+ bamboo_service.save!
expect(bamboo_service.password).to be_nil
end
@@ -94,7 +94,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do
bamboo_service = service
bamboo_service.username = 'some_name'
- bamboo_service.save
+ bamboo_service.save!
expect(bamboo_service.password).to eq('password')
end
@@ -104,7 +104,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do
bamboo_service.bamboo_url = 'http://gitlab_edited.com'
bamboo_service.password = 'password'
- bamboo_service.save
+ bamboo_service.save!
expect(bamboo_service.password).to eq('password')
expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com')
@@ -117,7 +117,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do
bamboo_service.bamboo_url = 'http://gitlab_edited.com'
bamboo_service.password = 'password'
- bamboo_service.save
+ bamboo_service.save!
expect(bamboo_service.password).to eq('password')
expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com')
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 3d0c2cc1006..f6bf1551bf0 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe BuildkiteService, :use_clean_rails_memory_store_caching do
let(:project) { create(:project) }
subject(:service) do
- described_class.create(
+ described_class.create!(
project: project,
properties: {
service_hook: true,
diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb
index 45be5212508..02b266e4fae 100644
--- a/spec/models/project_services/chat_message/merge_message_spec.rb
+++ b/spec/models/project_services/chat_message/merge_message_spec.rb
@@ -29,23 +29,6 @@ RSpec.describe ChatMessage::MergeMessage do
}
end
- # Integration point in EE
- context 'when state is overridden' do
- it 'respects the overridden state' do
- allow(subject).to receive(:state_or_action_text) { 'devoured' }
-
- aggregate_failures do
- expect(subject.summary).not_to include('opened')
- expect(subject.summary).to include('devoured')
-
- activity_title = subject.activity[:title]
-
- expect(activity_title).not_to include('opened')
- expect(activity_title).to include('devoured')
- end
- end
- end
-
context 'without markdown' do
let(:color) { '#345' }
@@ -106,4 +89,56 @@ RSpec.describe ChatMessage::MergeMessage do
end
end
end
+
+ context 'approved' do
+ before do
+ args[:object_attributes][:action] = 'approved'
+ end
+
+ it 'returns a message regarding completed approval of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) approved merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> '\
+ 'in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'unapproved' do
+ before do
+ args[:object_attributes][:action] = 'unapproved'
+ end
+
+ it 'returns a message regarding revocation of completed approval of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) unapproved merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> '\
+ 'in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'approval' do
+ before do
+ args[:object_attributes][:action] = 'approval'
+ end
+
+ it 'returns a message regarding added approval of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) added their approval to merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> '\
+ 'in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'unapproval' do
+ before do
+ args[:object_attributes][:action] = 'unapproval'
+ end
+
+ it 'returns a message regarding revoking approval of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) removed their approval from merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> '\
+ 'in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
end
diff --git a/spec/models/project_services/ewm_service_spec.rb b/spec/models/project_services/ewm_service_spec.rb
new file mode 100644
index 00000000000..311c456569e
--- /dev/null
+++ b/spec/models/project_services/ewm_service_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe EwmService 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
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of(:project_url) }
+ it { is_expected.to validate_presence_of(:issues_url) }
+ it { is_expected.to validate_presence_of(:new_issue_url) }
+ it_behaves_like 'issue tracker service URL attribute', :project_url
+ it_behaves_like 'issue tracker service URL attribute', :issues_url
+ it_behaves_like 'issue tracker service URL attribute', :new_issue_url
+ end
+
+ context 'when service is inactive' do
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:project_url) }
+ it { is_expected.not_to validate_presence_of(:issues_url) }
+ it { is_expected.not_to validate_presence_of(:new_issue_url) }
+ end
+ end
+
+ describe "ReferencePatternValidation" do
+ it "extracts bug" do
+ expect(described_class.reference_pattern.match("This is bug 123")[:issue]).to eq("bug 123")
+ end
+
+ it "extracts task" do
+ expect(described_class.reference_pattern.match("This is task 123.")[:issue]).to eq("task 123")
+ end
+
+ it "extracts work item" do
+ expect(described_class.reference_pattern.match("This is work item 123 now")[:issue]).to eq("work item 123")
+ end
+
+ it "extracts workitem" do
+ expect(described_class.reference_pattern.match("workitem 123 at the beginning")[:issue]).to eq("workitem 123")
+ end
+
+ it "extracts defect" do
+ expect(described_class.reference_pattern.match("This is defect 123 defect")[:issue]).to eq("defect 123")
+ end
+
+ it "extracts rtcwi" do
+ expect(described_class.reference_pattern.match("This is rtcwi 123")[:issue]).to eq("rtcwi 123")
+ end
+ end
+end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 28bba893be4..7741fea8717 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -10,6 +10,11 @@ RSpec.describe JiraService do
let(:username) { 'jira-username' }
let(:password) { 'jira-password' }
let(:transition_id) { 'test27' }
+ let(:server_info_results) { { 'deploymentType' => 'Cloud' } }
+
+ before do
+ WebMock.stub_request(:get, /serverInfo/).to_return(body: server_info_results.to_json )
+ end
describe '#options' do
let(:options) do
@@ -23,7 +28,7 @@ RSpec.describe JiraService do
}
end
- let(:service) { described_class.create(options) }
+ let(:service) { described_class.create!(options) }
it 'sets the URL properly' do
# jira-ruby gem parses the URI and handles trailing slashes fine:
@@ -97,13 +102,13 @@ RSpec.describe JiraService do
}
end
- subject { described_class.create(params) }
+ subject { described_class.create!(params) }
it 'does not store data into properties' do
expect(subject.properties).to be_nil
end
- it 'stores data in data_fields correcty' do
+ it 'stores data in data_fields correctly' do
service = subject
expect(service.jira_tracker_data.url).to eq(url)
@@ -111,6 +116,35 @@ RSpec.describe JiraService do
expect(service.jira_tracker_data.username).to eq(username)
expect(service.jira_tracker_data.password).to eq(password)
expect(service.jira_tracker_data.jira_issue_transition_id).to eq(transition_id)
+ expect(service.jira_tracker_data.deployment_cloud?).to be_truthy
+ end
+
+ context 'when loading serverInfo' do
+ let!(:jira_service) { subject }
+
+ context 'Cloud instance' do
+ let(:server_info_results) { { 'deploymentType' => 'Cloud' } }
+
+ it 'is detected' do
+ expect(jira_service.jira_tracker_data.deployment_cloud?).to be_truthy
+ end
+ end
+
+ context 'Server instance' do
+ let(:server_info_results) { { 'deploymentType' => 'Server' } }
+
+ it 'is detected' do
+ expect(jira_service.jira_tracker_data.deployment_server?).to be_truthy
+ end
+ end
+
+ context 'Unknown instance' do
+ let(:server_info_results) { { 'deploymentType' => 'FutureCloud' } }
+
+ it 'is detected' do
+ expect(jira_service.jira_tracker_data.deployment_unknown?).to be_truthy
+ end
+ end
end
end
@@ -151,11 +185,11 @@ RSpec.describe JiraService do
describe '#update' do
context 'basic update' do
- let(:new_username) { 'new_username' }
- let(:new_url) { 'http://jira-new.example.com' }
+ let_it_be(:new_username) { 'new_username' }
+ let_it_be(:new_url) { 'http://jira-new.example.com' }
before do
- service.update(username: new_username, url: new_url)
+ service.update!(username: new_username, url: new_url)
end
it 'leaves properties field emtpy' do
@@ -173,6 +207,63 @@ RSpec.describe JiraService do
end
end
+ context 'when updating the url, api_url, username, or password' do
+ it 'updates deployment type' do
+ service.update!(url: 'http://first.url')
+ service.jira_tracker_data.update!(deployment_type: 'server')
+
+ expect(service.jira_tracker_data.deployment_server?).to be_truthy
+
+ service.update!(api_url: 'http://another.url')
+ service.jira_tracker_data.reload
+
+ expect(service.jira_tracker_data.deployment_cloud?).to be_truthy
+ expect(WebMock).to have_requested(:get, /serverInfo/).twice
+ end
+
+ it 'calls serverInfo for url' do
+ service.update!(url: 'http://first.url')
+
+ expect(WebMock).to have_requested(:get, /serverInfo/)
+ end
+
+ it 'calls serverInfo for api_url' do
+ service.update!(api_url: 'http://another.url')
+
+ expect(WebMock).to have_requested(:get, /serverInfo/)
+ end
+
+ it 'calls serverInfo for username' do
+ service.update!(username: 'test-user')
+
+ expect(WebMock).to have_requested(:get, /serverInfo/)
+ end
+
+ it 'calls serverInfo for password' do
+ service.update!(password: 'test-password')
+
+ expect(WebMock).to have_requested(:get, /serverInfo/)
+ end
+ end
+
+ context 'when not updating the url, api_url, username, or password' do
+ it 'does not update deployment type' do
+ expect {service.update!(jira_issue_transition_id: 'jira_issue_transition_id')}.to raise_error(ActiveRecord::RecordInvalid)
+
+ expect(WebMock).not_to have_requested(:get, /serverInfo/)
+ end
+ end
+
+ context 'when not allowed to test an instance or group' do
+ it 'does not update deployment type' do
+ allow(service).to receive(:can_test?).and_return(false)
+
+ service.update!(url: 'http://first.url')
+
+ expect(WebMock).not_to have_requested(:get, /serverInfo/)
+ end
+ end
+
context 'stored password invalidation' do
context 'when a password was previously set' do
context 'when only web url present' do
@@ -187,7 +278,7 @@ RSpec.describe JiraService do
it 'resets password if url changed' do
service
service.url = 'http://jira_edited.example.com'
- service.save
+ service.save!
expect(service.reload.url).to eq('http://jira_edited.example.com')
expect(service.password).to be_nil
@@ -195,7 +286,7 @@ RSpec.describe JiraService do
it 'does not reset password if url "changed" to the same url as before' do
service.url = 'http://jira.example.com'
- service.save
+ service.save!
expect(service.reload.url).to eq('http://jira.example.com')
expect(service.password).not_to be_nil
@@ -203,7 +294,7 @@ RSpec.describe JiraService do
it 'resets password if url not changed but api url added' do
service.api_url = 'http://jira_edited.example.com/rest/api/2'
- service.save
+ service.save!
expect(service.reload.api_url).to eq('http://jira_edited.example.com/rest/api/2')
expect(service.password).to be_nil
@@ -212,7 +303,7 @@ RSpec.describe JiraService do
it 'does not reset password if new url is set together with password, even if it\'s the same password' do
service.url = 'http://jira_edited.example.com'
service.password = password
- service.save
+ service.save!
expect(service.password).to eq(password)
expect(service.url).to eq('http://jira_edited.example.com')
@@ -221,14 +312,14 @@ RSpec.describe JiraService do
it 'resets password if url changed, even if setter called multiple times' do
service.url = 'http://jira1.example.com/rest/api/2'
service.url = 'http://jira1.example.com/rest/api/2'
- service.save
+ service.save!
expect(service.password).to be_nil
end
it 'does not reset password if username changed' do
service.username = 'some_name'
- service.save
+ service.save!
expect(service.reload.password).to eq(password)
end
@@ -236,7 +327,7 @@ RSpec.describe JiraService do
it 'does not reset password if password changed' do
service.url = 'http://jira_edited.example.com'
service.password = 'new_password'
- service.save
+ service.save!
expect(service.reload.password).to eq('new_password')
end
@@ -244,7 +335,7 @@ RSpec.describe JiraService do
it 'does not reset password if the password is touched and same as before' do
service.url = 'http://jira_edited.example.com'
service.password = password
- service.save
+ service.save!
expect(service.reload.password).to eq(password)
end
@@ -261,20 +352,20 @@ RSpec.describe JiraService do
it 'resets password if api url changed' do
service.api_url = 'http://jira_edited.example.com/rest/api/2'
- service.save
+ service.save!
expect(service.password).to be_nil
end
it 'does not reset password if url changed' do
service.url = 'http://jira_edited.example.com'
- service.save
+ service.save!
expect(service.password).to eq(password)
end
it 'resets password if api url set to empty' do
- service.update(api_url: '')
+ service.update!(api_url: '')
expect(service.reload.password).to be_nil
end
@@ -291,7 +382,7 @@ RSpec.describe JiraService do
it 'saves password if new url is set together with password' do
service.url = 'http://jira_edited.example.com/rest/api/2'
service.password = 'password'
- service.save
+ service.save!
expect(service.reload.password).to eq('password')
expect(service.reload.url).to eq('http://jira_edited.example.com/rest/api/2')
end
@@ -360,7 +451,7 @@ RSpec.describe JiraService do
allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return('JIRA-123')
allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
- @jira_service.save
+ @jira_service.save!
project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123'
@transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions'
@@ -627,32 +718,32 @@ RSpec.describe JiraService do
end
describe '#test' do
+ let(:server_info_results) { { 'url' => 'http://url', 'deploymentType' => 'Cloud' } }
+ let_it_be(:project) { create(:project, :repository) }
let(:jira_service) do
described_class.new(
url: url,
+ project: project,
username: username,
password: password
)
end
- def test_settings(url = 'jira.example.com')
- test_url = "http://#{url}/rest/api/2/serverInfo"
-
- WebMock.stub_request(:get, test_url).with(basic_auth: [username, password])
- .to_return(body: { url: 'http://url' }.to_json )
-
+ def server_info
jira_service.test(nil)
end
context 'when the test succeeds' do
it 'gets Jira project with URL when API URL not set' do
- expect(test_settings).to eq(success: true, result: { 'url' => 'http://url' })
+ expect(server_info).to eq(success: true, result: server_info_results)
+ expect(WebMock).to have_requested(:get, /jira.example.com/)
end
it 'gets Jira project with API URL if set' do
- jira_service.update(api_url: 'http://jira.api.com')
+ jira_service.update!(api_url: 'http://jira.api.com')
- expect(test_settings('jira.api.com')).to eq(success: true, result: { 'url' => 'http://url' })
+ expect(server_info).to eq(success: true, result: server_info_results)
+ expect(WebMock).to have_requested(:get, /jira.api.com/)
end
end
diff --git a/spec/models/project_services/packagist_service_spec.rb b/spec/models/project_services/packagist_service_spec.rb
index f710385b6e2..33b5c9809c7 100644
--- a/spec/models/project_services/packagist_service_spec.rb
+++ b/spec/models/project_services/packagist_service_spec.rb
@@ -3,20 +3,6 @@
require 'spec_helper'
RSpec.describe PackagistService do
- describe "Associations" do
- it { is_expected.to belong_to :project }
- it { is_expected.to have_one :service_hook }
- end
-
- let(:project) { create(:project) }
-
- let(:packagist_server) { 'https://packagist.example.com' }
- let(:packagist_username) { 'theUser' }
- let(:packagist_token) { 'verySecret' }
- let(:packagist_hook_url) do
- "#{packagist_server}/api/update-package?username=#{packagist_username}&apiToken=#{packagist_token}"
- end
-
let(:packagist_params) do
{
active: true,
@@ -29,11 +15,25 @@ RSpec.describe PackagistService do
}
end
+ let(:packagist_hook_url) do
+ "#{packagist_server}/api/update-package?username=#{packagist_username}&apiToken=#{packagist_token}"
+ end
+
+ let(:packagist_token) { 'verySecret' }
+ let(:packagist_username) { 'theUser' }
+ let(:packagist_server) { 'https://packagist.example.com' }
+ let(:project) { create(:project) }
+
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
describe '#execute' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
- let(:packagist_service) { described_class.create(packagist_params) }
+ let(:packagist_service) { described_class.create!(packagist_params) }
before do
stub_request(:post, packagist_hook_url)
diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb
index 9a8386c619e..21cc5d44558 100644
--- a/spec/models/project_services/pipelines_email_service_spec.rb
+++ b/spec/models/project_services/pipelines_email_service_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'when pipeline is succeeded' do
before do
data[:object_attributes][:status] = 'success'
- pipeline.update(status: 'success')
+ pipeline.update!(status: 'success')
end
it_behaves_like 'sending email'
@@ -91,7 +91,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'on default branch' do
before do
data[:object_attributes][:ref] = project.default_branch
- pipeline.update(ref: project.default_branch)
+ pipeline.update!(ref: project.default_branch)
end
context 'notifications are enabled only for default branch' do
@@ -115,7 +115,7 @@ RSpec.describe PipelinesEmailService, :mailer do
before do
create(:protected_branch, project: project, name: 'a-protected-branch')
data[:object_attributes][:ref] = 'a-protected-branch'
- pipeline.update(ref: 'a-protected-branch')
+ pipeline.update!(ref: 'a-protected-branch')
end
context 'notifications are enabled only for default branch' do
@@ -138,7 +138,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'on a neither protected nor default branch' do
before do
data[:object_attributes][:ref] = 'a-random-branch'
- pipeline.update(ref: 'a-random-branch')
+ pipeline.update!(ref: 'a-random-branch')
end
context 'notifications are enabled only for default branch' do
@@ -177,7 +177,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'with succeeded pipeline' do
before do
data[:object_attributes][:status] = 'success'
- pipeline.update(status: 'success')
+ pipeline.update!(status: 'success')
end
it_behaves_like 'not sending email'
@@ -195,7 +195,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'with succeeded pipeline' do
before do
data[:object_attributes][:status] = 'success'
- pipeline.update(status: 'success')
+ pipeline.update!(status: 'success')
end
it_behaves_like 'not sending email'
@@ -206,7 +206,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'on default branch' do
before do
data[:object_attributes][:ref] = project.default_branch
- pipeline.update(ref: project.default_branch)
+ pipeline.update!(ref: project.default_branch)
end
context 'notifications are enabled only for default branch' do
@@ -230,7 +230,7 @@ RSpec.describe PipelinesEmailService, :mailer do
before do
create(:protected_branch, project: project, name: 'a-protected-branch')
data[:object_attributes][:ref] = 'a-protected-branch'
- pipeline.update(ref: 'a-protected-branch')
+ pipeline.update!(ref: 'a-protected-branch')
end
context 'notifications are enabled only for default branch' do
@@ -253,7 +253,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'on a neither protected nor default branch' do
before do
data[:object_attributes][:ref] = 'a-random-branch'
- pipeline.update(ref: 'a-random-branch')
+ pipeline.update!(ref: 'a-random-branch')
end
context 'notifications are enabled only for default branch' do
@@ -281,7 +281,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'with failed pipeline' do
before do
data[:object_attributes][:status] = 'failed'
- pipeline.update(status: 'failed')
+ pipeline.update!(status: 'failed')
end
it_behaves_like 'not sending email'
@@ -295,7 +295,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'with failed pipeline' do
before do
data[:object_attributes][:status] = 'failed'
- pipeline.update(status: 'failed')
+ pipeline.update!(status: 'failed')
end
it_behaves_like 'sending email'
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index a3fda33664a..f71dad86a08 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do
let(:project) { create(:project) }
subject(:service) do
- described_class.create(
+ described_class.create!(
project: project,
properties: {
teamcity_url: teamcity_url,
@@ -85,7 +85,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do
teamcity_service = service
teamcity_service.teamcity_url = 'http://gitlab1.com'
- teamcity_service.save
+ teamcity_service.save!
expect(teamcity_service.password).to be_nil
end
@@ -94,7 +94,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do
teamcity_service = service
teamcity_service.username = 'some_name'
- teamcity_service.save
+ teamcity_service.save!
expect(teamcity_service.password).to eq('password')
end
@@ -104,7 +104,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do
teamcity_service.teamcity_url = 'http://gitlab_edited.com'
teamcity_service.password = 'password'
- teamcity_service.save
+ teamcity_service.save!
expect(teamcity_service.password).to eq('password')
expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com')
@@ -117,7 +117,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do
teamcity_service.teamcity_url = 'http://gitlab_edited.com'
teamcity_service.password = 'password'
- teamcity_service.save
+ teamcity_service.save!
expect(teamcity_service.password).to eq('password')
expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com')
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f589589af8f..fe971832695 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -61,6 +61,7 @@ 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(:ewm_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) }
@@ -84,7 +85,6 @@ RSpec.describe Project do
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
- it { is_expected.to have_many(:pages_domains) }
it { is_expected.to have_many(:labels).class_name('ProjectLabel') }
it { is_expected.to have_many(:users_star_projects) }
it { is_expected.to have_many(:repository_languages) }
@@ -124,6 +124,11 @@ RSpec.describe Project do
it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile') }
it { is_expected.to have_many(:pipeline_artifacts) }
+ # GitLab Pages
+ it { is_expected.to have_many(:pages_domains) }
+ it { is_expected.to have_one(:pages_metadatum) }
+ it { is_expected.to have_many(:pages_deployments) }
+
it_behaves_like 'model with repository' do
let_it_be(:container) { create(:project, :repository, path: 'somewhere') }
let(:stubbed_container) { build_stubbed(:project) }
@@ -131,7 +136,7 @@ RSpec.describe Project do
end
it_behaves_like 'model with wiki' do
- let(:container) { create(:project, :wiki_repo) }
+ let_it_be(:container) { create(:project, :wiki_repo) }
let(:container_without_wiki) { create(:project) }
end
@@ -202,11 +207,11 @@ RSpec.describe Project do
end
describe '#members & #requesters' do
- let(:project) { create(:project, :public) }
- let(:requester) { create(:user) }
- let(:developer) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:requester) { create(:user) }
+ let_it_be(:developer) { create(:user) }
- before do
+ before_all do
project.request_access(requester)
project.add_developer(developer)
end
@@ -453,9 +458,9 @@ RSpec.describe Project do
end
describe '#all_pipelines' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
- before do
+ before_all do
create(:ci_pipeline, project: project, ref: 'master', source: :web)
create(:ci_pipeline, project: project, ref: 'master', source: :external)
end
@@ -477,7 +482,7 @@ RSpec.describe Project do
end
describe '#has_packages?' do
- let(:project) { create(:project, :public) }
+ let_it_be(:project) { create(:project, :public) }
subject { project.has_packages?(package_type) }
@@ -517,14 +522,15 @@ RSpec.describe Project do
end
describe '#ci_pipelines' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
- before do
+ before_all do
create(:ci_pipeline, project: project, ref: 'master', source: :web)
create(:ci_pipeline, project: project, ref: 'master', source: :external)
+ create(:ci_pipeline, project: project, ref: 'master', source: :webide)
end
- it 'has ci pipelines' do
+ it 'excludes dangling pipelines such as :webide' do
expect(project.ci_pipelines.size).to eq(2)
end
@@ -542,7 +548,7 @@ RSpec.describe Project do
describe '#autoclose_referenced_issues' do
context 'when DB entry is nil' do
- let(:project) { create(:project, autoclose_referenced_issues: nil) }
+ let(:project) { build(:project, autoclose_referenced_issues: nil) }
it 'returns true' do
expect(project.autoclose_referenced_issues).to be_truthy
@@ -550,7 +556,7 @@ RSpec.describe Project do
end
context 'when DB entry is true' do
- let(:project) { create(:project, autoclose_referenced_issues: true) }
+ let(:project) { build(:project, autoclose_referenced_issues: true) }
it 'returns true' do
expect(project.autoclose_referenced_issues).to be_truthy
@@ -558,7 +564,7 @@ RSpec.describe Project do
end
context 'when DB entry is false' do
- let(:project) { create(:project, autoclose_referenced_issues: false) }
+ let(:project) { build(:project, autoclose_referenced_issues: false) }
it 'returns false' do
expect(project.autoclose_referenced_issues).to be_falsey
@@ -768,8 +774,8 @@ RSpec.describe Project do
end
describe "#new_issuable_address" do
- let(:project) { create(:project, path: "somewhere") }
- let(:user) { create(:user) }
+ let_it_be(:project) { create(:project, path: "somewhere") }
+ let_it_be(:user) { create(:user) }
context 'incoming email enabled' do
before do
@@ -850,11 +856,11 @@ RSpec.describe Project do
end
describe '#get_issue' do
- let(:project) { create(:project) }
- let!(:issue) { create(:issue, project: project) }
- let(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let!(:issue) { create(:issue, project: project) }
- before do
+ before_all do
project.add_developer(user)
end
@@ -926,7 +932,7 @@ RSpec.describe Project do
end
describe '#issue_exists?' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
it 'is truthy when issue exists' do
expect(project).to receive(:get_issue).and_return(double)
@@ -1019,7 +1025,7 @@ RSpec.describe Project do
end
describe '#cache_has_external_issue_tracker' do
- let(:project) { create(:project, has_external_issue_tracker: nil) }
+ let_it_be(:project) { create(:project, has_external_issue_tracker: nil) }
it 'stores true if there is any external_issue_tracker' do
services = double(:service, external_issue_trackers: [RedmineService.new])
@@ -1049,7 +1055,7 @@ RSpec.describe Project do
end
describe '#cache_has_external_wiki' do
- let(:project) { create(:project, has_external_wiki: nil) }
+ let_it_be(:project) { create(:project, has_external_wiki: nil) }
it 'stores true if there is any external_wikis' do
services = double(:service, external_wikis: [ExternalWikiService.new])
@@ -1115,7 +1121,7 @@ RSpec.describe Project do
end
describe '#external_wiki' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
context 'with an active external wiki' do
before do
@@ -1268,60 +1274,6 @@ RSpec.describe Project do
end
end
- describe '#pipeline_for' do
- let(:project) { create(:project, :repository) }
-
- shared_examples 'giving the correct pipeline' do
- it { is_expected.to eq(pipeline) }
-
- context 'return latest' do
- let!(:pipeline2) { create_pipeline(project) }
-
- it { is_expected.to eq(pipeline2) }
- end
- end
-
- context 'with a matching pipeline' do
- let!(:pipeline) { create_pipeline(project) }
-
- context 'with explicit sha' do
- subject { project.pipeline_for('master', pipeline.sha) }
-
- it_behaves_like 'giving the correct pipeline'
-
- context 'with supplied id' do
- let!(:other_pipeline) { create_pipeline(project) }
-
- subject { project.pipeline_for('master', pipeline.sha, other_pipeline.id) }
-
- it { is_expected.to eq(other_pipeline) }
- end
- end
-
- context 'with implicit sha' do
- subject { project.pipeline_for('master') }
-
- it_behaves_like 'giving the correct pipeline'
- end
- end
-
- context 'when there is no matching pipeline' do
- subject { project.pipeline_for('master') }
-
- it { is_expected.to be_nil }
- end
- end
-
- describe '#pipelines_for' do
- let(:project) { create(:project, :repository) }
- let!(:pipeline) { create_pipeline(project) }
- let!(:other_pipeline) { create_pipeline(project) }
-
- subject { project.pipelines_for(project.default_branch, project.commit.sha) }
-
- it { is_expected.to contain_exactly(pipeline, other_pipeline) }
- end
-
describe '#builds_enabled' do
let(:project) { create(:project) }
@@ -1362,6 +1314,36 @@ RSpec.describe Project do
end
end
+ describe '.with_active_jira_services' do
+ it 'returns the correct project' do
+ active_jira_service = create(:jira_service)
+ active_service = create(:service, active: true)
+
+ expect(described_class.with_active_jira_services).to include(active_jira_service.project)
+ expect(described_class.with_active_jira_services).not_to include(active_service.project)
+ end
+ end
+
+ describe '.with_jira_dvcs_cloud' do
+ it 'returns the correct project' do
+ jira_dvcs_cloud_project = create(:project, :jira_dvcs_cloud)
+ jira_dvcs_server_project = create(:project, :jira_dvcs_server)
+
+ expect(described_class.with_jira_dvcs_cloud).to include(jira_dvcs_cloud_project)
+ expect(described_class.with_jira_dvcs_cloud).not_to include(jira_dvcs_server_project)
+ end
+ end
+
+ describe '.with_jira_dvcs_server' do
+ it 'returns the correct project' do
+ jira_dvcs_server_project = create(:project, :jira_dvcs_server)
+ jira_dvcs_cloud_project = create(:project, :jira_dvcs_cloud)
+
+ expect(described_class.with_jira_dvcs_server).to include(jira_dvcs_server_project)
+ expect(described_class.with_jira_dvcs_server).not_to include(jira_dvcs_cloud_project)
+ end
+ end
+
describe '.cached_count', :use_clean_rails_memory_store_caching do
let(:group) { create(:group, :public) }
let!(:project1) { create(:project, :public, group: group) }
@@ -1759,7 +1741,7 @@ RSpec.describe Project do
end
describe '#visibility_level_allowed?' do
- let(:project) { create(:project, :internal) }
+ let_it_be(:project) { create(:project, :internal) }
context 'when checking on non-forked project' do
it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
@@ -1768,7 +1750,6 @@ RSpec.describe Project do
end
context 'when checking on forked project' do
- let(:project) { create(:project, :internal) }
let(:forked_project) { fork_project(project) }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
@@ -1953,7 +1934,7 @@ RSpec.describe Project do
end
describe '.optionally_search' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
it 'searches for projects matching the query if one is given' do
relation = described_class.optionally_search(project.name)
@@ -2010,7 +1991,7 @@ RSpec.describe Project do
end
describe '.search_by_title' do
- let(:project) { create(:project, name: 'kittens') }
+ let_it_be(:project) { create(:project, name: 'kittens') }
it 'returns projects with a matching name' do
expect(described_class.search_by_title(project.name)).to eq([project])
@@ -2026,11 +2007,11 @@ RSpec.describe Project do
end
context 'when checking projects from groups' do
- let(:private_group) { create(:group, visibility_level: 0) }
- let(:internal_group) { create(:group, visibility_level: 10) }
+ let(:private_group) { build(:group, visibility_level: 0) }
+ let(:internal_group) { build(:group, visibility_level: 10) }
- let(:private_project) { create(:project, :private, group: private_group) }
- let(:internal_project) { create(:project, :internal, group: internal_group) }
+ let(:private_project) { build(:project, :private, group: private_group) }
+ let(:internal_project) { build(:project, :internal, group: internal_group) }
context 'when group is private project can not be internal' do
it { expect(private_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_falsey }
@@ -2094,7 +2075,7 @@ RSpec.describe Project do
end
describe '#create_repository' do
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { build(:project, :repository) }
context 'using a regular repository' do
it 'creates the repository' do
@@ -2120,7 +2101,7 @@ RSpec.describe Project do
end
describe '#ensure_repository' do
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { build(:project, :repository) }
it 'creates the repository if it not exist' do
allow(project).to receive(:repository_exists?).and_return(false)
@@ -2174,7 +2155,7 @@ RSpec.describe Project do
end
describe '#container_registry_url' do
- let(:project) { create(:project) }
+ let_it_be(:project) { build(:project) }
subject { project.container_registry_url }
@@ -2201,7 +2182,7 @@ RSpec.describe Project do
end
describe '#has_container_registry_tags?' do
- let(:project) { create(:project) }
+ let(:project) { build(:project) }
context 'when container registry is enabled' do
before do
@@ -2267,7 +2248,7 @@ RSpec.describe Project do
describe '#ci_config_path=' do
using RSpec::Parameterized::TableSyntax
- let(:project) { create(:project) }
+ let(:project) { build_stubbed(:project) }
where(:default_ci_config_path, :project_ci_config_path, :expected_ci_config_path) do
nil | :notset | :default
@@ -2322,8 +2303,8 @@ RSpec.describe Project do
end
describe '#latest_successful_build_for_ref' do
- let(:project) { create(:project, :repository) }
- let(:pipeline) { create_pipeline(project) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:pipeline) { create_pipeline(project) }
it_behaves_like 'latest successful build for sha or ref'
@@ -2338,47 +2319,95 @@ RSpec.describe Project do
end
end
- describe '#latest_pipeline_for_ref' do
- let(:project) { create(:project, :repository) }
+ describe '#latest_pipeline' do
+ let_it_be(:project) { create(:project, :repository) }
let(:second_branch) { project.repository.branches[2] }
let!(:pipeline_for_default_branch) do
- create(:ci_empty_pipeline, project: project, sha: project.commit.id,
- ref: project.default_branch)
+ create(:ci_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)
+ create(:ci_pipeline, project: project, sha: second_branch.target,
+ ref: second_branch.name)
end
- before do
- create(:ci_empty_pipeline, project: project, sha: project.commit.parent.id,
- ref: project.default_branch)
+ let!(:other_pipeline_for_default_branch) do
+ create(:ci_pipeline, project: project, sha: project.commit.parent.id,
+ ref: project.default_branch)
end
context 'default repository branch' do
- subject { project.latest_pipeline_for_ref(project.default_branch) }
+ context 'when explicitly provided' do
+ subject { project.latest_pipeline(project.default_branch) }
+
+ it { is_expected.to eq(pipeline_for_default_branch) }
+ end
+
+ context 'when not provided' do
+ subject { project.latest_pipeline }
+
+ it { is_expected.to eq(pipeline_for_default_branch) }
+ end
+
+ context 'with provided sha' do
+ subject { project.latest_pipeline(project.default_branch, project.commit.parent.id) }
- it { is_expected.to eq(pipeline_for_default_branch) }
+ it { is_expected.to eq(other_pipeline_for_default_branch) }
+ end
end
context 'provided ref' do
- subject { project.latest_pipeline_for_ref(second_branch.name) }
+ subject { project.latest_pipeline(second_branch.name) }
it { is_expected.to eq(pipeline_for_second_branch) }
+
+ context 'with provided sha' do
+ let!(:latest_pipeline_for_ref) do
+ create(:ci_pipeline, project: project, sha: pipeline_for_second_branch.sha,
+ ref: pipeline_for_second_branch.ref)
+ end
+
+ subject { project.latest_pipeline(second_branch.name, second_branch.target) }
+
+ it { is_expected.to eq(latest_pipeline_for_ref) }
+ end
end
context 'bad ref' do
- subject { project.latest_pipeline_for_ref(SecureRandom.uuid) }
+ before do
+ # ensure we don't skip the filter by ref by mistakenly return this pipeline
+ create(:ci_pipeline, project: project)
+ end
+
+ subject { project.latest_pipeline(SecureRandom.uuid) }
it { is_expected.to be_nil }
end
+
+ context 'on deleted ref' do
+ let(:branch) { project.repository.branches.last }
+
+ let!(:pipeline_on_deleted_ref) do
+ create(:ci_pipeline, project: project, sha: branch.target, ref: branch.name)
+ end
+
+ before do
+ project.repository.rm_branch(project.owner, branch.name)
+ end
+
+ subject { project.latest_pipeline(branch.name) }
+
+ it 'always returns nil despite a pipeline exists' do
+ expect(subject).to be_nil
+ end
+ end
end
describe '#latest_successful_build_for_sha' do
- let(:project) { create(:project, :repository) }
- let(:pipeline) { create_pipeline(project) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:pipeline) { create_pipeline(project) }
it_behaves_like 'latest successful build for sha or ref'
@@ -2386,8 +2415,8 @@ RSpec.describe Project do
end
describe '#latest_successful_build_for_ref!' do
- let(:project) { create(:project, :repository) }
- let(:pipeline) { create_pipeline(project) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:pipeline) { create_pipeline(project) }
context 'with many builds' do
it 'gives the latest builds from latest pipeline' do
@@ -2460,7 +2489,7 @@ RSpec.describe Project do
end
describe '#jira_import_status' do
- let(:project) { create(:project, import_type: 'jira') }
+ let_it_be(:project) { create(:project, import_type: 'jira') }
context 'when no jira imports' do
it 'returns none' do
@@ -2666,7 +2695,7 @@ RSpec.describe Project do
end
describe '#remote_mirror_available?' do
- let(:project) { create(:project) }
+ let(:project) { build_stubbed(:project) }
context 'when remote mirror global setting is enabled' do
it 'returns true' do
@@ -2707,10 +2736,10 @@ RSpec.describe Project do
end
describe '#ancestors_upto' do
- let(:parent) { create(:group) }
- let(:child) { create(:group, parent: parent) }
- let(:child2) { create(:group, parent: child) }
- let(:project) { create(:project, namespace: child2) }
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:child) { create(:group, parent: parent) }
+ let_it_be(:child2) { create(:group, parent: child) }
+ let_it_be(:project) { create(:project, namespace: child2) }
it 'returns all ancestors when no namespace is given' do
expect(project.ancestors_upto).to contain_exactly(child2, child, parent)
@@ -2755,7 +2784,7 @@ RSpec.describe Project do
end
describe '#emails_disabled?' do
- let(:project) { create(:project, emails_disabled: false) }
+ let(:project) { build(:project, emails_disabled: false) }
context 'emails disabled in group' do
it 'returns true' do
@@ -2783,7 +2812,7 @@ RSpec.describe Project do
end
describe '#lfs_enabled?' do
- let(:project) { create(:project) }
+ let(:project) { build(:project) }
shared_examples 'project overrides group' do
it 'returns true when enabled in project' do
@@ -2845,7 +2874,7 @@ RSpec.describe Project do
end
describe '#change_head' do
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
it 'returns error if branch does not exist' do
expect(project.change_head('unexisted-branch')).to be false
@@ -2876,6 +2905,20 @@ RSpec.describe Project do
end
end
+ describe '#lfs_objects_for_repository_types' do
+ let(:project) { create(:project) }
+
+ it 'returns LFS objects of the specified type only' do
+ none, design, wiki = *[nil, :design, :wiki].map do |type|
+ create(:lfs_objects_project, project: project, repository_type: type).lfs_object
+ end
+
+ expect(project.lfs_objects_for_repository_types(nil)).to contain_exactly(none)
+ expect(project.lfs_objects_for_repository_types(nil, :wiki)).to contain_exactly(none, wiki)
+ expect(project.lfs_objects_for_repository_types(:design)).to contain_exactly(design)
+ end
+ end
+
context 'forks' do
include ProjectForksHelper
@@ -2951,68 +2994,6 @@ RSpec.describe Project do
expect(project.forks).to contain_exactly(forked_project)
end
end
-
- describe '#lfs_storage_project' do
- it 'returns self for non-forks' do
- expect(project.lfs_storage_project).to eq project
- end
-
- it 'returns the fork network root for forks' do
- second_fork = fork_project(forked_project)
-
- expect(second_fork.lfs_storage_project).to eq project
- end
-
- it 'returns self when fork_source is nil' do
- expect(forked_project).to receive(:fork_source).and_return(nil)
-
- expect(forked_project.lfs_storage_project).to eq forked_project
- end
- end
-
- describe '#all_lfs_objects' do
- let(:lfs_object) { create(:lfs_object) }
-
- context 'when LFS object is only associated to the source' do
- before do
- project.lfs_objects << lfs_object
- end
-
- it 'returns the lfs object for a project' do
- expect(project.all_lfs_objects).to contain_exactly(lfs_object)
- end
-
- it 'returns the lfs object for a fork' do
- expect(forked_project.all_lfs_objects).to contain_exactly(lfs_object)
- end
- end
-
- context 'when LFS object is only associated to the fork' do
- before do
- forked_project.lfs_objects << lfs_object
- end
-
- it 'returns nothing' do
- expect(project.all_lfs_objects).to be_empty
- end
-
- it 'returns the lfs object for a fork' do
- expect(forked_project.all_lfs_objects).to contain_exactly(lfs_object)
- end
- end
-
- context 'when LFS object is associated to both source and fork' do
- before do
- project.lfs_objects << lfs_object
- forked_project.lfs_objects << lfs_object
- end
-
- it 'returns the lfs object for the source and fork' do
- expect(project.all_lfs_objects).to contain_exactly(lfs_object)
- expect(forked_project.all_lfs_objects).to contain_exactly(lfs_object)
- end
- end
- end
end
describe '#set_repository_read_only!' do
@@ -3040,7 +3021,7 @@ RSpec.describe Project do
end
describe '#pushes_since_gc' do
- let(:project) { create(:project) }
+ let(:project) { build_stubbed(:project) }
after do
project.reset_pushes_since_gc
@@ -3062,7 +3043,7 @@ RSpec.describe Project do
end
describe '#increment_pushes_since_gc' do
- let(:project) { create(:project) }
+ let(:project) { build_stubbed(:project) }
after do
project.reset_pushes_since_gc
@@ -3076,7 +3057,7 @@ RSpec.describe Project do
end
describe '#reset_pushes_since_gc' do
- let(:project) { create(:project) }
+ let(:project) { build_stubbed(:project) }
after do
project.reset_pushes_since_gc
@@ -3092,7 +3073,7 @@ RSpec.describe Project do
end
describe '#deployment_variables' do
- let(:project) { create(:project) }
+ let(:project) { build_stubbed(:project) }
let(:environment) { 'production' }
let(:namespace) { 'namespace' }
@@ -3169,7 +3150,7 @@ RSpec.describe Project do
end
describe '#default_environment' do
- let(:project) { create(:project) }
+ let(:project) { build(:project) }
it 'returns production environment when it exists' do
production = create(:environment, name: "production", project: project)
@@ -3191,7 +3172,7 @@ RSpec.describe Project do
end
describe '#ci_variables_for' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
let(:environment_scope) { '*' }
let!(:ci_variable) do
@@ -3346,7 +3327,7 @@ RSpec.describe Project do
end
describe '#ci_instance_variables_for' do
- let(:project) { create(:project) }
+ let(:project) { build_stubbed(:project) }
let!(:instance_variable) do
create(:ci_instance_variable, value: 'secret')
@@ -5831,32 +5812,57 @@ RSpec.describe Project do
end
end
- context 'pages deployed' do
+ describe '#mark_pages_as_deployed' do
let(:project) { create(:project) }
+ let(:artifacts_archive) { create(:ci_job_artifact, project: project) }
- {
- mark_pages_as_deployed: true,
- mark_pages_as_not_deployed: false
- }.each do |method_name, flag|
- describe method_name do
- it "creates new record and sets deployed to #{flag} if none exists yet" do
- project.pages_metadatum.destroy!
- project.reload
+ it "works when artifacts_archive is missing" do
+ project.mark_pages_as_deployed
- project.send(method_name)
+ expect(project.pages_metadatum.reload.deployed).to eq(true)
+ end
- expect(project.pages_metadatum.reload.deployed).to eq(flag)
- end
+ it "creates new record and sets deployed to true if none exists yet" do
+ project.pages_metadatum.destroy!
+ project.reload
- it "updates the existing record and sets deployed to #{flag}" do
- pages_metadatum = project.pages_metadatum
- pages_metadatum.update!(deployed: !flag)
+ project.mark_pages_as_deployed(artifacts_archive: artifacts_archive)
- expect { project.send(method_name) }.to change {
- pages_metadatum.reload.deployed
- }.from(!flag).to(flag)
- end
- end
+ expect(project.pages_metadatum.reload.deployed).to eq(true)
+ end
+
+ it "updates the existing record and sets deployed to true and records artifact archive" do
+ pages_metadatum = project.pages_metadatum
+ pages_metadatum.update!(deployed: false)
+
+ expect do
+ project.mark_pages_as_deployed(artifacts_archive: artifacts_archive)
+ end.to change { pages_metadatum.reload.deployed }.from(false).to(true)
+ .and change { pages_metadatum.reload.artifacts_archive }.from(nil).to(artifacts_archive)
+ end
+ end
+
+ describe '#mark_pages_as_not_deployed' do
+ let(:project) { create(:project) }
+ let(:artifacts_archive) { create(:ci_job_artifact, project: project) }
+
+ it "creates new record and sets deployed to false if none exists yet" do
+ project.pages_metadatum.destroy!
+ project.reload
+
+ project.mark_pages_as_not_deployed
+
+ expect(project.pages_metadatum.reload.deployed).to eq(false)
+ end
+
+ it "updates the existing record and sets deployed to false and clears artifacts_archive" do
+ pages_metadatum = project.pages_metadatum
+ pages_metadatum.update!(deployed: true, artifacts_archive: artifacts_archive)
+
+ expect do
+ project.mark_pages_as_not_deployed
+ end.to change { pages_metadatum.reload.deployed }.from(true).to(false)
+ .and change { pages_metadatum.reload.artifacts_archive }.from(artifacts_archive).to(nil)
end
end
@@ -6043,6 +6049,18 @@ RSpec.describe Project do
end
end
+ describe '#jira_subscription_exists?' do
+ let(:project) { create(:project) }
+
+ subject { project.jira_subscription_exists? }
+
+ context 'jira connect subscription exists' do
+ let!(:jira_connect_subscription) { create(:jira_connect_subscription, namespace: project.namespace) }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
describe 'with services and chat names' do
subject { create(:project) }
@@ -6088,53 +6106,6 @@ RSpec.describe Project do
end
end
- describe '#all_lfs_objects_oids' do
- let(:project) { create(:project) }
- let(:lfs_object) { create(:lfs_object) }
- let(:another_lfs_object) { create(:lfs_object) }
-
- subject { project.all_lfs_objects_oids }
-
- context 'when project has associated LFS objects' do
- before do
- create(:lfs_objects_project, lfs_object: lfs_object, project: project)
- create(:lfs_objects_project, lfs_object: another_lfs_object, project: project)
- end
-
- it 'returns OIDs of LFS objects' do
- expect(subject).to match_array([lfs_object.oid, another_lfs_object.oid])
- end
-
- context 'and there are specified oids' do
- subject { project.all_lfs_objects_oids(oids: [lfs_object.oid]) }
-
- it 'returns OIDs of LFS objects that match specified oids' do
- expect(subject).to eq([lfs_object.oid])
- end
- end
- end
-
- context 'when fork has associated LFS objects to itself and source' do
- let(:source) { create(:project) }
- let(:project) { fork_project(source) }
-
- before do
- create(:lfs_objects_project, lfs_object: lfs_object, project: source)
- create(:lfs_objects_project, lfs_object: another_lfs_object, project: project)
- end
-
- it 'returns OIDs of LFS objects' do
- expect(subject).to match_array([lfs_object.oid, another_lfs_object.oid])
- end
- end
-
- context 'when project has no associated LFS objects' do
- it 'returns empty array' do
- expect(subject).to be_empty
- end
- end
- end
-
describe '#lfs_objects_oids' do
let(:project) { create(:project) }
let(:lfs_object) { create(:lfs_object) }
@@ -6475,6 +6446,49 @@ RSpec.describe Project do
end
end
+ describe '#enabled_group_deploy_keys' do
+ let_it_be(:project) { create(:project) }
+
+ subject { project.enabled_group_deploy_keys }
+
+ context 'when a project does not have a group' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'when a project has a parent group' do
+ let!(:group) { create(:group, projects: [project]) }
+
+ context 'and this group has a group deploy key enabled' do
+ let!(:group_deploy_key) { create(:group_deploy_key, groups: [group]) }
+
+ it { is_expected.to contain_exactly(group_deploy_key) }
+
+ context 'and this group has parent group which also has a group deploy key enabled' do
+ let(:super_group) { create(:group) }
+
+ it 'returns both group deploy keys' do
+ super_group = create(:group)
+ super_group_deploy_key = create(:group_deploy_key, groups: [super_group])
+ group.update!(parent: super_group)
+
+ expect(subject).to contain_exactly(group_deploy_key, super_group_deploy_key)
+ end
+ end
+ end
+
+ context 'and another group has a group deploy key enabled' do
+ let_it_be(:group_deploy_key) { create(:group_deploy_key) }
+
+ it 'does not return this group deploy key' do
+ another_group = create(:group)
+ create(:group_deploy_key, groups: [another_group])
+
+ expect(subject).to be_empty
+ end
+ end
+ end
+ end
+
def finish_job(export_job)
export_job.start
export_job.finish
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 5f66de3a63c..383fabcfffb 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -32,8 +32,9 @@ RSpec.describe ProjectStatistics do
repository_size: 2.exabytes,
wiki_size: 1.exabytes,
lfs_objects_size: 2.exabytes,
- build_artifacts_size: 2.exabytes - 1,
- snippets_size: 1.exabyte
+ build_artifacts_size: 1.exabyte,
+ snippets_size: 1.exabyte,
+ pipeline_artifacts_size: 1.exabyte - 1
)
statistics.reload
@@ -42,9 +43,10 @@ RSpec.describe ProjectStatistics do
expect(statistics.repository_size).to eq(2.exabytes)
expect(statistics.wiki_size).to eq(1.exabytes)
expect(statistics.lfs_objects_size).to eq(2.exabytes)
- expect(statistics.build_artifacts_size).to eq(2.exabytes - 1)
+ expect(statistics.build_artifacts_size).to eq(1.exabyte)
expect(statistics.storage_size).to eq(8.exabytes - 1)
expect(statistics.snippets_size).to eq(1.exabyte)
+ expect(statistics.pipeline_artifacts_size).to eq(1.exabyte - 1)
end
end
@@ -282,12 +284,13 @@ RSpec.describe ProjectStatistics do
repository_size: 2,
wiki_size: 4,
lfs_objects_size: 3,
- snippets_size: 2
+ snippets_size: 2,
+ pipeline_artifacts_size: 3
)
statistics.reload
- expect(statistics.storage_size).to eq 11
+ expect(statistics.storage_size).to eq 14
end
it 'works during wiki_size backfill' do
@@ -339,6 +342,12 @@ RSpec.describe ProjectStatistics do
it_behaves_like 'a statistic that increases storage_size'
end
+ context 'when adjusting :pipeline_artifacts_size' do
+ let(:stat) { :pipeline_artifacts_size }
+
+ it_behaves_like 'a statistic that increases storage_size'
+ end
+
context 'when adjusting :packages_size' do
let(:stat) { :packages_size }
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 34ec856459c..bbc056889d6 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -3,6 +3,8 @@
require "spec_helper"
RSpec.describe ProjectTeam do
+ include ProjectForksHelper
+
let(:maintainer) { create(:user) }
let(:reporter) { create(:user) }
let(:guest) { create(:user) }
@@ -237,6 +239,35 @@ RSpec.describe ProjectTeam do
end
end
+ describe '#contributor?' do
+ let(:project) { create(:project, :public, :repository) }
+
+ context 'when user is a member of project' do
+ before do
+ project.add_maintainer(maintainer)
+ project.add_reporter(reporter)
+ project.add_guest(guest)
+ end
+
+ it { expect(project.team.contributor?(maintainer.id)).to be false }
+ it { expect(project.team.contributor?(reporter.id)).to be false }
+ it { expect(project.team.contributor?(guest.id)).to be false }
+ end
+
+ context 'when user has at least one merge request merged into default_branch' do
+ let(:contributor) { create(:user) }
+ let(:user_without_access) { create(:user) }
+ let(:first_fork_project) { fork_project(project, contributor, repository: true) }
+
+ before do
+ create(:merge_request, :merged, author: contributor, target_project: project, source_project: first_fork_project, target_branch: project.default_branch.to_s)
+ end
+
+ it { expect(project.team.contributor?(contributor.id)).to be true }
+ it { expect(project.team.contributor?(user_without_access.id)).to be false }
+ end
+ end
+
describe '#max_member_access' do
let(:requester) { create(:user) }
@@ -366,6 +397,66 @@ RSpec.describe ProjectTeam do
end
end
+ describe '#contribution_check_for_user_ids', :request_store do
+ let(:project) { create(:project, :public, :repository) }
+ let(:contributor) { create(:user) }
+ let(:second_contributor) { create(:user) }
+ let(:user_without_access) { create(:user) }
+ let(:first_fork_project) { fork_project(project, contributor, repository: true) }
+ let(:second_fork_project) { fork_project(project, second_contributor, repository: true) }
+
+ let(:users) do
+ [contributor, second_contributor, user_without_access].map(&:id)
+ end
+
+ let(:expected) do
+ {
+ contributor.id => true,
+ second_contributor.id => true,
+ user_without_access.id => false
+ }
+ end
+
+ before do
+ create(:merge_request, :merged, author: contributor, target_project: project, source_project: first_fork_project, target_branch: project.default_branch.to_s)
+ create(:merge_request, :merged, author: second_contributor, target_project: project, source_project: second_fork_project, target_branch: project.default_branch.to_s)
+ end
+
+ def contributors(users)
+ project.team.contribution_check_for_user_ids(users)
+ end
+
+ it 'does not perform extra queries when asked for users who have already been found' do
+ contributors(users)
+
+ expect { contributors([contributor.id]) }.not_to exceed_query_limit(0)
+
+ expect(contributors([contributor.id])).to eq(expected)
+ end
+
+ it 'only requests the extra users when uncached users are passed' do
+ new_contributor = create(:user)
+ new_fork_project = fork_project(project, new_contributor, repository: true)
+ second_new_user = create(:user)
+ all_users = users + [new_contributor.id, second_new_user.id]
+ create(:merge_request, :merged, author: new_contributor, target_project: project, source_project: new_fork_project, target_branch: project.default_branch.to_s)
+
+ expected_all = expected.merge(new_contributor.id => true,
+ second_new_user.id => false)
+
+ contributors(users)
+
+ queries = ActiveRecord::QueryRecorder.new { contributors(all_users) }
+
+ expect(queries.count).to eq(1)
+ expect(contributors([new_contributor.id])).to eq(expected_all)
+ end
+
+ it 'returns correct contributors' do
+ expect(contributors(users)).to eq(expected)
+ end
+ end
+
shared_examples 'max member access for users' do
let(:project) { create(:project) }
let(:group) { create(:group) }
@@ -438,9 +529,9 @@ RSpec.describe ProjectTeam do
it 'does not perform extra queries when asked for users who have already been found' do
access_levels(users)
- expect { access_levels(users) }.not_to exceed_query_limit(0)
+ expect { access_levels([maintainer.id]) }.not_to exceed_query_limit(0)
- expect(access_levels(users)).to eq(expected)
+ expect(access_levels([maintainer.id])).to eq(expected)
end
it 'only requests the extra users when uncached users are passed' do
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index d9c5fed542e..29c3d0e1a73 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -17,19 +17,28 @@ RSpec.describe ProjectWiki do
end
end
- describe '#update_container_activity' do
+ describe '#after_wiki_activity' do
it 'updates project activity' do
wiki_container.update!(
last_activity_at: nil,
last_repository_updated_at: nil
)
- subject.create_page('Test Page', 'This is content')
+ subject.send(:after_wiki_activity)
wiki_container.reload
expect(wiki_container.last_activity_at).to be_within(1.minute).of(Time.current)
expect(wiki_container.last_repository_updated_at).to be_within(1.minute).of(Time.current)
end
end
+
+ describe '#after_post_receive' do
+ it 'updates project activity and expires caches' do
+ expect(wiki).to receive(:after_wiki_activity)
+ expect(ProjectCacheWorker).to receive(:perform_async).with(wiki_container.id, [], [:wiki_size])
+
+ subject.send(:after_post_receive)
+ end
+ end
end
end
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index ebc9760ab14..4c3151f431c 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -142,6 +142,20 @@ RSpec.describe RemoteMirror, :mailer do
end
end
+ describe '#bare_url' do
+ it 'returns the URL without any credentials' do
+ remote_mirror = build(:remote_mirror, url: 'http://user:pass@example.com/foo')
+
+ expect(remote_mirror.bare_url).to eq('http://example.com/foo')
+ end
+
+ it 'returns an empty string when the URL is nil' do
+ remote_mirror = build(:remote_mirror, url: nil)
+
+ expect(remote_mirror.bare_url).to eq('')
+ end
+ end
+
describe '#update_repository' do
it 'performs update including options' do
git_remote_mirror = stub_const('Gitlab::Git::RemoteMirror', spy)
@@ -283,7 +297,7 @@ RSpec.describe RemoteMirror, :mailer do
let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first }
around do |example|
- Timecop.freeze { example.run }
+ freeze_time { example.run }
end
context 'with remote mirroring disabled' do
@@ -397,7 +411,7 @@ RSpec.describe RemoteMirror, :mailer do
let(:timestamp) { Time.current - 5.minutes }
around do |example|
- Timecop.freeze { example.run }
+ freeze_time { example.run }
end
before do
@@ -442,16 +456,18 @@ RSpec.describe RemoteMirror, :mailer do
end
describe '#disabled?' do
+ let_it_be(:project) { create(:project, :repository) }
+
subject { remote_mirror.disabled? }
context 'when disabled' do
- let(:remote_mirror) { build(:remote_mirror, enabled: false) }
+ let(:remote_mirror) { build(:remote_mirror, project: project, enabled: false) }
it { is_expected.to be_truthy }
end
context 'when enabled' do
- let(:remote_mirror) { build(:remote_mirror, enabled: true) }
+ let(:remote_mirror) { build(:remote_mirror, project: project, enabled: true) }
it { is_expected.to be_falsy }
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index a6b79e55f02..a3042d619eb 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1263,6 +1263,7 @@ RSpec.describe Repository do
%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
+ %w(ab) | %w(abc/d a b) | false
end
with_them do
diff --git a/spec/models/resource_iteration_event_spec.rb b/spec/models/resource_iteration_event_spec.rb
deleted file mode 100644
index fe1310d7bf1..00000000000
--- a/spec/models/resource_iteration_event_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# 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_label_event_spec.rb b/spec/models/resource_label_event_spec.rb
index 6a235d3aa17..960db31d488 100644
--- a/spec/models/resource_label_event_spec.rb
+++ b/spec/models/resource_label_event_spec.rb
@@ -3,10 +3,12 @@
require 'spec_helper'
RSpec.describe ResourceLabelEvent, type: :model do
- subject { build(:resource_label_event, issue: issue) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:label) { create(:label, project: project) }
- let(:issue) { create(:issue) }
- let(:merge_request) { create(:merge_request) }
+ subject { build(:resource_label_event, issue: issue, label: label) }
it_behaves_like 'having unique enum values'
diff --git a/spec/models/resource_state_event_spec.rb b/spec/models/resource_state_event_spec.rb
index 1381b45cf9e..fc6575b2db8 100644
--- a/spec/models/resource_state_event_spec.rb
+++ b/spec/models/resource_state_event_spec.rb
@@ -11,4 +11,32 @@ RSpec.describe ResourceStateEvent, 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'
+
+ describe 'validations' do
+ describe 'Issuable validation' do
+ it 'is valid if an issue is set' do
+ subject.attributes = { issue: build_stubbed(:issue), merge_request: nil }
+
+ expect(subject).to be_valid
+ end
+
+ it 'is valid if a merge request is set' do
+ subject.attributes = { issue: nil, merge_request: build_stubbed(:merge_request) }
+
+ expect(subject).to be_valid
+ end
+
+ it 'is invalid if both issue and merge request are set' do
+ subject.attributes = { issue: build_stubbed(:issue), merge_request: build_stubbed(:merge_request) }
+
+ expect(subject).not_to be_valid
+ end
+
+ it 'is invalid if there is no issuable set' do
+ subject.attributes = { issue: nil, merge_request: nil }
+
+ expect(subject).not_to be_valid
+ end
+ end
+ end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index c4a9c0329c7..32e2012e284 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -3,8 +3,12 @@
require 'spec_helper'
RSpec.describe Service do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
describe "Associations" do
it { is_expected.to belong_to :project }
+ it { is_expected.to belong_to :group }
it { is_expected.to have_one :service_hook }
it { is_expected.to have_one :jira_tracker_data }
it { is_expected.to have_one :issue_tracker_data }
@@ -13,9 +17,6 @@ RSpec.describe Service do
describe 'validations' do
using RSpec::Parameterized::TableSyntax
- let(:group) { create(:group) }
- let(:project) { create(:project) }
-
it { is_expected.to validate_presence_of(:type) }
where(:project_id, :group_id, :template, :instance, :valid) do
@@ -91,17 +92,12 @@ RSpec.describe Service do
end
end
- describe '#operating?' do
- it 'is false when the service is not active' do
- expect(build(:service).operating?).to eq(false)
- end
-
- it 'is false when the service is not persisted' do
- expect(build(:service, active: true).operating?).to eq(false)
- end
+ describe '.for_group' do
+ let!(:service1) { create(:jira_service, project_id: nil, group_id: group.id) }
+ let!(:service2) { create(:jira_service) }
- it 'is true when the service is active and persisted' do
- expect(create(:service, active: true).operating?).to eq(true)
+ it 'returns the right group service' do
+ expect(described_class.for_group(group)).to match_array([service1])
end
end
@@ -134,20 +130,34 @@ RSpec.describe Service do
end
end
+ describe '#operating?' do
+ it 'is false when the service is not active' do
+ expect(build(:service).operating?).to eq(false)
+ end
+
+ it 'is false when the service is not persisted' do
+ expect(build(:service, active: true).operating?).to eq(false)
+ end
+
+ it 'is true when the service is active and persisted' do
+ expect(create(:service, active: true).operating?).to eq(true)
+ end
+ end
+
describe "Test Button" do
+ let(:service) { build(:service, project: project) }
+
describe '#can_test?' do
subject { service.can_test? }
- let(:service) { create(:service, project: project) }
-
context 'when repository is not empty' do
- let(:project) { create(:project, :repository) }
+ let(:project) { build(:project, :repository) }
it { is_expected.to be true }
end
context 'when repository is empty' do
- let(:project) { create(:project) }
+ let(:project) { build(:project) }
it { is_expected.to be true }
end
@@ -161,14 +171,23 @@ RSpec.describe Service do
it { is_expected.to be_falsey }
end
end
+
+ context 'when group-level service' do
+ Service.available_services_types.each do |service_type|
+ let(:service) do
+ service_type.constantize.new(group_id: group.id)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
describe '#test' do
let(:data) { 'test' }
- let(:service) { create(:service, project: project) }
context 'when repository is not empty' do
- let(:project) { create(:project, :repository) }
+ let(:project) { build(:project, :repository) }
it 'test runs execute' do
expect(service).to receive(:execute).with(data)
@@ -178,7 +197,7 @@ RSpec.describe Service do
end
context 'when repository is empty' do
- let(:project) { create(:project) }
+ let(:project) { build(:project) }
it 'test runs execute' do
expect(service).to receive(:execute).with(data)
@@ -189,14 +208,27 @@ RSpec.describe Service do
end
end
- describe '.find_or_initialize_instances' do
+ describe '.find_or_initialize_integration' do
+ let!(:service1) { create(:jira_service, project_id: nil, group_id: group.id) }
+ let!(:service2) { create(:jira_service) }
+
+ it 'returns the right service' do
+ expect(Service.find_or_initialize_integration('jira', group_id: group)).to eq(service1)
+ end
+
+ it 'does not create a new service' do
+ expect { Service.find_or_initialize_integration('redmine', group_id: group) }.not_to change { Service.count }
+ end
+ end
+
+ describe '.find_or_initialize_all' do
shared_examples 'service instances' do
it 'returns the available service instances' do
- expect(Service.find_or_initialize_instances.pluck(:type)).to match_array(Service.available_services_types)
+ expect(Service.find_or_initialize_all(Service.for_instance).pluck(:type)).to match_array(Service.available_services_types)
end
it 'does not create service instances' do
- expect { Service.find_or_initialize_instances }.not_to change { Service.count }
+ expect { Service.find_or_initialize_all(Service.for_instance) }.not_to change { Service.count }
end
end
@@ -211,9 +243,9 @@ RSpec.describe Service do
it_behaves_like 'service instances'
- context 'with a previous existing service (Previous) and a new service (Asana)' do
+ context 'with a previous existing service (MockCiService) and a new service (Asana)' do
before do
- Service.insert(type: 'PreviousService', instance: true)
+ Service.insert(type: 'MockCiService', instance: true)
Service.delete_by(type: 'AsanaService', instance: true)
end
@@ -231,8 +263,6 @@ RSpec.describe Service do
end
describe 'template' do
- let(:project) { create(:project) }
-
shared_examples 'retrieves service templates' do
it 'returns the available service templates' do
expect(Service.find_or_create_templates.pluck(:type)).to match_array(Service.available_services_types)
@@ -390,29 +420,49 @@ RSpec.describe Service do
end
end
- describe 'instance' do
- describe '.instance_for' do
- let_it_be(:jira_service) { create(:jira_service, :instance) }
- let_it_be(:slack_service) { create(:slack_service, :instance) }
-
- subject { described_class.instance_for(type) }
+ describe '.default_integration' do
+ context 'with an instance-level service' do
+ let_it_be(:instance_service) { create(:jira_service, :instance) }
- context 'Hipchat serivce' do
- let(:type) { 'HipchatService' }
+ it 'returns the instance service' do
+ expect(described_class.default_integration('JiraService', project)).to eq(instance_service)
+ end
- it { is_expected.to eq(nil) }
+ it 'returns nil for nonexistent service type' do
+ expect(described_class.default_integration('HipchatService', project)).to eq(nil)
end
- context 'Jira serivce' do
- let(:type) { 'JiraService' }
+ context 'with a group service' do
+ let_it_be(:group_service) { create(:jira_service, group_id: group.id, project_id: nil) }
- it { is_expected.to eq(jira_service) }
- end
+ it 'returns the group service for a project' do
+ expect(described_class.default_integration('JiraService', project)).to eq(group_service)
+ end
+
+ it 'returns the instance service for a group' do
+ expect(described_class.default_integration('JiraService', group)).to eq(instance_service)
+ end
- context 'Slack serivce' do
- let(:type) { 'SlackService' }
+ context 'with a subgroup' do
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let!(:project) { create(:project, group: subgroup) }
- it { is_expected.to eq(slack_service) }
+ it 'returns the closest group service for a project' do
+ expect(described_class.default_integration('JiraService', project)).to eq(group_service)
+ end
+
+ it 'returns the closest group service for a subgroup' do
+ expect(described_class.default_integration('JiraService', subgroup)).to eq(group_service)
+ end
+
+ context 'having a service' do
+ let!(:subgroup_service) { create(:jira_service, group_id: subgroup.id, project_id: nil) }
+
+ it 'returns the closest group service for a project' do
+ expect(described_class.default_integration('JiraService', project)).to eq(subgroup_service)
+ end
+ end
+ end
end
end
end
@@ -420,7 +470,7 @@ RSpec.describe Service do
describe "{property}_changed?" do
let(:service) do
BambooService.create(
- project: create(:project),
+ project: project,
properties: {
bamboo_url: 'http://gitlab.com',
username: 'mic',
@@ -460,7 +510,7 @@ RSpec.describe Service do
describe "{property}_touched?" do
let(:service) do
BambooService.create(
- project: create(:project),
+ project: project,
properties: {
bamboo_url: 'http://gitlab.com',
username: 'mic',
@@ -500,7 +550,7 @@ RSpec.describe Service do
describe "{property}_was" do
let(:service) do
BambooService.create(
- project: create(:project),
+ project: project,
properties: {
bamboo_url: 'http://gitlab.com',
username: 'mic',
@@ -540,7 +590,7 @@ RSpec.describe Service do
describe 'initialize service with no properties' do
let(:service) do
BugzillaService.create(
- project: create(:project),
+ project: project,
project_url: 'http://gitlab.example.com'
)
end
@@ -555,7 +605,6 @@ RSpec.describe Service do
end
describe "callbacks" do
- let(:project) { create(:project) }
let!(:service) do
RedmineService.new(
project: project,
@@ -622,8 +671,7 @@ RSpec.describe Service do
end
context 'logging' do
- let(:project) { create(:project) }
- let(:service) { create(:service, project: project) }
+ let(:service) { build(:service, project: project) }
let(:test_message) { "test message" }
let(:arguments) do
{
diff --git a/spec/models/snippet_input_action_spec.rb b/spec/models/snippet_input_action_spec.rb
index ca61b80df4c..43dc70bea98 100644
--- a/spec/models/snippet_input_action_spec.rb
+++ b/spec/models/snippet_input_action_spec.rb
@@ -22,9 +22,9 @@ RSpec.describe SnippetInputAction do
:move | 'foobar' | 'foobar' | nil | nil | false | :previous_path
:move | 'foobar' | 'foobar' | '' | nil | false | :previous_path
:move | 'foobar' | 'foobar' | 'foobar' | nil | false | :file_path
- :move | nil | 'foobar' | 'foobar' | nil | false | :file_path
- :move | '' | 'foobar' | 'foobar' | nil | false | :file_path
- :move | nil | 'foobar' | 'foo1' | nil | false | :file_path
+ :move | nil | 'foobar' | 'foobar' | nil | true | nil
+ :move | '' | 'foobar' | 'foobar' | nil | true | nil
+ :move | nil | 'foobar' | 'foo1' | nil | true | nil
:move | 'foobar' | nil | 'foo1' | nil | true | nil
:move | 'foobar' | '' | 'foo1' | nil | true | nil
:create | 'foobar' | nil | 'foobar' | nil | false | :content
diff --git a/spec/models/snippet_repository_spec.rb b/spec/models/snippet_repository_spec.rb
index 0f5e0bfc75c..95602a4de0e 100644
--- a/spec/models/snippet_repository_spec.rb
+++ b/spec/models/snippet_repository_spec.rb
@@ -108,6 +108,7 @@ RSpec.describe SnippetRepository do
before do
allow(snippet).to receive(:repository).and_return(repo)
allow(repo).to receive(:ls_files).and_return([])
+ allow(repo).to receive(:root_ref).and_return('master')
end
it 'infers the commit action based on the parameters if not present' do
@@ -197,7 +198,7 @@ RSpec.describe SnippetRepository do
shared_examples 'snippet repository with file names' do |*filenames|
it 'sets a name for unnamed files' do
- ls_files = snippet.repository.ls_files(nil)
+ ls_files = snippet.repository.ls_files(snippet.default_branch)
expect(ls_files).to include(*filenames)
end
end
@@ -306,6 +307,6 @@ RSpec.describe SnippetRepository do
end
def first_blob(snippet)
- snippet.repository.blob_at('master', snippet.repository.ls_files(nil).first)
+ snippet.repository.blob_at('master', snippet.repository.ls_files(snippet.default_branch).first)
end
end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 3f9c6981de1..ab614a6d45c 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -133,10 +133,10 @@ RSpec.describe Snippet do
end
describe '#file_name' do
- let(:project) { create(:project) }
+ let(:snippet) { build(:snippet, file_name: file_name) }
context 'file_name is nil' do
- let(:snippet) { create(:snippet, project: project, file_name: nil) }
+ let(:file_name) { nil }
it 'returns an empty string' do
expect(snippet.file_name).to eq ''
@@ -144,10 +144,10 @@ RSpec.describe Snippet do
end
context 'file_name is not nil' do
- let(:snippet) { create(:snippet, project: project, file_name: 'foo.txt') }
+ let(:file_name) { 'foo.txt' }
it 'returns the file_name' do
- expect(snippet.file_name).to eq 'foo.txt'
+ expect(snippet.file_name).to eq file_name
end
end
end
@@ -161,7 +161,7 @@ RSpec.describe Snippet do
end
describe '.search' do
- let(:snippet) { create(:snippet, title: 'test snippet', description: 'description') }
+ let_it_be(:snippet) { create(:snippet, title: 'test snippet', description: 'description') }
it 'returns snippets with a matching title' do
expect(described_class.search(snippet.title)).to eq([snippet])
@@ -219,25 +219,23 @@ RSpec.describe Snippet do
end
describe '.with_optional_visibility' do
+ let_it_be(:public_snippet) { create(:snippet, :public) }
+ let_it_be(:private_snippet) { create(:snippet, :private) }
+
context 'when a visibility level is provided' do
it 'returns snippets with the given visibility' do
- create(:snippet, :private)
-
- snippet = create(:snippet, :public)
snippets = described_class
.with_optional_visibility(Gitlab::VisibilityLevel::PUBLIC)
- expect(snippets).to eq([snippet])
+ expect(snippets).to eq([public_snippet])
end
end
context 'when a visibility level is not provided' do
it 'returns all snippets' do
- snippet1 = create(:snippet, :public)
- snippet2 = create(:snippet, :private)
snippets = described_class.with_optional_visibility
- expect(snippets).to include(snippet1, snippet2)
+ expect(snippets).to include(public_snippet, private_snippet)
end
end
end
@@ -254,12 +252,13 @@ RSpec.describe Snippet do
end
describe '.only_include_projects_visible_to' do
- let!(:project1) { create(:project, :public) }
- let!(:project2) { create(:project, :internal) }
- let!(:project3) { create(:project, :private) }
- let!(:snippet1) { create(:project_snippet, project: project1) }
- let!(:snippet2) { create(:project_snippet, project: project2) }
- let!(:snippet3) { create(:project_snippet, project: project3) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:project1) { create(:project_empty_repo, :public, namespace: author.namespace) }
+ let_it_be(:project2) { create(:project_empty_repo, :internal, namespace: author.namespace) }
+ let_it_be(:project3) { create(:project_empty_repo, :private, namespace: author.namespace) }
+ let_it_be(:snippet1) { create(:project_snippet, project: project1, author: author) }
+ let_it_be(:snippet2) { create(:project_snippet, project: project2, author: author) }
+ let_it_be(:snippet3) { create(:project_snippet, project: project3, author: author) }
context 'when a user is provided' do
it 'returns snippets visible to the user' do
@@ -283,55 +282,47 @@ RSpec.describe Snippet do
end
describe 'only_include_projects_with_snippets_enabled' do
- context 'when the include_private option is enabled' do
- it 'includes snippets for projects with snippets set to private' do
- project = create(:project)
-
- project.project_feature
- .update(snippets_access_level: ProjectFeature::PRIVATE)
+ let_it_be(:project, reload: true) { create(:project_empty_repo) }
+ let_it_be(:snippet) { create(:project_snippet, project: project) }
- snippet = create(:project_snippet, project: project)
+ let(:access_level) { ProjectFeature::ENABLED }
- snippets = described_class
- .only_include_projects_with_snippets_enabled(include_private: true)
-
- expect(snippets).to eq([snippet])
- end
+ before do
+ project.project_feature.update(snippets_access_level: access_level)
end
- context 'when the include_private option is not enabled' do
- it 'does not include snippets for projects that have snippets set to private' do
- project = create(:project)
+ it 'includes snippets for projects with snippets enabled' do
+ snippets = described_class.only_include_projects_with_snippets_enabled
- project.project_feature
- .update(snippets_access_level: ProjectFeature::PRIVATE)
+ expect(snippets).to eq([snippet])
+ end
- create(:project_snippet, project: project)
+ context 'when snippet_access_level is private' do
+ let(:access_level) { ProjectFeature::PRIVATE }
- snippets = described_class.only_include_projects_with_snippets_enabled
+ context 'when the include_private option is enabled' do
+ it 'includes snippets for projects with snippets set to private' do
+ snippets = described_class.only_include_projects_with_snippets_enabled(include_private: true)
- expect(snippets).to be_empty
+ expect(snippets).to eq([snippet])
+ end
end
- end
-
- it 'includes snippets for projects with snippets enabled' do
- project = create(:project)
- project.project_feature
- .update(snippets_access_level: ProjectFeature::ENABLED)
+ context 'when the include_private option is not enabled' do
+ it 'does not include snippets for projects that have snippets set to private' do
+ snippets = described_class.only_include_projects_with_snippets_enabled
- snippet = create(:project_snippet, project: project)
- snippets = described_class.only_include_projects_with_snippets_enabled
-
- expect(snippets).to eq([snippet])
+ expect(snippets).to be_empty
+ end
+ end
end
end
describe '.only_include_authorized_projects' do
it 'only includes snippets for projects the user is authorized to see' do
user = create(:user)
- project1 = create(:project, :private)
- project2 = create(:project, :private)
+ project1 = create(:project_empty_repo, :private)
+ project2 = create(:project_empty_repo, :private)
project1.team.add_developer(user)
@@ -345,43 +336,34 @@ RSpec.describe Snippet do
end
describe '.for_project_with_user' do
- context 'when a user is provided' do
- it 'returns an empty collection if the user can not view the snippets' do
- project = create(:project, :private)
- user = create(:user)
+ let_it_be(:public_project) { create(:project_empty_repo, :public) }
+ let_it_be(:private_project) { create(:project_empty_repo, :private) }
- project.project_feature
- .update(snippets_access_level: ProjectFeature::ENABLED)
+ context 'when a user is provided' do
+ let_it_be(:user) { create(:user) }
- create(:project_snippet, :public, project: project)
+ it 'returns an empty collection if the user can not view the snippets' do
+ create(:project_snippet, :public, project: private_project)
- expect(described_class.for_project_with_user(project, user)).to be_empty
+ expect(described_class.for_project_with_user(private_project, user)).to be_empty
end
it 'returns the snippets if the user is a member of the project' do
- project = create(:project, :private)
- user = create(:user)
- snippet = create(:project_snippet, project: project)
+ snippet = create(:project_snippet, project: private_project)
- project.team.add_developer(user)
+ private_project.team.add_developer(user)
- snippets = described_class.for_project_with_user(project, user)
+ snippets = described_class.for_project_with_user(private_project, user)
expect(snippets).to eq([snippet])
end
it 'returns public snippets for a public project the user is not a member of' do
- project = create(:project, :public)
-
- project.project_feature
- .update(snippets_access_level: ProjectFeature::ENABLED)
+ snippet = create(:project_snippet, :public, project: public_project)
- user = create(:user)
- snippet = create(:project_snippet, :public, project: project)
+ create(:project_snippet, :private, project: public_project)
- create(:project_snippet, :private, project: project)
-
- snippets = described_class.for_project_with_user(project, user)
+ snippets = described_class.for_project_with_user(public_project, user)
expect(snippets).to eq([snippet])
end
@@ -389,26 +371,17 @@ RSpec.describe Snippet do
context 'when a user is not provided' do
it 'returns an empty collection for a private project' do
- project = create(:project, :private)
-
- project.project_feature
- .update(snippets_access_level: ProjectFeature::ENABLED)
+ create(:project_snippet, :public, project: private_project)
- create(:project_snippet, :public, project: project)
-
- expect(described_class.for_project_with_user(project)).to be_empty
+ expect(described_class.for_project_with_user(private_project)).to be_empty
end
it 'returns public snippets for a public project' do
- project = create(:project, :public)
- snippet = create(:project_snippet, :public, project: project)
-
- project.project_feature
- .update(snippets_access_level: ProjectFeature::PUBLIC)
+ snippet = create(:project_snippet, :public, project: public_project)
- create(:project_snippet, :private, project: project)
+ create(:project_snippet, :private, project: public_project)
- snippets = described_class.for_project_with_user(project)
+ snippets = described_class.for_project_with_user(public_project)
expect(snippets).to eq([snippet])
end
@@ -430,34 +403,30 @@ RSpec.describe Snippet do
end
describe '#participants' do
- let(:project) { create(:project, :public) }
- let(:snippet) { create(:snippet, content: 'foo', project: project) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:snippet) { create(:snippet, content: 'foo', project: project) }
- let!(:note1) do
+ let_it_be(:note1) do
create(:note_on_project_snippet,
noteable: snippet,
project: project,
note: 'a')
end
- let!(:note2) do
+ let_it_be(:note2) do
create(:note_on_project_snippet,
noteable: snippet,
project: project,
note: 'b')
end
- it 'includes the snippet author' do
- expect(snippet.participants).to include(snippet.author)
- end
-
- it 'includes the note authors' do
- expect(snippet.participants).to include(note1.author, note2.author)
+ it 'includes the snippet author and note authors' do
+ expect(snippet.participants).to include(snippet.author, note1.author, note2.author)
end
end
describe '#check_for_spam' do
- let(:snippet) { create :snippet, visibility_level: visibility_level }
+ let(:snippet) { create(:snippet, visibility_level: visibility_level) }
subject do
snippet.assign_attributes(title: title)
@@ -500,7 +469,7 @@ RSpec.describe Snippet do
end
describe '#blob' do
- let(:snippet) { create(:snippet) }
+ let(:snippet) { build(:snippet) }
it 'returns a blob representing the snippet data' do
blob = snippet.blob
@@ -514,6 +483,14 @@ RSpec.describe Snippet do
describe '#blobs' do
let(:snippet) { create(:snippet) }
+ it 'returns a blob representing the snippet data' do
+ blob = snippet.blob
+
+ expect(blob).to be_a(Blob)
+ expect(blob.path).to eq(snippet.file_name)
+ expect(blob.data).to eq(snippet.content)
+ end
+
context 'when repository does not exist' do
it 'returns empty array' do
expect(snippet.blobs).to be_empty
@@ -527,14 +504,6 @@ RSpec.describe Snippet do
expect(snippet.blobs).to all(be_a(Blob))
end
end
-
- it 'returns a blob representing the snippet data' do
- blob = snippet.blob
-
- expect(blob).to be_a(Blob)
- expect(blob.path).to eq(snippet.file_name)
- expect(blob.data).to eq(snippet.content)
- end
end
describe '#to_json' do
@@ -554,7 +523,7 @@ RSpec.describe Snippet do
end
describe '#storage' do
- let(:snippet) { create(:snippet) }
+ let(:snippet) { build(:snippet, id: 1) }
it "stores snippet in #{Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX} dir" do
expect(snippet.storage.disk_path).to start_with Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX
@@ -775,6 +744,16 @@ RSpec.describe Snippet do
subject
end
+
+ context 'when ref is nil' do
+ let(:ref) { nil }
+
+ it 'lists files from the repository from the deafult_branch' do
+ expect(snippet.repository).to receive(:ls_files).with(snippet.default_branch)
+
+ subject
+ end
+ end
end
context 'when snippet does not have a repository' do
@@ -787,4 +766,26 @@ RSpec.describe Snippet do
end
end
end
+
+ describe '#multiple_files?' do
+ subject { snippet.multiple_files? }
+
+ context 'when snippet has multiple files' do
+ let(:snippet) { create(:snippet, :repository) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when snippet does not have multiple files' do
+ let(:snippet) { create(:snippet, :empty_repo) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when the snippet does not have a repository' do
+ let(:snippet) { build(:snippet) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/models/snippet_statistics_spec.rb b/spec/models/snippet_statistics_spec.rb
index ad25bd7b3be..8def6a0bbd4 100644
--- a/spec/models/snippet_statistics_spec.rb
+++ b/spec/models/snippet_statistics_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe SnippetStatistics do
subject { statistics.update_file_count }
it 'updates the count of files' do
- file_count = snippet_with_repo.repository.ls_files(nil).count
+ file_count = snippet_with_repo.repository.ls_files(snippet_with_repo.default_branch).count
subject
diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb
index 68bb86bfa49..01ae80a61d1 100644
--- a/spec/models/terraform/state_spec.rb
+++ b/spec/models/terraform/state_spec.rb
@@ -7,8 +7,9 @@ RSpec.describe Terraform::State do
let(:terraform_state_file) { fixture_file('terraform/terraform.tfstate') }
- it { is_expected.to belong_to(:project) }
+ it { is_expected.to be_a FileStoreMounter }
+ it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:locked_by_user).class_name('User') }
it { is_expected.to validate_presence_of(:project_id) }
@@ -23,14 +24,6 @@ RSpec.describe Terraform::State do
expect(subject.file.read).to eq(terraform_state_file)
end
end
-
- context 'when no file exists' do
- subject { create(:terraform_state) }
-
- it 'creates a default file' do
- expect(subject.file.read).to eq('{"version":1}')
- end
- end
end
describe '#file_store' do
@@ -56,4 +49,55 @@ RSpec.describe Terraform::State do
it_behaves_like 'mounted file in local store'
end
end
+
+ describe '#latest_file' do
+ subject { terraform_state.latest_file }
+
+ context 'versioning is enabled' do
+ let(:terraform_state) { create(:terraform_state, :with_version) }
+ let(:latest_version) { terraform_state.latest_version }
+
+ it { is_expected.to eq latest_version.file }
+
+ context 'but no version exists yet' do
+ let(:terraform_state) { create(:terraform_state) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'versioning is disabled' do
+ let(:terraform_state) { create(:terraform_state, :with_file) }
+
+ it { is_expected.to eq terraform_state.file }
+ end
+ end
+
+ describe '#update_file!' do
+ let(:version) { 2 }
+ let(:data) { Hash[terraform_version: '0.12.21'].to_json }
+
+ subject { terraform_state.update_file!(CarrierWaveStringFile.new(data), version: version) }
+
+ context 'versioning is enabled' do
+ let(:terraform_state) { create(:terraform_state) }
+
+ it 'creates a new version' do
+ expect { subject }.to change { Terraform::StateVersion.count }
+
+ expect(terraform_state.latest_version.version).to eq(version)
+ expect(terraform_state.latest_version.file.read).to eq(data)
+ end
+ end
+
+ context 'versioning is disabled' do
+ let(:terraform_state) { create(:terraform_state, :with_file) }
+
+ it 'modifies the existing state record' do
+ expect { subject }.not_to change { Terraform::StateVersion.count }
+
+ expect(terraform_state.latest_file.read).to eq(data)
+ end
+ end
+ end
end
diff --git a/spec/models/terraform/state_version_spec.rb b/spec/models/terraform/state_version_spec.rb
new file mode 100644
index 00000000000..72dd29e1571
--- /dev/null
+++ b/spec/models/terraform/state_version_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Terraform::StateVersion do
+ it { is_expected.to be_a FileStoreMounter }
+
+ it { is_expected.to belong_to(:terraform_state).required }
+ it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
+
+ describe 'scopes' do
+ describe '.ordered_by_version_desc' do
+ let(:terraform_state) { create(:terraform_state) }
+ let(:versions) { [4, 2, 5, 1, 3] }
+
+ subject { described_class.ordered_by_version_desc }
+
+ before do
+ versions.each do |version|
+ create(:terraform_state_version, terraform_state: terraform_state, version: version)
+ end
+ end
+
+ it { expect(subject.map(&:version)).to eq(versions.sort.reverse) }
+ end
+ end
+
+ context 'file storage' do
+ subject { create(:terraform_state_version) }
+
+ before do
+ stub_terraform_state_object_storage(Terraform::StateUploader)
+ end
+
+ describe '#file' do
+ let(:terraform_state_file) { fixture_file('terraform/terraform.tfstate') }
+
+ before do
+ subject.file = CarrierWaveStringFile.new(terraform_state_file)
+ subject.save!
+ end
+
+ it 'returns the saved file' do
+ expect(subject.file.read).to eq(terraform_state_file)
+ end
+ end
+
+ describe '#file_store' do
+ it 'returns the value' do
+ [ObjectStorage::Store::LOCAL, ObjectStorage::Store::REMOTE].each do |store|
+ subject.update!(file_store: store)
+
+ expect(subject.file_store).to eq(store)
+ end
+ end
+ end
+
+ 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
+ end
+
+ context 'when file is stored locally' do
+ before do
+ stub_terraform_state_object_storage(enabled: false)
+ end
+
+ it 'sets file_store to local' do
+ expect(subject.file_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/user_agent_detail_spec.rb b/spec/models/user_agent_detail_spec.rb
index e3f3d9c342b..ca81ef38ecc 100644
--- a/spec/models/user_agent_detail_spec.rb
+++ b/spec/models/user_agent_detail_spec.rb
@@ -18,8 +18,10 @@ RSpec.describe UserAgentDetail do
end
describe '.valid?' do
+ let(:issue) { create(:issue) }
+
it 'is valid with a subject' do
- detail = build(:user_agent_detail)
+ detail = build(:user_agent_detail, subject: issue)
expect(detail).to be_valid
end
diff --git a/spec/models/user_interacted_project_spec.rb b/spec/models/user_interacted_project_spec.rb
index 2fec8be76e8..aa038b06d8d 100644
--- a/spec/models/user_interacted_project_spec.rb
+++ b/spec/models/user_interacted_project_spec.rb
@@ -3,14 +3,17 @@
require 'spec_helper'
RSpec.describe UserInteractedProject do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:author) { project.creator }
+
describe '.track' do
subject { described_class.track(event) }
- let(:event) { build(:event) }
+ let(:event) { build(:event, project: project, author: author) }
Event.actions.each_key do |action|
context "for all actions (event types)" do
- let(:event) { build(:event, action: action) }
+ let(:event) { build(:event, project: project, author: author, action: action) }
it 'creates a record' do
expect { subject }.to change { described_class.count }.from(0).to(1)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index e9077ed4143..1841288cd4b 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -47,6 +47,9 @@ RSpec.describe User do
it { is_expected.to delegate_method(:sourcegraph_enabled).to(:user_preference) }
it { is_expected.to delegate_method(:sourcegraph_enabled=).to(:user_preference).with_arguments(:args) }
+ it { is_expected.to delegate_method(:gitpod_enabled).to(:user_preference) }
+ it { is_expected.to delegate_method(:gitpod_enabled=).to(:user_preference).with_arguments(:args) }
+
it { is_expected.to delegate_method(:setup_for_company).to(:user_preference) }
it { is_expected.to delegate_method(:setup_for_company=).to(:user_preference).with_arguments(:args) }
@@ -68,6 +71,7 @@ RSpec.describe User do
it { is_expected.to have_one(:namespace) }
it { is_expected.to have_one(:status) }
it { is_expected.to have_one(:user_detail) }
+ it { is_expected.to have_one(:atlassian_identity) }
it { is_expected.to have_one(:user_highest_role) }
it { is_expected.to have_many(:snippets).dependent(:destroy) }
it { is_expected.to have_many(:members) }
@@ -179,6 +183,58 @@ RSpec.describe User do
end.to have_enqueued_job.on_queue('mailers').exactly(:twice)
end
end
+
+ context 'emails sent on changing password' do
+ context 'when password is updated' do
+ context 'default behaviour' do
+ it 'enqueues the `password changed` email' do
+ user.password = User.random_password
+
+ expect { user.save! }.to have_enqueued_mail(DeviseMailer, :password_change)
+ end
+
+ it 'does not enqueue the `admin changed your password` email' do
+ user.password = User.random_password
+
+ expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_change_by_admin)
+ end
+ end
+
+ context '`admin changed your password` email' do
+ it 'is enqueued only when explicitly allowed' do
+ user.password = User.random_password
+ user.send_only_admin_changed_your_password_notification!
+
+ expect { user.save! }.to have_enqueued_mail(DeviseMailer, :password_change_by_admin)
+ end
+
+ it '`password changed` email is not enqueued if it is explicitly allowed' do
+ user.password = User.random_password
+ user.send_only_admin_changed_your_password_notification!
+
+ expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_changed)
+ end
+
+ it 'is not enqueued if sending notifications on password updates is turned off as per Devise config' do
+ user.password = User.random_password
+ user.send_only_admin_changed_your_password_notification!
+
+ allow(Devise).to receive(:send_password_change_notification).and_return(false)
+
+ expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_change_by_admin)
+ end
+ end
+ end
+
+ context 'when password is not updated' do
+ it 'does not enqueue the `admin changed your password` email even if explicitly allowed' do
+ user.name = 'John'
+ user.send_only_admin_changed_your_password_notification!
+
+ expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_change_by_admin)
+ end
+ end
+ end
end
describe 'validations' do
@@ -659,30 +715,40 @@ RSpec.describe User do
expect(users_with_two_factor).not_to include(user_without_2fa.id)
end
- it "returns users with 2fa enabled via U2F" do
- user_with_2fa = create(:user, :two_factor_via_u2f)
- user_without_2fa = create(:user)
- users_with_two_factor = described_class.with_two_factor.pluck(:id)
+ shared_examples "returns the right users" do |trait|
+ it "returns users with 2fa enabled via hardware token" do
+ user_with_2fa = create(:user, trait)
+ user_without_2fa = create(:user)
+ users_with_two_factor = described_class.with_two_factor.pluck(:id)
- expect(users_with_two_factor).to include(user_with_2fa.id)
- expect(users_with_two_factor).not_to include(user_without_2fa.id)
- end
+ expect(users_with_two_factor).to include(user_with_2fa.id)
+ expect(users_with_two_factor).not_to include(user_without_2fa.id)
+ end
- it "returns users with 2fa enabled via OTP and U2F" do
- user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
- user_without_2fa = create(:user)
- users_with_two_factor = described_class.with_two_factor.pluck(:id)
+ it "returns users with 2fa enabled via OTP and hardware token" do
+ user_with_2fa = create(:user, :two_factor_via_otp, trait)
+ user_without_2fa = create(:user)
+ users_with_two_factor = described_class.with_two_factor.pluck(:id)
- expect(users_with_two_factor).to eq([user_with_2fa.id])
- expect(users_with_two_factor).not_to include(user_without_2fa.id)
+ expect(users_with_two_factor).to eq([user_with_2fa.id])
+ expect(users_with_two_factor).not_to include(user_without_2fa.id)
+ end
+
+ it 'works with ORDER BY' do
+ user_with_2fa = create(:user, :two_factor_via_otp, trait)
+
+ expect(described_class
+ .with_two_factor
+ .reorder_by_name).to eq([user_with_2fa])
+ end
end
- it 'works with ORDER BY' do
- user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
+ describe "and U2F" do
+ it_behaves_like "returns the right users", :two_factor_via_u2f
+ end
- expect(described_class
- .with_two_factor
- .reorder_by_name).to eq([user_with_2fa])
+ describe "and WebAuthn" do
+ it_behaves_like "returns the right users", :two_factor_via_webauthn
end
end
@@ -696,22 +762,44 @@ RSpec.describe User do
expect(users_without_two_factor).not_to include(user_with_2fa.id)
end
- it "excludes users with 2fa enabled via U2F" do
- user_with_2fa = create(:user, :two_factor_via_u2f)
- user_without_2fa = create(:user)
- users_without_two_factor = described_class.without_two_factor.pluck(:id)
+ describe "and u2f" do
+ it "excludes users with 2fa enabled via U2F" do
+ user_with_2fa = create(:user, :two_factor_via_u2f)
+ user_without_2fa = create(:user)
+ users_without_two_factor = described_class.without_two_factor.pluck(:id)
- expect(users_without_two_factor).to include(user_without_2fa.id)
- expect(users_without_two_factor).not_to include(user_with_2fa.id)
+ expect(users_without_two_factor).to include(user_without_2fa.id)
+ expect(users_without_two_factor).not_to include(user_with_2fa.id)
+ end
+
+ it "excludes users with 2fa enabled via OTP and U2F" do
+ user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
+ user_without_2fa = create(:user)
+ users_without_two_factor = described_class.without_two_factor.pluck(:id)
+
+ expect(users_without_two_factor).to include(user_without_2fa.id)
+ expect(users_without_two_factor).not_to include(user_with_2fa.id)
+ end
end
- it "excludes users with 2fa enabled via OTP and U2F" do
- user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
- user_without_2fa = create(:user)
- users_without_two_factor = described_class.without_two_factor.pluck(:id)
+ describe "and webauthn" do
+ it "excludes users with 2fa enabled via WebAuthn" do
+ user_with_2fa = create(:user, :two_factor_via_webauthn)
+ user_without_2fa = create(:user)
+ users_without_two_factor = described_class.without_two_factor.pluck(:id)
- expect(users_without_two_factor).to include(user_without_2fa.id)
- expect(users_without_two_factor).not_to include(user_with_2fa.id)
+ expect(users_without_two_factor).to include(user_without_2fa.id)
+ expect(users_without_two_factor).not_to include(user_with_2fa.id)
+ end
+
+ it "excludes users with 2fa enabled via OTP and WebAuthn" do
+ user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_webauthn)
+ user_without_2fa = create(:user)
+ users_without_two_factor = described_class.without_two_factor.pluck(:id)
+
+ expect(users_without_two_factor).to include(user_without_2fa.id)
+ expect(users_without_two_factor).not_to include(user_with_2fa.id)
+ end
end
end
@@ -1662,7 +1750,7 @@ RSpec.describe User do
# add user to project
project.add_maintainer(user)
- # create invite to projet
+ # create invite to project
create(:project_member, :developer, project: project, invite_token: '1234', invite_email: 'inviteduser1@example.com')
# create request to join project
@@ -4490,6 +4578,44 @@ RSpec.describe User do
end
end
+ describe '#notification_settings_for_groups' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:groups) { create_list(:group, 2) }
+
+ subject { user.notification_settings_for_groups(arg) }
+
+ before do
+ groups.each do |group|
+ group.add_maintainer(user)
+ end
+ end
+
+ shared_examples_for 'notification_settings_for_groups method' do
+ it 'returns NotificationSetting objects for provided groups', :aggregate_failures do
+ expect(subject.count).to eq(groups.count)
+ expect(subject.map(&:source_id)).to match_array(groups.map(&:id))
+ end
+ end
+
+ context 'when given an ActiveRecord relationship' do
+ let_it_be(:arg) { Group.where(id: groups.map(&:id)) }
+
+ it_behaves_like 'notification_settings_for_groups method'
+
+ it 'uses #select to maintain lazy querying behavior' do
+ expect(arg).to receive(:select).and_call_original
+
+ subject
+ end
+ end
+
+ context 'when given an Array of Groups' do
+ let_it_be(:arg) { groups }
+
+ it_behaves_like 'notification_settings_for_groups method'
+ end
+ end
+
describe '#notification_email_for' do
let(:user) { create(:user) }
let(:group) { create(:group) }
diff --git a/spec/models/x509_certificate_spec.rb b/spec/models/x509_certificate_spec.rb
index 880c5014a84..d3b4470d3f4 100644
--- a/spec/models/x509_certificate_spec.rb
+++ b/spec/models/x509_certificate_spec.rb
@@ -68,6 +68,8 @@ RSpec.describe X509Certificate do
end
describe 'validators' do
+ let_it_be(:issuer) { create(:x509_issuer) }
+
it 'accepts correct subject_key_identifier' do
subject_key_identifiers = [
'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB',
@@ -75,7 +77,7 @@ RSpec.describe X509Certificate do
]
subject_key_identifiers.each do |identifier|
- expect(build(:x509_certificate, subject_key_identifier: identifier)).to be_valid
+ expect(build(:x509_certificate, x509_issuer: issuer, subject_key_identifier: identifier)).to be_valid
end
end
@@ -88,7 +90,7 @@ RSpec.describe X509Certificate do
]
subject_key_identifiers.each do |identifier|
- expect(build(:x509_certificate, subject_key_identifier: identifier)).to be_invalid
+ expect(build(:x509_certificate, x509_issuer: issuer, subject_key_identifier: identifier)).to be_invalid
end
end
@@ -99,7 +101,7 @@ RSpec.describe X509Certificate do
]
emails.each do |email|
- expect(build(:x509_certificate, email: email)).to be_valid
+ expect(build(:x509_certificate, x509_issuer: issuer, email: email)).to be_valid
end
end
@@ -110,20 +112,20 @@ RSpec.describe X509Certificate do
]
emails.each do |email|
- expect(build(:x509_certificate, email: email)).to be_invalid
+ expect(build(:x509_certificate, x509_issuer: issuer, email: email)).to be_invalid
end
end
it 'accepts valid serial_number' do
- expect(build(:x509_certificate, serial_number: 123412341234)).to be_valid
+ expect(build(:x509_certificate, x509_issuer: issuer, serial_number: 123412341234)).to be_valid
# rfc 5280 - 4.1.2.2 Serial number (20 octets is the maximum)
- expect(build(:x509_certificate, serial_number: 1461501637330902918203684832716283019655932542975)).to be_valid
- expect(build(:x509_certificate, serial_number: 'ffffffffffffffffffffffffffffffffffffffff'.to_i(16))).to be_valid
+ expect(build(:x509_certificate, x509_issuer: issuer, serial_number: 1461501637330902918203684832716283019655932542975)).to be_valid
+ expect(build(:x509_certificate, x509_issuer: issuer, serial_number: 'ffffffffffffffffffffffffffffffffffffffff'.to_i(16))).to be_valid
end
it 'rejects invalid serial_number' do
- expect(build(:x509_certificate, serial_number: "sgsgfsdgdsfg")).to be_invalid
+ expect(build(:x509_certificate, x509_issuer: issuer, serial_number: "sgsgfsdgdsfg")).to be_invalid
end
end
end