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