From 85dc423f7090da0a52c73eb66faf22ddb20efff9 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Sat, 19 Sep 2020 01:45:44 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-4-stable-ee --- spec/models/alert_management/alert_spec.rb | 72 ++- .../instance_statistics/measurement_spec.rb | 45 ++ spec/models/application_record_spec.rb | 68 +-- spec/models/application_setting_spec.rb | 29 +- spec/models/atlassian/identity_spec.rb | 34 ++ spec/models/audit_event_partitioned_spec.rb | 13 +- spec/models/authentication_event_spec.rb | 15 + spec/models/badges/project_badge_spec.rb | 4 +- .../blob_viewer/metrics_dashboard_yml_spec.rb | 253 +++++++--- spec/models/board_group_recent_visit_spec.rb | 2 +- spec/models/board_project_recent_visit_spec.rb | 2 +- spec/models/ci/bridge_spec.rb | 1 + spec/models/ci/build_metadata_spec.rb | 2 +- spec/models/ci/build_spec.rb | 145 ++++-- spec/models/ci/build_trace_chunk_spec.rb | 123 ++++- spec/models/ci/build_trace_chunks/fog_spec.rb | 12 +- spec/models/ci/instance_variable_spec.rb | 6 +- spec/models/ci/job_artifact_spec.rb | 40 +- spec/models/ci/legacy_stage_spec.rb | 2 +- spec/models/ci/persistent_ref_spec.rb | 8 +- spec/models/ci/pipeline_artifact_spec.rb | 78 ++- spec/models/ci/pipeline_spec.rb | 473 +++++++++++++++--- spec/models/ci/ref_spec.rb | 72 ++- spec/models/ci/runner_spec.rb | 6 +- spec/models/ci_platform_metric_spec.rb | 94 ++++ spec/models/clusters/agent_spec.rb | 11 + .../clusters/applications/prometheus_spec.rb | 17 +- spec/models/clusters/cluster_spec.rb | 1 + spec/models/clusters/kubernetes_namespace_spec.rb | 3 +- spec/models/commit_range_spec.rb | 23 +- spec/models/commit_status_spec.rb | 18 +- spec/models/concerns/ci/artifactable_spec.rb | 36 ++ spec/models/concerns/from_except_spec.rb | 7 + spec/models/concerns/from_intersect_spec.rb | 7 + spec/models/concerns/from_set_operator_spec.rb | 20 + spec/models/concerns/from_union_spec.rb | 35 +- spec/models/concerns/id_in_ordered_spec.rb | 33 ++ spec/models/concerns/issuable_spec.rb | 178 ++++++- spec/models/concerns/milestoneable_spec.rb | 8 + spec/models/concerns/prometheus_adapter_spec.rb | 10 +- spec/models/cycle_analytics/issue_spec.rb | 8 +- spec/models/cycle_analytics/plan_spec.rb | 6 +- spec/models/cycle_analytics/production_spec.rb | 53 --- spec/models/deployment_spec.rb | 18 +- .../design_management/design_collection_spec.rb | 60 ++- spec/models/design_management/design_spec.rb | 26 +- spec/models/dev_ops_report/metric_spec.rb | 13 + spec/models/dev_ops_score/metric_spec.rb | 13 - spec/models/diff_note_spec.rb | 4 +- spec/models/draft_note_spec.rb | 8 +- spec/models/environment_spec.rb | 8 +- spec/models/environment_status_spec.rb | 20 +- spec/models/event_spec.rb | 16 +- spec/models/group_deploy_key_spec.rb | 21 + spec/models/group_spec.rb | 15 + spec/models/issuable_severity_spec.rb | 25 + spec/models/issue_link_spec.rb | 53 +++ spec/models/issue_spec.rb | 78 ++- spec/models/iteration_spec.rb | 53 +++ spec/models/jira_connect_installation_spec.rb | 45 ++ spec/models/jira_connect_subscription_spec.rb | 15 + spec/models/member_spec.rb | 70 ++- spec/models/merge_request/metrics_spec.rb | 11 - spec/models/merge_request_diff_commit_spec.rb | 7 +- spec/models/merge_request_diff_file_spec.rb | 15 +- spec/models/merge_request_diff_spec.rb | 36 +- spec/models/merge_request_reviewer_spec.rb | 14 + spec/models/merge_request_spec.rb | 382 +++++++++++---- spec/models/metrics/dashboard/annotation_spec.rb | 2 +- spec/models/milestone_spec.rb | 10 +- .../namespace/root_storage_statistics_spec.rb | 3 + spec/models/namespace_spec.rb | 187 +++++++- spec/models/note_spec.rb | 100 +++- spec/models/operations/feature_flag_scope_spec.rb | 391 +++++++++++++++ spec/models/operations/feature_flag_spec.rb | 258 ++++++++++ .../operations/feature_flags/strategy_spec.rb | 323 +++++++++++++ .../operations/feature_flags/user_list_spec.rb | 102 ++++ .../models/operations/feature_flags_client_spec.rb | 21 + spec/models/packages/package_file_spec.rb | 6 +- spec/models/packages/package_spec.rb | 118 ++++- spec/models/pages/lookup_path_spec.rb | 61 ++- spec/models/pages_deployment_spec.rb | 21 + spec/models/pages_domain_spec.rb | 10 - .../prometheus_dashboard_spec.rb | 93 +++- spec/models/product_analytics_event_spec.rb | 25 + spec/models/project_feature_usage_spec.rb | 59 +++ .../models/project_services/bamboo_service_spec.rb | 10 +- .../project_services/buildkite_service_spec.rb | 2 +- .../chat_message/merge_message_spec.rb | 69 ++- spec/models/project_services/ewm_service_spec.rb | 61 +++ spec/models/project_services/jira_service_spec.rb | 147 ++++-- .../project_services/packagist_service_spec.rb | 30 +- .../pipelines_email_service_spec.rb | 22 +- .../project_services/teamcity_service_spec.rb | 10 +- spec/models/project_spec.rb | 526 +++++++++++---------- spec/models/project_statistics_spec.rb | 19 +- spec/models/project_team_spec.rb | 95 +++- spec/models/project_wiki_spec.rb | 13 +- spec/models/remote_mirror_spec.rb | 24 +- spec/models/repository_spec.rb | 1 + spec/models/resource_iteration_event_spec.rb | 17 - spec/models/resource_label_event_spec.rb | 8 +- spec/models/resource_state_event_spec.rb | 28 ++ spec/models/service_spec.rb | 148 ++++-- spec/models/snippet_input_action_spec.rb | 6 +- spec/models/snippet_repository_spec.rb | 5 +- spec/models/snippet_spec.rb | 215 ++++----- spec/models/snippet_statistics_spec.rb | 2 +- spec/models/terraform/state_spec.rb | 62 ++- spec/models/terraform/state_version_spec.rb | 76 +++ spec/models/user_agent_detail_spec.rb | 4 +- spec/models/user_interacted_project_spec.rb | 7 +- spec/models/user_spec.rb | 188 ++++++-- spec/models/x509_certificate_spec.rb | 18 +- 114 files changed, 5355 insertions(+), 1347 deletions(-) create mode 100644 spec/models/analytics/instance_statistics/measurement_spec.rb create mode 100644 spec/models/atlassian/identity_spec.rb create mode 100644 spec/models/authentication_event_spec.rb create mode 100644 spec/models/ci_platform_metric_spec.rb create mode 100644 spec/models/concerns/from_except_spec.rb create mode 100644 spec/models/concerns/from_intersect_spec.rb create mode 100644 spec/models/concerns/from_set_operator_spec.rb create mode 100644 spec/models/concerns/id_in_ordered_spec.rb delete mode 100644 spec/models/cycle_analytics/production_spec.rb create mode 100644 spec/models/dev_ops_report/metric_spec.rb delete mode 100644 spec/models/dev_ops_score/metric_spec.rb create mode 100644 spec/models/issuable_severity_spec.rb create mode 100644 spec/models/issue_link_spec.rb create mode 100644 spec/models/jira_connect_installation_spec.rb create mode 100644 spec/models/jira_connect_subscription_spec.rb create mode 100644 spec/models/merge_request_reviewer_spec.rb create mode 100644 spec/models/operations/feature_flag_scope_spec.rb create mode 100644 spec/models/operations/feature_flag_spec.rb create mode 100644 spec/models/operations/feature_flags/strategy_spec.rb create mode 100644 spec/models/operations/feature_flags/user_list_spec.rb create mode 100644 spec/models/operations/feature_flags_client_spec.rb create mode 100644 spec/models/pages_deployment_spec.rb create mode 100644 spec/models/project_feature_usage_spec.rb create mode 100644 spec/models/project_services/ewm_service_spec.rb delete mode 100644 spec/models/resource_iteration_event_spec.rb create mode 100644 spec/models/terraform/state_version_spec.rb (limited to 'spec/models') diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb index f937a879400..eb9dcca842d 100644 --- a/spec/models/alert_management/alert_spec.rb +++ b/spec/models/alert_management/alert_spec.rb @@ -332,35 +332,39 @@ RSpec.describe AlertManagement::Alert do end end - describe '#details' do - let(:payload) do - { - 'title' => 'Details title', - 'custom' => { - 'alert' => { - 'fields' => %w[one two] - } - }, - 'yet' => { - 'another' => 'field' - } - } - end - - let(:alert) { build(:alert_management_alert, project: project, title: 'Details title', payload: payload) } - - subject { alert.details } - - it 'renders the payload as inline hash' do - is_expected.to eq( - 'custom.alert.fields' => %w[one two], - 'yet.another' => 'field' - ) + describe '.reference_pattern' do + subject { described_class.reference_pattern } + + it { is_expected.to match('gitlab-org/gitlab^alert#123') } + end + + describe '.link_reference_pattern' do + subject { described_class.link_reference_pattern } + + it { is_expected.to match(triggered_alert.details_url) } + it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab/alert_management/123") } + it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab/issues/123") } + it { is_expected.not_to match("gitlab-org/gitlab/-/alert_management/123") } + end + + describe '.reference_valid?' do + using RSpec::Parameterized::TableSyntax + + where(:ref, :result) do + '123456' | true + '1' | true + '-1' | false + nil | false + '123456891012345678901234567890' | false + end + + with_them do + it { expect(described_class.reference_valid?(ref)).to eq(result) } end end describe '#to_reference' do - it { expect(triggered_alert.to_reference).to eq('') } + it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") } end describe '#trigger' do @@ -449,22 +453,4 @@ RSpec.describe AlertManagement::Alert do expect { subject }.to change { alert.events }.by(1) end end - - describe '#present' do - context 'when alert is generic' do - let(:alert) { triggered_alert } - - it 'uses generic alert presenter' do - expect(alert.present).to be_kind_of(AlertManagement::AlertPresenter) - end - end - - context 'when alert is Prometheus specific' do - let(:alert) { build(:alert_management_alert, :prometheus, project: project) } - - it 'uses Prometheus Alert presenter' do - expect(alert.present).to be_kind_of(AlertManagement::PrometheusAlertPresenter) - end - end - end end diff --git a/spec/models/analytics/instance_statistics/measurement_spec.rb b/spec/models/analytics/instance_statistics/measurement_spec.rb new file mode 100644 index 00000000000..4df847ea524 --- /dev/null +++ b/spec/models/analytics/instance_statistics/measurement_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Analytics::InstanceStatistics::Measurement, type: :model do + describe 'validation' do + let!(:measurement) { create(:instance_statistics_measurement) } + + it { is_expected.to validate_presence_of(:recorded_at) } + it { is_expected.to validate_presence_of(:identifier) } + it { is_expected.to validate_presence_of(:count) } + it { is_expected.to validate_uniqueness_of(:recorded_at).scoped_to(:identifier) } + end + + describe 'identifiers enum' do + it 'maps to the correct values' do + expect(described_class.identifiers).to eq({ + projects: 1, + users: 2, + issues: 3, + merge_requests: 4, + groups: 5, + pipelines: 6 + }.with_indifferent_access) + end + end + + describe 'scopes' do + let_it_be(:measurement_1) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago) } + let_it_be(:measurement_2) { create(:instance_statistics_measurement, :project_count, recorded_at: 2.days.ago) } + let_it_be(:measurement_3) { create(:instance_statistics_measurement, :group_count, recorded_at: 5.days.ago) } + + describe '.order_by_latest' do + subject { described_class.order_by_latest } + + it { is_expected.to eq([measurement_2, measurement_3, measurement_1]) } + end + + describe '.with_identifier' do + subject { described_class.with_identifier(:projects) } + + it { is_expected.to match_array([measurement_1, measurement_2]) } + end + end +end diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb index d9ab326505b..5ea1907543a 100644 --- a/spec/models/application_record_spec.rb +++ b/spec/models/application_record_spec.rb @@ -30,49 +30,51 @@ RSpec.describe ApplicationRecord do end end - describe '.safe_find_or_create_by' do - it 'creates the user avoiding race conditions' do - expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique) - allow(Suggestion).to receive(:find_or_create_by).and_call_original + context 'safe find or create methods' do + let_it_be(:note) { create(:diff_note_on_merge_request) } - expect { Suggestion.safe_find_or_create_by(build(:suggestion).attributes) } - .to change { Suggestion.count }.by(1) - end + let(:suggestion_attributes) { attributes_for(:suggestion).merge!(note_id: note.id) } - it 'passes a block to find_or_create_by' do - attributes = build(:suggestion).attributes + describe '.safe_find_or_create_by' do + it 'creates the suggestion avoiding race conditions' do + expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique) + allow(Suggestion).to receive(:find_or_create_by).and_call_original - expect do |block| - Suggestion.safe_find_or_create_by(attributes, &block) - end.to yield_with_args(an_object_having_attributes(attributes)) - end + expect { Suggestion.safe_find_or_create_by(suggestion_attributes) } + .to change { Suggestion.count }.by(1) + end - it 'does not create a record when is not valid' do - raw_usage_data = RawUsageData.safe_find_or_create_by({ recorded_at: nil }) + it 'passes a block to find_or_create_by' do + expect do |block| + Suggestion.safe_find_or_create_by(suggestion_attributes, &block) + end.to yield_with_args(an_object_having_attributes(suggestion_attributes)) + end - expect(raw_usage_data.id).to be_nil - expect(raw_usage_data).not_to be_valid - end - end + it 'does not create a record when is not valid' do + raw_usage_data = RawUsageData.safe_find_or_create_by({ recorded_at: nil }) - describe '.safe_find_or_create_by!' do - it 'creates a record using safe_find_or_create_by' do - expect(Suggestion).to receive(:find_or_create_by).and_call_original - - expect(Suggestion.safe_find_or_create_by!(build(:suggestion).attributes)) - .to be_a(Suggestion) + expect(raw_usage_data.id).to be_nil + expect(raw_usage_data).not_to be_valid + end end - it 'raises a validation error if the record was not persisted' do - expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid) - end + describe '.safe_find_or_create_by!' do + it 'creates a record using safe_find_or_create_by' do + expect(Suggestion).to receive(:find_or_create_by).and_call_original + + expect(Suggestion.safe_find_or_create_by!(suggestion_attributes)) + .to be_a(Suggestion) + end - it 'passes a block to find_or_create_by' do - attributes = build(:suggestion).attributes + it 'raises a validation error if the record was not persisted' do + expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid) + end - expect do |block| - Suggestion.safe_find_or_create_by!(attributes, &block) - end.to yield_with_args(an_object_having_attributes(attributes)) + it 'passes a block to find_or_create_by' do + expect do |block| + Suggestion.safe_find_or_create_by!(suggestion_attributes, &block) + end.to yield_with_args(an_object_having_attributes(suggestion_attributes)) + end end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index ab25608e2f0..9f76fb3330d 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -71,6 +71,8 @@ RSpec.describe ApplicationSetting do it { is_expected.not_to allow_value('three').for(:push_event_activities_limit) } it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) } + it { is_expected.to validate_numericality_of(:container_registry_delete_tags_service_timeout).only_integer.is_greater_than_or_equal_to(0) } + it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) } it { is_expected.to validate_numericality_of(:wiki_page_max_content_bytes).only_integer.is_greater_than_or_equal_to(1024) } it { is_expected.to validate_presence_of(:max_artifacts_size) } @@ -189,14 +191,10 @@ RSpec.describe ApplicationSetting do it { is_expected.not_to allow_value(nil).for(:snowplow_collector_hostname) } it { is_expected.to allow_value("snowplow.gitlab.com").for(:snowplow_collector_hostname) } it { is_expected.not_to allow_value('/example').for(:snowplow_collector_hostname) } - it { is_expected.to allow_value('https://example.org').for(:snowplow_iglu_registry_url) } - it { is_expected.not_to allow_value('not-a-url').for(:snowplow_iglu_registry_url) } - it { is_expected.to allow_value(nil).for(:snowplow_iglu_registry_url) } end context 'when snowplow is not enabled' do it { is_expected.to allow_value(nil).for(:snowplow_collector_hostname) } - it { is_expected.to allow_value(nil).for(:snowplow_iglu_registry_url) } end context "when user accepted let's encrypt terms of service" do @@ -640,6 +638,29 @@ RSpec.describe ApplicationSetting do is_expected.to be_invalid end end + + context 'gitpod settings' do + it 'is invalid if gitpod is enabled and no url is provided' do + allow(subject).to receive(:gitpod_enabled).and_return(true) + allow(subject).to receive(:gitpod_url).and_return(nil) + + is_expected.to be_invalid + end + + it 'is invalid if gitpod is enabled and an empty url is provided' do + allow(subject).to receive(:gitpod_enabled).and_return(true) + allow(subject).to receive(:gitpod_url).and_return('') + + is_expected.to be_invalid + end + + it 'is invalid if gitpod is enabled and an invalid url is provided' do + allow(subject).to receive(:gitpod_enabled).and_return(true) + allow(subject).to receive(:gitpod_url).and_return('javascript:alert("test")//') + + is_expected.to be_invalid + end + end end context 'restrict creating duplicates' do diff --git a/spec/models/atlassian/identity_spec.rb b/spec/models/atlassian/identity_spec.rb new file mode 100644 index 00000000000..a1dfe5b0e51 --- /dev/null +++ b/spec/models/atlassian/identity_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Atlassian::Identity do + describe 'associations' do + it { is_expected.to belong_to(:user) } + end + + describe 'validations' do + subject { create(:atlassian_identity) } + + it { is_expected.to validate_presence_of(:extern_uid) } + it { is_expected.to validate_uniqueness_of(:extern_uid) } + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_uniqueness_of(:user) } + end + + describe 'encrypted tokens' do + let(:token) { SecureRandom.alphanumeric(1254) } + let(:refresh_token) { SecureRandom.alphanumeric(45) } + let(:identity) { create(:atlassian_identity, token: token, refresh_token: refresh_token) } + + it 'saves the encrypted token, refresh token and corresponding ivs' do + expect(identity.encrypted_token).not_to be_nil + expect(identity.encrypted_token_iv).not_to be_nil + expect(identity.encrypted_refresh_token).not_to be_nil + expect(identity.encrypted_refresh_token_iv).not_to be_nil + + expect(identity.token).to eq(token) + expect(identity.refresh_token).to eq(refresh_token) + end + end +end diff --git a/spec/models/audit_event_partitioned_spec.rb b/spec/models/audit_event_partitioned_spec.rb index fe69f0083b7..ab48e291f78 100644 --- a/spec/models/audit_event_partitioned_spec.rb +++ b/spec/models/audit_event_partitioned_spec.rb @@ -7,7 +7,10 @@ RSpec.describe AuditEventPartitioned do let(:partitioned_table) { described_class } it 'has the same columns as the source table' do - expect(partitioned_table.column_names).to match_array(source_table.column_names) + column_names_from_source_table = column_names(source_table) + column_names_from_partioned_table = column_names(partitioned_table) + + expect(column_names_from_partioned_table).to match_array(column_names_from_source_table) end it 'has the same null constraints as the source table' do @@ -30,6 +33,14 @@ RSpec.describe AuditEventPartitioned do expect(event_from_partitioned_table).to eq(event_from_source_table) end + def column_names(table) + table.connection.select_all(<<~SQL) + SELECT c.column_name + FROM information_schema.columns c + WHERE c.table_name = '#{table.table_name}' + SQL + end + def null_constraints(table) table.connection.select_all(<<~SQL) SELECT c.column_name, c.is_nullable diff --git a/spec/models/authentication_event_spec.rb b/spec/models/authentication_event_spec.rb new file mode 100644 index 00000000000..56b0111f2c7 --- /dev/null +++ b/spec/models/authentication_event_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AuthenticationEvent do + describe 'associations' do + it { is_expected.to belong_to(:user).optional } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:provider) } + it { is_expected.to validate_presence_of(:user_name) } + it { is_expected.to validate_presence_of(:result) } + end +end diff --git a/spec/models/badges/project_badge_spec.rb b/spec/models/badges/project_badge_spec.rb index 9b9836129a6..78bfd4e18c0 100644 --- a/spec/models/badges/project_badge_spec.rb +++ b/spec/models/badges/project_badge_spec.rb @@ -27,8 +27,8 @@ RSpec.describe ProjectBadge do end context 'methods' do - let(:badge) { build(:project_badge, link_url: placeholder_url, image_url: placeholder_url) } - let!(:project) { badge.project } + let(:badge) { build_stubbed(:project_badge, link_url: placeholder_url, image_url: placeholder_url) } + let(:project) { badge.project } describe '#rendered_link_url' do let(:method) { :link_url } diff --git a/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb index 057f0f32158..84dfc5186a8 100644 --- a/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb +++ b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb @@ -9,119 +9,228 @@ RSpec.describe BlobViewer::MetricsDashboardYml do let_it_be(:project) { create(:project, :repository) } let(:blob) { fake_blob(path: '.gitlab/dashboards/custom-dashboard.yml', data: data) } let(:sha) { sample_commit.id } + let(:data) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') } subject(:viewer) { described_class.new(blob) } - context 'when the definition is valid' do - let(:data) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) } + context 'with metrics_dashboard_exhaustive_validations feature flag on' do + before do + stub_feature_flags(metrics_dashboard_exhaustive_validations: true) + end + + context 'when the definition is valid' do + before do + allow(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).and_return([]) + end + + describe '#valid?' do + it 'calls prepare! on the viewer' do + expect(viewer).to receive(:prepare!) + + viewer.valid? + end + + it 'processes dashboard yaml and returns true', :aggregate_failures do + yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw! + + expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader| + expect(loader).to receive(:load_raw!).and_call_original + end + expect(Gitlab::Metrics::Dashboard::Validator) + .to receive(:errors) + .with(yml, dashboard_path: '.gitlab/dashboards/custom-dashboard.yml', project: project) + .and_call_original + expect(viewer.valid?).to be true + end + end + + describe '#errors' do + it 'returns empty array' do + expect(viewer.errors).to eq [] + end + end + end + + context 'when definition is invalid' do + let(:error) { ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new } + let(:data) do + <<~YAML + dashboard: + YAML + end + + before do + allow(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).and_return([error]) + end - describe '#valid?' do - it 'calls prepare! on the viewer' do - allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json) + describe '#valid?' do + it 'returns false' do + expect(viewer.valid?).to be false + end + end - expect(viewer).to receive(:prepare!) + describe '#errors' do + it 'returns validation errors' do + expect(viewer.errors).to eq ["Dashboard failed schema validation"] + end + end + end - viewer.valid? + context 'when YAML syntax is invalid' do + let(:data) do + <<~YAML + dashboard: 'empty metrics' + panel_groups: + - group: 'Group Title' + YAML end - it 'returns true', :aggregate_failures do - yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw! + describe '#valid?' do + it 'returns false' do + expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors) + expect(viewer.valid?).to be false + end + end - expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader| - expect(loader).to receive(:load_raw!).and_call_original + describe '#errors' do + it 'returns validation errors' do + expect(viewer.errors).to all be_kind_of String end - expect(PerformanceMonitoring::PrometheusDashboard) - .to receive(:from_json) - .with(yml) - .and_call_original - expect(viewer.valid?).to be_truthy end end - describe '#errors' do - it 'returns nil' do - allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json) + context 'when YAML loader raises error' do + let(:data) do + <<~YAML + large yaml file + YAML + end + + before do + allow(::Gitlab::Config::Loader::Yaml).to receive(:new) + .and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big') + end - expect(viewer.errors).to be nil + it 'is invalid' do + expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors) + expect(viewer.valid?).to be false + end + + it 'returns validation errors' do + expect(viewer.errors).to eq ['The parsed YAML is too big'] end end end - context 'when definition is invalid' do - let(:error) { ActiveModel::ValidationError.new(PerformanceMonitoring::PrometheusDashboard.new.tap(&:validate)) } - let(:data) do - <<~YAML - dashboard: - YAML + context 'with metrics_dashboard_exhaustive_validations feature flag off' do + before do + stub_feature_flags(metrics_dashboard_exhaustive_validations: false) end - describe '#valid?' do - it 'returns false' do - expect(PerformanceMonitoring::PrometheusDashboard) - .to receive(:from_json).and_raise(error) + context 'when the definition is valid' do + describe '#valid?' do + before do + allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json) + end + + it 'calls prepare! on the viewer' do + expect(viewer).to receive(:prepare!) - expect(viewer.valid?).to be_falsey + viewer.valid? + end + + it 'processes dashboard yaml and returns true', :aggregate_failures do + yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw! + + expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader| + expect(loader).to receive(:load_raw!).and_call_original + end + expect(PerformanceMonitoring::PrometheusDashboard) + .to receive(:from_json) + .with(yml) + .and_call_original + expect(viewer.valid?).to be true + end + end + + describe '#errors' do + it 'returns empty array' do + expect(viewer.errors).to eq [] + end end end - describe '#errors' do - it 'returns validation errors' do - allow(PerformanceMonitoring::PrometheusDashboard) - .to receive(:from_json).and_raise(error) + context 'when definition is invalid' do + let(:error) { ActiveModel::ValidationError.new(PerformanceMonitoring::PrometheusDashboard.new.tap(&:validate)) } + let(:data) do + <<~YAML + dashboard: + YAML + end + + describe '#valid?' do + it 'returns false' do + expect(PerformanceMonitoring::PrometheusDashboard) + .to receive(:from_json).and_raise(error) - expect(viewer.errors).to be error.model.errors + expect(viewer.valid?).to be false + end + end + + describe '#errors' do + it 'returns validation errors' do + allow(PerformanceMonitoring::PrometheusDashboard) + .to receive(:from_json).and_raise(error) + + expect(viewer.errors).to eq error.model.errors.messages.map { |messages| messages.join(': ') } + end end end - end - context 'when YAML syntax is invalid' do - let(:data) do - <<~YAML + context 'when YAML syntax is invalid' do + let(:data) do + <<~YAML dashboard: 'empty metrics' panel_groups: - group: 'Group Title' - YAML - end - - describe '#valid?' do - it 'returns false' do - expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json) - expect(viewer.valid?).to be_falsey + YAML end - end - describe '#errors' do - it 'returns validation errors' do - yaml_wrapped_errors = { 'YAML syntax': ["(): did not find expected key while parsing a block mapping at line 1 column 1"] } + describe '#valid?' do + it 'returns false' do + expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json) + expect(viewer.valid?).to be false + end + end - expect(viewer.errors).to be_kind_of ActiveModel::Errors - expect(viewer.errors.messages).to eql(yaml_wrapped_errors) + describe '#errors' do + it 'returns validation errors' do + expect(viewer.errors).to eq ["YAML syntax: (): did not find expected key while parsing a block mapping at line 1 column 1"] + end end end - end - context 'when YAML loader raises error' do - let(:data) do - <<~YAML + context 'when YAML loader raises error' do + let(:data) do + <<~YAML large yaml file - YAML - end - - before do - allow(::Gitlab::Config::Loader::Yaml).to receive(:new) - .and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big') - end + YAML + end - it 'is invalid' do - expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json) - expect(viewer.valid?).to be(false) - end + before do + allow(::Gitlab::Config::Loader::Yaml).to( + receive(:new).and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big') + ) + end - it 'returns validation errors' do - yaml_wrapped_errors = { 'YAML syntax': ["The parsed YAML is too big"] } + it 'is invalid' do + expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json) + expect(viewer.valid?).to be false + end - expect(viewer.errors).to be_kind_of(ActiveModel::Errors) - expect(viewer.errors.messages).to eq(yaml_wrapped_errors) + it 'returns validation errors' do + expect(viewer.errors).to eq ["YAML syntax: The parsed YAML is too big"] + end end end end diff --git a/spec/models/board_group_recent_visit_spec.rb b/spec/models/board_group_recent_visit_spec.rb index 4d16e1ff839..c6fbd263072 100644 --- a/spec/models/board_group_recent_visit_spec.rb +++ b/spec/models/board_group_recent_visit_spec.rb @@ -28,7 +28,7 @@ RSpec.describe BoardGroupRecentVisit do let!(:visit) { create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago } it 'updates the timestamp' do - Timecop.freeze do + freeze_time do described_class.visited!(user, board) expect(described_class.count).to eq 1 diff --git a/spec/models/board_project_recent_visit_spec.rb b/spec/models/board_project_recent_visit_spec.rb index 8e74405fd8c..145a4f5b1a7 100644 --- a/spec/models/board_project_recent_visit_spec.rb +++ b/spec/models/board_project_recent_visit_spec.rb @@ -28,7 +28,7 @@ RSpec.describe BoardProjectRecentVisit do let!(:visit) { create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago } it 'updates the timestamp' do - Timecop.freeze do + freeze_time do described_class.visited!(user, board) expect(described_class.count).to eq 1 diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index 3a459e5897a..850fc1ec6e6 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -50,6 +50,7 @@ RSpec.describe Ci::Bridge do CI_PROJECT_PATH_SLUG CI_PROJECT_NAMESPACE CI_PROJECT_ROOT_NAMESPACE CI_PIPELINE_IID CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION CI_COMMIT_REF_PROTECTED + CI_COMMIT_TIMESTAMP ] expect(bridge.scoped_variables_hash.keys).to include(*variables) diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb index e4d71632957..069864fa765 100644 --- a/spec/models/ci/build_metadata_spec.rb +++ b/spec/models/ci/build_metadata_spec.rb @@ -73,7 +73,7 @@ RSpec.describe Ci::BuildMetadata do context 'when both runner and job timeouts are set' do before do - build.update(runner: runner) + build.update!(runner: runner) end context 'when job timeout is higher than runner timeout' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 069ac23c5a4..1e551d9ee33 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -25,6 +25,7 @@ RSpec.describe Ci::Build do it { is_expected.to have_many(:sourced_pipelines) } it { is_expected.to have_many(:job_variables) } it { is_expected.to have_many(:report_results) } + it { is_expected.to have_many(:pages_deployments) } it { is_expected.to have_one(:deployment) } it { is_expected.to have_one(:runner_session) } @@ -448,7 +449,7 @@ RSpec.describe Ci::Build do end it 'schedules BuildScheduleWorker at the right time' do - Timecop.freeze do + freeze_time do expect(Ci::BuildScheduleWorker) .to receive(:perform_at).with(be_like_time(1.minute.since), build.id) @@ -496,7 +497,7 @@ RSpec.describe Ci::Build do let(:option) { { start_in: '1 day' } } it 'returns date after 1 day' do - Timecop.freeze do + freeze_time do is_expected.to eq(1.day.since) end end @@ -506,7 +507,7 @@ RSpec.describe Ci::Build do let(:option) { { start_in: '1 week' } } it 'returns date after 1 week' do - Timecop.freeze do + freeze_time do is_expected.to eq(1.week.since) end end @@ -566,18 +567,18 @@ RSpec.describe Ci::Build do let(:runner) { create(:ci_runner, :project, projects: [build.project]) } before do - runner.update(contacted_at: 1.second.ago) + runner.update!(contacted_at: 1.second.ago) end it { is_expected.to be_truthy } it 'that is inactive' do - runner.update(active: false) + runner.update!(active: false) is_expected.to be_falsey end it 'that is not online' do - runner.update(contacted_at: nil) + runner.update!(contacted_at: nil) is_expected.to be_falsey end @@ -612,6 +613,46 @@ RSpec.describe Ci::Build do end end + describe '#locked_artifacts?' do + subject(:locked_artifacts) { build.locked_artifacts? } + + context 'when pipeline is artifacts_locked' do + before do + build.pipeline.artifacts_locked! + end + + context 'artifacts archive does not exist' do + let(:build) { create(:ci_build) } + + it { is_expected.to be_falsy } + end + + context 'artifacts archive exists' do + let(:build) { create(:ci_build, :artifacts) } + + it { is_expected.to be_truthy } + end + end + + context 'when pipeline is unlocked' do + before do + build.pipeline.unlocked! + end + + context 'artifacts archive does not exist' do + let(:build) { create(:ci_build) } + + it { is_expected.to be_falsy } + end + + context 'artifacts archive exists' do + let(:build) { create(:ci_build, :artifacts) } + + it { is_expected.to be_falsy } + end + end + end + describe '#available_artifacts?' do let(:build) { create(:ci_build) } @@ -683,7 +724,7 @@ RSpec.describe Ci::Build do context 'is expired' do before do - build.update(artifacts_expire_at: Time.current - 7.days) + build.update!(artifacts_expire_at: Time.current - 7.days) end it { is_expected.to be_truthy } @@ -691,7 +732,7 @@ RSpec.describe Ci::Build do context 'is not expired' do before do - build.update(artifacts_expire_at: Time.current + 7.days) + build.update!(artifacts_expire_at: Time.current + 7.days) end it { is_expected.to be_falsey } @@ -1012,18 +1053,53 @@ RSpec.describe Ci::Build do end describe '#hide_secrets' do + let(:metrics) { spy('metrics') } let(:subject) { build.hide_secrets(data) } context 'hide runners token' do let(:data) { "new #{project.runners_token} data"} it { is_expected.to match(/^new x+ data$/) } + + it 'increments trace mutation metric' do + build.hide_secrets(data, metrics) + + expect(metrics) + .to have_received(:increment_trace_operation) + .with(operation: :mutated) + end end context 'hide build token' do let(:data) { "new #{build.token} data"} it { is_expected.to match(/^new x+ data$/) } + + it 'increments trace mutation metric' do + build.hide_secrets(data, metrics) + + expect(metrics) + .to have_received(:increment_trace_operation) + .with(operation: :mutated) + end + end + + context 'when build does not include secrets' do + let(:data) { 'my build log' } + + it 'does not mutate trace' do + trace = build.hide_secrets(data) + + expect(trace).to eq data + end + + it 'does not increment trace mutation metric' do + build.hide_secrets(data, metrics) + + expect(metrics) + .not_to have_received(:increment_trace_operation) + .with(operation: :mutated) + end end end @@ -1200,7 +1276,7 @@ RSpec.describe Ci::Build do context 'when environment is defined' do before do - build.update(environment: 'review') + build.update!(environment: 'review') end it { is_expected.to be_truthy } @@ -1208,7 +1284,7 @@ RSpec.describe Ci::Build do context 'when environment is not defined' do before do - build.update(environment: nil) + build.update!(environment: nil) end it { is_expected.to be_falsey } @@ -1316,7 +1392,7 @@ RSpec.describe Ci::Build do context 'when environment is defined' do before do - build.update(environment: 'review') + build.update!(environment: 'review') end context 'no action is defined' do @@ -1325,7 +1401,7 @@ RSpec.describe Ci::Build do context 'and start action is defined' do before do - build.update(options: { environment: { action: 'start' } } ) + build.update!(options: { environment: { action: 'start' } } ) end it { is_expected.to be_truthy } @@ -1334,7 +1410,7 @@ RSpec.describe Ci::Build do context 'when environment is not defined' do before do - build.update(environment: nil) + build.update!(environment: nil) end it { is_expected.to be_falsey } @@ -1346,7 +1422,7 @@ RSpec.describe Ci::Build do context 'when environment is defined' do before do - build.update(environment: 'review') + build.update!(environment: 'review') end context 'no action is defined' do @@ -1355,7 +1431,7 @@ RSpec.describe Ci::Build do context 'and stop action is defined' do before do - build.update(options: { environment: { action: 'stop' } } ) + build.update!(options: { environment: { action: 'stop' } } ) end it { is_expected.to be_truthy } @@ -1364,7 +1440,7 @@ RSpec.describe Ci::Build do context 'when environment is not defined' do before do - build.update(environment: nil) + build.update!(environment: nil) end it { is_expected.to be_falsey } @@ -1687,7 +1763,7 @@ RSpec.describe Ci::Build do describe '#action?' do before do - build.update(when: value) + build.update!(when: value) end subject { build.action? } @@ -2232,7 +2308,7 @@ RSpec.describe Ci::Build do describe '#has_expiring_archive_artifacts?' do context 'when artifacts have expiration date set' do before do - build.update(artifacts_expire_at: 1.day.from_now) + build.update!(artifacts_expire_at: 1.day.from_now) end context 'and job artifacts archive record exists' do @@ -2252,7 +2328,7 @@ RSpec.describe Ci::Build do context 'when artifacts do not have expiration date set' do before do - build.update(artifacts_expire_at: nil) + build.update!(artifacts_expire_at: nil) end it 'does not have expiring artifacts' do @@ -2329,6 +2405,7 @@ RSpec.describe Ci::Build do { key: 'CI_COMMIT_TITLE', value: pipeline.git_commit_title, public: true, masked: false }, { key: 'CI_COMMIT_DESCRIPTION', value: pipeline.git_commit_description, public: true, masked: false }, { key: 'CI_COMMIT_REF_PROTECTED', value: (!!pipeline.protected_ref?).to_s, public: true, masked: false }, + { key: 'CI_COMMIT_TIMESTAMP', value: pipeline.git_commit_timestamp, public: true, masked: false }, { key: 'CI_BUILD_REF', value: build.sha, public: true, masked: false }, { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true, masked: false }, { key: 'CI_BUILD_REF_NAME', value: build.ref, public: true, masked: false }, @@ -2526,7 +2603,7 @@ RSpec.describe Ci::Build do end before do - build.update(user: user) + build.update!(user: user) end it { user_variables.each { |v| is_expected.to include(v) } } @@ -2562,7 +2639,7 @@ RSpec.describe Ci::Build do end before do - build.update(environment: 'production') + build.update!(environment: 'production') end shared_examples 'containing environment variables' do @@ -2589,7 +2666,7 @@ RSpec.describe Ci::Build do context 'when the URL was set from the job' do before do - build.update(options: { environment: { url: url } }) + build.update!(options: { environment: { url: url } }) end it_behaves_like 'containing environment variables' @@ -2607,7 +2684,7 @@ RSpec.describe Ci::Build do context 'when the URL was not set from the job, but environment' do before do - environment.update(external_url: url) + environment.update!(external_url: url) end it_behaves_like 'containing environment variables' @@ -2643,7 +2720,7 @@ RSpec.describe Ci::Build do context 'when build started manually' do before do - build.update(when: :manual) + build.update!(when: :manual) end let(:manual_variable) do @@ -2669,8 +2746,8 @@ RSpec.describe Ci::Build do end before do - build.update(tag: false) - pipeline.update(tag: false) + build.update!(tag: false) + pipeline.update!(tag: false) end it { is_expected.to include(branch_variable) } @@ -2682,8 +2759,8 @@ RSpec.describe Ci::Build do end before do - build.update(tag: true) - pipeline.update(tag: true) + build.update!(tag: true) + pipeline.update!(tag: true) end it { is_expected.to include(tag_variable) } @@ -2860,7 +2937,7 @@ RSpec.describe Ci::Build do context 'and is disabled for project' do before do - project.update(container_registry_enabled: false) + project.update!(container_registry_enabled: false) end it { is_expected.to include(ci_registry) } @@ -2869,7 +2946,7 @@ RSpec.describe Ci::Build do context 'and is enabled for project' do before do - project.update(container_registry_enabled: true) + project.update!(container_registry_enabled: true) end it { is_expected.to include(ci_registry) } @@ -2881,7 +2958,7 @@ RSpec.describe Ci::Build do let(:runner) { create(:ci_runner, description: 'description', tag_list: %w(docker linux)) } before do - build.update(runner: runner) + build.update!(runner: runner) end it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true, masked: false }) } @@ -3685,7 +3762,7 @@ RSpec.describe Ci::Build do subject { described_class.where(id: build).matches_tag_ids(tag_ids) } before do - build.update(tag_list: build_tag_list) + build.update!(tag_list: build_tag_list) end context 'when have different tags' do @@ -3731,7 +3808,7 @@ RSpec.describe Ci::Build do subject { described_class.where(id: build).with_any_tags } before do - build.update(tag_list: tag_list) + build.update!(tag_list: tag_list) end context 'when does have tags' do @@ -4087,7 +4164,7 @@ RSpec.describe Ci::Build do let(:path) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' } around do |example| - Timecop.freeze { example.run } + freeze_time { example.run } end before do diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb index a6362d46449..fefe5e3bfca 100644 --- a/spec/models/ci/build_trace_chunk_spec.rb +++ b/spec/models/ci/build_trace_chunk_spec.rb @@ -21,6 +21,25 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do stub_artifacts_object_storage end + describe 'chunk creation' do + let(:metrics) { spy('metrics') } + + before do + allow(::Gitlab::Ci::Trace::Metrics) + .to receive(:new) + .and_return(metrics) + end + + it 'increments trace operation chunked metric' do + build_trace_chunk.save! + + expect(metrics) + .to have_received(:increment_trace_operation) + .with(operation: :chunked) + .once + end + end + context 'FastDestroyAll' do let(:parent) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: parent) } @@ -32,7 +51,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do expect(external_data_counter).to be > 0 expect(subjects.count).to be > 0 - expect { subjects.first.destroy }.to raise_error('`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`') + expect { subjects.first.destroy! }.to raise_error('`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`') expect { subjects.destroy_all }.to raise_error('`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`') # rubocop: disable Cop/DestroyAll expect(subjects.count).to be > 0 @@ -57,7 +76,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do expect(external_data_counter).to be > 0 expect(subjects.count).to be > 0 - expect { parent.destroy }.not_to raise_error + expect { parent.destroy! }.not_to raise_error expect(subjects.count).to eq(0) expect(external_data_counter).to eq(0) @@ -222,6 +241,8 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do subject build_trace_chunk.reload + + expect(build_trace_chunk.checksum).to be_present expect(build_trace_chunk.fog?).to be_truthy expect(build_trace_chunk.data).to eq(new_data) end @@ -344,6 +365,24 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do it_behaves_like 'Scheduling no sidekiq worker' end end + + describe 'append metrics' do + let(:metrics) { spy('metrics') } + + before do + allow(::Gitlab::Ci::Trace::Metrics) + .to receive(:new) + .and_return(metrics) + end + + it 'increments trace operation appended metric' do + build_trace_chunk.append('123456', 0) + + expect(metrics) + .to have_received(:increment_trace_operation) + .with(operation: :appended) + end + end end describe '#truncate' do @@ -461,6 +500,8 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do end describe '#persist_data!' do + let(:build) { create(:ci_build, :running) } + subject { build_trace_chunk.persist_data! } shared_examples_for 'Atomic operation' do @@ -502,6 +543,12 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) end + it 'calculates CRC32 checksum' do + subject + + expect(build_trace_chunk.reload.checksum).to eq '3398914352' + end + it_behaves_like 'Atomic operation' end @@ -516,6 +563,18 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil end + + context 'when chunk is a final one' do + before do + create(:ci_build_pending_state, build: build) + end + + it 'persists the data' do + subject + + expect(build_trace_chunk.fog?).to be_truthy + end + end end end @@ -565,6 +624,18 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data) expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil end + + context 'when chunk is a final one' do + before do + create(:ci_build_pending_state, build: build) + end + + it 'persists the data' do + subject + + expect(build_trace_chunk.fog?).to be_truthy + end + end end end @@ -614,6 +685,54 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do end end + describe 'final?' do + let(:build) { create(:ci_build, :running) } + + context 'when build pending state exists' do + before do + create(:ci_build_pending_state, build: build) + end + + context 'when chunks is not the last one' do + before do + create(:ci_build_trace_chunk, chunk_index: 1, build: build) + end + + it 'is not a final chunk' do + expect(build.reload.pending_state).to be_present + expect(build_trace_chunk).not_to be_final + end + end + + context 'when chunks is the last one' do + it 'is a final chunk' do + expect(build.reload.pending_state).to be_present + expect(build_trace_chunk).to be_final + end + end + end + + context 'when build pending state does not exist' do + context 'when chunks is not the last one' do + before do + create(:ci_build_trace_chunk, chunk_index: 1, build: build) + end + + it 'is not a final chunk' do + expect(build.reload.pending_state).not_to be_present + expect(build_trace_chunk).not_to be_final + end + end + + context 'when chunks is the last one' do + it 'is not a final chunk' do + expect(build.reload.pending_state).not_to be_present + expect(build_trace_chunk).not_to be_final + end + end + end + end + describe 'deletes data in redis after a parent record destroyed' do let(:project) { create(:project) } diff --git a/spec/models/ci/build_trace_chunks/fog_spec.rb b/spec/models/ci/build_trace_chunks/fog_spec.rb index a44ae58dfd2..b7e9adab04a 100644 --- a/spec/models/ci/build_trace_chunks/fog_spec.rb +++ b/spec/models/ci/build_trace_chunks/fog_spec.rb @@ -46,9 +46,7 @@ RSpec.describe Ci::BuildTraceChunks::Fog do end describe '#set_data' do - subject { data_store.set_data(model, data) } - - let(:data) { 'abc123' } + let(:new_data) { 'abc123' } context 'when data exists' do let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') } @@ -56,9 +54,9 @@ RSpec.describe Ci::BuildTraceChunks::Fog do it 'overwrites data' do expect(data_store.data(model)).to eq('sample data in fog') - subject + data_store.set_data(model, new_data) - expect(data_store.data(model)).to eq('abc123') + expect(data_store.data(model)).to eq new_data end end @@ -68,9 +66,9 @@ RSpec.describe Ci::BuildTraceChunks::Fog do it 'sets new data' do expect(data_store.data(model)).to be_nil - subject + data_store.set_data(model, new_data) - expect(data_store.data(model)).to eq('abc123') + expect(data_store.data(model)).to eq new_data end end end diff --git a/spec/models/ci/instance_variable_spec.rb b/spec/models/ci/instance_variable_spec.rb index 15d0c911bc4..0ef1dfbd55c 100644 --- a/spec/models/ci/instance_variable_spec.rb +++ b/spec/models/ci/instance_variable_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Ci::InstanceVariable do let(:value) { SecureRandom.alphanumeric(10_002) } it 'raises a database level error' do - expect { variable.save }.to raise_error(ActiveRecord::StatementInvalid) + expect { variable.save! }.to raise_error(ActiveRecord::StatementInvalid) end end @@ -36,7 +36,7 @@ RSpec.describe Ci::InstanceVariable do let(:value) { SecureRandom.alphanumeric(10_000) } it 'does not raise database level error' do - expect { variable.save }.not_to raise_error + expect { variable.save! }.not_to raise_error end end end @@ -85,7 +85,7 @@ RSpec.describe Ci::InstanceVariable do it 'resets the cache when records are deleted' do expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable) - protected_variable.destroy + protected_variable.destroy! expect(described_class.all_cached).to contain_exactly(unprotected_variable) end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 91a669aa3f4..779839df670 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -20,7 +20,9 @@ RSpec.describe Ci::JobArtifact do it_behaves_like 'having unique enum values' it_behaves_like 'UpdateProjectStatistics' do - subject { build(:ci_job_artifact, :archive, size: 107464) } + let_it_be(:job, reload: true) { create(:ci_build) } + + subject { build(:ci_job_artifact, :archive, job: job, size: 107464) } end describe '.not_expired' do @@ -343,42 +345,6 @@ RSpec.describe Ci::JobArtifact do end end - describe '#each_blob' do - context 'when file format is gzip' do - context 'when gzip file contains one file' do - let(:artifact) { build(:ci_job_artifact, :junit) } - - it 'iterates blob once' do - expect { |b| artifact.each_blob(&b) }.to yield_control.once - end - end - - context 'when gzip file contains three files' do - let(:artifact) { build(:ci_job_artifact, :junit_with_three_testsuites) } - - it 'iterates blob three times' do - expect { |b| artifact.each_blob(&b) }.to yield_control.exactly(3).times - end - end - end - - context 'when file format is raw' do - let(:artifact) { build(:ci_job_artifact, :codequality, file_format: :raw) } - - it 'iterates blob once' do - expect { |b| artifact.each_blob(&b) }.to yield_control.once - end - end - - context 'when there are no adapters for the file format' do - let(:artifact) { build(:ci_job_artifact, :junit, file_format: :zip) } - - it 'raises an error' do - expect { |b| artifact.each_blob(&b) }.to raise_error(described_class::NotSupportedAdapterError) - end - end - end - describe 'expired?' do subject { artifact.expired? } diff --git a/spec/models/ci/legacy_stage_spec.rb b/spec/models/ci/legacy_stage_spec.rb index c53f6abb037..2487ad85889 100644 --- a/spec/models/ci/legacy_stage_spec.rb +++ b/spec/models/ci/legacy_stage_spec.rb @@ -116,7 +116,7 @@ RSpec.describe Ci::LegacyStage do let!(:new_build) { create_job(:ci_build, status: :success) } before do - stage_build.update(retried: true) + stage_build.update!(retried: true) end it "returns status of latest build" do diff --git a/spec/models/ci/persistent_ref_spec.rb b/spec/models/ci/persistent_ref_spec.rb index 18552317025..e488580ae7b 100644 --- a/spec/models/ci/persistent_ref_spec.rb +++ b/spec/models/ci/persistent_ref_spec.rb @@ -24,7 +24,7 @@ RSpec.describe Ci::PersistentRef do context 'when a persistent ref exists' do before do - pipeline.persistent_ref.create + pipeline.persistent_ref.create # rubocop: disable Rails/SaveBang end it { is_expected.to eq(true) } @@ -32,7 +32,7 @@ RSpec.describe Ci::PersistentRef do end describe '#create' do - subject { pipeline.persistent_ref.create } + subject { pipeline.persistent_ref.create } # rubocop: disable Rails/SaveBang let(:pipeline) { create(:ci_pipeline, sha: sha, project: project) } let(:project) { create(:project, :repository) } @@ -58,7 +58,7 @@ RSpec.describe Ci::PersistentRef do context 'when a persistent ref already exists' do before do - pipeline.persistent_ref.create + pipeline.persistent_ref.create # rubocop: disable Rails/SaveBang end it 'overwrites a persistent ref' do @@ -78,7 +78,7 @@ RSpec.describe Ci::PersistentRef do context 'when a persistent ref exists' do before do - pipeline.persistent_ref.create + pipeline.persistent_ref.create # rubocop: disable Rails/SaveBang end it 'deletes the ref' do diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb index 9d63d74a6cc..8cbace845a9 100644 --- a/spec/models/ci/pipeline_artifact_spec.rb +++ b/spec/models/ci/pipeline_artifact_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Ci::PipelineArtifact, type: :model do - let_it_be(:coverage_report) { create(:ci_pipeline_artifact) } + let(:coverage_report) { create(:ci_pipeline_artifact) } describe 'associations' do it { is_expected.to belong_to(:pipeline) } @@ -12,6 +12,12 @@ RSpec.describe Ci::PipelineArtifact, type: :model do it_behaves_like 'having unique enum values' + it_behaves_like 'UpdateProjectStatistics' do + let_it_be(:pipeline, reload: true) { create(:ci_pipeline) } + + subject { build(:ci_pipeline_artifact, pipeline: pipeline) } + end + describe 'validations' do it { is_expected.to validate_presence_of(:pipeline) } it { is_expected.to validate_presence_of(:project) } @@ -44,38 +50,74 @@ RSpec.describe Ci::PipelineArtifact, type: :model do end end - describe '#set_size' do + describe 'file is being stored' do subject { create(:ci_pipeline_artifact) } - context 'when file is being created' do - it 'sets the size' do - expect(subject.size).to eq(85) + context 'when existing object has local store' do + it_behaves_like 'mounted file in local store' + end + + context 'when direct upload is enabled' do + before do + stub_artifacts_object_storage(Ci::PipelineArtifactUploader, direct_upload: true) + end + + context 'when file is stored' do + it_behaves_like 'mounted file in object store' end end - context 'when file is being updated' do - it 'updates the size' do - subject.update!(file: fixture_file_upload('spec/fixtures/dk.png')) + context 'when file contains multi-byte characters' do + let(:coverage_report_multibyte) { create(:ci_pipeline_artifact, :with_multibyte_characters) } - expect(subject.size).to eq(1062) + it 'sets the size in bytesize' do + expect(coverage_report_multibyte.size).to eq(14) end end end - describe 'file is being stored' do - subject { create(:ci_pipeline_artifact) } + describe '.has_code_coverage?' do + subject { Ci::PipelineArtifact.has_code_coverage? } - context 'when existing object has local store' do - it_behaves_like 'mounted file in local store' + context 'when pipeline artifact has a code coverage' do + let!(:pipeline_artifact) { create(:ci_pipeline_artifact) } + + it 'returns true' do + expect(subject).to be_truthy + end end - context 'when direct upload is enabled' do - before do - stub_artifacts_object_storage(Ci::PipelineArtifactUploader, direct_upload: true) + context 'when pipeline artifact does not have a code coverage' do + it 'returns false' do + expect(subject).to be_falsey end + end + end - context 'when file is stored' do - it_behaves_like 'mounted file in object store' + describe '.find_with_code_coverage' do + subject { Ci::PipelineArtifact.find_with_code_coverage } + + context 'when pipeline artifact has a coverage report' do + let!(:coverage_report) { create(:ci_pipeline_artifact) } + + it 'returns a pipeline artifact with a code coverage' do + expect(subject.file_type).to eq('code_coverage') + end + end + + context 'when pipeline artifact does not have a coverage report' do + it 'returns nil' do + expect(subject).to be_nil + end + end + end + + describe '#present' do + subject { coverage_report.present } + + context 'when file_type is code_coverage' do + it 'uses code coverage presenter' do + expect(subject.present).to be_kind_of(Ci::PipelineArtifacts::CodeCoveragePresenter) end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index b4e80fa7588..228a1e8f7a2 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2,12 +2,14 @@ require 'spec_helper' -RSpec.describe Ci::Pipeline, :mailer do +RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do include ProjectForksHelper include StubRequests + include Ci::SourcePipelineHelpers - let(:user) { create(:user) } - let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:namespace) { create_default(:namespace) } + let_it_be(:project) { create_default(:project, :repository) } let(:pipeline) do create(:ci_empty_pipeline, status: :created, project: project) @@ -220,6 +222,26 @@ RSpec.describe Ci::Pipeline, :mailer do end end + describe '.ci_sources' do + subject { described_class.ci_sources } + + let!(:push_pipeline) { create(:ci_pipeline, source: :push) } + let!(:web_pipeline) { create(:ci_pipeline, source: :web) } + let!(:api_pipeline) { create(:ci_pipeline, source: :api) } + let!(:webide_pipeline) { create(:ci_pipeline, source: :webide) } + let!(:child_pipeline) { create(:ci_pipeline, source: :parent_pipeline) } + + it 'contains pipelines having CI only sources' do + expect(subject).to contain_exactly(push_pipeline, web_pipeline, api_pipeline) + end + + it 'filters on expected sources' do + expect(::Enums::Ci::Pipeline.ci_sources.keys).to contain_exactly( + *%i[unknown push web trigger schedule api external pipeline chat + merge_request_event external_pull_request_event]) + end + end + describe '.outside_pipeline_family' do subject(:outside_pipeline_family) { described_class.outside_pipeline_family(upstream_pipeline) } @@ -714,6 +736,7 @@ RSpec.describe Ci::Pipeline, :mailer do CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION CI_COMMIT_REF_PROTECTED + CI_COMMIT_TIMESTAMP CI_BUILD_REF CI_BUILD_BEFORE_SHA CI_BUILD_REF_NAME @@ -879,7 +902,7 @@ RSpec.describe Ci::Pipeline, :mailer do context 'when there is auto_canceled_by' do before do - pipeline.update(auto_canceled_by: create(:ci_empty_pipeline)) + pipeline.update!(auto_canceled_by: create(:ci_empty_pipeline)) end it 'is auto canceled' do @@ -1237,14 +1260,42 @@ RSpec.describe Ci::Pipeline, :mailer do end describe 'pipeline caching' do - before do - pipeline.config_source = 'repository_source' + context 'if pipeline is cacheable' do + before do + pipeline.source = 'push' + end + + it 'performs ExpirePipelinesCacheWorker' do + expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id) + + pipeline.cancel + end end - it 'performs ExpirePipelinesCacheWorker' do - expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id) + context 'if pipeline is not cacheable' do + before do + pipeline.source = 'webide' + end - pipeline.cancel + it 'deos not perform ExpirePipelinesCacheWorker' do + expect(ExpirePipelineCacheWorker).not_to receive(:perform_async) + + pipeline.cancel + end + end + end + + describe '#dangling?' do + it 'returns true if pipeline comes from any dangling sources' do + pipeline.source = Enums::Ci::Pipeline.dangling_sources.each_key.first + + expect(pipeline).to be_dangling + end + + it 'returns true if pipeline comes from any CI sources' do + pipeline.source = Enums::Ci::Pipeline.ci_sources.each_key.first + + expect(pipeline).not_to be_dangling end end @@ -1309,34 +1360,126 @@ RSpec.describe Ci::Pipeline, :mailer do end end - context 'when pipeline is bridge triggered' do - before do - pipeline.source_bridge = create(:ci_bridge) - end + def auto_devops_pipelines_completed_total(status) + Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines').get(status: status) + end + end + + describe 'bridge triggered pipeline' do + shared_examples 'upstream downstream pipeline' do + let!(:source_pipeline) { create(:ci_sources_pipeline, pipeline: downstream_pipeline, source_job: bridge) } + let!(:job) { downstream_pipeline.builds.first } context 'when source bridge is dependent on pipeline status' do - before do - allow(pipeline.source_bridge).to receive(:dependent?).and_return(true) - end + let!(:bridge) { create(:ci_bridge, :strategy_depend, pipeline: upstream_pipeline) } it 'schedules the pipeline bridge worker' do - expect(::Ci::PipelineBridgeStatusWorker).to receive(:perform_async) + expect(::Ci::PipelineBridgeStatusWorker).to receive(:perform_async).with(downstream_pipeline.id) + + downstream_pipeline.succeed! + end + + context 'when the downstream pipeline first fails then retries and succeeds' do + it 'makes the upstream pipeline successful' do + Sidekiq::Testing.inline! { job.drop! } + + expect(downstream_pipeline.reload).to be_failed + expect(upstream_pipeline.reload).to be_failed + + Sidekiq::Testing.inline! do + new_job = Ci::Build.retry(job, project.users.first) + + expect(downstream_pipeline.reload).to be_running + expect(upstream_pipeline.reload).to be_running + + new_job.success! + end + + expect(downstream_pipeline.reload).to be_success + expect(upstream_pipeline.reload).to be_success + end + end + + context 'when the downstream pipeline first succeeds then retries and fails' do + it 'makes the upstream pipeline failed' do + Sidekiq::Testing.inline! { job.success! } + + expect(downstream_pipeline.reload).to be_success + expect(upstream_pipeline.reload).to be_success + + Sidekiq::Testing.inline! do + new_job = Ci::Build.retry(job, project.users.first) + + expect(downstream_pipeline.reload).to be_running + expect(upstream_pipeline.reload).to be_running + + new_job.drop! + end + + expect(downstream_pipeline.reload).to be_failed + expect(upstream_pipeline.reload).to be_failed + end + end + + context 'when the upstream pipeline has another dependent upstream pipeline' do + let!(:upstream_of_upstream_pipeline) { create(:ci_pipeline) } + + before do + upstream_bridge = create(:ci_bridge, :strategy_depend, pipeline: upstream_of_upstream_pipeline) + create(:ci_sources_pipeline, pipeline: upstream_pipeline, + source_job: upstream_bridge) + end + + context 'when the downstream pipeline first fails then retries and succeeds' do + it 'makes upstream pipelines successful' do + Sidekiq::Testing.inline! { job.drop! } + + expect(downstream_pipeline.reload).to be_failed + expect(upstream_pipeline.reload).to be_failed + expect(upstream_of_upstream_pipeline.reload).to be_failed - pipeline.succeed! + Sidekiq::Testing.inline! do + new_job = Ci::Build.retry(job, project.users.first) + + expect(downstream_pipeline.reload).to be_running + expect(upstream_pipeline.reload).to be_running + expect(upstream_of_upstream_pipeline.reload).to be_running + + new_job.success! + end + + expect(downstream_pipeline.reload).to be_success + expect(upstream_pipeline.reload).to be_success + expect(upstream_of_upstream_pipeline.reload).to be_success + end + end end end context 'when source bridge is not dependent on pipeline status' do + let!(:bridge) { create(:ci_bridge, pipeline: upstream_pipeline) } + it 'does not schedule the pipeline bridge worker' do expect(::Ci::PipelineBridgeStatusWorker).not_to receive(:perform_async) - pipeline.succeed! + downstream_pipeline.succeed! end end end - def auto_devops_pipelines_completed_total(status) - Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines').get(status: status) + context 'multi-project pipelines' do + let!(:downstream_project) { create(:project, :repository) } + let!(:upstream_pipeline) { create(:ci_pipeline, project: project) } + let!(:downstream_pipeline) { create(:ci_pipeline, :with_job, project: downstream_project) } + + it_behaves_like 'upstream downstream pipeline' + end + + context 'parent-child pipelines' do + let!(:upstream_pipeline) { create(:ci_pipeline, project: project) } + let!(:downstream_pipeline) { create(:ci_pipeline, :with_job, project: project) } + + it_behaves_like 'upstream downstream pipeline' end end @@ -1436,8 +1579,6 @@ RSpec.describe Ci::Pipeline, :mailer do context 'when repository exists' do using RSpec::Parameterized::TableSyntax - let(:project) { create(:project, :repository) } - where(:tag, :ref, :result) do false | 'master' | true false | 'non-existent-branch' | false @@ -1457,6 +1598,7 @@ RSpec.describe Ci::Pipeline, :mailer do end context 'when repository does not exist' do + let(:project) { create(:project) } let(:pipeline) do create(:ci_empty_pipeline, project: project, ref: 'master') end @@ -1468,8 +1610,6 @@ RSpec.describe Ci::Pipeline, :mailer do end context 'with non-empty project' do - let(:project) { create(:project, :repository) } - let(:pipeline) do create(:ci_pipeline, project: project, @@ -1524,7 +1664,7 @@ RSpec.describe Ci::Pipeline, :mailer do it 'looks up a commit for a tag' do expect(project.repository.branch_names).not_to include 'v1.0.0' - pipeline.update(sha: project.commit('v1.0.0').sha, ref: 'v1.0.0', tag: true) + pipeline.update!(sha: project.commit('v1.0.0').sha, ref: 'v1.0.0', tag: true) expect(pipeline).to be_tag expect(pipeline).to be_latest @@ -1533,7 +1673,7 @@ RSpec.describe Ci::Pipeline, :mailer do context 'with not latest sha' do before do - pipeline.update(sha: project.commit("#{project.default_branch}~1").sha) + pipeline.update!(sha: project.commit("#{project.default_branch}~1").sha) end it 'returns false' do @@ -1561,7 +1701,7 @@ RSpec.describe Ci::Pipeline, :mailer do let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') } before do - manual.update(retried: true) + manual.update!(retried: true) end it 'returns latest one' do @@ -1596,10 +1736,8 @@ RSpec.describe Ci::Pipeline, :mailer do describe '#modified_paths' do context 'when old and new revisions are set' do - let(:project) { create(:project, :repository) } - before do - pipeline.update(before_sha: '1234abcd', sha: '2345bcde') + pipeline.update!(before_sha: '1234abcd', sha: '2345bcde') end it 'fetches stats for changes between commits' do @@ -1866,8 +2004,6 @@ RSpec.describe Ci::Pipeline, :mailer do end describe '.latest_pipeline_per_commit' do - let(:project) { create(:project) } - let!(:commit_123_ref_master) do create( :ci_empty_pipeline, @@ -1962,15 +2098,14 @@ RSpec.describe Ci::Pipeline, :mailer do end describe '.last_finished_for_ref_id' do - let(:project) { create(:project, :repository) } let(:branch) { project.default_branch } let(:ref) { project.ci_refs.take } - let(:config_source) { Ci::PipelineEnums.config_sources[:parameter_source] } + let(:dangling_source) { Enums::Ci::Pipeline.sources[:ondemand_dast_scan] } let!(:pipeline1) { create(:ci_pipeline, :success, project: project, ref: branch) } let!(:pipeline2) { create(:ci_pipeline, :success, project: project, ref: branch) } let!(:pipeline3) { create(:ci_pipeline, :failed, project: project, ref: branch) } let!(:pipeline4) { create(:ci_pipeline, :success, project: project, ref: branch) } - let!(:pipeline5) { create(:ci_pipeline, :success, project: project, ref: branch, config_source: config_source) } + let!(:pipeline5) { create(:ci_pipeline, :success, project: project, ref: branch, source: dangling_source) } it 'returns the expected pipeline' do result = described_class.last_finished_for_ref_id(ref.id) @@ -2452,7 +2587,6 @@ RSpec.describe Ci::Pipeline, :mailer do end describe "#merge_requests_as_head_pipeline" do - let(:project) { create(:project) } let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: 'a288a022a53a5a944fae87bcec6efc87b7061808') } it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do @@ -2575,11 +2709,11 @@ RSpec.describe Ci::Pipeline, :mailer do end describe '#same_family_pipeline_ids' do - subject(:same_family_pipeline_ids) { pipeline.same_family_pipeline_ids } + subject { pipeline.same_family_pipeline_ids.map(&:id) } context 'when pipeline is not child nor parent' do it 'returns just the pipeline id' do - expect(same_family_pipeline_ids).to contain_exactly(pipeline.id) + expect(subject).to contain_exactly(pipeline.id) end end @@ -2588,21 +2722,12 @@ RSpec.describe Ci::Pipeline, :mailer do let(:sibling) { create(:ci_pipeline, project: pipeline.project) } before do - create(:ci_sources_pipeline, - source_job: create(:ci_build, pipeline: parent), - source_project: parent.project, - pipeline: pipeline, - project: pipeline.project) - - create(:ci_sources_pipeline, - source_job: create(:ci_build, pipeline: parent), - source_project: parent.project, - pipeline: sibling, - project: sibling.project) + create_source_pipeline(parent, pipeline) + create_source_pipeline(parent, sibling) end it 'returns parent sibling and self ids' do - expect(same_family_pipeline_ids).to contain_exactly(parent.id, pipeline.id, sibling.id) + expect(subject).to contain_exactly(parent.id, pipeline.id, sibling.id) end end @@ -2610,15 +2735,43 @@ RSpec.describe Ci::Pipeline, :mailer do let(:child) { create(:ci_pipeline, project: pipeline.project) } before do - create(:ci_sources_pipeline, - source_job: create(:ci_build, pipeline: pipeline), - source_project: pipeline.project, - pipeline: child, - project: child.project) + create_source_pipeline(pipeline, child) end it 'returns self and child ids' do - expect(same_family_pipeline_ids).to contain_exactly(pipeline.id, child.id) + expect(subject).to contain_exactly(pipeline.id, child.id) + end + end + + context 'when pipeline is a child of a child pipeline' do + let(:ancestor) { create(:ci_pipeline, project: pipeline.project) } + let(:parent) { create(:ci_pipeline, project: pipeline.project) } + let(:cousin_parent) { create(:ci_pipeline, project: pipeline.project) } + let(:cousin) { create(:ci_pipeline, project: pipeline.project) } + + before do + create_source_pipeline(ancestor, parent) + create_source_pipeline(ancestor, cousin_parent) + create_source_pipeline(parent, pipeline) + create_source_pipeline(cousin_parent, cousin) + end + + it 'returns all family ids' do + expect(subject).to contain_exactly( + ancestor.id, parent.id, cousin_parent.id, cousin.id, pipeline.id + ) + end + end + + context 'when pipeline is a triggered pipeline' do + let(:upstream) { create(:ci_pipeline, project: create(:project)) } + + before do + create_source_pipeline(upstream, pipeline) + end + + it 'returns self id' do + expect(subject).to contain_exactly(pipeline.id) end end end @@ -2685,7 +2838,8 @@ RSpec.describe Ci::Pipeline, :mailer do end describe 'notifications when pipeline success or failed' do - let(:project) { create(:project, :repository) } + let(:namespace) { create(:namespace) } + let(:project) { create(:project, :repository, namespace: namespace) } let(:pipeline) do create(:ci_pipeline, @@ -2698,7 +2852,7 @@ RSpec.describe Ci::Pipeline, :mailer do project.add_developer(pipeline.user) pipeline.user.global_notification_setting - .update(level: 'custom', failed_pipeline: true, success_pipeline: true) + .update!(level: 'custom', failed_pipeline: true, success_pipeline: true) perform_enqueued_jobs do pipeline.enqueue @@ -2948,6 +3102,54 @@ RSpec.describe Ci::Pipeline, :mailer do end end + describe '#has_coverage_reports?' do + subject { pipeline.has_coverage_reports? } + + context 'when pipeline has a code coverage artifact' do + let(:pipeline) { create(:ci_pipeline, :with_coverage_report_artifact, :running, project: project) } + + it { expect(subject).to be_truthy } + end + + context 'when pipeline does not have a code coverage artifact' do + let(:pipeline) { create(:ci_pipeline, :success, project: project) } + + it { expect(subject).to be_falsey } + end + end + + describe '#can_generate_coverage_reports?' do + subject { pipeline.can_generate_coverage_reports? } + + context 'when pipeline has builds with coverage reports' do + before do + create(:ci_build, :coverage_reports, pipeline: pipeline, project: project) + end + + context 'when pipeline status is running' do + let(:pipeline) { create(:ci_pipeline, :running, project: project) } + + it { expect(subject).to be_falsey } + end + + context 'when pipeline status is success' do + let(:pipeline) { create(:ci_pipeline, :success, project: project) } + + it { expect(subject).to be_truthy } + end + end + + context 'when pipeline does not have builds with coverage reports' do + before do + create(:ci_build, :artifacts, pipeline: pipeline, project: project) + end + + let(:pipeline) { create(:ci_pipeline, :success, project: project) } + + it { expect(subject).to be_falsey } + end + end + describe '#test_report_summary' do subject { pipeline.test_report_summary } @@ -3228,7 +3430,8 @@ RSpec.describe Ci::Pipeline, :mailer do end describe '#parent_pipeline' do - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } context 'when pipeline is triggered by a pipeline from the same project' do @@ -3283,7 +3486,7 @@ RSpec.describe Ci::Pipeline, :mailer do end describe '#child_pipelines' do - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project) } context 'when pipeline triggered other pipelines on same project' do @@ -3413,4 +3616,152 @@ RSpec.describe Ci::Pipeline, :mailer do it { is_expected.to eq(Gitlab::Git::TAG_REF_PREFIX + pipeline.source_ref.to_s) } end end + + describe "#builds_with_coverage" do + it 'returns builds with coverage only' do + rspec = create(:ci_build, name: 'rspec', coverage: 97.1, pipeline: pipeline) + jest = create(:ci_build, name: 'jest', coverage: 94.1, pipeline: pipeline) + karma = create(:ci_build, name: 'karma', coverage: nil, pipeline: pipeline) + + builds = pipeline.builds_with_coverage + + expect(builds).to include(rspec, jest) + expect(builds).not_to include(karma) + end + end + + describe '#base_and_ancestors' do + let(:same_project) { false } + + subject { pipeline.base_and_ancestors(same_project: same_project) } + + context 'when pipeline is not child nor parent' do + it 'returns just the pipeline itself' do + expect(subject).to contain_exactly(pipeline) + end + end + + context 'when pipeline is child' do + let(:parent) { create(:ci_pipeline, project: pipeline.project) } + let(:sibling) { create(:ci_pipeline, project: pipeline.project) } + + before do + create_source_pipeline(parent, pipeline) + create_source_pipeline(parent, sibling) + end + + it 'returns parent and self' do + expect(subject).to contain_exactly(parent, pipeline) + end + end + + context 'when pipeline is parent' do + let(:child) { create(:ci_pipeline, project: pipeline.project) } + + before do + create_source_pipeline(pipeline, child) + end + + it 'returns self' do + expect(subject).to contain_exactly(pipeline) + end + end + + context 'when pipeline is a child of a child pipeline' do + let(:ancestor) { create(:ci_pipeline, project: pipeline.project) } + let(:parent) { create(:ci_pipeline, project: pipeline.project) } + + before do + create_source_pipeline(ancestor, parent) + create_source_pipeline(parent, pipeline) + end + + it 'returns self, parent and ancestor' do + expect(subject).to contain_exactly(ancestor, parent, pipeline) + end + end + + context 'when pipeline is a triggered pipeline' do + let(:upstream) { create(:ci_pipeline, project: create(:project)) } + + before do + create_source_pipeline(upstream, pipeline) + end + + context 'same_project: false' do + it 'returns upstream and self' do + expect(subject).to contain_exactly(pipeline, upstream) + end + end + + context 'same_project: true' do + let(:same_project) { true } + + it 'returns self' do + expect(subject).to contain_exactly(pipeline) + end + end + end + end + + describe 'reset_ancestor_bridges!' do + context 'when the pipeline is a child pipeline and the bridge is depended' do + let!(:parent_pipeline) { create(:ci_pipeline, project: project) } + let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) } + + it 'marks source bridge as pending' do + pipeline.reset_ancestor_bridges! + + expect(bridge.reload).to be_pending + end + + context 'when the parent pipeline has a dependent upstream pipeline' do + let!(:upstream_bridge) do + create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true) + end + + it 'marks all source bridges as pending' do + pipeline.reset_ancestor_bridges! + + expect(bridge.reload).to be_pending + expect(upstream_bridge.reload).to be_pending + end + end + end + + context 'when the pipeline is a child pipeline and the bridge is not depended' do + let!(:parent_pipeline) { create(:ci_pipeline, project: project) } + let!(:bridge) { create_bridge(parent_pipeline, pipeline, false) } + + it 'does not touch source bridge' do + pipeline.reset_ancestor_bridges! + + expect(bridge.reload).to be_success + end + + context 'when the parent pipeline has a dependent upstream pipeline' do + let!(:upstream_bridge) do + create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true) + end + + it 'does not touch any source bridge' do + pipeline.reset_ancestor_bridges! + + expect(bridge.reload).to be_success + expect(upstream_bridge.reload).to be_success + end + end + end + + private + + def create_bridge(upstream, downstream, depend = false) + options = depend ? { trigger: { strategy: 'depend' } } : {} + + bridge = create(:ci_bridge, pipeline: upstream, status: 'success', options: options) + create(:ci_sources_pipeline, pipeline: downstream, source_job: bridge) + + bridge + end + end end diff --git a/spec/models/ci/ref_spec.rb b/spec/models/ci/ref_spec.rb index 8bce3c10d8c..cb62646532c 100644 --- a/spec/models/ci/ref_spec.rb +++ b/spec/models/ci/ref_spec.rb @@ -16,51 +16,33 @@ RSpec.describe Ci::Ref do stub_const('Ci::PipelineSuccessUnlockArtifactsWorker', unlock_artifacts_worker_spy) end - context 'when keep latest artifact feature is enabled' do - before do - stub_feature_flags(keep_latest_artifacts_for_ref: true) - end - - where(:initial_state, :action, :count) do - :unknown | :succeed! | 1 - :unknown | :do_fail! | 0 - :success | :succeed! | 1 - :success | :do_fail! | 0 - :failed | :succeed! | 1 - :failed | :do_fail! | 0 - :fixed | :succeed! | 1 - :fixed | :do_fail! | 0 - :broken | :succeed! | 1 - :broken | :do_fail! | 0 - :still_failing | :succeed | 1 - :still_failing | :do_fail | 0 - end - - with_them do - context "when transitioning states" do - before do - status_value = Ci::Ref.state_machines[:status].states[initial_state].value - ci_ref.update!(status: status_value) - end - - it 'calls unlock artifacts service' do - ci_ref.send(action) - - expect(unlock_artifacts_worker_spy).to have_received(:perform_async).exactly(count).times - end - end - end + where(:initial_state, :action, :count) do + :unknown | :succeed! | 1 + :unknown | :do_fail! | 0 + :success | :succeed! | 1 + :success | :do_fail! | 0 + :failed | :succeed! | 1 + :failed | :do_fail! | 0 + :fixed | :succeed! | 1 + :fixed | :do_fail! | 0 + :broken | :succeed! | 1 + :broken | :do_fail! | 0 + :still_failing | :succeed | 1 + :still_failing | :do_fail | 0 end - context 'when keep latest artifact feature is not enabled' do - before do - stub_feature_flags(keep_latest_artifacts_for_ref: false) - end + with_them do + context "when transitioning states" do + before do + status_value = Ci::Ref.state_machines[:status].states[initial_state].value + ci_ref.update!(status: status_value) + end - it 'does not call unlock artifacts service' do - ci_ref.succeed! + it 'calls unlock artifacts service' do + ci_ref.send(action) - expect(unlock_artifacts_worker_spy).not_to have_received(:perform_async) + expect(unlock_artifacts_worker_spy).to have_received(:perform_async).exactly(count).times + end end end end @@ -125,8 +107,8 @@ RSpec.describe Ci::Ref do describe '#last_finished_pipeline_id' do let(:pipeline_status) { :running } - let(:config_source) { Ci::PipelineEnums.config_sources[:repository_source] } - let(:pipeline) { create(:ci_pipeline, pipeline_status, config_source: config_source) } + let(:pipeline_source) { Enums::Ci::Pipeline.sources[:push] } + let(:pipeline) { create(:ci_pipeline, pipeline_status, source: pipeline_source) } let(:ci_ref) { pipeline.ci_ref } context 'when there are no finished pipelines' do @@ -142,8 +124,8 @@ RSpec.describe Ci::Ref do expect(ci_ref.last_finished_pipeline_id).to eq(pipeline.id) end - context 'when the pipeline is not a ci_source' do - let(:config_source) { Ci::PipelineEnums.config_sources[:parameter_source] } + context 'when the pipeline a dangling pipeline' do + let(:pipeline_source) { Enums::Ci::Pipeline.sources[:ondemand_dast_scan] } it 'returns nil' do expect(ci_ref.last_finished_pipeline_id).to be_nil diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 8247ebf1144..3e5d068d780 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -570,7 +570,7 @@ RSpec.describe Ci::Runner do let!(:last_update) { runner.ensure_runner_queue_value } before do - Ci::UpdateRunnerService.new(runner).update(description: 'new runner') + Ci::UpdateRunnerService.new(runner).update(description: 'new runner') # rubocop: disable Rails/SaveBang end it 'sets a new last_update value' do @@ -660,7 +660,7 @@ RSpec.describe Ci::Runner do before do runner.tick_runner_queue - runner.destroy + runner.destroy! end it 'cleans up the queue' do @@ -878,7 +878,7 @@ RSpec.describe Ci::Runner do it 'can be destroyed' do subject - expect { subject.destroy }.to change { described_class.count }.by(-1) + expect { subject.destroy! }.to change { described_class.count }.by(-1) end end diff --git a/spec/models/ci_platform_metric_spec.rb b/spec/models/ci_platform_metric_spec.rb new file mode 100644 index 00000000000..0b00875df43 --- /dev/null +++ b/spec/models/ci_platform_metric_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe CiPlatformMetric do + subject { build(:ci_platform_metric) } + + it_behaves_like 'a BulkInsertSafe model', CiPlatformMetric do + let(:valid_items_for_bulk_insertion) { build_list(:ci_platform_metric, 10) } + let(:invalid_items_for_bulk_insertion) { [] } # class does not have any non-constraint validations defined + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:recorded_at) } + it { is_expected.to validate_presence_of(:count) } + it { is_expected.to validate_numericality_of(:count).only_integer.is_greater_than(0) } + it { is_expected.to allow_values('').for(:platform_target) } + it { is_expected.not_to allow_values(nil).for(:platform_target) } + it { is_expected.to validate_length_of(:platform_target).is_at_most(255) } + end + + describe '.insert_auto_devops_platform_targets!' do + def platform_target_counts_by_day + report = Hash.new { |hash, key| hash[key] = {} } + described_class.all.each do |metric| + date = metric.recorded_at.to_date + report[date][metric.platform_target] = metric.count + end + report + end + + context 'when there is already existing metrics data' do + let!(:metric_1) { create(:ci_platform_metric) } + let!(:metric_2) { create(:ci_platform_metric) } + + it 'does not erase any existing data' do + described_class.insert_auto_devops_platform_targets! + + expect(described_class.all.to_a).to contain_exactly(metric_1, metric_2) + end + end + + context 'when there are multiple platform target variables' do + let(:today) { Time.zone.local(1982, 4, 24) } + let(:tomorrow) { today + 1.day } + + it 'inserts platform target counts for that day' do + Timecop.freeze(today) do + create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'ECS') + create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'ECS') + create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FARGATE') + create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FARGATE') + create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FARGATE') + described_class.insert_auto_devops_platform_targets! + end + Timecop.freeze(tomorrow) do + create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FARGATE') + described_class.insert_auto_devops_platform_targets! + end + + expect(platform_target_counts_by_day).to eq({ + today.to_date => { 'ECS' => 2, 'FARGATE' => 3 }, + tomorrow.to_date => { 'ECS' => 2, 'FARGATE' => 4 } + }) + end + end + + context 'when there are invalid ci variable values for platform_target' do + let(:today) { Time.zone.local(1982, 4, 24) } + + it 'ignores those values' do + Timecop.freeze(today) do + create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'ECS') + create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FOO') + create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'BAR') + described_class.insert_auto_devops_platform_targets! + end + + expect(platform_target_counts_by_day).to eq({ + today.to_date => { 'ECS' => 1 } + }) + end + end + + context 'when there are no platform target variables' do + it 'does not generate any new platform metrics' do + create(:ci_variable, key: 'KEY_WHATEVER', value: 'ECS') + described_class.insert_auto_devops_platform_targets! + + expect(platform_target_counts_by_day).to eq({}) + end + end + end +end diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb index bb1fc021e66..99de0d1ddf7 100644 --- a/spec/models/clusters/agent_spec.rb +++ b/spec/models/clusters/agent_spec.rb @@ -12,6 +12,17 @@ RSpec.describe Clusters::Agent do it { is_expected.to validate_length_of(:name).is_at_most(63) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } + describe 'scopes' do + describe '.with_name' do + let!(:matching_name) { create(:cluster_agent, name: 'matching-name') } + let!(:other_name) { create(:cluster_agent, name: 'other-name') } + + subject { described_class.with_name(matching_name.name) } + + it { is_expected.to contain_exactly(matching_name) } + end + end + describe 'validation' do describe 'name validation' do it 'rejects names that do not conform to RFC 1123', :aggregate_failures do diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 1215b38a9a2..82971596176 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -46,7 +46,7 @@ RSpec.describe Clusters::Applications::Prometheus do subject { create(:clusters_applications_prometheus, :installed, cluster: cluster) } it 'sets last_update_started_at to now' do - Timecop.freeze do + freeze_time do expect { subject.make_updating }.to change { subject.reload.last_update_started_at }.to be_within(1.second).of(Time.current) end end @@ -109,10 +109,13 @@ RSpec.describe Clusters::Applications::Prometheus do expect(subject.prometheus_client).to be_instance_of(Gitlab::PrometheusClient) end - it 'copies proxy_url, options and headers from kube client to prometheus_client' do + it 'merges proxy_url, options and headers from kube client with prometheus_client options' do expect(Gitlab::PrometheusClient) .to(receive(:new)) - .with(a_valid_url, kube_client.rest_client.options.merge(headers: kube_client.headers)) + .with(a_valid_url, kube_client.rest_client.options.merge({ + headers: kube_client.headers, + timeout: PrometheusAdapter::DEFAULT_PROMETHEUS_REQUEST_TIMEOUT_SEC + })) subject.prometheus_client end @@ -150,7 +153,7 @@ RSpec.describe Clusters::Applications::Prometheus do it 'is initialized with 3 arguments' do expect(subject.name).to eq('prometheus') expect(subject.chart).to eq('stable/prometheus') - expect(subject.version).to eq('9.5.2') + expect(subject.version).to eq('10.4.1') expect(subject).to be_rbac expect(subject.files).to eq(prometheus.files) end @@ -167,7 +170,7 @@ RSpec.describe Clusters::Applications::Prometheus do let(:prometheus) { create(:clusters_applications_prometheus, :errored, version: '2.0.0') } it 'is initialized with the locked version' do - expect(subject.version).to eq('9.5.2') + expect(subject.version).to eq('10.4.1') end end @@ -238,7 +241,7 @@ RSpec.describe Clusters::Applications::Prometheus do it 'is initialized with 3 arguments' do expect(patch_command.name).to eq('prometheus') expect(patch_command.chart).to eq('stable/prometheus') - expect(patch_command.version).to eq('9.5.2') + expect(patch_command.version).to eq('10.4.1') expect(patch_command.files).to eq(prometheus.files) end end @@ -350,7 +353,7 @@ RSpec.describe Clusters::Applications::Prometheus do let(:timestamp) { Time.current - 5.minutes } around do |example| - Timecop.freeze { example.run } + freeze_time { example.run } end before do diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 2d0b5af0e77..024539e34ec 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -42,6 +42,7 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do it { is_expected.to delegate_method(:available?).to(:application_ingress).with_prefix } it { is_expected.to delegate_method(:available?).to(:application_prometheus).with_prefix } it { is_expected.to delegate_method(:available?).to(:application_knative).with_prefix } + it { is_expected.to delegate_method(:available?).to(:application_elastic_stack).with_prefix } it { is_expected.to delegate_method(:external_ip).to(:application_ingress).with_prefix } it { is_expected.to delegate_method(:external_hostname).to(:application_ingress).with_prefix } diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb index 2920bbf2b58..3b903fe34f9 100644 --- a/spec/models/clusters/kubernetes_namespace_spec.rb +++ b/spec/models/clusters/kubernetes_namespace_spec.rb @@ -61,7 +61,8 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do end describe 'namespace uniqueness validation' do - let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, namespace: 'my-namespace') } + let_it_be(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, cluster: cluster, namespace: 'my-namespace') } subject { kubernetes_namespace } diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 3fb8708c884..334833e884b 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -3,25 +3,22 @@ require 'spec_helper' RSpec.describe CommitRange do + let(:range2) { described_class.new("#{sha_from}..#{sha_to}", project) } + let(:range) { described_class.new("#{sha_from}...#{sha_to}", project) } + let(:full_sha_to) { commit2.id } + let(:full_sha_from) { commit1.id } + let(:sha_to) { commit2.short_id } + let(:sha_from) { commit1.short_id } + let!(:commit2) { project.commit } + let!(:commit1) { project.commit("HEAD~2") } + let!(:project) { create(:project, :public, :repository) } + describe 'modules' do subject { described_class } it { is_expected.to include_module(Referable) } end - let!(:project) { create(:project, :public, :repository) } - let!(:commit1) { project.commit("HEAD~2") } - let!(:commit2) { project.commit } - - let(:sha_from) { commit1.short_id } - let(:sha_to) { commit2.short_id } - - let(:full_sha_from) { commit1.id } - let(:full_sha_to) { commit2.id } - - let(:range) { described_class.new("#{sha_from}...#{sha_to}", project) } - let(:range2) { described_class.new("#{sha_from}..#{sha_to}", project) } - it 'raises ArgumentError when given an invalid range string' do expect { described_class.new("Foo", project) }.to raise_error(ArgumentError) end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 7f893d6a100..6e23f95af03 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -494,6 +494,10 @@ RSpec.describe CommitStatus do end describe '#group_name' do + let(:commit_status) do + build(:commit_status, pipeline: pipeline, stage: 'test') + end + subject { commit_status.group_name } tests = { @@ -510,7 +514,19 @@ RSpec.describe CommitStatus do 'rspec:windows 0 : / 1' => 'rspec:windows', 'rspec:windows 0 : / 1 name' => 'rspec:windows name', '0 1 name ruby' => 'name ruby', - '0 :/ 1 name ruby' => 'name ruby' + '0 :/ 1 name ruby' => 'name ruby', + 'rspec: [aws]' => 'rspec: [aws]', + 'rspec: [aws] 0/1' => 'rspec: [aws]', + 'rspec: [aws, max memory]' => 'rspec', + 'rspec:linux: [aws, max memory, data]' => 'rspec:linux', + 'rspec: [inception: [something, other thing], value]' => 'rspec', + 'rspec:windows 0/1: [name, other]' => 'rspec:windows', + 'rspec:windows: [name, other] 0/1' => 'rspec:windows', + 'rspec:windows: [name, 0/1] 0/1' => 'rspec:windows', + 'rspec:windows: [0/1, name]' => 'rspec:windows', + 'rspec:windows: [, ]' => 'rspec:windows', + 'rspec:windows: [name]' => 'rspec:windows: [name]', + 'rspec:windows: [name,other]' => 'rspec:windows: [name,other]' } tests.each do |name, group_name| diff --git a/spec/models/concerns/ci/artifactable_spec.rb b/spec/models/concerns/ci/artifactable_spec.rb index 13c2ff5efe5..f05189abdd2 100644 --- a/spec/models/concerns/ci/artifactable_spec.rb +++ b/spec/models/concerns/ci/artifactable_spec.rb @@ -18,4 +18,40 @@ RSpec.describe Ci::Artifactable do it { is_expected.to be_const_defined(:FILE_FORMAT_ADAPTERS) } end end + + describe '#each_blob' do + context 'when file format is gzip' do + context 'when gzip file contains one file' do + let(:artifact) { build(:ci_job_artifact, :junit) } + + it 'iterates blob once' do + expect { |b| artifact.each_blob(&b) }.to yield_control.once + end + end + + context 'when gzip file contains three files' do + let(:artifact) { build(:ci_job_artifact, :junit_with_three_testsuites) } + + it 'iterates blob three times' do + expect { |b| artifact.each_blob(&b) }.to yield_control.exactly(3).times + end + end + end + + context 'when file format is raw' do + let(:artifact) { build(:ci_job_artifact, :codequality, file_format: :raw) } + + it 'iterates blob once' do + expect { |b| artifact.each_blob(&b) }.to yield_control.once + end + end + + context 'when there are no adapters for the file format' do + let(:artifact) { build(:ci_job_artifact, :junit, file_format: :zip) } + + it 'raises an error' do + expect { |b| artifact.each_blob(&b) }.to raise_error(described_class::NotSupportedAdapterError) + end + end + end end diff --git a/spec/models/concerns/from_except_spec.rb b/spec/models/concerns/from_except_spec.rb new file mode 100644 index 00000000000..3b081b4f572 --- /dev/null +++ b/spec/models/concerns/from_except_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe FromExcept do + it_behaves_like 'from set operator', Gitlab::SQL::Except +end diff --git a/spec/models/concerns/from_intersect_spec.rb b/spec/models/concerns/from_intersect_spec.rb new file mode 100644 index 00000000000..78884f8ae56 --- /dev/null +++ b/spec/models/concerns/from_intersect_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe FromIntersect do + it_behaves_like 'from set operator', Gitlab::SQL::Intersect +end diff --git a/spec/models/concerns/from_set_operator_spec.rb b/spec/models/concerns/from_set_operator_spec.rb new file mode 100644 index 00000000000..8ebbb5550c9 --- /dev/null +++ b/spec/models/concerns/from_set_operator_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe FromSetOperator do + describe 'when set operator method already exists' do + let(:redefine_method) do + Class.new do + def self.from_union + # This method intentionally left blank. + end + + extend FromSetOperator + define_set_operator Gitlab::SQL::Union + end + end + + it { expect { redefine_method }.to raise_exception(RuntimeError) } + end +end diff --git a/spec/models/concerns/from_union_spec.rb b/spec/models/concerns/from_union_spec.rb index 9819a6ec3de..bd2893090a8 100644 --- a/spec/models/concerns/from_union_spec.rb +++ b/spec/models/concerns/from_union_spec.rb @@ -3,38 +3,13 @@ require 'spec_helper' RSpec.describe FromUnion do - describe '.from_union' do - let(:model) do - Class.new(ActiveRecord::Base) do - self.table_name = 'users' - - include FromUnion + [true, false].each do |sql_set_operator| + context "when sql-set-operators feature flag is #{sql_set_operator}" do + before do + stub_feature_flags(sql_set_operators: sql_set_operator) end - end - - it 'selects from the results of the UNION' do - query = model.from_union([model.where(id: 1), model.where(id: 2)]) - - expect(query.to_sql).to match(/FROM \(\(SELECT.+\)\nUNION\n\(SELECT.+\)\) users/m) - end - - it 'supports the use of a custom alias for the sub query' do - query = model.from_union( - [model.where(id: 1), model.where(id: 2)], - alias_as: 'kittens' - ) - - expect(query.to_sql).to match(/FROM \(\(SELECT.+\)\nUNION\n\(SELECT.+\)\) kittens/m) - end - - it 'supports keeping duplicate rows' do - query = model.from_union( - [model.where(id: 1), model.where(id: 2)], - remove_duplicates: false - ) - expect(query.to_sql) - .to match(/FROM \(\(SELECT.+\)\nUNION ALL\n\(SELECT.+\)\) users/m) + it_behaves_like 'from set operator', Gitlab::SQL::Union end end end diff --git a/spec/models/concerns/id_in_ordered_spec.rb b/spec/models/concerns/id_in_ordered_spec.rb new file mode 100644 index 00000000000..a3b434caac6 --- /dev/null +++ b/spec/models/concerns/id_in_ordered_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe IdInOrdered do + describe 'Issue' do + describe '.id_in_ordered' do + it 'returns issues matching the ids in the same order as the ids' do + issue1 = create(:issue) + issue2 = create(:issue) + issue3 = create(:issue) + issue4 = create(:issue) + issue5 = create(:issue) + + expect(Issue.id_in_ordered([issue3.id, issue1.id, issue4.id, issue5.id, issue2.id])).to eq([ + issue3, issue1, issue4, issue5, issue2 + ]) + end + + context 'when the ids are not an array of integers' do + it 'raises ArgumentError' do + expect { Issue.id_in_ordered([1, 'no SQL injection']) }.to raise_error(ArgumentError, "ids must be an array of integers") + end + end + + context 'when an empty array is given' do + it 'does not raise error' do + expect(Issue.id_in_ordered([]).to_a).to be_empty + end + end + end + end +end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 0824b5c7834..44561e2e55a 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -295,20 +295,14 @@ RSpec.describe Issuable do end describe "#new?" do - it "returns true when created today and record hasn't been updated" do - allow(issue).to receive(:today?).and_return(true) - expect(issue.new?).to be_truthy - end - - it "returns false when not created today" do - allow(issue).to receive(:today?).and_return(false) + it "returns false when created 30 hours ago" do + allow(issue).to receive(:created_at).and_return(Time.current - 30.hours) expect(issue.new?).to be_falsey end - it "returns false when record has been updated" do - allow(issue).to receive(:today?).and_return(true) - issue.update_attribute(:updated_at, 1.hour.ago) - expect(issue.new?).to be_falsey + it "returns true when created 20 hours ago" do + allow(issue).to receive(:created_at).and_return(Time.current - 20.hours) + expect(issue.new?).to be_truthy end end @@ -824,4 +818,166 @@ RSpec.describe Issuable do it_behaves_like 'matches_cross_reference_regex? fails fast' end end + + describe '#supports_time_tracking?' do + using RSpec::Parameterized::TableSyntax + + where(:issuable_type, :supports_time_tracking) do + :issue | true + :incident | false + :merge_request | true + end + + with_them do + let(:issuable) { build_stubbed(issuable_type) } + + subject { issuable.supports_time_tracking? } + + it { is_expected.to eq(supports_time_tracking) } + end + end + + describe '#supports_severity?' do + using RSpec::Parameterized::TableSyntax + + where(:issuable_type, :supports_severity) do + :issue | false + :incident | true + :merge_request | false + end + + with_them do + let(:issuable) { build_stubbed(issuable_type) } + + subject { issuable.supports_severity? } + + it { is_expected.to eq(supports_severity) } + end + end + + describe '#incident?' do + using RSpec::Parameterized::TableSyntax + + where(:issuable_type, :incident) do + :issue | false + :incident | true + :merge_request | false + end + + with_them do + let(:issuable) { build_stubbed(issuable_type) } + + subject { issuable.incident? } + + it { is_expected.to eq(incident) } + end + end + + describe '#supports_issue_type?' do + using RSpec::Parameterized::TableSyntax + + where(:issuable_type, :supports_issue_type) do + :issue | true + :merge_request | false + end + + with_them do + let(:issuable) { build_stubbed(issuable_type) } + + subject { issuable.supports_issue_type? } + + it { is_expected.to eq(supports_issue_type) } + end + end + + describe '#severity' do + subject { issuable.severity } + + context 'when issuable is not an incident' do + using RSpec::Parameterized::TableSyntax + + where(:issuable_type, :severity) do + :issue | 'unknown' + :merge_request | 'unknown' + end + + with_them do + let(:issuable) { build_stubbed(issuable_type) } + + it { is_expected.to eq(severity) } + end + end + + context 'when issuable type is an incident' do + let!(:issuable) { build_stubbed(:incident) } + + context 'when incident does not have issuable_severity' do + it 'returns default serverity' do + is_expected.to eq(IssuableSeverity::DEFAULT) + end + end + + context 'when incident has issuable_severity' do + let!(:issuable_severity) { build_stubbed(:issuable_severity, issue: issuable, severity: 'critical') } + + it 'returns issuable serverity' do + is_expected.to eq('critical') + end + end + end + end + + describe '#update_severity' do + let(:severity) { 'low' } + + subject(:update_severity) { issuable.update_severity(severity) } + + context 'when issuable not an incident' do + %i(issue merge_request).each do |issuable_type| + let(:issuable) { build_stubbed(issuable_type) } + + it { is_expected.to be_nil } + + it 'does not set severity' do + expect { subject }.not_to change(IssuableSeverity, :count) + end + end + end + + context 'when issuable is an incident' do + let!(:issuable) { create(:incident) } + + context 'when issuable does not have issuable severity yet' do + it 'creates new record' do + expect { update_severity }.to change { IssuableSeverity.where(issue: issuable).count }.to(1) + end + + it 'sets severity to specified value' do + expect { update_severity }.to change { issuable.severity }.to('low') + end + end + + context 'when issuable has an issuable severity' do + let!(:issuable_severity) { create(:issuable_severity, issue: issuable, severity: 'medium') } + + it 'does not create new record' do + expect { update_severity }.not_to change(IssuableSeverity, :count) + end + + it 'updates existing issuable severity' do + expect { update_severity }.to change { issuable_severity.severity }.to(severity) + end + end + + context 'when severity value is unsupported' do + let(:severity) { 'unsupported-severity' } + + it 'sets the severity to default value' do + update_severity + + expect(issuable.issuable_severity.severity).to eq(IssuableSeverity::DEFAULT) + end + end + end + end end diff --git a/spec/models/concerns/milestoneable_spec.rb b/spec/models/concerns/milestoneable_spec.rb index 3dd6f1450c7..f5b82e42ad4 100644 --- a/spec/models/concerns/milestoneable_spec.rb +++ b/spec/models/concerns/milestoneable_spec.rb @@ -100,6 +100,14 @@ RSpec.describe Milestoneable do expect(merge_request.supports_milestone?).to be_truthy end end + + context "for incidents" do + let(:incident) { build(:incident) } + + it 'returns false' do + expect(incident.supports_milestone?).to be_falsy + end + end end describe 'release scopes' do diff --git a/spec/models/concerns/prometheus_adapter_spec.rb b/spec/models/concerns/prometheus_adapter_spec.rb index e795e2b06cb..235e505c6e9 100644 --- a/spec/models/concerns/prometheus_adapter_spec.rb +++ b/spec/models/concerns/prometheus_adapter_spec.rb @@ -25,7 +25,7 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do let(:validation_respone) { { data: { valid: true } } } around do |example| - Timecop.freeze { example.run } + freeze_time { example.run } end context 'with valid data' do @@ -45,7 +45,7 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do let(:environment) { build_stubbed(:environment, slug: 'env-slug') } around do |example| - Timecop.freeze { example.run } + freeze_time { example.run } end context 'with valid data' do @@ -85,7 +85,7 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do let(:deployment_query) { Gitlab::Prometheus::Queries::DeploymentQuery } around do |example| - Timecop.freeze { example.run } + freeze_time { example.run } end context 'with valid data' do @@ -107,7 +107,7 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do let(:time_window) { [1552642245.067, 1552642095.831] } around do |example| - Timecop.freeze { example.run } + freeze_time { example.run } end context 'with valid data' do @@ -137,7 +137,7 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do end around do |example| - Timecop.freeze { example.run } + freeze_time { example.run } end context 'when service is inactive' do diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index 9372ef5f0e6..5857365ceab 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -15,17 +15,17 @@ RSpec.describe 'CycleAnalytics#issue' do generate_cycle_analytics_spec( phase: :issue, data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } }, - start_time_conditions: [["issue created", -> (context, data) { data[:issue].save }]], + start_time_conditions: [["issue created", -> (context, data) { data[:issue].save! }]], end_time_conditions: [["issue associated with a milestone", -> (context, data) do if data[:issue].persisted? - data[:issue].update(milestone: context.create(:milestone, project: context.project)) + data[:issue].update!(milestone: context.create(:milestone, project: context.project)) end end], ["list label added to issue", -> (context, data) do if data[:issue].persisted? - data[:issue].update(label_ids: [context.create(:list).label_id]) + data[:issue].update!(label_ids: [context.create(:list).label_id]) end end]], post_fn: -> (context, data) do @@ -35,7 +35,7 @@ RSpec.describe 'CycleAnalytics#issue' do it "returns nil" do regular_label = create(:label) issue = create(:issue, project: project) - issue.update(label_ids: [regular_label.id]) + issue.update!(label_ids: [regular_label.id]) create_merge_request_closing_issue(user, project, issue) merge_merge_requests_closing_issue(user, project, issue) diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb index 364694a11e1..2b9be64da2b 100644 --- a/spec/models/cycle_analytics/plan_spec.rb +++ b/spec/models/cycle_analytics/plan_spec.rb @@ -22,11 +22,11 @@ RSpec.describe 'CycleAnalytics#plan' do end, start_time_conditions: [["issue associated with a milestone", -> (context, data) do - data[:issue].update(milestone: context.create(:milestone, project: context.project)) + data[:issue].update!(milestone: context.create(:milestone, project: context.project)) end], ["list label added to issue", -> (context, data) do - data[:issue].update(label_ids: [context.create(:list).label_id]) + data[:issue].update!(label_ids: [context.create(:list).label_id]) end]], end_time_conditions: [["issue mentioned in a commit", -> (context, data) do @@ -40,7 +40,7 @@ RSpec.describe 'CycleAnalytics#plan' do branch_name = generate(:branch) label = create(:label) issue = create(:issue, project: project) - issue.update(label_ids: [label.id]) + issue.update!(label_ids: [label.id]) create_commit_referencing_issue(issue, branch_name: branch_name) create_merge_request_closing_issue(user, project, issue, source_branch: branch_name) diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb deleted file mode 100644 index cf4d57d6b73..00000000000 --- a/spec/models/cycle_analytics/production_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'CycleAnalytics#production' do - extend CycleAnalyticsHelpers::TestGeneration - - let_it_be(:project) { create(:project, :repository) } - let_it_be(:from_date) { 10.days.ago } - let_it_be(:user) { project.owner } - let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) } - - subject { project_level } - - generate_cycle_analytics_spec( - phase: :production, - data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } }, - start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save }]], - before_end_fn: lambda do |context, data| - context.create_merge_request_closing_issue(context.user, context.project, data[:issue]) - context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue]) - end, - end_time_conditions: - [["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master(context.user, context.project) }], - ["production deploy happens after merge request is merged (along with other changes)", - lambda do |context, data| - # Make other changes on master - context.project.repository.commit("sha_that_does_not_matter") - - context.deploy_master(context.user, context.project) - end]]) - - context "when a regular merge request (that doesn't close the issue) is merged and deployed" do - it "returns nil" do - merge_request = create(:merge_request) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(user, project) - - expect(subject[:production].project_median).to be_nil - end - end - - context "when the deployment happens to a non-production environment" do - it "returns nil" do - issue = build(:issue, project: project) - merge_request = create_merge_request_closing_issue(user, project, issue) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(user, project, environment: 'staging') - - expect(subject[:production].project_median).to be_nil - end - end -end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index b320390711e..1c7b11257ce 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -44,10 +44,14 @@ RSpec.describe Deployment do describe 'modules' do it_behaves_like 'AtomicInternalId' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:deployable) { create(:ci_build, project: project) } + let_it_be(:environment) { create(:environment, project: project) } + let(:internal_id_attribute) { :iid } - let(:instance) { build(:deployment) } + let(:instance) { build(:deployment, deployable: deployable, environment: environment) } let(:scope) { :project } - let(:scope_attrs) { { project: instance.project } } + let(:scope_attrs) { { project: project } } let(:usage) { :deployments } end end @@ -99,7 +103,7 @@ RSpec.describe Deployment do end it 'starts running' do - Timecop.freeze do + freeze_time do expect(deployment).to be_running expect(deployment.finished_at).to be_nil end @@ -110,7 +114,7 @@ RSpec.describe Deployment do let(:deployment) { create(:deployment, :running) } it 'has correct status' do - Timecop.freeze do + freeze_time do deployment.succeed! expect(deployment).to be_success @@ -137,7 +141,7 @@ RSpec.describe Deployment do let(:deployment) { create(:deployment, :running) } it 'has correct status' do - Timecop.freeze do + freeze_time do deployment.drop! expect(deployment).to be_failed @@ -157,7 +161,7 @@ RSpec.describe Deployment do let(:deployment) { create(:deployment, :running) } it 'has correct status' do - Timecop.freeze do + freeze_time do deployment.cancel! expect(deployment).to be_canceled @@ -584,7 +588,7 @@ RSpec.describe Deployment do end it 'updates finished_at when transitioning to a finished status' do - Timecop.freeze do + freeze_time do deploy.update_status('success') expect(deploy.read_attribute(:finished_at)).to eq(Time.current) diff --git a/spec/models/design_management/design_collection_spec.rb b/spec/models/design_management/design_collection_spec.rb index de766d5ce09..8575cc80b5b 100644 --- a/spec/models/design_management/design_collection_spec.rb +++ b/spec/models/design_management/design_collection_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' RSpec.describe DesignManagement::DesignCollection do include DesignManagementTestHelpers + using RSpec::Parameterized::TableSyntax - let_it_be(:issue, reload: true) { create(:issue) } + let_it_be(:issue, refind: true) { create(:issue) } subject(:collection) { described_class.new(issue) } @@ -41,7 +42,62 @@ RSpec.describe DesignManagement::DesignCollection do design2 = collection.find_or_create_design!(filename: 'design2.jpg') - expect(collection.designs.ordered(issue.project)).to eq([design1, design2]) + expect(collection.designs.ordered).to eq([design1, design2]) + end + end + + describe "#copy_state", :clean_gitlab_redis_shared_state do + it "defaults to ready" do + expect(collection).to be_copy_ready + end + + it "persists its state changes between initializations" do + collection.start_copy! + + expect(described_class.new(issue)).to be_copy_in_progress + end + + where(:state, :can_start, :can_end, :can_error, :can_reset) do + "ready" | true | false | true | true + "in_progress" | false | true | true | true + "error" | false | false | false | true + end + + with_them do + it "maintains state machine transition rules", :aggregate_failures do + collection.copy_state = state + + expect(collection.can_start_copy?).to eq(can_start) + expect(collection.can_end_copy?).to eq(can_end) + end + end + + describe "clearing the redis cached state when state changes back to ready" do + def redis_copy_state + Gitlab::Redis::SharedState.with do |redis| + redis.get(collection.send(:copy_state_cache_key)) + end + end + + def fire_state_events(*events) + events.each do |event| + collection.fire_copy_state_event(event) + end + end + + it "clears the cached state on end_copy!", :aggregate_failures do + fire_state_events(:start) + + expect { collection.end_copy! }.to change { redis_copy_state }.from("in_progress").to(nil) + expect(collection).to be_copy_ready + end + + it "clears the cached state on reset_copy!", :aggregate_failures do + fire_state_events(:start, :error) + + expect { collection.reset_copy! }.to change { redis_copy_state }.from("error").to(nil) + expect(collection).to be_copy_ready + end end end diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb index 2c129f883b9..d4adc0d42d0 100644 --- a/spec/models/design_management/design_spec.rb +++ b/spec/models/design_management/design_spec.rb @@ -12,8 +12,10 @@ RSpec.describe DesignManagement::Design do let_it_be(:deleted_design) { create(:design, :with_versions, deleted: true) } it_behaves_like 'a class that supports relative positioning' do + let_it_be(:relative_parent) { create(:issue) } + let(:factory) { :design } - let(:default_params) { { issue: issue } } + let(:default_params) { { issue: relative_parent } } end describe 'relations' do @@ -161,27 +163,7 @@ RSpec.describe DesignManagement::Design do end it 'sorts by relative position and ID in ascending order' do - expect(described_class.ordered(issue.project)).to eq([design2, design1, design3, deleted_design]) - end - - context 'when the :reorder_designs feature is enabled for the project' do - before do - stub_feature_flags(reorder_designs: issue.project) - end - - it 'sorts by relative position and ID in ascending order' do - expect(described_class.ordered(issue.project)).to eq([design2, design1, design3, deleted_design]) - end - end - - context 'when the :reorder_designs feature is disabled' do - before do - stub_feature_flags(reorder_designs: false) - end - - it 'sorts by ID in ascending order' do - expect(described_class.ordered(issue.project)).to eq([design1, design2, design3, deleted_design]) - end + expect(described_class.ordered).to eq([design2, design1, design3, deleted_design]) end end diff --git a/spec/models/dev_ops_report/metric_spec.rb b/spec/models/dev_ops_report/metric_spec.rb new file mode 100644 index 00000000000..191692f43a4 --- /dev/null +++ b/spec/models/dev_ops_report/metric_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe DevOpsReport::Metric do + let(:conv_dev_index) { create(:dev_ops_report_metric) } + + describe '#percentage_score' do + it 'returns stored percentage score' do + expect(conv_dev_index.percentage_score('issues')).to eq(13.331) + end + end +end diff --git a/spec/models/dev_ops_score/metric_spec.rb b/spec/models/dev_ops_score/metric_spec.rb deleted file mode 100644 index 60001d0667d..00000000000 --- a/spec/models/dev_ops_score/metric_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe DevOpsScore::Metric do - let(:conv_dev_index) { create(:dev_ops_score_metric) } - - describe '#percentage_score' do - it 'returns stored percentage score' do - expect(conv_dev_index.percentage_score('issues')).to eq(13.331) - end - end -end diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index 8a6176bf045..f7ce44f7281 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -35,7 +35,9 @@ RSpec.describe DiffNote do subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } describe 'validations' do - it_behaves_like 'a valid diff positionable note', :diff_note_on_commit + it_behaves_like 'a valid diff positionable note' do + subject { build(:diff_note_on_commit, project: project, commit_id: commit_id, position: position) } + end end describe "#position=" do diff --git a/spec/models/draft_note_spec.rb b/spec/models/draft_note_spec.rb index 64b06bf5c8f..580a588ae1d 100644 --- a/spec/models/draft_note_spec.rb +++ b/spec/models/draft_note_spec.rb @@ -5,11 +5,13 @@ require 'spec_helper' RSpec.describe DraftNote do include RepoHelpers - let(:project) { create(:project, :repository) } - let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } describe 'validations' do - it_behaves_like 'a valid diff positionable note', :draft_note + it_behaves_like 'a valid diff positionable note' do + subject { build(:draft_note, merge_request: merge_request, commit_id: commit_id, position: position) } + end end describe 'delegations' do diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 2696d144db4..433ede97b82 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -854,8 +854,8 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do end it 'overrides reactive_cache_limit_enabled? with a FF' do - environment_with_enabled_ff = FactoryBot.build(:environment) - environment_with_disabled_ff = FactoryBot.build(:environment) + environment_with_enabled_ff = build(:environment, project: create(:project)) + environment_with_disabled_ff = build(:environment, project: create(:project)) stub_feature_flags(reactive_caching_limit_environment: environment_with_enabled_ff.project) @@ -1222,7 +1222,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do let(:environment) { build(:environment, :will_auto_stop) } it 'returns when it will expire' do - Timecop.freeze { is_expected.to eq(1.day.to_i) } + freeze_time { is_expected.to eq(1.day.to_i) } end end @@ -1248,7 +1248,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do end with_them do it 'sets correct auto_stop_in' do - Timecop.freeze do + freeze_time do if expected_result.is_a?(Integer) || expected_result.nil? subject diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb index 7eefb8f714a..a6954fb5d56 100644 --- a/spec/models/environment_status_spec.rb +++ b/spec/models/environment_status_spec.rb @@ -90,22 +90,6 @@ RSpec.describe EnvironmentStatus do end end - describe '.after_merge_request' do - let(:admin) { create(:admin) } - let(:pipeline) { create(:ci_pipeline, sha: sha) } - - before do - merge_request.mark_as_merged! - end - - it 'is based on merge_request.merge_commit_sha' do - expect(merge_request).to receive(:merge_commit_sha) - expect(merge_request).not_to receive(:diff_head_sha) - - described_class.after_merge_request(merge_request, admin) - end - end - describe '.for_deployed_merge_request' do context 'when a merge request has no explicitly linked deployments' do it 'returns the statuses based on the CI pipelines' do @@ -191,7 +175,7 @@ RSpec.describe EnvironmentStatus do let(:environment) { build.deployment.environment } let(:user) { project.owner } - context 'when environment is created on a forked project' do + context 'when environment is created on a forked project', :sidekiq_inline do let(:project) { create(:project, :repository) } let(:forked) { fork_project(project, user, repository: true) } let(:sha) { forked.commit.sha } @@ -205,7 +189,7 @@ RSpec.describe EnvironmentStatus do head_pipeline: pipeline) end - it 'returns environment status', :sidekiq_might_not_need_inline do + it 'returns environment status' do expect(subject.count).to eq(1) expect(subject[0].environment).to eq(environment) expect(subject[0].merge_request).to eq(merge_request) diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 015a86cb28b..bafcb7a3741 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -81,6 +81,8 @@ RSpec.describe Event do describe 'validations' do describe 'action' do context 'for a design' do + let_it_be(:author) { create(:user) } + where(:action, :valid) do valid = described_class::DESIGN_ACTIONS.map(&:to_s).to_set @@ -90,7 +92,7 @@ RSpec.describe Event do end with_them do - let(:event) { build(:design_event, action: action) } + let(:event) { build(:design_event, author: author, action: action) } specify { expect(event.valid?).to eq(valid) } end @@ -722,9 +724,17 @@ RSpec.describe Event do note_on_commit: true } valid_target_factories.map do |kind, needs_project| - extra_data = needs_project ? { project: project } : {} + extra_data = if kind == :merge_request + { source_project: project } + elsif needs_project + { project: project } + else + {} + end + target = kind == :project ? nil : build(kind, **extra_data) - [kind, build(:event, :created, project: project, target: target)] + + [kind, build(:event, :created, author: project.owner, project: project, target: target)] end.to_h end diff --git a/spec/models/group_deploy_key_spec.rb b/spec/models/group_deploy_key_spec.rb index 6757c5534ce..dfb4fee593f 100644 --- a/spec/models/group_deploy_key_spec.rb +++ b/spec/models/group_deploy_key_spec.rb @@ -82,4 +82,25 @@ RSpec.describe GroupDeployKey do end end end + + describe '.for_groups' do + context 'when group deploy keys are enabled for some groups' do + let_it_be(:group1) { create(:group) } + let_it_be(:group2) { create(:group) } + let_it_be(:group3) { create(:group) } + let_it_be(:gdk1) { create(:group_deploy_key) } + let_it_be(:gdk2) { create(:group_deploy_key) } + let_it_be(:gdk3) { create(:group_deploy_key) } + + it 'returns these group deploy keys' do + gdk1.groups << group1 + gdk1.groups << group2 + gdk2.groups << group3 + gdk3.groups << group2 + + expect(described_class.for_groups([group1.id, group3.id])).to contain_exactly(gdk1, gdk2) + expect(described_class.for_groups([group2.id])).to contain_exactly(gdk1, gdk3) + end + end + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 3eb74da09e1..15972f66fd6 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -27,6 +27,7 @@ RSpec.describe Group do it { is_expected.to have_many(:milestones) } it { is_expected.to have_many(:iterations) } it { is_expected.to have_many(:group_deploy_keys) } + it { is_expected.to have_many(:services) } describe '#members & #requesters' do let(:requester) { create(:user) } @@ -652,6 +653,19 @@ RSpec.describe Group do expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::MAINTAINER) end end + + context 'evaluating admin access level' do + let_it_be(:admin) { create(:admin) } + + it 'returns OWNER by default' do + expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::OWNER) + end + + it 'returns NO_ACCESS when only concrete membership should be considered' do + expect(group.max_member_access_for_user(admin, only_concrete_membership: true)) + .to eq(Gitlab::Access::NO_ACCESS) + end + end end describe '#members_with_parents' do @@ -692,6 +706,7 @@ RSpec.describe Group do before do create(:group_member, user: user, group: group_parent, access_level: parent_group_access_level) create(:group_member, user: user, group: group, access_level: group_access_level) + create(:group_member, :minimal_access, user: create(:user), source: group) create(:group_member, user: user, group: group_child, access_level: child_group_access_level) end diff --git a/spec/models/issuable_severity_spec.rb b/spec/models/issuable_severity_spec.rb new file mode 100644 index 00000000000..4dfd19756cb --- /dev/null +++ b/spec/models/issuable_severity_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe IssuableSeverity, type: :model do + let_it_be(:issuable_severity) { create(:issuable_severity) } + + describe 'associations' do + it { is_expected.to belong_to(:issue) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:severity) } + it { is_expected.to validate_presence_of(:issue) } + it { is_expected.to validate_uniqueness_of(:issue) } + end + + describe 'enums' do + let(:severity_values) do + { unknown: 0, low: 1, medium: 2, high: 3, critical: 4 } + end + + it { is_expected.to define_enum_for(:severity).with_values(severity_values) } + end +end diff --git a/spec/models/issue_link_spec.rb b/spec/models/issue_link_spec.rb new file mode 100644 index 00000000000..00791d4a48b --- /dev/null +++ b/spec/models/issue_link_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe IssueLink do + describe 'Associations' do + it { is_expected.to belong_to(:source).class_name('Issue') } + it { is_expected.to belong_to(:target).class_name('Issue') } + end + + describe 'link_type' do + it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1, is_blocked_by: 2) } + + it 'provides the "related" as default link_type' do + expect(create(:issue_link).link_type).to eq 'relates_to' + end + end + + describe 'Validation' do + subject { create :issue_link } + + it { is_expected.to validate_presence_of(:source) } + it { is_expected.to validate_presence_of(:target) } + it do + is_expected.to validate_uniqueness_of(:source) + .scoped_to(:target_id) + .with_message(/already related/) + end + + context 'self relation' do + let(:issue) { create :issue } + + context 'cannot be validated' do + it 'does not invalidate object with self relation error' do + issue_link = build :issue_link, source: issue, target: nil + + issue_link.valid? + + expect(issue_link.errors[:source]).to be_empty + end + end + + context 'can be invalidated' do + it 'invalidates object' do + issue_link = build :issue_link, source: issue, target: issue + + expect(issue_link).to be_invalid + expect(issue_link.errors[:source]).to include('cannot be related to itself') + end + end + end + end +end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 59634524e74..283d945157b 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -133,6 +133,7 @@ RSpec.describe Issue do let_it_be(:project) { create(:project) } let_it_be(:issue) { create(:issue, project: project) } let_it_be(:incident) { create(:incident, project: project) } + let_it_be(:test_case) { create(:quality_test_case, project: project) } it 'gives issues with the given issue type' do expect(described_class.with_issue_type('issue')) @@ -140,8 +141,8 @@ RSpec.describe Issue do end it 'gives issues with the given issue type' do - expect(described_class.with_issue_type(%w(issue incident))) - .to contain_exactly(issue, incident) + expect(described_class.with_issue_type(%w(issue incident test_case))) + .to contain_exactly(issue, incident, test_case) end end @@ -311,6 +312,50 @@ RSpec.describe Issue do end end + describe '#related_issues' do + let(:user) { create(:user) } + let(:authorized_project) { create(:project) } + let(:authorized_project2) { create(:project) } + let(:unauthorized_project) { create(:project) } + + let(:authorized_issue_a) { create(:issue, project: authorized_project) } + let(:authorized_issue_b) { create(:issue, project: authorized_project) } + let(:authorized_issue_c) { create(:issue, project: authorized_project2) } + + let(:unauthorized_issue) { create(:issue, project: unauthorized_project) } + + let!(:issue_link_a) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_b) } + let!(:issue_link_b) { create(:issue_link, source: authorized_issue_a, target: unauthorized_issue) } + let!(:issue_link_c) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_c) } + + before do + authorized_project.add_developer(user) + authorized_project2.add_developer(user) + end + + it 'returns only authorized related issues for given user' do + expect(authorized_issue_a.related_issues(user)) + .to contain_exactly(authorized_issue_b, authorized_issue_c) + end + + it 'returns issues with valid issue_link_type' do + link_types = authorized_issue_a.related_issues(user).map(&:issue_link_type) + + expect(link_types).not_to be_empty + expect(link_types).not_to include(nil) + end + + describe 'when a user cannot read cross project' do + it 'only returns issues within the same project' do + expect(Ability).to receive(:allowed?).with(user, :read_all_resources, :global).at_least(:once).and_call_original + expect(Ability).to receive(:allowed?).with(user, :read_cross_project).and_return(false) + + expect(authorized_issue_a.related_issues(user)) + .to contain_exactly(authorized_issue_b) + end + end + end + describe '#can_move?' do let(:issue) { create(:issue) } @@ -1139,4 +1184,33 @@ RSpec.describe Issue do expect(context[:label_url_method]).to eq(:project_issues_url) end end + + describe 'scheduling rebalancing' do + before do + allow_next_instance_of(RelativePositioning::Mover) do |mover| + allow(mover).to receive(:move) { raise ActiveRecord::QueryCanceled } + end + end + + let(:project) { build_stubbed(:project_empty_repo) } + let(:issue) { build_stubbed(:issue, relative_position: 100, project: project) } + + it 'schedules rebalancing if we time-out when moving' do + lhs = build_stubbed(:issue, relative_position: 99, project: project) + to_move = build(:issue, project: project) + expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id) + + expect { to_move.move_between(lhs, issue) }.to raise_error(ActiveRecord::QueryCanceled) + end + end + + describe '#allows_reviewers?' do + it 'returns false as issues do not support reviewers feature' do + stub_feature_flags(merge_request_reviewers: true) + + issue = build_stubbed(:issue) + + expect(issue.allows_reviewers?).to be(false) + end + end end diff --git a/spec/models/iteration_spec.rb b/spec/models/iteration_spec.rb index 5c684fa9771..19a1625aad3 100644 --- a/spec/models/iteration_spec.rb +++ b/spec/models/iteration_spec.rb @@ -32,6 +32,59 @@ RSpec.describe Iteration do end end + describe '.filter_by_state' do + let_it_be(:closed_iteration) { create(:iteration, :closed, :skip_future_date_validation, group: group, start_date: 8.days.ago, due_date: 2.days.ago) } + let_it_be(:started_iteration) { create(:iteration, :started, :skip_future_date_validation, group: group, start_date: 1.day.ago, due_date: 6.days.from_now) } + let_it_be(:upcoming_iteration) { create(:iteration, :upcoming, group: group, start_date: 1.week.from_now, due_date: 2.weeks.from_now) } + + shared_examples_for 'filter_by_state' do + it 'filters by the given state' do + expect(described_class.filter_by_state(Iteration.all, state)).to match(expected_iterations) + end + end + + context 'filtering by closed iterations' do + it_behaves_like 'filter_by_state' do + let(:state) { 'closed' } + let(:expected_iterations) { [closed_iteration] } + end + end + + context 'filtering by started iterations' do + it_behaves_like 'filter_by_state' do + let(:state) { 'started' } + let(:expected_iterations) { [started_iteration] } + end + end + + context 'filtering by opened iterations' do + it_behaves_like 'filter_by_state' do + let(:state) { 'opened' } + let(:expected_iterations) { [started_iteration, upcoming_iteration] } + end + end + + context 'filtering by upcoming iterations' do + it_behaves_like 'filter_by_state' do + let(:state) { 'upcoming' } + let(:expected_iterations) { [upcoming_iteration] } + end + end + + context 'filtering by "all"' do + it_behaves_like 'filter_by_state' do + let(:state) { 'all' } + let(:expected_iterations) { [closed_iteration, started_iteration, upcoming_iteration] } + end + end + + context 'filtering by nonexistent filter' do + it 'raises ArgumentError' do + expect { described_class.filter_by_state(Iteration.none, 'unknown') }.to raise_error(ArgumentError, 'Unknown state filter: unknown') + end + end + end + context 'Validations' do subject { build(:iteration, group: group, start_date: start_date, due_date: due_date) } diff --git a/spec/models/jira_connect_installation_spec.rb b/spec/models/jira_connect_installation_spec.rb new file mode 100644 index 00000000000..8ef96114c45 --- /dev/null +++ b/spec/models/jira_connect_installation_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe JiraConnectInstallation do + describe 'associations' do + it { is_expected.to have_many(:subscriptions).class_name('JiraConnectSubscription') } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:client_key) } + it { is_expected.to validate_uniqueness_of(:client_key) } + it { is_expected.to validate_presence_of(:shared_secret) } + it { is_expected.to validate_presence_of(:base_url) } + + it { is_expected.to allow_value('https://test.atlassian.net').for(:base_url) } + it { is_expected.not_to allow_value('not/a/url').for(:base_url) } + end + + describe '.for_project' do + let(:other_group) { create(:group) } + let(:parent_group) { create(:group) } + let(:group) { create(:group, parent: parent_group) } + let(:project) { create(:project, group: group) } + + subject { described_class.for_project(project) } + + it 'returns installations with subscriptions for project' do + sub_on_project_namespace = create(:jira_connect_subscription, namespace: group) + sub_on_ancestor_namespace = create(:jira_connect_subscription, namespace: parent_group) + + # Subscription on other group that shouldn't be returned + create(:jira_connect_subscription, namespace: other_group) + + expect(subject).to contain_exactly(sub_on_project_namespace.installation, sub_on_ancestor_namespace.installation) + end + + it 'returns distinct installations' do + subscription = create(:jira_connect_subscription, namespace: group) + create(:jira_connect_subscription, namespace: parent_group, installation: subscription.installation) + + expect(subject).to contain_exactly(subscription.installation) + end + end +end diff --git a/spec/models/jira_connect_subscription_spec.rb b/spec/models/jira_connect_subscription_spec.rb new file mode 100644 index 00000000000..548c030f4c4 --- /dev/null +++ b/spec/models/jira_connect_subscription_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe JiraConnectSubscription do + describe 'associations' do + it { is_expected.to belong_to(:installation).class_name('JiraConnectInstallation') } + it { is_expected.to belong_to(:namespace).class_name('Namespace') } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:installation) } + it { is_expected.to validate_presence_of(:namespace) } + end +end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 52f47f0a211..39807747cc0 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -16,14 +16,14 @@ RSpec.describe Member do it { is_expected.to validate_presence_of(:user) } it { is_expected.to validate_presence_of(:source) } - it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.all_values) } it_behaves_like 'an object with email-formated attributes', :invite_email do subject { build(:project_member) } end context "when an invite email is provided" do - let(:member) { build(:project_member, invite_email: "user@example.com", user: nil) } + let_it_be(:project) { create(:project) } + let(:member) { build(:project_member, source: project, invite_email: "user@example.com", user: nil) } it "doesn't require a user" do expect(member).to be_valid @@ -149,6 +149,7 @@ RSpec.describe Member do accepted_request_user = create(:user).tap { |u| project.request_access(u) } @accepted_request_member = project.requesters.find_by(user_id: accepted_request_user.id).tap { |m| m.accept_request } + @member_with_minimal_access = create(:group_member, :minimal_access, source: group) end describe '.access_for_user_ids' do @@ -179,6 +180,15 @@ RSpec.describe Member do it { expect(described_class.non_invite).to include @accepted_request_member } end + describe '.non_minimal_access' do + it { expect(described_class.non_minimal_access).to include @maintainer } + it { expect(described_class.non_minimal_access).to include @invited_member } + it { expect(described_class.non_minimal_access).to include @accepted_invite_member } + it { expect(described_class.non_minimal_access).to include @requested_member } + it { expect(described_class.non_minimal_access).to include @accepted_request_member } + it { expect(described_class.non_minimal_access).not_to include @member_with_minimal_access } + end + describe '.request' do it { expect(described_class.request).not_to include @maintainer } it { expect(described_class.request).not_to include @invited_member } @@ -256,6 +266,34 @@ RSpec.describe Member do it { is_expected.not_to include @blocked_maintainer } it { is_expected.not_to include @blocked_developer } end + + describe '.active' do + subject { described_class.active.to_a } + + it { is_expected.to include @owner } + it { is_expected.to include @maintainer } + it { is_expected.to include @invited_member } + it { is_expected.to include @accepted_invite_member } + it { is_expected.not_to include @requested_member } + it { is_expected.to include @accepted_request_member } + it { is_expected.not_to include @blocked_maintainer } + it { is_expected.not_to include @blocked_developer } + it { is_expected.not_to include @member_with_minimal_access } + end + + describe '.active_without_invites_and_requests' do + subject { described_class.active_without_invites_and_requests.to_a } + + it { is_expected.to include @owner } + it { is_expected.to include @maintainer } + it { is_expected.not_to include @invited_member } + it { is_expected.to include @accepted_invite_member } + it { is_expected.not_to include @requested_member } + it { is_expected.to include @accepted_request_member } + it { is_expected.not_to include @blocked_maintainer } + it { is_expected.not_to include @blocked_developer } + it { is_expected.not_to include @member_with_minimal_access } + end end describe "Delegate methods" do @@ -630,6 +668,32 @@ RSpec.describe Member do end end + describe '.find_by_invite_token' do + let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) } + + it 'finds the member' do + expect(described_class.find_by_invite_token(member.raw_invite_token)).to eq member + end + end + + describe "#invite_to_unknown_user?" do + subject { member.invite_to_unknown_user? } + + let(:member) { create(:project_member, invite_email: "user@example.com", invite_token: '1234', user: user) } + + context 'when user is nil' do + let(:user) { nil } + + it { is_expected.to eq(true) } + end + + context 'when user is set' do + let(:user) { build(:user) } + + it { is_expected.to eq(false) } + end + end + describe "destroying a record", :delete do it "refreshes user's authorized projects" do project = create(:project, :private) @@ -655,7 +719,7 @@ RSpec.describe Member do describe 'create member' do let!(:source) { create(source_type) } - subject { create(member_type, :guest, user: user, source_type => source) } + subject { create(member_type, :guest, user: user, source: source) } include_examples 'update highest role with exclusive lease' end diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb index 82402b95597..760eaf1ac7f 100644 --- a/spec/models/merge_request/metrics_spec.rb +++ b/spec/models/merge_request/metrics_spec.rb @@ -9,17 +9,6 @@ RSpec.describe MergeRequest::Metrics do it { is_expected.to belong_to(:merged_by).class_name('User') } end - it 'sets `target_project_id` before save' do - merge_request = create(:merge_request) - metrics = merge_request.metrics - - metrics.update_column(:target_project_id, nil) - - metrics.save! - - expect(metrics.target_project_id).to eq(merge_request.target_project_id) - end - describe 'scopes' do let_it_be(:metrics_1) { create(:merge_request).metrics.tap { |m| m.update!(merged_at: 10.days.ago) } } let_it_be(:metrics_2) { create(:merge_request).metrics.tap { |m| m.update!(merged_at: 5.days.ago) } } diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb index 84fdfae1ed1..2edf44ecdc4 100644 --- a/spec/models/merge_request_diff_commit_spec.rb +++ b/spec/models/merge_request_diff_commit_spec.rb @@ -7,7 +7,12 @@ RSpec.describe MergeRequestDiffCommit do let(:project) { merge_request.project } it_behaves_like 'a BulkInsertSafe model', MergeRequestDiffCommit do - let(:valid_items_for_bulk_insertion) { build_list(:merge_request_diff_commit, 10) } + let(:valid_items_for_bulk_insertion) do + build_list(:merge_request_diff_commit, 10) do |mr_diff_commit| + mr_diff_commit.merge_request_diff = create(:merge_request_diff) + end + end + let(:invalid_items_for_bulk_insertion) { [] } # class does not have any validations defined end diff --git a/spec/models/merge_request_diff_file_spec.rb b/spec/models/merge_request_diff_file_spec.rb index 25971f63338..5a48438adab 100644 --- a/spec/models/merge_request_diff_file_spec.rb +++ b/spec/models/merge_request_diff_file_spec.rb @@ -4,7 +4,12 @@ require 'spec_helper' RSpec.describe MergeRequestDiffFile do it_behaves_like 'a BulkInsertSafe model', MergeRequestDiffFile do - let(:valid_items_for_bulk_insertion) { build_list(:merge_request_diff_file, 10) } + let(:valid_items_for_bulk_insertion) do + build_list(:merge_request_diff_file, 10) do |mr_diff_file| + mr_diff_file.merge_request_diff = create(:merge_request_diff) + end + end + let(:invalid_items_for_bulk_insertion) { [] } # class does not have any validations defined end @@ -25,6 +30,14 @@ RSpec.describe MergeRequestDiffFile do it 'unpacks from base 64' do expect(subject.diff).to eq(unpacked) end + + context 'invalid base64' do + let(:packed) { '---/dev/null' } + + it 'returns the raw diff' do + expect(subject.diff).to eq(packed) + end + end end context 'when the diff is not marked as binary' do diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index e02c71a1c6f..2c64201e84d 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -293,6 +293,7 @@ RSpec.describe MergeRequestDiff do it 'does nothing with an empty diff' do stub_external_diffs_setting(enabled: true) MergeRequestDiffFile.where(merge_request_diff_id: diff.id).delete_all + diff.save! # update files_count expect(diff).not_to receive(:update!) @@ -675,8 +676,39 @@ RSpec.describe MergeRequestDiff do end describe '#files_count' do - it 'returns number of diff files' do - expect(diff_with_commits.files_count).to eq(diff_with_commits.merge_request_diff_files.count) + let_it_be(:merge_request) { create(:merge_request) } + + let(:diff) { merge_request.merge_request_diff } + let(:actual_count) { diff.merge_request_diff_files.count } + + it 'is set by default' do + expect(diff.read_attribute(:files_count)).to eq(actual_count) + end + + it 'is set to the sentinel value if the actual value exceeds it' do + stub_const("#{described_class}::FILES_COUNT_SENTINEL", actual_count - 1) + + diff.save! # update the files_count column with the stub in place + + expect(diff.read_attribute(:files_count)).to eq(described_class::FILES_COUNT_SENTINEL) + end + + it 'uses the cached count if present' do + diff.update_columns(files_count: actual_count + 1) + + expect(diff.files_count).to eq(actual_count + 1) + end + + it 'uses the actual count if nil' do + diff.update_columns(files_count: nil) + + expect(diff.files_count).to eq(actual_count) + end + + it 'uses the actual count if overflown' do + diff.update_columns(files_count: described_class::FILES_COUNT_SENTINEL) + + expect(diff.files_count).to eq(actual_count) end end diff --git a/spec/models/merge_request_reviewer_spec.rb b/spec/models/merge_request_reviewer_spec.rb new file mode 100644 index 00000000000..55199cd96ad --- /dev/null +++ b/spec/models/merge_request_reviewer_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequestReviewer do + let(:merge_request) { create(:merge_request) } + + subject { merge_request.merge_request_reviewers.build(reviewer: create(:user)) } + + describe 'associations' do + it { is_expected.to belong_to(:merge_request).class_name('MergeRequest') } + it { is_expected.to belong_to(:reviewer).class_name('User') } + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 6edef54b153..98f709a0610 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2,13 +2,16 @@ require 'spec_helper' -RSpec.describe MergeRequest do +RSpec.describe MergeRequest, factory_default: :keep do include RepoHelpers include ProjectForksHelper include ReactiveCachingHelpers using RSpec::Parameterized::TableSyntax + let_it_be(:namespace) { create_default(:namespace) } + let_it_be(:project, refind: true) { create_default(:project, :repository) } + subject { create(:merge_request) } describe 'associations' do @@ -18,6 +21,7 @@ RSpec.describe MergeRequest do it { is_expected.to belong_to(:source_project).class_name('Project') } it { is_expected.to belong_to(:merge_user).class_name("User") } it { is_expected.to have_many(:assignees).through(:merge_request_assignees) } + it { is_expected.to have_many(:reviewers).through(:merge_request_reviewers) } it { is_expected.to have_many(:merge_request_diffs) } it { is_expected.to have_many(:user_mentions).class_name("MergeRequestUserMention") } it { is_expected.to belong_to(:milestone) } @@ -57,6 +61,24 @@ RSpec.describe MergeRequest do end end + describe '.order_merged_at_asc' do + let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) } + let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) } + + it 'returns MRs ordered by merged_at ascending' do + expect(described_class.order_merged_at_asc).to eq([older_mr, newer_mr]) + end + end + + describe '.order_merged_at_desc' do + let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) } + let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) } + + it 'returns MRs ordered by merged_at descending' do + expect(described_class.order_merged_at_desc).to eq([newer_mr, older_mr]) + end + end + describe '#squash_in_progress?' do let(:repo_path) do Gitlab::GitalyClient::StorageSettings.allow_disk_access do @@ -282,18 +304,6 @@ RSpec.describe MergeRequest do expect(merge_request.target_project_id).to eq(project.id) expect(merge_request.target_project_id).to eq(merge_request.metrics.target_project_id) end - - context 'when metrics record already exists with NULL target_project_id' do - before do - merge_request.metrics.update_column(:target_project_id, nil) - end - - it 'returns the metrics record' do - metrics_record = merge_request.ensure_metrics - - expect(metrics_record).to be_persisted - end - end end end @@ -359,7 +369,7 @@ RSpec.describe MergeRequest do it 'returns merge requests that match the given merge commit' do note = create(:track_mr_picking_note, commit_id: '456abc') - create(:track_mr_picking_note, commit_id: '456def') + create(:track_mr_picking_note, project: create(:project), commit_id: '456def') expect(described_class.by_cherry_pick_sha('456abc')).to eq([note.noteable]) end @@ -427,6 +437,23 @@ RSpec.describe MergeRequest do end end + describe '.sort_by_attribute' do + context 'merged_at' do + let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) } + let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) } + + it 'sorts asc' do + merge_requests = described_class.sort_by_attribute(:merged_at_asc) + expect(merge_requests).to eq([older_mr, newer_mr]) + end + + it 'sorts desc' do + merge_requests = described_class.sort_by_attribute(:merged_at_desc) + expect(merge_requests).to eq([newer_mr, older_mr]) + end + end + end + describe '#target_branch_sha' do let(:project) { create(:project, :repository) } @@ -831,7 +858,7 @@ RSpec.describe MergeRequest do end context 'with commit diff note' do - let(:other_merge_request) { create(:merge_request) } + let(:other_merge_request) { create(:merge_request, source_project: create(:project, :repository)) } let!(:diff_note) do create(:diff_note_on_commit, project: merge_request.project) @@ -854,8 +881,10 @@ RSpec.describe MergeRequest do end describe '#diff_size' do + let_it_be(:project) { create(:project, :repository) } + let(:merge_request) do - build(:merge_request, source_branch: 'expand-collapse-files', target_branch: 'master') + build(:merge_request, source_project: project, source_branch: 'expand-collapse-files', target_branch: 'master') end context 'when there are MR diffs' do @@ -1030,6 +1059,8 @@ RSpec.describe MergeRequest do end describe '#closes_issues' do + let(:project) { create(:project) } + let(:issue0) { create :issue, project: subject.project } let(:issue1) { create :issue, project: subject.project } @@ -1037,6 +1068,8 @@ RSpec.describe MergeRequest do let(:commit1) { double('commit1', safe_message: "Fixes #{issue0.to_reference}") } let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") } + subject { create(:merge_request, source_project: project) } + before do subject.project.add_developer(subject.author) allow(subject).to receive(:commits).and_return([commit0, commit1, commit2]) @@ -1087,6 +1120,8 @@ RSpec.describe MergeRequest do end context 'when the project has an external issue tracker' do + subject { create(:merge_request, source_project: create(:project, :repository)) } + before do subject.project.add_developer(subject.author) commit = double(:commit, safe_message: 'Fixes TEST-3') @@ -1253,14 +1288,13 @@ RSpec.describe MergeRequest do end describe "#source_branch_exists?" do - let(:merge_request) { subject } + let(:project) { create(:project, :repository) } + let(:merge_request) { create(:merge_request, source_project: project) } let(:repository) { merge_request.source_project.repository } context 'when the source project is set' do - it 'memoizes the value and returns the result' do - expect(repository).to receive(:branch_exists?).once.with(merge_request.source_branch).and_return(true) - - 2.times { expect(merge_request.source_branch_exists?).to eq(true) } + it 'returns true when the branch exists' do + expect(merge_request.source_branch_exists?).to eq(true) end end @@ -1480,7 +1514,9 @@ RSpec.describe MergeRequest do end context 'new merge request' do - subject { build(:merge_request) } + let_it_be(:project) { create(:project, :repository) } + + subject { build(:merge_request, source_project: project) } context 'compare commits' do before do @@ -1575,11 +1611,36 @@ RSpec.describe MergeRequest do before do subject.mark_as_merged! - subject.update_attribute(:merge_commit_sha, pipeline.sha) end - it 'returns the post-merge pipeline' do - expect(subject.merge_pipeline).to eq(pipeline) + context 'and there is a merge commit' do + before do + subject.update_attribute(:merge_commit_sha, pipeline.sha) + end + + it 'returns the pipeline associated with that merge request' do + expect(subject.merge_pipeline).to eq(pipeline) + end + end + + context 'and there is no merge commit, but there is a diff head' do + before do + allow(subject).to receive(:diff_head_sha).and_return(pipeline.sha) + end + + it 'returns the pipeline associated with that merge request' do + expect(subject.merge_pipeline).to eq(pipeline) + end + end + + context 'and there is no merge commit, but there is a squash commit' do + before do + subject.update_attribute(:squash_commit_sha, pipeline.sha) + end + + it 'returns the pipeline associated with that merge request' do + expect(subject.merge_pipeline).to eq(pipeline) + end end end end @@ -1706,16 +1767,14 @@ RSpec.describe MergeRequest do describe '#has_test_reports?' do subject { merge_request.has_test_reports? } - let(:project) { create(:project, :repository) } - context 'when head pipeline has test reports' do - let(:merge_request) { create(:merge_request, :with_test_reports, source_project: project) } + let(:merge_request) { create(:merge_request, :with_test_reports) } it { is_expected.to be_truthy } end context 'when head pipeline does not have test reports' do - let(:merge_request) { create(:merge_request, source_project: project) } + let(:merge_request) { create(:merge_request) } it { is_expected.to be_falsey } end @@ -1724,16 +1783,14 @@ RSpec.describe MergeRequest do describe '#has_accessibility_reports?' do subject { merge_request.has_accessibility_reports? } - let(:project) { create(:project, :repository) } - context 'when head pipeline has an accessibility reports' do - let(:merge_request) { create(:merge_request, :with_accessibility_reports, source_project: project) } + let(:merge_request) { create(:merge_request, :with_accessibility_reports) } it { is_expected.to be_truthy } end context 'when head pipeline does not have accessibility reports' do - let(:merge_request) { create(:merge_request, source_project: project) } + let(:merge_request) { create(:merge_request) } it { is_expected.to be_falsey } end @@ -1742,27 +1799,23 @@ RSpec.describe MergeRequest do describe '#has_coverage_reports?' do subject { merge_request.has_coverage_reports? } - let(:project) { create(:project, :repository) } - context 'when head pipeline has coverage reports' do - let(:merge_request) { create(:merge_request, :with_coverage_reports, source_project: project) } + let(:merge_request) { create(:merge_request, :with_coverage_reports) } it { is_expected.to be_truthy } end context 'when head pipeline does not have coverage reports' do - let(:merge_request) { create(:merge_request, source_project: project) } + let(:merge_request) { create(:merge_request) } it { is_expected.to be_falsey } end end describe '#has_terraform_reports?' do - let_it_be(:project) { create(:project, :repository) } - context 'when head pipeline has terraform reports' do it 'returns true' do - merge_request = create(:merge_request, :with_terraform_reports, source_project: project) + merge_request = create(:merge_request, :with_terraform_reports) expect(merge_request.has_terraform_reports?).to be_truthy end @@ -1770,7 +1823,7 @@ RSpec.describe MergeRequest do context 'when head pipeline does not have terraform reports' do it 'returns false' do - merge_request = create(:merge_request, source_project: project) + merge_request = create(:merge_request) expect(merge_request.has_terraform_reports?).to be_falsey end @@ -1778,8 +1831,7 @@ RSpec.describe MergeRequest do end describe '#calculate_reactive_cache' do - let(:project) { create(:project, :repository) } - let(:merge_request) { create(:merge_request, source_project: project) } + let(:merge_request) { create(:merge_request) } subject { merge_request.calculate_reactive_cache(service_class_name) } @@ -1867,12 +1919,6 @@ RSpec.describe MergeRequest do subject { merge_request.find_coverage_reports } context 'when head pipeline has coverage reports' do - let!(:job) do - create(:ci_build, options: { artifacts: { reports: { cobertura: ['cobertura-coverage.xml'] } } }, pipeline: pipeline) - end - - let!(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) } - context 'when reactive cache worker is parsing results asynchronously' do it 'returns status' do expect(subject[:status]).to eq(:parsing) @@ -2074,11 +2120,13 @@ RSpec.describe MergeRequest do end context 'when merge request is not persisted' do + let_it_be(:project) { create(:project, :repository) } + context 'when compare commits are set in the service' do let(:commit) { spy('commit') } subject do - build(:merge_request, compare_commits: [commit, commit]) + build(:merge_request, source_project: project, compare_commits: [commit, commit]) end it 'returns commits from compare commits temporary data' do @@ -2087,7 +2135,7 @@ RSpec.describe MergeRequest do end context 'when compare commits are not set in the service' do - subject { build(:merge_request) } + subject { build(:merge_request, source_project: project) } it 'returns array with diff head sha element only' do expect(subject.all_commit_shas).to eq [subject.diff_head_sha] @@ -2112,7 +2160,63 @@ RSpec.describe MergeRequest do end end + describe '#merged_commit_sha' do + it 'returns nil when not merged' do + expect(subject.merged_commit_sha).to be_nil + end + + context 'when the MR is merged' do + let(:sha) { 'f7ce827c314c9340b075657fd61c789fb01cf74d' } + + before do + subject.mark_as_merged! + end + + it 'returns merge_commit_sha when there is a merge_commit_sha' do + subject.update_attribute(:merge_commit_sha, sha) + + expect(subject.merged_commit_sha).to eq(sha) + end + + it 'returns squash_commit_sha when there is a squash_commit_sha' do + subject.update_attribute(:squash_commit_sha, sha) + + expect(subject.merged_commit_sha).to eq(sha) + end + + it 'returns diff_head_sha when there are no merge_commit_sha and squash_commit_sha' do + allow(subject).to receive(:diff_head_sha).and_return(sha) + + expect(subject.merged_commit_sha).to eq(sha) + end + end + end + + describe '#short_merged_commit_sha' do + context 'when merged_commit_sha is nil' do + before do + allow(subject).to receive(:merged_commit_sha).and_return(nil) + end + + it 'returns nil' do + expect(subject.short_merged_commit_sha).to be_nil + end + end + + context 'when merged_commit_sha is present' do + before do + allow(subject).to receive(:merged_commit_sha).and_return('f7ce827c314c9340b075657fd61c789fb01cf74d') + end + + it 'returns shortened merged_commit_sha' do + expect(subject.short_merged_commit_sha).to eq('f7ce827c') + end + end + end + describe '#can_be_reverted?' do + subject { create(:merge_request, source_project: create(:project, :repository)) } + context 'when there is no merge_commit for the MR' do before do subject.metrics.update!(merged_at: Time.current.utc) @@ -2301,8 +2405,6 @@ RSpec.describe MergeRequest do end describe '#participants' do - let(:project) { create(:project, :public) } - let(:mr) do create(:merge_request, source_project: project, target_project: project) end @@ -2410,9 +2512,7 @@ RSpec.describe MergeRequest do end describe '#mergeable?' do - let(:project) { create(:project) } - - subject { create(:merge_request, source_project: project) } + subject { build_stubbed(:merge_request) } it 'returns false if #mergeable_state? is false' do expect(subject).to receive(:mergeable_state?) { false } @@ -2427,6 +2527,57 @@ RSpec.describe MergeRequest do expect(subject.mergeable?).to be_truthy end + + context 'with skip_ci_check option' do + using RSpec::Parameterized::TableSyntax + + before do + allow(subject).to receive_messages(check_mergeability: nil, + can_be_merged?: true, + broken?: false) + end + + where(:mergeable_ci_state, :skip_ci_check, :expected_mergeable) do + false | false | false + false | true | true + true | false | true + true | true | true + end + + with_them do + it 'overrides mergeable_ci_state?' do + allow(subject).to receive(:mergeable_ci_state?) { mergeable_ci_state } + + expect(subject.mergeable?(skip_ci_check: skip_ci_check)).to eq(expected_mergeable) + end + end + end + + context 'with skip_discussions_check option' do + using RSpec::Parameterized::TableSyntax + + before do + allow(subject).to receive_messages(mergeable_ci_state?: true, + check_mergeability: nil, + can_be_merged?: true, + broken?: false) + end + + where(:mergeable_discussions_state, :skip_discussions_check, :expected_mergeable) do + false | false | false + false | true | true + true | false | true + true | true | true + end + + with_them do + it 'overrides mergeable_discussions_state?' do + allow(subject).to receive(:mergeable_discussions_state?) { mergeable_discussions_state } + + expect(subject.mergeable?(skip_discussions_check: skip_discussions_check)).to eq(expected_mergeable) + end + end + end end describe '#check_mergeability' do @@ -2482,9 +2633,7 @@ RSpec.describe MergeRequest do end describe '#mergeable_state?' do - let(:project) { create(:project, :repository) } - - subject { create(:merge_request, source_project: project) } + subject { create(:merge_request) } it 'checks if merge request can be merged' do allow(subject).to receive(:mergeable_ci_state?) { true } @@ -2532,6 +2681,10 @@ RSpec.describe MergeRequest do it 'returns false' do expect(subject.mergeable_state?).to be_falsey end + + it 'returns true when skipping ci check' do + expect(subject.mergeable_state?(skip_ci_check: true)).to be(true) + end end context 'when #mergeable_discussions_state? is false' do @@ -2599,9 +2752,9 @@ RSpec.describe MergeRequest do let(:pipeline) { create(:ci_empty_pipeline) } context 'when it is only allowed to merge when build is green' do - let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true) } + let_it_be(:project) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true) } - subject { build(:merge_request, target_project: project) } + subject { build(:merge_request, source_project: project) } context 'and a failed pipeline is associated' do before do @@ -2640,9 +2793,9 @@ RSpec.describe MergeRequest do end context 'when it is only allowed to merge when build is green or skipped' do - let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true, allow_merge_on_skipped_pipeline: true) } + let_it_be(:project) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true, allow_merge_on_skipped_pipeline: true) } - subject { build(:merge_request, target_project: project) } + subject { build(:merge_request, source_project: project) } context 'and a failed pipeline is associated' do before do @@ -2681,9 +2834,9 @@ RSpec.describe MergeRequest do end context 'when merges are not restricted to green builds' do - let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: false) } + let_it_be(:project) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: false) } - subject { build(:merge_request, target_project: project) } + subject { build(:merge_request, source_project: project) } context 'and a failed pipeline is associated' do before do @@ -2725,7 +2878,7 @@ RSpec.describe MergeRequest do let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) } context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do - let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) } + let_it_be(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) } context 'with all discussions resolved' do before do @@ -2974,6 +3127,10 @@ RSpec.describe MergeRequest do end describe '#branch_merge_base_commit' do + let(:project) { create(:project, :repository) } + + subject { create(:merge_request, :with_diffs, source_project: project) } + context 'source and target branch exist' do it { expect(subject.branch_merge_base_commit.sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') } it { expect(subject.branch_merge_base_commit).to be_a(Commit) } @@ -2993,7 +3150,9 @@ RSpec.describe MergeRequest do describe "#diff_refs" do context "with diffs" do - subject { create(:merge_request, :with_diffs) } + let(:project) { create(:project, :repository) } + + subject { create(:merge_request, :with_diffs, source_project: project) } let(:expected_diff_refs) do Gitlab::Diff::DiffRefs.new( @@ -3195,7 +3354,8 @@ RSpec.describe MergeRequest do pipeline end - let(:project) { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) } + let_it_be(:project) { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) } + let(:developer) { create(:user) } let(:user) { create(:user) } let(:merge_request) { create(:merge_request, source_project: project) } @@ -3289,8 +3449,7 @@ RSpec.describe MergeRequest do end describe '#pipeline_coverage_delta' do - let!(:project) { create(:project, :repository) } - let!(:merge_request) { create(:merge_request, source_project: project) } + let!(:merge_request) { create(:merge_request) } let!(:source_pipeline) do create(:ci_pipeline, @@ -3396,7 +3555,9 @@ RSpec.describe MergeRequest do end describe '#merge_request_diff_for' do - subject { create(:merge_request, importing: true) } + let(:project) { create(:project, :repository) } + + subject { create(:merge_request, importing: true, source_project: project) } let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) } @@ -3427,9 +3588,10 @@ RSpec.describe MergeRequest do end describe '#version_params_for' do - subject { create(:merge_request, importing: true) } + let(:project) { create(:project, :repository) } + + subject { create(:merge_request, importing: true, source_project: project) } - let(:project) { subject.project } let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) } let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') } @@ -3460,6 +3622,10 @@ RSpec.describe MergeRequest do end describe '#fetch_ref!' do + let(:project) { create(:project, :repository) } + + subject { create(:merge_request, :with_diffs, source_project: project) } + it 'fetches the ref correctly' do expect { subject.target_project.repository.delete_refs(subject.ref_path) }.not_to raise_error @@ -3482,8 +3648,10 @@ RSpec.describe MergeRequest do end context 'state machine transitions' do + let(:project) { create(:project, :repository) } + describe '#unlock_mr' do - subject { create(:merge_request, state: 'locked', merge_jid: 123) } + subject { create(:merge_request, state: 'locked', source_project: project, merge_jid: 123) } it 'updates merge request head pipeline and sets merge_jid to nil', :sidekiq_might_not_need_inline do pipeline = create(:ci_empty_pipeline, project: subject.project, ref: subject.source_branch, sha: subject.source_branch_sha) @@ -3500,7 +3668,7 @@ RSpec.describe MergeRequest do let(:notification_service) { double(:notification_service) } let(:todo_service) { double(:todo_service) } - subject { create(:merge_request, state, merge_status: :unchecked) } + subject { create(:merge_request, state, source_project: project, merge_status: :unchecked) } before do allow(NotificationService).to receive(:new).and_return(notification_service) @@ -3589,7 +3757,7 @@ RSpec.describe MergeRequest do end context 'source branch is missing' do - subject { create(:merge_request, :invalid, :opened, merge_status: :unchecked, target_branch: 'master') } + subject { create(:merge_request, :invalid, :opened, source_project: project, merge_status: :unchecked, target_branch: 'master') } before do allow(subject.project.repository).to receive(:can_be_merged?).and_call_original @@ -3622,10 +3790,8 @@ RSpec.describe MergeRequest do end describe '#should_be_rebased?' do - let(:project) { create(:project, :repository) } - it 'returns false for the same source and target branches' do - merge_request = create(:merge_request, source_project: project, target_project: project) + merge_request = build_stubbed(:merge_request, source_project: project, target_project: project) expect(merge_request.should_be_rebased?).to be_falsey end @@ -3640,7 +3806,7 @@ RSpec.describe MergeRequest do end with_them do - let(:merge_request) { create(:merge_request) } + let(:merge_request) { build_stubbed(:merge_request) } subject { merge_request.rebase_in_progress? } @@ -3863,7 +4029,7 @@ RSpec.describe MergeRequest do describe '#cleanup_refs' do subject { merge_request.cleanup_refs(only: only) } - let(:merge_request) { build(:merge_request) } + let(:merge_request) { build(:merge_request, source_project: create(:project, :repository)) } context 'when removing all refs' do let(:only) { :all } @@ -4084,4 +4250,58 @@ RSpec.describe MergeRequest do expect(context[:label_url_method]).to eq(:project_merge_requests_url) end end + + describe '#head_pipeline_builds_with_coverage' do + it 'delegates to head_pipeline' do + expect(subject) + .to delegate_method(:builds_with_coverage) + .to(:head_pipeline) + .with_prefix + .with_arguments(allow_nil: true) + end + end + + describe '#allows_reviewers?' do + it 'returns false without merge_request_reviewers feature' do + stub_feature_flags(merge_request_reviewers: false) + + merge_request = build_stubbed(:merge_request) + + expect(merge_request.allows_reviewers?).to be(false) + end + + it 'returns true with merge_request_reviewers feature' do + stub_feature_flags(merge_request_reviewers: true) + + merge_request = build_stubbed(:merge_request) + + expect(merge_request.allows_reviewers?).to be(true) + end + end + + describe '#merge_ref_head' do + let(:merge_request) { create(:merge_request) } + + context 'when merge_ref_sha is not present' do + let!(:result) do + MergeRequests::MergeToRefService + .new(merge_request.project, merge_request.author) + .execute(merge_request) + end + + it 'returns the commit based on merge ref path' do + expect(merge_request.merge_ref_head.id).to eq(result[:commit_id]) + end + end + + context 'when merge_ref_sha is present' do + before do + merge_request.update!(merge_ref_sha: merge_request.project.repository.commit.id) + end + + it 'returns the commit based on cached merge_ref_sha' do + expect(merge_request.merge_ref_head.id).to eq(merge_request.merge_ref_sha) + end + end + end end diff --git a/spec/models/metrics/dashboard/annotation_spec.rb b/spec/models/metrics/dashboard/annotation_spec.rb index bd4baeb8851..4b7492016f3 100644 --- a/spec/models/metrics/dashboard/annotation_spec.rb +++ b/spec/models/metrics/dashboard/annotation_spec.rb @@ -100,7 +100,7 @@ RSpec.describe Metrics::Dashboard::Annotation do describe '#ending_before' do it 'returns annotations only for appointed dashboard' do - Timecop.freeze do + freeze_time do twelve_minutes_old_annotation = create(:metrics_dashboard_annotation, starting_at: 15.minutes.ago, ending_at: 12.minutes.ago) create(:metrics_dashboard_annotation, starting_at: 15.minutes.ago, ending_at: 11.minutes.ago) diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index b52b035e130..e611484f5ee 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -3,6 +3,11 @@ require 'spec_helper' RSpec.describe Milestone do + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } + let(:milestone) { create(:milestone, project: project) } + let(:project) { create(:project, :public) } + it_behaves_like 'a timebox', :milestone describe 'MilestoneStruct#serializable_hash' do @@ -47,11 +52,6 @@ RSpec.describe Milestone do it { is_expected.to have_many(:milestone_releases) } end - let(:project) { create(:project, :public) } - let(:milestone) { create(:milestone, project: project) } - let(:issue) { create(:issue, project: project) } - let(:user) { create(:user) } - describe '.predefined_id?' do it 'returns true for a predefined Milestone ID' do expect(Milestone.predefined_id?(described_class::Upcoming.id)).to be true diff --git a/spec/models/namespace/root_storage_statistics_spec.rb b/spec/models/namespace/root_storage_statistics_spec.rb index ce6f875ee09..92a8d17a2a8 100644 --- a/spec/models/namespace/root_storage_statistics_spec.rb +++ b/spec/models/namespace/root_storage_statistics_spec.rb @@ -44,6 +44,7 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do total_packages_size = stat1.packages_size + stat2.packages_size total_storage_size = stat1.storage_size + stat2.storage_size total_snippets_size = stat1.snippets_size + stat2.snippets_size + total_pipeline_artifacts_size = stat1.pipeline_artifacts_size + stat2.pipeline_artifacts_size expect(root_storage_statistics.repository_size).to eq(total_repository_size) expect(root_storage_statistics.wiki_size).to eq(total_wiki_size) @@ -52,6 +53,7 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do expect(root_storage_statistics.packages_size).to eq(total_packages_size) expect(root_storage_statistics.storage_size).to eq(total_storage_size) expect(root_storage_statistics.snippets_size).to eq(total_snippets_size) + expect(root_storage_statistics.pipeline_artifacts_size).to eq(total_pipeline_artifacts_size) end it 'works when there are no projects' do @@ -67,6 +69,7 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do expect(root_storage_statistics.packages_size).to eq(0) expect(root_storage_statistics.storage_size).to eq(0) expect(root_storage_statistics.snippets_size).to eq(0) + expect(root_storage_statistics.pipeline_artifacts_size).to eq(0) end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 4ef2ddd218a..ca1f06370d4 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -175,12 +175,13 @@ RSpec.describe Namespace do end describe '.with_statistics' do - let(:namespace) { create :namespace } + let_it_be(:namespace) { create(:namespace) } let(:project1) do create(:project, namespace: namespace, statistics: build(:project_statistics, + namespace: namespace, repository_size: 101, wiki_size: 505, lfs_objects_size: 202, @@ -193,6 +194,7 @@ RSpec.describe Namespace do create(:project, namespace: namespace, statistics: build(:project_statistics, + namespace: namespace, repository_size: 10, wiki_size: 50, lfs_objects_size: 20, @@ -391,14 +393,14 @@ RSpec.describe Namespace do let(:uploads_dir) { FileUploader.root } let(:pages_dir) { File.join(TestEnv.pages_path) } - def expect_project_directories_at(namespace_path) + def expect_project_directories_at(namespace_path, with_pages: true) expected_repository_path = File.join(TestEnv.repos_path, namespace_path, 'the-project.git') expected_upload_path = File.join(uploads_dir, namespace_path, 'the-project') expected_pages_path = File.join(pages_dir, namespace_path, 'the-project') expect(File.directory?(expected_repository_path)).to be_truthy expect(File.directory?(expected_upload_path)).to be_truthy - expect(File.directory?(expected_pages_path)).to be_truthy + expect(File.directory?(expected_pages_path)).to be(with_pages) end before do @@ -407,43 +409,156 @@ RSpec.describe Namespace do FileUtils.mkdir_p(File.join(pages_dir, project.full_path)) end + after do + FileUtils.remove_entry(File.join(TestEnv.repos_path, parent.full_path), true) + FileUtils.remove_entry(File.join(TestEnv.repos_path, new_parent.full_path), true) + FileUtils.remove_entry(File.join(TestEnv.repos_path, child.full_path), true) + FileUtils.remove_entry(File.join(uploads_dir, project.full_path), true) + FileUtils.remove_entry(pages_dir, true) + end + context 'renaming child' do - it 'correctly moves the repository, uploads and pages' do - child.update!(path: 'renamed') + context 'when no projects have pages deployed' do + it 'moves the repository and uploads', :sidekiq_inline do + project.pages_metadatum.update!(deployed: false) + child.update!(path: 'renamed') - expect_project_directories_at('parent/renamed') + expect_project_directories_at('parent/renamed', with_pages: false) + end + end + + context 'when the project has pages deployed' do + before do + project.pages_metadatum.update!(deployed: true) + end + + it 'correctly moves the repository, uploads and pages', :sidekiq_inline do + child.update!(path: 'renamed') + + expect_project_directories_at('parent/renamed') + end + + it 'performs the move async of pages async' do + expect(PagesTransferWorker).to receive(:perform_async).with('rename_namespace', ['parent/child', 'parent/renamed']) + + child.update!(path: 'renamed') + end end end context 'renaming parent' do - it 'correctly moves the repository, uploads and pages' do - parent.update!(path: 'renamed') + context 'when no projects have pages deployed' do + it 'moves the repository and uploads', :sidekiq_inline do + project.pages_metadatum.update!(deployed: false) + parent.update!(path: 'renamed') - expect_project_directories_at('renamed/child') + expect_project_directories_at('renamed/child', with_pages: false) + end + end + + context 'when the project has pages deployed' do + before do + project.pages_metadatum.update!(deployed: true) + end + + it 'correctly moves the repository, uploads and pages', :sidekiq_inline do + parent.update!(path: 'renamed') + + expect_project_directories_at('renamed/child') + end + + it 'performs the move async of pages async' do + expect(PagesTransferWorker).to receive(:perform_async).with('rename_namespace', %w(parent renamed)) + + parent.update!(path: 'renamed') + end end end context 'moving from one parent to another' do - it 'correctly moves the repository, uploads and pages' do - child.update!(parent: new_parent) + context 'when no projects have pages deployed' do + it 'moves the repository and uploads', :sidekiq_inline do + project.pages_metadatum.update!(deployed: false) + child.update!(parent: new_parent) + + expect_project_directories_at('new_parent/child', with_pages: false) + end + end - expect_project_directories_at('new_parent/child') + context 'when the project has pages deployed' do + before do + project.pages_metadatum.update!(deployed: true) + end + + it 'correctly moves the repository, uploads and pages', :sidekiq_inline do + child.update!(parent: new_parent) + + expect_project_directories_at('new_parent/child') + end + + it 'performs the move async of pages async' do + expect(PagesTransferWorker).to receive(:perform_async).with('move_namespace', %w(child parent new_parent)) + + child.update!(parent: new_parent) + end end end context 'moving from having a parent to root' do - it 'correctly moves the repository, uploads and pages' do - child.update!(parent: nil) + context 'when no projects have pages deployed' do + it 'moves the repository and uploads', :sidekiq_inline do + project.pages_metadatum.update!(deployed: false) + child.update!(parent: nil) + + expect_project_directories_at('child', with_pages: false) + end + end + + context 'when the project has pages deployed' do + before do + project.pages_metadatum.update!(deployed: true) + end + + it 'correctly moves the repository, uploads and pages', :sidekiq_inline do + child.update!(parent: nil) - expect_project_directories_at('child') + expect_project_directories_at('child') + end + + it 'performs the move async of pages async' do + expect(PagesTransferWorker).to receive(:perform_async).with('move_namespace', ['child', 'parent', nil]) + + child.update!(parent: nil) + end end end context 'moving from root to having a parent' do - it 'correctly moves the repository, uploads and pages' do - parent.update!(parent: new_parent) + context 'when no projects have pages deployed' do + it 'moves the repository and uploads', :sidekiq_inline do + project.pages_metadatum.update!(deployed: false) + parent.update!(parent: new_parent) - expect_project_directories_at('new_parent/parent/child') + expect_project_directories_at('new_parent/parent/child', with_pages: false) + end + end + + context 'when the project has pages deployed' do + before do + project.pages_metadatum.update!(deployed: true) + end + + it 'correctly moves the repository, uploads and pages', :sidekiq_inline do + parent.update!(parent: new_parent) + + expect_project_directories_at('new_parent/parent/child') + end + + it 'performs the move async of pages async' do + expect(PagesTransferWorker).to receive(:perform_async).with('move_namespace', ['parent', nil, 'new_parent']) + + parent.update!(parent: new_parent) + end end end end @@ -588,6 +703,21 @@ RSpec.describe Namespace do end end + describe ".clean_name" do + context "when the name complies with the group name regex" do + it "returns the name as is" do + valid_name = "Hello - World _ (Hi.)" + expect(described_class.clean_name(valid_name)).to eq(valid_name) + end + end + + context "when the name does not comply with the group name regex" do + it "sanitizes the name by replacing all invalid char sequences with a space" do + expect(described_class.clean_name("Green'! Test~~~")).to eq("Green Test") + end + end + end + describe "#default_branch_protection" do let(:namespace) { create(:namespace) } let(:default_branch_protection) { nil } @@ -1101,6 +1231,27 @@ RSpec.describe Namespace do end end + describe '#any_project_with_pages_deployed?' do + it 'returns true if any project nested under the group has pages deployed' do + parent_1 = create(:group) # Three projects, one with pages + child_1_1 = create(:group, parent: parent_1) # Two projects, one with pages + child_1_2 = create(:group, parent: parent_1) # One project, no pages + parent_2 = create(:group) # No projects + + create(:project, group: child_1_1).tap do |project| + project.pages_metadatum.update!(deployed: true) + end + + create(:project, group: child_1_1) + create(:project, group: child_1_2) + + expect(parent_1.any_project_with_pages_deployed?).to be(true) + expect(child_1_1.any_project_with_pages_deployed?).to be(true) + expect(child_1_2.any_project_with_pages_deployed?).to be(false) + expect(parent_2.any_project_with_pages_deployed?).to be(false) + end + end + describe '#has_parent?' do it 'returns true when the group has a parent' do group = create(:group, :nested) diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 7edd7849bbe..a3417ee5fc7 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -67,7 +67,7 @@ RSpec.describe Note do end context 'when noteable is a personal snippet' do - subject { build(:note_on_personal_snippet) } + subject { build(:note_on_personal_snippet, noteable: create(:personal_snippet)) } it 'is valid without project' do is_expected.to be_valid @@ -109,7 +109,8 @@ RSpec.describe Note do describe 'callbacks' do describe '#notify_after_create' do it 'calls #after_note_created on the noteable' do - note = build(:note) + noteable = create(:issue) + note = build(:note, project: noteable.project, noteable: noteable) expect(note).to receive(:notify_after_create).and_call_original expect(note.noteable).to receive(:after_note_created).with(note) @@ -285,6 +286,56 @@ RSpec.describe Note do end end + describe "noteable_author?" do + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:project) { create(:project, :public, :repository) } + + context 'when note is on commit' do + let(:noteable) { create(:commit, project: project, author: user1) } + + context 'if user is the noteable author' do + let(:note) { create(:discussion_note_on_commit, commit_id: noteable.id, project: project, author: user1) } + let(:diff_note) { create(:diff_note_on_commit, commit_id: noteable.id, project: project, author: user1) } + + it 'returns true' do + expect(note.noteable_author?(noteable)).to be true + expect(diff_note.noteable_author?(noteable)).to be true + end + end + + context 'if user is not the noteable author' do + let(:note) { create(:discussion_note_on_commit, commit_id: noteable.id, project: project, author: user2) } + let(:diff_note) { create(:diff_note_on_commit, commit_id: noteable.id, project: project, author: user2) } + + it 'returns false' do + expect(note.noteable_author?(noteable)).to be false + expect(diff_note.noteable_author?(noteable)).to be false + end + end + end + + context 'when note is on issue' do + let(:noteable) { create(:issue, project: project, author: user1) } + + context 'if user is the noteable author' do + let(:note) { create(:note, noteable: noteable, author: user1, project: project) } + + it 'returns true' do + expect(note.noteable_author?(noteable)).to be true + end + end + + context 'if user is not the noteable author' do + let(:note) { create(:note, noteable: noteable, author: user2, project: project) } + + it 'returns false' do + expect(note.noteable_author?(noteable)).to be false + end + end + end + end + describe "edited?" do let(:note) { build(:note, updated_by_id: nil, created_at: Time.current, updated_at: Time.current + 5.hours) } @@ -840,7 +891,8 @@ RSpec.describe Note do let(:html) { '

some html

'} context 'note for a project snippet' do - let(:note) { build(:note_on_project_snippet) } + let(:snippet) { create(:project_snippet) } + let(:note) { build(:note_on_project_snippet, project: snippet.project, noteable: snippet) } before do expect(Banzai::Renderer).to receive(:cacheless_render_field) @@ -855,7 +907,8 @@ RSpec.describe Note do end context 'note for a personal snippet' do - let(:note) { build(:note_on_personal_snippet) } + let(:snippet) { create(:personal_snippet) } + let(:note) { build(:note_on_personal_snippet, noteable: snippet) } before do expect(Banzai::Renderer).to receive(:cacheless_render_field) @@ -889,7 +942,7 @@ RSpec.describe Note do context 'for a note on a commit' do it 'returns true' do - note = build(:note_on_commit) + note = build(:note_on_commit, project: create(:project, :repository)) expect(note.can_be_discussion_note?).to be_truthy end @@ -913,7 +966,7 @@ RSpec.describe Note do context 'for a diff note on commit' do it 'returns false' do - note = build(:diff_note_on_commit) + note = build(:diff_note_on_commit, project: create(:project, :repository)) expect(note.can_be_discussion_note?).to be_falsey end @@ -1143,7 +1196,8 @@ RSpec.describe Note do end describe 'expiring ETag cache' do - let(:note) { build(:note_on_issue) } + let_it_be(:issue) { create(:issue) } + let(:note) { build(:note, project: issue.project, noteable: issue) } def expect_expiration(noteable) expect_any_instance_of(Gitlab::EtagCaching::Store) @@ -1224,22 +1278,6 @@ RSpec.describe Note do end end - describe '#special_role=' do - let(:role) { Note::SpecialRole::FIRST_TIME_CONTRIBUTOR } - - it 'assigns role' do - subject.special_role = role - - expect(subject.special_role).to eq(role) - end - - it 'does not assign unknown role' do - expect { subject.special_role = :bogus }.to raise_error(/Role is undefined/) - - expect(subject.special_role).to be_nil - end - end - describe '#parent' do it 'returns project for project notes' do project = create(:project) @@ -1416,4 +1454,20 @@ RSpec.describe Note do expect(note.parent_user).to be_nil end end + + describe '#skip_notification?' do + subject(:skip_notification?) { note.skip_notification? } + + context 'when there is no review' do + let(:note) { build(:note) } + + it { is_expected.to be_falsey } + end + + context 'when the review exists' do + let(:note) { build(:note, :with_review) } + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/models/operations/feature_flag_scope_spec.rb b/spec/models/operations/feature_flag_scope_spec.rb new file mode 100644 index 00000000000..29d338d8b29 --- /dev/null +++ b/spec/models/operations/feature_flag_scope_spec.rb @@ -0,0 +1,391 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Operations::FeatureFlagScope do + describe 'associations' do + it { is_expected.to belong_to(:feature_flag) } + end + + describe 'validations' do + context 'when duplicate environment scope is going to be created' do + let!(:existing_feature_flag_scope) do + create(:operations_feature_flag_scope) + end + + let(:new_feature_flag_scope) do + build(:operations_feature_flag_scope, + feature_flag: existing_feature_flag_scope.feature_flag, + environment_scope: existing_feature_flag_scope.environment_scope) + end + + it 'validates uniqueness of environment scope' do + new_feature_flag_scope.save + + expect(new_feature_flag_scope.errors[:environment_scope]) + .to include("(#{existing_feature_flag_scope.environment_scope})" \ + " has already been taken") + end + end + + context 'when environment scope of a default scope is updated' do + let!(:feature_flag) { create(:operations_feature_flag) } + let!(:scope_default) { feature_flag.default_scope } + + it 'keeps default scope intact' do + scope_default.update(environment_scope: 'review/*') + + expect(scope_default.errors[:environment_scope]) + .to include("cannot be changed from default scope") + end + end + + context 'when a default scope is destroyed' do + let!(:feature_flag) { create(:operations_feature_flag) } + let!(:scope_default) { feature_flag.default_scope } + + it 'prevents from destroying the default scope' do + expect { scope_default.destroy! }.to raise_error(ActiveRecord::ReadOnlyRecord) + end + end + + describe 'strategy validations' do + it 'handles null strategies which can occur while adding the column during migration' do + scope = create(:operations_feature_flag_scope, active: true) + allow(scope).to receive(:strategies).and_return(nil) + + scope.active = false + scope.save + + expect(scope.errors[:strategies]).to be_empty + end + + it 'validates multiple strategies' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: "default", parameters: {} }, + { name: "invalid", parameters: {} }]) + + expect(scope.errors[:strategies]).not_to be_empty + end + + where(:invalid_value) do + [{}, 600, "bad", [{ name: 'default', parameters: {} }, 300]] + end + with_them do + it 'must be an array of strategy hashes' do + scope = create(:operations_feature_flag_scope) + + scope.strategies = invalid_value + scope.save + + expect(scope.errors[:strategies]).to eq(['must be an array of strategy hashes']) + end + end + + describe 'name' do + using RSpec::Parameterized::TableSyntax + + where(:name, :params, :expected) do + 'default' | {} | [] + 'gradualRolloutUserId' | { groupId: 'mygroup', percentage: '50' } | [] + 'userWithId' | { userIds: 'sam' } | [] + 5 | nil | ['strategy name is invalid'] + nil | nil | ['strategy name is invalid'] + "nothing" | nil | ['strategy name is invalid'] + "" | nil | ['strategy name is invalid'] + 40.0 | nil | ['strategy name is invalid'] + {} | nil | ['strategy name is invalid'] + [] | nil | ['strategy name is invalid'] + end + with_them do + it 'must be one of "default", "gradualRolloutUserId", or "userWithId"' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: name, parameters: params }]) + + expect(scope.errors[:strategies]).to eq(expected) + end + end + end + + describe 'parameters' do + context 'when the strategy name is gradualRolloutUserId' do + it 'must have parameters' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'gradualRolloutUserId' }]) + + expect(scope.errors[:strategies]).to eq(['parameters are invalid']) + end + + where(:invalid_parameters) do + [nil, {}, { percentage: '40', groupId: 'mygroup', userIds: '4' }, { percentage: '40' }, + { percentage: '40', groupId: 'mygroup', extra: nil }, { groupId: 'mygroup' }] + end + with_them do + it 'must have valid parameters for the strategy' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'gradualRolloutUserId', + parameters: invalid_parameters }]) + + expect(scope.errors[:strategies]).to eq(['parameters are invalid']) + end + end + + it 'allows the parameters in any order' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'gradualRolloutUserId', + parameters: { percentage: '10', groupId: 'mygroup' } }]) + + expect(scope.errors[:strategies]).to be_empty + end + + describe 'percentage' do + where(:invalid_value) do + [50, 40.0, { key: "value" }, "garbage", "00", "01", "101", "-1", "-10", "0100", + "1000", "10.0", "5%", "25%", "100hi", "e100", "30m", " ", "\r\n", "\n", "\t", + "\n10", "20\n", "\n100", "100\n", "\n ", nil] + end + with_them do + it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'gradualRolloutUserId', + parameters: { groupId: 'mygroup', percentage: invalid_value } }]) + + expect(scope.errors[:strategies]).to eq(['percentage must be a string between 0 and 100 inclusive']) + end + end + + where(:valid_value) do + %w[0 1 10 38 100 93] + end + with_them do + it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'gradualRolloutUserId', + parameters: { groupId: 'mygroup', percentage: valid_value } }]) + + expect(scope.errors[:strategies]).to eq([]) + end + end + end + + describe 'groupId' do + where(:invalid_value) do + [nil, 4, 50.0, {}, 'spaces bad', 'bad$', '%bad', '', '!bad', + '.bad', 'Bad', 'bad1', "", " ", "b" * 33, "ba_d", "ba\nd"] + end + with_them do + it 'must be a string value of up to 32 lowercase characters' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'gradualRolloutUserId', + parameters: { groupId: invalid_value, percentage: '40' } }]) + + expect(scope.errors[:strategies]).to eq(['groupId parameter is invalid']) + end + end + + where(:valid_value) do + ["somegroup", "anothergroup", "okay", "g", "a" * 32] + end + with_them do + it 'must be a string value of up to 32 lowercase characters' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'gradualRolloutUserId', + parameters: { groupId: valid_value, percentage: '40' } }]) + + expect(scope.errors[:strategies]).to eq([]) + end + end + end + end + + context 'when the strategy name is userWithId' do + it 'must have parameters' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'userWithId' }]) + + expect(scope.errors[:strategies]).to eq(['parameters are invalid']) + end + + where(:invalid_parameters) do + [nil, { userIds: 'sam', percentage: '40' }, { userIds: 'sam', some: 'param' }, { percentage: '40' }, {}] + end + with_them do + it 'must have valid parameters for the strategy' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'userWithId', parameters: invalid_parameters }]) + + expect(scope.errors[:strategies]).to eq(['parameters are invalid']) + end + end + + describe 'userIds' do + where(:valid_value) do + ["", "sam", "1", "a", "uuid-of-some-kind", "sam,fred,tom,jane,joe,mike", + "gitlab@example.com", "123,4", "UPPER,Case,charActeRS", "0", + "$valid$email#2345#$%..{}+=-)?\\/@example.com", "spaces allowed", + "a" * 256, "a,#{'b' * 256},ccc", "many spaces"] + end + with_them do + it 'is valid with a string of comma separated values' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'userWithId', parameters: { userIds: valid_value } }]) + + expect(scope.errors[:strategies]).to be_empty + end + end + + where(:invalid_value) do + [1, 2.5, {}, [], nil, "123\n456", "1,2,3,12\t3", "\n", "\n\r", + "joe\r,sam", "1,2,2", "1,,2", "1,2,,,,", "b" * 257, "1, ,2", "tim, ,7", " ", + " ", " ,1", "1, ", " leading,1", "1,trailing ", "1, both ,2"] + end + with_them do + it 'is invalid' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'userWithId', parameters: { userIds: invalid_value } }]) + + expect(scope.errors[:strategies]).to include( + 'userIds must be a string of unique comma separated values each 256 characters or less' + ) + end + end + end + end + + context 'when the strategy name is default' do + it 'must have parameters' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'default' }]) + + expect(scope.errors[:strategies]).to eq(['parameters are invalid']) + end + + where(:invalid_value) do + [{ groupId: "hi", percentage: "7" }, "", "nothing", 7, nil, [], 2.5] + end + with_them do + it 'must be empty' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'default', + parameters: invalid_value }]) + + expect(scope.errors[:strategies]).to eq(['parameters are invalid']) + end + end + + it 'must be empty' do + feature_flag = create(:operations_feature_flag) + scope = described_class.create(feature_flag: feature_flag, + environment_scope: 'production', active: true, + strategies: [{ name: 'default', + parameters: {} }]) + + expect(scope.errors[:strategies]).to be_empty + end + end + end + end + end + + describe '.enabled' do + subject { described_class.enabled } + + let!(:feature_flag_scope) do + create(:operations_feature_flag_scope, active: active) + end + + context 'when scope is active' do + let(:active) { true } + + it 'returns the scope' do + is_expected.to include(feature_flag_scope) + end + end + + context 'when scope is inactive' do + let(:active) { false } + + it 'returns an empty array' do + is_expected.not_to include(feature_flag_scope) + end + end + end + + describe '.disabled' do + subject { described_class.disabled } + + let!(:feature_flag_scope) do + create(:operations_feature_flag_scope, active: active) + end + + context 'when scope is active' do + let(:active) { true } + + it 'returns an empty array' do + is_expected.not_to include(feature_flag_scope) + end + end + + context 'when scope is inactive' do + let(:active) { false } + + it 'returns the scope' do + is_expected.to include(feature_flag_scope) + end + end + end + + describe '.for_unleash_client' do + it 'returns scopes for the specified project' do + project1 = create(:project) + project2 = create(:project) + expected_feature_flag = create(:operations_feature_flag, project: project1) + create(:operations_feature_flag, project: project2) + + scopes = described_class.for_unleash_client(project1, 'sandbox').to_a + + expect(scopes).to contain_exactly(*expected_feature_flag.scopes) + end + + it 'returns a scope that matches exactly over a match with a wild card' do + project = create(:project) + feature_flag = create(:operations_feature_flag, project: project) + create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'production*') + expected_scope = create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'production') + + scopes = described_class.for_unleash_client(project, 'production').to_a + + expect(scopes).to contain_exactly(expected_scope) + end + end +end diff --git a/spec/models/operations/feature_flag_spec.rb b/spec/models/operations/feature_flag_spec.rb new file mode 100644 index 00000000000..83d6c6b95a3 --- /dev/null +++ b/spec/models/operations/feature_flag_spec.rb @@ -0,0 +1,258 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Operations::FeatureFlag do + include FeatureFlagHelpers + + subject { create(:operations_feature_flag) } + + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:scopes) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } + it { is_expected.to define_enum_for(:version).with_values(legacy_flag: 1, new_version_flag: 2) } + + context 'a version 1 feature flag' do + it 'is valid if associated with Operations::FeatureFlagScope models' do + project = create(:project) + feature_flag = described_class.create({ name: 'test', project: project, version: 1, + scopes_attributes: [{ environment_scope: '*', active: false }] }) + + expect(feature_flag).to be_valid + end + + it 'is invalid if associated with Operations::FeatureFlags::Strategy models' do + project = create(:project) + feature_flag = described_class.create({ name: 'test', project: project, version: 1, + strategies_attributes: [{ name: 'default', parameters: {} }] }) + + expect(feature_flag.errors.messages).to eq({ + version_associations: ["version 1 feature flags may not have strategies"] + }) + end + end + + context 'a version 2 feature flag' do + it 'is invalid if associated with Operations::FeatureFlagScope models' do + project = create(:project) + feature_flag = described_class.create({ name: 'test', project: project, version: 2, + scopes_attributes: [{ environment_scope: '*', active: false }] }) + + expect(feature_flag.errors.messages).to eq({ + version_associations: ["version 2 feature flags may not have scopes"] + }) + end + + it 'is valid if associated with Operations::FeatureFlags::Strategy models' do + project = create(:project) + feature_flag = described_class.create({ name: 'test', project: project, version: 2, + strategies_attributes: [{ name: 'default', parameters: {} }] }) + + expect(feature_flag).to be_valid + end + end + + it_behaves_like 'AtomicInternalId', validate_presence: true do + let(:internal_id_attribute) { :iid } + let(:instance) { build(:operations_feature_flag) } + let(:scope) { :project } + let(:scope_attrs) { { project: instance.project } } + let(:usage) { :operations_feature_flags } + end + end + + describe 'feature flag version' do + it 'defaults to 1 if unspecified' do + project = create(:project) + + feature_flag = described_class.create(name: 'my_flag', project: project, active: true) + + expect(feature_flag).to be_valid + expect(feature_flag.version_before_type_cast).to eq(1) + end + end + + describe 'Scope creation' do + subject { described_class.new(**params) } + + let(:project) { create(:project) } + + let(:params) do + { name: 'test', project: project, scopes_attributes: scopes_attributes } + end + + let(:scopes_attributes) do + [{ environment_scope: '*', active: false }, + { environment_scope: 'review/*', active: true }] + end + + it { is_expected.to be_valid } + + context 'when the first scope is not wildcard' do + let(:scopes_attributes) do + [{ environment_scope: 'review/*', active: true }, + { environment_scope: '*', active: false }] + end + + it { is_expected.not_to be_valid } + end + end + + describe 'the default scope' do + let_it_be(:project) { create(:project) } + + context 'with a version 1 feature flag' do + it 'creates a default scope' do + feature_flag = described_class.create({ name: 'test', project: project, scopes_attributes: [], version: 1 }) + + expect(feature_flag.scopes.count).to eq(1) + expect(feature_flag.scopes.first.environment_scope).to eq('*') + end + + it 'allows specifying the default scope in the parameters' do + feature_flag = described_class.create({ name: 'test', project: project, + scopes_attributes: [{ environment_scope: '*', active: false }, + { environment_scope: 'review/*', active: true }], version: 1 }) + + expect(feature_flag.scopes.count).to eq(2) + expect(feature_flag.scopes.first.environment_scope).to eq('*') + end + end + + context 'with a version 2 feature flag' do + it 'does not create a default scope' do + feature_flag = described_class.create({ name: 'test', project: project, scopes_attributes: [], version: 2 }) + + expect(feature_flag.scopes).to eq([]) + end + end + end + + describe '.enabled' do + subject { described_class.enabled } + + context 'when the feature flag is active' do + let!(:feature_flag) { create(:operations_feature_flag, active: true) } + + it 'returns the flag' do + is_expected.to eq([feature_flag]) + end + end + + context 'when the feature flag is active and all scopes are inactive' do + let!(:feature_flag) { create(:operations_feature_flag, active: true) } + + it 'returns the flag' do + feature_flag.default_scope.update!(active: false) + + is_expected.to eq([feature_flag]) + end + end + + context 'when the feature flag is inactive' do + let!(:feature_flag) { create(:operations_feature_flag, active: false) } + + it 'does not return the flag' do + is_expected.to be_empty + end + end + + context 'when the feature flag is inactive and all scopes are active' do + let!(:feature_flag) { create(:operations_feature_flag, active: false) } + + it 'does not return the flag' do + feature_flag.default_scope.update!(active: true) + + is_expected.to be_empty + end + end + end + + describe '.disabled' do + subject { described_class.disabled } + + context 'when the feature flag is active' do + let!(:feature_flag) { create(:operations_feature_flag, active: true) } + + it 'does not return the flag' do + is_expected.to be_empty + end + end + + context 'when the feature flag is active and all scopes are inactive' do + let!(:feature_flag) { create(:operations_feature_flag, active: true) } + + it 'does not return the flag' do + feature_flag.default_scope.update!(active: false) + + is_expected.to be_empty + end + end + + context 'when the feature flag is inactive' do + let!(:feature_flag) { create(:operations_feature_flag, active: false) } + + it 'returns the flag' do + is_expected.to eq([feature_flag]) + end + end + + context 'when the feature flag is inactive and all scopes are active' do + let!(:feature_flag) { create(:operations_feature_flag, active: false) } + + it 'returns the flag' do + feature_flag.default_scope.update!(active: true) + + is_expected.to eq([feature_flag]) + end + end + end + + describe '.for_unleash_client' do + let_it_be(:project) { create(:project) } + let!(:feature_flag) do + create(:operations_feature_flag, project: project, + name: 'feature1', active: true, version: 2) + end + + let!(:strategy) do + create(:operations_strategy, feature_flag: feature_flag, + name: 'default', parameters: {}) + end + + it 'matches wild cards in the scope' do + create(:operations_scope, strategy: strategy, environment_scope: 'review/*') + + flags = described_class.for_unleash_client(project, 'review/feature-branch') + + expect(flags).to eq([feature_flag]) + end + + it 'matches wild cards case sensitively' do + create(:operations_scope, strategy: strategy, environment_scope: 'Staging/*') + + flags = described_class.for_unleash_client(project, 'staging/feature') + + expect(flags).to eq([]) + end + + it 'returns feature flags ordered by id' do + create(:operations_scope, strategy: strategy, environment_scope: 'production') + feature_flag_b = create(:operations_feature_flag, project: project, + name: 'feature2', active: true, version: 2) + strategy_b = create(:operations_strategy, feature_flag: feature_flag_b, + name: 'default', parameters: {}) + create(:operations_scope, strategy: strategy_b, environment_scope: '*') + + flags = described_class.for_unleash_client(project, 'production') + + expect(flags.map(&:id)).to eq([feature_flag.id, feature_flag_b.id]) + end + end +end diff --git a/spec/models/operations/feature_flags/strategy_spec.rb b/spec/models/operations/feature_flags/strategy_spec.rb new file mode 100644 index 00000000000..04e3ef26e9d --- /dev/null +++ b/spec/models/operations/feature_flags/strategy_spec.rb @@ -0,0 +1,323 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Operations::FeatureFlags::Strategy do + let_it_be(:project) { create(:project) } + + describe 'validations' do + it do + is_expected.to validate_inclusion_of(:name) + .in_array(%w[default gradualRolloutUserId userWithId gitlabUserList]) + .with_message('strategy name is invalid') + end + + describe 'parameters' do + context 'when the strategy name is invalid' do + where(:invalid_name) do + [nil, {}, [], 'nothing', 3] + end + with_them do + it 'skips parameters validation' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: invalid_name, parameters: { bad: 'params' }) + + expect(strategy.errors[:name]).to eq(['strategy name is invalid']) + expect(strategy.errors[:parameters]).to be_empty + end + end + end + + context 'when the strategy name is gradualRolloutUserId' do + where(:invalid_parameters) do + [nil, {}, { percentage: '40', groupId: 'mygroup', userIds: '4' }, { percentage: '40' }, + { percentage: '40', groupId: 'mygroup', extra: nil }, { groupId: 'mygroup' }] + end + with_them do + it 'must have valid parameters for the strategy' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'gradualRolloutUserId', parameters: invalid_parameters) + + expect(strategy.errors[:parameters]).to eq(['parameters are invalid']) + end + end + + it 'allows the parameters in any order' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'gradualRolloutUserId', + parameters: { percentage: '10', groupId: 'mygroup' }) + + expect(strategy.errors[:parameters]).to be_empty + end + + describe 'percentage' do + where(:invalid_value) do + [50, 40.0, { key: "value" }, "garbage", "00", "01", "101", "-1", "-10", "0100", + "1000", "10.0", "5%", "25%", "100hi", "e100", "30m", " ", "\r\n", "\n", "\t", + "\n10", "20\n", "\n100", "100\n", "\n ", nil] + end + with_them do + it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'gradualRolloutUserId', + parameters: { groupId: 'mygroup', percentage: invalid_value }) + + expect(strategy.errors[:parameters]).to eq(['percentage must be a string between 0 and 100 inclusive']) + end + end + + where(:valid_value) do + %w[0 1 10 38 100 93] + end + with_them do + it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'gradualRolloutUserId', + parameters: { groupId: 'mygroup', percentage: valid_value }) + + expect(strategy.errors[:parameters]).to eq([]) + end + end + end + + describe 'groupId' do + where(:invalid_value) do + [nil, 4, 50.0, {}, 'spaces bad', 'bad$', '%bad', '', '!bad', + '.bad', 'Bad', 'bad1', "", " ", "b" * 33, "ba_d", "ba\nd"] + end + with_them do + it 'must be a string value of up to 32 lowercase characters' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'gradualRolloutUserId', + parameters: { groupId: invalid_value, percentage: '40' }) + + expect(strategy.errors[:parameters]).to eq(['groupId parameter is invalid']) + end + end + + where(:valid_value) do + ["somegroup", "anothergroup", "okay", "g", "a" * 32] + end + with_them do + it 'must be a string value of up to 32 lowercase characters' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'gradualRolloutUserId', + parameters: { groupId: valid_value, percentage: '40' }) + + expect(strategy.errors[:parameters]).to eq([]) + end + end + end + end + + context 'when the strategy name is userWithId' do + where(:invalid_parameters) do + [nil, { userIds: 'sam', percentage: '40' }, { userIds: 'sam', some: 'param' }, { percentage: '40' }, {}] + end + with_them do + it 'must have valid parameters for the strategy' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'userWithId', parameters: invalid_parameters) + + expect(strategy.errors[:parameters]).to eq(['parameters are invalid']) + end + end + + describe 'userIds' do + where(:valid_value) do + ["", "sam", "1", "a", "uuid-of-some-kind", "sam,fred,tom,jane,joe,mike", + "gitlab@example.com", "123,4", "UPPER,Case,charActeRS", "0", + "$valid$email#2345#$%..{}+=-)?\\/@example.com", "spaces allowed", + "a" * 256, "a,#{'b' * 256},ccc", "many spaces"] + end + with_them do + it 'is valid with a string of comma separated values' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'userWithId', parameters: { userIds: valid_value }) + + expect(strategy.errors[:parameters]).to be_empty + end + end + + where(:invalid_value) do + [1, 2.5, {}, [], nil, "123\n456", "1,2,3,12\t3", "\n", "\n\r", + "joe\r,sam", "1,2,2", "1,,2", "1,2,,,,", "b" * 257, "1, ,2", "tim, ,7", " ", + " ", " ,1", "1, ", " leading,1", "1,trailing ", "1, both ,2"] + end + with_them do + it 'is invalid' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'userWithId', parameters: { userIds: invalid_value }) + + expect(strategy.errors[:parameters]).to include( + 'userIds must be a string of unique comma separated values each 256 characters or less' + ) + end + end + end + end + + context 'when the strategy name is default' do + where(:invalid_value) do + [{ groupId: "hi", percentage: "7" }, "", "nothing", 7, nil, [], 2.5] + end + with_them do + it 'must be empty' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'default', + parameters: invalid_value) + + expect(strategy.errors[:parameters]).to eq(['parameters are invalid']) + end + end + + it 'must be empty' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'default', + parameters: {}) + + expect(strategy.errors[:parameters]).to be_empty + end + end + + context 'when the strategy name is gitlabUserList' do + where(:invalid_value) do + [{ groupId: "default", percentage: "7" }, "", "nothing", 7, nil, [], 2.5, { userIds: 'user1' }] + end + with_them do + it 'must be empty' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'gitlabUserList', + parameters: invalid_value) + + expect(strategy.errors[:parameters]).to eq(['parameters are invalid']) + end + end + + it 'must be empty' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'gitlabUserList', + parameters: {}) + + expect(strategy.errors[:parameters]).to be_empty + end + end + end + + describe 'associations' do + context 'when name is gitlabUserList' do + it 'is valid when associated with a user list' do + feature_flag = create(:operations_feature_flag, project: project) + user_list = create(:operations_feature_flag_user_list, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'gitlabUserList', + user_list: user_list, + parameters: {}) + + expect(strategy.errors[:user_list]).to be_empty + end + + it 'is invalid without a user list' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'gitlabUserList', + parameters: {}) + + expect(strategy.errors[:user_list]).to eq(["can't be blank"]) + end + + it 'is invalid when associated with a user list from another project' do + other_project = create(:project) + feature_flag = create(:operations_feature_flag, project: project) + user_list = create(:operations_feature_flag_user_list, project: other_project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'gitlabUserList', + user_list: user_list, + parameters: {}) + + expect(strategy.errors[:user_list]).to eq(['must belong to the same project']) + end + end + + context 'when name is default' do + it 'is invalid when associated with a user list' do + feature_flag = create(:operations_feature_flag, project: project) + user_list = create(:operations_feature_flag_user_list, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'default', + user_list: user_list, + parameters: {}) + + expect(strategy.errors[:user_list]).to eq(['must be blank']) + end + + it 'is valid without a user list' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'default', + parameters: {}) + + expect(strategy.errors[:user_list]).to be_empty + end + end + + context 'when name is userWithId' do + it 'is invalid when associated with a user list' do + feature_flag = create(:operations_feature_flag, project: project) + user_list = create(:operations_feature_flag_user_list, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'userWithId', + user_list: user_list, + parameters: { userIds: 'user1' }) + + expect(strategy.errors[:user_list]).to eq(['must be blank']) + end + + it 'is valid without a user list' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'userWithId', + parameters: { userIds: 'user1' }) + + expect(strategy.errors[:user_list]).to be_empty + end + end + + context 'when name is gradualRolloutUserId' do + it 'is invalid when associated with a user list' do + feature_flag = create(:operations_feature_flag, project: project) + user_list = create(:operations_feature_flag_user_list, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'gradualRolloutUserId', + user_list: user_list, + parameters: { groupId: 'default', percentage: '10' }) + + expect(strategy.errors[:user_list]).to eq(['must be blank']) + end + + it 'is valid without a user list' do + feature_flag = create(:operations_feature_flag, project: project) + strategy = described_class.create(feature_flag: feature_flag, + name: 'gradualRolloutUserId', + parameters: { groupId: 'default', percentage: '10' }) + + expect(strategy.errors[:user_list]).to be_empty + end + end + end + end +end diff --git a/spec/models/operations/feature_flags/user_list_spec.rb b/spec/models/operations/feature_flags/user_list_spec.rb new file mode 100644 index 00000000000..020416aa7bc --- /dev/null +++ b/spec/models/operations/feature_flags/user_list_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Operations::FeatureFlags::UserList do + subject { create(:operations_feature_flag_user_list) } + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } + it { is_expected.to validate_length_of(:name).is_at_least(1).is_at_most(255) } + + describe 'user_xids' do + where(:valid_value) do + ["", "sam", "1", "a", "uuid-of-some-kind", "sam,fred,tom,jane,joe,mike", + "gitlab@example.com", "123,4", "UPPER,Case,charActeRS", "0", + "$valid$email#2345#$%..{}+=-)?\\/@example.com", "spaces allowed", + "a" * 256, "a,#{'b' * 256},ccc", "many spaces"] + end + with_them do + it 'is valid with a string of comma separated values' do + user_list = described_class.create(user_xids: valid_value) + + expect(user_list.errors[:user_xids]).to be_empty + end + end + + where(:typecast_value) do + [1, 2.5, {}, []] + end + with_them do + it 'automatically casts values of other types' do + user_list = described_class.create(user_xids: typecast_value) + + expect(user_list.errors[:user_xids]).to be_empty + expect(user_list.user_xids).to eq(typecast_value.to_s) + end + end + + where(:invalid_value) do + [nil, "123\n456", "1,2,3,12\t3", "\n", "\n\r", + "joe\r,sam", "1,2,2", "1,,2", "1,2,,,,", "b" * 257, "1, ,2", "tim, ,7", " ", + " ", " ,1", "1, ", " leading,1", "1,trailing ", "1, both ,2"] + end + with_them do + it 'is invalid' do + user_list = described_class.create(user_xids: invalid_value) + + expect(user_list.errors[:user_xids]).to include( + 'user_xids must be a string of unique comma separated values each 256 characters or less' + ) + end + end + end + end + + describe 'url_helpers' do + it 'generates paths based on the internal id' do + create(:operations_feature_flag_user_list) + project_b = create(:project) + list_b = create(:operations_feature_flag_user_list, project: project_b) + + path = ::Gitlab::Routing.url_helpers.project_feature_flags_user_list_path(project_b, list_b) + + expect(path).to eq("/#{project_b.full_path}/-/feature_flags_user_lists/#{list_b.iid}") + end + end + + describe '#destroy' do + it 'deletes the model if it is not associated with any feature flag strategies' do + project = create(:project) + user_list = described_class.create(project: project, name: 'My User List', user_xids: 'user1,user2') + + user_list.destroy + + expect(described_class.count).to eq(0) + end + + it 'does not delete the model if it is associated with a feature flag strategy' do + project = create(:project) + user_list = described_class.create(project: project, name: 'My User List', user_xids: 'user1,user2') + feature_flag = create(:operations_feature_flag, :new_version_flag, project: project) + strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'gitlabUserList', user_list: user_list) + + user_list.destroy + + expect(described_class.count).to eq(1) + expect(::Operations::FeatureFlags::StrategyUserList.count).to eq(1) + expect(strategy.reload.user_list).to eq(user_list) + expect(strategy.valid?).to eq(true) + end + end + + it_behaves_like 'AtomicInternalId' do + let(:internal_id_attribute) { :iid } + let(:instance) { build(:operations_feature_flag_user_list) } + let(:scope) { :project } + let(:scope_attrs) { { project: instance.project } } + let(:usage) { :operations_user_lists } + end +end diff --git a/spec/models/operations/feature_flags_client_spec.rb b/spec/models/operations/feature_flags_client_spec.rb new file mode 100644 index 00000000000..05988d676f3 --- /dev/null +++ b/spec/models/operations/feature_flags_client_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Operations::FeatureFlagsClient do + subject { create(:operations_feature_flags_client) } + + describe 'associations' do + it { is_expected.to belong_to(:project) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + end + + describe '#token' do + it "ensures that token is always set" do + expect(subject.token).not_to be_empty + end + end +end diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb index 7cc8e13d449..6b992dbc2a5 100644 --- a/spec/models/packages/package_file_spec.rb +++ b/spec/models/packages/package_file_spec.rb @@ -33,15 +33,17 @@ RSpec.describe Packages::PackageFile, type: :model do end context 'updating project statistics' do + let_it_be(:package, reload: true) { create(:package) } + context 'when the package file has an explicit size' do it_behaves_like 'UpdateProjectStatistics' do - subject { build(:package_file, :jar, size: 42) } + subject { build(:package_file, :jar, package: package, size: 42) } end end context 'when the package file does not have a size' do it_behaves_like 'UpdateProjectStatistics' do - subject { build(:package_file, size: nil) } + subject { build(:package_file, package: package, size: nil) } end end end diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index 4170bf595f0..ea1f75d04e7 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Packages::Package, type: :model do describe 'relationships' do it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:creator) } it { is_expected.to have_many(:package_files).dependent(:destroy) } it { is_expected.to have_many(:dependency_links).inverse_of(:package) } it { is_expected.to have_many(:tags).inverse_of(:package) } @@ -84,7 +85,7 @@ RSpec.describe Packages::Package, type: :model do end describe 'validations' do - subject { create(:package) } + subject { build(:package) } it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id, :version, :package_type) } @@ -95,7 +96,7 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.not_to allow_value("my(dom$$$ain)com.my-app").for(:name) } context 'conan package' do - subject { create(:conan_package) } + subject { build_stubbed(:conan_package) } let(:fifty_one_characters) {'f_b' * 17} @@ -112,7 +113,7 @@ RSpec.describe Packages::Package, type: :model do describe '#version' do RSpec.shared_examples 'validating version to be SemVer compliant for' do |factory_name| context "for #{factory_name}" do - subject { create(factory_name) } + subject { build_stubbed(factory_name) } it { is_expected.to allow_value('1.2.3').for(:version) } it { is_expected.to allow_value('1.2.3-beta').for(:version) } @@ -126,7 +127,7 @@ RSpec.describe Packages::Package, type: :model do end context 'conan package' do - subject { create(:conan_package) } + subject { build_stubbed(:conan_package) } let(:fifty_one_characters) {'1.2' * 17} @@ -142,7 +143,7 @@ RSpec.describe Packages::Package, type: :model do end context 'maven package' do - subject { create(:maven_package) } + subject { build_stubbed(:maven_package) } it { is_expected.to allow_value('0').for(:version) } it { is_expected.to allow_value('1').for(:version) } @@ -169,6 +170,92 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) } end + context 'pypi package' do + subject { create(:pypi_package) } + + it { is_expected.to allow_value('0.1').for(:version) } + it { is_expected.to allow_value('2.0').for(:version) } + it { is_expected.to allow_value('1.2.0').for(:version) } + it { is_expected.to allow_value('0100!0.0').for(:version) } + it { is_expected.to allow_value('00!1.2').for(:version) } + it { is_expected.to allow_value('1.0a').for(:version) } + it { is_expected.to allow_value('1.0-a').for(:version) } + it { is_expected.to allow_value('1.0.a1').for(:version) } + it { is_expected.to allow_value('1.0a1').for(:version) } + it { is_expected.to allow_value('1.0-a1').for(:version) } + it { is_expected.to allow_value('1.0alpha1').for(:version) } + it { is_expected.to allow_value('1.0b1').for(:version) } + it { is_expected.to allow_value('1.0beta1').for(:version) } + it { is_expected.to allow_value('1.0rc1').for(:version) } + it { is_expected.to allow_value('1.0pre1').for(:version) } + it { is_expected.to allow_value('1.0preview1').for(:version) } + it { is_expected.to allow_value('1.0.dev1').for(:version) } + it { is_expected.to allow_value('1.0.DEV1').for(:version) } + it { is_expected.to allow_value('1.0.post1').for(:version) } + it { is_expected.to allow_value('1.0.rev1').for(:version) } + it { is_expected.to allow_value('1.0.r1').for(:version) } + it { is_expected.to allow_value('1.0c2').for(:version) } + it { is_expected.to allow_value('2012.15').for(:version) } + it { is_expected.to allow_value('1.0+5').for(:version) } + it { is_expected.to allow_value('1.0+abc.5').for(:version) } + it { is_expected.to allow_value('1!1.1').for(:version) } + it { is_expected.to allow_value('1.0c3').for(:version) } + it { is_expected.to allow_value('1.0rc2').for(:version) } + it { is_expected.to allow_value('1.0c1').for(:version) } + it { is_expected.to allow_value('1.0b2-346').for(:version) } + it { is_expected.to allow_value('1.0b2.post345').for(:version) } + it { is_expected.to allow_value('1.0b2.post345.dev456').for(:version) } + it { is_expected.to allow_value('1.2.rev33+123456').for(:version) } + it { is_expected.to allow_value('1.1.dev1').for(:version) } + it { is_expected.to allow_value('1.0b1.dev456').for(:version) } + it { is_expected.to allow_value('1.0a12.dev456').for(:version) } + it { is_expected.to allow_value('1.0b2').for(:version) } + it { is_expected.to allow_value('1.0.dev456').for(:version) } + it { is_expected.to allow_value('1.0c1.dev456').for(:version) } + it { is_expected.to allow_value('1.0.post456').for(:version) } + it { is_expected.to allow_value('1.0.post456.dev34').for(:version) } + it { is_expected.to allow_value('1.2+123abc').for(:version) } + it { is_expected.to allow_value('1.2+abc').for(:version) } + it { is_expected.to allow_value('1.2+abc123').for(:version) } + it { is_expected.to allow_value('1.2+abc123def').for(:version) } + it { is_expected.to allow_value('1.2+1234.abc').for(:version) } + it { is_expected.to allow_value('1.2+123456').for(:version) } + it { is_expected.to allow_value('1.2.r32+123456').for(:version) } + it { is_expected.to allow_value('1!1.2.rev33+123456').for(:version) } + it { is_expected.to allow_value('1.0a12').for(:version) } + it { is_expected.to allow_value('1.2.3-45+abcdefgh').for(:version) } + it { is_expected.to allow_value('v1.2.3').for(:version) } + it { is_expected.not_to allow_value('1.2.3-45-abcdefgh').for(:version) } + it { is_expected.not_to allow_value('..1.2.3').for(:version) } + it { is_expected.not_to allow_value(' 1.2.3').for(:version) } + it { is_expected.not_to allow_value("1.2.3 \r\t").for(:version) } + it { is_expected.not_to allow_value("\r\t 1.2.3").for(:version) } + it { is_expected.not_to allow_value('1./2.3').for(:version) } + it { is_expected.not_to allow_value('1.2.3-4/../../').for(:version) } + it { is_expected.not_to allow_value('1.2.3-4%2e%2e%').for(:version) } + it { is_expected.not_to allow_value('../../../../../1.2.3').for(:version) } + it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) } + end + + context 'generic package' do + subject { build_stubbed(:generic_package) } + + it { is_expected.to validate_presence_of(:version) } + it { is_expected.to allow_value('1.2.3').for(:version) } + it { is_expected.to allow_value('1.3.350').for(:version) } + it { is_expected.not_to allow_value('1.3.350-20201230123456').for(:version) } + it { is_expected.not_to allow_value('..1.2.3').for(:version) } + it { is_expected.not_to allow_value(' 1.2.3').for(:version) } + it { is_expected.not_to allow_value("1.2.3 \r\t").for(:version) } + it { is_expected.not_to allow_value("\r\t 1.2.3").for(:version) } + it { is_expected.not_to allow_value('1.2.3-4/../../').for(:version) } + it { is_expected.not_to allow_value('1.2.3-4%2e%2e%').for(:version) } + it { is_expected.not_to allow_value('../../../../../1.2.3').for(:version) } + it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) } + it { is_expected.not_to allow_value('').for(:version) } + it { is_expected.not_to allow_value(nil).for(:version) } + end + it_behaves_like 'validating version to be SemVer compliant for', :npm_package it_behaves_like 'validating version to be SemVer compliant for', :nuget_package end @@ -178,7 +265,7 @@ RSpec.describe Packages::Package, type: :model do let!(:package) { create(:npm_package) } it 'will not allow a package of the same name' do - new_package = build(:npm_package, name: package.name) + new_package = build(:npm_package, project: create(:project), name: package.name) expect(new_package).not_to be_valid end @@ -482,4 +569,23 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.to contain_exactly(*tags) } end end + + describe 'plan_limits' do + Packages::Package.package_types.keys.without('composer').each do |pt| + plan_limit_name = if pt == 'generic' + "#{pt}_packages_max_file_size" + else + "#{pt}_max_file_size" + end + + context "File size limits for #{pt}" do + let(:package) { create("#{pt}_package") } + + it "plan_limits includes column #{plan_limit_name}" do + expect { package.project.actual_limits.send(plan_limit_name) } + .not_to raise_error(NoMethodError) + end + end + end + end end diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb index 38bd9b39a56..cb1938a0113 100644 --- a/spec/models/pages/lookup_path_spec.rb +++ b/spec/models/pages/lookup_path_spec.rb @@ -3,20 +3,20 @@ require 'spec_helper' RSpec.describe Pages::LookupPath do - let(:project) do - instance_double(Project, - id: 12345, - private_pages?: true, - pages_https_only?: true, - full_path: 'the/full/path' - ) + let_it_be(:project) do + create(:project, :pages_private, pages_https_only: true) end subject(:lookup_path) { described_class.new(project) } + before do + stub_pages_setting(access_control: true, external_https: ["1.1.1.1:443"]) + stub_artifacts_object_storage + end + describe '#project_id' do it 'delegates to Project#id' do - expect(lookup_path.project_id).to eq(12345) + expect(lookup_path.project_id).to eq(project.id) end end @@ -47,12 +47,49 @@ RSpec.describe Pages::LookupPath do end describe '#source' do - it 'sets the source type to "file"' do - expect(lookup_path.source[:type]).to eq('file') + shared_examples 'uses disk storage' do + it 'sets the source type to "file"' do + expect(lookup_path.source[:type]).to eq('file') + end + + it 'sets the source path to the project full path suffixed with "public/' do + expect(lookup_path.source[:path]).to eq(project.full_path + "/public/") + end end - it 'sets the source path to the project full path suffixed with "public/' do - expect(lookup_path.source[:path]).to eq('the/full/path/public/') + include_examples 'uses disk storage' + + context 'when artifact_id from build job is present in pages metadata' do + let(:artifacts_archive) { create(:ci_job_artifact, :zip, :remote_store, project: project) } + + before do + project.mark_pages_as_deployed(artifacts_archive: artifacts_archive) + end + + it 'sets the source type to "zip"' do + expect(lookup_path.source[:type]).to eq('zip') + end + + it 'sets the source path to the artifacts archive URL' do + Timecop.freeze do + expect(lookup_path.source[:path]).to eq(artifacts_archive.file.url(expire_at: 1.day.from_now)) + expect(lookup_path.source[:path]).to include("Expires=86400") + end + end + + context 'when artifact is not uploaded to object storage' do + let(:artifacts_archive) { create(:ci_job_artifact, :zip) } + + include_examples 'uses disk storage' + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(pages_artifacts_archive: false) + end + + include_examples 'uses disk storage' + end end end diff --git a/spec/models/pages_deployment_spec.rb b/spec/models/pages_deployment_spec.rb new file mode 100644 index 00000000000..eafff1ed59a --- /dev/null +++ b/spec/models/pages_deployment_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe PagesDeployment do + describe 'associations' do + it { is_expected.to belong_to(:project).required } + it { is_expected.to belong_to(:ci_build).optional } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:file) } + it { is_expected.to validate_presence_of(:size) } + it { is_expected.to validate_numericality_of(:size).only_integer.is_greater_than(0) } + it { is_expected.to validate_inclusion_of(:file_store).in_array(ObjectStorage::SUPPORTED_STORES) } + + it 'is valid when created from the factory' do + expect(create(:pages_deployment)).to be_valid + end + end +end diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index 78980f8cdab..1d5369e608e 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -354,16 +354,6 @@ RSpec.describe PagesDomain do domain.destroy! end - it 'delegates to Projects::UpdatePagesConfigurationService when not running async' do - stub_feature_flags(async_update_pages_config: false) - - service = instance_double('Projects::UpdatePagesConfigurationService') - expect(Projects::UpdatePagesConfigurationService).to receive(:new) { service } - expect(service).to receive(:execute) - - create(:pages_domain, project: project) - end - it "schedules a PagesUpdateConfigurationWorker" do expect(PagesUpdateConfigurationWorker).to receive(:perform_async).with(project.id) diff --git a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb index 61174a7d0c5..634690d5d0b 100644 --- a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb +++ b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb @@ -219,20 +219,93 @@ RSpec.describe PerformanceMonitoring::PrometheusDashboard do end describe '#schema_validation_warnings' do - context 'when schema is valid' do - it 'returns nil' do - expect(described_class).to receive(:from_json) - expect(described_class.new.schema_validation_warnings).to be_nil + let(:environment) { create(:environment, project: project) } + let(:path) { '.gitlab/dashboards/test.yml' } + let(:project) { create(:project, :repository, :custom_repo, files: { path => dashboard_schema.to_yaml }) } + + subject(:schema_validation_warnings) { described_class.new(dashboard_schema.merge(path: path, environment: environment)).schema_validation_warnings } + + before do + allow(Gitlab::Metrics::Dashboard::Finder).to receive(:find_raw).with(project, dashboard_path: path).and_call_original + end + + context 'metrics_dashboard_exhaustive_validations is on' do + before do + stub_feature_flags(metrics_dashboard_exhaustive_validations: true) + end + + context 'when schema is valid' do + let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) } + + it 'returns empty array' do + expect(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).with(dashboard_schema, dashboard_path: path, project: project).and_return([]) + + expect(schema_validation_warnings).to eq [] + end + end + + context 'when schema is invalid' do + let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) } + + it 'returns array with errors messages' do + error = ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new + + expect(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).with(dashboard_schema, dashboard_path: path, project: project).and_return([error]) + + expect(schema_validation_warnings).to eq [error.message] + end + end + + context 'when YAML has wrong syntax' do + let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) } + + subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings } + + it 'returns array with errors messages' do + expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors) + + expect(schema_validation_warnings).to eq ['Invalid yaml'] + end end end - context 'when schema is invalid' do - it 'returns array with errors messages' do - instance = described_class.new - instance.errors.add(:test, 'test error') + context 'metrics_dashboard_exhaustive_validations is off' do + before do + stub_feature_flags(metrics_dashboard_exhaustive_validations: false) + end + + context 'when schema is valid' do + let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) } + + it 'returns empty array' do + expect(described_class).to receive(:from_json).with(dashboard_schema) + + expect(schema_validation_warnings).to eq [] + end + end + + context 'when schema is invalid' do + let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) } + + it 'returns array with errors messages' do + instance = described_class.new + instance.errors.add(:test, 'test error') + + expect(described_class).to receive(:from_json).and_raise(ActiveModel::ValidationError.new(instance)) + expect(described_class.new.schema_validation_warnings).to eq ['test: test error'] + end + end - expect(described_class).to receive(:from_json).and_raise(ActiveModel::ValidationError.new(instance)) - expect(described_class.new.schema_validation_warnings).to eq ['test: test error'] + context 'when YAML has wrong syntax' do + let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) } + + subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings } + + it 'returns array with errors messages' do + expect(described_class).not_to receive(:from_json) + + expect(schema_validation_warnings).to eq ['Invalid yaml'] + end end end end diff --git a/spec/models/product_analytics_event_spec.rb b/spec/models/product_analytics_event_spec.rb index afdb5b690f8..286729b8398 100644 --- a/spec/models/product_analytics_event_spec.rb +++ b/spec/models/product_analytics_event_spec.rb @@ -35,4 +35,29 @@ RSpec.describe ProductAnalyticsEvent, type: :model do it { expect(described_class.count_by_graph('platform', 7.days)).to eq({ 'app' => 1, 'web' => 2 }) } it { expect(described_class.count_by_graph('platform', 30.days)).to eq({ 'app' => 1, 'mobile' => 1, 'web' => 2 }) } end + + describe '.by_category_and_action' do + let_it_be(:event) { create(:product_analytics_event, se_category: 'catA', se_action: 'actA') } + + before do + create(:product_analytics_event, se_category: 'catA', se_action: 'actB') + create(:product_analytics_event, se_category: 'catB', se_action: 'actA') + end + + it { expect(described_class.by_category_and_action('catA', 'actA')).to match_array([event]) } + end + + describe '.count_collector_tstamp_by_day' do + let_it_be(:time_now) { Time.zone.now } + let_it_be(:time_ago) { Time.zone.now - 5.days } + + let_it_be(:events) do + create_list(:product_analytics_event, 3, collector_tstamp: time_now) + + create_list(:product_analytics_event, 2, collector_tstamp: time_ago) + end + + subject { described_class.count_collector_tstamp_by_day(7.days) } + + it { is_expected.to eq({ time_now.beginning_of_day => 3, time_ago.beginning_of_day => 2 }) } + end end diff --git a/spec/models/project_feature_usage_spec.rb b/spec/models/project_feature_usage_spec.rb new file mode 100644 index 00000000000..908b98ee9c2 --- /dev/null +++ b/spec/models/project_feature_usage_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ProjectFeatureUsage, type: :model do + describe '.jira_dvcs_integrations_enabled_count' do + it 'returns count of projects with Jira DVCS Cloud enabled' do + create(:project).feature_usage.log_jira_dvcs_integration_usage + create(:project).feature_usage.log_jira_dvcs_integration_usage + + expect(described_class.with_jira_dvcs_integration_enabled.count).to eq(2) + end + + it 'returns count of projects with Jira DVCS Server enabled' do + create(:project).feature_usage.log_jira_dvcs_integration_usage(cloud: false) + create(:project).feature_usage.log_jira_dvcs_integration_usage(cloud: false) + + expect(described_class.with_jira_dvcs_integration_enabled(cloud: false).count).to eq(2) + end + end + + describe '#log_jira_dvcs_integration_usage' do + let(:project) { create(:project) } + + subject { project.feature_usage } + + it 'logs Jira DVCS Cloud last sync' do + Timecop.freeze do + subject.log_jira_dvcs_integration_usage + + expect(subject.jira_dvcs_server_last_sync_at).to be_nil + expect(subject.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.current) + end + end + + it 'logs Jira DVCS Server last sync' do + Timecop.freeze do + subject.log_jira_dvcs_integration_usage(cloud: false) + + expect(subject.jira_dvcs_server_last_sync_at).to be_like_time(Time.current) + expect(subject.jira_dvcs_cloud_last_sync_at).to be_nil + end + end + + context 'when log_jira_dvcs_integration_usage is called simultaneously for the same project' do + it 'logs the latest call' do + feature_usage = project.feature_usage + feature_usage.log_jira_dvcs_integration_usage + first_logged_at = feature_usage.jira_dvcs_cloud_last_sync_at + + Timecop.freeze(1.hour.from_now) do + ProjectFeatureUsage.new(project_id: project.id).log_jira_dvcs_integration_usage + end + + expect(feature_usage.reload.jira_dvcs_cloud_last_sync_at).to be > first_logged_at + end + end + end +end diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index 4d2474cc56a..45afbcca96d 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do let_it_be(:project) { create(:project) } subject(:service) do - described_class.create( + described_class.create!( project: project, properties: { bamboo_url: bamboo_url, @@ -85,7 +85,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do bamboo_service = service bamboo_service.bamboo_url = 'http://gitlab1.com' - bamboo_service.save + bamboo_service.save! expect(bamboo_service.password).to be_nil end @@ -94,7 +94,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do bamboo_service = service bamboo_service.username = 'some_name' - bamboo_service.save + bamboo_service.save! expect(bamboo_service.password).to eq('password') end @@ -104,7 +104,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do bamboo_service.bamboo_url = 'http://gitlab_edited.com' bamboo_service.password = 'password' - bamboo_service.save + bamboo_service.save! expect(bamboo_service.password).to eq('password') expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com') @@ -117,7 +117,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do bamboo_service.bamboo_url = 'http://gitlab_edited.com' bamboo_service.password = 'password' - bamboo_service.save + bamboo_service.save! expect(bamboo_service.password).to eq('password') expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com') diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb index 3d0c2cc1006..f6bf1551bf0 100644 --- a/spec/models/project_services/buildkite_service_spec.rb +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -9,7 +9,7 @@ RSpec.describe BuildkiteService, :use_clean_rails_memory_store_caching do let(:project) { create(:project) } subject(:service) do - described_class.create( + described_class.create!( project: project, properties: { service_hook: true, diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb index 45be5212508..02b266e4fae 100644 --- a/spec/models/project_services/chat_message/merge_message_spec.rb +++ b/spec/models/project_services/chat_message/merge_message_spec.rb @@ -29,23 +29,6 @@ RSpec.describe ChatMessage::MergeMessage do } end - # Integration point in EE - context 'when state is overridden' do - it 'respects the overridden state' do - allow(subject).to receive(:state_or_action_text) { 'devoured' } - - aggregate_failures do - expect(subject.summary).not_to include('opened') - expect(subject.summary).to include('devoured') - - activity_title = subject.activity[:title] - - expect(activity_title).not_to include('opened') - expect(activity_title).to include('devoured') - end - end - end - context 'without markdown' do let(:color) { '#345' } @@ -106,4 +89,56 @@ RSpec.describe ChatMessage::MergeMessage do end end end + + context 'approved' do + before do + args[:object_attributes][:action] = 'approved' + end + + it 'returns a message regarding completed approval of merge requests' do + expect(subject.pretext).to eq( + 'Test User (test.user) approved merge request '\ + 'in ') + expect(subject.attachments).to be_empty + end + end + + context 'unapproved' do + before do + args[:object_attributes][:action] = 'unapproved' + end + + it 'returns a message regarding revocation of completed approval of merge requests' do + expect(subject.pretext).to eq( + 'Test User (test.user) unapproved merge request '\ + 'in ') + expect(subject.attachments).to be_empty + end + end + + context 'approval' do + before do + args[:object_attributes][:action] = 'approval' + end + + it 'returns a message regarding added approval of merge requests' do + expect(subject.pretext).to eq( + 'Test User (test.user) added their approval to merge request '\ + 'in ') + expect(subject.attachments).to be_empty + end + end + + context 'unapproval' do + before do + args[:object_attributes][:action] = 'unapproval' + end + + it 'returns a message regarding revoking approval of merge requests' do + expect(subject.pretext).to eq( + 'Test User (test.user) removed their approval from merge request '\ + 'in ') + expect(subject.attachments).to be_empty + end + end end diff --git a/spec/models/project_services/ewm_service_spec.rb b/spec/models/project_services/ewm_service_spec.rb new file mode 100644 index 00000000000..311c456569e --- /dev/null +++ b/spec/models/project_services/ewm_service_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EwmService do + describe 'Associations' do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'when service is active' do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of(:project_url) } + it { is_expected.to validate_presence_of(:issues_url) } + it { is_expected.to validate_presence_of(:new_issue_url) } + it_behaves_like 'issue tracker service URL attribute', :project_url + it_behaves_like 'issue tracker service URL attribute', :issues_url + it_behaves_like 'issue tracker service URL attribute', :new_issue_url + end + + context 'when service is inactive' do + before do + subject.active = false + end + + it { is_expected.not_to validate_presence_of(:project_url) } + it { is_expected.not_to validate_presence_of(:issues_url) } + it { is_expected.not_to validate_presence_of(:new_issue_url) } + end + end + + describe "ReferencePatternValidation" do + it "extracts bug" do + expect(described_class.reference_pattern.match("This is bug 123")[:issue]).to eq("bug 123") + end + + it "extracts task" do + expect(described_class.reference_pattern.match("This is task 123.")[:issue]).to eq("task 123") + end + + it "extracts work item" do + expect(described_class.reference_pattern.match("This is work item 123 now")[:issue]).to eq("work item 123") + end + + it "extracts workitem" do + expect(described_class.reference_pattern.match("workitem 123 at the beginning")[:issue]).to eq("workitem 123") + end + + it "extracts defect" do + expect(described_class.reference_pattern.match("This is defect 123 defect")[:issue]).to eq("defect 123") + end + + it "extracts rtcwi" do + expect(described_class.reference_pattern.match("This is rtcwi 123")[:issue]).to eq("rtcwi 123") + end + end +end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 28bba893be4..7741fea8717 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -10,6 +10,11 @@ RSpec.describe JiraService do let(:username) { 'jira-username' } let(:password) { 'jira-password' } let(:transition_id) { 'test27' } + let(:server_info_results) { { 'deploymentType' => 'Cloud' } } + + before do + WebMock.stub_request(:get, /serverInfo/).to_return(body: server_info_results.to_json ) + end describe '#options' do let(:options) do @@ -23,7 +28,7 @@ RSpec.describe JiraService do } end - let(:service) { described_class.create(options) } + let(:service) { described_class.create!(options) } it 'sets the URL properly' do # jira-ruby gem parses the URI and handles trailing slashes fine: @@ -97,13 +102,13 @@ RSpec.describe JiraService do } end - subject { described_class.create(params) } + subject { described_class.create!(params) } it 'does not store data into properties' do expect(subject.properties).to be_nil end - it 'stores data in data_fields correcty' do + it 'stores data in data_fields correctly' do service = subject expect(service.jira_tracker_data.url).to eq(url) @@ -111,6 +116,35 @@ RSpec.describe JiraService do expect(service.jira_tracker_data.username).to eq(username) expect(service.jira_tracker_data.password).to eq(password) expect(service.jira_tracker_data.jira_issue_transition_id).to eq(transition_id) + expect(service.jira_tracker_data.deployment_cloud?).to be_truthy + end + + context 'when loading serverInfo' do + let!(:jira_service) { subject } + + context 'Cloud instance' do + let(:server_info_results) { { 'deploymentType' => 'Cloud' } } + + it 'is detected' do + expect(jira_service.jira_tracker_data.deployment_cloud?).to be_truthy + end + end + + context 'Server instance' do + let(:server_info_results) { { 'deploymentType' => 'Server' } } + + it 'is detected' do + expect(jira_service.jira_tracker_data.deployment_server?).to be_truthy + end + end + + context 'Unknown instance' do + let(:server_info_results) { { 'deploymentType' => 'FutureCloud' } } + + it 'is detected' do + expect(jira_service.jira_tracker_data.deployment_unknown?).to be_truthy + end + end end end @@ -151,11 +185,11 @@ RSpec.describe JiraService do describe '#update' do context 'basic update' do - let(:new_username) { 'new_username' } - let(:new_url) { 'http://jira-new.example.com' } + let_it_be(:new_username) { 'new_username' } + let_it_be(:new_url) { 'http://jira-new.example.com' } before do - service.update(username: new_username, url: new_url) + service.update!(username: new_username, url: new_url) end it 'leaves properties field emtpy' do @@ -173,6 +207,63 @@ RSpec.describe JiraService do end end + context 'when updating the url, api_url, username, or password' do + it 'updates deployment type' do + service.update!(url: 'http://first.url') + service.jira_tracker_data.update!(deployment_type: 'server') + + expect(service.jira_tracker_data.deployment_server?).to be_truthy + + service.update!(api_url: 'http://another.url') + service.jira_tracker_data.reload + + expect(service.jira_tracker_data.deployment_cloud?).to be_truthy + expect(WebMock).to have_requested(:get, /serverInfo/).twice + end + + it 'calls serverInfo for url' do + service.update!(url: 'http://first.url') + + expect(WebMock).to have_requested(:get, /serverInfo/) + end + + it 'calls serverInfo for api_url' do + service.update!(api_url: 'http://another.url') + + expect(WebMock).to have_requested(:get, /serverInfo/) + end + + it 'calls serverInfo for username' do + service.update!(username: 'test-user') + + expect(WebMock).to have_requested(:get, /serverInfo/) + end + + it 'calls serverInfo for password' do + service.update!(password: 'test-password') + + expect(WebMock).to have_requested(:get, /serverInfo/) + end + end + + context 'when not updating the url, api_url, username, or password' do + it 'does not update deployment type' do + expect {service.update!(jira_issue_transition_id: 'jira_issue_transition_id')}.to raise_error(ActiveRecord::RecordInvalid) + + expect(WebMock).not_to have_requested(:get, /serverInfo/) + end + end + + context 'when not allowed to test an instance or group' do + it 'does not update deployment type' do + allow(service).to receive(:can_test?).and_return(false) + + service.update!(url: 'http://first.url') + + expect(WebMock).not_to have_requested(:get, /serverInfo/) + end + end + context 'stored password invalidation' do context 'when a password was previously set' do context 'when only web url present' do @@ -187,7 +278,7 @@ RSpec.describe JiraService do it 'resets password if url changed' do service service.url = 'http://jira_edited.example.com' - service.save + service.save! expect(service.reload.url).to eq('http://jira_edited.example.com') expect(service.password).to be_nil @@ -195,7 +286,7 @@ RSpec.describe JiraService do it 'does not reset password if url "changed" to the same url as before' do service.url = 'http://jira.example.com' - service.save + service.save! expect(service.reload.url).to eq('http://jira.example.com') expect(service.password).not_to be_nil @@ -203,7 +294,7 @@ RSpec.describe JiraService do it 'resets password if url not changed but api url added' do service.api_url = 'http://jira_edited.example.com/rest/api/2' - service.save + service.save! expect(service.reload.api_url).to eq('http://jira_edited.example.com/rest/api/2') expect(service.password).to be_nil @@ -212,7 +303,7 @@ RSpec.describe JiraService do it 'does not reset password if new url is set together with password, even if it\'s the same password' do service.url = 'http://jira_edited.example.com' service.password = password - service.save + service.save! expect(service.password).to eq(password) expect(service.url).to eq('http://jira_edited.example.com') @@ -221,14 +312,14 @@ RSpec.describe JiraService do it 'resets password if url changed, even if setter called multiple times' do service.url = 'http://jira1.example.com/rest/api/2' service.url = 'http://jira1.example.com/rest/api/2' - service.save + service.save! expect(service.password).to be_nil end it 'does not reset password if username changed' do service.username = 'some_name' - service.save + service.save! expect(service.reload.password).to eq(password) end @@ -236,7 +327,7 @@ RSpec.describe JiraService do it 'does not reset password if password changed' do service.url = 'http://jira_edited.example.com' service.password = 'new_password' - service.save + service.save! expect(service.reload.password).to eq('new_password') end @@ -244,7 +335,7 @@ RSpec.describe JiraService do it 'does not reset password if the password is touched and same as before' do service.url = 'http://jira_edited.example.com' service.password = password - service.save + service.save! expect(service.reload.password).to eq(password) end @@ -261,20 +352,20 @@ RSpec.describe JiraService do it 'resets password if api url changed' do service.api_url = 'http://jira_edited.example.com/rest/api/2' - service.save + service.save! expect(service.password).to be_nil end it 'does not reset password if url changed' do service.url = 'http://jira_edited.example.com' - service.save + service.save! expect(service.password).to eq(password) end it 'resets password if api url set to empty' do - service.update(api_url: '') + service.update!(api_url: '') expect(service.reload.password).to be_nil end @@ -291,7 +382,7 @@ RSpec.describe JiraService do it 'saves password if new url is set together with password' do service.url = 'http://jira_edited.example.com/rest/api/2' service.password = 'password' - service.save + service.save! expect(service.reload.password).to eq('password') expect(service.reload.url).to eq('http://jira_edited.example.com/rest/api/2') end @@ -360,7 +451,7 @@ RSpec.describe JiraService do allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return('JIRA-123') allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) - @jira_service.save + @jira_service.save! project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123' @transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions' @@ -627,32 +718,32 @@ RSpec.describe JiraService do end describe '#test' do + let(:server_info_results) { { 'url' => 'http://url', 'deploymentType' => 'Cloud' } } + let_it_be(:project) { create(:project, :repository) } let(:jira_service) do described_class.new( url: url, + project: project, username: username, password: password ) end - def test_settings(url = 'jira.example.com') - test_url = "http://#{url}/rest/api/2/serverInfo" - - WebMock.stub_request(:get, test_url).with(basic_auth: [username, password]) - .to_return(body: { url: 'http://url' }.to_json ) - + def server_info jira_service.test(nil) end context 'when the test succeeds' do it 'gets Jira project with URL when API URL not set' do - expect(test_settings).to eq(success: true, result: { 'url' => 'http://url' }) + expect(server_info).to eq(success: true, result: server_info_results) + expect(WebMock).to have_requested(:get, /jira.example.com/) end it 'gets Jira project with API URL if set' do - jira_service.update(api_url: 'http://jira.api.com') + jira_service.update!(api_url: 'http://jira.api.com') - expect(test_settings('jira.api.com')).to eq(success: true, result: { 'url' => 'http://url' }) + expect(server_info).to eq(success: true, result: server_info_results) + expect(WebMock).to have_requested(:get, /jira.api.com/) end end diff --git a/spec/models/project_services/packagist_service_spec.rb b/spec/models/project_services/packagist_service_spec.rb index f710385b6e2..33b5c9809c7 100644 --- a/spec/models/project_services/packagist_service_spec.rb +++ b/spec/models/project_services/packagist_service_spec.rb @@ -3,20 +3,6 @@ require 'spec_helper' RSpec.describe PackagistService do - describe "Associations" do - it { is_expected.to belong_to :project } - it { is_expected.to have_one :service_hook } - end - - let(:project) { create(:project) } - - let(:packagist_server) { 'https://packagist.example.com' } - let(:packagist_username) { 'theUser' } - let(:packagist_token) { 'verySecret' } - let(:packagist_hook_url) do - "#{packagist_server}/api/update-package?username=#{packagist_username}&apiToken=#{packagist_token}" - end - let(:packagist_params) do { active: true, @@ -29,11 +15,25 @@ RSpec.describe PackagistService do } end + let(:packagist_hook_url) do + "#{packagist_server}/api/update-package?username=#{packagist_username}&apiToken=#{packagist_token}" + end + + let(:packagist_token) { 'verySecret' } + let(:packagist_username) { 'theUser' } + let(:packagist_server) { 'https://packagist.example.com' } + let(:project) { create(:project) } + + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + describe '#execute' do let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) } - let(:packagist_service) { described_class.create(packagist_params) } + let(:packagist_service) { described_class.create!(packagist_params) } before do stub_request(:post, packagist_hook_url) diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb index 9a8386c619e..21cc5d44558 100644 --- a/spec/models/project_services/pipelines_email_service_spec.rb +++ b/spec/models/project_services/pipelines_email_service_spec.rb @@ -81,7 +81,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'when pipeline is succeeded' do before do data[:object_attributes][:status] = 'success' - pipeline.update(status: 'success') + pipeline.update!(status: 'success') end it_behaves_like 'sending email' @@ -91,7 +91,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'on default branch' do before do data[:object_attributes][:ref] = project.default_branch - pipeline.update(ref: project.default_branch) + pipeline.update!(ref: project.default_branch) end context 'notifications are enabled only for default branch' do @@ -115,7 +115,7 @@ RSpec.describe PipelinesEmailService, :mailer do before do create(:protected_branch, project: project, name: 'a-protected-branch') data[:object_attributes][:ref] = 'a-protected-branch' - pipeline.update(ref: 'a-protected-branch') + pipeline.update!(ref: 'a-protected-branch') end context 'notifications are enabled only for default branch' do @@ -138,7 +138,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'on a neither protected nor default branch' do before do data[:object_attributes][:ref] = 'a-random-branch' - pipeline.update(ref: 'a-random-branch') + pipeline.update!(ref: 'a-random-branch') end context 'notifications are enabled only for default branch' do @@ -177,7 +177,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'with succeeded pipeline' do before do data[:object_attributes][:status] = 'success' - pipeline.update(status: 'success') + pipeline.update!(status: 'success') end it_behaves_like 'not sending email' @@ -195,7 +195,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'with succeeded pipeline' do before do data[:object_attributes][:status] = 'success' - pipeline.update(status: 'success') + pipeline.update!(status: 'success') end it_behaves_like 'not sending email' @@ -206,7 +206,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'on default branch' do before do data[:object_attributes][:ref] = project.default_branch - pipeline.update(ref: project.default_branch) + pipeline.update!(ref: project.default_branch) end context 'notifications are enabled only for default branch' do @@ -230,7 +230,7 @@ RSpec.describe PipelinesEmailService, :mailer do before do create(:protected_branch, project: project, name: 'a-protected-branch') data[:object_attributes][:ref] = 'a-protected-branch' - pipeline.update(ref: 'a-protected-branch') + pipeline.update!(ref: 'a-protected-branch') end context 'notifications are enabled only for default branch' do @@ -253,7 +253,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'on a neither protected nor default branch' do before do data[:object_attributes][:ref] = 'a-random-branch' - pipeline.update(ref: 'a-random-branch') + pipeline.update!(ref: 'a-random-branch') end context 'notifications are enabled only for default branch' do @@ -281,7 +281,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'with failed pipeline' do before do data[:object_attributes][:status] = 'failed' - pipeline.update(status: 'failed') + pipeline.update!(status: 'failed') end it_behaves_like 'not sending email' @@ -295,7 +295,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'with failed pipeline' do before do data[:object_attributes][:status] = 'failed' - pipeline.update(status: 'failed') + pipeline.update!(status: 'failed') end it_behaves_like 'sending email' diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index a3fda33664a..f71dad86a08 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do let(:project) { create(:project) } subject(:service) do - described_class.create( + described_class.create!( project: project, properties: { teamcity_url: teamcity_url, @@ -85,7 +85,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do teamcity_service = service teamcity_service.teamcity_url = 'http://gitlab1.com' - teamcity_service.save + teamcity_service.save! expect(teamcity_service.password).to be_nil end @@ -94,7 +94,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do teamcity_service = service teamcity_service.username = 'some_name' - teamcity_service.save + teamcity_service.save! expect(teamcity_service.password).to eq('password') end @@ -104,7 +104,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do teamcity_service.teamcity_url = 'http://gitlab_edited.com' teamcity_service.password = 'password' - teamcity_service.save + teamcity_service.save! expect(teamcity_service.password).to eq('password') expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com') @@ -117,7 +117,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do teamcity_service.teamcity_url = 'http://gitlab_edited.com' teamcity_service.password = 'password' - teamcity_service.save + teamcity_service.save! expect(teamcity_service.password).to eq('password') expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com') diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f589589af8f..fe971832695 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -61,6 +61,7 @@ RSpec.describe Project do it { is_expected.to have_one(:youtrack_service) } it { is_expected.to have_one(:custom_issue_tracker_service) } it { is_expected.to have_one(:bugzilla_service) } + it { is_expected.to have_one(:ewm_service) } it { is_expected.to have_one(:external_wiki_service) } it { is_expected.to have_one(:confluence_service) } it { is_expected.to have_one(:project_feature) } @@ -84,7 +85,6 @@ RSpec.describe Project do it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:triggers) } - it { is_expected.to have_many(:pages_domains) } it { is_expected.to have_many(:labels).class_name('ProjectLabel') } it { is_expected.to have_many(:users_star_projects) } it { is_expected.to have_many(:repository_languages) } @@ -124,6 +124,11 @@ RSpec.describe Project do it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile') } it { is_expected.to have_many(:pipeline_artifacts) } + # GitLab Pages + it { is_expected.to have_many(:pages_domains) } + it { is_expected.to have_one(:pages_metadatum) } + it { is_expected.to have_many(:pages_deployments) } + it_behaves_like 'model with repository' do let_it_be(:container) { create(:project, :repository, path: 'somewhere') } let(:stubbed_container) { build_stubbed(:project) } @@ -131,7 +136,7 @@ RSpec.describe Project do end it_behaves_like 'model with wiki' do - let(:container) { create(:project, :wiki_repo) } + let_it_be(:container) { create(:project, :wiki_repo) } let(:container_without_wiki) { create(:project) } end @@ -202,11 +207,11 @@ RSpec.describe Project do end describe '#members & #requesters' do - let(:project) { create(:project, :public) } - let(:requester) { create(:user) } - let(:developer) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:requester) { create(:user) } + let_it_be(:developer) { create(:user) } - before do + before_all do project.request_access(requester) project.add_developer(developer) end @@ -453,9 +458,9 @@ RSpec.describe Project do end describe '#all_pipelines' do - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } - before do + before_all do create(:ci_pipeline, project: project, ref: 'master', source: :web) create(:ci_pipeline, project: project, ref: 'master', source: :external) end @@ -477,7 +482,7 @@ RSpec.describe Project do end describe '#has_packages?' do - let(:project) { create(:project, :public) } + let_it_be(:project) { create(:project, :public) } subject { project.has_packages?(package_type) } @@ -517,14 +522,15 @@ RSpec.describe Project do end describe '#ci_pipelines' do - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } - before do + before_all do create(:ci_pipeline, project: project, ref: 'master', source: :web) create(:ci_pipeline, project: project, ref: 'master', source: :external) + create(:ci_pipeline, project: project, ref: 'master', source: :webide) end - it 'has ci pipelines' do + it 'excludes dangling pipelines such as :webide' do expect(project.ci_pipelines.size).to eq(2) end @@ -542,7 +548,7 @@ RSpec.describe Project do describe '#autoclose_referenced_issues' do context 'when DB entry is nil' do - let(:project) { create(:project, autoclose_referenced_issues: nil) } + let(:project) { build(:project, autoclose_referenced_issues: nil) } it 'returns true' do expect(project.autoclose_referenced_issues).to be_truthy @@ -550,7 +556,7 @@ RSpec.describe Project do end context 'when DB entry is true' do - let(:project) { create(:project, autoclose_referenced_issues: true) } + let(:project) { build(:project, autoclose_referenced_issues: true) } it 'returns true' do expect(project.autoclose_referenced_issues).to be_truthy @@ -558,7 +564,7 @@ RSpec.describe Project do end context 'when DB entry is false' do - let(:project) { create(:project, autoclose_referenced_issues: false) } + let(:project) { build(:project, autoclose_referenced_issues: false) } it 'returns false' do expect(project.autoclose_referenced_issues).to be_falsey @@ -768,8 +774,8 @@ RSpec.describe Project do end describe "#new_issuable_address" do - let(:project) { create(:project, path: "somewhere") } - let(:user) { create(:user) } + let_it_be(:project) { create(:project, path: "somewhere") } + let_it_be(:user) { create(:user) } context 'incoming email enabled' do before do @@ -850,11 +856,11 @@ RSpec.describe Project do end describe '#get_issue' do - let(:project) { create(:project) } - let!(:issue) { create(:issue, project: project) } - let(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let!(:issue) { create(:issue, project: project) } - before do + before_all do project.add_developer(user) end @@ -926,7 +932,7 @@ RSpec.describe Project do end describe '#issue_exists?' do - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } it 'is truthy when issue exists' do expect(project).to receive(:get_issue).and_return(double) @@ -1019,7 +1025,7 @@ RSpec.describe Project do end describe '#cache_has_external_issue_tracker' do - let(:project) { create(:project, has_external_issue_tracker: nil) } + let_it_be(:project) { create(:project, has_external_issue_tracker: nil) } it 'stores true if there is any external_issue_tracker' do services = double(:service, external_issue_trackers: [RedmineService.new]) @@ -1049,7 +1055,7 @@ RSpec.describe Project do end describe '#cache_has_external_wiki' do - let(:project) { create(:project, has_external_wiki: nil) } + let_it_be(:project) { create(:project, has_external_wiki: nil) } it 'stores true if there is any external_wikis' do services = double(:service, external_wikis: [ExternalWikiService.new]) @@ -1115,7 +1121,7 @@ RSpec.describe Project do end describe '#external_wiki' do - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } context 'with an active external wiki' do before do @@ -1268,60 +1274,6 @@ RSpec.describe Project do end end - describe '#pipeline_for' do - let(:project) { create(:project, :repository) } - - shared_examples 'giving the correct pipeline' do - it { is_expected.to eq(pipeline) } - - context 'return latest' do - let!(:pipeline2) { create_pipeline(project) } - - it { is_expected.to eq(pipeline2) } - end - end - - context 'with a matching pipeline' do - let!(:pipeline) { create_pipeline(project) } - - context 'with explicit sha' do - subject { project.pipeline_for('master', pipeline.sha) } - - it_behaves_like 'giving the correct pipeline' - - context 'with supplied id' do - let!(:other_pipeline) { create_pipeline(project) } - - subject { project.pipeline_for('master', pipeline.sha, other_pipeline.id) } - - it { is_expected.to eq(other_pipeline) } - end - end - - context 'with implicit sha' do - subject { project.pipeline_for('master') } - - it_behaves_like 'giving the correct pipeline' - end - end - - context 'when there is no matching pipeline' do - subject { project.pipeline_for('master') } - - it { is_expected.to be_nil } - end - end - - describe '#pipelines_for' do - let(:project) { create(:project, :repository) } - let!(:pipeline) { create_pipeline(project) } - let!(:other_pipeline) { create_pipeline(project) } - - subject { project.pipelines_for(project.default_branch, project.commit.sha) } - - it { is_expected.to contain_exactly(pipeline, other_pipeline) } - end - describe '#builds_enabled' do let(:project) { create(:project) } @@ -1362,6 +1314,36 @@ RSpec.describe Project do end end + describe '.with_active_jira_services' do + it 'returns the correct project' do + active_jira_service = create(:jira_service) + active_service = create(:service, active: true) + + expect(described_class.with_active_jira_services).to include(active_jira_service.project) + expect(described_class.with_active_jira_services).not_to include(active_service.project) + end + end + + describe '.with_jira_dvcs_cloud' do + it 'returns the correct project' do + jira_dvcs_cloud_project = create(:project, :jira_dvcs_cloud) + jira_dvcs_server_project = create(:project, :jira_dvcs_server) + + expect(described_class.with_jira_dvcs_cloud).to include(jira_dvcs_cloud_project) + expect(described_class.with_jira_dvcs_cloud).not_to include(jira_dvcs_server_project) + end + end + + describe '.with_jira_dvcs_server' do + it 'returns the correct project' do + jira_dvcs_server_project = create(:project, :jira_dvcs_server) + jira_dvcs_cloud_project = create(:project, :jira_dvcs_cloud) + + expect(described_class.with_jira_dvcs_server).to include(jira_dvcs_server_project) + expect(described_class.with_jira_dvcs_server).not_to include(jira_dvcs_cloud_project) + end + end + describe '.cached_count', :use_clean_rails_memory_store_caching do let(:group) { create(:group, :public) } let!(:project1) { create(:project, :public, group: group) } @@ -1759,7 +1741,7 @@ RSpec.describe Project do end describe '#visibility_level_allowed?' do - let(:project) { create(:project, :internal) } + let_it_be(:project) { create(:project, :internal) } context 'when checking on non-forked project' do it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy } @@ -1768,7 +1750,6 @@ RSpec.describe Project do end context 'when checking on forked project' do - let(:project) { create(:project, :internal) } let(:forked_project) { fork_project(project) } it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy } @@ -1953,7 +1934,7 @@ RSpec.describe Project do end describe '.optionally_search' do - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } it 'searches for projects matching the query if one is given' do relation = described_class.optionally_search(project.name) @@ -2010,7 +1991,7 @@ RSpec.describe Project do end describe '.search_by_title' do - let(:project) { create(:project, name: 'kittens') } + let_it_be(:project) { create(:project, name: 'kittens') } it 'returns projects with a matching name' do expect(described_class.search_by_title(project.name)).to eq([project]) @@ -2026,11 +2007,11 @@ RSpec.describe Project do end context 'when checking projects from groups' do - let(:private_group) { create(:group, visibility_level: 0) } - let(:internal_group) { create(:group, visibility_level: 10) } + let(:private_group) { build(:group, visibility_level: 0) } + let(:internal_group) { build(:group, visibility_level: 10) } - let(:private_project) { create(:project, :private, group: private_group) } - let(:internal_project) { create(:project, :internal, group: internal_group) } + let(:private_project) { build(:project, :private, group: private_group) } + let(:internal_project) { build(:project, :internal, group: internal_group) } context 'when group is private project can not be internal' do it { expect(private_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_falsey } @@ -2094,7 +2075,7 @@ RSpec.describe Project do end describe '#create_repository' do - let(:project) { create(:project, :repository) } + let_it_be(:project) { build(:project, :repository) } context 'using a regular repository' do it 'creates the repository' do @@ -2120,7 +2101,7 @@ RSpec.describe Project do end describe '#ensure_repository' do - let(:project) { create(:project, :repository) } + let_it_be(:project) { build(:project, :repository) } it 'creates the repository if it not exist' do allow(project).to receive(:repository_exists?).and_return(false) @@ -2174,7 +2155,7 @@ RSpec.describe Project do end describe '#container_registry_url' do - let(:project) { create(:project) } + let_it_be(:project) { build(:project) } subject { project.container_registry_url } @@ -2201,7 +2182,7 @@ RSpec.describe Project do end describe '#has_container_registry_tags?' do - let(:project) { create(:project) } + let(:project) { build(:project) } context 'when container registry is enabled' do before do @@ -2267,7 +2248,7 @@ RSpec.describe Project do describe '#ci_config_path=' do using RSpec::Parameterized::TableSyntax - let(:project) { create(:project) } + let(:project) { build_stubbed(:project) } where(:default_ci_config_path, :project_ci_config_path, :expected_ci_config_path) do nil | :notset | :default @@ -2322,8 +2303,8 @@ RSpec.describe Project do end describe '#latest_successful_build_for_ref' do - let(:project) { create(:project, :repository) } - let(:pipeline) { create_pipeline(project) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:pipeline) { create_pipeline(project) } it_behaves_like 'latest successful build for sha or ref' @@ -2338,47 +2319,95 @@ RSpec.describe Project do end end - describe '#latest_pipeline_for_ref' do - let(:project) { create(:project, :repository) } + describe '#latest_pipeline' do + let_it_be(:project) { create(:project, :repository) } let(:second_branch) { project.repository.branches[2] } let!(:pipeline_for_default_branch) do - create(:ci_empty_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch) + create(:ci_pipeline, project: project, sha: project.commit.id, + ref: project.default_branch) end let!(:pipeline_for_second_branch) do - create(:ci_empty_pipeline, project: project, sha: second_branch.target, - ref: second_branch.name) + create(:ci_pipeline, project: project, sha: second_branch.target, + ref: second_branch.name) end - before do - create(:ci_empty_pipeline, project: project, sha: project.commit.parent.id, - ref: project.default_branch) + let!(:other_pipeline_for_default_branch) do + create(:ci_pipeline, project: project, sha: project.commit.parent.id, + ref: project.default_branch) end context 'default repository branch' do - subject { project.latest_pipeline_for_ref(project.default_branch) } + context 'when explicitly provided' do + subject { project.latest_pipeline(project.default_branch) } + + it { is_expected.to eq(pipeline_for_default_branch) } + end + + context 'when not provided' do + subject { project.latest_pipeline } + + it { is_expected.to eq(pipeline_for_default_branch) } + end + + context 'with provided sha' do + subject { project.latest_pipeline(project.default_branch, project.commit.parent.id) } - it { is_expected.to eq(pipeline_for_default_branch) } + it { is_expected.to eq(other_pipeline_for_default_branch) } + end end context 'provided ref' do - subject { project.latest_pipeline_for_ref(second_branch.name) } + subject { project.latest_pipeline(second_branch.name) } it { is_expected.to eq(pipeline_for_second_branch) } + + context 'with provided sha' do + let!(:latest_pipeline_for_ref) do + create(:ci_pipeline, project: project, sha: pipeline_for_second_branch.sha, + ref: pipeline_for_second_branch.ref) + end + + subject { project.latest_pipeline(second_branch.name, second_branch.target) } + + it { is_expected.to eq(latest_pipeline_for_ref) } + end end context 'bad ref' do - subject { project.latest_pipeline_for_ref(SecureRandom.uuid) } + before do + # ensure we don't skip the filter by ref by mistakenly return this pipeline + create(:ci_pipeline, project: project) + end + + subject { project.latest_pipeline(SecureRandom.uuid) } it { is_expected.to be_nil } end + + context 'on deleted ref' do + let(:branch) { project.repository.branches.last } + + let!(:pipeline_on_deleted_ref) do + create(:ci_pipeline, project: project, sha: branch.target, ref: branch.name) + end + + before do + project.repository.rm_branch(project.owner, branch.name) + end + + subject { project.latest_pipeline(branch.name) } + + it 'always returns nil despite a pipeline exists' do + expect(subject).to be_nil + end + end end describe '#latest_successful_build_for_sha' do - let(:project) { create(:project, :repository) } - let(:pipeline) { create_pipeline(project) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:pipeline) { create_pipeline(project) } it_behaves_like 'latest successful build for sha or ref' @@ -2386,8 +2415,8 @@ RSpec.describe Project do end describe '#latest_successful_build_for_ref!' do - let(:project) { create(:project, :repository) } - let(:pipeline) { create_pipeline(project) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:pipeline) { create_pipeline(project) } context 'with many builds' do it 'gives the latest builds from latest pipeline' do @@ -2460,7 +2489,7 @@ RSpec.describe Project do end describe '#jira_import_status' do - let(:project) { create(:project, import_type: 'jira') } + let_it_be(:project) { create(:project, import_type: 'jira') } context 'when no jira imports' do it 'returns none' do @@ -2666,7 +2695,7 @@ RSpec.describe Project do end describe '#remote_mirror_available?' do - let(:project) { create(:project) } + let(:project) { build_stubbed(:project) } context 'when remote mirror global setting is enabled' do it 'returns true' do @@ -2707,10 +2736,10 @@ RSpec.describe Project do end describe '#ancestors_upto' do - let(:parent) { create(:group) } - let(:child) { create(:group, parent: parent) } - let(:child2) { create(:group, parent: child) } - let(:project) { create(:project, namespace: child2) } + let_it_be(:parent) { create(:group) } + let_it_be(:child) { create(:group, parent: parent) } + let_it_be(:child2) { create(:group, parent: child) } + let_it_be(:project) { create(:project, namespace: child2) } it 'returns all ancestors when no namespace is given' do expect(project.ancestors_upto).to contain_exactly(child2, child, parent) @@ -2755,7 +2784,7 @@ RSpec.describe Project do end describe '#emails_disabled?' do - let(:project) { create(:project, emails_disabled: false) } + let(:project) { build(:project, emails_disabled: false) } context 'emails disabled in group' do it 'returns true' do @@ -2783,7 +2812,7 @@ RSpec.describe Project do end describe '#lfs_enabled?' do - let(:project) { create(:project) } + let(:project) { build(:project) } shared_examples 'project overrides group' do it 'returns true when enabled in project' do @@ -2845,7 +2874,7 @@ RSpec.describe Project do end describe '#change_head' do - let(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } it 'returns error if branch does not exist' do expect(project.change_head('unexisted-branch')).to be false @@ -2876,6 +2905,20 @@ RSpec.describe Project do end end + describe '#lfs_objects_for_repository_types' do + let(:project) { create(:project) } + + it 'returns LFS objects of the specified type only' do + none, design, wiki = *[nil, :design, :wiki].map do |type| + create(:lfs_objects_project, project: project, repository_type: type).lfs_object + end + + expect(project.lfs_objects_for_repository_types(nil)).to contain_exactly(none) + expect(project.lfs_objects_for_repository_types(nil, :wiki)).to contain_exactly(none, wiki) + expect(project.lfs_objects_for_repository_types(:design)).to contain_exactly(design) + end + end + context 'forks' do include ProjectForksHelper @@ -2951,68 +2994,6 @@ RSpec.describe Project do expect(project.forks).to contain_exactly(forked_project) end end - - describe '#lfs_storage_project' do - it 'returns self for non-forks' do - expect(project.lfs_storage_project).to eq project - end - - it 'returns the fork network root for forks' do - second_fork = fork_project(forked_project) - - expect(second_fork.lfs_storage_project).to eq project - end - - it 'returns self when fork_source is nil' do - expect(forked_project).to receive(:fork_source).and_return(nil) - - expect(forked_project.lfs_storage_project).to eq forked_project - end - end - - describe '#all_lfs_objects' do - let(:lfs_object) { create(:lfs_object) } - - context 'when LFS object is only associated to the source' do - before do - project.lfs_objects << lfs_object - end - - it 'returns the lfs object for a project' do - expect(project.all_lfs_objects).to contain_exactly(lfs_object) - end - - it 'returns the lfs object for a fork' do - expect(forked_project.all_lfs_objects).to contain_exactly(lfs_object) - end - end - - context 'when LFS object is only associated to the fork' do - before do - forked_project.lfs_objects << lfs_object - end - - it 'returns nothing' do - expect(project.all_lfs_objects).to be_empty - end - - it 'returns the lfs object for a fork' do - expect(forked_project.all_lfs_objects).to contain_exactly(lfs_object) - end - end - - context 'when LFS object is associated to both source and fork' do - before do - project.lfs_objects << lfs_object - forked_project.lfs_objects << lfs_object - end - - it 'returns the lfs object for the source and fork' do - expect(project.all_lfs_objects).to contain_exactly(lfs_object) - expect(forked_project.all_lfs_objects).to contain_exactly(lfs_object) - end - end - end end describe '#set_repository_read_only!' do @@ -3040,7 +3021,7 @@ RSpec.describe Project do end describe '#pushes_since_gc' do - let(:project) { create(:project) } + let(:project) { build_stubbed(:project) } after do project.reset_pushes_since_gc @@ -3062,7 +3043,7 @@ RSpec.describe Project do end describe '#increment_pushes_since_gc' do - let(:project) { create(:project) } + let(:project) { build_stubbed(:project) } after do project.reset_pushes_since_gc @@ -3076,7 +3057,7 @@ RSpec.describe Project do end describe '#reset_pushes_since_gc' do - let(:project) { create(:project) } + let(:project) { build_stubbed(:project) } after do project.reset_pushes_since_gc @@ -3092,7 +3073,7 @@ RSpec.describe Project do end describe '#deployment_variables' do - let(:project) { create(:project) } + let(:project) { build_stubbed(:project) } let(:environment) { 'production' } let(:namespace) { 'namespace' } @@ -3169,7 +3150,7 @@ RSpec.describe Project do end describe '#default_environment' do - let(:project) { create(:project) } + let(:project) { build(:project) } it 'returns production environment when it exists' do production = create(:environment, name: "production", project: project) @@ -3191,7 +3172,7 @@ RSpec.describe Project do end describe '#ci_variables_for' do - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } let(:environment_scope) { '*' } let!(:ci_variable) do @@ -3346,7 +3327,7 @@ RSpec.describe Project do end describe '#ci_instance_variables_for' do - let(:project) { create(:project) } + let(:project) { build_stubbed(:project) } let!(:instance_variable) do create(:ci_instance_variable, value: 'secret') @@ -5831,32 +5812,57 @@ RSpec.describe Project do end end - context 'pages deployed' do + describe '#mark_pages_as_deployed' do let(:project) { create(:project) } + let(:artifacts_archive) { create(:ci_job_artifact, project: project) } - { - mark_pages_as_deployed: true, - mark_pages_as_not_deployed: false - }.each do |method_name, flag| - describe method_name do - it "creates new record and sets deployed to #{flag} if none exists yet" do - project.pages_metadatum.destroy! - project.reload + it "works when artifacts_archive is missing" do + project.mark_pages_as_deployed - project.send(method_name) + expect(project.pages_metadatum.reload.deployed).to eq(true) + end - expect(project.pages_metadatum.reload.deployed).to eq(flag) - end + it "creates new record and sets deployed to true if none exists yet" do + project.pages_metadatum.destroy! + project.reload - it "updates the existing record and sets deployed to #{flag}" do - pages_metadatum = project.pages_metadatum - pages_metadatum.update!(deployed: !flag) + project.mark_pages_as_deployed(artifacts_archive: artifacts_archive) - expect { project.send(method_name) }.to change { - pages_metadatum.reload.deployed - }.from(!flag).to(flag) - end - end + expect(project.pages_metadatum.reload.deployed).to eq(true) + end + + it "updates the existing record and sets deployed to true and records artifact archive" do + pages_metadatum = project.pages_metadatum + pages_metadatum.update!(deployed: false) + + expect do + project.mark_pages_as_deployed(artifacts_archive: artifacts_archive) + end.to change { pages_metadatum.reload.deployed }.from(false).to(true) + .and change { pages_metadatum.reload.artifacts_archive }.from(nil).to(artifacts_archive) + end + end + + describe '#mark_pages_as_not_deployed' do + let(:project) { create(:project) } + let(:artifacts_archive) { create(:ci_job_artifact, project: project) } + + it "creates new record and sets deployed to false if none exists yet" do + project.pages_metadatum.destroy! + project.reload + + project.mark_pages_as_not_deployed + + expect(project.pages_metadatum.reload.deployed).to eq(false) + end + + it "updates the existing record and sets deployed to false and clears artifacts_archive" do + pages_metadatum = project.pages_metadatum + pages_metadatum.update!(deployed: true, artifacts_archive: artifacts_archive) + + expect do + project.mark_pages_as_not_deployed + end.to change { pages_metadatum.reload.deployed }.from(true).to(false) + .and change { pages_metadatum.reload.artifacts_archive }.from(artifacts_archive).to(nil) end end @@ -6043,6 +6049,18 @@ RSpec.describe Project do end end + describe '#jira_subscription_exists?' do + let(:project) { create(:project) } + + subject { project.jira_subscription_exists? } + + context 'jira connect subscription exists' do + let!(:jira_connect_subscription) { create(:jira_connect_subscription, namespace: project.namespace) } + + it { is_expected.to eq(true) } + end + end + describe 'with services and chat names' do subject { create(:project) } @@ -6088,53 +6106,6 @@ RSpec.describe Project do end end - describe '#all_lfs_objects_oids' do - let(:project) { create(:project) } - let(:lfs_object) { create(:lfs_object) } - let(:another_lfs_object) { create(:lfs_object) } - - subject { project.all_lfs_objects_oids } - - context 'when project has associated LFS objects' do - before do - create(:lfs_objects_project, lfs_object: lfs_object, project: project) - create(:lfs_objects_project, lfs_object: another_lfs_object, project: project) - end - - it 'returns OIDs of LFS objects' do - expect(subject).to match_array([lfs_object.oid, another_lfs_object.oid]) - end - - context 'and there are specified oids' do - subject { project.all_lfs_objects_oids(oids: [lfs_object.oid]) } - - it 'returns OIDs of LFS objects that match specified oids' do - expect(subject).to eq([lfs_object.oid]) - end - end - end - - context 'when fork has associated LFS objects to itself and source' do - let(:source) { create(:project) } - let(:project) { fork_project(source) } - - before do - create(:lfs_objects_project, lfs_object: lfs_object, project: source) - create(:lfs_objects_project, lfs_object: another_lfs_object, project: project) - end - - it 'returns OIDs of LFS objects' do - expect(subject).to match_array([lfs_object.oid, another_lfs_object.oid]) - end - end - - context 'when project has no associated LFS objects' do - it 'returns empty array' do - expect(subject).to be_empty - end - end - end - describe '#lfs_objects_oids' do let(:project) { create(:project) } let(:lfs_object) { create(:lfs_object) } @@ -6475,6 +6446,49 @@ RSpec.describe Project do end end + describe '#enabled_group_deploy_keys' do + let_it_be(:project) { create(:project) } + + subject { project.enabled_group_deploy_keys } + + context 'when a project does not have a group' do + it { is_expected.to be_empty } + end + + context 'when a project has a parent group' do + let!(:group) { create(:group, projects: [project]) } + + context 'and this group has a group deploy key enabled' do + let!(:group_deploy_key) { create(:group_deploy_key, groups: [group]) } + + it { is_expected.to contain_exactly(group_deploy_key) } + + context 'and this group has parent group which also has a group deploy key enabled' do + let(:super_group) { create(:group) } + + it 'returns both group deploy keys' do + super_group = create(:group) + super_group_deploy_key = create(:group_deploy_key, groups: [super_group]) + group.update!(parent: super_group) + + expect(subject).to contain_exactly(group_deploy_key, super_group_deploy_key) + end + end + end + + context 'and another group has a group deploy key enabled' do + let_it_be(:group_deploy_key) { create(:group_deploy_key) } + + it 'does not return this group deploy key' do + another_group = create(:group) + create(:group_deploy_key, groups: [another_group]) + + expect(subject).to be_empty + end + end + end + end + def finish_job(export_job) export_job.start export_job.finish diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb index 5f66de3a63c..383fabcfffb 100644 --- a/spec/models/project_statistics_spec.rb +++ b/spec/models/project_statistics_spec.rb @@ -32,8 +32,9 @@ RSpec.describe ProjectStatistics do repository_size: 2.exabytes, wiki_size: 1.exabytes, lfs_objects_size: 2.exabytes, - build_artifacts_size: 2.exabytes - 1, - snippets_size: 1.exabyte + build_artifacts_size: 1.exabyte, + snippets_size: 1.exabyte, + pipeline_artifacts_size: 1.exabyte - 1 ) statistics.reload @@ -42,9 +43,10 @@ RSpec.describe ProjectStatistics do expect(statistics.repository_size).to eq(2.exabytes) expect(statistics.wiki_size).to eq(1.exabytes) expect(statistics.lfs_objects_size).to eq(2.exabytes) - expect(statistics.build_artifacts_size).to eq(2.exabytes - 1) + expect(statistics.build_artifacts_size).to eq(1.exabyte) expect(statistics.storage_size).to eq(8.exabytes - 1) expect(statistics.snippets_size).to eq(1.exabyte) + expect(statistics.pipeline_artifacts_size).to eq(1.exabyte - 1) end end @@ -282,12 +284,13 @@ RSpec.describe ProjectStatistics do repository_size: 2, wiki_size: 4, lfs_objects_size: 3, - snippets_size: 2 + snippets_size: 2, + pipeline_artifacts_size: 3 ) statistics.reload - expect(statistics.storage_size).to eq 11 + expect(statistics.storage_size).to eq 14 end it 'works during wiki_size backfill' do @@ -339,6 +342,12 @@ RSpec.describe ProjectStatistics do it_behaves_like 'a statistic that increases storage_size' end + context 'when adjusting :pipeline_artifacts_size' do + let(:stat) { :pipeline_artifacts_size } + + it_behaves_like 'a statistic that increases storage_size' + end + context 'when adjusting :packages_size' do let(:stat) { :packages_size } diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 34ec856459c..bbc056889d6 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -3,6 +3,8 @@ require "spec_helper" RSpec.describe ProjectTeam do + include ProjectForksHelper + let(:maintainer) { create(:user) } let(:reporter) { create(:user) } let(:guest) { create(:user) } @@ -237,6 +239,35 @@ RSpec.describe ProjectTeam do end end + describe '#contributor?' do + let(:project) { create(:project, :public, :repository) } + + context 'when user is a member of project' do + before do + project.add_maintainer(maintainer) + project.add_reporter(reporter) + project.add_guest(guest) + end + + it { expect(project.team.contributor?(maintainer.id)).to be false } + it { expect(project.team.contributor?(reporter.id)).to be false } + it { expect(project.team.contributor?(guest.id)).to be false } + end + + context 'when user has at least one merge request merged into default_branch' do + let(:contributor) { create(:user) } + let(:user_without_access) { create(:user) } + let(:first_fork_project) { fork_project(project, contributor, repository: true) } + + before do + create(:merge_request, :merged, author: contributor, target_project: project, source_project: first_fork_project, target_branch: project.default_branch.to_s) + end + + it { expect(project.team.contributor?(contributor.id)).to be true } + it { expect(project.team.contributor?(user_without_access.id)).to be false } + end + end + describe '#max_member_access' do let(:requester) { create(:user) } @@ -366,6 +397,66 @@ RSpec.describe ProjectTeam do end end + describe '#contribution_check_for_user_ids', :request_store do + let(:project) { create(:project, :public, :repository) } + let(:contributor) { create(:user) } + let(:second_contributor) { create(:user) } + let(:user_without_access) { create(:user) } + let(:first_fork_project) { fork_project(project, contributor, repository: true) } + let(:second_fork_project) { fork_project(project, second_contributor, repository: true) } + + let(:users) do + [contributor, second_contributor, user_without_access].map(&:id) + end + + let(:expected) do + { + contributor.id => true, + second_contributor.id => true, + user_without_access.id => false + } + end + + before do + create(:merge_request, :merged, author: contributor, target_project: project, source_project: first_fork_project, target_branch: project.default_branch.to_s) + create(:merge_request, :merged, author: second_contributor, target_project: project, source_project: second_fork_project, target_branch: project.default_branch.to_s) + end + + def contributors(users) + project.team.contribution_check_for_user_ids(users) + end + + it 'does not perform extra queries when asked for users who have already been found' do + contributors(users) + + expect { contributors([contributor.id]) }.not_to exceed_query_limit(0) + + expect(contributors([contributor.id])).to eq(expected) + end + + it 'only requests the extra users when uncached users are passed' do + new_contributor = create(:user) + new_fork_project = fork_project(project, new_contributor, repository: true) + second_new_user = create(:user) + all_users = users + [new_contributor.id, second_new_user.id] + create(:merge_request, :merged, author: new_contributor, target_project: project, source_project: new_fork_project, target_branch: project.default_branch.to_s) + + expected_all = expected.merge(new_contributor.id => true, + second_new_user.id => false) + + contributors(users) + + queries = ActiveRecord::QueryRecorder.new { contributors(all_users) } + + expect(queries.count).to eq(1) + expect(contributors([new_contributor.id])).to eq(expected_all) + end + + it 'returns correct contributors' do + expect(contributors(users)).to eq(expected) + end + end + shared_examples 'max member access for users' do let(:project) { create(:project) } let(:group) { create(:group) } @@ -438,9 +529,9 @@ RSpec.describe ProjectTeam do it 'does not perform extra queries when asked for users who have already been found' do access_levels(users) - expect { access_levels(users) }.not_to exceed_query_limit(0) + expect { access_levels([maintainer.id]) }.not_to exceed_query_limit(0) - expect(access_levels(users)).to eq(expected) + expect(access_levels([maintainer.id])).to eq(expected) end it 'only requests the extra users when uncached users are passed' do diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index d9c5fed542e..29c3d0e1a73 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -17,19 +17,28 @@ RSpec.describe ProjectWiki do end end - describe '#update_container_activity' do + describe '#after_wiki_activity' do it 'updates project activity' do wiki_container.update!( last_activity_at: nil, last_repository_updated_at: nil ) - subject.create_page('Test Page', 'This is content') + subject.send(:after_wiki_activity) wiki_container.reload expect(wiki_container.last_activity_at).to be_within(1.minute).of(Time.current) expect(wiki_container.last_repository_updated_at).to be_within(1.minute).of(Time.current) end end + + describe '#after_post_receive' do + it 'updates project activity and expires caches' do + expect(wiki).to receive(:after_wiki_activity) + expect(ProjectCacheWorker).to receive(:perform_async).with(wiki_container.id, [], [:wiki_size]) + + subject.send(:after_post_receive) + end + end end end diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index ebc9760ab14..4c3151f431c 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -142,6 +142,20 @@ RSpec.describe RemoteMirror, :mailer do end end + describe '#bare_url' do + it 'returns the URL without any credentials' do + remote_mirror = build(:remote_mirror, url: 'http://user:pass@example.com/foo') + + expect(remote_mirror.bare_url).to eq('http://example.com/foo') + end + + it 'returns an empty string when the URL is nil' do + remote_mirror = build(:remote_mirror, url: nil) + + expect(remote_mirror.bare_url).to eq('') + end + end + describe '#update_repository' do it 'performs update including options' do git_remote_mirror = stub_const('Gitlab::Git::RemoteMirror', spy) @@ -283,7 +297,7 @@ RSpec.describe RemoteMirror, :mailer do let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } around do |example| - Timecop.freeze { example.run } + freeze_time { example.run } end context 'with remote mirroring disabled' do @@ -397,7 +411,7 @@ RSpec.describe RemoteMirror, :mailer do let(:timestamp) { Time.current - 5.minutes } around do |example| - Timecop.freeze { example.run } + freeze_time { example.run } end before do @@ -442,16 +456,18 @@ RSpec.describe RemoteMirror, :mailer do end describe '#disabled?' do + let_it_be(:project) { create(:project, :repository) } + subject { remote_mirror.disabled? } context 'when disabled' do - let(:remote_mirror) { build(:remote_mirror, enabled: false) } + let(:remote_mirror) { build(:remote_mirror, project: project, enabled: false) } it { is_expected.to be_truthy } end context 'when enabled' do - let(:remote_mirror) { build(:remote_mirror, enabled: true) } + let(:remote_mirror) { build(:remote_mirror, project: project, enabled: true) } it { is_expected.to be_falsy } end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index a6b79e55f02..a3042d619eb 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1263,6 +1263,7 @@ RSpec.describe Repository do %w(a b c/z) | %w(c d) | true %w(a/b/z) | %w(a/b) | false # we only consider refs ambiguous before the first slash %w(a/b/z) | %w(a/b a) | true + %w(ab) | %w(abc/d a b) | false end with_them do diff --git a/spec/models/resource_iteration_event_spec.rb b/spec/models/resource_iteration_event_spec.rb deleted file mode 100644 index fe1310d7bf1..00000000000 --- a/spec/models/resource_iteration_event_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ResourceIterationEvent, type: :model do - it_behaves_like 'a resource event' - it_behaves_like 'a resource event for issues' - it_behaves_like 'a resource event for merge requests' - - it_behaves_like 'having unique enum values' - it_behaves_like 'timebox resource event validations' - it_behaves_like 'timebox resource event actions' - - describe 'associations' do - it { is_expected.to belong_to(:iteration) } - end -end diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb index 6a235d3aa17..960db31d488 100644 --- a/spec/models/resource_label_event_spec.rb +++ b/spec/models/resource_label_event_spec.rb @@ -3,10 +3,12 @@ require 'spec_helper' RSpec.describe ResourceLabelEvent, type: :model do - subject { build(:resource_label_event, issue: issue) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + let_it_be(:label) { create(:label, project: project) } - let(:issue) { create(:issue) } - let(:merge_request) { create(:merge_request) } + subject { build(:resource_label_event, issue: issue, label: label) } it_behaves_like 'having unique enum values' diff --git a/spec/models/resource_state_event_spec.rb b/spec/models/resource_state_event_spec.rb index 1381b45cf9e..fc6575b2db8 100644 --- a/spec/models/resource_state_event_spec.rb +++ b/spec/models/resource_state_event_spec.rb @@ -11,4 +11,32 @@ RSpec.describe ResourceStateEvent, type: :model do it_behaves_like 'a resource event' it_behaves_like 'a resource event for issues' it_behaves_like 'a resource event for merge requests' + + describe 'validations' do + describe 'Issuable validation' do + it 'is valid if an issue is set' do + subject.attributes = { issue: build_stubbed(:issue), merge_request: nil } + + expect(subject).to be_valid + end + + it 'is valid if a merge request is set' do + subject.attributes = { issue: nil, merge_request: build_stubbed(:merge_request) } + + expect(subject).to be_valid + end + + it 'is invalid if both issue and merge request are set' do + subject.attributes = { issue: build_stubbed(:issue), merge_request: build_stubbed(:merge_request) } + + expect(subject).not_to be_valid + end + + it 'is invalid if there is no issuable set' do + subject.attributes = { issue: nil, merge_request: nil } + + expect(subject).not_to be_valid + end + end + end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index c4a9c0329c7..32e2012e284 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -3,8 +3,12 @@ require 'spec_helper' RSpec.describe Service do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + describe "Associations" do it { is_expected.to belong_to :project } + it { is_expected.to belong_to :group } it { is_expected.to have_one :service_hook } it { is_expected.to have_one :jira_tracker_data } it { is_expected.to have_one :issue_tracker_data } @@ -13,9 +17,6 @@ RSpec.describe Service do describe 'validations' do using RSpec::Parameterized::TableSyntax - let(:group) { create(:group) } - let(:project) { create(:project) } - it { is_expected.to validate_presence_of(:type) } where(:project_id, :group_id, :template, :instance, :valid) do @@ -91,17 +92,12 @@ RSpec.describe Service do end end - describe '#operating?' do - it 'is false when the service is not active' do - expect(build(:service).operating?).to eq(false) - end - - it 'is false when the service is not persisted' do - expect(build(:service, active: true).operating?).to eq(false) - end + describe '.for_group' do + let!(:service1) { create(:jira_service, project_id: nil, group_id: group.id) } + let!(:service2) { create(:jira_service) } - it 'is true when the service is active and persisted' do - expect(create(:service, active: true).operating?).to eq(true) + it 'returns the right group service' do + expect(described_class.for_group(group)).to match_array([service1]) end end @@ -134,20 +130,34 @@ RSpec.describe Service do end end + describe '#operating?' do + it 'is false when the service is not active' do + expect(build(:service).operating?).to eq(false) + end + + it 'is false when the service is not persisted' do + expect(build(:service, active: true).operating?).to eq(false) + end + + it 'is true when the service is active and persisted' do + expect(create(:service, active: true).operating?).to eq(true) + end + end + describe "Test Button" do + let(:service) { build(:service, project: project) } + describe '#can_test?' do subject { service.can_test? } - let(:service) { create(:service, project: project) } - context 'when repository is not empty' do - let(:project) { create(:project, :repository) } + let(:project) { build(:project, :repository) } it { is_expected.to be true } end context 'when repository is empty' do - let(:project) { create(:project) } + let(:project) { build(:project) } it { is_expected.to be true } end @@ -161,14 +171,23 @@ RSpec.describe Service do it { is_expected.to be_falsey } end end + + context 'when group-level service' do + Service.available_services_types.each do |service_type| + let(:service) do + service_type.constantize.new(group_id: group.id) + end + + it { is_expected.to be_falsey } + end + end end describe '#test' do let(:data) { 'test' } - let(:service) { create(:service, project: project) } context 'when repository is not empty' do - let(:project) { create(:project, :repository) } + let(:project) { build(:project, :repository) } it 'test runs execute' do expect(service).to receive(:execute).with(data) @@ -178,7 +197,7 @@ RSpec.describe Service do end context 'when repository is empty' do - let(:project) { create(:project) } + let(:project) { build(:project) } it 'test runs execute' do expect(service).to receive(:execute).with(data) @@ -189,14 +208,27 @@ RSpec.describe Service do end end - describe '.find_or_initialize_instances' do + describe '.find_or_initialize_integration' do + let!(:service1) { create(:jira_service, project_id: nil, group_id: group.id) } + let!(:service2) { create(:jira_service) } + + it 'returns the right service' do + expect(Service.find_or_initialize_integration('jira', group_id: group)).to eq(service1) + end + + it 'does not create a new service' do + expect { Service.find_or_initialize_integration('redmine', group_id: group) }.not_to change { Service.count } + end + end + + describe '.find_or_initialize_all' do shared_examples 'service instances' do it 'returns the available service instances' do - expect(Service.find_or_initialize_instances.pluck(:type)).to match_array(Service.available_services_types) + expect(Service.find_or_initialize_all(Service.for_instance).pluck(:type)).to match_array(Service.available_services_types) end it 'does not create service instances' do - expect { Service.find_or_initialize_instances }.not_to change { Service.count } + expect { Service.find_or_initialize_all(Service.for_instance) }.not_to change { Service.count } end end @@ -211,9 +243,9 @@ RSpec.describe Service do it_behaves_like 'service instances' - context 'with a previous existing service (Previous) and a new service (Asana)' do + context 'with a previous existing service (MockCiService) and a new service (Asana)' do before do - Service.insert(type: 'PreviousService', instance: true) + Service.insert(type: 'MockCiService', instance: true) Service.delete_by(type: 'AsanaService', instance: true) end @@ -231,8 +263,6 @@ RSpec.describe Service do end describe 'template' do - let(:project) { create(:project) } - shared_examples 'retrieves service templates' do it 'returns the available service templates' do expect(Service.find_or_create_templates.pluck(:type)).to match_array(Service.available_services_types) @@ -390,29 +420,49 @@ RSpec.describe Service do end end - describe 'instance' do - describe '.instance_for' do - let_it_be(:jira_service) { create(:jira_service, :instance) } - let_it_be(:slack_service) { create(:slack_service, :instance) } - - subject { described_class.instance_for(type) } + describe '.default_integration' do + context 'with an instance-level service' do + let_it_be(:instance_service) { create(:jira_service, :instance) } - context 'Hipchat serivce' do - let(:type) { 'HipchatService' } + it 'returns the instance service' do + expect(described_class.default_integration('JiraService', project)).to eq(instance_service) + end - it { is_expected.to eq(nil) } + it 'returns nil for nonexistent service type' do + expect(described_class.default_integration('HipchatService', project)).to eq(nil) end - context 'Jira serivce' do - let(:type) { 'JiraService' } + context 'with a group service' do + let_it_be(:group_service) { create(:jira_service, group_id: group.id, project_id: nil) } - it { is_expected.to eq(jira_service) } - end + it 'returns the group service for a project' do + expect(described_class.default_integration('JiraService', project)).to eq(group_service) + end + + it 'returns the instance service for a group' do + expect(described_class.default_integration('JiraService', group)).to eq(instance_service) + end - context 'Slack serivce' do - let(:type) { 'SlackService' } + context 'with a subgroup' do + let_it_be(:subgroup) { create(:group, parent: group) } + let!(:project) { create(:project, group: subgroup) } - it { is_expected.to eq(slack_service) } + it 'returns the closest group service for a project' do + expect(described_class.default_integration('JiraService', project)).to eq(group_service) + end + + it 'returns the closest group service for a subgroup' do + expect(described_class.default_integration('JiraService', subgroup)).to eq(group_service) + end + + context 'having a service' do + let!(:subgroup_service) { create(:jira_service, group_id: subgroup.id, project_id: nil) } + + it 'returns the closest group service for a project' do + expect(described_class.default_integration('JiraService', project)).to eq(subgroup_service) + end + end + end end end end @@ -420,7 +470,7 @@ RSpec.describe Service do describe "{property}_changed?" do let(:service) do BambooService.create( - project: create(:project), + project: project, properties: { bamboo_url: 'http://gitlab.com', username: 'mic', @@ -460,7 +510,7 @@ RSpec.describe Service do describe "{property}_touched?" do let(:service) do BambooService.create( - project: create(:project), + project: project, properties: { bamboo_url: 'http://gitlab.com', username: 'mic', @@ -500,7 +550,7 @@ RSpec.describe Service do describe "{property}_was" do let(:service) do BambooService.create( - project: create(:project), + project: project, properties: { bamboo_url: 'http://gitlab.com', username: 'mic', @@ -540,7 +590,7 @@ RSpec.describe Service do describe 'initialize service with no properties' do let(:service) do BugzillaService.create( - project: create(:project), + project: project, project_url: 'http://gitlab.example.com' ) end @@ -555,7 +605,6 @@ RSpec.describe Service do end describe "callbacks" do - let(:project) { create(:project) } let!(:service) do RedmineService.new( project: project, @@ -622,8 +671,7 @@ RSpec.describe Service do end context 'logging' do - let(:project) { create(:project) } - let(:service) { create(:service, project: project) } + let(:service) { build(:service, project: project) } let(:test_message) { "test message" } let(:arguments) do { diff --git a/spec/models/snippet_input_action_spec.rb b/spec/models/snippet_input_action_spec.rb index ca61b80df4c..43dc70bea98 100644 --- a/spec/models/snippet_input_action_spec.rb +++ b/spec/models/snippet_input_action_spec.rb @@ -22,9 +22,9 @@ RSpec.describe SnippetInputAction do :move | 'foobar' | 'foobar' | nil | nil | false | :previous_path :move | 'foobar' | 'foobar' | '' | nil | false | :previous_path :move | 'foobar' | 'foobar' | 'foobar' | nil | false | :file_path - :move | nil | 'foobar' | 'foobar' | nil | false | :file_path - :move | '' | 'foobar' | 'foobar' | nil | false | :file_path - :move | nil | 'foobar' | 'foo1' | nil | false | :file_path + :move | nil | 'foobar' | 'foobar' | nil | true | nil + :move | '' | 'foobar' | 'foobar' | nil | true | nil + :move | nil | 'foobar' | 'foo1' | nil | true | nil :move | 'foobar' | nil | 'foo1' | nil | true | nil :move | 'foobar' | '' | 'foo1' | nil | true | nil :create | 'foobar' | nil | 'foobar' | nil | false | :content diff --git a/spec/models/snippet_repository_spec.rb b/spec/models/snippet_repository_spec.rb index 0f5e0bfc75c..95602a4de0e 100644 --- a/spec/models/snippet_repository_spec.rb +++ b/spec/models/snippet_repository_spec.rb @@ -108,6 +108,7 @@ RSpec.describe SnippetRepository do before do allow(snippet).to receive(:repository).and_return(repo) allow(repo).to receive(:ls_files).and_return([]) + allow(repo).to receive(:root_ref).and_return('master') end it 'infers the commit action based on the parameters if not present' do @@ -197,7 +198,7 @@ RSpec.describe SnippetRepository do shared_examples 'snippet repository with file names' do |*filenames| it 'sets a name for unnamed files' do - ls_files = snippet.repository.ls_files(nil) + ls_files = snippet.repository.ls_files(snippet.default_branch) expect(ls_files).to include(*filenames) end end @@ -306,6 +307,6 @@ RSpec.describe SnippetRepository do end def first_blob(snippet) - snippet.repository.blob_at('master', snippet.repository.ls_files(nil).first) + snippet.repository.blob_at('master', snippet.repository.ls_files(snippet.default_branch).first) end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 3f9c6981de1..ab614a6d45c 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -133,10 +133,10 @@ RSpec.describe Snippet do end describe '#file_name' do - let(:project) { create(:project) } + let(:snippet) { build(:snippet, file_name: file_name) } context 'file_name is nil' do - let(:snippet) { create(:snippet, project: project, file_name: nil) } + let(:file_name) { nil } it 'returns an empty string' do expect(snippet.file_name).to eq '' @@ -144,10 +144,10 @@ RSpec.describe Snippet do end context 'file_name is not nil' do - let(:snippet) { create(:snippet, project: project, file_name: 'foo.txt') } + let(:file_name) { 'foo.txt' } it 'returns the file_name' do - expect(snippet.file_name).to eq 'foo.txt' + expect(snippet.file_name).to eq file_name end end end @@ -161,7 +161,7 @@ RSpec.describe Snippet do end describe '.search' do - let(:snippet) { create(:snippet, title: 'test snippet', description: 'description') } + let_it_be(:snippet) { create(:snippet, title: 'test snippet', description: 'description') } it 'returns snippets with a matching title' do expect(described_class.search(snippet.title)).to eq([snippet]) @@ -219,25 +219,23 @@ RSpec.describe Snippet do end describe '.with_optional_visibility' do + let_it_be(:public_snippet) { create(:snippet, :public) } + let_it_be(:private_snippet) { create(:snippet, :private) } + context 'when a visibility level is provided' do it 'returns snippets with the given visibility' do - create(:snippet, :private) - - snippet = create(:snippet, :public) snippets = described_class .with_optional_visibility(Gitlab::VisibilityLevel::PUBLIC) - expect(snippets).to eq([snippet]) + expect(snippets).to eq([public_snippet]) end end context 'when a visibility level is not provided' do it 'returns all snippets' do - snippet1 = create(:snippet, :public) - snippet2 = create(:snippet, :private) snippets = described_class.with_optional_visibility - expect(snippets).to include(snippet1, snippet2) + expect(snippets).to include(public_snippet, private_snippet) end end end @@ -254,12 +252,13 @@ RSpec.describe Snippet do end describe '.only_include_projects_visible_to' do - let!(:project1) { create(:project, :public) } - let!(:project2) { create(:project, :internal) } - let!(:project3) { create(:project, :private) } - let!(:snippet1) { create(:project_snippet, project: project1) } - let!(:snippet2) { create(:project_snippet, project: project2) } - let!(:snippet3) { create(:project_snippet, project: project3) } + let_it_be(:author) { create(:user) } + let_it_be(:project1) { create(:project_empty_repo, :public, namespace: author.namespace) } + let_it_be(:project2) { create(:project_empty_repo, :internal, namespace: author.namespace) } + let_it_be(:project3) { create(:project_empty_repo, :private, namespace: author.namespace) } + let_it_be(:snippet1) { create(:project_snippet, project: project1, author: author) } + let_it_be(:snippet2) { create(:project_snippet, project: project2, author: author) } + let_it_be(:snippet3) { create(:project_snippet, project: project3, author: author) } context 'when a user is provided' do it 'returns snippets visible to the user' do @@ -283,55 +282,47 @@ RSpec.describe Snippet do end describe 'only_include_projects_with_snippets_enabled' do - context 'when the include_private option is enabled' do - it 'includes snippets for projects with snippets set to private' do - project = create(:project) - - project.project_feature - .update(snippets_access_level: ProjectFeature::PRIVATE) + let_it_be(:project, reload: true) { create(:project_empty_repo) } + let_it_be(:snippet) { create(:project_snippet, project: project) } - snippet = create(:project_snippet, project: project) + let(:access_level) { ProjectFeature::ENABLED } - snippets = described_class - .only_include_projects_with_snippets_enabled(include_private: true) - - expect(snippets).to eq([snippet]) - end + before do + project.project_feature.update(snippets_access_level: access_level) end - context 'when the include_private option is not enabled' do - it 'does not include snippets for projects that have snippets set to private' do - project = create(:project) + it 'includes snippets for projects with snippets enabled' do + snippets = described_class.only_include_projects_with_snippets_enabled - project.project_feature - .update(snippets_access_level: ProjectFeature::PRIVATE) + expect(snippets).to eq([snippet]) + end - create(:project_snippet, project: project) + context 'when snippet_access_level is private' do + let(:access_level) { ProjectFeature::PRIVATE } - snippets = described_class.only_include_projects_with_snippets_enabled + context 'when the include_private option is enabled' do + it 'includes snippets for projects with snippets set to private' do + snippets = described_class.only_include_projects_with_snippets_enabled(include_private: true) - expect(snippets).to be_empty + expect(snippets).to eq([snippet]) + end end - end - - it 'includes snippets for projects with snippets enabled' do - project = create(:project) - project.project_feature - .update(snippets_access_level: ProjectFeature::ENABLED) + context 'when the include_private option is not enabled' do + it 'does not include snippets for projects that have snippets set to private' do + snippets = described_class.only_include_projects_with_snippets_enabled - snippet = create(:project_snippet, project: project) - snippets = described_class.only_include_projects_with_snippets_enabled - - expect(snippets).to eq([snippet]) + expect(snippets).to be_empty + end + end end end describe '.only_include_authorized_projects' do it 'only includes snippets for projects the user is authorized to see' do user = create(:user) - project1 = create(:project, :private) - project2 = create(:project, :private) + project1 = create(:project_empty_repo, :private) + project2 = create(:project_empty_repo, :private) project1.team.add_developer(user) @@ -345,43 +336,34 @@ RSpec.describe Snippet do end describe '.for_project_with_user' do - context 'when a user is provided' do - it 'returns an empty collection if the user can not view the snippets' do - project = create(:project, :private) - user = create(:user) + let_it_be(:public_project) { create(:project_empty_repo, :public) } + let_it_be(:private_project) { create(:project_empty_repo, :private) } - project.project_feature - .update(snippets_access_level: ProjectFeature::ENABLED) + context 'when a user is provided' do + let_it_be(:user) { create(:user) } - create(:project_snippet, :public, project: project) + it 'returns an empty collection if the user can not view the snippets' do + create(:project_snippet, :public, project: private_project) - expect(described_class.for_project_with_user(project, user)).to be_empty + expect(described_class.for_project_with_user(private_project, user)).to be_empty end it 'returns the snippets if the user is a member of the project' do - project = create(:project, :private) - user = create(:user) - snippet = create(:project_snippet, project: project) + snippet = create(:project_snippet, project: private_project) - project.team.add_developer(user) + private_project.team.add_developer(user) - snippets = described_class.for_project_with_user(project, user) + snippets = described_class.for_project_with_user(private_project, user) expect(snippets).to eq([snippet]) end it 'returns public snippets for a public project the user is not a member of' do - project = create(:project, :public) - - project.project_feature - .update(snippets_access_level: ProjectFeature::ENABLED) + snippet = create(:project_snippet, :public, project: public_project) - user = create(:user) - snippet = create(:project_snippet, :public, project: project) + create(:project_snippet, :private, project: public_project) - create(:project_snippet, :private, project: project) - - snippets = described_class.for_project_with_user(project, user) + snippets = described_class.for_project_with_user(public_project, user) expect(snippets).to eq([snippet]) end @@ -389,26 +371,17 @@ RSpec.describe Snippet do context 'when a user is not provided' do it 'returns an empty collection for a private project' do - project = create(:project, :private) - - project.project_feature - .update(snippets_access_level: ProjectFeature::ENABLED) + create(:project_snippet, :public, project: private_project) - create(:project_snippet, :public, project: project) - - expect(described_class.for_project_with_user(project)).to be_empty + expect(described_class.for_project_with_user(private_project)).to be_empty end it 'returns public snippets for a public project' do - project = create(:project, :public) - snippet = create(:project_snippet, :public, project: project) - - project.project_feature - .update(snippets_access_level: ProjectFeature::PUBLIC) + snippet = create(:project_snippet, :public, project: public_project) - create(:project_snippet, :private, project: project) + create(:project_snippet, :private, project: public_project) - snippets = described_class.for_project_with_user(project) + snippets = described_class.for_project_with_user(public_project) expect(snippets).to eq([snippet]) end @@ -430,34 +403,30 @@ RSpec.describe Snippet do end describe '#participants' do - let(:project) { create(:project, :public) } - let(:snippet) { create(:snippet, content: 'foo', project: project) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:snippet) { create(:snippet, content: 'foo', project: project) } - let!(:note1) do + let_it_be(:note1) do create(:note_on_project_snippet, noteable: snippet, project: project, note: 'a') end - let!(:note2) do + let_it_be(:note2) do create(:note_on_project_snippet, noteable: snippet, project: project, note: 'b') end - it 'includes the snippet author' do - expect(snippet.participants).to include(snippet.author) - end - - it 'includes the note authors' do - expect(snippet.participants).to include(note1.author, note2.author) + it 'includes the snippet author and note authors' do + expect(snippet.participants).to include(snippet.author, note1.author, note2.author) end end describe '#check_for_spam' do - let(:snippet) { create :snippet, visibility_level: visibility_level } + let(:snippet) { create(:snippet, visibility_level: visibility_level) } subject do snippet.assign_attributes(title: title) @@ -500,7 +469,7 @@ RSpec.describe Snippet do end describe '#blob' do - let(:snippet) { create(:snippet) } + let(:snippet) { build(:snippet) } it 'returns a blob representing the snippet data' do blob = snippet.blob @@ -514,6 +483,14 @@ RSpec.describe Snippet do describe '#blobs' do let(:snippet) { create(:snippet) } + it 'returns a blob representing the snippet data' do + blob = snippet.blob + + expect(blob).to be_a(Blob) + expect(blob.path).to eq(snippet.file_name) + expect(blob.data).to eq(snippet.content) + end + context 'when repository does not exist' do it 'returns empty array' do expect(snippet.blobs).to be_empty @@ -527,14 +504,6 @@ RSpec.describe Snippet do expect(snippet.blobs).to all(be_a(Blob)) end end - - it 'returns a blob representing the snippet data' do - blob = snippet.blob - - expect(blob).to be_a(Blob) - expect(blob.path).to eq(snippet.file_name) - expect(blob.data).to eq(snippet.content) - end end describe '#to_json' do @@ -554,7 +523,7 @@ RSpec.describe Snippet do end describe '#storage' do - let(:snippet) { create(:snippet) } + let(:snippet) { build(:snippet, id: 1) } it "stores snippet in #{Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX} dir" do expect(snippet.storage.disk_path).to start_with Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX @@ -775,6 +744,16 @@ RSpec.describe Snippet do subject end + + context 'when ref is nil' do + let(:ref) { nil } + + it 'lists files from the repository from the deafult_branch' do + expect(snippet.repository).to receive(:ls_files).with(snippet.default_branch) + + subject + end + end end context 'when snippet does not have a repository' do @@ -787,4 +766,26 @@ RSpec.describe Snippet do end end end + + describe '#multiple_files?' do + subject { snippet.multiple_files? } + + context 'when snippet has multiple files' do + let(:snippet) { create(:snippet, :repository) } + + it { is_expected.to be_truthy } + end + + context 'when snippet does not have multiple files' do + let(:snippet) { create(:snippet, :empty_repo) } + + it { is_expected.to be_falsey } + end + + context 'when the snippet does not have a repository' do + let(:snippet) { build(:snippet) } + + it { is_expected.to be_falsey } + end + end end diff --git a/spec/models/snippet_statistics_spec.rb b/spec/models/snippet_statistics_spec.rb index ad25bd7b3be..8def6a0bbd4 100644 --- a/spec/models/snippet_statistics_spec.rb +++ b/spec/models/snippet_statistics_spec.rb @@ -36,7 +36,7 @@ RSpec.describe SnippetStatistics do subject { statistics.update_file_count } it 'updates the count of files' do - file_count = snippet_with_repo.repository.ls_files(nil).count + file_count = snippet_with_repo.repository.ls_files(snippet_with_repo.default_branch).count subject diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb index 68bb86bfa49..01ae80a61d1 100644 --- a/spec/models/terraform/state_spec.rb +++ b/spec/models/terraform/state_spec.rb @@ -7,8 +7,9 @@ RSpec.describe Terraform::State do let(:terraform_state_file) { fixture_file('terraform/terraform.tfstate') } - it { is_expected.to belong_to(:project) } + it { is_expected.to be_a FileStoreMounter } + it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:locked_by_user).class_name('User') } it { is_expected.to validate_presence_of(:project_id) } @@ -23,14 +24,6 @@ RSpec.describe Terraform::State do expect(subject.file.read).to eq(terraform_state_file) end end - - context 'when no file exists' do - subject { create(:terraform_state) } - - it 'creates a default file' do - expect(subject.file.read).to eq('{"version":1}') - end - end end describe '#file_store' do @@ -56,4 +49,55 @@ RSpec.describe Terraform::State do it_behaves_like 'mounted file in local store' end end + + describe '#latest_file' do + subject { terraform_state.latest_file } + + context 'versioning is enabled' do + let(:terraform_state) { create(:terraform_state, :with_version) } + let(:latest_version) { terraform_state.latest_version } + + it { is_expected.to eq latest_version.file } + + context 'but no version exists yet' do + let(:terraform_state) { create(:terraform_state) } + + it { is_expected.to be_nil } + end + end + + context 'versioning is disabled' do + let(:terraform_state) { create(:terraform_state, :with_file) } + + it { is_expected.to eq terraform_state.file } + end + end + + describe '#update_file!' do + let(:version) { 2 } + let(:data) { Hash[terraform_version: '0.12.21'].to_json } + + subject { terraform_state.update_file!(CarrierWaveStringFile.new(data), version: version) } + + context 'versioning is enabled' do + let(:terraform_state) { create(:terraform_state) } + + it 'creates a new version' do + expect { subject }.to change { Terraform::StateVersion.count } + + expect(terraform_state.latest_version.version).to eq(version) + expect(terraform_state.latest_version.file.read).to eq(data) + end + end + + context 'versioning is disabled' do + let(:terraform_state) { create(:terraform_state, :with_file) } + + it 'modifies the existing state record' do + expect { subject }.not_to change { Terraform::StateVersion.count } + + expect(terraform_state.latest_file.read).to eq(data) + end + end + end end diff --git a/spec/models/terraform/state_version_spec.rb b/spec/models/terraform/state_version_spec.rb new file mode 100644 index 00000000000..72dd29e1571 --- /dev/null +++ b/spec/models/terraform/state_version_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Terraform::StateVersion do + it { is_expected.to be_a FileStoreMounter } + + it { is_expected.to belong_to(:terraform_state).required } + it { is_expected.to belong_to(:created_by_user).class_name('User').optional } + + describe 'scopes' do + describe '.ordered_by_version_desc' do + let(:terraform_state) { create(:terraform_state) } + let(:versions) { [4, 2, 5, 1, 3] } + + subject { described_class.ordered_by_version_desc } + + before do + versions.each do |version| + create(:terraform_state_version, terraform_state: terraform_state, version: version) + end + end + + it { expect(subject.map(&:version)).to eq(versions.sort.reverse) } + end + end + + context 'file storage' do + subject { create(:terraform_state_version) } + + before do + stub_terraform_state_object_storage(Terraform::StateUploader) + end + + describe '#file' do + let(:terraform_state_file) { fixture_file('terraform/terraform.tfstate') } + + before do + subject.file = CarrierWaveStringFile.new(terraform_state_file) + subject.save! + end + + it 'returns the saved file' do + expect(subject.file.read).to eq(terraform_state_file) + end + end + + describe '#file_store' do + it 'returns the value' do + [ObjectStorage::Store::LOCAL, ObjectStorage::Store::REMOTE].each do |store| + subject.update!(file_store: store) + + expect(subject.file_store).to eq(store) + end + end + end + + describe '#update_file_store' do + context 'when file is stored in object storage' do + it 'sets file_store to remote' do + expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE) + end + end + + context 'when file is stored locally' do + before do + stub_terraform_state_object_storage(enabled: false) + end + + it 'sets file_store to local' do + expect(subject.file_store).to eq(ObjectStorage::Store::LOCAL) + end + end + end + end +end diff --git a/spec/models/user_agent_detail_spec.rb b/spec/models/user_agent_detail_spec.rb index e3f3d9c342b..ca81ef38ecc 100644 --- a/spec/models/user_agent_detail_spec.rb +++ b/spec/models/user_agent_detail_spec.rb @@ -18,8 +18,10 @@ RSpec.describe UserAgentDetail do end describe '.valid?' do + let(:issue) { create(:issue) } + it 'is valid with a subject' do - detail = build(:user_agent_detail) + detail = build(:user_agent_detail, subject: issue) expect(detail).to be_valid end diff --git a/spec/models/user_interacted_project_spec.rb b/spec/models/user_interacted_project_spec.rb index 2fec8be76e8..aa038b06d8d 100644 --- a/spec/models/user_interacted_project_spec.rb +++ b/spec/models/user_interacted_project_spec.rb @@ -3,14 +3,17 @@ require 'spec_helper' RSpec.describe UserInteractedProject do + let_it_be(:project) { create(:project) } + let_it_be(:author) { project.creator } + describe '.track' do subject { described_class.track(event) } - let(:event) { build(:event) } + let(:event) { build(:event, project: project, author: author) } Event.actions.each_key do |action| context "for all actions (event types)" do - let(:event) { build(:event, action: action) } + let(:event) { build(:event, project: project, author: author, action: action) } it 'creates a record' do expect { subject }.to change { described_class.count }.from(0).to(1) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e9077ed4143..1841288cd4b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -47,6 +47,9 @@ RSpec.describe User do it { is_expected.to delegate_method(:sourcegraph_enabled).to(:user_preference) } it { is_expected.to delegate_method(:sourcegraph_enabled=).to(:user_preference).with_arguments(:args) } + it { is_expected.to delegate_method(:gitpod_enabled).to(:user_preference) } + it { is_expected.to delegate_method(:gitpod_enabled=).to(:user_preference).with_arguments(:args) } + it { is_expected.to delegate_method(:setup_for_company).to(:user_preference) } it { is_expected.to delegate_method(:setup_for_company=).to(:user_preference).with_arguments(:args) } @@ -68,6 +71,7 @@ RSpec.describe User do it { is_expected.to have_one(:namespace) } it { is_expected.to have_one(:status) } it { is_expected.to have_one(:user_detail) } + it { is_expected.to have_one(:atlassian_identity) } it { is_expected.to have_one(:user_highest_role) } it { is_expected.to have_many(:snippets).dependent(:destroy) } it { is_expected.to have_many(:members) } @@ -179,6 +183,58 @@ RSpec.describe User do end.to have_enqueued_job.on_queue('mailers').exactly(:twice) end end + + context 'emails sent on changing password' do + context 'when password is updated' do + context 'default behaviour' do + it 'enqueues the `password changed` email' do + user.password = User.random_password + + expect { user.save! }.to have_enqueued_mail(DeviseMailer, :password_change) + end + + it 'does not enqueue the `admin changed your password` email' do + user.password = User.random_password + + expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_change_by_admin) + end + end + + context '`admin changed your password` email' do + it 'is enqueued only when explicitly allowed' do + user.password = User.random_password + user.send_only_admin_changed_your_password_notification! + + expect { user.save! }.to have_enqueued_mail(DeviseMailer, :password_change_by_admin) + end + + it '`password changed` email is not enqueued if it is explicitly allowed' do + user.password = User.random_password + user.send_only_admin_changed_your_password_notification! + + expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_changed) + end + + it 'is not enqueued if sending notifications on password updates is turned off as per Devise config' do + user.password = User.random_password + user.send_only_admin_changed_your_password_notification! + + allow(Devise).to receive(:send_password_change_notification).and_return(false) + + expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_change_by_admin) + end + end + end + + context 'when password is not updated' do + it 'does not enqueue the `admin changed your password` email even if explicitly allowed' do + user.name = 'John' + user.send_only_admin_changed_your_password_notification! + + expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_change_by_admin) + end + end + end end describe 'validations' do @@ -659,30 +715,40 @@ RSpec.describe User do expect(users_with_two_factor).not_to include(user_without_2fa.id) end - it "returns users with 2fa enabled via U2F" do - user_with_2fa = create(:user, :two_factor_via_u2f) - user_without_2fa = create(:user) - users_with_two_factor = described_class.with_two_factor.pluck(:id) + shared_examples "returns the right users" do |trait| + it "returns users with 2fa enabled via hardware token" do + user_with_2fa = create(:user, trait) + user_without_2fa = create(:user) + users_with_two_factor = described_class.with_two_factor.pluck(:id) - expect(users_with_two_factor).to include(user_with_2fa.id) - expect(users_with_two_factor).not_to include(user_without_2fa.id) - end + expect(users_with_two_factor).to include(user_with_2fa.id) + expect(users_with_two_factor).not_to include(user_without_2fa.id) + end - it "returns users with 2fa enabled via OTP and U2F" do - user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f) - user_without_2fa = create(:user) - users_with_two_factor = described_class.with_two_factor.pluck(:id) + it "returns users with 2fa enabled via OTP and hardware token" do + user_with_2fa = create(:user, :two_factor_via_otp, trait) + user_without_2fa = create(:user) + users_with_two_factor = described_class.with_two_factor.pluck(:id) - expect(users_with_two_factor).to eq([user_with_2fa.id]) - expect(users_with_two_factor).not_to include(user_without_2fa.id) + expect(users_with_two_factor).to eq([user_with_2fa.id]) + expect(users_with_two_factor).not_to include(user_without_2fa.id) + end + + it 'works with ORDER BY' do + user_with_2fa = create(:user, :two_factor_via_otp, trait) + + expect(described_class + .with_two_factor + .reorder_by_name).to eq([user_with_2fa]) + end end - it 'works with ORDER BY' do - user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f) + describe "and U2F" do + it_behaves_like "returns the right users", :two_factor_via_u2f + end - expect(described_class - .with_two_factor - .reorder_by_name).to eq([user_with_2fa]) + describe "and WebAuthn" do + it_behaves_like "returns the right users", :two_factor_via_webauthn end end @@ -696,22 +762,44 @@ RSpec.describe User do expect(users_without_two_factor).not_to include(user_with_2fa.id) end - it "excludes users with 2fa enabled via U2F" do - user_with_2fa = create(:user, :two_factor_via_u2f) - user_without_2fa = create(:user) - users_without_two_factor = described_class.without_two_factor.pluck(:id) + describe "and u2f" do + it "excludes users with 2fa enabled via U2F" do + user_with_2fa = create(:user, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_without_two_factor = described_class.without_two_factor.pluck(:id) - expect(users_without_two_factor).to include(user_without_2fa.id) - expect(users_without_two_factor).not_to include(user_with_2fa.id) + expect(users_without_two_factor).to include(user_without_2fa.id) + expect(users_without_two_factor).not_to include(user_with_2fa.id) + end + + it "excludes users with 2fa enabled via OTP and U2F" do + user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_without_two_factor = described_class.without_two_factor.pluck(:id) + + expect(users_without_two_factor).to include(user_without_2fa.id) + expect(users_without_two_factor).not_to include(user_with_2fa.id) + end end - it "excludes users with 2fa enabled via OTP and U2F" do - user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f) - user_without_2fa = create(:user) - users_without_two_factor = described_class.without_two_factor.pluck(:id) + describe "and webauthn" do + it "excludes users with 2fa enabled via WebAuthn" do + user_with_2fa = create(:user, :two_factor_via_webauthn) + user_without_2fa = create(:user) + users_without_two_factor = described_class.without_two_factor.pluck(:id) - expect(users_without_two_factor).to include(user_without_2fa.id) - expect(users_without_two_factor).not_to include(user_with_2fa.id) + expect(users_without_two_factor).to include(user_without_2fa.id) + expect(users_without_two_factor).not_to include(user_with_2fa.id) + end + + it "excludes users with 2fa enabled via OTP and WebAuthn" do + user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_webauthn) + user_without_2fa = create(:user) + users_without_two_factor = described_class.without_two_factor.pluck(:id) + + expect(users_without_two_factor).to include(user_without_2fa.id) + expect(users_without_two_factor).not_to include(user_with_2fa.id) + end end end @@ -1662,7 +1750,7 @@ RSpec.describe User do # add user to project project.add_maintainer(user) - # create invite to projet + # create invite to project create(:project_member, :developer, project: project, invite_token: '1234', invite_email: 'inviteduser1@example.com') # create request to join project @@ -4490,6 +4578,44 @@ RSpec.describe User do end end + describe '#notification_settings_for_groups' do + let_it_be(:user) { create(:user) } + let_it_be(:groups) { create_list(:group, 2) } + + subject { user.notification_settings_for_groups(arg) } + + before do + groups.each do |group| + group.add_maintainer(user) + end + end + + shared_examples_for 'notification_settings_for_groups method' do + it 'returns NotificationSetting objects for provided groups', :aggregate_failures do + expect(subject.count).to eq(groups.count) + expect(subject.map(&:source_id)).to match_array(groups.map(&:id)) + end + end + + context 'when given an ActiveRecord relationship' do + let_it_be(:arg) { Group.where(id: groups.map(&:id)) } + + it_behaves_like 'notification_settings_for_groups method' + + it 'uses #select to maintain lazy querying behavior' do + expect(arg).to receive(:select).and_call_original + + subject + end + end + + context 'when given an Array of Groups' do + let_it_be(:arg) { groups } + + it_behaves_like 'notification_settings_for_groups method' + end + end + describe '#notification_email_for' do let(:user) { create(:user) } let(:group) { create(:group) } diff --git a/spec/models/x509_certificate_spec.rb b/spec/models/x509_certificate_spec.rb index 880c5014a84..d3b4470d3f4 100644 --- a/spec/models/x509_certificate_spec.rb +++ b/spec/models/x509_certificate_spec.rb @@ -68,6 +68,8 @@ RSpec.describe X509Certificate do end describe 'validators' do + let_it_be(:issuer) { create(:x509_issuer) } + it 'accepts correct subject_key_identifier' do subject_key_identifiers = [ 'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB', @@ -75,7 +77,7 @@ RSpec.describe X509Certificate do ] subject_key_identifiers.each do |identifier| - expect(build(:x509_certificate, subject_key_identifier: identifier)).to be_valid + expect(build(:x509_certificate, x509_issuer: issuer, subject_key_identifier: identifier)).to be_valid end end @@ -88,7 +90,7 @@ RSpec.describe X509Certificate do ] subject_key_identifiers.each do |identifier| - expect(build(:x509_certificate, subject_key_identifier: identifier)).to be_invalid + expect(build(:x509_certificate, x509_issuer: issuer, subject_key_identifier: identifier)).to be_invalid end end @@ -99,7 +101,7 @@ RSpec.describe X509Certificate do ] emails.each do |email| - expect(build(:x509_certificate, email: email)).to be_valid + expect(build(:x509_certificate, x509_issuer: issuer, email: email)).to be_valid end end @@ -110,20 +112,20 @@ RSpec.describe X509Certificate do ] emails.each do |email| - expect(build(:x509_certificate, email: email)).to be_invalid + expect(build(:x509_certificate, x509_issuer: issuer, email: email)).to be_invalid end end it 'accepts valid serial_number' do - expect(build(:x509_certificate, serial_number: 123412341234)).to be_valid + expect(build(:x509_certificate, x509_issuer: issuer, serial_number: 123412341234)).to be_valid # rfc 5280 - 4.1.2.2 Serial number (20 octets is the maximum) - expect(build(:x509_certificate, serial_number: 1461501637330902918203684832716283019655932542975)).to be_valid - expect(build(:x509_certificate, serial_number: 'ffffffffffffffffffffffffffffffffffffffff'.to_i(16))).to be_valid + expect(build(:x509_certificate, x509_issuer: issuer, serial_number: 1461501637330902918203684832716283019655932542975)).to be_valid + expect(build(:x509_certificate, x509_issuer: issuer, serial_number: 'ffffffffffffffffffffffffffffffffffffffff'.to_i(16))).to be_valid end it 'rejects invalid serial_number' do - expect(build(:x509_certificate, serial_number: "sgsgfsdgdsfg")).to be_invalid + expect(build(:x509_certificate, x509_issuer: issuer, serial_number: "sgsgfsdgdsfg")).to be_invalid end end end -- cgit v1.2.3