summaryrefslogtreecommitdiff
path: root/spec/models
diff options
context:
space:
mode:
Diffstat (limited to 'spec/models')
-rw-r--r--spec/models/abuse/trust_score_spec.rb57
-rw-r--r--spec/models/abuse_report_spec.rb212
-rw-r--r--spec/models/achievements/user_achievement_spec.rb29
-rw-r--r--spec/models/active_session_spec.rb46
-rw-r--r--spec/models/airflow/dags_spec.rb17
-rw-r--r--spec/models/alert_management/alert_assignee_spec.rb6
-rw-r--r--spec/models/alert_management/alert_spec.rb10
-rw-r--r--spec/models/alert_management/alert_user_mention_spec.rb6
-rw-r--r--spec/models/analytics/cycle_analytics/stage_spec.rb74
-rw-r--r--spec/models/application_record_spec.rb10
-rw-r--r--spec/models/application_setting_spec.rb104
-rw-r--r--spec/models/audit_event_spec.rb4
-rw-r--r--spec/models/authentication_event_spec.rb15
-rw-r--r--spec/models/award_emoji_spec.rb35
-rw-r--r--spec/models/awareness_session_spec.rb163
-rw-r--r--spec/models/blob_viewer/metrics_dashboard_yml_spec.rb16
-rw-r--r--spec/models/blob_viewer/package_json_spec.rb13
-rw-r--r--spec/models/board_spec.rb8
-rw-r--r--spec/models/broadcast_message_spec.rb52
-rw-r--r--spec/models/bulk_import_spec.rb29
-rw-r--r--spec/models/bulk_imports/batch_tracker_spec.rb16
-rw-r--r--spec/models/bulk_imports/entity_spec.rb95
-rw-r--r--spec/models/bulk_imports/export_batch_spec.rb17
-rw-r--r--spec/models/bulk_imports/export_spec.rb3
-rw-r--r--spec/models/bulk_imports/file_transfer/group_config_spec.rb49
-rw-r--r--spec/models/bulk_imports/file_transfer/project_config_spec.rb41
-rw-r--r--spec/models/bulk_imports/tracker_spec.rb5
-rw-r--r--spec/models/chat_name_spec.rb7
-rw-r--r--spec/models/ci/bridge_spec.rb32
-rw-r--r--spec/models/ci/build_dependencies_spec.rb11
-rw-r--r--spec/models/ci/build_metadata_spec.rb12
-rw-r--r--spec/models/ci/build_need_spec.rb2
-rw-r--r--spec/models/ci/build_pending_state_spec.rb7
-rw-r--r--spec/models/ci/build_report_result_spec.rb13
-rw-r--r--spec/models/ci/build_runner_session_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb671
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb9
-rw-r--r--spec/models/ci/build_trace_metadata_spec.rb2
-rw-r--r--spec/models/ci/build_trace_spec.rb6
-rw-r--r--spec/models/ci/catalog/listing_spec.rb66
-rw-r--r--spec/models/ci/catalog/resource_spec.rb42
-rw-r--r--spec/models/ci/commit_with_pipeline_spec.rb45
-rw-r--r--spec/models/ci/daily_build_group_report_result_spec.rb6
-rw-r--r--spec/models/ci/group_spec.rb12
-rw-r--r--spec/models/ci/group_variable_spec.rb14
-rw-r--r--spec/models/ci/job_artifact_spec.rb31
-rw-r--r--spec/models/ci/job_token/allowlist_spec.rb24
-rw-r--r--spec/models/ci/job_token/scope_spec.rb18
-rw-r--r--spec/models/ci/job_variable_spec.rb2
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb17
-rw-r--r--spec/models/ci/pipeline_spec.rb479
-rw-r--r--spec/models/ci/processable_spec.rb20
-rw-r--r--spec/models/ci/ref_spec.rb7
-rw-r--r--spec/models/ci/resource_group_spec.rb21
-rw-r--r--spec/models/ci/runner_machine_spec.rb197
-rw-r--r--spec/models/ci/runner_manager_build_spec.rb100
-rw-r--r--spec/models/ci/runner_manager_spec.rb291
-rw-r--r--spec/models/ci/runner_spec.rb302
-rw-r--r--spec/models/ci/runner_version_spec.rb13
-rw-r--r--spec/models/ci/secure_file_spec.rb29
-rw-r--r--spec/models/ci/sources/pipeline_spec.rb7
-rw-r--r--spec/models/ci/stage_spec.rb24
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/clusters/agent_spec.rb195
-rw-r--r--spec/models/clusters/agent_token_spec.rb22
-rw-r--r--spec/models/clusters/agents/authorizations/ci_access/group_authorization_spec.rb (renamed from spec/models/clusters/agents/group_authorization_spec.rb)4
-rw-r--r--spec/models/clusters/agents/authorizations/ci_access/implicit_authorization_spec.rb (renamed from spec/models/clusters/agents/implicit_authorization_spec.rb)2
-rw-r--r--spec/models/clusters/agents/authorizations/ci_access/project_authorization_spec.rb (renamed from spec/models/clusters/agents/project_authorization_spec.rb)4
-rw-r--r--spec/models/clusters/agents/authorizations/user_access/group_authorization_spec.rb71
-rw-r--r--spec/models/clusters/agents/authorizations/user_access/project_authorization_spec.rb50
-rw-r--r--spec/models/clusters/applications/crossplane_spec.rb62
-rw-r--r--spec/models/clusters/applications/helm_spec.rb116
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb180
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb130
-rw-r--r--spec/models/clusters/applications/knative_spec.rb260
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb349
-rw-r--r--spec/models/clusters/applications/runner_spec.rb127
-rw-r--r--spec/models/clusters/cluster_spec.rb254
-rw-r--r--spec/models/clusters/integrations/prometheus_spec.rb4
-rw-r--r--spec/models/clusters/kubernetes_namespace_spec.rb10
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb43
-rw-r--r--spec/models/commit_collection_spec.rb33
-rw-r--r--spec/models/commit_spec.rb152
-rw-r--r--spec/models/commit_status_spec.rb55
-rw-r--r--spec/models/compare_spec.rb34
-rw-r--r--spec/models/concerns/atomic_internal_id_spec.rb99
-rw-r--r--spec/models/concerns/awareness_spec.rb39
-rw-r--r--spec/models/concerns/ci/has_status_spec.rb26
-rw-r--r--spec/models/concerns/ci/maskable_spec.rb2
-rw-r--r--spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb80
-rw-r--r--spec/models/concerns/ci/partitionable/switch_spec.rb9
-rw-r--r--spec/models/concerns/ci/partitionable_spec.rb12
-rw-r--r--spec/models/concerns/ci/track_environment_usage_spec.rb20
-rw-r--r--spec/models/concerns/clusters/agents/authorization_config_scopes_spec.rb21
-rw-r--r--spec/models/concerns/clusters/agents/authorizations/ci_access/config_scopes_spec.rb21
-rw-r--r--spec/models/concerns/clusters/agents/authorizations/user_access/scopes_spec.rb27
-rw-r--r--spec/models/concerns/database_event_tracking_spec.rb44
-rw-r--r--spec/models/concerns/deployment_platform_spec.rb45
-rw-r--r--spec/models/concerns/each_batch_spec.rb32
-rw-r--r--spec/models/concerns/expirable_spec.rb66
-rw-r--r--spec/models/concerns/has_user_type_spec.rb18
-rw-r--r--spec/models/concerns/issuable_spec.rb36
-rw-r--r--spec/models/concerns/mentionable_spec.rb2
-rw-r--r--spec/models/concerns/prometheus_adapter_spec.rb2
-rw-r--r--spec/models/concerns/protected_ref_access_spec.rb45
-rw-r--r--spec/models/concerns/redis_cacheable_spec.rb52
-rw-r--r--spec/models/concerns/require_email_verification_spec.rb16
-rw-r--r--spec/models/concerns/routable_spec.rb11
-rw-r--r--spec/models/concerns/subscribable_spec.rb26
-rw-r--r--spec/models/concerns/taskable_spec.rb7
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb5
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/base_spec.rb37
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb149
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb92
-rw-r--r--spec/models/concerns/uniquify_spec.rb44
-rw-r--r--spec/models/concerns/web_hooks/has_web_hooks_spec.rb41
-rw-r--r--spec/models/container_registry/data_repair_detail_spec.rb29
-rw-r--r--spec/models/container_registry/event_spec.rb24
-rw-r--r--spec/models/container_repository_spec.rb100
-rw-r--r--spec/models/customer_relations/contact_spec.rb8
-rw-r--r--spec/models/customer_relations/organization_spec.rb74
-rw-r--r--spec/models/deployment_spec.rb24
-rw-r--r--spec/models/design_management/design_collection_spec.rb2
-rw-r--r--spec/models/design_management/design_spec.rb17
-rw-r--r--spec/models/design_management/git_repository_spec.rb64
-rw-r--r--spec/models/design_management/repository_spec.rb57
-rw-r--r--spec/models/design_management/version_spec.rb10
-rw-r--r--spec/models/diff_note_spec.rb35
-rw-r--r--spec/models/environment_spec.rb33
-rw-r--r--spec/models/environment_status_spec.rb39
-rw-r--r--spec/models/error_tracking/project_error_tracking_setting_spec.rb38
-rw-r--r--spec/models/event_collection_spec.rb3
-rw-r--r--spec/models/event_spec.rb26
-rw-r--r--spec/models/generic_commit_status_spec.rb3
-rw-r--r--spec/models/group_group_link_spec.rb127
-rw-r--r--spec/models/group_label_spec.rb43
-rw-r--r--spec/models/group_spec.rb62
-rw-r--r--spec/models/hooks/web_hook_spec.rb24
-rw-r--r--spec/models/import_export_upload_spec.rb2
-rw-r--r--spec/models/import_failure_spec.rb27
-rw-r--r--spec/models/instance_configuration_spec.rb6
-rw-r--r--spec/models/integration_spec.rb2
-rw-r--r--spec/models/integrations/apple_app_store_spec.rb15
-rw-r--r--spec/models/integrations/buildkite_spec.rb9
-rw-r--r--spec/models/integrations/campfire_spec.rb14
-rw-r--r--spec/models/integrations/chat_message/deployment_message_spec.rb2
-rw-r--r--spec/models/integrations/every_integration_spec.rb4
-rw-r--r--spec/models/integrations/ewm_spec.rb12
-rw-r--r--spec/models/integrations/field_spec.rb16
-rw-r--r--spec/models/integrations/gitlab_slack_application_spec.rb337
-rw-r--r--spec/models/integrations/google_play_spec.rb101
-rw-r--r--spec/models/integrations/hangouts_chat_spec.rb17
-rw-r--r--spec/models/integrations/harbor_spec.rb2
-rw-r--r--spec/models/integrations/jira_spec.rb164
-rw-r--r--spec/models/integrations/mattermost_slash_commands_spec.rb9
-rw-r--r--spec/models/integrations/prometheus_spec.rb50
-rw-r--r--spec/models/integrations/redmine_spec.rb4
-rw-r--r--spec/models/integrations/slack_slash_commands_spec.rb9
-rw-r--r--spec/models/integrations/slack_workspace/api_scope_spec.rb20
-rw-r--r--spec/models/integrations/squash_tm_spec.rb117
-rw-r--r--spec/models/integrations/youtrack_spec.rb6
-rw-r--r--spec/models/internal_id_spec.rb12
-rw-r--r--spec/models/issue_spec.rb260
-rw-r--r--spec/models/key_spec.rb2
-rw-r--r--spec/models/label_spec.rb11
-rw-r--r--spec/models/member_spec.rb117
-rw-r--r--spec/models/members/member_role_spec.rb107
-rw-r--r--spec/models/members/project_member_spec.rb20
-rw-r--r--spec/models/merge_request/diff_llm_summary_spec.rb17
-rw-r--r--spec/models/merge_request/metrics_spec.rb8
-rw-r--r--spec/models/merge_request_spec.rb138
-rw-r--r--spec/models/milestone_spec.rb12
-rw-r--r--spec/models/ml/candidate_spec.rb127
-rw-r--r--spec/models/ml/experiment_spec.rb26
-rw-r--r--spec/models/namespace/aggregation_schedule_spec.rb26
-rw-r--r--spec/models/namespace/package_setting_spec.rb52
-rw-r--r--spec/models/namespace/root_storage_statistics_spec.rb162
-rw-r--r--spec/models/namespace_setting_spec.rb92
-rw-r--r--spec/models/namespace_spec.rb483
-rw-r--r--spec/models/namespaces/randomized_suffix_path_spec.rb2
-rw-r--r--spec/models/note_spec.rb53
-rw-r--r--spec/models/notes/note_metadata_spec.rb35
-rw-r--r--spec/models/oauth_access_token_spec.rb4
-rw-r--r--spec/models/onboarding/completion_spec.rb73
-rw-r--r--spec/models/onboarding/progress_spec.rb6
-rw-r--r--spec/models/operations/feature_flag_spec.rb6
-rw-r--r--spec/models/organization_spec.rb98
-rw-r--r--spec/models/packages/debian/file_metadatum_spec.rb44
-rw-r--r--spec/models/packages/debian/group_distribution_spec.rb7
-rw-r--r--spec/models/packages/debian/project_distribution_spec.rb7
-rw-r--r--spec/models/packages/dependency_spec.rb19
-rw-r--r--spec/models/packages/event_spec.rb51
-rw-r--r--spec/models/packages/npm/metadata_cache_spec.rb150
-rw-r--r--spec/models/packages/npm/metadatum_spec.rb14
-rw-r--r--spec/models/packages/package_file_spec.rb12
-rw-r--r--spec/models/packages/package_spec.rb79
-rw-r--r--spec/models/pages/lookup_path_spec.rb92
-rw-r--r--spec/models/pages_deployment_spec.rb60
-rw-r--r--spec/models/pages_domain_spec.rb39
-rw-r--r--spec/models/personal_access_token_spec.rb93
-rw-r--r--spec/models/plan_limits_spec.rb3
-rw-r--r--spec/models/preloaders/labels_preloader_spec.rb14
-rw-r--r--spec/models/preloaders/runner_manager_policy_preloader_spec.rb38
-rw-r--r--spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb16
-rw-r--r--spec/models/preloaders/users_max_access_level_by_project_preloader_spec.rb61
-rw-r--r--spec/models/preloaders/users_max_access_level_in_projects_preloader_spec.rb51
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb18
-rw-r--r--spec/models/project_feature_spec.rb36
-rw-r--r--spec/models/project_label_spec.rb35
-rw-r--r--spec/models/project_setting_spec.rb70
-rw-r--r--spec/models/project_spec.rb598
-rw-r--r--spec/models/project_wiki_spec.rb29
-rw-r--r--spec/models/projects/data_transfer_spec.rb32
-rw-r--r--spec/models/projects/forks/details_spec.rb (renamed from spec/models/projects/forks/divergence_counts_spec.rb)78
-rw-r--r--spec/models/projects/import_export/relation_export_spec.rb22
-rw-r--r--spec/models/protected_branch/merge_access_level_spec.rb5
-rw-r--r--spec/models/protected_branch/push_access_level_spec.rb5
-rw-r--r--spec/models/protected_branch_spec.rb169
-rw-r--r--spec/models/protected_tag/create_access_level_spec.rb13
-rw-r--r--spec/models/releases/link_spec.rb9
-rw-r--r--spec/models/releases/source_spec.rb6
-rw-r--r--spec/models/repository_spec.rb313
-rw-r--r--spec/models/resource_events/abuse_report_event_spec.rb17
-rw-r--r--spec/models/resource_events/issue_assignment_event_spec.rb17
-rw-r--r--spec/models/resource_events/merge_request_assignment_event_spec.rb17
-rw-r--r--spec/models/resource_state_event_spec.rb14
-rw-r--r--spec/models/serverless/domain_cluster_spec.rb75
-rw-r--r--spec/models/serverless/domain_spec.rb97
-rw-r--r--spec/models/serverless/function_spec.rb21
-rw-r--r--spec/models/service_desk/custom_email_credential_spec.rb67
-rw-r--r--spec/models/service_desk/custom_email_verification_spec.rb170
-rw-r--r--spec/models/service_desk_setting_spec.rb65
-rw-r--r--spec/models/slack_integration_spec.rb147
-rw-r--r--spec/models/snippet_spec.rb32
-rw-r--r--spec/models/spam_log_spec.rb3
-rw-r--r--spec/models/terraform/state_spec.rb2
-rw-r--r--spec/models/terraform/state_version_spec.rb4
-rw-r--r--spec/models/u2f_registration_spec.rb141
-rw-r--r--spec/models/upload_spec.rb8
-rw-r--r--spec/models/user_detail_spec.rb20
-rw-r--r--spec/models/user_preference_spec.rb63
-rw-r--r--spec/models/user_spec.rb526
-rw-r--r--spec/models/users/credit_card_validation_spec.rb103
-rw-r--r--spec/models/wiki_directory_spec.rb9
-rw-r--r--spec/models/wiki_page/meta_spec.rb10
-rw-r--r--spec/models/wiki_page_spec.rb14
-rw-r--r--spec/models/work_item_spec.rb244
-rw-r--r--spec/models/work_items/resource_link_event_spec.rb16
-rw-r--r--spec/models/work_items/widget_definition_spec.rb5
-rw-r--r--spec/models/work_items/widgets/award_emoji_spec.rb30
-rw-r--r--spec/models/work_items/widgets/notifications_spec.rb19
251 files changed, 9483 insertions, 5455 deletions
diff --git a/spec/models/abuse/trust_score_spec.rb b/spec/models/abuse/trust_score_spec.rb
new file mode 100644
index 00000000000..755309ac699
--- /dev/null
+++ b/spec/models/abuse/trust_score_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Abuse::TrustScore, feature_category: :instance_resiliency do
+ let_it_be(:user) { create(:user) }
+
+ let(:correlation_id) { nil }
+
+ let(:abuse_trust_score) do
+ create(:abuse_trust_score, user: user, correlation_id_value: correlation_id)
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:score) }
+ it { is_expected.to validate_presence_of(:source) }
+ end
+
+ describe 'create' do
+ subject { abuse_trust_score }
+
+ before do
+ allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('123abc')
+ stub_const('Abuse::TrustScore::MAX_EVENTS', 2)
+ end
+
+ context 'if correlation ID is nil' do
+ it 'adds the correlation id' do
+ expect(subject.correlation_id_value).to eq('123abc')
+ end
+ end
+
+ context 'if correlation ID is set' do
+ let(:correlation_id) { 'already-set' }
+
+ it 'does not change the correlation id' do
+ expect(subject.correlation_id_value).to eq('already-set')
+ end
+ end
+
+ context 'if max events is exceeded' do
+ it 'removes the oldest events' do
+ first = create(:abuse_trust_score, user: user)
+ create(:abuse_trust_score, user: user)
+ create(:abuse_trust_score, user: user)
+
+ expect(user.abuse_trust_scores.count).to eq(2)
+ expect(described_class.find_by_id(first.id)).to eq(nil)
+ end
+ end
+ end
+end
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 7995cc36383..edfac39728f 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe AbuseReport, feature_category: :insider_threat do
+ include Gitlab::Routing.url_helpers
+
let_it_be(:report, reload: true) { create(:abuse_report) }
let_it_be(:user, reload: true) { create(:admin) }
@@ -13,6 +15,7 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
describe 'associations' do
it { is_expected.to belong_to(:reporter).class_name('User') }
it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_many(:events).class_name('ResourceEvents::AbuseReportEvent').inverse_of(:abuse_report) }
it "aliases reporter to author" do
expect(subject.author).to be(subject.reporter)
@@ -68,6 +71,53 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
"https://gitlab.com/#{SecureRandom.alphanumeric(494)}"
]).for(:links_to_spam)
}
+
+ context 'for screenshot' do
+ let(:txt_file) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') }
+ let(:img_file) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') }
+
+ it { is_expected.not_to allow_value(txt_file).for(:screenshot) }
+ it { is_expected.to allow_value(img_file).for(:screenshot) }
+
+ it { is_expected.to allow_value(nil).for(:screenshot) }
+ it { is_expected.to allow_value('').for(:screenshot) }
+ end
+ end
+
+ describe 'scopes' do
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:report1) { create(:abuse_report, reporter: reporter) }
+ let_it_be(:report2) { create(:abuse_report, :closed, category: 'phishing') }
+
+ describe '.by_reporter_id' do
+ subject(:results) { described_class.by_reporter_id(reporter.id) }
+
+ it 'returns reports with reporter_id equal to the given user id' do
+ expect(subject).to match_array([report1])
+ end
+ end
+
+ describe '.open' do
+ subject(:results) { described_class.open }
+
+ it 'returns reports without resolved_at value' do
+ expect(subject).to match_array([report, report1])
+ end
+ end
+
+ describe '.closed' do
+ subject(:results) { described_class.closed }
+
+ it 'returns reports with resolved_at value' do
+ expect(subject).to match_array([report2])
+ end
+ end
+
+ describe '.by_category' do
+ it 'returns abuse reports with the specified category' do
+ expect(described_class.by_category('phishing')).to match_array([report2])
+ end
+ end
end
describe 'before_validation' do
@@ -109,6 +159,168 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
end
end
+ describe '#screenshot_path' do
+ let(:report) { create(:abuse_report, :with_screenshot) }
+
+ context 'with asset host configured' do
+ let(:asset_host) { 'https://gitlab-assets.example.com' }
+
+ before do
+ allow(ActionController::Base).to receive(:asset_host) { asset_host }
+ end
+
+ it 'returns a full URL with the asset host and system path' do
+ expect(report.screenshot_path).to eq("#{asset_host}#{report.screenshot.url}")
+ end
+ end
+
+ context 'when no asset path configured' do
+ let(:base_url) { Gitlab.config.gitlab.base_url }
+
+ it 'returns a full URL with the base url and system path' do
+ expect(report.screenshot_path).to eq("#{base_url}#{report.screenshot.url}")
+ end
+ end
+ end
+
+ describe '#report_type' do
+ let(:report) { build_stubbed(:abuse_report, reported_from_url: url) }
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:merge_request) { create(:merge_request) }
+ let_it_be(:user) { create(:user) }
+
+ subject { report.report_type }
+
+ context 'when reported from an issue' do
+ let(:url) { project_issue_url(issue.project, issue) }
+
+ it { is_expected.to eq :issue }
+ end
+
+ context 'when reported from a merge request' do
+ let(:url) { project_merge_request_url(merge_request.project, merge_request) }
+
+ it { is_expected.to eq :merge_request }
+ end
+
+ context 'when reported from a profile' do
+ let(:url) { user_url(user) }
+
+ it { is_expected.to eq :profile }
+ end
+
+ describe 'comment type' do
+ context 'when reported from an issue comment' do
+ let(:url) { project_issue_url(issue.project, issue, anchor: 'note_123') }
+
+ it { is_expected.to eq :comment }
+ end
+
+ context 'when reported from a merge request comment' do
+ let(:url) { project_merge_request_url(merge_request.project, merge_request, anchor: 'note_123') }
+
+ it { is_expected.to eq :comment }
+ end
+
+ context 'when anchor exists not from an issue or merge request URL' do
+ let(:url) { user_url(user, anchor: 'note_123') }
+
+ it { is_expected.to eq :profile }
+ end
+
+ context 'when note id is invalid' do
+ let(:url) { project_merge_request_url(merge_request.project, merge_request, anchor: 'note_12x') }
+
+ it { is_expected.to eq :merge_request }
+ end
+ end
+
+ context 'when URL cannot be matched' do
+ let(:url) { '/xxx' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#reported_content' do
+ let(:report) { build_stubbed(:abuse_report, reported_from_url: url) }
+ let_it_be(:issue) { create(:issue, description: 'issue description') }
+ let_it_be(:merge_request) { create(:merge_request, description: 'mr description') }
+ let_it_be(:user) { create(:user) }
+
+ subject { report.reported_content }
+
+ context 'when reported from an issue' do
+ let(:url) { project_issue_url(issue.project, issue) }
+
+ it { is_expected.to eq issue.description_html }
+ end
+
+ context 'when reported from a merge request' do
+ let(:url) { project_merge_request_url(merge_request.project, merge_request) }
+
+ it { is_expected.to eq merge_request.description_html }
+ end
+
+ context 'when reported from a merge request with an invalid note ID' do
+ let(:url) do
+ "#{project_merge_request_url(merge_request.project, merge_request)}#note_[]"
+ end
+
+ it { is_expected.to eq merge_request.description_html }
+ end
+
+ context 'when reported from a profile' do
+ let(:url) { user_url(user) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when reported from an unknown URL' do
+ let(:url) { '/xxx' }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when reported from an invalid URL' do
+ let(:url) { 'http://example.com/[]' }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when reported from an issue comment' do
+ let(:note) { create(:note, noteable: issue, project: issue.project, note: 'comment in issue') }
+ let(:url) { project_issue_url(issue.project, issue, anchor: "note_#{note.id}") }
+
+ it { is_expected.to eq note.note_html }
+ end
+
+ context 'when reported from a merge request comment' do
+ let(:note) { create(:note, noteable: merge_request, project: merge_request.project, note: 'comment in mr') }
+ let(:url) { project_merge_request_url(merge_request.project, merge_request, anchor: "note_#{note.id}") }
+
+ it { is_expected.to eq note.note_html }
+ end
+
+ context 'when report type cannot be determined, because the comment does not exist' do
+ let(:url) do
+ project_merge_request_url(merge_request.project, merge_request, anchor: "note_#{non_existing_record_id}")
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#other_reports_for_user' do
+ let(:report) { create(:abuse_report) }
+ let(:another_user_report) { create(:abuse_report, user: report.user) }
+ let(:another_report) { create(:abuse_report) }
+
+ it 'returns other reports for the same user' do
+ expect(report.other_reports_for_user).to match_array(another_user_report)
+ end
+ end
+
describe 'enums' do
let(:categories) do
{
diff --git a/spec/models/achievements/user_achievement_spec.rb b/spec/models/achievements/user_achievement_spec.rb
index 9d88bfdd477..41eaadafa67 100644
--- a/spec/models/achievements/user_achievement_spec.rb
+++ b/spec/models/achievements/user_achievement_spec.rb
@@ -7,7 +7,34 @@ RSpec.describe Achievements::UserAchievement, type: :model, feature_category: :u
it { is_expected.to belong_to(:achievement).inverse_of(:user_achievements).required }
it { is_expected.to belong_to(:user).inverse_of(:user_achievements).required }
- it { is_expected.to belong_to(:awarded_by_user).class_name('User').inverse_of(:awarded_user_achievements).optional }
+ it { is_expected.to belong_to(:awarded_by_user).class_name('User').inverse_of(:awarded_user_achievements).required }
it { is_expected.to belong_to(:revoked_by_user).class_name('User').inverse_of(:revoked_user_achievements).optional }
+
+ describe '#revoked?' do
+ subject { achievement.revoked? }
+
+ context 'when revoked' do
+ let_it_be(:achievement) { create(:user_achievement, :revoked) }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when not revoked' do
+ let_it_be(:achievement) { create(:user_achievement) }
+
+ it { is_expected.to be false }
+ end
+ end
+ end
+
+ describe 'scopes' do
+ let_it_be(:user_achievement) { create(:user_achievement) }
+ let_it_be(:revoked_user_achievement) { create(:user_achievement, :revoked) }
+
+ describe '.not_revoked' do
+ it 'only returns user achievements which have not been revoked' do
+ expect(described_class.not_revoked).to contain_exactly(user_achievement)
+ end
+ end
end
end
diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb
index 3665f13015e..8717b2a1075 100644
--- a/spec/models/active_session_spec.rb
+++ b/spec/models/active_session_spec.rb
@@ -190,8 +190,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
Gitlab::Redis::Sessions.with do |redis|
expect(redis.scan_each.to_a).to include(
- described_class.key_name(user.id, session_id), # current session
- described_class.key_name_v1(user.id, session_id), # support for mixed deployment
+ described_class.key_name(user.id, session_id), # current session
lookup_key
)
end
@@ -217,19 +216,6 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
end
end
- it 'is possible to log in only using the old session key' do
- session_id = "2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae"
- ActiveSession.set(user, request)
-
- Gitlab::Redis::SharedState.with do |redis|
- redis.del(described_class.key_name(user.id, session_id))
- end
-
- sessions = ActiveSession.list(user)
-
- expect(sessions).to be_present
- end
-
it 'keeps the created_at from the login on consecutive requests' do
created_at = Time.zone.parse('2018-03-12 09:06')
updated_at = created_at + 1.minute
@@ -593,7 +579,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
let(:active_count) { 3 }
before do
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Sessions.with do |redis|
active_count.times do |number|
redis.set(
key_name(user.id, number),
@@ -608,13 +594,13 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
end
it 'removes obsolete lookup entries' do
- active = Gitlab::Redis::SharedState.with do |redis|
+ active = Gitlab::Redis::Sessions.with do |redis|
ActiveSession.cleaned_up_lookup_entries(redis, user)
end
expect(active.count).to eq(active_count)
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Sessions.with do |redis|
lookup_entries = redis.smembers(lookup_key)
expect(lookup_entries.count).to eq(active_count)
@@ -627,7 +613,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
it 'reports the removed entries' do
removed = []
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Sessions.with do |redis|
ActiveSession.cleaned_up_lookup_entries(redis, user, removed)
end
@@ -663,4 +649,26 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
it_behaves_like 'cleaning up lookup entries'
end
end
+
+ describe '.set_active_user_cookie' do
+ let(:auth) { double(cookies: {}) }
+
+ it 'sets marketing cookie' do
+ ActiveSession.set_active_user_cookie(auth)
+ expect(auth.cookies[:about_gitlab_active_user][:value]).to be_truthy
+ end
+ end
+
+ describe '.unset_active_user_cookie' do
+ let(:auth) { double(cookies: {}) }
+
+ before do
+ ActiveSession.set_active_user_cookie(auth)
+ end
+
+ it 'unsets marketing cookie' do
+ ActiveSession.unset_active_user_cookie(auth)
+ expect(auth.cookies[:about_gitlab_active_user]).to be_nil
+ end
+ end
end
diff --git a/spec/models/airflow/dags_spec.rb b/spec/models/airflow/dags_spec.rb
deleted file mode 100644
index ff3c4522779..00000000000
--- a/spec/models/airflow/dags_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Airflow::Dags, feature_category: :dataops do
- describe 'associations' do
- it { is_expected.to belong_to(:project) }
- end
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:project) }
- it { is_expected.to validate_presence_of(:dag_name) }
- it { is_expected.to validate_length_of(:dag_name).is_at_most(255) }
- it { is_expected.to validate_length_of(:schedule).is_at_most(255) }
- it { is_expected.to validate_length_of(:fileloc).is_at_most(255) }
- end
-end
diff --git a/spec/models/alert_management/alert_assignee_spec.rb b/spec/models/alert_management/alert_assignee_spec.rb
index c50a3ec0d01..647195380b3 100644
--- a/spec/models/alert_management/alert_assignee_spec.rb
+++ b/spec/models/alert_management/alert_assignee_spec.rb
@@ -5,7 +5,11 @@ require 'spec_helper'
RSpec.describe AlertManagement::AlertAssignee do
describe 'associations' do
it { is_expected.to belong_to(:alert) }
- it { is_expected.to belong_to(:assignee) }
+
+ it do
+ is_expected.to belong_to(:assignee).class_name('User')
+ .with_foreign_key(:user_id).inverse_of(:alert_assignees)
+ end
end
describe 'validations' do
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index 685ed81ec84..ff77ca2ab64 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -16,9 +16,13 @@ RSpec.describe AlertManagement::Alert do
it { is_expected.to belong_to(:prometheus_alert).optional }
it { is_expected.to belong_to(:environment).optional }
it { is_expected.to have_many(:assignees).through(:alert_assignees) }
- it { is_expected.to have_many(:notes) }
- it { is_expected.to have_many(:ordered_notes) }
- it { is_expected.to have_many(:user_mentions) }
+ it { is_expected.to have_many(:notes).inverse_of(:noteable) }
+ it { is_expected.to have_many(:ordered_notes).class_name('Note').inverse_of(:noteable) }
+
+ it do
+ is_expected.to have_many(:user_mentions).class_name('AlertManagement::AlertUserMention')
+ .with_foreign_key(:alert_management_alert_id).inverse_of(:alert)
+ end
end
describe 'validations' do
diff --git a/spec/models/alert_management/alert_user_mention_spec.rb b/spec/models/alert_management/alert_user_mention_spec.rb
index 27c3d290dde..083bf667bea 100644
--- a/spec/models/alert_management/alert_user_mention_spec.rb
+++ b/spec/models/alert_management/alert_user_mention_spec.rb
@@ -4,7 +4,11 @@ require 'spec_helper'
RSpec.describe AlertManagement::AlertUserMention do
describe 'associations' do
- it { is_expected.to belong_to(:alert_management_alert) }
+ it do
+ is_expected.to belong_to(:alert).class_name('::AlertManagement::Alert')
+ .with_foreign_key(:alert_management_alert_id).inverse_of(:user_mentions)
+ end
+
it { is_expected.to belong_to(:note) }
end
diff --git a/spec/models/analytics/cycle_analytics/stage_spec.rb b/spec/models/analytics/cycle_analytics/stage_spec.rb
index 57748f8942e..960d8d3e964 100644
--- a/spec/models/analytics/cycle_analytics/stage_spec.rb
+++ b/spec/models/analytics/cycle_analytics/stage_spec.rb
@@ -32,24 +32,24 @@ RSpec.describe Analytics::CycleAnalytics::Stage, feature_category: :value_stream
before do
# event identifiers are the same
create(:cycle_analytics_stage, name: 'Stage A1', namespace: group,
- start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+ start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
create(:cycle_analytics_stage, name: 'Stage A2', namespace: sub_group,
- start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+ start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
create(:cycle_analytics_stage, name: 'Stage A3', namespace: sub_group,
- start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+ start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
create(:cycle_analytics_stage, name: 'Stage A4', project: project,
- start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+ start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
create(:cycle_analytics_stage,
- name: 'Stage B1',
- namespace: group,
- start_event_identifier: :merge_request_last_build_started,
- end_event_identifier: :merge_request_last_build_finished)
+ name: 'Stage B1',
+ namespace: group,
+ start_event_identifier: :merge_request_last_build_started,
+ end_event_identifier: :merge_request_last_build_finished)
create(:cycle_analytics_stage, name: 'Stage C1', project: project,
- start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production)
+ start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production)
create(:cycle_analytics_stage, name: 'Stage C2', project: project,
- start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production)
+ start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production)
end
it 'returns distinct stages by the event identifiers' do
@@ -69,14 +69,11 @@ RSpec.describe Analytics::CycleAnalytics::Stage, feature_category: :value_stream
end
end
- describe 'events tracking' do
- let(:category) { described_class.to_s }
- let(:label) { described_class.table_name }
+ it_behaves_like 'database events tracking' do
let(:namespace) { create(:group) }
- let(:action) { "database_event_#{property}" }
let(:value_stream) { create(:cycle_analytics_value_stream) }
- let(:feature_flag_name) { :product_intelligence_database_event_tracking }
- let(:stage) { described_class.create!(stage_params) }
+ let(:record) { described_class.create!(stage_params) }
+ let(:update_params) { { name: 'st 2' } }
let(:stage_params) do
{
namespace: namespace,
@@ -86,50 +83,5 @@ RSpec.describe Analytics::CycleAnalytics::Stage, feature_category: :value_stream
group_value_stream_id: value_stream.id
}
end
-
- let(:record_tracked_attributes) do
- {
- "id" => stage.id,
- "created_at" => stage.created_at,
- "updated_at" => stage.updated_at,
- "relative_position" => stage.relative_position,
- "start_event_identifier" => stage.start_event_identifier,
- "end_event_identifier" => stage.end_event_identifier,
- "group_id" => stage.group_id,
- "start_event_label_id" => stage.start_event_label_id,
- "end_event_label_id" => stage.end_event_label_id,
- "hidden" => stage.hidden,
- "custom" => stage.custom,
- "name" => stage.name,
- "group_value_stream_id" => stage.group_value_stream_id
- }
- end
-
- describe '#create' do
- it_behaves_like 'Snowplow event tracking' do
- let(:property) { 'create' }
- let(:extra) { record_tracked_attributes }
-
- subject(:new_group_stage) { stage }
- end
- end
-
- describe '#update', :freeze_time do
- it_behaves_like 'Snowplow event tracking' do
- subject(:create_group_stage) { stage.update!(name: 'st 2') }
-
- let(:extra) { record_tracked_attributes.merge('name' => 'st 2') }
- let(:property) { 'update' }
- end
- end
-
- describe '#destroy' do
- it_behaves_like 'Snowplow event tracking' do
- subject(:delete_stage_group) { stage.destroy! }
-
- let(:extra) { record_tracked_attributes }
- let(:property) { 'destroy' }
- end
- end
end
end
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index c1cd44e9007..ee3065cf8f2 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -106,12 +106,12 @@ RSpec.describe ApplicationRecord do
describe '.where_not_exists' do
it 'produces a WHERE NOT EXISTS query' do
- create(:user, :two_factor_via_u2f)
+ create(:user, :two_factor_via_webauthn)
user_2 = create(:user)
expect(
User.where_not_exists(
- U2fRegistration.where(U2fRegistration.arel_table[:user_id].eq(User.arel_table[:id])))
+ WebauthnRegistration.where(WebauthnRegistration.arel_table[:user_id].eq(User.arel_table[:id])))
).to match_array([user_2])
end
end
@@ -258,7 +258,7 @@ RSpec.describe ApplicationRecord do
before do
ApplicationRecord.connection.execute(<<~SQL)
- create table tests (
+ create table _test_tests (
id bigserial primary key not null,
ignore_me text
)
@@ -267,7 +267,7 @@ RSpec.describe ApplicationRecord do
context 'without an ignored column' do
let(:test_model) do
Class.new(ApplicationRecord) do
- self.table_name = 'tests'
+ self.table_name = :_test_tests
end
end
@@ -278,7 +278,7 @@ RSpec.describe ApplicationRecord do
let(:test_model) do
Class.new(ApplicationRecord) do
include IgnorableColumns
- self.table_name = 'tests'
+ self.table_name = :_test_tests
ignore_columns :ignore_me, remove_after: '2100-01-01', remove_with: '99.12'
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 5b99c68ec80..98bfb3366d2 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
+RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
using RSpec::Parameterized::TableSyntax
subject(:setting) { described_class.create_from_defaults }
@@ -31,6 +31,20 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
let(:ftp) { 'ftp://example.com' }
let(:javascript) { 'javascript:alert(window.opener.document.location)' }
+ let_it_be(:valid_database_apdex_settings) do
+ {
+ prometheus_api_url: 'Prometheus URL',
+ apdex_sli_query: {
+ main: 'Apdex SLI query main',
+ ci: 'Apdex SLI query ci'
+ },
+ apdex_slo: {
+ main: 0.99,
+ ci: 0.98
+ }
+ }
+ end
+
it { is_expected.to allow_value(nil).for(:home_page_url) }
it { is_expected.to allow_value(http).for(:home_page_url) }
it { is_expected.to allow_value(https).for(:home_page_url) }
@@ -132,6 +146,9 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
it { is_expected.to allow_value(false).for(:user_defaults_to_private_profile) }
it { is_expected.not_to allow_value(nil).for(:user_defaults_to_private_profile) }
+ it { is_expected.to allow_values([true, false]).for(:deny_all_requests_except_allowed) }
+ it { is_expected.not_to allow_value(nil).for(:deny_all_requests_except_allowed) }
+
it 'ensures max_pages_size is an integer greater than 0 (or equal to 0 to indicate unlimited/maximum)' do
is_expected.to validate_numericality_of(:max_pages_size).only_integer.is_greater_than_or_equal_to(0)
.is_less_than(::Gitlab::Pages::MAX_SIZE / 1.megabyte)
@@ -182,7 +199,8 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
it { is_expected.not_to allow_value('default' => 101).for(:repository_storages_weighted).with_message("value for 'default' must be between 0 and 100") }
it { is_expected.not_to allow_value('default' => 100, shouldntexist: 50).for(:repository_storages_weighted).with_message("can't include: shouldntexist") }
- %i[notes_create_limit search_rate_limit search_rate_limit_unauthenticated users_get_by_id_limit].each do |setting|
+ %i[notes_create_limit search_rate_limit search_rate_limit_unauthenticated users_get_by_id_limit
+ projects_api_rate_limit_unauthenticated].each do |setting|
it { is_expected.to allow_value(400).for(setting) }
it { is_expected.not_to allow_value('two').for(setting) }
it { is_expected.not_to allow_value(nil).for(setting) }
@@ -209,6 +227,12 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
it { is_expected.to allow_value('disabled').for(:whats_new_variant) }
it { is_expected.not_to allow_value(nil).for(:whats_new_variant) }
+ it { is_expected.to allow_value('http://example.com/').for(:public_runner_releases_url) }
+ it { is_expected.not_to allow_value(nil).for(:public_runner_releases_url) }
+
+ it { is_expected.to allow_value([true, false]).for(:update_runner_versions_enabled) }
+ it { is_expected.not_to allow_value(nil).for(:update_runner_versions_enabled) }
+
it { is_expected.not_to allow_value(['']).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value(['OBVIOUSLY_WRONG']).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value(%w(project project)).for(:valid_runner_registrars) }
@@ -228,6 +252,27 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
it { is_expected.to allow_value(false).for(:allow_runner_registration_token) }
it { is_expected.not_to allow_value(nil).for(:allow_runner_registration_token) }
+ it { is_expected.to allow_value(true).for(:gitlab_dedicated_instance) }
+ it { is_expected.to allow_value(false).for(:gitlab_dedicated_instance) }
+ it { is_expected.not_to allow_value(nil).for(:gitlab_dedicated_instance) }
+
+ it { is_expected.not_to allow_value(random: :value).for(:database_apdex_settings) }
+ it { is_expected.to allow_value(nil).for(:database_apdex_settings) }
+ it { is_expected.to allow_value(valid_database_apdex_settings).for(:database_apdex_settings) }
+
+ it { is_expected.to allow_value([true, false]).for(:silent_mode_enabled) }
+ it { is_expected.not_to allow_value(nil).for(:silent_mode_enabled) }
+
+ it { is_expected.to allow_value(0).for(:ci_max_includes) }
+ it { is_expected.to allow_value(200).for(:ci_max_includes) }
+ it { is_expected.not_to allow_value('abc').for(:ci_max_includes) }
+ it { is_expected.not_to allow_value(nil).for(:ci_max_includes) }
+ it { is_expected.not_to allow_value(10.5).for(:ci_max_includes) }
+ it { is_expected.not_to allow_value(-1).for(:ci_max_includes) }
+
+ it { is_expected.to allow_value([true, false]).for(:remember_me_enabled) }
+ it { is_expected.not_to allow_value(nil).for(:remember_me_enabled) }
+
context 'when deactivate_dormant_users is enabled' do
before do
stub_application_setting(deactivate_dormant_users: true)
@@ -268,6 +313,17 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
end
end
+ context 'import_sources validation' do
+ before do
+ subject.import_sources = %w[github bitbucket gitlab git gitlab_project gitea manifest phabricator]
+ end
+
+ it 'removes phabricator as an import source' do
+ subject.validate
+ expect(subject.import_sources).to eq(%w[github bitbucket git gitlab_project gitea manifest])
+ end
+ end
+
context 'grafana_url validations' do
before do
subject.instance_variable_set(:@parsed_grafana_url, nil)
@@ -318,7 +374,7 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
end
end
- describe 'default_branch_name validaitions' do
+ describe 'default_branch_name validations' do
context "when javascript tags get sanitized properly" do
it "gets sanitized properly" do
setting.update!(default_branch_name: "hello<script>alert(1)</script>")
@@ -506,6 +562,13 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
.is_less_than(65536)
end
+ specify do
+ is_expected.to validate_numericality_of(:archive_builds_in_seconds)
+ .only_integer
+ .is_greater_than_or_equal_to(1.day.seconds.to_i)
+ .with_message('must be at least 1 day')
+ end
+
describe 'usage_ping_enabled setting' do
shared_examples 'usage ping enabled' do
it do
@@ -585,6 +648,23 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
end
end
+ describe 'setting validated as `addressable_url` configured with external URI' do
+ before do
+ # Use any property that has the `addressable_url` validation.
+ setting.help_page_documentation_base_url = 'http://example.com'
+ end
+
+ it 'is valid by default' do
+ expect(setting).to be_valid
+ end
+
+ it 'is invalid when unpersisted `deny_all_requests_except_allowed` property is true' do
+ setting.deny_all_requests_except_allowed = true
+
+ expect(setting).not_to be_valid
+ end
+ end
+
context 'key restrictions' do
it 'does not allow all key types to be disabled' do
Gitlab::SSHPublicKey.supported_types.each do |type|
@@ -1124,6 +1204,11 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
it { is_expected.to allow_value(*Gitlab::I18n.available_locales).for(:default_preferred_language) }
it { is_expected.not_to allow_value(nil, '', 'invalid_locale').for(:default_preferred_language) }
end
+
+ context 'for default_syntax_highlighting_theme' do
+ it { is_expected.to allow_value(*Gitlab::ColorSchemes.valid_ids).for(:default_syntax_highlighting_theme) }
+ it { is_expected.not_to allow_value(nil, 0, Gitlab::ColorSchemes.available_schemes.size + 1).for(:default_syntax_highlighting_theme) }
+ end
end
context 'restrict creating duplicates' do
@@ -1144,6 +1229,17 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
end
end
+ describe 'ADDRESSABLE_URL_VALIDATION_OPTIONS' do
+ it 'is applied to all addressable_url validated properties' do
+ url_validators = described_class.validators.select { |validator| validator.is_a?(AddressableUrlValidator) }
+
+ url_validators.each do |validator|
+ expect(validator.options).to match(hash_including(described_class::ADDRESSABLE_URL_VALIDATION_OPTIONS)),
+ "#{validator.attributes} should use ADDRESSABLE_URL_VALIDATION_OPTIONS"
+ end
+ end
+ end
+
describe '#disabled_oauth_sign_in_sources=' do
before do
allow(Devise).to receive(:omniauth_providers).and_return([:github])
@@ -1451,7 +1547,7 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
it { is_expected.to validate_numericality_of(:inactive_projects_min_size_mb).is_greater_than_or_equal_to(0) }
it "deletes the redis key used for tracking inactive projects deletion warning emails when setting is updated",
- :clean_gitlab_redis_shared_state do
+ :clean_gitlab_redis_shared_state do
Gitlab::Redis::SharedState.with do |redis|
redis.hset("inactive_projects_deletion_warning_email_notified", "project:1", "2020-01-01")
end
diff --git a/spec/models/audit_event_spec.rb b/spec/models/audit_event_spec.rb
index 9f2724cebee..9e667836b45 100644
--- a/spec/models/audit_event_spec.rb
+++ b/spec/models/audit_event_spec.rb
@@ -3,6 +3,10 @@
require 'spec_helper'
RSpec.describe AuditEvent do
+ describe 'associations' do
+ it { is_expected.to belong_to(:user).with_foreign_key(:author_id).inverse_of(:audit_events) }
+ end
+
describe 'validations' do
include_examples 'validates IP address' do
let(:attribute) { :ip_address }
diff --git a/spec/models/authentication_event_spec.rb b/spec/models/authentication_event_spec.rb
index 23e253c2a28..17fe10b5b4e 100644
--- a/spec/models/authentication_event_spec.rb
+++ b/spec/models/authentication_event_spec.rb
@@ -71,4 +71,19 @@ RSpec.describe AuthenticationEvent do
it { is_expected.to eq(false) }
end
end
+
+ describe '.most_used_ip_address_for_user' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:most_used_ip_address) { '::1' }
+ let_it_be(:another_ip_address) { '127.0.0.1' }
+
+ subject { described_class.most_used_ip_address_for_user(user) }
+
+ before do
+ create_list(:authentication_event, 2, user: user, ip_address: most_used_ip_address)
+ create(:authentication_event, user: user, ip_address: another_ip_address)
+ end
+
+ it { is_expected.to eq(most_used_ip_address) }
+ end
end
diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb
index 2593c9b3595..99006f8ddce 100644
--- a/spec/models/award_emoji_spec.rb
+++ b/spec/models/award_emoji_spec.rb
@@ -63,22 +63,43 @@ RSpec.describe AwardEmoji do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:emoji) { create(:custom_emoji, name: 'partyparrot', namespace: group) }
+ let_it_be(:project) { create(:project, namespace: group) }
- before do
+ before_all do
group.add_maintainer(user)
end
- %i[issue merge_request note_on_issue snippet].each do |awardable_type|
- let_it_be(:project) { create(:project, namespace: group) }
- let(:awardable) { create(awardable_type, project: project) }
-
- it "is accepted on #{awardable_type}" do
+ shared_examples 'awardable' do
+ it 'is accepted' do
new_award = build(:award_emoji, user: user, awardable: awardable, name: emoji.name)
-
expect(new_award).to be_valid
end
end
+ context 'with issue' do
+ let(:awardable) { create(:issue, project: project) }
+
+ include_examples 'awardable'
+ end
+
+ context 'with merge_request' do
+ let(:awardable) { create(:merge_request, source_project: project) }
+
+ include_examples 'awardable'
+ end
+
+ context 'with note_on_issue' do
+ let(:awardable) { create(:note_on_issue, project: project) }
+
+ include_examples 'awardable'
+ end
+
+ context 'with snippet' do
+ let(:awardable) { create(:snippet, project: project) }
+
+ include_examples 'awardable'
+ end
+
it 'is accepted on subgroup issue' do
subgroup = create(:group, parent: group)
project = create(:project, namespace: subgroup)
diff --git a/spec/models/awareness_session_spec.rb b/spec/models/awareness_session_spec.rb
deleted file mode 100644
index 854ce5957f7..00000000000
--- a/spec/models/awareness_session_spec.rb
+++ /dev/null
@@ -1,163 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe AwarenessSession, :clean_gitlab_redis_shared_state do
- subject { AwarenessSession.for(session_id) }
-
- let!(:user) { create(:user) }
- let(:session_id) { 1 }
-
- describe "when initiating a session" do
- it "provides a string representation of the model instance" do
- expected = "awareness_session=6b86b273ff34fce"
-
- expect(subject.to_s).to eql(expected)
- end
-
- it "provides a parameterized version of the session identifier" do
- expected = "6b86b273ff34fce"
-
- expect(subject.to_param).to eql(expected)
- end
- end
-
- describe "when a user joins a session" do
- let(:user2) { create(:user) }
-
- let(:presence_ttl) { 15.minutes }
-
- it "changes number of session members" do
- expect { subject.join(user) }.to change(subject, :size).by(1)
- end
-
- it "returns user as member of session with last_activity timestamp" do
- freeze_time do
- subject.join(user)
-
- session_users = subject.users_with_last_activity
- session_user, last_activity = session_users.first
-
- expect(session_user.id).to be(user.id)
- expect(last_activity).to be_eql(Time.now.utc)
- end
- end
-
- it "maintains user ID and last_activity pairs" do
- now = Time.zone.now
-
- travel_to now - 1.minute do
- subject.join(user2)
- end
-
- travel_to now do
- subject.join(user)
- end
-
- session_users = subject.users_with_last_activity
-
- expect(session_users[0].first.id).to eql(user.id)
- expect(session_users[0].last.to_i).to eql(now.to_i)
-
- expect(session_users[1].first.id).to eql(user2.id)
- expect(session_users[1].last.to_i).to eql((now - 1.minute).to_i)
- end
-
- it "reports user as present" do
- freeze_time do
- subject.join(user)
-
- expect(subject.present?(user, threshold: presence_ttl)).to be true
- end
- end
-
- it "reports user as away after a certain time on inactivity" do
- subject.join(user)
-
- travel_to((presence_ttl + 1.minute).from_now) do
- expect(subject.away?(user, threshold: presence_ttl)).to be true
- end
- end
-
- it "reports user as present still when there was some activity" do
- subject.join(user)
-
- travel_to((presence_ttl - 1.minute).from_now) do
- subject.touch!(user)
- end
-
- travel_to((presence_ttl + 1.minute).from_now) do
- expect(subject.present?(user, threshold: presence_ttl)).to be true
- end
- end
-
- it "creates user and session awareness keys in store" do
- subject.join(user)
-
- Gitlab::Redis::SharedState.with do |redis|
- keys = redis.scan_each(match: "gitlab:awareness:*").to_a
-
- expect(keys.size).to be(2)
- end
- end
-
- it "sets a timeout for user and session key" do
- subject.join(user)
- subject_id = Digest::SHA256.hexdigest(session_id.to_s)[0, 15]
-
- Gitlab::Redis::SharedState.with do |redis|
- ttl_session = redis.ttl("gitlab:awareness:session:#{subject_id}:users")
- ttl_user = redis.ttl("gitlab:awareness:user:#{user.id}:sessions")
-
- expect(ttl_session).to be > 0
- expect(ttl_user).to be > 0
- end
- end
-
- it "fetches user(s) from database" do
- subject.join(user)
-
- expect(subject.users.first).to eql(user)
- end
-
- it "fetches and filters online user(s) from database" do
- subject.join(user)
-
- travel 2.hours do
- subject.join(user2)
-
- online_users = subject.online_users_with_last_activity
- online_user, _ = online_users.first
-
- expect(online_users.size).to be 1
- expect(online_user).to eql(user2)
- end
- end
- end
-
- describe "when a user leaves a session" do
- it "changes number of session members" do
- subject.join(user)
-
- expect { subject.leave(user) }.to change(subject, :size).by(-1)
- end
-
- it "destroys the session when it was the last user" do
- subject.join(user)
-
- expect { subject.leave(user) }.to change(subject, :id).to(nil)
- end
- end
-
- describe "when last user leaves a session" do
- it "session and user keys are removed" do
- subject.join(user)
-
- Gitlab::Redis::SharedState.with do |redis|
- expect { subject.leave(user) }
- .to change { redis.scan_each(match: "gitlab:awareness:*").to_a.size }
- .to(0)
- end
- end
- end
-end
diff --git a/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
index d28fa0bbe97..c9ac13eefc0 100644
--- a/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
+++ b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BlobViewer::MetricsDashboardYml do
+RSpec.describe BlobViewer::MetricsDashboardYml, feature_category: :metrics do
include FakeBlobHelpers
include RepoHelpers
@@ -119,4 +119,18 @@ RSpec.describe BlobViewer::MetricsDashboardYml do
expect(viewer.errors).to eq ["YAML syntax: The parsed YAML is too big"]
end
end
+
+ describe '.can_render?' do
+ subject { described_class.can_render?(blob) }
+
+ it { is_expected.to be false }
+
+ context 'when metrics dashboard feature is available' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
+ it { is_expected.to be true }
+ end
+ end
end
diff --git a/spec/models/blob_viewer/package_json_spec.rb b/spec/models/blob_viewer/package_json_spec.rb
index 1dcba3bcb4f..781623c0d3d 100644
--- a/spec/models/blob_viewer/package_json_spec.rb
+++ b/spec/models/blob_viewer/package_json_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BlobViewer::PackageJson do
+RSpec.describe BlobViewer::PackageJson, feature_category: :source_code_management do
include FakeBlobHelpers
let(:project) { build_stubbed(:project) }
@@ -59,6 +59,17 @@ RSpec.describe BlobViewer::PackageJson do
expect(subject.manager_url).to eq("https://yarnpkg.com/")
end
end
+
+ context 'when json is an array' do
+ let(:data) { '[]' }
+
+ it 'does not raise an error', :aggregate_failures do
+ expect(subject).to receive(:prepare!)
+
+ expect { subject.yarn? }.not_to raise_error
+ expect(subject.yarn?).to be_falsey
+ end
+ end
end
context 'npm' do
diff --git a/spec/models/board_spec.rb b/spec/models/board_spec.rb
index 6017298e85b..f469dee5ba1 100644
--- a/spec/models/board_spec.rb
+++ b/spec/models/board_spec.rb
@@ -8,7 +8,13 @@ RSpec.describe Board do
describe 'relationships' do
it { is_expected.to belong_to(:project) }
- it { is_expected.to have_many(:lists).order(list_type: :asc, position: :asc).dependent(:delete_all) }
+
+ it do
+ is_expected.to have_many(:lists).order(list_type: :asc, position: :asc).dependent(:delete_all)
+ .inverse_of(:board)
+ end
+
+ it { is_expected.to have_many(:destroyable_lists).order(list_type: :asc, position: :asc).inverse_of(:board) }
end
describe 'validations' do
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 8fdc9852f6e..5fcf6813b0a 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -24,7 +24,11 @@ RSpec.describe BroadcastMessage do
it { is_expected.to allow_value(1).for(:broadcast_type) }
it { is_expected.not_to allow_value(nil).for(:broadcast_type) }
it { is_expected.not_to allow_value(nil).for(:target_access_levels) }
- it { is_expected.to validate_inclusion_of(:target_access_levels).in_array(described_class::ALLOWED_TARGET_ACCESS_LEVELS) }
+
+ it do
+ is_expected.to validate_inclusion_of(:target_access_levels)
+ .in_array(described_class::ALLOWED_TARGET_ACCESS_LEVELS)
+ end
end
describe 'default values' do
@@ -187,32 +191,6 @@ RSpec.describe BroadcastMessage do
shared_examples "matches with user access level" do |broadcast_type|
let_it_be(:target_access_levels) { [Gitlab::Access::GUEST] }
- let(:feature_flag_state) { true }
-
- before do
- stub_feature_flags(role_targeted_broadcast_messages: feature_flag_state)
- end
-
- context 'when feature flag is disabled' do
- let(:feature_flag_state) { false }
-
- context 'when message is role-targeted' do
- let_it_be(:message) { create(:broadcast_message, target_access_levels: target_access_levels, broadcast_type: broadcast_type) }
-
- it 'does not return the message' do
- expect(subject.call(nil, Gitlab::Access::GUEST)).to be_empty
- end
- end
-
- context 'when message is not role-targeted' do
- let_it_be(:message) { create(:broadcast_message, target_access_levels: [], broadcast_type: broadcast_type) }
-
- it 'returns the message' do
- expect(subject.call(nil, Gitlab::Access::GUEST)).to include(message)
- end
- end
- end
-
context 'when target_access_levels is empty' do
let_it_be(:message) { create(:broadcast_message, target_access_levels: [], broadcast_type: broadcast_type) }
@@ -226,7 +204,9 @@ RSpec.describe BroadcastMessage do
end
context 'when target_access_levels is not empty' do
- let_it_be(:message) { create(:broadcast_message, target_access_levels: target_access_levels, broadcast_type: broadcast_type) }
+ let_it_be(:message) do
+ create(:broadcast_message, target_access_levels: target_access_levels, broadcast_type: broadcast_type)
+ end
it "does not return the message if user access level is nil" do
expect(subject.call).to be_empty
@@ -250,26 +230,18 @@ RSpec.describe BroadcastMessage do
before do
cache.write(described_class::BANNER_CACHE_KEY, [message])
- allow(BroadcastMessage).to receive(:cache) { cache }
+ allow(described_class).to receive(:cache) { cache }
end
it 'does not raise error (e.g. NoMethodError from nil.empty?)' do
expect { subject.call }.not_to raise_error
end
-
- context 'when feature flag is disabled' do
- it 'does not raise error (e.g. NoMethodError from nil.empty?)' do
- stub_feature_flags(role_targeted_broadcast_messages: false)
-
- expect { subject.call }.not_to raise_error
- end
- end
end
end
describe '.current', :use_clean_rails_memory_store_caching do
subject do
- -> (path = nil, user_access_level = nil) do
+ ->(path = nil, user_access_level = nil) do
described_class.current(current_path: path, user_access_level: user_access_level)
end
end
@@ -301,7 +273,7 @@ RSpec.describe BroadcastMessage do
describe '.current_banner_messages', :use_clean_rails_memory_store_caching do
subject do
- -> (path = nil, user_access_level = nil) do
+ ->(path = nil, user_access_level = nil) do
described_class.current_banner_messages(current_path: path, user_access_level: user_access_level)
end
end
@@ -331,7 +303,7 @@ RSpec.describe BroadcastMessage do
describe '.current_notification_messages', :use_clean_rails_memory_store_caching do
subject do
- -> (path = nil, user_access_level = nil) do
+ ->(path = nil, user_access_level = nil) do
described_class.current_notification_messages(current_path: path, user_access_level: user_access_level)
end
end
diff --git a/spec/models/bulk_import_spec.rb b/spec/models/bulk_import_spec.rb
index 3430da43f62..acb1f4a2ef7 100644
--- a/spec/models/bulk_import_spec.rb
+++ b/spec/models/bulk_import_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImport, type: :model do
+RSpec.describe BulkImport, type: :model, feature_category: :importers do
let_it_be(:created_bulk_import) { create(:bulk_import, :created) }
let_it_be(:started_bulk_import) { create(:bulk_import, :started) }
let_it_be(:finished_bulk_import) { create(:bulk_import, :finished) }
@@ -48,4 +48,31 @@ RSpec.describe BulkImport, type: :model do
expect(bulk_import.source_version_info.to_s).to eq(bulk_import.source_version)
end
end
+
+ describe '#update_has_failures' do
+ let(:import) { create(:bulk_import, :started) }
+ let(:entity) { create(:bulk_import_entity, bulk_import: import) }
+
+ context 'when entity has failures' do
+ it 'sets has_failures flag to true' do
+ expect(import.has_failures).to eq(false)
+
+ entity.update!(has_failures: true)
+ import.fail_op!
+
+ expect(import.has_failures).to eq(true)
+ end
+ end
+
+ context 'when entity does not have failures' do
+ it 'sets has_failures flag to false' do
+ expect(import.has_failures).to eq(false)
+
+ entity.update!(has_failures: false)
+ import.fail_op!
+
+ expect(import.has_failures).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/models/bulk_imports/batch_tracker_spec.rb b/spec/models/bulk_imports/batch_tracker_spec.rb
new file mode 100644
index 00000000000..336943228c7
--- /dev/null
+++ b/spec/models/bulk_imports/batch_tracker_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::BatchTracker, type: :model, feature_category: :importers do
+ describe 'associations' do
+ it { is_expected.to belong_to(:tracker) }
+ end
+
+ describe 'validations' do
+ subject { build(:bulk_import_batch_tracker) }
+
+ it { is_expected.to validate_presence_of(:batch_number) }
+ it { is_expected.to validate_uniqueness_of(:batch_number).scoped_to(:tracker_id) }
+ end
+end
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index 56796aa1fe4..c7ace3d2b78 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -3,11 +3,18 @@
require 'spec_helper'
RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers do
+ subject { described_class.new(group: Group.new) }
+
describe 'associations' do
it { is_expected.to belong_to(:bulk_import).required }
it { is_expected.to belong_to(:parent) }
- it { is_expected.to belong_to(:group) }
+ it { is_expected.to belong_to(:group).optional.with_foreign_key(:namespace_id).inverse_of(:bulk_import_entities) }
it { is_expected.to belong_to(:project) }
+
+ it do
+ is_expected.to have_many(:trackers).class_name('BulkImports::Tracker')
+ .with_foreign_key(:bulk_import_entity_id).inverse_of(:entity)
+ end
end
describe 'validations' do
@@ -18,35 +25,8 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
it { is_expected.to define_enum_for(:source_type).with_values(%i[group_entity project_entity]) }
context 'when formatting with regexes' do
- subject { described_class.new(group: Group.new) }
-
- it { is_expected.to allow_values('namespace', 'parent/namespace', 'parent/group/subgroup', '').for(:destination_namespace) }
- it { is_expected.not_to allow_values('parent/namespace/', '/namespace', 'parent group/subgroup', '@namespace').for(:destination_namespace) }
-
it { is_expected.to allow_values('source', 'source/path', 'source/full/path').for(:source_full_path) }
it { is_expected.not_to allow_values('/source', 'http://source/path', 'sou rce/full/path', '').for(:source_full_path) }
-
- it { is_expected.to allow_values('destination', 'destination-slug', 'new-destination-slug').for(:destination_slug) }
-
- # it { is_expected.not_to allow_values('destination/slug', '/destination-slug', 'destination slug').for(:destination_slug) } <-- this test should
- # succeed but it's failing possibly due to rspec caching. To ensure this case is covered see the more cumbersome test below:
- context 'when destination_slug is invalid' do
- let(:invalid_slugs) { ['destination/slug', '/destination-slug', 'destination slug'] }
- let(:error_message) do
- 'cannot start with a non-alphanumeric character except for periods or underscores, ' \
- 'can contain only alphanumeric characters, periods, and underscores, ' \
- 'cannot end with a period or forward slash, and has no ' \
- 'leading or trailing forward slashes'
- end
-
- it 'raises an error' do
- invalid_slugs.each do |slug|
- entity = build(:bulk_import_entity, :group_entity, group: build(:group), project: nil, destination_slug: slug)
- expect(entity).not_to be_valid
- expect(entity.errors.errors[0].message).to include(error_message)
- end
- end
- end
end
context 'when associated with a group and project' do
@@ -93,8 +73,6 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
end
it 'is invalid as a project_entity' do
- stub_feature_flags(bulk_import_projects: true)
-
entity = build(:bulk_import_entity, :project_entity, group: build(:group), project: nil)
expect(entity).not_to be_valid
@@ -104,8 +82,6 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
context 'when associated with a project and no group' do
it 'is valid' do
- stub_feature_flags(bulk_import_projects: true)
-
entity = build(:bulk_import_entity, :project_entity, group: nil, project: build(:project))
expect(entity).to be_valid
@@ -135,8 +111,6 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
context 'when the parent is a project import' do
it 'is invalid' do
- stub_feature_flags(bulk_import_projects: true)
-
entity = build(:bulk_import_entity, parent: build(:bulk_import_entity, :project_entity))
expect(entity).not_to be_valid
@@ -178,38 +152,13 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
end
end
- context 'when bulk_import_projects feature flag is disabled and source_type is a project_entity' do
- it 'is invalid' do
- stub_feature_flags(bulk_import_projects: false)
-
- entity = build(:bulk_import_entity, :project_entity)
-
- expect(entity).not_to be_valid
- expect(entity.errors[:base]).to include('invalid entity source type')
- end
- end
-
- context 'when bulk_import_projects feature flag is enabled and source_type is a project_entity' do
+ context 'when source_type is a project_entity' do
it 'is valid' do
- stub_feature_flags(bulk_import_projects: true)
-
entity = build(:bulk_import_entity, :project_entity)
expect(entity).to be_valid
end
end
-
- context 'when bulk_import_projects feature flag is enabled on root ancestor level and source_type is a project_entity' do
- it 'is valid' do
- top_level_namespace = create(:group)
-
- stub_feature_flags(bulk_import_projects: top_level_namespace)
-
- entity = build(:bulk_import_entity, :project_entity, destination_namespace: top_level_namespace.full_path)
-
- expect(entity).to be_valid
- end
- end
end
describe '#encoded_source_full_path' do
@@ -412,4 +361,30 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
end
end
end
+
+ describe '#update_has_failures' do
+ let(:entity) { create(:bulk_import_entity) }
+
+ context 'when entity has failures' do
+ it 'sets has_failures flag to true' do
+ expect(entity.has_failures).to eq(false)
+
+ create(:bulk_import_failure, entity: entity)
+
+ entity.fail_op!
+
+ expect(entity.has_failures).to eq(true)
+ end
+ end
+
+ context 'when entity does not have failures' do
+ it 'sets has_failures flag to false' do
+ expect(entity.has_failures).to eq(false)
+
+ entity.fail_op!
+
+ expect(entity.has_failures).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/models/bulk_imports/export_batch_spec.rb b/spec/models/bulk_imports/export_batch_spec.rb
new file mode 100644
index 00000000000..43209921b9c
--- /dev/null
+++ b/spec/models/bulk_imports/export_batch_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::ExportBatch, type: :model, feature_category: :importers do
+ describe 'associations' do
+ it { is_expected.to belong_to(:export) }
+ it { is_expected.to have_one(:upload) }
+ end
+
+ describe 'validations' do
+ subject { build(:bulk_import_export_batch) }
+
+ it { is_expected.to validate_presence_of(:batch_number) }
+ it { is_expected.to validate_uniqueness_of(:batch_number).scoped_to(:export_id) }
+ end
+end
diff --git a/spec/models/bulk_imports/export_spec.rb b/spec/models/bulk_imports/export_spec.rb
index d85b77d599b..7173d032bc2 100644
--- a/spec/models/bulk_imports/export_spec.rb
+++ b/spec/models/bulk_imports/export_spec.rb
@@ -2,11 +2,12 @@
require 'spec_helper'
-RSpec.describe BulkImports::Export, type: :model do
+RSpec.describe BulkImports::Export, type: :model, feature_category: :importers do
describe 'associations' do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_one(:upload) }
+ it { is_expected.to have_many(:batches) }
end
describe 'validations' do
diff --git a/spec/models/bulk_imports/file_transfer/group_config_spec.rb b/spec/models/bulk_imports/file_transfer/group_config_spec.rb
index 8660114b719..e50f52c728f 100644
--- a/spec/models/bulk_imports/file_transfer/group_config_spec.rb
+++ b/spec/models/bulk_imports/file_transfer/group_config_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::FileTransfer::GroupConfig do
+RSpec.describe BulkImports::FileTransfer::GroupConfig, feature_category: :importers do
let_it_be(:exportable) { create(:group) }
let_it_be(:hex) { '123' }
@@ -49,4 +49,51 @@ RSpec.describe BulkImports::FileTransfer::GroupConfig do
expect(subject.relation_excluded_keys('group')).to include('owner_id')
end
end
+
+ describe '#batchable_relation?' do
+ context 'when relation is batchable' do
+ it 'returns true' do
+ expect(subject.batchable_relation?('labels')).to eq(true)
+ end
+ end
+
+ context 'when relation is not batchable' do
+ it 'returns false' do
+ expect(subject.batchable_relation?('namespace_settings')).to eq(false)
+ end
+ end
+
+ context 'when relation is not listed as portable' do
+ it 'returns false' do
+ expect(subject.batchable_relation?('foo')).to eq(false)
+ end
+ end
+ end
+
+ describe '#batchable_relations' do
+ it 'returns a list of collection associations for a group' do
+ expect(subject.batchable_relations).to include('labels', 'boards', 'milestones')
+ expect(subject.batchable_relations).not_to include('namespace_settings')
+ end
+ end
+
+ describe '#export_service_for' do
+ context 'when relation is a tree' do
+ it 'returns TreeExportService' do
+ expect(subject.export_service_for('labels')).to eq(BulkImports::TreeExportService)
+ end
+ end
+
+ context 'when relation is a file' do
+ it 'returns FileExportService' do
+ expect(subject.export_service_for('uploads')).to eq(BulkImports::FileExportService)
+ end
+ end
+
+ context 'when relation is unknown' do
+ it 'raises' do
+ expect { subject.export_service_for('foo') }.to raise_error(BulkImports::Error, 'Unsupported export relation')
+ end
+ end
+ end
end
diff --git a/spec/models/bulk_imports/file_transfer/project_config_spec.rb b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
index 0f02c5c546f..014f624165c 100644
--- a/spec/models/bulk_imports/file_transfer/project_config_spec.rb
+++ b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::FileTransfer::ProjectConfig do
+RSpec.describe BulkImports::FileTransfer::ProjectConfig, feature_category: :importers do
let_it_be(:exportable) { create(:project) }
let_it_be(:hex) { '123' }
@@ -42,6 +42,18 @@ RSpec.describe BulkImports::FileTransfer::ProjectConfig do
it 'returns relation tree of a top level relation' do
expect(subject.top_relation_tree('labels')).to eq('priorities' => {})
end
+
+ it 'returns relation tree with merged with deprecated tree' do
+ expect(subject.top_relation_tree('ci_pipelines')).to match(
+ a_hash_including(
+ {
+ 'external_pull_request' => {},
+ 'merge_request' => {},
+ 'stages' => { 'bridges' => {}, 'builds' => {}, 'generic_commit_statuses' => {}, 'statuses' => {} }
+ }
+ )
+ )
+ end
end
describe '#relation_excluded_keys' do
@@ -97,4 +109,31 @@ RSpec.describe BulkImports::FileTransfer::ProjectConfig do
expect(subject.file_relations).to contain_exactly('uploads', 'lfs_objects', 'repository', 'design')
end
end
+
+ describe '#batchable_relation?' do
+ context 'when relation is batchable' do
+ it 'returns true' do
+ expect(subject.batchable_relation?('issues')).to eq(true)
+ end
+ end
+
+ context 'when relation is not batchable' do
+ it 'returns false' do
+ expect(subject.batchable_relation?('project_feature')).to eq(false)
+ end
+ end
+
+ context 'when relation is not listed as portable' do
+ it 'returns false' do
+ expect(subject.batchable_relation?('foo')).to eq(false)
+ end
+ end
+ end
+
+ describe '#batchable_relations' do
+ it 'returns a list of collection associations for a project' do
+ expect(subject.batchable_relations).to include('issues', 'merge_requests', 'milestones')
+ expect(subject.batchable_relations).not_to include('project_feature', 'ci_cd_settings')
+ end
+ end
end
diff --git a/spec/models/bulk_imports/tracker_spec.rb b/spec/models/bulk_imports/tracker_spec.rb
index 1516ab106cb..a618a12df6b 100644
--- a/spec/models/bulk_imports/tracker_spec.rb
+++ b/spec/models/bulk_imports/tracker_spec.rb
@@ -4,7 +4,10 @@ require 'spec_helper'
RSpec.describe BulkImports::Tracker, type: :model do
describe 'associations' do
- it { is_expected.to belong_to(:entity).required }
+ it do
+ is_expected.to belong_to(:entity).required.class_name('BulkImports::Entity')
+ .with_foreign_key(:bulk_import_entity_id).inverse_of(:trackers)
+ end
end
describe 'validations' do
diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb
index 0838c232872..9d6b1a56458 100644
--- a/spec/models/chat_name_spec.rb
+++ b/spec/models/chat_name_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe ChatName, feature_category: :integrations do
subject { chat_name }
- it { is_expected.to belong_to(:integration) }
it { is_expected.to belong_to(:user) }
it { is_expected.to validate_presence_of(:user) }
@@ -16,12 +15,6 @@ RSpec.describe ChatName, feature_category: :integrations do
it { is_expected.to validate_uniqueness_of(:chat_id).scoped_to(:team_id) }
- it 'is not removed when the project is deleted' do
- expect { subject.reload.integration.project.delete }.not_to change { ChatName.count }
-
- expect(ChatName.where(id: subject.id)).to exist
- end
-
describe '#update_last_used_at', :clean_gitlab_redis_shared_state do
it 'updates the last_used_at timestamp' do
expect(subject.last_used_at).to be_nil
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index 7b307de87c7..ac994735928 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -12,9 +12,7 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
end
let(:bridge) do
- create(:ci_bridge, :variables, status: :created,
- options: options,
- pipeline: pipeline)
+ create(:ci_bridge, :variables, status: :created, options: options, pipeline: pipeline)
end
let(:options) do
@@ -40,16 +38,6 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
it 'returns true' do
expect(bridge.retryable?).to eq(true)
end
-
- context 'without ci_recreate_downstream_pipeline ff' do
- before do
- stub_feature_flags(ci_recreate_downstream_pipeline: false)
- end
-
- it 'returns false' do
- expect(bridge.retryable?).to eq(false)
- end
- end
end
context 'when there is a pipeline loop detected' do
@@ -172,7 +160,9 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
where(:downstream_status, :upstream_status) do
[
%w[success success],
- *::Ci::Pipeline.completed_statuses.without(:success).map { |status| [status.to_s, 'failed'] }
+ %w[canceled canceled],
+ %w[failed failed],
+ %w[skipped failed]
]
end
@@ -564,11 +554,13 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
let!(:prepare2) { create(:ci_build, name: 'prepare2', pipeline: pipeline, stage_idx: 0) }
let!(:prepare3) { create(:ci_build, name: 'prepare3', pipeline: pipeline, stage_idx: 0) }
let!(:bridge) do
- create(:ci_bridge, pipeline: pipeline,
- stage_idx: 1,
- scheduling_type: 'dag',
- needs_attributes: [{ name: 'prepare1', artifacts: true },
- { name: 'prepare2', artifacts: false }])
+ create(
+ :ci_bridge,
+ pipeline: pipeline,
+ stage_idx: 1,
+ scheduling_type: 'dag',
+ needs_attributes: [{ name: 'prepare1', artifacts: true }, { name: 'prepare2', artifacts: false }]
+ )
end
let!(:job_variable_1) { create(:ci_job_variable, :dotenv_source, job: prepare1) }
@@ -581,7 +573,7 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
end
end
- describe 'metadata partitioning', :ci_partitioning do
+ describe 'metadata partitioning', :ci_partitionable do
let(:pipeline) { create(:ci_pipeline, project: project, partition_id: ci_testing_partition_id) }
let(:bridge) do
diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb
index 1dd0386060d..0709aa47ff1 100644
--- a/spec/models/ci/build_dependencies_spec.rb
+++ b/spec/models/ci/build_dependencies_spec.rb
@@ -7,10 +7,13 @@ RSpec.describe Ci::BuildDependencies do
let_it_be(:project, reload: true) { create(:project, :repository) }
let_it_be(:pipeline, reload: true) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: project.default_branch,
- status: 'success')
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: project.commit.id,
+ ref: project.default_branch,
+ status: 'success'
+ )
end
let(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index fb50ba89cd3..8ed0e50e4b0 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -7,10 +7,13 @@ RSpec.describe Ci::BuildMetadata do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group, build_timeout: 2000) }
let_it_be(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: project.default_branch,
- status: 'success')
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: project.commit.id,
+ ref: project.default_branch,
+ status: 'success'
+ )
end
let_it_be_with_reload(:runner) { create(:ci_runner) }
@@ -22,7 +25,6 @@ RSpec.describe Ci::BuildMetadata do
it { is_expected.to belong_to(:build) }
it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:runner_machine) }
describe '#update_timeout_state' do
subject { metadata }
diff --git a/spec/models/ci/build_need_spec.rb b/spec/models/ci/build_need_spec.rb
index aa1c57d1788..e46a2b8cf85 100644
--- a/spec/models/ci/build_need_spec.rb
+++ b/spec/models/ci/build_need_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe Ci::BuildNeed, model: true, feature_category: :continuous_integra
stub_current_partition_id
end
- it 'creates build needs successfully', :aggregate_failures do
+ it 'creates build needs successfully', :aggregate_failures, :ci_partitionable do
ci_build.needs_attributes = [
{ name: "build", artifacts: true },
{ name: "build2", artifacts: true },
diff --git a/spec/models/ci/build_pending_state_spec.rb b/spec/models/ci/build_pending_state_spec.rb
index bff0b35f878..c978e3dba36 100644
--- a/spec/models/ci/build_pending_state_spec.rb
+++ b/spec/models/ci/build_pending_state_spec.rb
@@ -3,10 +3,15 @@
require 'spec_helper'
RSpec.describe Ci::BuildPendingState, feature_category: :continuous_integration do
+ describe 'associations' do
+ it do
+ is_expected.to belong_to(:build).class_name('Ci::Build').with_foreign_key(:build_id).inverse_of(:pending_state)
+ end
+ end
+
describe 'validations' do
subject(:pending_state) { build(:ci_build_pending_state) }
- it { is_expected.to belong_to(:build) }
it { is_expected.to validate_presence_of(:build) }
end
diff --git a/spec/models/ci/build_report_result_spec.rb b/spec/models/ci/build_report_result_spec.rb
index 90b23d3e824..90426f60c73 100644
--- a/spec/models/ci/build_report_result_spec.rb
+++ b/spec/models/ci/build_report_result_spec.rb
@@ -33,6 +33,19 @@ RSpec.describe Ci::BuildReportResult do
expect(build_report_result.errors.full_messages).to eq(["Data must be a valid json schema"])
end
end
+
+ context 'when data tests is invalid' do
+ it 'returns errors' do
+ build_report_result.data = {
+ 'tests' => {
+ 'invalid' => 'invalid'
+ }
+ }
+
+ expect(build_report_result).to be_invalid
+ expect(build_report_result.errors.full_messages).to eq(["Data must be a valid json schema"])
+ end
+ end
end
describe '#tests_name' do
diff --git a/spec/models/ci/build_runner_session_spec.rb b/spec/models/ci/build_runner_session_spec.rb
index 5e1a489ed8b..002aff25593 100644
--- a/spec/models/ci/build_runner_session_spec.rb
+++ b/spec/models/ci/build_runner_session_spec.rb
@@ -175,7 +175,7 @@ RSpec.describe Ci::BuildRunnerSession, model: true, feature_category: :continuou
end
end
- describe 'partitioning' do
+ describe 'partitioning', :ci_partitionable do
include Ci::PartitioningHelpers
let(:new_pipeline) { create(:ci_pipeline) }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 80d6693e08e..e3e78acb7e5 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -11,10 +11,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
let_it_be(:project, reload: true) { create_default(:project, :repository, group: group) }
let_it_be(:pipeline, reload: true) do
- create_default(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: project.default_branch,
- status: 'success')
+ create_default(
+ :ci_pipeline,
+ project: project,
+ sha: project.commit.id,
+ ref: project.default_branch,
+ status: 'success'
+ )
end
let_it_be(:build, refind: true) { create(:ci_build, pipeline: pipeline) }
@@ -22,20 +25,35 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
it { is_expected.to belong_to(:runner) }
it { is_expected.to belong_to(:trigger_request) }
it { is_expected.to belong_to(:erased_by) }
+ it { is_expected.to belong_to(:pipeline).inverse_of(:builds) }
it { is_expected.to have_many(:needs).with_foreign_key(:build_id) }
- it { is_expected.to have_many(:sourced_pipelines).with_foreign_key(:source_job_id) }
- it { is_expected.to have_one(:sourced_pipeline).with_foreign_key(:source_job_id) }
+
+ it do
+ is_expected.to have_many(:sourced_pipelines).class_name('Ci::Sources::Pipeline').with_foreign_key(:source_job_id)
+ .inverse_of(:build)
+ end
+
it { is_expected.to have_many(:job_variables).with_foreign_key(:job_id) }
it { is_expected.to have_many(:report_results).with_foreign_key(:build_id) }
it { is_expected.to have_many(:pages_deployments).with_foreign_key(:ci_build_id) }
it { is_expected.to have_one(:deployment) }
- it { is_expected.to have_one(:runner_machine).through(:metadata) }
+ it { is_expected.to have_one(:runner_manager).through(:runner_manager_build) }
it { is_expected.to have_one(:runner_session).with_foreign_key(:build_id) }
it { is_expected.to have_one(:trace_metadata).with_foreign_key(:build_id) }
it { is_expected.to have_one(:runtime_metadata).with_foreign_key(:build_id) }
- it { is_expected.to have_one(:pending_state).with_foreign_key(:build_id) }
+ it { is_expected.to have_one(:pending_state).with_foreign_key(:build_id).inverse_of(:build) }
+
+ it do
+ is_expected.to have_one(:queuing_entry).class_name('Ci::PendingBuild').with_foreign_key(:build_id).inverse_of(:build)
+ end
+
+ it do
+ is_expected.to have_one(:runtime_metadata).class_name('Ci::RunningBuild').with_foreign_key(:build_id)
+ .inverse_of(:build)
+ end
+
it { is_expected.to have_many(:terraform_state_versions).inverse_of(:build).with_foreign_key(:ci_build_id) }
it { is_expected.to validate_presence_of(:ref) }
@@ -1040,7 +1058,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'non public artifacts' do
- let(:build) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
+ let(:build) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) }
it { is_expected.to be_falsey }
end
@@ -1134,6 +1152,12 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
{ cache: [{ key: "key", paths: ["public"], policy: "pull-push" }] }
end
+ let(:options_with_fallback_keys) do
+ { cache: [
+ { key: "key", paths: ["public"], policy: "pull-push", fallback_keys: %w(key1 key2) }
+ ] }
+ end
+
subject { build.cache }
context 'when build has cache' do
@@ -1149,6 +1173,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
] }
end
+ let(:options_with_fallback_keys) do
+ { cache: [
+ { key: "key", paths: ["public"], policy: "pull-push", fallback_keys: %w(key3 key4) },
+ { key: "key2", paths: ["public"], policy: "pull-push", fallback_keys: %w(key5 key6) }
+ ] }
+ end
+
before do
allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(1)
end
@@ -1160,8 +1191,21 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
allow(build.pipeline).to receive(:protected_ref?).and_return(true)
end
- it do
- is_expected.to all(a_hash_including(key: a_string_matching(/-protected$/)))
+ context 'without the `unprotect` option' do
+ it do
+ is_expected.to all(a_hash_including(key: a_string_matching(/-protected$/)))
+ end
+
+ context 'and the caches have fallback keys' do
+ let(:options) { options_with_fallback_keys }
+
+ it do
+ is_expected.to all(a_hash_including({
+ key: a_string_matching(/-protected$/),
+ fallback_keys: array_including(a_string_matching(/-protected$/))
+ }))
+ end
+ end
end
context 'and the cache has the `unprotect` option' do
@@ -1175,6 +1219,20 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
it do
is_expected.to all(a_hash_including(key: a_string_matching(/-non_protected$/)))
end
+
+ context 'and the caches have fallback keys' do
+ let(:options) do
+ options_with_fallback_keys[:cache].each { |entry| entry[:unprotect] = true }
+ options_with_fallback_keys
+ end
+
+ it do
+ is_expected.to all(a_hash_including({
+ key: a_string_matching(/-non_protected$/),
+ fallback_keys: array_including(a_string_matching(/-non_protected$/))
+ }))
+ end
+ end
end
end
@@ -1186,6 +1244,17 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
it do
is_expected.to all(a_hash_including(key: a_string_matching(/-non_protected$/)))
end
+
+ context 'and the caches have fallback keys' do
+ let(:options) { options_with_fallback_keys }
+
+ it do
+ is_expected.to all(a_hash_including({
+ key: a_string_matching(/-non_protected$/),
+ fallback_keys: array_including(a_string_matching(/-non_protected$/))
+ }))
+ end
+ end
end
context 'when separated caches are disabled' do
@@ -1201,6 +1270,23 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
it 'is expected to have no type suffix' do
is_expected.to match([a_hash_including(key: 'key-1'), a_hash_including(key: 'key2-1')])
end
+
+ context 'and the caches have fallback keys' do
+ let(:options) { options_with_fallback_keys }
+
+ it do
+ is_expected.to match([
+ a_hash_including({
+ key: 'key-1',
+ fallback_keys: %w(key3-1 key4-1)
+ }),
+ a_hash_including({
+ key: 'key2-1',
+ fallback_keys: %w(key5-1 key6-1)
+ })
+ ])
+ end
+ end
end
context 'running on not protected ref' do
@@ -1211,6 +1297,23 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
it 'is expected to have no type suffix' do
is_expected.to match([a_hash_including(key: 'key-1'), a_hash_including(key: 'key2-1')])
end
+
+ context 'and the caches have fallback keys' do
+ let(:options) { options_with_fallback_keys }
+
+ it do
+ is_expected.to match([
+ a_hash_including({
+ key: 'key-1',
+ fallback_keys: %w(key3-1 key4-1)
+ }),
+ a_hash_including({
+ key: 'key2-1',
+ fallback_keys: %w(key5-1 key6-1)
+ })
+ ])
+ end
+ end
end
end
end
@@ -1221,6 +1324,17 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
it { is_expected.to be_an(Array).and all(include(key: a_string_matching(/^key-1-(?>protected|non_protected)/))) }
+
+ context 'and the cache have fallback keys' do
+ let(:options) { options_with_fallback_keys }
+
+ it do
+ is_expected.to be_an(Array).and all(include({
+ key: a_string_matching(/^key-1-(?>protected|non_protected)/),
+ fallback_keys: array_including(a_string_matching(/^key\d-1-(?>protected|non_protected)/))
+ }))
+ end
+ end
end
context 'when project does not have jobs_cache_index' do
@@ -1231,6 +1345,21 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
it do
is_expected.to eq(options[:cache].map { |entry| entry.merge(key: "#{entry[:key]}-non_protected") })
end
+
+ context 'and the cache have fallback keys' do
+ let(:options) { options_with_fallback_keys }
+
+ it do
+ is_expected.to eq(
+ options[:cache].map do |entry|
+ entry[:key] = "#{entry[:key]}-non_protected"
+ entry[:fallback_keys].map! { |key| "#{key}-non_protected" }
+
+ entry
+ end
+ )
+ end
+ end
end
end
@@ -1243,6 +1372,29 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
+ describe '#fallback_cache_keys_defined?' do
+ subject { build }
+
+ it 'returns false when fallback keys are not defined' do
+ expect(subject.fallback_cache_keys_defined?).to be false
+ end
+
+ context "with fallbacks keys" do
+ before do
+ allow(build).to receive(:options).and_return({
+ cache: [{
+ key: "key1",
+ fallback_keys: %w(key2)
+ }]
+ })
+ end
+
+ it 'returns true when fallback keys are defined' do
+ expect(subject.fallback_cache_keys_defined?).to be true
+ end
+ end
+ end
+
describe '#triggered_by?' do
subject { build.triggered_by?(user) }
@@ -1422,11 +1574,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
let(:build) { create(:ci_build, trait, pipeline: pipeline) }
let(:event) { state }
- context "when transitioning to #{params[:state]}" do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
+ context "when transitioning to #{params[:state]}", :saas do
it 'increments build_completed_report_type metric' do
expect(
::Gitlab::Ci::Artifacts::Metrics
@@ -1463,7 +1611,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
ActiveRecord::QueryRecorder.new { subject }
end
- index_for_build = recorded.log.index { |l| l.include?("UPDATE \"ci_builds\"") }
+ index_for_build = recorded.log.index { |l| l.include?("UPDATE #{described_class.quoted_table_name}") }
index_for_deployment = recorded.log.index { |l| l.include?("UPDATE \"deployments\"") }
expect(index_for_build).to be < index_for_deployment
@@ -1688,10 +1836,12 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
context 'when environment uses $CI_COMMIT_REF_NAME' do
let(:build) do
- create(:ci_build,
- ref: 'master',
- environment: 'review/$CI_COMMIT_REF_NAME',
- pipeline: pipeline)
+ create(
+ :ci_build,
+ ref: 'master',
+ environment: 'review/$CI_COMMIT_REF_NAME',
+ pipeline: pipeline
+ )
end
it { is_expected.to eq('review/master') }
@@ -1699,10 +1849,12 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
context 'when environment uses yaml_variables containing symbol keys' do
let(:build) do
- create(:ci_build,
- yaml_variables: [{ key: :APP_HOST, value: 'host' }],
- environment: 'review/$APP_HOST',
- pipeline: pipeline)
+ create(
+ :ci_build,
+ yaml_variables: [{ key: :APP_HOST, value: 'host' }],
+ environment: 'review/$APP_HOST',
+ pipeline: pipeline
+ )
end
it 'returns an expanded environment name with a list of variables' do
@@ -1724,12 +1876,26 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
context 'when using persisted variables' do
let(:build) do
- create(:ci_build, environment: 'review/x$CI_BUILD_ID', pipeline: pipeline)
+ create(:ci_build, environment: 'review/x$CI_JOB_ID', pipeline: pipeline)
end
it { is_expected.to eq('review/x') }
end
+ context 'when FF `ci_remove_legacy_predefined_variables` is disabled' do
+ before do
+ stub_feature_flags(ci_remove_legacy_predefined_variables: false)
+ end
+
+ context 'when using persisted variables' do
+ let(:build) do
+ create(:ci_build, environment: 'review/x$CI_BUILD_ID', pipeline: pipeline)
+ end
+
+ it { is_expected.to eq('review/x') }
+ end
+ end
+
context 'when environment name uses a nested variable' do
let(:yaml_variables) do
[
@@ -1738,11 +1904,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
let(:build) do
- create(:ci_build,
- ref: 'master',
- yaml_variables: yaml_variables,
- environment: 'review/$ENVIRONMENT_NAME',
- pipeline: pipeline)
+ create(
+ :ci_build,
+ ref: 'master',
+ yaml_variables: yaml_variables,
+ environment: 'review/$ENVIRONMENT_NAME',
+ pipeline: pipeline
+ )
end
it { is_expected.to eq('review/master') }
@@ -1926,62 +2094,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
- describe '#failed_but_allowed?' do
- subject { build.failed_but_allowed? }
-
- context 'when build is not allowed to fail' do
- before do
- build.allow_failure = false
- end
-
- context 'and build.status is success' do
- before do
- build.status = 'success'
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'and build.status is failed' do
- before do
- build.status = 'failed'
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when build is allowed to fail' do
- before do
- build.allow_failure = true
- end
-
- context 'and build.status is success' do
- before do
- build.status = 'success'
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'and build status is failed' do
- before do
- build.status = 'failed'
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when build is a manual action' do
- before do
- build.status = 'manual'
- end
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
describe 'flags' do
describe '#cancelable?' do
subject { build }
@@ -2058,14 +2170,14 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
- describe '#runner_machine' do
+ describe '#runner_manager' do
let_it_be(:runner) { create(:ci_runner) }
- let_it_be(:runner_machine) { create(:ci_runner_machine, runner: runner) }
- let_it_be(:build) { create(:ci_build, runner_machine: runner_machine) }
+ let_it_be(:runner_manager) { create(:ci_runner_machine, runner: runner) }
+ let_it_be(:build) { create(:ci_build, runner_manager: runner_manager) }
- subject(:build_runner_machine) { described_class.find(build.id).runner_machine }
+ subject(:build_runner_manager) { described_class.find(build.id).runner_manager }
- it { is_expected.to eq(runner_machine) }
+ it { is_expected.to eq(runner_manager) }
end
describe '#tag_list' do
@@ -2130,8 +2242,14 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
describe 'build auto retry feature' do
context 'with deployment job' do
let(:build) do
- create(:ci_build, :deploy_to_production, :with_deployment,
- user: user, pipeline: pipeline, project: project)
+ create(
+ :ci_build,
+ :deploy_to_production,
+ :with_deployment,
+ user: user,
+ pipeline: pipeline,
+ project: project
+ )
end
before do
@@ -2762,6 +2880,89 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
{ key: 'CI_JOB_URL', value: project.web_url + "/-/jobs/#{build.id}", public: true, masked: false },
{ key: 'CI_JOB_TOKEN', value: 'my-token', public: false, masked: true },
{ key: 'CI_JOB_STARTED_AT', value: build.started_at&.iso8601, public: true, masked: false },
+ { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true, masked: false },
+ { key: 'CI_REGISTRY_PASSWORD', value: 'my-token', public: false, masked: true },
+ { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false, masked: false },
+ { key: 'CI_DEPENDENCY_PROXY_USER', value: 'gitlab-ci-token', public: true, masked: false },
+ { key: 'CI_DEPENDENCY_PROXY_PASSWORD', value: 'my-token', public: false, masked: true },
+ { key: 'CI_JOB_JWT', value: 'ci.job.jwt', public: false, masked: true },
+ { key: 'CI_JOB_JWT_V1', value: 'ci.job.jwt', public: false, masked: true },
+ { key: 'CI_JOB_JWT_V2', value: 'ci.job.jwtv2', public: false, masked: true },
+ { key: 'CI_JOB_NAME', value: 'test', public: true, masked: false },
+ { key: 'CI_JOB_NAME_SLUG', value: 'test', public: true, masked: false },
+ { key: 'CI_JOB_STAGE', value: 'test', public: true, masked: false },
+ { key: 'CI_NODE_TOTAL', value: '1', public: true, masked: false },
+ { key: 'CI', value: 'true', public: true, masked: false },
+ { key: 'GITLAB_CI', value: 'true', public: true, masked: false },
+ { key: 'CI_SERVER_URL', value: Gitlab.config.gitlab.url, public: true, masked: false },
+ { key: 'CI_SERVER_HOST', value: Gitlab.config.gitlab.host, public: true, masked: false },
+ { key: 'CI_SERVER_PORT', value: Gitlab.config.gitlab.port.to_s, public: true, masked: false },
+ { key: 'CI_SERVER_PROTOCOL', value: Gitlab.config.gitlab.protocol, public: true, masked: false },
+ { key: 'CI_SERVER_SHELL_SSH_HOST', value: Gitlab.config.gitlab_shell.ssh_host.to_s, public: true, masked: false },
+ { key: 'CI_SERVER_SHELL_SSH_PORT', value: Gitlab.config.gitlab_shell.ssh_port.to_s, public: true, masked: false },
+ { key: 'CI_SERVER_NAME', value: 'GitLab', public: true, masked: false },
+ { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true, masked: false },
+ { key: 'CI_SERVER_VERSION_MAJOR', value: Gitlab.version_info.major.to_s, public: true, masked: false },
+ { key: 'CI_SERVER_VERSION_MINOR', value: Gitlab.version_info.minor.to_s, public: true, masked: false },
+ { key: 'CI_SERVER_VERSION_PATCH', value: Gitlab.version_info.patch.to_s, public: true, masked: false },
+ { key: 'CI_SERVER_REVISION', value: Gitlab.revision, public: true, masked: false },
+ { key: 'GITLAB_FEATURES', value: project.licensed_features.join(','), public: true, masked: false },
+ { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true, masked: false },
+ { key: 'CI_PROJECT_NAME', value: project.path, public: true, masked: false },
+ { key: 'CI_PROJECT_TITLE', value: project.title, public: true, masked: false },
+ { key: 'CI_PROJECT_DESCRIPTION', value: project.description, public: true, masked: false },
+ { key: 'CI_PROJECT_PATH', value: project.full_path, public: true, masked: false },
+ { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true, masked: false },
+ { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true, masked: false },
+ { key: 'CI_PROJECT_NAMESPACE_ID', value: project.namespace.id.to_s, public: true, masked: false },
+ { key: 'CI_PROJECT_ROOT_NAMESPACE', value: project.namespace.root_ancestor.path, public: true, masked: false },
+ { key: 'CI_PROJECT_URL', value: project.web_url, public: true, masked: false },
+ { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true, masked: false },
+ { key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: project.repository_languages.map(&:name).join(',').downcase, public: true, masked: false },
+ { key: 'CI_PROJECT_CLASSIFICATION_LABEL', value: project.external_authorization_classification_label, public: true, masked: false },
+ { key: 'CI_DEFAULT_BRANCH', value: project.default_branch, public: true, masked: false },
+ { key: 'CI_CONFIG_PATH', value: project.ci_config_path_or_default, public: true, masked: false },
+ { key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host, public: true, masked: false },
+ { key: 'CI_PAGES_URL', value: project.pages_url, public: true, masked: false },
+ { key: 'CI_DEPENDENCY_PROXY_SERVER', value: Gitlab.host_with_port, public: true, masked: false },
+ { key: 'CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX',
+ value: "#{Gitlab.host_with_port}/#{project.namespace.root_ancestor.path.downcase}#{DependencyProxy::URL_SUFFIX}",
+ public: true,
+ masked: false },
+ { key: 'CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX',
+ value: "#{Gitlab.host_with_port}/#{project.namespace.full_path.downcase}#{DependencyProxy::URL_SUFFIX}",
+ public: true,
+ masked: false },
+ { key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true, masked: false },
+ { key: 'CI_API_GRAPHQL_URL', value: 'http://localhost/api/graphql', public: true, masked: false },
+ { key: 'CI_TEMPLATE_REGISTRY_HOST', value: template_registry_host, public: true, masked: false },
+ { key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true, masked: false },
+ { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true, masked: false },
+ { key: 'CI_PIPELINE_CREATED_AT', value: pipeline.created_at.iso8601, public: true, masked: false },
+ { key: 'CI_COMMIT_SHA', value: build.sha, public: true, masked: false },
+ { key: 'CI_COMMIT_SHORT_SHA', value: build.short_sha, public: true, masked: false },
+ { key: 'CI_COMMIT_BEFORE_SHA', value: build.before_sha, public: true, masked: false },
+ { key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true, masked: false },
+ { key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true, masked: false },
+ { key: 'CI_COMMIT_BRANCH', value: build.ref, public: true, masked: false },
+ { key: 'CI_COMMIT_MESSAGE', value: pipeline.git_commit_message, public: true, masked: false },
+ { 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_COMMIT_AUTHOR', value: pipeline.git_author_full_text, public: true, masked: false }
+ ]
+ end
+
+ # Remove this definition when FF `ci_remove_legacy_predefined_variables` is removed
+ let(:predefined_with_legacy_variables) do
+ [
+ { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true, masked: false },
+ { key: 'CI_PIPELINE_URL', value: project.web_url + "/-/pipelines/#{pipeline.id}", public: true, masked: false },
+ { key: 'CI_JOB_ID', value: build.id.to_s, public: true, masked: false },
+ { key: 'CI_JOB_URL', value: project.web_url + "/-/jobs/#{build.id}", public: true, masked: false },
+ { key: 'CI_JOB_TOKEN', value: 'my-token', public: false, masked: true },
+ { key: 'CI_JOB_STARTED_AT', value: build.started_at&.iso8601, public: true, masked: false },
{ key: 'CI_BUILD_ID', value: build.id.to_s, public: true, masked: false },
{ key: 'CI_BUILD_TOKEN', value: 'my-token', public: false, masked: true },
{ key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true, masked: false },
@@ -2784,6 +2985,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
{ key: 'CI_SERVER_HOST', value: Gitlab.config.gitlab.host, public: true, masked: false },
{ key: 'CI_SERVER_PORT', value: Gitlab.config.gitlab.port.to_s, public: true, masked: false },
{ key: 'CI_SERVER_PROTOCOL', value: Gitlab.config.gitlab.protocol, public: true, masked: false },
+ { key: 'CI_SERVER_SHELL_SSH_HOST', value: Gitlab.config.gitlab_shell.ssh_host.to_s, public: true, masked: false },
+ { key: 'CI_SERVER_SHELL_SSH_PORT', value: Gitlab.config.gitlab_shell.ssh_port.to_s, public: true, masked: false },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true, masked: false },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true, masked: false },
{ key: 'CI_SERVER_VERSION_MAJOR', value: Gitlab.version_info.major.to_s, public: true, masked: false },
@@ -2818,6 +3021,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
public: true,
masked: false },
{ key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true, masked: false },
+ { key: 'CI_API_GRAPHQL_URL', value: 'http://localhost/api/graphql', public: true, masked: false },
{ key: 'CI_TEMPLATE_REGISTRY_HOST', value: template_registry_host, public: true, masked: false },
{ key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true, masked: false },
{ key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true, masked: false },
@@ -2851,6 +3055,14 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
it { is_expected.to be_instance_of(Gitlab::Ci::Variables::Collection) }
it { expect(subject.to_runner_variables).to eq(predefined_variables) }
+ context 'when FF `ci_remove_legacy_predefined_variables` is disabled' do
+ before do
+ stub_feature_flags(ci_remove_legacy_predefined_variables: false)
+ end
+
+ it { expect(subject.to_runner_variables).to eq(predefined_with_legacy_variables) }
+ end
+
it 'excludes variables that require an environment or user' do
environment_based_variables_collection = subject.filter do |variable|
%w[
@@ -2877,14 +3089,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
- context 'when the opt_in_jwt project setting is true' do
- it 'does not include the JWT variables' do
- project.ci_cd_settings.update!(opt_in_jwt: true)
-
- expect(subject.pluck(:key)).not_to include('CI_JOB_JWT', 'CI_JOB_JWT_V1', 'CI_JOB_JWT_V2')
- end
- end
-
describe 'variables ordering' do
context 'when variables hierarchy is stubbed' do
let(:build_pre_var) { { key: 'build', value: 'value', public: true, masked: false } }
@@ -2941,16 +3145,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
before do
- create(:environment, project: build.project,
- name: 'staging')
+ create(:environment, project: build.project, name: 'staging')
- build.yaml_variables = [{ key: 'YAML_VARIABLE',
- value: 'var',
- public: true }]
+ build.yaml_variables = [{ key: 'YAML_VARIABLE', value: 'var', public: true }]
build.environment = 'staging'
# CI_ENVIRONMENT_NAME is set in predefined_variables when job environment is provided
- predefined_variables.insert(20, { key: 'CI_ENVIRONMENT_NAME', value: 'staging', public: true, masked: false })
+ predefined_variables.insert(18, { key: 'CI_ENVIRONMENT_NAME', value: 'staging', public: true, masked: false })
end
it 'matches explicit variables ordering' do
@@ -3003,6 +3204,97 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
end
+
+ context 'when FF `ci_remove_legacy_predefined_variables` is disabled' do
+ before do
+ stub_feature_flags(ci_remove_legacy_predefined_variables: false)
+ end
+
+ context 'when build has environment and user-provided variables' do
+ let(:expected_variables) do
+ predefined_with_legacy_variables.map { |variable| variable.fetch(:key) } +
+ %w[YAML_VARIABLE CI_ENVIRONMENT_NAME CI_ENVIRONMENT_SLUG
+ CI_ENVIRONMENT_ACTION CI_ENVIRONMENT_TIER CI_ENVIRONMENT_URL]
+ end
+
+ before do
+ create(:environment, project: build.project, name: 'staging')
+
+ build.yaml_variables = [{ key: 'YAML_VARIABLE', value: 'var', public: true }]
+ build.environment = 'staging'
+
+ # CI_ENVIRONMENT_NAME is set in predefined_variables when job environment is provided
+ predefined_with_legacy_variables.insert(20, { key: 'CI_ENVIRONMENT_NAME', value: 'staging', public: true, masked: false })
+ end
+
+ it 'matches explicit variables ordering' do
+ received_variables = subject.map { |variable| variable[:key] }
+
+ expect(received_variables).to eq expected_variables
+ end
+
+ describe 'CI_ENVIRONMENT_ACTION' do
+ let(:enviroment_action_variable) { subject.find { |variable| variable[:key] == 'CI_ENVIRONMENT_ACTION' } }
+
+ shared_examples 'defaults value' do
+ it 'value matches start' do
+ expect(enviroment_action_variable[:value]).to eq('start')
+ end
+ end
+
+ it_behaves_like 'defaults value'
+
+ context 'when options is set' do
+ before do
+ build.update!(options: options)
+ end
+
+ context 'when options is empty' do
+ let(:options) { {} }
+
+ it_behaves_like 'defaults value'
+ end
+
+ context 'when options is nil' do
+ let(:options) { nil }
+
+ it_behaves_like 'defaults value'
+ end
+
+ context 'when options environment is specified' do
+ let(:options) { { environment: {} } }
+
+ it_behaves_like 'defaults value'
+ end
+
+ context 'when options environment action specified' do
+ let(:options) { { environment: { action: 'stop' } } }
+
+ it 'matches the specified action' do
+ expect(enviroment_action_variable[:value]).to eq('stop')
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ context 'when the build has ID tokens' do
+ before do
+ build.update!(
+ id_tokens: { 'TEST_ID_TOKEN' => { 'aud' => 'https://client.test' } }
+ )
+ end
+
+ it 'includes the tokens and excludes the predefined JWT variables' do
+ runner_vars = subject.to_runner_variables.pluck(:key)
+
+ expect(runner_vars).to include('TEST_ID_TOKEN')
+ expect(runner_vars).not_to include('CI_JOB_JWT')
+ expect(runner_vars).not_to include('CI_JOB_JWT_V1')
+ expect(runner_vars).not_to include('CI_JOB_JWT_V2')
+ end
end
end
@@ -3046,12 +3338,14 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
let!(:environment) do
- create(:environment,
- project: build.project,
- name: 'production',
- slug: 'prod-slug',
- tier: 'production',
- external_url: '')
+ create(
+ :environment,
+ project: build.project,
+ name: 'production',
+ slug: 'prod-slug',
+ tier: 'production',
+ external_url: ''
+ )
end
before do
@@ -3184,10 +3478,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
let(:tag_message) { project.repository.tags.first.message }
let!(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: tag_name,
- status: 'success')
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: project.commit.id,
+ ref: tag_name,
+ status: 'success'
+ )
end
let!(:build) { create(:ci_build, pipeline: pipeline, ref: tag_name) }
@@ -3218,8 +3515,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
before do
- create(:ci_variable,
- ci_variable.slice(:key, :value).merge(project: project))
+ create(:ci_variable, ci_variable.slice(:key, :value).merge(project: project))
end
it { is_expected.to include(ci_variable) }
@@ -3233,9 +3529,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
before do
- create(:ci_variable,
- :protected,
- protected_variable.slice(:key, :value).merge(project: project))
+ create(:ci_variable, :protected, protected_variable.slice(:key, :value).merge(project: project))
end
context 'when the branch is protected' do
@@ -3265,8 +3559,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
before do
- create(:ci_group_variable,
- ci_variable.slice(:key, :value).merge(group: group))
+ create(:ci_group_variable, ci_variable.slice(:key, :value).merge(group: group))
end
it { is_expected.to include(ci_variable) }
@@ -3280,9 +3573,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
before do
- create(:ci_group_variable,
- :protected,
- protected_variable.slice(:key, :value).merge(group: group))
+ create(:ci_group_variable, :protected, protected_variable.slice(:key, :value).merge(group: group))
end
context 'when the branch is protected' do
@@ -3335,9 +3626,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
let!(:pipeline_schedule_variable) do
- create(:ci_pipeline_schedule_variable,
- key: 'SCHEDULE_VARIABLE_KEY',
- pipeline_schedule: pipeline_schedule)
+ create(:ci_pipeline_schedule_variable, key: 'SCHEDULE_VARIABLE_KEY', pipeline_schedule: pipeline_schedule)
end
before do
@@ -3352,10 +3641,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
let_it_be_with_reload(:project) { create(:project, :public, :repository, group: group) }
let_it_be_with_reload(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: project.default_branch,
- status: 'success')
+ create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch, status: 'success')
end
let_it_be_with_refind(:build) { create(:ci_build, pipeline: pipeline) }
@@ -3600,7 +3886,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
[
{ key: 'APP_STORE_CONNECT_API_KEY_ISSUER_ID', value: apple_app_store_integration.app_store_issuer_id, masked: true, public: false },
{ key: 'APP_STORE_CONNECT_API_KEY_KEY', value: Base64.encode64(apple_app_store_integration.app_store_private_key), masked: true, public: false },
- { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: apple_app_store_integration.app_store_key_id, masked: true, public: false }
+ { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: apple_app_store_integration.app_store_key_id, masked: true, public: false },
+ { key: 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64', value: "true", masked: false, public: false }
]
end
@@ -3626,6 +3913,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_ISSUER_ID' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY_ID' }).to be_nil
+ expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64' }).to be_nil
end
end
end
@@ -3635,6 +3923,47 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_ISSUER_ID' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY_ID' }).to be_nil
+ expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64' }).to be_nil
+ end
+ end
+ end
+
+ context 'for the google_play integration' do
+ let_it_be(:google_play_integration) { create(:google_play_integration) }
+
+ let(:google_play_variables) do
+ [
+ { key: 'SUPPLY_JSON_KEY_DATA', value: google_play_integration.service_account_key, masked: true, public: false }
+ ]
+ end
+
+ context 'when the google_play integration exists' do
+ context 'when a build is protected' do
+ before do
+ allow(build.pipeline).to receive(:protected_ref?).and_return(true)
+ build.project.update!(google_play_integration: google_play_integration)
+ end
+
+ it 'includes google_play variables' do
+ is_expected.to include(*google_play_variables)
+ end
+ end
+
+ context 'when a build is not protected' do
+ before do
+ allow(build.pipeline).to receive(:protected_ref?).and_return(false)
+ build.project.update!(google_play_integration: google_play_integration)
+ end
+
+ it 'does not include the google_play variable' do
+ expect(subject[:key] == 'SUPPLY_JSON_KEY_DATA').to eq(false)
+ end
+ end
+ end
+
+ context 'when the googel_play integration does not exist' do
+ it 'does not include google_play variable' do
+ expect(subject[:key] == 'SUPPLY_JSON_KEY_DATA').to eq(false)
end
end
end
@@ -3656,6 +3985,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
'ID_TOKEN_1' => { aud: 'developers' },
'ID_TOKEN_2' => { aud: 'maintainers' }
})
+ build.runner = build_stubbed(:ci_runner)
end
subject(:runner_vars) { build.variables.to_runner_variables }
@@ -3750,8 +4080,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
keys = %w[CI_JOB_ID
CI_JOB_URL
CI_JOB_TOKEN
- CI_BUILD_ID
- CI_BUILD_TOKEN
CI_REGISTRY_USER
CI_REGISTRY_PASSWORD
CI_REPOSITORY_URL
@@ -3763,6 +4091,30 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
expect(names).not_to include(*keys)
end
end
+
+ context 'when FF `ci_remove_legacy_predefined_variables` is disabled' do
+ before do
+ stub_feature_flags(ci_remove_legacy_predefined_variables: false)
+ end
+
+ it 'does not return prohibited variables' do
+ keys = %w[CI_JOB_ID
+ CI_JOB_URL
+ CI_JOB_TOKEN
+ CI_BUILD_ID
+ CI_BUILD_TOKEN
+ CI_REGISTRY_USER
+ CI_REGISTRY_PASSWORD
+ CI_REPOSITORY_URL
+ CI_ENVIRONMENT_URL
+ CI_DEPLOY_USER
+ CI_DEPLOY_PASSWORD]
+
+ build.scoped_variables.map { |env| env[:key] }.tap do |names|
+ expect(names).not_to include(*keys)
+ end
+ end
+ end
end
context 'with dependency variables' do
@@ -5720,9 +6072,11 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
describe '#runtime_hooks' do
let(:build1) do
- FactoryBot.build(:ci_build,
- options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } },
- pipeline: pipeline)
+ FactoryBot.build(
+ :ci_build,
+ options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } },
+ pipeline: pipeline
+ )
end
subject(:runtime_hooks) { build1.runtime_hooks }
@@ -5784,15 +6138,9 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
expect(build.token).to be_nil
expect(build.changes).to be_empty
end
-
- it 'does not remove the token when FF is disabled' do
- stub_feature_flags(remove_job_token_on_completion: false)
-
- expect { build.remove_token! }.not_to change(build, :token)
- end
end
- describe 'metadata partitioning', :ci_partitioning do
+ describe 'metadata partitioning', :ci_partitionable do
let(:pipeline) { create(:ci_pipeline, project: project, partition_id: ci_testing_partition_id) }
let(:build) do
@@ -5905,4 +6253,31 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
end
+
+ describe 'token format for builds transiting into pending' do
+ let(:partition_id) { 100 }
+ let(:ci_build) { described_class.new(partition_id: partition_id) }
+
+ context 'when build is initialized without a token and transits to pending' do
+ let(:partition_id_prefix_in_16_bit_encode) { partition_id.to_s(16) + '_' }
+
+ it 'generates a token' do
+ expect { ci_build.enqueue }
+ .to change { ci_build.token }.from(nil).to(a_string_starting_with(partition_id_prefix_in_16_bit_encode))
+ end
+ end
+
+ context 'when build is initialized with a token and transits to pending' do
+ let(:token) { 'an_existing_secret_token' }
+
+ before do
+ ci_build.set_token(token)
+ end
+
+ it 'does not change the existing token' do
+ expect { ci_build.enqueue }
+ .not_to change { ci_build.token }.from(token)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index ac0a18a176d..355905cdabd 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -20,6 +20,12 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
stub_artifacts_object_storage
end
+ describe 'associations' do
+ it do
+ is_expected.to belong_to(:build).class_name('Ci::Build').with_foreign_key(:build_id).inverse_of(:trace_chunks)
+ end
+ end
+
it_behaves_like 'having unique enum values'
def redis_instance
@@ -633,8 +639,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
build_trace_chunk.checksum = '12345'
expect { build_trace_chunk.persist_data! }
- .to raise_error(described_class::FailedToPersistDataError,
- /Modifed build trace chunk detected/)
+ .to raise_error(described_class::FailedToPersistDataError, /Modifed build trace chunk detected/)
end
end
diff --git a/spec/models/ci/build_trace_metadata_spec.rb b/spec/models/ci/build_trace_metadata_spec.rb
index 2ab300e4054..866d94b4cbe 100644
--- a/spec/models/ci/build_trace_metadata_spec.rb
+++ b/spec/models/ci/build_trace_metadata_spec.rb
@@ -159,7 +159,7 @@ RSpec.describe Ci::BuildTraceMetadata, feature_category: :continuous_integration
end
end
- describe 'partitioning' do
+ describe 'partitioning', :ci_partitionable do
include Ci::PartitioningHelpers
let_it_be(:pipeline) { create(:ci_pipeline) }
diff --git a/spec/models/ci/build_trace_spec.rb b/spec/models/ci/build_trace_spec.rb
index 907b49dc180..30e4ef760d7 100644
--- a/spec/models/ci/build_trace_spec.rb
+++ b/spec/models/ci/build_trace_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildTrace do
+RSpec.describe Ci::BuildTrace, feature_category: :continuous_integration do
let(:build) { build_stubbed(:ci_build) }
let(:state) { nil }
let(:data) { StringIO.new('the-stream') }
@@ -13,7 +13,7 @@ RSpec.describe Ci::BuildTrace do
subject { described_class.new(build: build, stream: stream, state: state) }
- shared_examples 'delegates methods' do
+ describe 'delegated methods' do
it { is_expected.to delegate_method(:state).to(:trace) }
it { is_expected.to delegate_method(:append).to(:trace) }
it { is_expected.to delegate_method(:truncated).to(:trace) }
@@ -25,8 +25,6 @@ RSpec.describe Ci::BuildTrace do
it { is_expected.to delegate_method(:complete?).to(:build).with_prefix }
end
- it_behaves_like 'delegates methods'
-
it 'returns formatted trace' do
expect(subject.lines).to eq(
[
diff --git a/spec/models/ci/catalog/listing_spec.rb b/spec/models/ci/catalog/listing_spec.rb
new file mode 100644
index 00000000000..93d70a3f63e
--- /dev/null
+++ b/spec/models/ci/catalog/listing_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:project_1) { create(:project, namespace: namespace) }
+ let_it_be(:project_2) { create(:project, namespace: namespace) }
+ let_it_be(:project_3) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:list) { described_class.new(namespace, user) }
+
+ describe '#new' do
+ context 'when namespace is not a root namespace' do
+ let(:namespace) { create(:group, :nested) }
+
+ it 'raises an exception' do
+ expect { list }.to raise_error(ArgumentError, 'Namespace is not a root namespace')
+ end
+ end
+ end
+
+ describe '#resources' do
+ subject(:resources) { list.resources }
+
+ context 'when the user has access to all projects in the namespace' do
+ before do
+ namespace.add_developer(user)
+ end
+
+ context 'when the namespace has no catalog resources' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'when the namespace has catalog resources' do
+ let!(:resource) { create(:catalog_resource, project: project_1) }
+ let!(:other_namespace_resource) { create(:catalog_resource, project: project_3) }
+
+ it 'contains only catalog resources for projects in that namespace' do
+ is_expected.to contain_exactly(resource)
+ end
+ end
+ end
+
+ context 'when the user only has access to some projects in the namespace' do
+ let!(:resource_1) { create(:catalog_resource, project: project_1) }
+ let!(:resource_2) { create(:catalog_resource, project: project_2) }
+
+ before do
+ project_1.add_developer(user)
+ project_2.add_guest(user)
+ end
+
+ it 'only returns catalog resources for projects the user has access to' do
+ is_expected.to contain_exactly(resource_1)
+ end
+ end
+
+ context 'when the user does not have access to the namespace' do
+ let!(:resource) { create(:catalog_resource, project: project_1) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+end
diff --git a/spec/models/ci/catalog/resource_spec.rb b/spec/models/ci/catalog/resource_spec.rb
new file mode 100644
index 00000000000..a239bbad857
--- /dev/null
+++ b/spec/models/ci/catalog/resource_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:resource) { create(:catalog_resource, project: project) }
+
+ let_it_be(:releases) do
+ [
+ create(:release, project: project, released_at: Time.zone.now - 2.days),
+ create(:release, project: project, released_at: Time.zone.now - 1.day),
+ create(:release, project: project, released_at: Time.zone.now)
+ ]
+ end
+
+ it { is_expected.to belong_to(:project) }
+
+ it { is_expected.to delegate_method(:avatar_path).to(:project) }
+ it { is_expected.to delegate_method(:description).to(:project) }
+ it { is_expected.to delegate_method(:name).to(:project) }
+
+ describe '.for_projects' do
+ it 'returns catalog resources for the given project IDs' do
+ resources_for_projects = described_class.for_projects(project.id)
+
+ expect(resources_for_projects).to contain_exactly(resource)
+ end
+ end
+
+ describe '#versions' do
+ it 'returns releases ordered by released date descending' do
+ expect(resource.versions).to eq(releases.reverse)
+ end
+ end
+
+ describe '#latest_version' do
+ it 'returns the latest release' do
+ expect(resource.latest_version).to eq(releases.last)
+ end
+ end
+end
diff --git a/spec/models/ci/commit_with_pipeline_spec.rb b/spec/models/ci/commit_with_pipeline_spec.rb
index 320143535e2..766e99288c0 100644
--- a/spec/models/ci/commit_with_pipeline_spec.rb
+++ b/spec/models/ci/commit_with_pipeline_spec.rb
@@ -2,9 +2,9 @@
require 'spec_helper'
-RSpec.describe Ci::CommitWithPipeline do
- let(:project) { create(:project, :public, :repository) }
- let(:commit) { described_class.new(project.commit) }
+RSpec.describe Ci::CommitWithPipeline, feature_category: :continuous_integration do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let(:commit) { described_class.new(project.commit) }
describe '#last_pipeline' do
let!(:first_pipeline) do
@@ -27,28 +27,43 @@ RSpec.describe Ci::CommitWithPipeline do
end
describe '#lazy_latest_pipeline' do
- let(:commit_1) do
- described_class.new(Commit.new(RepoHelpers.sample_commit, project))
+ let_it_be(:other_project) { create(:project, :repository) }
+
+ let_it_be(:commits_with_pipelines) do
+ [
+ described_class.new(Commit.new(RepoHelpers.sample_commit, project)),
+ described_class.new(Commit.new(RepoHelpers.another_sample_commit, project)),
+ described_class.new(Commit.new(RepoHelpers.sample_big_commit, project)),
+ described_class.new(Commit.new(RepoHelpers.sample_commit, other_project))
+ ]
end
- let(:commit_2) do
- described_class.new(Commit.new(RepoHelpers.another_sample_commit, project))
+ let_it_be(:commits) do
+ commits_with_pipelines + [
+ described_class.new(Commit.new(RepoHelpers.another_sample_commit, other_project))
+ ]
end
- let!(:commits) { [commit_1, commit_2] }
+ before(:all) do
+ commits_with_pipelines.each do |commit|
+ create(:ci_empty_pipeline, project: commit.project, sha: commit.sha)
+ end
+ end
- it 'executes only 1 SQL query' do
+ it 'returns the correct pipelines with only 1 SQL query per project', :aggregate_failures do
recorder = ActiveRecord::QueryRecorder.new do
- # Running this first ensures we don't run one query for every
- # commit.
+ # batch commits
commits.each(&:lazy_latest_pipeline)
- # This forces the execution of the SQL queries necessary to load the
- # data.
- commits.each { |c| c.latest_pipeline.try(:id) }
+ # assert result correctness
+ commits_with_pipelines.each do |commit|
+ expect(commit.lazy_latest_pipeline.project).to eq(commit.project)
+ end
+
+ expect(commits.last.lazy_latest_pipeline&.itself).to be_nil
end
- expect(recorder.count).to eq(1)
+ expect(recorder.count).to eq(2)
end
end
diff --git a/spec/models/ci/daily_build_group_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb
index cd55817243f..6f73d89d760 100644
--- a/spec/models/ci/daily_build_group_report_result_spec.rb
+++ b/spec/models/ci/daily_build_group_report_result_spec.rb
@@ -6,7 +6,11 @@ RSpec.describe Ci::DailyBuildGroupReportResult do
let(:daily_build_group_report_result) { build(:ci_daily_build_group_report_result) }
describe 'associations' do
- it { is_expected.to belong_to(:last_pipeline) }
+ it do
+ is_expected.to belong_to(:last_pipeline).class_name('Ci::Pipeline')
+ .with_foreign_key(:last_pipeline_id).inverse_of(:daily_build_group_report_results)
+ end
+
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:group) }
end
diff --git a/spec/models/ci/group_spec.rb b/spec/models/ci/group_spec.rb
index 4900bc792af..82e4f7ce8fc 100644
--- a/spec/models/ci/group_spec.rb
+++ b/spec/models/ci/group_spec.rb
@@ -111,11 +111,13 @@ RSpec.describe Ci::Group do
end
def create_build(type, status: 'success', **opts)
- create(type, pipeline: pipeline,
- stage: stage.name,
- status: status,
- stage_id: stage.id,
- **opts)
+ create(
+ type, pipeline: pipeline,
+ stage: stage.name,
+ status: status,
+ stage_id: stage.id,
+ **opts
+ )
end
end
end
diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb
index e73319cfcd7..a2751b9fb20 100644
--- a/spec/models/ci/group_variable_spec.rb
+++ b/spec/models/ci/group_variable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::GroupVariable, feature_category: :pipeline_authoring do
+RSpec.describe Ci::GroupVariable, feature_category: :secrets_management do
let_it_be_with_refind(:group) { create(:group) }
subject { build(:ci_group_variable, group: group) }
@@ -65,4 +65,16 @@ RSpec.describe Ci::GroupVariable, feature_category: :pipeline_authoring do
expect(subject.audit_details).to eq(subject.key)
end
end
+
+ describe '#group_name' do
+ it "equals to the name of the group the variable belongs to" do
+ expect(subject.group_name).to eq(subject.group.name)
+ end
+ end
+
+ describe '#group_ci_cd_settings_path' do
+ it "equals to the path of the CI/CD settings of the group the variable belongs to" do
+ expect(subject.group_ci_cd_settings_path).to eq(Gitlab::Routing.url_helpers.group_settings_ci_cd_path(subject.group))
+ end
+ end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index e94445f17cd..a34657adf60 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do
describe "Associations" do
it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:job) }
+ it { is_expected.to belong_to(:job).class_name('Ci::Build').with_foreign_key(:job_id).inverse_of(:job_artifacts) }
it { is_expected.to validate_presence_of(:job) }
it { is_expected.to validate_presence_of(:partition_id) }
end
@@ -243,6 +243,29 @@ RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do
end
end
+ describe '.non_trace' do
+ subject { described_class.non_trace }
+
+ context 'when there is only a trace job artifact' do
+ let!(:trace) { create(:ci_job_artifact, :trace) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when there is only a non-trace job artifact' do
+ let!(:junit) { create(:ci_job_artifact, :junit) }
+
+ it { is_expected.to eq([junit]) }
+ end
+
+ context 'when there are both trace and non-trace job artifacts' do
+ let!(:trace) { create(:ci_job_artifact, :trace) }
+ let!(:junit) { create(:ci_job_artifact, :junit) }
+
+ it { is_expected.to eq([junit]) }
+ end
+ end
+
describe '.downloadable' do
subject { described_class.downloadable }
@@ -460,9 +483,11 @@ RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do
context "when #{file_type} type with other formats" do
described_class.file_formats.except(file_format).values.each do |other_format|
- let(:artifact) { build(:ci_job_artifact, file_type: file_type, file_format: other_format) }
+ context "with #{other_format}" do
+ let(:artifact) { build(:ci_job_artifact, file_type: file_type, file_format: other_format) }
- it { is_expected.not_to be_valid }
+ it { is_expected.not_to be_valid }
+ end
end
end
end
diff --git a/spec/models/ci/job_token/allowlist_spec.rb b/spec/models/ci/job_token/allowlist_spec.rb
index 3a2673c7c26..3d29a637d68 100644
--- a/spec/models/ci/job_token/allowlist_spec.rb
+++ b/spec/models/ci/job_token/allowlist_spec.rb
@@ -16,10 +16,12 @@ RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integratio
context 'when no projects are added to the scope' do
[:inbound, :outbound].each do |d|
- let(:direction) { d }
+ context "with #{d}" do
+ let(:direction) { d }
- it 'returns the project defining the scope' do
- expect(projects).to contain_exactly(source_project)
+ it 'returns the project defining the scope' do
+ expect(projects).to contain_exactly(source_project)
+ end
end
end
end
@@ -47,15 +49,17 @@ RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integratio
subject { allowlist.add!(added_project, user: user) }
[:inbound, :outbound].each do |d|
- let(:direction) { d }
+ context "with #{d}" do
+ let(:direction) { d }
- it 'adds the project' do
- subject
+ it 'adds the project' do
+ subject
- expect(allowlist.projects).to contain_exactly(source_project, added_project)
- expect(subject.added_by_id).to eq(user.id)
- expect(subject.source_project_id).to eq(source_project.id)
- expect(subject.target_project_id).to eq(added_project.id)
+ expect(allowlist.projects).to contain_exactly(source_project, added_project)
+ expect(subject.added_by_id).to eq(user.id)
+ expect(subject.source_project_id).to eq(source_project.id)
+ expect(subject.target_project_id).to eq(added_project.id)
+ end
end
end
end
diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb
index 9ae061a3702..7aa861a3dab 100644
--- a/spec/models/ci/job_token/scope_spec.rb
+++ b/spec/models/ci/job_token/scope_spec.rb
@@ -63,12 +63,14 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
subject { scope.add!(new_project, direction: direction, user: user) }
[:inbound, :outbound].each do |d|
- let(:direction) { d }
+ context "with #{d}" do
+ let(:direction) { d }
- it 'adds the project' do
- subject
+ it 'adds the project' do
+ subject
- expect(scope.send("#{direction}_projects")).to contain_exactly(current_project, new_project)
+ expect(scope.send("#{direction}_projects")).to contain_exactly(current_project, new_project)
+ end
end
end
@@ -160,13 +162,5 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
include_examples 'enforces outbound scope only'
end
-
- context 'when inbound scope flag disabled' do
- before do
- stub_feature_flags(ci_inbound_job_token_scope: false)
- end
-
- include_examples 'enforces outbound scope only'
- end
end
end
diff --git a/spec/models/ci/job_variable_spec.rb b/spec/models/ci/job_variable_spec.rb
index 0a65708160a..a56e6b6be43 100644
--- a/spec/models/ci/job_variable_spec.rb
+++ b/spec/models/ci/job_variable_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Ci::JobVariable, feature_category: :continuous_integration do
describe 'associations' do
let!(:job_variable) { create(:ci_job_variable) }
- it { is_expected.to belong_to(:job) }
+ it { is_expected.to belong_to(:job).class_name('Ci::Build').with_foreign_key(:job_id).inverse_of(:job_variables) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:job_id) }
end
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index 9b70f7c2839..c441be58edf 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:owner) }
- it { is_expected.to have_many(:pipelines) }
+ it { is_expected.to have_many(:pipelines).dependent(:nullify) }
it { is_expected.to have_many(:variables) }
it { is_expected.to respond_to(:ref) }
@@ -281,4 +281,19 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
let!(:model) { create(:ci_pipeline_schedule, project: parent) }
end
end
+
+ describe 'before_destroy' do
+ let_it_be_with_reload(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: ' 0 0 * * * ') }
+ let_it_be_with_reload(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) }
+
+ it 'nullifys associated pipelines' do
+ expect(pipeline_schedule).to receive(:nullify_dependent_associations_in_batches).and_call_original
+
+ result = pipeline_schedule.destroy
+
+ expect(result).to be_truthy
+ expect(pipeline.reload.pipeline_schedule).to be_nil
+ expect(described_class.find_by(id: pipeline_schedule.id)).to be_nil
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 61422978df7..5b67cbbc86b 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -19,32 +19,67 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
- it { is_expected.to belong_to(:auto_canceled_by) }
+ it { is_expected.to belong_to(:auto_canceled_by).class_name('Ci::Pipeline').inverse_of(:auto_canceled_pipelines) }
it { is_expected.to belong_to(:pipeline_schedule) }
it { is_expected.to belong_to(:merge_request) }
it { is_expected.to belong_to(:external_pull_request) }
it { is_expected.to have_many(:statuses) }
- it { is_expected.to have_many(:trigger_requests) }
+ it { is_expected.to have_many(:trigger_requests).with_foreign_key(:commit_id).inverse_of(:pipeline) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:builds) }
- it { is_expected.to have_many(:statuses_order_id_desc) }
+
+ it do
+ is_expected.to have_many(:statuses_order_id_desc)
+ .class_name('CommitStatus').with_foreign_key(:commit_id).inverse_of(:pipeline)
+ end
+
it { is_expected.to have_many(:bridges) }
it { is_expected.to have_many(:job_artifacts).through(:builds) }
it { is_expected.to have_many(:build_trace_chunks).through(:builds) }
- it { is_expected.to have_many(:auto_canceled_pipelines) }
- it { is_expected.to have_many(:auto_canceled_jobs) }
- it { is_expected.to have_many(:sourced_pipelines) }
+
it { is_expected.to have_many(:triggered_pipelines) }
it { is_expected.to have_many(:pipeline_artifacts) }
- it { is_expected.to have_one(:chat_data) }
+ it do
+ is_expected.to have_many(:failed_builds).class_name('Ci::Build')
+ .with_foreign_key(:commit_id).inverse_of(:pipeline)
+ end
+
+ it do
+ is_expected.to have_many(:cancelable_statuses).class_name('CommitStatus')
+ .with_foreign_key(:commit_id).inverse_of(:pipeline)
+ end
+
+ it do
+ is_expected.to have_many(:auto_canceled_pipelines).class_name('Ci::Pipeline')
+ .with_foreign_key(:auto_canceled_by_id).inverse_of(:auto_canceled_by)
+ end
+
+ it do
+ is_expected.to have_many(:auto_canceled_jobs).class_name('CommitStatus')
+ .with_foreign_key(:auto_canceled_by_id).inverse_of(:auto_canceled_by)
+ end
+
+ it do
+ is_expected.to have_many(:sourced_pipelines).class_name('Ci::Sources::Pipeline')
+ .with_foreign_key(:source_pipeline_id).inverse_of(:source_pipeline)
+ end
+
it { is_expected.to have_one(:source_pipeline) }
+ it { is_expected.to have_one(:chat_data) }
it { is_expected.to have_one(:triggered_by_pipeline) }
it { is_expected.to have_one(:source_job) }
it { is_expected.to have_one(:pipeline_config) }
it { is_expected.to have_one(:pipeline_metadata) }
+ it do
+ is_expected.to have_many(:daily_build_group_report_results).class_name('Ci::DailyBuildGroupReportResult')
+ .with_foreign_key(:last_pipeline_id).inverse_of(:last_pipeline)
+ end
+
+ it { is_expected.to have_many(:latest_builds_report_results).through(:latest_builds).source(:report_results) }
+
it { is_expected.to respond_to :git_author_name }
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :git_author_full_text }
@@ -409,6 +444,16 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
+ describe '.preload_pipeline_metadata' do
+ let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project, user: user, name: 'Chatops pipeline') }
+
+ it 'loads associations' do
+ result = described_class.preload_pipeline_metadata.first
+
+ expect(result.association(:pipeline_metadata).loaded?).to be(true)
+ end
+ end
+
describe '.ci_sources' do
subject { described_class.ci_sources }
@@ -462,11 +507,13 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
let!(:other_pipeline) { create(:ci_pipeline, project: project) }
before do
- create(:ci_sources_pipeline,
- source_job: create(:ci_build, pipeline: upstream_pipeline),
- source_project: project,
- pipeline: child_pipeline,
- project: project)
+ create(
+ :ci_sources_pipeline,
+ source_job: create(:ci_build, pipeline: upstream_pipeline),
+ source_project: project,
+ pipeline: child_pipeline,
+ project: project
+ )
end
it 'only returns pipelines outside pipeline family' do
@@ -485,11 +532,13 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
let!(:other_pipeline) { create(:ci_pipeline, project: project) }
before do
- create(:ci_sources_pipeline,
- source_job: create(:ci_build, pipeline: upstream_pipeline),
- source_project: project,
- pipeline: child_pipeline,
- project: project)
+ create(
+ :ci_sources_pipeline,
+ source_job: create(:ci_build, pipeline: upstream_pipeline),
+ source_project: project,
+ pipeline: child_pipeline,
+ project: project
+ )
end
it 'only returns older pipelines outside pipeline family' do
@@ -497,6 +546,17 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
+ describe '.order_id_desc' do
+ subject(:pipelines_ordered_by_id) { described_class.order_id_desc }
+
+ let(:older_pipeline) { create(:ci_pipeline, id: 99, project: project) }
+ let(:newest_pipeline) { create(:ci_pipeline, id: 100, project: project) }
+
+ it 'only returns the pipelines ordered by id' do
+ expect(pipelines_ordered_by_id).to eq([newest_pipeline, older_pipeline])
+ end
+ end
+
describe '.jobs_count_in_alive_pipelines' do
before do
::Ci::HasStatus::ALIVE_STATUSES.each do |status|
@@ -872,6 +932,26 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
expect(pipeline).to be_valid
end
end
+
+ context 'when source is unknown' do
+ subject(:pipeline) { create(:ci_empty_pipeline, :created) }
+
+ let(:attr) { :source }
+ let(:attr_value) { :unknown }
+
+ it_behaves_like 'having enum with nil value'
+ end
+ end
+
+ describe '#config_source' do
+ context 'when source is unknown' do
+ subject(:pipeline) { create(:ci_empty_pipeline, :created) }
+
+ let(:attr) { :config_source }
+ let(:attr_value) { :unknown_source }
+
+ it_behaves_like 'having enum with nil value'
+ end
end
describe '#block' do
@@ -1131,29 +1211,41 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
describe 'legacy stages' do
before do
- create(:commit_status, pipeline: pipeline,
- stage: 'build',
- name: 'linux',
- stage_idx: 0,
- status: 'success')
-
- create(:commit_status, pipeline: pipeline,
- stage: 'build',
- name: 'mac',
- stage_idx: 0,
- status: 'failed')
-
- create(:commit_status, pipeline: pipeline,
- stage: 'deploy',
- name: 'staging',
- stage_idx: 2,
- status: 'running')
-
- create(:commit_status, pipeline: pipeline,
- stage: 'test',
- name: 'rspec',
- stage_idx: 1,
- status: 'success')
+ create(
+ :commit_status,
+ pipeline: pipeline,
+ stage: 'build',
+ name: 'linux',
+ stage_idx: 0,
+ status: 'success'
+ )
+
+ create(
+ :commit_status,
+ pipeline: pipeline,
+ stage: 'build',
+ name: 'mac',
+ stage_idx: 0,
+ status: 'failed'
+ )
+
+ create(
+ :commit_status,
+ pipeline: pipeline,
+ stage: 'deploy',
+ name: 'staging',
+ stage_idx: 2,
+ status: 'running'
+ )
+
+ create(
+ :commit_status,
+ pipeline: pipeline,
+ stage: 'test',
+ name: 'rspec',
+ stage_idx: 1,
+ status: 'success'
+ )
end
describe '#stages_count' do
@@ -1604,8 +1696,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
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)
+ create(:ci_sources_pipeline, pipeline: upstream_pipeline, source_job: upstream_bridge)
end
context 'when the downstream pipeline first fails then retries and succeeds' do
@@ -1661,13 +1752,163 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
+ describe 'merge status subscription trigger' do
+ shared_examples 'state transition not triggering GraphQL subscription mergeRequestMergeStatusUpdated' do
+ context 'when state transitions to running' do
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.run }
+ end
+ end
+
+ context 'when state transitions to success' do
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.succeed }
+ end
+ end
+
+ context 'when state transitions to failed' do
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.drop }
+ end
+ end
+
+ context 'when state transitions to canceled' do
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.cancel }
+ end
+ end
+
+ context 'when state transitions to skipped' do
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.skip }
+ end
+ end
+ end
+
+ shared_examples 'state transition triggering GraphQL subscription mergeRequestMergeStatusUpdated' do
+ context 'when state transitions to running' do
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.run }
+ end
+ end
+
+ context 'when state transitions to success' do
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.succeed }
+ end
+ end
+
+ context 'when state transitions to failed' do
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.drop }
+ end
+ end
+
+ context 'when state transitions to canceled' do
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.cancel }
+ end
+ end
+
+ context 'when state transitions to skipped' do
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.skip }
+ end
+ end
+
+ context 'when only_allow_merge_if_pipeline_succeeds? returns false' do
+ let(:only_allow_merge_if_pipeline_succeeds?) { false }
+
+ it_behaves_like 'state transition not triggering GraphQL subscription mergeRequestMergeStatusUpdated'
+ end
+
+ context 'when pipeline_trigger_merge_status feature flag is disabled' do
+ before do
+ stub_feature_flags(pipeline_trigger_merge_status: false)
+ end
+
+ it_behaves_like 'state transition not triggering GraphQL subscription mergeRequestMergeStatusUpdated'
+ end
+ end
+
+ context 'when pipeline has merge requests' do
+ let(:merge_request) do
+ create(
+ :merge_request,
+ :simple,
+ source_project: project,
+ target_project: project
+ )
+ end
+
+ let(:only_allow_merge_if_pipeline_succeeds?) { true }
+
+ before do
+ allow(project)
+ .to receive(:only_allow_merge_if_pipeline_succeeds?)
+ .and_return(only_allow_merge_if_pipeline_succeeds?)
+ end
+
+ context 'when for a specific merge request' do
+ let(:pipeline) do
+ create(
+ :ci_pipeline,
+ project: project,
+ merge_request: merge_request
+ )
+ end
+
+ it_behaves_like 'state transition triggering GraphQL subscription mergeRequestMergeStatusUpdated'
+
+ context 'when pipeline is a child' do
+ let(:parent_pipeline) do
+ create(
+ :ci_pipeline,
+ project: project,
+ merge_request: merge_request
+ )
+ end
+
+ let(:pipeline) do
+ create(
+ :ci_pipeline,
+ child_of: parent_pipeline,
+ merge_request: merge_request
+ )
+ end
+
+ it_behaves_like 'state transition not triggering GraphQL subscription mergeRequestMergeStatusUpdated'
+ end
+ end
+
+ context 'when for merge requests matching the source branch and SHA' do
+ let(:pipeline) do
+ create(
+ :ci_pipeline,
+ project: project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha
+ )
+ end
+
+ it_behaves_like 'state transition triggering GraphQL subscription mergeRequestMergeStatusUpdated'
+ end
+ end
+
+ context 'when pipeline has no merge requests' do
+ it_behaves_like 'state transition not triggering GraphQL subscription mergeRequestMergeStatusUpdated'
+ end
+ end
+
def create_build(name, *traits, queued_at: current, started_from: 0, **opts)
- create(:ci_build, *traits,
- name: name,
- pipeline: pipeline,
- queued_at: queued_at,
- started_at: queued_at + started_from,
- **opts)
+ create(
+ :ci_build, *traits,
+ name: name,
+ pipeline: pipeline,
+ queued_at: queued_at,
+ started_at: queued_at + started_from,
+ **opts
+ )
end
end
@@ -1715,9 +1956,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
let(:pipeline) { build(:ci_pipeline, merge_request: merge_request) }
let(:merge_request) do
- create(:merge_request, :simple,
- source_project: project,
- target_project: project)
+ create(:merge_request, :simple, source_project: project, target_project: project)
end
it 'returns false' do
@@ -1758,17 +1997,17 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
context 'when ref is merge request' do
let(:pipeline) do
- create(:ci_pipeline,
- source: :merge_request_event,
- merge_request: merge_request)
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request)
end
let(:merge_request) do
- create(:merge_request,
- source_project: project,
- source_branch: 'feature',
- target_project: project,
- target_branch: 'master')
+ create(
+ :merge_request,
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master'
+ )
end
it 'returns branch ref' do
@@ -1812,35 +2051,63 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
context 'with non-empty project' do
let(:pipeline) do
- create(:ci_pipeline,
- ref: project.default_branch,
- sha: project.commit.sha)
+ create(
+ :ci_pipeline,
+ project: project,
+ ref: project.default_branch,
+ sha: project.commit.sha
+ )
end
describe '#lazy_ref_commit' do
let(:another) do
- create(:ci_pipeline,
- ref: 'feature',
- sha: project.commit('feature').sha)
+ create(
+ :ci_pipeline,
+ project: project,
+ ref: 'feature',
+ sha: project.commit('feature').sha
+ )
end
let(:unicode) do
- create(:ci_pipeline,
- ref: 'ü/unicode/multi-byte')
+ create(
+ :ci_pipeline,
+ project: project,
+ ref: 'ü/unicode/multi-byte'
+ )
+ end
+
+ let(:in_another_project) do
+ other_project = create(:project, :repository)
+ create(
+ :ci_pipeline,
+ project: other_project,
+ ref: other_project.default_branch,
+ sha: other_project.commit.sha
+ )
end
- it 'returns the latest commit for a ref lazily' do
+ it 'returns the latest commit for a ref lazily', :aggregate_failures do
expect(project.repository)
.to receive(:list_commits_by_ref_name).once
.and_call_original
+ requests_before = Gitlab::GitalyClient.get_request_count
pipeline.lazy_ref_commit
another.lazy_ref_commit
unicode.lazy_ref_commit
+ in_another_project.lazy_ref_commit
+ requests_after = Gitlab::GitalyClient.get_request_count
+
+ expect(requests_after - requests_before).to eq(0)
expect(pipeline.lazy_ref_commit.id).to eq pipeline.sha
expect(another.lazy_ref_commit.id).to eq another.sha
- expect(unicode.lazy_ref_commit).to be_nil
+ expect(unicode.lazy_ref_commit.itself).to be_nil
+ expect(in_another_project.lazy_ref_commit.id).to eq in_another_project.sha
+
+ expect(pipeline.lazy_ref_commit.repository.container).to eq project
+ expect(in_another_project.lazy_ref_commit.repository.container).to eq in_another_project.project
end
end
@@ -1969,9 +2236,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
let(:merge_request) do
- create(:merge_request, :simple,
- source_project: project,
- target_project: project)
+ create(:merge_request, :simple, source_project: project, target_project: project)
end
it 'returns merge request modified paths' do
@@ -1996,8 +2261,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
describe '#modified_paths_since' do
let(:project) do
- create(:project, :custom_repo,
- files: { 'file1.txt' => 'file 1' })
+ create(:project, :custom_repo, files: { 'file1.txt' => 'file 1' })
end
let(:user) { project.owner }
@@ -3270,19 +3534,23 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
let(:target_branch) { 'master' }
let!(:pipeline) do
- create(:ci_pipeline,
- source: :merge_request_event,
- project: pipeline_project,
- ref: source_branch,
- merge_request: merge_request)
+ create(
+ :ci_pipeline,
+ source: :merge_request_event,
+ project: pipeline_project,
+ ref: source_branch,
+ merge_request: merge_request
+ )
end
let(:merge_request) do
- create(:merge_request,
- source_project: pipeline_project,
- source_branch: source_branch,
- target_project: project,
- target_branch: target_branch)
+ create(
+ :merge_request,
+ source_project: pipeline_project,
+ source_branch: source_branch,
+ target_project: project,
+ target_branch: target_branch
+ )
end
it 'returns an associated merge request' do
@@ -3293,19 +3561,23 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
let(:target_branch_2) { 'merge-test' }
let!(:pipeline_2) do
- create(:ci_pipeline,
- source: :merge_request_event,
- project: pipeline_project,
- ref: source_branch,
- merge_request: merge_request_2)
+ create(
+ :ci_pipeline,
+ source: :merge_request_event,
+ project: pipeline_project,
+ ref: source_branch,
+ merge_request: merge_request_2
+ )
end
let(:merge_request_2) do
- create(:merge_request,
- source_project: pipeline_project,
- source_branch: source_branch,
- target_project: project,
- target_branch: target_branch_2)
+ create(
+ :merge_request,
+ source_project: pipeline_project,
+ source_branch: source_branch,
+ target_project: project,
+ target_branch: target_branch_2
+ )
end
it 'does not return an associated merge request' do
@@ -3701,10 +3973,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
let(:project) { create(:project, :repository, namespace: namespace) }
let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- sha: project.commit('master').sha,
- user: project.first_owner)
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: project.commit('master').sha,
+ user: project.first_owner
+ )
end
before do
@@ -4488,10 +4762,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
let(:stage_name) { 'test' }
let(:stage) do
- create(:ci_stage,
- pipeline: pipeline,
- project: pipeline.project,
- name: 'test')
+ create(:ci_stage, pipeline: pipeline, project: pipeline.project, name: 'test')
end
before do
@@ -5208,11 +5479,11 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
describe '#cluster_agent_authorizations' do
let(:pipeline) { create(:ci_empty_pipeline, :created) }
- let(:authorization) { instance_double(Clusters::Agents::GroupAuthorization) }
+ let(:authorization) { instance_double(Clusters::Agents::Authorizations::CiAccess::GroupAuthorization) }
let(:finder) { double(execute: [authorization]) }
it 'retrieves authorization records from the finder and caches the result' do
- expect(Clusters::AgentAuthorizationsFinder).to receive(:new).once
+ expect(Clusters::Agents::Authorizations::CiAccess::Finder).to receive(:new).once
.with(pipeline.project)
.and_return(finder)
diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb
index db22d8f3a6c..34a56162dd9 100644
--- a/spec/models/ci/processable_spec.rb
+++ b/spec/models/ci/processable_spec.rb
@@ -43,11 +43,13 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
let_it_be_with_refind(:processable) do
- create(:ci_build, :failed, :picked, :expired, :erased, :queued, :coverage, :tags,
- :allowed_to_fail, :on_tag, :triggered, :teardown_environment, :resource_group,
- description: 'my-job', stage: 'test', stage_id: stage.id,
- pipeline: pipeline, auto_canceled_by: another_pipeline,
- scheduled_at: 10.seconds.since)
+ create(
+ :ci_build, :failed, :picked, :expired, :erased, :queued, :coverage, :tags,
+ :allowed_to_fail, :on_tag, :triggered, :teardown_environment, :resource_group,
+ description: 'my-job', stage: 'test', stage_id: stage.id,
+ pipeline: pipeline, auto_canceled_by: another_pipeline,
+ scheduled_at: 10.seconds.since
+ )
end
let_it_be(:internal_job_variable) { create(:ci_job_variable, job: processable) }
@@ -83,7 +85,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
sourced_pipelines sourced_pipeline artifacts_file_store artifacts_metadata_store
- metadata runner_machine_id runner_machine runner_session trace_chunks upstream_pipeline_id
+ metadata runner_manager_build runner_manager runner_session trace_chunks upstream_pipeline_id
artifacts_file artifacts_metadata artifacts_size commands
resource resource_group_id processed security_scans author
pipeline_id report_results pending_state pages_deployments
@@ -95,8 +97,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
before_all do
# Create artifacts to check that the associations are rejected when cloning
Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS.each do |file_type, file_format|
- create(:ci_job_artifact, file_format,
- file_type: file_type, job: processable, expire_at: processable.artifacts_expire_at)
+ create(:ci_job_artifact, file_format, file_type: file_type, job: processable, expire_at: processable.artifacts_expire_at)
end
create(:ci_job_variable, :dotenv_source, job: processable)
@@ -193,8 +194,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
context 'when it has a deployment' do
let!(:processable) do
- create(:ci_build, :with_deployment, :deploy_to_production,
- pipeline: pipeline, stage_id: stage.id, project: project)
+ create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline, stage_id: stage.id, project: project)
end
it 'persists the expanded environment name' do
diff --git a/spec/models/ci/ref_spec.rb b/spec/models/ci/ref_spec.rb
index ffbda4b459f..eab5a40bc30 100644
--- a/spec/models/ci/ref_spec.rb
+++ b/spec/models/ci/ref_spec.rb
@@ -105,8 +105,11 @@ RSpec.describe Ci::Ref do
context 'when pipeline is a detached merge request pipeline' do
let(:merge_request) do
- create(:merge_request, target_project: project, target_branch: 'master',
- source_project: project, source_branch: 'feature')
+ create(
+ :merge_request,
+ target_project: project, target_branch: 'master',
+ source_project: project, source_branch: 'feature'
+ )
end
let!(:pipeline) do
diff --git a/spec/models/ci/resource_group_spec.rb b/spec/models/ci/resource_group_spec.rb
index 01acf5194f0..6d518d5c874 100644
--- a/spec/models/ci/resource_group_spec.rb
+++ b/spec/models/ci/resource_group_spec.rb
@@ -118,7 +118,7 @@ RSpec.describe Ci::ResourceGroup do
let!(:resource_group) { create(:ci_resource_group, process_mode: process_mode, project: project) }
- Ci::HasStatus::STATUSES_ENUM.keys.each do |status|
+ Ci::HasStatus::STATUSES_ENUM.keys.each do |status| # rubocop:diable RSpec/UselessDynamicDefinition
let!("build_1_#{status}") { create(:ci_build, pipeline: pipeline_1, status: status, resource_group: resource_group) }
let!("build_2_#{status}") { create(:ci_build, pipeline: pipeline_2, status: status, resource_group: resource_group) }
end
@@ -165,4 +165,23 @@ RSpec.describe Ci::ResourceGroup do
end
end
end
+
+ describe '#current_processable' do
+ subject { resource_group.current_processable }
+
+ let(:build) { create(:ci_build) }
+ let(:resource_group) { create(:ci_resource_group) }
+
+ context 'when resource is retained by a build' do
+ before do
+ resource_group.assign_resource_to(build)
+ end
+
+ it { is_expected.to eq(build) }
+ end
+
+ context 'when resource is not retained by a build' do
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/models/ci/runner_machine_spec.rb b/spec/models/ci/runner_machine_spec.rb
deleted file mode 100644
index d0979d8a485..00000000000
--- a/spec/models/ci/runner_machine_spec.rb
+++ /dev/null
@@ -1,197 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model do
- it_behaves_like 'having unique enum values'
-
- it { is_expected.to belong_to(:runner) }
- it { is_expected.to belong_to(:runner_version).with_foreign_key(:version) }
- it { is_expected.to have_many(:build_metadata) }
- it { is_expected.to have_many(:builds).through(:build_metadata) }
-
- describe 'validation' do
- it { is_expected.to validate_presence_of(:runner) }
- it { is_expected.to validate_presence_of(:system_xid) }
- it { is_expected.to validate_length_of(:system_xid).is_at_most(64) }
- it { is_expected.to validate_length_of(:version).is_at_most(2048) }
- it { is_expected.to validate_length_of(:revision).is_at_most(255) }
- it { is_expected.to validate_length_of(:platform).is_at_most(255) }
- it { is_expected.to validate_length_of(:architecture).is_at_most(255) }
- it { is_expected.to validate_length_of(:ip_address).is_at_most(1024) }
-
- context 'when runner has config' do
- it 'is valid' do
- runner_machine = build(:ci_runner_machine, config: { gpus: "all" })
-
- expect(runner_machine).to be_valid
- end
- end
-
- context 'when runner has an invalid config' do
- it 'is invalid' do
- runner_machine = build(:ci_runner_machine, config: { test: 1 })
-
- expect(runner_machine).not_to be_valid
- end
- end
- end
-
- describe '.stale', :freeze_time do
- subject { described_class.stale.ids }
-
- let!(:runner_machine1) { create(:ci_runner_machine, :stale) }
- let!(:runner_machine2) { create(:ci_runner_machine, :stale, contacted_at: nil) }
- let!(:runner_machine3) { create(:ci_runner_machine, created_at: 6.months.ago, contacted_at: Time.current) }
- let!(:runner_machine4) { create(:ci_runner_machine, created_at: 5.days.ago) }
- let!(:runner_machine5) do
- create(:ci_runner_machine, created_at: (7.days - 1.second).ago, contacted_at: (7.days - 1.second).ago)
- end
-
- it 'returns stale runner machines' do
- is_expected.to match_array([runner_machine1.id, runner_machine2.id])
- end
- end
-
- describe '#heartbeat', :freeze_time do
- let(:runner_machine) { create(:ci_runner_machine) }
- let(:executor) { 'shell' }
- let(:version) { '15.0.1' }
- let(:values) do
- {
- ip_address: '8.8.8.8',
- architecture: '18-bit',
- config: { gpus: "all" },
- executor: executor,
- version: version
- }
- end
-
- subject(:heartbeat) do
- runner_machine.heartbeat(values)
- end
-
- context 'when database was updated recently' do
- before do
- runner_machine.contacted_at = Time.current
- end
-
- it 'schedules version update' do
- expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version).once
-
- heartbeat
-
- expect(runner_machine.runner_version).to be_nil
- end
-
- it 'updates cache' do
- expect_redis_update
-
- heartbeat
- end
-
- context 'with only ip_address specified' do
- let(:values) do
- { ip_address: '1.1.1.1' }
- end
-
- it 'updates only ip_address' do
- attrs = Gitlab::Json.dump(ip_address: '1.1.1.1', contacted_at: Time.current)
-
- Gitlab::Redis::Cache.with do |redis|
- redis_key = runner_machine.send(:cache_attribute_key)
- expect(redis).to receive(:set).with(redis_key, attrs, any_args)
- end
-
- heartbeat
- end
- end
- end
-
- context 'when database was not updated recently' do
- before do
- runner_machine.contacted_at = 2.hours.ago
-
- allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version).once
- end
-
- context 'with invalid runner_machine' do
- before do
- runner_machine.runner = nil
- end
-
- it 'still updates redis cache and database' do
- expect(runner_machine).to be_invalid
-
- expect_redis_update
- does_db_update
-
- expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
- .with(version).once
- end
- end
-
- context 'with unchanged runner_machine version' do
- let(:runner_machine) { create(:ci_runner_machine, version: version) }
-
- it 'does not schedule ci_runner_versions update' do
- heartbeat
-
- expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
- end
- end
-
- it 'updates redis cache and database' do
- expect_redis_update
- does_db_update
-
- expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
- .with(version).once
- end
-
- Ci::Runner::EXECUTOR_NAME_TO_TYPES.each_key do |executor|
- context "with #{executor} executor" do
- let(:executor) { executor }
-
- it 'updates with expected executor type' do
- expect_redis_update
-
- heartbeat
-
- expect(runner_machine.reload.read_attribute(:executor_type)).to eq(expected_executor_type)
- end
-
- def expected_executor_type
- executor.gsub(/[+-]/, '_')
- end
- end
- end
-
- context "with an unknown executor type" do
- let(:executor) { 'some-unknown-type' }
-
- it 'updates with unknown executor type' do
- expect_redis_update
-
- heartbeat
-
- expect(runner_machine.reload.read_attribute(:executor_type)).to eq('unknown')
- end
- end
- end
-
- def expect_redis_update
- Gitlab::Redis::Cache.with do |redis|
- redis_key = runner_machine.send(:cache_attribute_key)
- expect(redis).to receive(:set).with(redis_key, anything, any_args).and_call_original
- end
- end
-
- def does_db_update
- expect { heartbeat }.to change { runner_machine.reload.read_attribute(:contacted_at) }
- .and change { runner_machine.reload.read_attribute(:architecture) }
- .and change { runner_machine.reload.read_attribute(:config) }
- .and change { runner_machine.reload.read_attribute(:executor_type) }
- end
- end
-end
diff --git a/spec/models/ci/runner_manager_build_spec.rb b/spec/models/ci/runner_manager_build_spec.rb
new file mode 100644
index 00000000000..3a381313b76
--- /dev/null
+++ b/spec/models/ci/runner_manager_build_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::RunnerManagerBuild, model: true, feature_category: :runner_fleet do
+ let_it_be(:runner) { create(:ci_runner) }
+ let_it_be(:runner_manager) { create(:ci_runner_machine, runner: runner) }
+ let_it_be(:build) { create(:ci_build, runner_manager: runner_manager) }
+
+ it { is_expected.to belong_to(:build) }
+ it { is_expected.to belong_to(:runner_manager) }
+
+ describe 'partitioning' do
+ context 'with build' do
+ let(:build) { FactoryBot.build(:ci_build, partition_id: ci_testing_partition_id) }
+ let(:runner_manager_build) { FactoryBot.build(:ci_runner_machine_build, build: build) }
+
+ it 'sets partition_id to the current partition value' do
+ expect { runner_manager_build.valid? }.to change { runner_manager_build.partition_id }
+ .to(ci_testing_partition_id)
+ end
+
+ context 'when it is already set' do
+ let(:runner_manager_build) { FactoryBot.build(:ci_runner_machine_build, partition_id: 125) }
+
+ it 'does not change the partition_id value' do
+ expect { runner_manager_build.valid? }.not_to change { runner_manager_build.partition_id }
+ end
+ end
+ end
+
+ context 'without build' do
+ let(:runner_manager_build) { FactoryBot.build(:ci_runner_machine_build, build: nil) }
+
+ it { is_expected.to validate_presence_of(:partition_id) }
+
+ it 'does not change the partition_id value' do
+ expect { runner_manager_build.valid? }.not_to change { runner_manager_build.partition_id }
+ end
+ end
+ end
+
+ describe 'ci_sliding_list partitioning' do
+ let(:connection) { described_class.connection }
+ let(:partition_manager) { Gitlab::Database::Partitioning::PartitionManager.new(described_class) }
+
+ let(:partitioning_strategy) { described_class.partitioning_strategy }
+
+ it { expect(partitioning_strategy.missing_partitions).to be_empty }
+ it { expect(partitioning_strategy.extra_partitions).to be_empty }
+ it { expect(partitioning_strategy.current_partitions).to include partitioning_strategy.initial_partition }
+ it { expect(partitioning_strategy.active_partition).to be_present }
+ end
+
+ context 'loose foreign key on p_ci_runner_manager_builds.runner_manager_id' do # rubocop:disable RSpec/ContextWording
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:ci_runner_machine) }
+ let!(:model) { create(:ci_runner_machine_build, runner_manager: parent) }
+ end
+ end
+
+ describe '.for_build' do
+ subject(:for_build) { described_class.for_build(build_id) }
+
+ context 'with valid build_id' do
+ let(:build_id) { build.id }
+
+ it { is_expected.to contain_exactly(described_class.find_by_build_id(build_id)) }
+ end
+
+ context 'with valid build_ids' do
+ let(:build2) { create(:ci_build, runner_manager: runner_manager) }
+ let(:build_id) { [build, build2] }
+
+ it { is_expected.to eq(described_class.where(build_id: build_id)) }
+ end
+
+ context 'with non-existeng build_id' do
+ let(:build_id) { non_existing_record_id }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe '.pluck_runner_manager_id_and_build_id' do
+ subject { scope.pluck_build_id_and_runner_manager_id }
+
+ context 'with default scope' do
+ let(:scope) { described_class }
+
+ it { is_expected.to eq({ build.id => runner_manager.id }) }
+ end
+
+ context 'with scope excluding build' do
+ let(:scope) { described_class.where(build_id: non_existing_record_id) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+end
diff --git a/spec/models/ci/runner_manager_spec.rb b/spec/models/ci/runner_manager_spec.rb
new file mode 100644
index 00000000000..d69c9ef845e
--- /dev/null
+++ b/spec/models/ci/runner_manager_spec.rb
@@ -0,0 +1,291 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::RunnerManager, feature_category: :runner_fleet, type: :model do
+ it_behaves_like 'having unique enum values'
+
+ it_behaves_like 'it has loose foreign keys' do
+ let(:factory_name) { :ci_runner_machine }
+ end
+
+ it { is_expected.to belong_to(:runner) }
+ it { is_expected.to belong_to(:runner_version).with_foreign_key(:version) }
+ it { is_expected.to have_many(:runner_manager_builds) }
+ it { is_expected.to have_many(:builds).through(:runner_manager_builds) }
+
+ describe 'validation' do
+ it { is_expected.to validate_presence_of(:runner) }
+ it { is_expected.to validate_presence_of(:system_xid) }
+ it { is_expected.to validate_length_of(:system_xid).is_at_most(64) }
+ it { is_expected.to validate_length_of(:version).is_at_most(2048) }
+ it { is_expected.to validate_length_of(:revision).is_at_most(255) }
+ it { is_expected.to validate_length_of(:platform).is_at_most(255) }
+ it { is_expected.to validate_length_of(:architecture).is_at_most(255) }
+ it { is_expected.to validate_length_of(:ip_address).is_at_most(1024) }
+
+ context 'when runner has config' do
+ it 'is valid' do
+ runner_manager = build(:ci_runner_machine, config: { gpus: "all" })
+
+ expect(runner_manager).to be_valid
+ end
+ end
+
+ context 'when runner has an invalid config' do
+ it 'is invalid' do
+ runner_manager = build(:ci_runner_machine, config: { test: 1 })
+
+ expect(runner_manager).not_to be_valid
+ end
+ end
+ end
+
+ describe '.stale', :freeze_time do
+ subject { described_class.stale.ids }
+
+ let!(:runner_manager1) { create(:ci_runner_machine, :stale) }
+ let!(:runner_manager2) { create(:ci_runner_machine, :stale, contacted_at: nil) }
+ let!(:runner_manager3) { create(:ci_runner_machine, created_at: 6.months.ago, contacted_at: Time.current) }
+ let!(:runner_manager4) { create(:ci_runner_machine, created_at: 5.days.ago) }
+ let!(:runner_manager5) do
+ create(:ci_runner_machine, created_at: (7.days - 1.second).ago, contacted_at: (7.days - 1.second).ago)
+ end
+
+ it 'returns stale runner managers' do
+ is_expected.to match_array([runner_manager1.id, runner_manager2.id])
+ end
+ end
+
+ describe '.online_contact_time_deadline', :freeze_time do
+ subject { described_class.online_contact_time_deadline }
+
+ it { is_expected.to eq(2.hours.ago) }
+ end
+
+ describe '.stale_deadline', :freeze_time do
+ subject { described_class.stale_deadline }
+
+ it { is_expected.to eq(7.days.ago) }
+ end
+
+ describe '#status', :freeze_time do
+ let(:runner_manager) { build(:ci_runner_machine, created_at: 8.days.ago) }
+
+ subject { runner_manager.status }
+
+ context 'if never connected' do
+ before do
+ runner_manager.contacted_at = nil
+ end
+
+ it { is_expected.to eq(:stale) }
+
+ context 'if created recently' do
+ before do
+ runner_manager.created_at = 1.day.ago
+ end
+
+ it { is_expected.to eq(:never_contacted) }
+ end
+ end
+
+ context 'if contacted 1s ago' do
+ before do
+ runner_manager.contacted_at = 1.second.ago
+ end
+
+ it { is_expected.to eq(:online) }
+ end
+
+ context 'if contacted recently' do
+ before do
+ runner_manager.contacted_at = 2.hours.ago
+ end
+
+ it { is_expected.to eq(:offline) }
+ end
+
+ context 'if contacted long time ago' do
+ before do
+ runner_manager.contacted_at = 7.days.ago
+ end
+
+ it { is_expected.to eq(:stale) }
+ end
+ end
+
+ describe '#heartbeat', :freeze_time do
+ let(:runner_manager) { create(:ci_runner_machine, version: '15.0.0') }
+ let(:executor) { 'shell' }
+ let(:values) do
+ {
+ ip_address: '8.8.8.8',
+ architecture: '18-bit',
+ config: { gpus: "all" },
+ executor: executor,
+ version: version
+ }
+ end
+
+ subject(:heartbeat) do
+ runner_manager.heartbeat(values)
+ end
+
+ context 'when database was updated recently' do
+ before do
+ runner_manager.contacted_at = Time.current
+ end
+
+ context 'when version is changed' do
+ let(:version) { '15.0.1' }
+
+ before do
+ allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version)
+ end
+
+ it 'schedules version information update' do
+ heartbeat
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).with(version).once
+ end
+
+ it 'updates cache' do
+ expect_redis_update
+
+ heartbeat
+
+ expect(runner_manager.runner_version).to be_nil
+ end
+
+ context 'when fetching runner releases is disabled' do
+ before do
+ stub_application_setting(update_runner_versions_enabled: false)
+ end
+
+ it 'does not schedule version information update' do
+ heartbeat
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
+ end
+ end
+ end
+
+ context 'with only ip_address specified' do
+ let(:values) do
+ { ip_address: '1.1.1.1' }
+ end
+
+ it 'updates only ip_address' do
+ expect_redis_update(values.merge(contacted_at: Time.current))
+
+ heartbeat
+ end
+
+ context 'with new version having been cached' do
+ let(:version) { '15.0.1' }
+
+ before do
+ runner_manager.cache_attributes(version: version)
+ end
+
+ it 'does not lose cached version value' do
+ expect { heartbeat }.not_to change { runner_manager.version }.from(version)
+ end
+ end
+ end
+ end
+
+ context 'when database was not updated recently' do
+ before do
+ runner_manager.contacted_at = 2.hours.ago
+
+ allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version)
+ end
+
+ context 'when version is changed' do
+ let(:version) { '15.0.1' }
+
+ context 'with invalid runner_manager' do
+ before do
+ runner_manager.runner = nil
+ end
+
+ it 'still updates redis cache and database' do
+ expect(runner_manager).to be_invalid
+
+ expect_redis_update
+ does_db_update
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
+ .with(version).once
+ end
+ end
+
+ it 'updates redis cache and database' do
+ expect_redis_update
+ does_db_update
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
+ .with(version).once
+ end
+ end
+
+ context 'with unchanged runner_manager version' do
+ let(:version) { runner_manager.version }
+
+ it 'does not schedule ci_runner_versions update' do
+ heartbeat
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
+ end
+
+ Ci::Runner::EXECUTOR_NAME_TO_TYPES.each_key do |executor|
+ context "with #{executor} executor" do
+ let(:executor) { executor }
+
+ it 'updates with expected executor type' do
+ expect_redis_update
+
+ heartbeat
+
+ expect(runner_manager.reload.read_attribute(:executor_type)).to eq(expected_executor_type)
+ end
+
+ def expected_executor_type
+ executor.gsub(/[+-]/, '_')
+ end
+ end
+ end
+
+ context 'with an unknown executor type' do
+ let(:executor) { 'some-unknown-type' }
+
+ it 'updates with unknown executor type' do
+ expect_redis_update
+
+ heartbeat
+
+ expect(runner_manager.reload.read_attribute(:executor_type)).to eq('unknown')
+ end
+ end
+ end
+ end
+
+ def expect_redis_update(values = anything)
+ values_json = values == anything ? anything : Gitlab::Json.dump(values)
+
+ Gitlab::Redis::Cache.with do |redis|
+ redis_key = runner_manager.send(:cache_attribute_key)
+ expect(redis).to receive(:set).with(redis_key, values_json, any_args).and_call_original
+ end
+ end
+
+ def does_db_update
+ expect { heartbeat }.to change { runner_manager.reload.read_attribute(:contacted_at) }
+ .and change { runner_manager.reload.read_attribute(:architecture) }
+ .and change { runner_manager.reload.read_attribute(:config) }
+ .and change { runner_manager.reload.read_attribute(:executor_type) }
+ end
+ end
+end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 01d5fe7f90b..d202fef0ed0 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -273,7 +273,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
- shared_examples '.belonging_to_parent_group_of_project' do
+ shared_examples '.belonging_to_parent_groups_of_project' do
let_it_be(:group1) { create(:group) }
let_it_be(:project1) { create(:project, group: group1) }
let_it_be(:runner1) { create(:ci_runner, :group, groups: [group1]) }
@@ -284,7 +284,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
let(:project_id) { project1.id }
- subject(:result) { described_class.belonging_to_parent_group_of_project(project_id) }
+ subject(:result) { described_class.belonging_to_parent_groups_of_project(project_id) }
it 'returns the group runner' do
expect(result).to contain_exactly(runner1)
@@ -310,7 +310,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
context 'when use_traversal_ids* are enabled' do
- it_behaves_like '.belonging_to_parent_group_of_project'
+ it_behaves_like '.belonging_to_parent_groups_of_project'
end
context 'when use_traversal_ids* are disabled' do
@@ -322,7 +322,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
)
end
- it_behaves_like '.belonging_to_parent_group_of_project'
+ it_behaves_like '.belonging_to_parent_groups_of_project'
end
context 'with instance runners sharing enabled' do
@@ -541,9 +541,9 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
describe '.stale', :freeze_time do
subject { described_class.stale }
- let!(:runner1) { create(:ci_runner, :instance, created_at: 4.months.ago, contacted_at: 3.months.ago + 10.seconds) }
- let!(:runner2) { create(:ci_runner, :instance, created_at: 4.months.ago, contacted_at: 3.months.ago - 1.second) }
- let!(:runner3) { create(:ci_runner, :instance, created_at: 3.months.ago - 1.second, contacted_at: nil) }
+ let!(:runner1) { create(:ci_runner, :instance, created_at: 4.months.ago, contacted_at: 3.months.ago + 1.second) }
+ let!(:runner2) { create(:ci_runner, :instance, created_at: 4.months.ago, contacted_at: 3.months.ago) }
+ let!(:runner3) { create(:ci_runner, :instance, created_at: 3.months.ago, contacted_at: nil) }
let!(:runner4) { create(:ci_runner, :instance, created_at: 2.months.ago, contacted_at: nil) }
it 'returns stale runners' do
@@ -551,7 +551,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
- describe '#stale?', :clean_gitlab_redis_cache do
+ describe '#stale?', :clean_gitlab_redis_cache, :freeze_time do
let(:runner) { build(:ci_runner, :instance) }
subject { runner.stale? }
@@ -570,11 +570,11 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
using RSpec::Parameterized::TableSyntax
where(:created_at, :contacted_at, :expected_stale?) do
- nil | nil | false
- 3.months.ago - 1.second | 3.months.ago - 0.001.seconds | true
- 3.months.ago - 1.second | 3.months.ago + 1.hour | false
- 3.months.ago - 1.second | nil | true
- 3.months.ago + 1.hour | nil | false
+ nil | nil | false
+ 3.months.ago | 3.months.ago | true
+ 3.months.ago | (3.months - 1.hour).ago | false
+ 3.months.ago | nil | true
+ (3.months - 1.hour).ago | nil | false
end
with_them do
@@ -588,9 +588,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
runner.contacted_at = contacted_at
end
- specify do
- is_expected.to eq(expected_stale?)
- end
+ it { is_expected.to eq(expected_stale?) }
end
context 'with cache value' do
@@ -599,9 +597,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
stub_redis_runner_contacted_at(contacted_at.to_s)
end
- specify do
- is_expected.to eq(expected_stale?)
- end
+ it { is_expected.to eq(expected_stale?) }
end
def stub_redis_runner_contacted_at(value)
@@ -617,7 +613,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
- describe '.online' do
+ describe '.online', :freeze_time do
subject { described_class.online }
let!(:runner1) { create(:ci_runner, :instance, contacted_at: 2.hours.ago) }
@@ -626,7 +622,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
it { is_expected.to match_array([runner2]) }
end
- describe '#online?', :clean_gitlab_redis_cache do
+ describe '#online?', :clean_gitlab_redis_cache, :freeze_time do
let(:runner) { build(:ci_runner, :instance) }
subject { runner.online? }
@@ -891,26 +887,17 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
- describe '#status' do
- let(:runner) { build(:ci_runner, :instance, created_at: 4.months.ago) }
- let(:legacy_mode) {}
+ describe '#status', :freeze_time do
+ let(:runner) { build(:ci_runner, :instance, created_at: 3.months.ago) }
- subject { runner.status(legacy_mode) }
+ subject { runner.status }
context 'never connected' do
before do
runner.contacted_at = nil
end
- context 'with legacy_mode enabled' do
- let(:legacy_mode) { '14.5' }
-
- it { is_expected.to eq(:stale) }
- end
-
- context 'with legacy_mode disabled' do
- it { is_expected.to eq(:stale) }
- end
+ it { is_expected.to eq(:stale) }
context 'created recently' do
before do
@@ -927,15 +914,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
runner.active = false
end
- context 'with legacy_mode enabled' do
- let(:legacy_mode) { '14.5' }
-
- it { is_expected.to eq(:paused) }
- end
-
- context 'with legacy_mode disabled' do
- it { is_expected.to eq(:online) }
- end
+ it { is_expected.to eq(:online) }
end
context 'contacted 1s ago' do
@@ -948,7 +927,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
context 'contacted recently' do
before do
- runner.contacted_at = (3.months - 1.hour).ago
+ runner.contacted_at = (3.months - 1.second).ago
end
it { is_expected.to eq(:offline) }
@@ -956,22 +935,14 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
context 'contacted long time ago' do
before do
- runner.contacted_at = (3.months + 1.second).ago
+ runner.contacted_at = 3.months.ago
end
- context 'with legacy_mode enabled' do
- let(:legacy_mode) { '14.5' }
-
- it { is_expected.to eq(:stale) }
- end
-
- context 'with legacy_mode disabled' do
- it { is_expected.to eq(:stale) }
- end
+ it { is_expected.to eq(:stale) }
end
end
- describe '#deprecated_rest_status' do
+ describe '#deprecated_rest_status', :freeze_time do
let(:runner) { create(:ci_runner, :instance, contacted_at: 1.second.ago) }
subject { runner.deprecated_rest_status }
@@ -994,8 +965,8 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
context 'contacted long time ago' do
before do
- runner.created_at = 1.year.ago
- runner.contacted_at = 1.year.ago
+ runner.created_at = 3.months.ago
+ runner.contacted_at = 3.months.ago
end
it { is_expected.to eq(:stale) }
@@ -1076,13 +1047,13 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
- describe '#heartbeat' do
- let(:runner) { create(:ci_runner, :project) }
+ describe '#heartbeat', :freeze_time do
+ let(:runner) { create(:ci_runner, :project, version: '15.0.0') }
let(:executor) { 'shell' }
- let(:version) { '15.0.1' }
+ let(:values) { { architecture: '18-bit', config: { gpus: "all" }, executor: executor, version: version } }
subject(:heartbeat) do
- runner.heartbeat(architecture: '18-bit', config: { gpus: "all" }, executor: executor, version: version)
+ runner.heartbeat(values)
end
context 'when database was updated recently' do
@@ -1090,29 +1061,61 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
runner.contacted_at = Time.current
end
- it 'updates cache' do
- expect_redis_update
- expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to receive(:perform_async)
+ context 'when version is changed' do
+ let(:version) { '15.0.1' }
- heartbeat
+ before do
+ allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version)
+ end
- expect(runner.runner_version).to be_nil
+ it 'updates cache' do
+ expect_redis_update
+
+ heartbeat
+
+ expect(runner.runner_version).to be_nil
+ end
+
+ it 'schedules version information update' do
+ heartbeat
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).with(version).once
+ end
+
+ context 'when fetching runner releases is disabled' do
+ before do
+ stub_application_setting(update_runner_versions_enabled: false)
+ end
+
+ it 'does not schedule version information update' do
+ heartbeat
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
+ end
+ end
end
context 'with only ip_address specified', :freeze_time do
- subject(:heartbeat) do
- runner.heartbeat(ip_address: '1.1.1.1')
+ let(:values) do
+ { ip_address: '1.1.1.1' }
end
it 'updates only ip_address' do
- attrs = Gitlab::Json.dump(ip_address: '1.1.1.1', contacted_at: Time.current)
+ expect_redis_update(values.merge(contacted_at: Time.current))
- Gitlab::Redis::Cache.with do |redis|
- redis_key = runner.send(:cache_attribute_key)
- expect(redis).to receive(:set).with(redis_key, attrs, any_args)
+ heartbeat
+ end
+
+ context 'with new version having been cached' do
+ let(:version) { '15.0.1' }
+
+ before do
+ runner.cache_attributes(version: version)
end
- heartbeat
+ it 'does not lose cached version value' do
+ expect { heartbeat }.not_to change { runner.version }.from(version)
+ end
end
end
end
@@ -1121,65 +1124,81 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
before do
runner.contacted_at = 2.hours.ago
- allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async)
+ allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version)
end
- context 'with invalid runner' do
- before do
- runner.runner_projects.delete_all
- end
+ context 'when version is changed' do
+ let(:version) { '15.0.1' }
- it 'still updates redis cache and database' do
- expect(runner).to be_invalid
+ context 'with invalid runner' do
+ before do
+ runner.runner_projects.delete_all
+ end
+
+ it 'still updates redis cache and database' do
+ expect(runner).to be_invalid
+
+ expect_redis_update
+ does_db_update
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).with(version).once
+ end
+ end
+
+ it 'updates redis cache and database' do
expect_redis_update
does_db_update
-
- expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).once
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).with(version).once
end
end
context 'with unchanged runner version' do
- let(:runner) { create(:ci_runner, version: version) }
+ let(:version) { runner.version }
it 'does not schedule ci_runner_versions update' do
heartbeat
expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
end
- end
- it 'updates redis cache and database' do
- expect_redis_update
- does_db_update
- expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).once
- end
+ Ci::Runner::EXECUTOR_NAME_TO_TYPES.each_key do |executor|
+ context "with #{executor} executor" do
+ let(:executor) { executor }
- %w(custom shell docker docker-windows docker-ssh ssh parallels virtualbox docker+machine docker-ssh+machine kubernetes some-unknown-type).each do |executor|
- context "with #{executor} executor" do
- let(:executor) { executor }
+ it 'updates with expected executor type' do
+ expect_redis_update
- it 'updates with expected executor type' do
- expect_redis_update
+ heartbeat
- heartbeat
+ expect(runner.reload.read_attribute(:executor_type)).to eq(expected_executor_type)
+ end
- expect(runner.reload.read_attribute(:executor_type)).to eq(expected_executor_type)
+ def expected_executor_type
+ executor.gsub(/[+-]/, '_')
+ end
end
+ end
- def expected_executor_type
- return 'unknown' if executor == 'some-unknown-type'
+ context 'with an unknown executor type' do
+ let(:executor) { 'some-unknown-type' }
+
+ it 'updates with unknown executor type' do
+ expect_redis_update
- executor.gsub(/[+-]/, '_')
+ heartbeat
+
+ expect(runner.reload.read_attribute(:executor_type)).to eq('unknown')
end
end
end
end
- def expect_redis_update
+ def expect_redis_update(values = anything)
+ values_json = values == anything ? anything : Gitlab::Json.dump(values)
+
Gitlab::Redis::Cache.with do |redis|
redis_key = runner.send(:cache_attribute_key)
- expect(redis).to receive(:set).with(redis_key, anything, any_args)
+ expect(redis).to receive(:set).with(redis_key, values_json, any_args).and_call_original
end
end
@@ -1994,4 +2013,85 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
end
+
+ describe '.with_creator' do
+ subject { described_class.with_creator }
+
+ let!(:user) { create(:admin) }
+ let!(:runner) { create(:ci_runner, creator: user) }
+
+ it { is_expected.to contain_exactly(runner) }
+ end
+
+ describe '#ensure_token' do
+ let(:runner) { described_class.new(registration_type: registration_type) }
+ let(:token) { 'an_existing_secret_token' }
+ let(:static_prefix) { described_class::CREATED_RUNNER_TOKEN_PREFIX }
+
+ context 'when runner is initialized without a token' do
+ context 'with registration_token' do
+ let(:registration_type) { :registration_token }
+
+ it 'generates a token' do
+ expect { runner.ensure_token }.to change { runner.token }.from(nil)
+ end
+ end
+
+ context 'with authenticated_user' do
+ let(:registration_type) { :authenticated_user }
+
+ it 'generates a token with prefix' do
+ expect { runner.ensure_token }.to change { runner.token }.from(nil).to(a_string_starting_with(static_prefix))
+ end
+ end
+ end
+
+ context 'when runner is initialized with a token' do
+ before do
+ runner.set_token(token)
+ end
+
+ context 'with registration_token' do
+ let(:registration_type) { :registration_token }
+
+ it 'does not change the existing token' do
+ expect { runner.ensure_token }.not_to change { runner.token }.from(token)
+ end
+ end
+
+ context 'with authenticated_user' do
+ let(:registration_type) { :authenticated_user }
+
+ it 'does not change the existing token' do
+ expect { runner.ensure_token }.not_to change { runner.token }.from(token)
+ end
+ end
+ end
+ end
+
+ describe '#gitlab_hosted?' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject(:runner) { build_stubbed(:ci_runner) }
+
+ where(:saas, :runner_type, :expected_value) do
+ true | :instance_type | true
+ true | :group_type | false
+ true | :project_type | false
+ false | :instance_type | false
+ false | :group_type | false
+ false | :project_type | false
+ end
+
+ with_them do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(saas)
+ runner.runner_type = runner_type
+ end
+
+ it 'returns the correct value based on saas and runner type' do
+ expect(runner.gitlab_hosted?).to eq(expected_value)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/runner_version_spec.rb b/spec/models/ci/runner_version_spec.rb
index 51a2f14c57c..bce1f2a6c39 100644
--- a/spec/models/ci/runner_version_spec.rb
+++ b/spec/models/ci/runner_version_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Ci::RunnerVersion, feature_category: :runner_fleet do
create(:ci_runner_version, version: 'abc123', status: :unavailable)
end
- it { is_expected.to have_many(:runner_machines).with_foreign_key(:version) }
+ it { is_expected.to have_many(:runner_managers).with_foreign_key(:version) }
it_behaves_like 'having unique enum values'
@@ -39,4 +39,15 @@ RSpec.describe Ci::RunnerVersion, feature_category: :runner_fleet do
describe 'validation' do
it { is_expected.to validate_length_of(:version).is_at_most(2048) }
end
+
+ describe '#status' do
+ context 'when is not processed' do
+ subject(:ci_runner_version) { create(:ci_runner_version, version: 'abc124', status: :not_processed) }
+
+ let(:attr) { :status }
+ let(:attr_value) { :not_processed }
+
+ it_behaves_like 'having enum with nil value'
+ end
+ end
end
diff --git a/spec/models/ci/secure_file_spec.rb b/spec/models/ci/secure_file_spec.rb
index 38ae908fb00..1043da33022 100644
--- a/spec/models/ci/secure_file_spec.rb
+++ b/spec/models/ci/secure_file_spec.rb
@@ -144,36 +144,43 @@ RSpec.describe Ci::SecureFile do
describe '#update_metadata!' do
it 'assigns the expected metadata when a parsable .cer file is supplied' do
- file = create(:ci_secure_file, name: 'file1.cer',
- file: CarrierWaveStringFile.new(fixture_file('ci_secure_files/sample.cer')))
+ file = create(
+ :ci_secure_file,
+ name: 'file1.cer',
+ file: CarrierWaveStringFile.new(fixture_file('ci_secure_files/sample.cer'))
+ )
file.update_metadata!
file.reload
- expect(file.expires_at).to eq(DateTime.parse('2022-04-26 19:20:40'))
+ expect(file.expires_at).to eq(DateTime.parse('2023-04-26 19:20:39'))
expect(file.metadata['id']).to eq('33669367788748363528491290218354043267')
expect(file.metadata['issuer']['CN']).to eq('Apple Worldwide Developer Relations Certification Authority')
expect(file.metadata['subject']['OU']).to eq('N7SYAN8PX8')
end
it 'assigns the expected metadata when a parsable .p12 file is supplied' do
- file = create(:ci_secure_file, name: 'file1.p12',
- file: CarrierWaveStringFile.new(fixture_file('ci_secure_files/sample.p12')))
+ file = create(
+ :ci_secure_file,
+ name: 'file1.p12',
+ file: CarrierWaveStringFile.new(fixture_file('ci_secure_files/sample.p12'))
+ )
file.update_metadata!
file.reload
- expect(file.expires_at).to eq(DateTime.parse('2022-09-21 14:56:00'))
+ expect(file.expires_at).to eq(DateTime.parse('2023-09-21 14:55:59'))
expect(file.metadata['id']).to eq('75949910542696343243264405377658443914')
expect(file.metadata['issuer']['CN']).to eq('Apple Worldwide Developer Relations Certification Authority')
expect(file.metadata['subject']['OU']).to eq('N7SYAN8PX8')
end
it 'assigns the expected metadata when a parsable .mobileprovision file is supplied' do
- file = create(:ci_secure_file, name: 'file1.mobileprovision',
- file: CarrierWaveStringFile.new(
- fixture_file('ci_secure_files/sample.mobileprovision')
- ))
+ file = create(
+ :ci_secure_file,
+ name: 'file1.mobileprovision',
+ file: CarrierWaveStringFile.new(fixture_file('ci_secure_files/sample.mobileprovision'))
+ )
file.update_metadata!
file.reload
@@ -187,7 +194,7 @@ RSpec.describe Ci::SecureFile do
it 'logs an error when something goes wrong with the file parsing' do
corrupt_file = create(:ci_secure_file, name: 'file1.cer', file: CarrierWaveStringFile.new('11111111'))
- message = 'Validation failed: Metadata must be a valid json schema - not enough data.'
+ message = 'Validation failed: Metadata must be a valid json schema - PEM_read_bio_X509: no start line.'
expect(Gitlab::AppLogger).to receive(:error).with("Secure File Parser Failure (#{corrupt_file.id}): #{message}")
corrupt_file.update_metadata!
end
diff --git a/spec/models/ci/sources/pipeline_spec.rb b/spec/models/ci/sources/pipeline_spec.rb
index 707872d0a15..036708ed61e 100644
--- a/spec/models/ci/sources/pipeline_spec.rb
+++ b/spec/models/ci/sources/pipeline_spec.rb
@@ -6,6 +6,11 @@ RSpec.describe Ci::Sources::Pipeline, feature_category: :continuous_integration
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:pipeline) }
+ it do
+ is_expected.to belong_to(:build).class_name('Ci::Build')
+ .with_foreign_key(:source_job_id).inverse_of(:sourced_pipelines)
+ end
+
it { is_expected.to belong_to(:source_project).class_name('::Project') }
it { is_expected.to belong_to(:source_job) }
it { is_expected.to belong_to(:source_bridge) }
@@ -32,7 +37,7 @@ RSpec.describe Ci::Sources::Pipeline, feature_category: :continuous_integration
end
end
- describe 'partitioning', :ci_partitioning do
+ describe 'partitioning', :ci_partitionable do
include Ci::PartitioningHelpers
let(:new_pipeline) { create(:ci_pipeline) }
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index b392ab4ed11..79e92082ee1 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -223,10 +223,13 @@ RSpec.describe Ci::Stage, :models do
with_them do
before do
statuses.each do |status|
- create(:commit_status, project: stage.project,
- pipeline: stage.pipeline,
- stage_id: stage.id,
- status: status)
+ create(
+ :commit_status,
+ project: stage.project,
+ pipeline: stage.pipeline,
+ stage_id: stage.id,
+ status: status
+ )
stage.update_legacy_status
end
@@ -239,11 +242,14 @@ RSpec.describe Ci::Stage, :models do
context 'when stage has warnings' do
before do
- create(:ci_build, project: stage.project,
- pipeline: stage.pipeline,
- stage_id: stage.id,
- status: :failed,
- allow_failure: true)
+ create(
+ :ci_build,
+ project: stage.project,
+ pipeline: stage.pipeline,
+ stage_id: stage.id,
+ status: :failed,
+ allow_failure: true
+ )
stage.update_legacy_status
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index ce64b3ea158..85327dbeb34 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Variable, feature_category: :pipeline_authoring do
+RSpec.describe Ci::Variable, feature_category: :secrets_management do
let_it_be_with_reload(:project) { create(:project) }
subject { build(:ci_variable, project: project) }
diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb
index de67bdb32aa..10081b955f4 100644
--- a/spec/models/clusters/agent_spec.rb
+++ b/spec/models/clusters/agent_spec.rb
@@ -2,16 +2,17 @@
require 'spec_helper'
-RSpec.describe Clusters::Agent do
+RSpec.describe Clusters::Agent, feature_category: :deployment_management do
subject { create(:cluster_agent) }
it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
it { is_expected.to belong_to(:project).class_name('::Project') }
it { is_expected.to have_many(:agent_tokens).class_name('Clusters::AgentToken').order(Clusters::AgentToken.arel_table[:last_used_at].desc.nulls_last) }
- it { is_expected.to have_many(:group_authorizations).class_name('Clusters::Agents::GroupAuthorization') }
- it { is_expected.to have_many(:authorized_groups).through(:group_authorizations) }
- it { is_expected.to have_many(:project_authorizations).class_name('Clusters::Agents::ProjectAuthorization') }
- it { is_expected.to have_many(:authorized_projects).through(:project_authorizations).class_name('::Project') }
+ it { is_expected.to have_many(:active_agent_tokens).class_name('Clusters::AgentToken').conditions(status: 0).order(Clusters::AgentToken.arel_table[:last_used_at].desc.nulls_last) }
+ it { is_expected.to have_many(:ci_access_group_authorizations).class_name('Clusters::Agents::Authorizations::CiAccess::GroupAuthorization') }
+ it { is_expected.to have_many(:ci_access_authorized_groups).through(:ci_access_group_authorizations) }
+ it { is_expected.to have_many(:ci_access_project_authorizations).class_name('Clusters::Agents::Authorizations::CiAccess::ProjectAuthorization') }
+ it { is_expected.to have_many(:ci_access_authorized_projects).through(:ci_access_project_authorizations).class_name('::Project') }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(63) }
@@ -163,4 +164,188 @@ RSpec.describe Clusters::Agent do
it { is_expected.to be_like_time(event2.recorded_at) }
end
+
+ describe '#ci_access_authorized_for?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:organization) { create(:group) }
+ let_it_be(:agent_management_project) { create(:project, group: organization) }
+ let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) }
+ let_it_be(:deployment_project) { create(:project, group: organization) }
+
+ let(:user) { create(:user) }
+
+ subject { agent.ci_access_authorized_for?(user) }
+
+ it { is_expected.to eq(false) }
+
+ context 'with project-level authorization' do
+ let!(:authorization) { create(:agent_ci_access_project_authorization, agent: agent, project: deployment_project) }
+
+ where(:user_role, :allowed) do
+ :guest | false
+ :reporter | false
+ :developer | true
+ :maintainer | true
+ :owner | true
+ end
+
+ with_them do
+ before do
+ deployment_project.add_member(user, user_role)
+ end
+
+ it { is_expected.to eq(allowed) }
+ end
+
+ context 'when expose_authorized_cluster_agents feature flag is disabled' do
+ before do
+ stub_feature_flags(expose_authorized_cluster_agents: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'with group-level authorization' do
+ let!(:authorization) { create(:agent_ci_access_group_authorization, agent: agent, group: organization) }
+
+ where(:user_role, :allowed) do
+ :guest | false
+ :reporter | false
+ :developer | true
+ :maintainer | true
+ :owner | true
+ end
+
+ with_them do
+ before do
+ organization.add_member(user, user_role)
+ end
+
+ it { is_expected.to eq(allowed) }
+ end
+
+ context 'when expose_authorized_cluster_agents feature flag is disabled' do
+ before do
+ stub_feature_flags(expose_authorized_cluster_agents: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#user_access_authorized_for?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:organization) { create(:group) }
+ let_it_be(:agent_management_project) { create(:project, group: organization) }
+ let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) }
+ let_it_be(:deployment_project) { create(:project, group: organization) }
+
+ let(:user) { create(:user) }
+
+ subject { agent.user_access_authorized_for?(user) }
+
+ it { is_expected.to eq(false) }
+
+ context 'with project-level authorization' do
+ let!(:authorization) { create(:agent_user_access_project_authorization, agent: agent, project: deployment_project) }
+
+ where(:user_role, :allowed) do
+ :guest | false
+ :reporter | false
+ :developer | true
+ :maintainer | true
+ :owner | true
+ end
+
+ with_them do
+ before do
+ deployment_project.add_member(user, user_role)
+ end
+
+ it { is_expected.to eq(allowed) }
+ end
+
+ context 'when expose_authorized_cluster_agents feature flag is disabled' do
+ before do
+ stub_feature_flags(expose_authorized_cluster_agents: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'with group-level authorization' do
+ let!(:authorization) { create(:agent_user_access_group_authorization, agent: agent, group: organization) }
+
+ where(:user_role, :allowed) do
+ :guest | false
+ :reporter | false
+ :developer | true
+ :maintainer | true
+ :owner | true
+ end
+
+ with_them do
+ before do
+ organization.add_member(user, user_role)
+ end
+
+ it { is_expected.to eq(allowed) }
+ end
+
+ context 'when expose_authorized_cluster_agents feature flag is disabled' do
+ before do
+ stub_feature_flags(expose_authorized_cluster_agents: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#user_access_config' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project) }
+ let_it_be_with_refind(:agent) { create(:cluster_agent, project: project) }
+
+ subject { agent.user_access_config }
+
+ it { is_expected.to be_nil }
+
+ context 'with user_access project authorizations' do
+ before do
+ create(:agent_user_access_project_authorization, agent: agent, project: project, config: config)
+ end
+
+ let(:config) { {} }
+
+ it { is_expected.to eq(config) }
+
+ context 'when access_as keyword exists' do
+ let(:config) { { 'access_as' => { 'agent' => {} } } }
+
+ it { is_expected.to eq(config) }
+ end
+ end
+
+ context 'with user_access group authorizations' do
+ before do
+ create(:agent_user_access_group_authorization, agent: agent, group: group, config: config)
+ end
+
+ let(:config) { {} }
+
+ it { is_expected.to eq(config) }
+
+ context 'when access_as keyword exists' do
+ let(:config) { { 'access_as' => { 'agent' => {} } } }
+
+ it { is_expected.to eq(config) }
+ end
+ end
+ end
end
diff --git a/spec/models/clusters/agent_token_spec.rb b/spec/models/clusters/agent_token_spec.rb
index 74723e3abd8..41f8215b713 100644
--- a/spec/models/clusters/agent_token_spec.rb
+++ b/spec/models/clusters/agent_token_spec.rb
@@ -24,13 +24,29 @@ RSpec.describe Clusters::AgentToken do
end
end
- describe '.with_status' do
+ describe 'status-related scopes' do
let!(:active_token) { create(:cluster_agent_token) }
let!(:revoked_token) { create(:cluster_agent_token, :revoked) }
- subject { described_class.with_status(:active) }
+ describe '.with_status' do
+ context 'when filtering by active status' do
+ subject { described_class.with_status(:active) }
- it { is_expected.to contain_exactly(active_token) }
+ it { is_expected.to contain_exactly(active_token) }
+ end
+
+ context 'when filtering by revoked status' do
+ subject { described_class.with_status(:revoked) }
+
+ it { is_expected.to contain_exactly(revoked_token) }
+ end
+ end
+
+ describe '.active' do
+ subject { described_class.active }
+
+ it { is_expected.to contain_exactly(active_token) }
+ end
end
end
diff --git a/spec/models/clusters/agents/group_authorization_spec.rb b/spec/models/clusters/agents/authorizations/ci_access/group_authorization_spec.rb
index baeb8f5464e..deabebde760 100644
--- a/spec/models/clusters/agents/group_authorization_spec.rb
+++ b/spec/models/clusters/agents/authorizations/ci_access/group_authorization_spec.rb
@@ -2,14 +2,14 @@
require 'spec_helper'
-RSpec.describe Clusters::Agents::GroupAuthorization do
+RSpec.describe Clusters::Agents::Authorizations::CiAccess::GroupAuthorization, feature_category: :deployment_management do
it { is_expected.to belong_to(:agent).class_name('Clusters::Agent').required }
it { is_expected.to belong_to(:group).class_name('::Group').required }
it { expect(described_class).to validate_jsonb_schema(['config']) }
describe '#config_project' do
- let(:record) { create(:agent_group_authorization) }
+ let(:record) { create(:agent_ci_access_group_authorization) }
it { expect(record.config_project).to eq(record.agent.project) }
end
diff --git a/spec/models/clusters/agents/implicit_authorization_spec.rb b/spec/models/clusters/agents/authorizations/ci_access/implicit_authorization_spec.rb
index 1f4c5b1ac9e..427858c7529 100644
--- a/spec/models/clusters/agents/implicit_authorization_spec.rb
+++ b/spec/models/clusters/agents/authorizations/ci_access/implicit_authorization_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Agents::ImplicitAuthorization do
+RSpec.describe Clusters::Agents::Authorizations::CiAccess::ImplicitAuthorization, feature_category: :deployment_management do
let_it_be(:agent) { create(:cluster_agent) }
subject { described_class.new(agent: agent) }
diff --git a/spec/models/clusters/agents/project_authorization_spec.rb b/spec/models/clusters/agents/authorizations/ci_access/project_authorization_spec.rb
index 9ba259356c7..fe5f3cb10ea 100644
--- a/spec/models/clusters/agents/project_authorization_spec.rb
+++ b/spec/models/clusters/agents/authorizations/ci_access/project_authorization_spec.rb
@@ -2,14 +2,14 @@
require 'spec_helper'
-RSpec.describe Clusters::Agents::ProjectAuthorization do
+RSpec.describe Clusters::Agents::Authorizations::CiAccess::ProjectAuthorization, feature_category: :deployment_management do
it { is_expected.to belong_to(:agent).class_name('Clusters::Agent').required }
it { is_expected.to belong_to(:project).class_name('Project').required }
it { expect(described_class).to validate_jsonb_schema(['config']) }
describe '#config_project' do
- let(:record) { create(:agent_project_authorization) }
+ let(:record) { create(:agent_ci_access_project_authorization) }
it { expect(record.config_project).to eq(record.agent.project) }
end
diff --git a/spec/models/clusters/agents/authorizations/user_access/group_authorization_spec.rb b/spec/models/clusters/agents/authorizations/user_access/group_authorization_spec.rb
new file mode 100644
index 00000000000..da94654268b
--- /dev/null
+++ b/spec/models/clusters/agents/authorizations/user_access/group_authorization_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::Authorizations::UserAccess::GroupAuthorization, feature_category: :deployment_management do
+ it { is_expected.to belong_to(:agent).class_name('Clusters::Agent').required }
+ it { is_expected.to belong_to(:group).class_name('::Group').required }
+
+ it { expect(described_class).to validate_jsonb_schema(['config']) }
+
+ describe '.for_user' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+
+ let!(:authorization) { create(:agent_user_access_group_authorization, group: group) }
+ let(:user) { create(:user) }
+
+ subject { described_class.for_user(user) }
+
+ where(:user_role, :expected_access_level) do
+ :guest | nil
+ :reporter | nil
+ :developer | Gitlab::Access::DEVELOPER
+ :maintainer | Gitlab::Access::MAINTAINER
+ :owner | Gitlab::Access::OWNER
+ end
+
+ with_them do
+ before do
+ group.add_member(user, user_role)
+ end
+
+ it 'returns the expected result' do
+ if expected_access_level
+ expect(subject).to contain_exactly(authorization)
+ expect(subject.first.access_level).to eq(expected_access_level)
+ else
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when authorization belongs to sub-group' do
+ let!(:authorization) { create(:agent_user_access_group_authorization, group: subgroup) }
+
+ it 'respects the role inheritance' do
+ if expected_access_level
+ expect(subject).to contain_exactly(authorization)
+ expect(subject.first.access_level).to eq(expected_access_level)
+ else
+ expect(subject).to be_empty
+ end
+ end
+
+ it 'respects the role override' do
+ subgroup.add_member(user, :owner)
+
+ expect(subject).to contain_exactly(authorization)
+ expect(subject.first.access_level).to eq(Gitlab::Access::OWNER)
+ end
+ end
+ end
+ end
+
+ describe '#config_project' do
+ let(:record) { create(:agent_user_access_group_authorization) }
+
+ it { expect(record.config_project).to eq(record.agent.project) }
+ end
+end
diff --git a/spec/models/clusters/agents/authorizations/user_access/project_authorization_spec.rb b/spec/models/clusters/agents/authorizations/user_access/project_authorization_spec.rb
new file mode 100644
index 00000000000..f6f6b328ad9
--- /dev/null
+++ b/spec/models/clusters/agents/authorizations/user_access/project_authorization_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::Authorizations::UserAccess::ProjectAuthorization, feature_category: :deployment_management do
+ it { is_expected.to belong_to(:agent).class_name('Clusters::Agent').required }
+ it { is_expected.to belong_to(:project).class_name('Project').required }
+
+ it { expect(described_class).to validate_jsonb_schema(['config']) }
+
+ describe '.for_user' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:authorization) { create(:agent_user_access_project_authorization, project: project) }
+
+ let(:user) { create(:user) }
+
+ subject { described_class.for_user(user) }
+
+ where(:user_role, :expected_access_level) do
+ :guest | nil
+ :reporter | nil
+ :developer | Gitlab::Access::DEVELOPER
+ :maintainer | Gitlab::Access::MAINTAINER
+ :owner | Gitlab::Access::OWNER
+ end
+
+ with_them do
+ before do
+ project.add_member(user, user_role)
+ end
+
+ it 'returns the expected result' do
+ if expected_access_level
+ expect(subject).to contain_exactly(authorization)
+ expect(subject.first.access_level).to eq(expected_access_level)
+ else
+ expect(subject).to be_empty
+ end
+ end
+ end
+ end
+
+ describe '#config_project' do
+ let(:record) { create(:agent_user_access_project_authorization) }
+
+ it { expect(record.config_project).to eq(record.agent.project) }
+ end
+end
diff --git a/spec/models/clusters/applications/crossplane_spec.rb b/spec/models/clusters/applications/crossplane_spec.rb
deleted file mode 100644
index d1abaa52c7f..00000000000
--- a/spec/models/clusters/applications/crossplane_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::Crossplane do
- let(:crossplane) { create(:clusters_applications_crossplane) }
-
- include_examples 'cluster application core specs', :clusters_applications_crossplane
- include_examples 'cluster application status specs', :clusters_applications_crossplane
- include_examples 'cluster application version specs', :clusters_applications_crossplane
- include_examples 'cluster application initial status specs'
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:stack) }
- end
-
- describe 'default values' do
- it { expect(subject.version).to eq(described_class::VERSION) }
- it { expect(subject.stack).to be_empty }
- end
-
- describe '#can_uninstall?' do
- subject { crossplane.can_uninstall? }
-
- it { is_expected.to be_truthy }
- end
-
- describe '#install_command' do
- let(:stack) { 'gcp' }
-
- subject { crossplane.install_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) }
-
- it 'is initialized with crossplane arguments' do
- expect(subject.name).to eq('crossplane')
- expect(subject.chart).to eq('crossplane/crossplane')
- expect(subject.repository).to eq('https://charts.crossplane.io/alpha')
- expect(subject.version).to eq('0.4.1')
- expect(subject).to be_rbac
- end
-
- context 'application failed to install previously' do
- let(:crossplane) { create(:clusters_applications_crossplane, :errored, version: '0.0.1') }
-
- it 'is initialized with the locked version' do
- expect(subject.version).to eq('0.4.1')
- end
- end
- end
-
- describe '#files' do
- let(:application) { crossplane }
- let(:values) { subject[:'values.yaml'] }
-
- subject { application.files }
-
- it 'includes crossplane specific keys in the values.yaml file' do
- expect(values).to include('clusterStacks')
- end
- end
-end
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
deleted file mode 100644
index 1b8be92475a..00000000000
--- a/spec/models/clusters/applications/helm_spec.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::Helm do
- include_examples 'cluster application core specs', :clusters_applications_helm
-
- describe 'default values' do
- it { expect(subject.version).to eq(Gitlab::Kubernetes::Helm::V2::BaseCommand::HELM_VERSION) }
- end
-
- describe '.available' do
- subject { described_class.available }
-
- let!(:installed_cluster) { create(:clusters_applications_helm, :installed) }
- let!(:updated_cluster) { create(:clusters_applications_helm, :updated) }
-
- before do
- create(:clusters_applications_helm, :errored)
- end
-
- it { is_expected.to contain_exactly(installed_cluster, updated_cluster) }
- end
-
- describe '#can_uninstall?' do
- subject(:application) { build(:clusters_applications_helm).can_uninstall? }
-
- it { is_expected.to eq true }
- end
-
- describe '#issue_client_cert' do
- let(:application) { create(:clusters_applications_helm) }
-
- subject { application.issue_client_cert }
-
- it 'returns a new cert' do
- is_expected.to be_kind_of(Gitlab::Kubernetes::Helm::V2::Certificate)
- expect(subject.cert_string).not_to eq(application.ca_cert)
- expect(subject.key_string).not_to eq(application.ca_key)
- end
- end
-
- describe '#install_command' do
- let(:helm) { create(:clusters_applications_helm) }
-
- subject { helm.install_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V2::InitCommand) }
-
- it 'is initialized with 1 arguments' do
- expect(subject.name).to eq('helm')
- end
-
- it 'has cert files' do
- expect(subject.files[:'ca.pem']).to be_present
- expect(subject.files[:'ca.pem']).to eq(helm.ca_cert)
-
- expect(subject.files[:'cert.pem']).to be_present
- expect(subject.files[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject.files[:'cert.pem'])
- expect(cert.not_after).to be > 999.years.from_now
- end
-
- describe 'rbac' do
- context 'rbac cluster' do
- it { expect(subject).to be_rbac }
- end
-
- context 'non rbac cluster' do
- before do
- helm.cluster.platform_kubernetes.abac!
- end
-
- it { expect(subject).not_to be_rbac }
- end
- end
- end
-
- describe '#uninstall_command' do
- let(:helm) { create(:clusters_applications_helm) }
-
- subject { helm.uninstall_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V2::ResetCommand) }
-
- it 'has name' do
- expect(subject.name).to eq('helm')
- end
-
- it 'has cert files' do
- expect(subject.files[:'ca.pem']).to be_present
- expect(subject.files[:'ca.pem']).to eq(helm.ca_cert)
-
- expect(subject.files[:'cert.pem']).to be_present
- expect(subject.files[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject.files[:'cert.pem'])
- expect(cert.not_after).to be > 999.years.from_now
- end
-
- describe 'rbac' do
- context 'rbac cluster' do
- it { expect(subject).to be_rbac }
- end
-
- context 'non rbac cluster' do
- before do
- helm.cluster.platform_kubernetes.abac!
- end
-
- it { expect(subject).not_to be_rbac }
- end
- end
- end
-end
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
deleted file mode 100644
index 2be59e5f515..00000000000
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ /dev/null
@@ -1,180 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::Ingress do
- let(:ingress) { create(:clusters_applications_ingress) }
-
- before do
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
- end
-
- it_behaves_like 'having unique enum values'
-
- include_examples 'cluster application core specs', :clusters_applications_ingress
- include_examples 'cluster application status specs', :clusters_applications_ingress
- include_examples 'cluster application version specs', :clusters_applications_ingress
- include_examples 'cluster application helm specs', :clusters_applications_ingress
- include_examples 'cluster application initial status specs'
-
- describe 'default values' do
- it { expect(subject.ingress_type).to eq("nginx") }
- it { expect(subject.version).to eq(described_class::VERSION) }
- end
-
- describe '#can_uninstall?' do
- subject { ingress.can_uninstall? }
-
- context 'with jupyter installed' do
- before do
- create(:clusters_applications_jupyter, :installed, cluster: ingress.cluster)
- end
-
- it 'returns false if external_ip_or_hostname? is true' do
- ingress.external_ip = 'IP'
-
- is_expected.to be_falsey
- end
-
- it 'returns false if external_ip_or_hostname? is false' do
- is_expected.to be_falsey
- end
- end
-
- context 'with jupyter installable' do
- before do
- create(:clusters_applications_jupyter, :installable, cluster: ingress.cluster)
- end
-
- it 'returns true if external_ip_or_hostname? is true' do
- ingress.external_ip = 'IP'
-
- is_expected.to be_truthy
- end
-
- it 'returns false if external_ip_or_hostname? is false' do
- is_expected.to be_falsey
- end
- end
-
- context 'with jupyter nil' do
- it 'returns false if external_ip_or_hostname? is false' do
- is_expected.to be_falsey
- end
-
- context 'if external_ip_or_hostname? is true' do
- context 'with IP' do
- before do
- ingress.external_ip = 'IP'
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'with hostname' do
- before do
- ingress.external_hostname = 'example.com'
- end
-
- it { is_expected.to be_truthy }
- end
- end
- end
- end
-
- describe '#make_installed!' do
- before do
- application.make_installed!
- end
-
- let(:application) { create(:clusters_applications_ingress, :installing) }
-
- it 'schedules a ClusterWaitForIngressIpAddressWorker' do
- expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_in)
- .with(Clusters::Applications::Ingress::FETCH_IP_ADDRESS_DELAY, 'ingress', application.id)
- end
- end
-
- describe '#schedule_status_update' do
- let(:application) { create(:clusters_applications_ingress, :installed) }
-
- before do
- application.schedule_status_update
- end
-
- it 'schedules a ClusterWaitForIngressIpAddressWorker' do
- expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_async)
- .with('ingress', application.id)
- end
-
- context 'when the application is not installed' do
- let(:application) { create(:clusters_applications_ingress, :installing) }
-
- it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
- expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_async)
- end
- end
-
- context 'when there is already an external_ip' do
- let(:application) { create(:clusters_applications_ingress, :installed, external_ip: '111.222.222.111') }
-
- it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
- expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
- end
- end
-
- context 'when there is already an external_hostname' do
- let(:application) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
-
- it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
- expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
- end
- end
- end
-
- describe '#install_command' do
- subject { ingress.install_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) }
-
- it 'is initialized with ingress arguments' do
- expect(subject.name).to eq('ingress')
- expect(subject.chart).to eq('ingress/nginx-ingress')
- expect(subject.version).to eq('1.40.2')
- expect(subject).to be_rbac
- expect(subject.files).to eq(ingress.files)
- end
-
- context 'on a non rbac enabled cluster' do
- before do
- ingress.cluster.platform_kubernetes.abac!
- end
-
- it { is_expected.not_to be_rbac }
- end
-
- context 'application failed to install previously' do
- let(:ingress) { create(:clusters_applications_ingress, :errored, version: 'nginx') }
-
- it 'is initialized with the locked version' do
- expect(subject.version).to eq('1.40.2')
- end
- end
- end
-
- describe '#files' do
- let(:application) { ingress }
- let(:values) { subject[:'values.yaml'] }
-
- subject { application.files }
-
- it 'includes ingress valid keys in values' do
- expect(values).to include('image')
- expect(values).to include('repository')
- expect(values).to include('stats')
- expect(values).to include('podAnnotations')
- expect(values).to include('clusterIP')
- end
- end
-end
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
deleted file mode 100644
index 9336d2352f8..00000000000
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ /dev/null
@@ -1,130 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::Jupyter do
- include_examples 'cluster application core specs', :clusters_applications_jupyter
- include_examples 'cluster application status specs', :clusters_applications_jupyter
- include_examples 'cluster application version specs', :clusters_applications_jupyter
- include_examples 'cluster application helm specs', :clusters_applications_jupyter
-
- it { is_expected.to belong_to(:oauth_application) }
-
- describe 'default values' do
- it { expect(subject.version).to eq(described_class::VERSION) }
- end
-
- describe '#can_uninstall?' do
- let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
- let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
-
- subject { jupyter.can_uninstall? }
-
- it { is_expected.to be_truthy }
- end
-
- describe '#set_initial_status' do
- before do
- jupyter.set_initial_status
- end
-
- context 'when ingress is not installed' do
- let(:cluster) { create(:cluster, :provided_by_gcp) }
- let(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
-
- it { expect(jupyter).to be_not_installable }
- end
-
- context 'when ingress is installed and external_ip is assigned' do
- let(:ingress) { create(:clusters_applications_ingress, :installed, external_ip: '127.0.0.1') }
- let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
-
- it { expect(jupyter).to be_installable }
- end
-
- context 'when ingress is installed and external_hostname is assigned' do
- let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
- let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
-
- it { expect(jupyter).to be_installable }
- end
- end
-
- describe '#install_command' do
- let!(:ingress) { create(:clusters_applications_ingress, :installed, external_ip: '127.0.0.1') }
- let!(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
-
- subject { jupyter.install_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) }
-
- it 'is initialized with 4 arguments' do
- expect(subject.name).to eq('jupyter')
- expect(subject.chart).to eq('jupyter/jupyterhub')
- expect(subject.version).to eq('0.9.0')
-
- expect(subject).to be_rbac
- expect(subject.repository).to eq('https://jupyterhub.github.io/helm-chart/')
- expect(subject.files).to eq(jupyter.files)
- end
-
- context 'on a non rbac enabled cluster' do
- before do
- jupyter.cluster.platform_kubernetes.abac!
- end
-
- it { is_expected.not_to be_rbac }
- end
-
- context 'application failed to install previously' do
- let(:jupyter) { create(:clusters_applications_jupyter, :errored, version: '0.0.1') }
-
- it 'is initialized with the locked version' do
- expect(subject.version).to eq('0.9.0')
- end
- end
- end
-
- describe '#files' do
- let(:cluster) { create(:cluster, :with_installed_helm, :provided_by_gcp, :project) }
- let(:application) { create(:clusters_applications_jupyter, cluster: cluster) }
- let(:values) { subject[:'values.yaml'] }
-
- subject { application.files }
-
- context 'when cluster belongs to a project' do
- it 'includes valid values' do
- expect(values).to include('ingress')
- expect(values).to include('hub')
- expect(values).to include('proxy')
- expect(values).to include('auth')
- expect(values).to include('singleuser')
- expect(values).to match(/clientId: '?#{application.oauth_application.uid}/)
- expect(values).to match(/callbackUrl: '?#{application.callback_url}/)
- expect(values).to include("gitlabProjectIdWhitelist:\n - #{application.cluster.project.id}")
- expect(values).to include("c.GitLabOAuthenticator.scope = ['api read_repository write_repository']")
- expect(values).to match(/GITLAB_HOST: '?#{Gitlab.config.gitlab.host}/)
- expect(values).to match(/GITLAB_CLUSTER_ID: '?#{application.cluster.id}/)
- end
- end
-
- context 'when cluster belongs to a group' do
- let(:group) { create(:group) }
- let(:cluster) { create(:cluster, :with_installed_helm, :provided_by_gcp, :group, groups: [group]) }
-
- it 'includes valid values' do
- expect(values).to include('ingress')
- expect(values).to include('hub')
- expect(values).to include('proxy')
- expect(values).to include('auth')
- expect(values).to include('singleuser')
- expect(values).to match(/clientId: '?#{application.oauth_application.uid}/)
- expect(values).to match(/callbackUrl: '?#{application.callback_url}/)
- expect(values).to include("gitlabGroupWhitelist:\n - #{group.to_param}")
- expect(values).to include("c.GitLabOAuthenticator.scope = ['api read_repository write_repository']")
- expect(values).to match(/GITLAB_HOST: '?#{Gitlab.config.gitlab.host}/)
- expect(values).to match(/GITLAB_CLUSTER_ID: '?#{application.cluster.id}/)
- end
- end
- end
-end
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
deleted file mode 100644
index f6b13f4a93f..00000000000
--- a/spec/models/clusters/applications/knative_spec.rb
+++ /dev/null
@@ -1,260 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::Knative do
- let(:knative) { create(:clusters_applications_knative) }
-
- before do
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
- allow(ClusterConfigureIstioWorker).to receive(:perform_async)
- end
-
- include_examples 'cluster application core specs', :clusters_applications_knative
- include_examples 'cluster application status specs', :clusters_applications_knative
- include_examples 'cluster application helm specs', :clusters_applications_knative
- include_examples 'cluster application version specs', :clusters_applications_knative
- include_examples 'cluster application initial status specs'
-
- describe 'associations' do
- it { is_expected.to have_one(:serverless_domain_cluster).class_name('::Serverless::DomainCluster').with_foreign_key('clusters_applications_knative_id').inverse_of(:knative) }
- end
-
- describe 'default values' do
- it { expect(subject.version).to eq(described_class::VERSION) }
- end
-
- describe 'when cloud run is enabled' do
- let(:cluster) { create(:cluster, :provided_by_gcp, :cloud_run_enabled) }
- let(:knative_cloud_run) { create(:clusters_applications_knative, cluster: cluster) }
-
- it { expect(knative_cloud_run).to be_not_installable }
- end
-
- describe 'when rbac is not enabled' do
- let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled) }
- let(:knative_no_rbac) { create(:clusters_applications_knative, cluster: cluster) }
-
- it { expect(knative_no_rbac).to be_not_installable }
- end
-
- describe 'make_installed with external_ip' do
- before do
- application.make_installed!
- end
-
- let(:application) { create(:clusters_applications_knative, :installing) }
-
- it 'schedules a ClusterWaitForIngressIpAddressWorker' do
- expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_in)
- .with(Clusters::Applications::Knative::FETCH_IP_ADDRESS_DELAY, 'knative', application.id)
- end
- end
-
- describe 'configuring istio ingress gateway' do
- context 'after installed' do
- let(:application) { create(:clusters_applications_knative, :installing) }
-
- before do
- application.make_installed!
- end
-
- it 'schedules a ClusterConfigureIstioWorker' do
- expect(ClusterConfigureIstioWorker).to have_received(:perform_async).with(application.cluster_id)
- end
- end
-
- context 'after updated' do
- let(:application) { create(:clusters_applications_knative, :updating) }
-
- before do
- application.make_installed!
- end
-
- it 'schedules a ClusterConfigureIstioWorker' do
- expect(ClusterConfigureIstioWorker).to have_received(:perform_async).with(application.cluster_id)
- end
- end
- end
-
- describe '#can_uninstall?' do
- subject { knative.can_uninstall? }
-
- it { is_expected.to be_truthy }
- end
-
- describe '#schedule_status_update with external_ip' do
- let(:application) { create(:clusters_applications_knative, :installed) }
-
- before do
- application.schedule_status_update
- end
-
- it 'schedules a ClusterWaitForIngressIpAddressWorker' do
- expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_async)
- .with('knative', application.id)
- end
-
- context 'when the application is not installed' do
- let(:application) { create(:clusters_applications_knative, :installing) }
-
- it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
- expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_async)
- end
- end
-
- context 'when there is already an external_ip' do
- let(:application) { create(:clusters_applications_knative, :installed, external_ip: '111.222.222.111') }
-
- it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
- expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
- end
- end
-
- context 'when there is already an external_hostname' do
- let(:application) { create(:clusters_applications_knative, :installed, external_hostname: 'localhost.localdomain') }
-
- it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
- expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
- end
- end
- end
-
- shared_examples 'a command' do
- it 'is an instance of Helm::InstallCommand' do
- expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand)
- end
-
- it 'is initialized with knative arguments' do
- expect(subject.name).to eq('knative')
- expect(subject.chart).to eq('knative/knative')
- expect(subject.files).to eq(knative.files)
- end
-
- it 'does not install metrics for prometheus' do
- expect(subject.postinstall).to be_empty
- end
-
- context 'with prometheus installed' do
- let(:prometheus) { create(:clusters_applications_prometheus, :installed) }
- let(:knative) { create(:clusters_applications_knative, cluster: prometheus.cluster) }
-
- subject { knative.install_command }
-
- it 'installs metrics' do
- expect(subject.postinstall).not_to be_empty
- expect(subject.postinstall.length).to be(1)
- expect(subject.postinstall[0]).to eql("kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}")
- end
- end
- end
-
- describe '#install_command' do
- subject { knative.install_command }
-
- it 'is initialized with latest version' do
- expect(subject.version).to eq('0.10.0')
- end
-
- it_behaves_like 'a command'
- end
-
- describe '#update_command' do
- let!(:current_installed_version) { knative.version = '0.1.0' }
-
- subject { knative.update_command }
-
- it 'is initialized with current version' do
- expect(subject.version).to eq(current_installed_version)
- end
-
- it_behaves_like 'a command'
- end
-
- describe '#uninstall_command' do
- subject { knative.uninstall_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand) }
-
- it "removes knative deployed services before uninstallation" do
- 2.times do |i|
- cluster_project = create(:cluster_project, cluster: knative.cluster)
-
- create(:cluster_kubernetes_namespace,
- cluster: cluster_project.cluster,
- cluster_project: cluster_project,
- project: cluster_project.project,
- namespace: "namespace_#{i}")
- end
-
- remove_namespaced_services_script = [
- "kubectl delete ksvc --all -n #{knative.cluster.kubernetes_namespaces.first.namespace}",
- "kubectl delete ksvc --all -n #{knative.cluster.kubernetes_namespaces.second.namespace}"
- ]
-
- expect(subject.predelete).to match_array(remove_namespaced_services_script)
- end
-
- it "initializes command with all necessary postdelete script" do
- api_groups = YAML.safe_load(File.read(Rails.root.join(Clusters::Applications::Knative::API_GROUPS_PATH)))
-
- remove_knative_istio_leftovers_script = [
- "kubectl delete --ignore-not-found ns knative-serving",
- "kubectl delete --ignore-not-found ns knative-build"
- ]
-
- full_delete_commands_size = api_groups.size + remove_knative_istio_leftovers_script.size
-
- expect(subject.postdelete).to include(*remove_knative_istio_leftovers_script)
- expect(subject.postdelete.size).to eq(full_delete_commands_size)
- expect(subject.postdelete[2]).to include("kubectl api-resources -o name --api-group #{api_groups[0]} | xargs -r kubectl delete --ignore-not-found crd")
- expect(subject.postdelete[3]).to include("kubectl api-resources -o name --api-group #{api_groups[1]} | xargs -r kubectl delete --ignore-not-found crd")
- end
- end
-
- describe '#files' do
- let(:application) { knative }
- let(:values) { subject[:'values.yaml'] }
-
- subject { application.files }
-
- it 'includes knative specific keys in the values.yaml file' do
- expect(values).to include('domain')
- end
- end
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:hostname) }
- end
-
- describe '#available_domains' do
- let!(:domain) { create(:pages_domain, :instance_serverless) }
-
- it 'returns all instance serverless domains' do
- expect(PagesDomain).to receive(:instance_serverless).and_call_original
-
- domains = subject.available_domains
-
- expect(domains.length).to eq(1)
- expect(domains).to include(domain)
- end
- end
-
- describe '#find_available_domain' do
- let!(:domain) { create(:pages_domain, :instance_serverless) }
-
- it 'returns the domain scoped to available domains' do
- expect(subject).to receive(:available_domains).and_call_original
- expect(subject.find_available_domain(domain.id)).to eq(domain)
- end
- end
-
- describe '#pages_domain' do
- let!(:sdc) { create(:serverless_domain_cluster, knative: knative) }
-
- it 'returns the the associated pages domain' do
- expect(knative.reload.pages_domain).to eq(sdc.pages_domain)
- end
- end
-end
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
deleted file mode 100644
index 15c3162270e..00000000000
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ /dev/null
@@ -1,349 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::Prometheus do
- include KubernetesHelpers
- include StubRequests
-
- include_examples 'cluster application core specs', :clusters_applications_prometheus
- include_examples 'cluster application status specs', :clusters_applications_prometheus
- include_examples 'cluster application version specs', :clusters_applications_prometheus
- include_examples 'cluster application helm specs', :clusters_applications_prometheus
- include_examples 'cluster application initial status specs'
-
- describe 'default values' do
- subject(:prometheus) { build(:clusters_applications_prometheus) }
-
- it { expect(prometheus.alert_manager_token).to be_an_instance_of(String) }
- it { expect(prometheus.version).to eq(described_class::VERSION) }
- end
-
- describe 'after_destroy' do
- let(:cluster) { create(:cluster, :with_installed_helm) }
- let(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
-
- it 'disables the corresponding integration' do
- application.destroy!
-
- expect(cluster.integration_prometheus).not_to be_enabled
- end
- end
-
- describe 'transition to installed' do
- let(:project) { create(:project) }
- let(:cluster) { create(:cluster, :with_installed_helm) }
- let(:application) { create(:clusters_applications_prometheus, :installing, cluster: cluster) }
-
- it 'enables the corresponding integration' do
- application.make_installed
-
- expect(cluster.integration_prometheus).to be_enabled
- end
- end
-
- describe 'transition to externally_installed' do
- let(:project) { create(:project) }
- let(:cluster) { create(:cluster, :with_installed_helm) }
- let(:application) { create(:clusters_applications_prometheus, :installing, cluster: cluster) }
-
- it 'enables the corresponding integration' do
- application.make_externally_installed!
-
- expect(cluster.integration_prometheus).to be_enabled
- end
- end
-
- describe 'transition to updating' do
- let(:project) { create(:project) }
- let(:cluster) { create(:cluster, projects: [project]) }
-
- subject { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
-
- it 'sets last_update_started_at to now' 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
- end
-
- describe '#managed_prometheus?' do
- subject { prometheus.managed_prometheus? }
-
- let(:prometheus) { build(:clusters_applications_prometheus) }
-
- it { is_expected.to be_truthy }
-
- context 'externally installed' do
- let(:prometheus) { build(:clusters_applications_prometheus, :externally_installed) }
-
- it { is_expected.to be_falsey }
- end
-
- context 'uninstalled' do
- let(:prometheus) { build(:clusters_applications_prometheus, :uninstalled) }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#can_uninstall?' do
- let(:prometheus) { create(:clusters_applications_prometheus) }
-
- subject { prometheus.can_uninstall? }
-
- it { is_expected.to be_truthy }
- end
-
- describe '#prometheus_client' do
- include_examples '#prometheus_client shared' do
- let(:factory) { :clusters_applications_prometheus }
- end
- end
-
- describe '#install_command' do
- let(:prometheus) { create(:clusters_applications_prometheus) }
-
- subject { prometheus.install_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) }
-
- it 'is initialized with 3 arguments' do
- expect(subject.name).to eq('prometheus')
- expect(subject.chart).to eq('prometheus/prometheus')
- expect(subject.version).to eq('10.4.1')
- expect(subject).to be_rbac
- expect(subject.files).to eq(prometheus.files)
- end
-
- context 'on a non rbac enabled cluster' do
- before do
- prometheus.cluster.platform_kubernetes.abac!
- end
-
- it { is_expected.not_to be_rbac }
- end
-
- context 'application failed to install previously' 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('10.4.1')
- end
- end
-
- it 'does not install knative metrics' do
- expect(subject.postinstall).to be_empty
- end
-
- context 'with knative installed' do
- let(:knative) { create(:clusters_applications_knative, :updated) }
- let(:prometheus) { create(:clusters_applications_prometheus, cluster: knative.cluster) }
-
- subject { prometheus.install_command }
-
- it 'installs knative metrics' do
- expect(subject.postinstall).to include("kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}")
- end
- end
- end
-
- describe '#uninstall_command' do
- let(:prometheus) { create(:clusters_applications_prometheus) }
-
- subject { prometheus.uninstall_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand) }
-
- it 'has the application name' do
- expect(subject.name).to eq('prometheus')
- end
-
- it 'has files' do
- expect(subject.files).to eq(prometheus.files)
- end
-
- it 'is rbac' do
- expect(subject).to be_rbac
- end
-
- describe '#predelete' do
- let(:knative) { create(:clusters_applications_knative, :updated) }
- let(:prometheus) { create(:clusters_applications_prometheus, cluster: knative.cluster) }
-
- subject { prometheus.uninstall_command.predelete }
-
- it 'deletes knative metrics' do
- metrics_config = Clusters::Applications::Knative::METRICS_CONFIG
- is_expected.to include("kubectl delete -f #{metrics_config} --ignore-not-found")
- end
- end
-
- context 'on a non rbac enabled cluster' do
- before do
- prometheus.cluster.platform_kubernetes.abac!
- end
-
- it { is_expected.not_to be_rbac }
- end
- end
-
- describe '#patch_command' do
- subject(:patch_command) { prometheus.patch_command(values) }
-
- let(:prometheus) { build(:clusters_applications_prometheus) }
- let(:values) { prometheus.values }
-
- it { is_expected.to be_an_instance_of(::Gitlab::Kubernetes::Helm::V3::PatchCommand) }
-
- it 'is initialized with 3 arguments' do
- expect(patch_command.name).to eq('prometheus')
- expect(patch_command.chart).to eq('prometheus/prometheus')
- expect(patch_command.version).to eq('10.4.1')
- expect(patch_command.files).to eq(prometheus.files)
- end
- end
-
- describe '#update_in_progress?' do
- context 'when app is updating' do
- it 'returns true' do
- cluster = create(:cluster)
- prometheus_app = build(:clusters_applications_prometheus, :updating, cluster: cluster)
-
- expect(prometheus_app.update_in_progress?).to be true
- end
- end
- end
-
- describe '#update_errored?' do
- context 'when app errored' do
- it 'returns true' do
- cluster = create(:cluster)
- prometheus_app = build(:clusters_applications_prometheus, :update_errored, cluster: cluster)
-
- expect(prometheus_app.update_errored?).to be true
- end
- end
- end
-
- describe '#files' do
- let(:application) { create(:clusters_applications_prometheus) }
- let(:values) { subject[:'values.yaml'] }
-
- subject { application.files }
-
- it 'includes prometheus valid values' do
- expect(values).to include('alertmanager')
- expect(values).to include('kubeStateMetrics')
- expect(values).to include('nodeExporter')
- expect(values).to include('pushgateway')
- expect(values).to include('serverFiles')
- end
- end
-
- describe '#files_with_replaced_values' do
- let(:application) { build(:clusters_applications_prometheus) }
- let(:files) { application.files }
-
- subject { application.files_with_replaced_values({ hello: :world }) }
-
- it 'does not modify #files' do
- expect(subject[:'values.yaml']).not_to eq(files[:'values.yaml'])
-
- expect(files[:'values.yaml']).to eq(application.values)
- end
-
- it 'returns values.yaml with replaced values' do
- expect(subject[:'values.yaml']).to eq({ hello: :world })
- end
-
- it 'uses values from #files, except for values.yaml' do
- allow(application).to receive(:files).and_return({
- 'values.yaml': 'some value specific to files',
- 'file_a.txt': 'file_a',
- 'file_b.txt': 'file_b'
- })
-
- expect(subject.except(:'values.yaml')).to eq({
- 'file_a.txt': 'file_a',
- 'file_b.txt': 'file_b'
- })
- end
- end
-
- describe '#configured?' do
- let(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
-
- subject { prometheus.configured? }
-
- context 'when a kubenetes client is present' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
-
- it { is_expected.to be_truthy }
-
- context 'when it is not availalble' do
- let(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when the kubernetes URL is blocked' do
- before do
- blocked_ip = '127.0.0.1' # localhost addresses are blocked by default
-
- stub_all_dns(cluster.platform.api_url, ip_address: blocked_ip)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when a kubenetes client is not present' do
- let(:cluster) { create(:cluster) }
-
- it { is_expected.to be_falsy }
- end
- end
-
- describe '#updated_since?' do
- let(:cluster) { create(:cluster) }
- let(:prometheus_app) { build(:clusters_applications_prometheus, cluster: cluster) }
- let(:timestamp) { Time.current - 5.minutes }
-
- around do |example|
- freeze_time { example.run }
- end
-
- before do
- prometheus_app.last_update_started_at = Time.current
- end
-
- context 'when app does not have status failed' do
- it 'returns true when last update started after the timestamp' do
- expect(prometheus_app.updated_since?(timestamp)).to be true
- end
-
- it 'returns false when last update started before the timestamp' do
- expect(prometheus_app.updated_since?(Time.current + 5.minutes)).to be false
- end
- end
-
- context 'when app has status failed' do
- it 'returns false when last update started after the timestamp' do
- prometheus_app.status = 6
-
- expect(prometheus_app.updated_since?(timestamp)).to be false
- end
- end
- end
-
- describe 'alert manager token' do
- subject { create(:clusters_applications_prometheus) }
-
- it 'is autogenerated on creation' do
- expect(subject.alert_manager_token).to match(/\A\h{32}\z/)
- expect(subject.encrypted_alert_manager_token).not_to be_nil
- expect(subject.encrypted_alert_manager_token_iv).not_to be_nil
- end
- end
-end
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
deleted file mode 100644
index 04b5ae9641d..00000000000
--- a/spec/models/clusters/applications/runner_spec.rb
+++ /dev/null
@@ -1,127 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::Runner do
- let(:ci_runner) { create(:ci_runner) }
-
- include_examples 'cluster application core specs', :clusters_applications_runner
- include_examples 'cluster application status specs', :clusters_applications_runner
- include_examples 'cluster application version specs', :clusters_applications_runner
- include_examples 'cluster application helm specs', :clusters_applications_runner
- include_examples 'cluster application initial status specs'
-
- it { is_expected.to belong_to(:runner) }
-
- describe 'default values' do
- it { expect(subject.version).to eq(described_class::VERSION) }
- end
-
- describe '#can_uninstall?' do
- let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
-
- subject { gitlab_runner.can_uninstall? }
-
- it { is_expected.to be_truthy }
- end
-
- describe '#install_command' do
- let(:kubeclient) { double('kubernetes client') }
- let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
-
- subject { gitlab_runner.install_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) }
-
- it 'is initialized with 4 arguments' do
- expect(subject.name).to eq('runner')
- expect(subject.chart).to eq('runner/gitlab-runner')
- expect(subject.version).to eq(Clusters::Applications::Runner::VERSION)
- expect(subject).to be_rbac
- expect(subject.repository).to eq('https://charts.gitlab.io')
- expect(subject.files).to eq(gitlab_runner.files)
- end
-
- context 'on a non rbac enabled cluster' do
- before do
- gitlab_runner.cluster.platform_kubernetes.abac!
- end
-
- it { is_expected.not_to be_rbac }
- end
-
- context 'application failed to install previously' do
- let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') }
-
- it 'is initialized with the locked version' do
- expect(subject.version).to eq(Clusters::Applications::Runner::VERSION)
- end
- end
- end
-
- describe '#files' do
- let(:application) { create(:clusters_applications_runner, runner: ci_runner) }
- let(:values) { subject[:'values.yaml'] }
-
- subject { application.files }
-
- it 'includes runner valid values' do
- expect(values).to include('concurrent')
- expect(values).to include('checkInterval')
- expect(values).to include('rbac')
- expect(values).to include('runners')
- expect(values).to include('privileged: true')
- expect(values).to include('image: ubuntu:16.04')
- expect(values).to include('resources')
- expect(values).to match(/gitlabUrl: ['"]?#{Regexp.escape(Gitlab::Routing.url_helpers.root_url)}/)
- end
-
- context 'with duplicated values on vendor/runner/values.yaml' do
- let(:stub_values) do
- {
- "concurrent" => 4,
- "checkInterval" => 3,
- "rbac" => {
- "create" => false
- },
- "clusterWideAccess" => false,
- "runners" => {
- "privileged" => false,
- "image" => "ubuntu:16.04",
- "builds" => {},
- "services" => {},
- "helpers" => {}
- }
- }
- end
-
- before do
- allow(application).to receive(:chart_values).and_return(stub_values)
- end
-
- it 'overwrites values.yaml' do
- expect(values).to match(/privileged: '?#{application.privileged}/)
- end
- end
- end
-
- describe '#make_uninstalling!' do
- subject { create(:clusters_applications_runner, :scheduled, runner: ci_runner) }
-
- it 'calls prepare_uninstall' do
- expect_next_instance_of(described_class) do |instance|
- expect(instance).to receive(:prepare_uninstall).and_call_original
- end
-
- subject.make_uninstalling!
- end
- end
-
- describe '#post_uninstall' do
- it 'destroys its runner' do
- application_runner = create(:clusters_applications_runner, :scheduled, runner: ci_runner)
-
- expect { application_runner.post_uninstall }.to change { Ci::Runner.count }.by(-1)
- end
- end
-end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 2a2e2899d24..d501325dd90 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching,
-feature_category: :kubernetes_management do
+ feature_category: :deployment_management do
include ReactiveCachingHelpers
include KubernetesHelpers
@@ -23,10 +23,6 @@ feature_category: :kubernetes_management do
it { is_expected.to have_one(:provider_aws) }
it { is_expected.to have_one(:platform_kubernetes) }
it { is_expected.to have_one(:integration_prometheus) }
- it { is_expected.to have_one(:application_helm) }
- it { is_expected.to have_one(:application_ingress) }
- it { is_expected.to have_one(:application_prometheus) }
- it { is_expected.to have_one(:application_runner) }
it { is_expected.to have_many(:kubernetes_namespaces) }
it { is_expected.to have_one(:cluster_project) }
it { is_expected.to have_many(:deployment_clusters) }
@@ -36,8 +32,6 @@ feature_category: :kubernetes_management do
it { is_expected.to delegate_method(:status).to(:provider) }
it { is_expected.to delegate_method(:status_reason).to(:provider) }
- 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 }
it { is_expected.to respond_to :project }
it { is_expected.to be_namespace_per_environment }
@@ -50,15 +44,6 @@ feature_category: :kubernetes_management do
let(:factory_name) { :cluster }
end
- describe 'applications have inverse_of: :cluster option' do
- let(:cluster) { create(:cluster) }
- let!(:helm) { create(:clusters_applications_helm, cluster: cluster) }
-
- it 'does not do a third query when referencing cluster again' do
- expect { cluster.application_helm.cluster }.not_to exceed_query_limit(2)
- end
- end
-
describe '.enabled' do
subject { described_class.enabled }
@@ -693,103 +678,6 @@ feature_category: :kubernetes_management do
end
end
- describe '.with_persisted_applications' do
- let(:cluster) { create(:cluster) }
- let!(:helm) { create(:clusters_applications_helm, :installed, cluster: cluster) }
-
- it 'preloads persisted applications' do
- query_rec = ActiveRecord::QueryRecorder.new do
- described_class.with_persisted_applications.find_by_id(cluster.id).application_helm
- end
-
- expect(query_rec.count).to eq(1)
- end
- end
-
- describe '#persisted_applications' do
- let(:cluster) { create(:cluster) }
-
- subject { cluster.persisted_applications }
-
- context 'when all applications are created' do
- let!(:helm) { create(:clusters_applications_helm, cluster: cluster) }
- let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
- let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
- let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
- let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
- let!(:knative) { create(:clusters_applications_knative, cluster: cluster) }
-
- it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter, knative)
- end
- end
-
- context 'when not all were created' do
- let!(:helm) { create(:clusters_applications_helm, cluster: cluster) }
- let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
-
- it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress)
- end
- end
- end
-
- describe '#applications' do
- let_it_be(:cluster, reload: true) { create(:cluster) }
-
- subject { cluster.applications }
-
- context 'when none of applications are created' do
- it 'returns a list of a new objects' do
- is_expected.not_to be_empty
- end
- end
-
- context 'when applications are created' do
- let(:cluster) { create(:cluster, :with_all_applications) }
-
- it 'returns a list of created applications', :aggregate_failures do
- is_expected.to have_attributes(size: described_class::APPLICATIONS.size)
- is_expected.to all(be_kind_of(::Clusters::Concerns::ApplicationCore))
- is_expected.to all(be_persisted)
- end
- end
- end
-
- describe '#find_or_build_application' do
- let_it_be(:cluster, reload: true) { create(:cluster) }
-
- it 'rejects classes that are not applications' do
- expect do
- cluster.find_or_build_application(Project)
- end.to raise_error(ArgumentError)
- end
-
- context 'when none of applications are created' do
- it 'returns the new application', :aggregate_failures do
- described_class::APPLICATIONS.values.each do |application_class|
- application = cluster.find_or_build_application(application_class)
-
- expect(application).to be_a(application_class)
- expect(application).not_to be_persisted
- end
- end
- end
-
- context 'when application is persisted' do
- let(:cluster) { create(:cluster, :with_all_applications) }
-
- it 'returns the persisted application', :aggregate_failures do
- described_class::APPLICATIONS.each_value do |application_class|
- application = cluster.find_or_build_application(application_class)
-
- expect(application).to be_kind_of(::Clusters::Concerns::ApplicationCore)
- expect(application).to be_persisted
- end
- end
- end
- end
-
describe '#allow_user_defined_namespace?' do
subject { cluster.allow_user_defined_namespace? }
@@ -839,7 +727,7 @@ feature_category: :kubernetes_management do
describe '#all_projects' do
context 'cluster_type is project_type' do
let(:project) { create(:project) }
- let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
+ let(:cluster) { create(:cluster, projects: [project]) }
it 'returns projects' do
expect(cluster.all_projects).to match_array [project]
@@ -849,7 +737,7 @@ feature_category: :kubernetes_management do
context 'cluster_type is group_type' do
let(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
- let(:cluster) { create(:cluster_for_group, :with_installed_helm, groups: [group]) }
+ let(:cluster) { create(:cluster_for_group, groups: [group]) }
it 'returns group projects' do
expect(cluster.all_projects.ids).to match_array [project.id]
@@ -1342,22 +1230,6 @@ feature_category: :kubernetes_management do
expect(cluster.prometheus_adapter).to eq(integration)
end
end
-
- context 'has application_prometheus' do
- let_it_be(:application) { create(:clusters_applications_prometheus, :no_helm_installed, cluster: cluster) }
-
- it 'returns nil' do
- expect(cluster.prometheus_adapter).to be_nil
- end
-
- context 'also has a integration_prometheus' do
- let_it_be(:integration) { create(:clusters_integrations_prometheus, cluster: cluster) }
-
- it 'returns the integration' do
- expect(cluster.prometheus_adapter).to eq(integration)
- end
- end
- end
end
describe '#delete_cached_resources!' do
@@ -1444,36 +1316,6 @@ feature_category: :kubernetes_management do
end
end
- describe '#knative_pre_installed?' do
- subject(:knative_pre_installed?) { cluster.knative_pre_installed? }
-
- before do
- allow(cluster).to receive(:provider).and_return(provider)
- end
-
- context 'without provider' do
- let(:provider) {}
-
- it { is_expected.to eq(false) }
- end
-
- context 'with provider' do
- let(:provider) { instance_double(Clusters::Providers::Aws, knative_pre_installed?: knative_pre_installed?) }
-
- context 'with knative_pre_installed? set to true' do
- let(:knative_pre_installed?) { true }
-
- it { is_expected.to eq(true) }
- end
-
- context 'with knative_pre_installed? set to false' do
- let(:knative_pre_installed?) { false }
-
- it { is_expected.to eq(false) }
- end
- end
- end
-
describe '#platform_kubernetes_active?' do
subject(:platform_kubernetes_active?) { cluster.platform_kubernetes_active? }
@@ -1533,94 +1375,4 @@ feature_category: :kubernetes_management do
end
end
end
-
- describe '#application_helm_available?' do
- subject(:application_helm_available?) { cluster.application_helm_available? }
-
- before do
- allow(cluster).to receive(:application_helm).and_return(application_helm)
- end
-
- context 'without application_helm' do
- let(:application_helm) {}
-
- it { is_expected.to eq(false) }
- end
-
- context 'with application_helm' do
- let(:application_helm) { instance_double(Clusters::Applications::Helm, available?: available?) }
-
- context 'with available? set to true' do
- let(:available?) { true }
-
- it { is_expected.to eq(true) }
- end
-
- context 'with available? set to false' do
- let(:available?) { false }
-
- it { is_expected.to eq(false) }
- end
- end
- end
-
- describe '#application_ingress_available?' do
- subject(:application_ingress_available?) { cluster.application_ingress_available? }
-
- before do
- allow(cluster).to receive(:application_ingress).and_return(application_ingress)
- end
-
- context 'without application_ingress' do
- let(:application_ingress) {}
-
- it { is_expected.to eq(false) }
- end
-
- context 'with application_ingress' do
- let(:application_ingress) { instance_double(Clusters::Applications::Ingress, available?: available?) }
-
- context 'with available? set to true' do
- let(:available?) { true }
-
- it { is_expected.to eq(true) }
- end
-
- context 'with available? set to false' do
- let(:available?) { false }
-
- it { is_expected.to eq(false) }
- end
- end
- end
-
- describe '#application_knative_available?' do
- subject(:application_knative_available?) { cluster.application_knative_available? }
-
- before do
- allow(cluster).to receive(:application_knative).and_return(application_knative)
- end
-
- context 'without application_knative' do
- let(:application_knative) {}
-
- it { is_expected.to eq(false) }
- end
-
- context 'with application_knative' do
- let(:application_knative) { instance_double(Clusters::Applications::Knative, available?: available?) }
-
- context 'with available? set to true' do
- let(:available?) { true }
-
- it { is_expected.to eq(true) }
- end
-
- context 'with available? set to false' do
- let(:available?) { false }
-
- it { is_expected.to eq(false) }
- end
- end
- end
end
diff --git a/spec/models/clusters/integrations/prometheus_spec.rb b/spec/models/clusters/integrations/prometheus_spec.rb
index d6d1105cdb1..f7ab0ae067c 100644
--- a/spec/models/clusters/integrations/prometheus_spec.rb
+++ b/spec/models/clusters/integrations/prometheus_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Clusters::Integrations::Prometheus do
describe 'after_destroy' do
subject(:integration) { create(:clusters_integrations_prometheus, cluster: cluster, enabled: true) }
- let(:cluster) { create(:cluster, :with_installed_helm) }
+ let(:cluster) { create(:cluster) }
it 'deactivates prometheus_integration' do
expect(Clusters::Applications::DeactivateIntegrationWorker)
@@ -41,7 +41,7 @@ RSpec.describe Clusters::Integrations::Prometheus do
describe 'after_save' do
subject(:integration) { create(:clusters_integrations_prometheus, cluster: cluster, enabled: enabled) }
- let(:cluster) { create(:cluster, :with_installed_helm) }
+ let(:cluster) { create(:cluster) }
let(:enabled) { true }
context 'when no change to enabled status' do
diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb
index e70cd15baca..40d381e4dd6 100644
--- a/spec/models/clusters/kubernetes_namespace_spec.rb
+++ b/spec/models/clusters/kubernetes_namespace_spec.rb
@@ -69,10 +69,12 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
context 'when cluster is using the namespace' do
before do
- create(:cluster_kubernetes_namespace,
- cluster: kubernetes_namespace.cluster,
- environment: kubernetes_namespace.environment,
- namespace: 'my-namespace')
+ create(
+ :cluster_kubernetes_namespace,
+ cluster: kubernetes_namespace.cluster,
+ environment: kubernetes_namespace.environment,
+ namespace: 'my-namespace'
+ )
end
it { is_expected.not_to be_valid }
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index b280275c2e5..c32abaf50f5 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -198,10 +198,12 @@ RSpec.describe Clusters::Platforms::Kubernetes do
subject { kubernetes.kubeclient }
before do
- create(:cluster_kubernetes_namespace,
- cluster: kubernetes.cluster,
- cluster_project: kubernetes.cluster.cluster_project,
- project: kubernetes.cluster.cluster_project.project)
+ create(
+ :cluster_kubernetes_namespace,
+ cluster: kubernetes.cluster,
+ cluster_project: kubernetes.cluster.cluster_project,
+ project: kubernetes.cluster.cluster_project.project
+ )
end
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::KubeClient) }
@@ -209,11 +211,13 @@ RSpec.describe Clusters::Platforms::Kubernetes do
context 'ca_pem is a single certificate' do
let(:ca_pem) { File.read(Rails.root.join('spec/fixtures/clusters/root_certificate.pem')) }
let(:kubernetes) do
- build(:cluster_platform_kubernetes,
- :configured,
- namespace: 'a-namespace',
- cluster: cluster,
- ca_pem: ca_pem)
+ build(
+ :cluster_platform_kubernetes,
+ :configured,
+ namespace: 'a-namespace',
+ cluster: cluster,
+ ca_pem: ca_pem
+ )
end
it 'adds it to cert_store' do
@@ -227,11 +231,13 @@ RSpec.describe Clusters::Platforms::Kubernetes do
context 'ca_pem is a chain' do
let(:cert_chain) { File.read(Rails.root.join('spec/fixtures/clusters/chain_certificates.pem')) }
let(:kubernetes) do
- build(:cluster_platform_kubernetes,
- :configured,
- namespace: 'a-namespace',
- cluster: cluster,
- ca_pem: cert_chain)
+ build(
+ :cluster_platform_kubernetes,
+ :configured,
+ namespace: 'a-namespace',
+ cluster: cluster,
+ ca_pem: cert_chain
+ )
end
where(:fixture_path) do
@@ -930,4 +936,13 @@ RSpec.describe Clusters::Platforms::Kubernetes do
end
end
end
+
+ describe '#authorization_type' do
+ subject(:kubernetes) { create(:cluster_platform_kubernetes) }
+
+ let(:attr) { :authorization_type }
+ let(:attr_value) { :unknown_authorization }
+
+ it_behaves_like 'having enum with nil value'
+ end
end
diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb
index 6dd34c3e21f..1d2d89573bb 100644
--- a/spec/models/commit_collection_spec.rb
+++ b/spec/models/commit_collection_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CommitCollection do
+RSpec.describe CommitCollection, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:commit) { project.commit("c1c67abbaf91f624347bb3ae96eabe3a1b742478") }
@@ -45,6 +45,24 @@ RSpec.describe CommitCollection do
end
end
+ describe '#committer_user_ids' do
+ subject(:collection) { described_class.new(project, [commit]) }
+
+ it 'returns an array of committer user IDs' do
+ user = create(:user, email: commit.committer_email)
+
+ expect(collection.committer_user_ids).to contain_exactly(user.id)
+ end
+
+ context 'when there are no committers' do
+ subject(:collection) { described_class.new(project, []) }
+
+ it 'returns an empty array' do
+ expect(collection.committer_user_ids).to be_empty
+ end
+ end
+ end
+
describe '#without_merge_commits' do
it 'returns all commits except merge commits' do
merge_commit = project.commit("60ecb67744cb56576c30214ff52294f8ce2def98")
@@ -191,6 +209,19 @@ RSpec.describe CommitCollection do
end
end
+ describe '#load_tags' do
+ let(:gitaly_commit_with_tags) { project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ let(:collection) { described_class.new(project, [gitaly_commit_with_tags]) }
+
+ subject { collection.load_tags }
+
+ it 'loads tags' do
+ subject
+
+ expect(collection.commits[0].referenced_by).to contain_exactly('refs/tags/v1.1.0')
+ end
+ end
+
describe '#respond_to_missing?' do
it 'returns true when the underlying Array responds to the message' do
collection = described_class.new(project, [])
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 36d0e37454d..edb856d34df 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -92,9 +92,11 @@ RSpec.describe Commit do
end
it 'parses date strings into Time instances' do
- commit = described_class.build_from_sidekiq_hash(project,
- id: '123',
- authored_date: Time.current.to_s)
+ commit = described_class.build_from_sidekiq_hash(
+ project,
+ id: '123',
+ authored_date: Time.current.to_s
+ )
expect(commit.authored_date).to be_a_kind_of(Time)
end
@@ -551,18 +553,22 @@ eos
let(:repository) { project.repository }
let(:merge_request) do
- create(:merge_request,
- source_branch: 'video',
- target_branch: 'master',
- source_project: project,
- author: user)
+ create(
+ :merge_request,
+ source_branch: 'video',
+ target_branch: 'master',
+ source_project: project,
+ author: user
+ )
end
let(:merge_commit) do
- merge_commit_id = repository.merge(user,
- merge_request.diff_head_sha,
- merge_request,
- 'Test message')
+ merge_commit_id = repository.merge(
+ user,
+ merge_request.diff_head_sha,
+ merge_request,
+ 'Test message'
+ )
repository.commit(merge_commit_id)
end
@@ -640,17 +646,21 @@ eos
let(:user2) { build(:user) }
let!(:note1) do
- create(:note_on_commit,
- commit_id: commit.id,
- project: project,
- note: 'foo')
+ create(
+ :note_on_commit,
+ commit_id: commit.id,
+ project: project,
+ note: 'foo'
+ )
end
let!(:note2) do
- create(:note_on_commit,
- commit_id: commit.id,
- project: project,
- note: 'bar')
+ create(
+ :note_on_commit,
+ commit_id: commit.id,
+ project: project,
+ note: 'bar'
+ )
end
before do
@@ -854,11 +864,13 @@ eos
let(:issue) { create(:issue, author: user, project: project) }
it 'returns true if the commit has been reverted' do
- create(:note_on_issue,
- noteable: issue,
- system: true,
- note: commit.revert_description(user),
- project: issue.project)
+ create(
+ :note_on_issue,
+ noteable: issue,
+ system: true,
+ note: commit.revert_description(user),
+ project: issue.project
+ )
expect_next_instance_of(Commit) do |revert_commit|
expect(revert_commit).to receive(:reverts_commit?)
@@ -873,4 +885,94 @@ eos
expect(commit.has_been_reverted?(user, issue.notes_with_associations)).to eq(false)
end
end
+
+ describe '#tipping_refs' do
+ let_it_be(:tag_name) { 'v1.1.0' }
+ let_it_be(:branch_names) { %w[master not-merged-branch v1.1.0] }
+
+ shared_examples 'tipping ref names' do
+ context 'when called without limits' do
+ it 'return tipping refs names' do
+ expect(called_method.call).to eq(expected)
+ end
+ end
+
+ context 'when called with limits' do
+ it 'return tipping refs names' do
+ limit = 1
+ expect(called_method.call(limit).size).to be <= limit
+ end
+ end
+
+ describe '#tipping_branches' do
+ let(:called_method) { ->(limit = 0) { commit.tipping_branches(limit: limit) } }
+ let(:expected) { branch_names }
+
+ it_behaves_like 'with tipping ref names'
+ end
+
+ describe '#tipping_tags' do
+ let(:called_method) { ->(limit = 0) { commit.tipping_tags(limit: limit) } }
+ let(:expected) { [tag_name] }
+
+ it_behaves_like 'with tipping ref names'
+ end
+ end
+ end
+
+ context 'containing refs' do
+ shared_examples 'containing ref names' do
+ context 'without arguments' do
+ it 'returns branch names containing the commit' do
+ expect(ref_containing.call).to eq(containing_refs)
+ end
+ end
+
+ context 'with limit argument' do
+ it 'returns the appropriate amount branch names' do
+ limit = 2
+ expect(ref_containing.call(limit: limit).size).to be <= limit
+ end
+ end
+
+ context 'with tipping refs excluded' do
+ let(:excluded_refs) do
+ project.repository.refs_by_oid(oid: commit_sha, ref_patterns: [ref_prefix]).map { |n| n.delete_prefix(ref_prefix) }
+ end
+
+ it 'returns branch names containing the commit without the one with the commit at tip' do
+ expect(ref_containing.call(excluded_tipped: true)).to eq(containing_refs - excluded_refs)
+ end
+
+ it 'returns the appropriate amount branch names with limit argument' do
+ limit = 2
+ expect(ref_containing.call(limit: limit, excluded_tipped: true).size).to be <= limit
+ end
+ end
+ end
+
+ describe '#branches_containing' do
+ let_it_be(:commit_sha) { project.commit.sha }
+ let_it_be(:containing_refs) { project.repository.branch_names_contains(commit_sha) }
+
+ let(:ref_prefix) { Gitlab::Git::BRANCH_REF_PREFIX }
+
+ let(:ref_containing) { ->(limit: 0, excluded_tipped: false) { commit.branches_containing(exclude_tipped: excluded_tipped, limit: limit) } }
+
+ it_behaves_like 'containing ref names'
+ end
+
+ describe '#tags_containing' do
+ let_it_be(:tag_name) { 'v1.1.0' }
+ let_it_be(:commit_sha) { project.repository.find_tag(tag_name).target_commit.sha }
+ let_it_be(:containing_refs) { %w[v1.1.0 v1.1.1] }
+
+ let(:ref_prefix) { Gitlab::Git::TAG_REF_PREFIX }
+
+ let(:commit) { project.repository.commit(commit_sha) }
+ let(:ref_containing) { ->(limit: 0, excluded_tipped: false) { commit.tags_containing(exclude_tipped: excluded_tipped, limit: limit) } }
+
+ it_behaves_like 'containing ref names'
+ end
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 4ff451af9de..38c45e8c975 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CommitStatus do
+RSpec.describe CommitStatus, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) do
@@ -17,7 +17,11 @@ RSpec.describe CommitStatus do
it_behaves_like 'having unique enum values'
- it { is_expected.to belong_to(:pipeline) }
+ it do
+ is_expected.to belong_to(:pipeline).class_name('Ci::Pipeline')
+ .with_foreign_key(:commit_id).inverse_of(:statuses)
+ end
+
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:auto_canceled_by) }
@@ -25,6 +29,11 @@ RSpec.describe CommitStatus do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) }
+ it { is_expected.to validate_length_of(:stage).is_at_most(255) }
+ it { is_expected.to validate_length_of(:ref).is_at_most(255) }
+ it { is_expected.to validate_length_of(:target_url).is_at_most(255) }
+ it { is_expected.to validate_length_of(:description).is_at_most(255) }
+
it { is_expected.to delegate_method(:sha).to(:pipeline) }
it { is_expected.to delegate_method(:short_sha).to(:pipeline) }
@@ -33,6 +42,7 @@ RSpec.describe CommitStatus do
it { is_expected.to respond_to :running? }
it { is_expected.to respond_to :pending? }
it { is_expected.not_to be_retried }
+ it { expect(described_class.primary_key).to eq('id') }
describe '#author' do
subject { commit_status.author }
@@ -422,29 +432,6 @@ RSpec.describe CommitStatus do
end
end
- describe '.exclude_ignored' do
- subject { described_class.exclude_ignored.order(:id) }
-
- let(:statuses) do
- [create_status(when: 'manual', status: 'skipped'),
- create_status(when: 'manual', status: 'success'),
- create_status(when: 'manual', status: 'failed'),
- create_status(when: 'on_failure', status: 'skipped'),
- create_status(when: 'on_failure', status: 'success'),
- create_status(when: 'on_failure', status: 'failed'),
- create_status(allow_failure: true, status: 'success'),
- create_status(allow_failure: true, status: 'failed'),
- create_status(allow_failure: false, status: 'success'),
- create_status(allow_failure: false, status: 'failed'),
- create_status(allow_failure: true, status: 'manual'),
- create_status(allow_failure: false, status: 'manual')]
- end
-
- it 'returns statuses without what we want to ignore' do
- is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9, 11))
- end
- end
-
describe '.failed_but_allowed' do
subject { described_class.failed_but_allowed.order(:id) }
@@ -578,6 +565,15 @@ RSpec.describe CommitStatus do
end
end
+ describe '.with_type' do
+ let_it_be(:build_job) { create_status(name: 'build job', type: ::Ci::Build) }
+ let_it_be(:bridge_job) { create_status(name: 'bridge job', type: ::Ci::Bridge) }
+
+ it 'returns statuses that match type' do
+ expect(described_class.with_type(::Ci::Build)).to contain_exactly(have_attributes(name: 'build job'))
+ end
+ end
+
describe '#before_sha' do
subject { commit_status.before_sha }
@@ -1062,4 +1058,13 @@ RSpec.describe CommitStatus do
end
end
end
+
+ describe '#failure_reason' do
+ subject(:status) { commit_status }
+
+ let(:attr) { :failure_reason }
+ let(:attr_value) { :unknown_failure }
+
+ it_behaves_like 'having enum with nil value'
+ end
end
diff --git a/spec/models/compare_spec.rb b/spec/models/compare_spec.rb
index dc8429fe77e..2206ed7bfe8 100644
--- a/spec/models/compare_spec.rb
+++ b/spec/models/compare_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Compare do
+RSpec.describe Compare, feature_category: :source_code_management do
include RepoHelpers
let(:project) { create(:project, :public, :repository) }
@@ -10,10 +10,11 @@ RSpec.describe Compare do
let(:start_commit) { sample_image_commit }
let(:head_commit) { sample_commit }
+ let(:straight) { false }
let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, start_commit.id, head_commit.id) }
- subject(:compare) { described_class.new(raw_compare, project) }
+ subject(:compare) { described_class.new(raw_compare, project, straight: straight) }
describe '#cache_key' do
subject { compare.cache_key }
@@ -147,4 +148,33 @@ RSpec.describe Compare do
end
end
end
+
+ describe '#to_param' do
+ subject { compare.to_param }
+
+ let(:start_commit) { another_sample_commit }
+ let(:base_commit) { head_commit }
+
+ it 'returns the range between base and head commits' do
+ is_expected.to eq(from: base_commit.id, to: head_commit.id)
+ end
+
+ context 'when straight mode is on' do
+ let(:straight) { true }
+
+ it 'returns the range between start and head commits' do
+ is_expected.to eq(from: start_commit.id, to: head_commit.id)
+ end
+ end
+
+ context 'when there are no merge base between commits' do
+ before do
+ allow(project).to receive(:merge_base_commit).and_return(nil)
+ end
+
+ it 'returns the range between start and head commits' do
+ is_expected.to eq(from: start_commit.id, to: head_commit.id)
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/atomic_internal_id_spec.rb b/spec/models/concerns/atomic_internal_id_spec.rb
index 5fe3141eb17..625d8fec0fb 100644
--- a/spec/models/concerns/atomic_internal_id_spec.rb
+++ b/spec/models/concerns/atomic_internal_id_spec.rb
@@ -250,11 +250,104 @@ RSpec.describe AtomicInternalId do
end
end
- describe '.track_project_iid!' do
+ describe '.track_namespace_iid!' do
it 'tracks the present value' do
expect do
- ::Issue.track_project_iid!(milestone.project, external_iid)
- end.to change { InternalId.find_by(project: milestone.project, usage: :issues)&.last_value.to_i }.to(external_iid)
+ ::Issue.track_namespace_iid!(milestone.project.project_namespace, external_iid)
+ end.to change {
+ InternalId.find_by(namespace: milestone.project.project_namespace, usage: :issues)&.last_value.to_i
+ }.to(external_iid)
+ end
+ end
+
+ context 'when transitioning a model from one scope to another' do
+ let!(:issue) { build(:issue, project: project) }
+ let(:old_issue_model) do
+ Class.new(ApplicationRecord) do
+ include AtomicInternalId
+
+ self.table_name = :issues
+
+ belongs_to :project
+ belongs_to :namespace
+
+ has_internal_id :iid, scope: :project
+
+ def self.name
+ 'TestClassA'
+ end
+ end
+ end
+
+ let(:old_issue_instance) { old_issue_model.new(issue.attributes) }
+ let(:new_issue_instance) { Issue.new(issue.attributes) }
+
+ it 'generates the iid on the new scope' do
+ # set a random iid, just so that it does not start at 1
+ old_issue_instance.iid = 123
+ old_issue_instance.save!
+
+ # creating a new old_issue_instance increments the iid.
+ expect { old_issue_model.new(issue.attributes).save! }.to change {
+ InternalId.find_by(project: project, usage: :issues)&.last_value.to_i
+ }.from(123).to(124).and(not_change { InternalId.count })
+
+ # creating a new Issue creates a new record in internal_ids, scoped to the namespace.
+ # Given the Issue#has_internal_id -> init definition the internal_ids#last_value would be the
+ # maximum between the old iid value in internal_ids, scoped to the project and max(iid) value from issues
+ # table by namespace_id.
+ # see Issue#has_internal_id
+ expect { new_issue_instance.save! }.to change {
+ InternalId.find_by(namespace: project.project_namespace, usage: :issues)&.last_value.to_i
+ }.to(125).and(change { InternalId.count }.by(1))
+
+ # transition back to project scope would generate overlapping IIDs and raise a duplicate key value error, unless
+ # we cleanup the issues usage scoped to the project first
+ expect { old_issue_model.new(issue.attributes).save! }.to raise_error(ActiveRecord::RecordNotUnique)
+
+ # delete issues usage scoped to te project
+ InternalId.where(project: project, usage: :issues).delete_all
+
+ expect { old_issue_model.new(issue.attributes).save! }.to change {
+ InternalId.find_by(project: project, usage: :issues)&.last_value.to_i
+ }.to(126).and(change { InternalId.count }.by(1))
+ end
+ end
+
+ context 'when models is scoped to namespace and does not have an init proc' do
+ let!(:issue) { build(:issue, namespace: create(:group)) }
+
+ let(:issue_model) do
+ Class.new(ApplicationRecord) do
+ include AtomicInternalId
+
+ self.table_name = :issues
+
+ belongs_to :project
+ belongs_to :namespace
+
+ has_internal_id :iid, scope: :namespace
+
+ def self.name
+ 'TestClass'
+ end
+ end
+ end
+
+ let(:model_instance) { issue_model.new(issue.attributes) }
+
+ it 'generates the iid on the new scope' do
+ expect { model_instance.save! }.to change {
+ InternalId.find_by(namespace: model_instance.namespace, usage: :issues)&.last_value.to_i
+ }.to(1).and(change { InternalId.count }.by(1))
+ end
+
+ it 'supplies a stream of iid values' do
+ expect do
+ issue_model.with_namespace_iid_supply(model_instance.namespace) do |supply|
+ 4.times { supply.next_value }
+ end
+ end.to change { InternalId.find_by(namespace: model_instance.namespace, usage: :issues)&.last_value.to_i }.by(4)
end
end
end
diff --git a/spec/models/concerns/awareness_spec.rb b/spec/models/concerns/awareness_spec.rb
deleted file mode 100644
index 67acacc7bb1..00000000000
--- a/spec/models/concerns/awareness_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Awareness, :clean_gitlab_redis_shared_state do
- subject { create(:user) }
-
- let(:session) { AwarenessSession.for(1) }
-
- describe "when joining a session" do
- it "increases the number of sessions" do
- expect { subject.join(session) }
- .to change { subject.session_ids.size }
- .by(1)
- end
- end
-
- describe "when leaving session" do
- it "decreases the number of sessions" do
- subject.join(session)
-
- expect { subject.leave(session) }
- .to change { subject.session_ids.size }
- .by(-1)
- end
- end
-
- describe "when joining multiple sessions" do
- let(:session2) { AwarenessSession.for(2) }
-
- it "increases number of active sessions for user" do
- expect do
- subject.join(session)
- subject.join(session2)
- end.to change { subject.session_ids.size }
- .by(2)
- end
- end
-end
diff --git a/spec/models/concerns/ci/has_status_spec.rb b/spec/models/concerns/ci/has_status_spec.rb
index 4ef690ca4c1..5e0a430aa13 100644
--- a/spec/models/concerns/ci/has_status_spec.rb
+++ b/spec/models/concerns/ci/has_status_spec.rb
@@ -393,24 +393,26 @@ RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
subject { object.blocked? }
%w[ci_pipeline ci_stage ci_build generic_commit_status].each do |type|
- let(:object) { build(type, status: status) }
+ context "when #{type}" do
+ let(:object) { build(type, status: status) }
- context 'when status is scheduled' do
- let(:status) { :scheduled }
+ context 'when status is scheduled' do
+ let(:status) { :scheduled }
- it { is_expected.to be_truthy }
- end
+ it { is_expected.to be_truthy }
+ end
- context 'when status is manual' do
- let(:status) { :manual }
+ context 'when status is manual' do
+ let(:status) { :manual }
- it { is_expected.to be_truthy }
- end
+ it { is_expected.to be_truthy }
+ end
- context 'when status is created' do
- let(:status) { :created }
+ context 'when status is created' do
+ let(:status) { :created }
- it { is_expected.to be_falsy }
+ it { is_expected.to be_falsy }
+ end
end
end
end
diff --git a/spec/models/concerns/ci/maskable_spec.rb b/spec/models/concerns/ci/maskable_spec.rb
index b57b2b15608..6e648a39f8f 100644
--- a/spec/models/concerns/ci/maskable_spec.rb
+++ b/spec/models/concerns/ci/maskable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Maskable, feature_category: :pipeline_authoring do
+RSpec.describe Ci::Maskable, feature_category: :secrets_management do
let(:variable) { build(:ci_variable) }
describe 'masked value validations' do
diff --git a/spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb b/spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb
deleted file mode 100644
index bb25d7d1665..00000000000
--- a/spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Ci::Partitionable::PartitionedFilter, :aggregate_failures, feature_category: :continuous_integration do
- before do
- create_tables(<<~SQL)
- CREATE TABLE _test_ci_jobs_metadata (
- id serial NOT NULL,
- partition_id int NOT NULL DEFAULT 10,
- name text,
- PRIMARY KEY (id, partition_id)
- ) PARTITION BY LIST(partition_id);
-
- CREATE TABLE _test_ci_jobs_metadata_1
- PARTITION OF _test_ci_jobs_metadata
- FOR VALUES IN (10);
- SQL
- end
-
- let(:model) do
- Class.new(Ci::ApplicationRecord) do
- include Ci::Partitionable::PartitionedFilter
-
- self.primary_key = :id
- self.table_name = :_test_ci_jobs_metadata
-
- def self.name
- 'TestCiJobMetadata'
- end
- end
- end
-
- let!(:record) { model.create! }
-
- let(:where_filter) do
- /WHERE "_test_ci_jobs_metadata"."id" = #{record.id} AND "_test_ci_jobs_metadata"."partition_id" = 10/
- end
-
- describe '#save' do
- it 'uses id and partition_id' do
- record.name = 'test'
- recorder = ActiveRecord::QueryRecorder.new { record.save! }
-
- expect(recorder.log).to include(where_filter)
- expect(record.name).to eq('test')
- end
- end
-
- describe '#update' do
- it 'uses id and partition_id' do
- recorder = ActiveRecord::QueryRecorder.new { record.update!(name: 'test') }
-
- expect(recorder.log).to include(where_filter)
- expect(record.name).to eq('test')
- end
- end
-
- describe '#delete' do
- it 'uses id and partition_id' do
- recorder = ActiveRecord::QueryRecorder.new { record.delete }
-
- expect(recorder.log).to include(where_filter)
- expect(model.count).to be_zero
- end
- end
-
- describe '#destroy' do
- it 'uses id and partition_id' do
- recorder = ActiveRecord::QueryRecorder.new { record.destroy! }
-
- expect(recorder.log).to include(where_filter)
- expect(model.count).to be_zero
- end
- end
-
- def create_tables(table_sql)
- Ci::ApplicationRecord.connection.execute(table_sql)
- end
-end
diff --git a/spec/models/concerns/ci/partitionable/switch_spec.rb b/spec/models/concerns/ci/partitionable/switch_spec.rb
index d955ad223f8..551ae111fa4 100644
--- a/spec/models/concerns/ci/partitionable/switch_spec.rb
+++ b/spec/models/concerns/ci/partitionable/switch_spec.rb
@@ -59,13 +59,14 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
model.include(Ci::Partitionable)
model.partitionable scope: ->(r) { 1 },
- through: { table: :_test_p_ci_jobs_metadata, flag: table_rollout_flag }
+ through: { table: :_test_p_ci_jobs_metadata, flag: table_rollout_flag }
model.belongs_to :job, anonymous_class: jobs_model
- jobs_model.has_one :metadata, anonymous_class: model,
- foreign_key: :job_id, inverse_of: :job,
- dependent: :destroy
+ jobs_model.has_one :metadata,
+ anonymous_class: model,
+ foreign_key: :job_id, inverse_of: :job,
+ dependent: :destroy
allow(Feature::Definition).to receive(:get).and_call_original
allow(Feature::Definition).to receive(:get).with(table_rollout_flag)
diff --git a/spec/models/concerns/ci/partitionable_spec.rb b/spec/models/concerns/ci/partitionable_spec.rb
index 430ef57d493..d41654e547e 100644
--- a/spec/models/concerns/ci/partitionable_spec.rb
+++ b/spec/models/concerns/ci/partitionable_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Ci::Partitionable do
ci_model.include(described_class)
ci_model.partitionable scope: ->(r) { 1 },
- through: { table: :_test_table_name, flag: :some_flag }
+ through: { table: :_test_table_name, flag: :some_flag }
end
it { expect(ci_model.routing_table_name).to eq(:_test_table_name) }
@@ -52,16 +52,16 @@ RSpec.describe Ci::Partitionable do
context 'when partitioned is true' do
let(:partitioned) { true }
- it { expect(ci_model.ancestors).to include(described_class::PartitionedFilter) }
- it { expect(ci_model).to be_partitioned }
+ it { expect(ci_model.ancestors).to include(PartitionedTable) }
+ it { expect(ci_model.partitioning_strategy).to be_a(Gitlab::Database::Partitioning::CiSlidingListStrategy) }
+ it { expect(ci_model.partitioning_strategy.partitioning_key).to eq(:partition_id) }
end
context 'when partitioned is false' do
let(:partitioned) { false }
- it { expect(ci_model.ancestors).not_to include(described_class::PartitionedFilter) }
-
- it { expect(ci_model).not_to be_partitioned }
+ it { expect(ci_model.ancestors).not_to include(PartitionedTable) }
+ it { expect(ci_model).not_to respond_to(:partitioning_strategy) }
end
end
end
diff --git a/spec/models/concerns/ci/track_environment_usage_spec.rb b/spec/models/concerns/ci/track_environment_usage_spec.rb
index d75972c49b5..ad89973eee5 100644
--- a/spec/models/concerns/ci/track_environment_usage_spec.rb
+++ b/spec/models/concerns/ci/track_environment_usage_spec.rb
@@ -8,10 +8,12 @@ RSpec.describe Ci::TrackEnvironmentUsage do
context 'when build is the verify action for the environment' do
let(:build) do
- build_stubbed(:ci_build,
- ref: 'master',
- environment: 'staging',
- options: { environment: { action: 'verify' } })
+ build_stubbed(
+ :ci_build,
+ ref: 'master',
+ environment: 'staging',
+ options: { environment: { action: 'verify' } }
+ )
end
it { is_expected.to be_truthy }
@@ -19,10 +21,12 @@ RSpec.describe Ci::TrackEnvironmentUsage do
context 'when build is not the verify action for the environment' do
let(:build) do
- build_stubbed(:ci_build,
- ref: 'master',
- environment: 'staging',
- options: { environment: { action: 'start' } })
+ build_stubbed(
+ :ci_build,
+ ref: 'master',
+ environment: 'staging',
+ options: { environment: { action: 'start' } }
+ )
end
it { is_expected.to be_falsey }
diff --git a/spec/models/concerns/clusters/agents/authorization_config_scopes_spec.rb b/spec/models/concerns/clusters/agents/authorization_config_scopes_spec.rb
deleted file mode 100644
index a4d1a33b3d5..00000000000
--- a/spec/models/concerns/clusters/agents/authorization_config_scopes_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Agents::AuthorizationConfigScopes do
- describe '.with_available_ci_access_fields' do
- let(:project) { create(:project) }
-
- let!(:agent_authorization_0) { create(:agent_project_authorization, project: project) }
- let!(:agent_authorization_1) { create(:agent_project_authorization, project: project, config: { access_as: {} }) }
- let!(:agent_authorization_2) { create(:agent_project_authorization, project: project, config: { access_as: { agent: {} } }) }
- let!(:impersonate_authorization) { create(:agent_project_authorization, project: project, config: { access_as: { impersonate: {} } }) }
- let!(:ci_user_authorization) { create(:agent_project_authorization, project: project, config: { access_as: { ci_user: {} } }) }
- let!(:ci_job_authorization) { create(:agent_project_authorization, project: project, config: { access_as: { ci_job: {} } }) }
- let!(:unexpected_authorization) { create(:agent_project_authorization, project: project, config: { access_as: { unexpected: {} } }) }
-
- subject { Clusters::Agents::ProjectAuthorization.with_available_ci_access_fields(project) }
-
- it { is_expected.to contain_exactly(agent_authorization_0, agent_authorization_1, agent_authorization_2) }
- end
-end
diff --git a/spec/models/concerns/clusters/agents/authorizations/ci_access/config_scopes_spec.rb b/spec/models/concerns/clusters/agents/authorizations/ci_access/config_scopes_spec.rb
new file mode 100644
index 00000000000..5c69ede11fc
--- /dev/null
+++ b/spec/models/concerns/clusters/agents/authorizations/ci_access/config_scopes_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::Authorizations::CiAccess::ConfigScopes, feature_category: :deployment_management do
+ describe '.with_available_ci_access_fields' do
+ let(:project) { create(:project) }
+
+ let!(:agent_authorization_0) { create(:agent_ci_access_project_authorization, project: project) }
+ let!(:agent_authorization_1) { create(:agent_ci_access_project_authorization, project: project, config: { access_as: {} }) }
+ let!(:agent_authorization_2) { create(:agent_ci_access_project_authorization, project: project, config: { access_as: { agent: {} } }) }
+ let!(:impersonate_authorization) { create(:agent_ci_access_project_authorization, project: project, config: { access_as: { impersonate: {} } }) }
+ let!(:ci_user_authorization) { create(:agent_ci_access_project_authorization, project: project, config: { access_as: { ci_user: {} } }) }
+ let!(:ci_job_authorization) { create(:agent_ci_access_project_authorization, project: project, config: { access_as: { ci_job: {} } }) }
+ let!(:unexpected_authorization) { create(:agent_ci_access_project_authorization, project: project, config: { access_as: { unexpected: {} } }) }
+
+ subject { Clusters::Agents::Authorizations::CiAccess::ProjectAuthorization.with_available_ci_access_fields(project) }
+
+ it { is_expected.to contain_exactly(agent_authorization_0, agent_authorization_1, agent_authorization_2) }
+ end
+end
diff --git a/spec/models/concerns/clusters/agents/authorizations/user_access/scopes_spec.rb b/spec/models/concerns/clusters/agents/authorizations/user_access/scopes_spec.rb
new file mode 100644
index 00000000000..9da7dde0afd
--- /dev/null
+++ b/spec/models/concerns/clusters/agents/authorizations/user_access/scopes_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::Authorizations::UserAccess::Scopes, feature_category: :deployment_management do
+ describe '.for_agent' do
+ let_it_be(:agent_1) { create(:cluster_agent) }
+ let_it_be(:agent_2) { create(:cluster_agent) }
+ let_it_be(:authorization_1) { create(:agent_user_access_project_authorization, agent: agent_1) }
+ let_it_be(:authorization_2) { create(:agent_user_access_project_authorization, agent: agent_2) }
+
+ subject { Clusters::Agents::Authorizations::UserAccess::ProjectAuthorization.for_agent(agent_1) }
+
+ it { is_expected.to contain_exactly(authorization_1) }
+ end
+
+ describe '.preloaded' do
+ let_it_be(:authorization) { create(:agent_user_access_project_authorization) }
+
+ subject { Clusters::Agents::Authorizations::UserAccess::ProjectAuthorization.preloaded }
+
+ it 'preloads the associated entities' do
+ expect(subject.first.association(:agent)).to be_loaded
+ expect(subject.first.agent.association(:project)).to be_loaded
+ end
+ end
+end
diff --git a/spec/models/concerns/database_event_tracking_spec.rb b/spec/models/concerns/database_event_tracking_spec.rb
index 976462b4174..502cecaaf76 100644
--- a/spec/models/concerns/database_event_tracking_spec.rb
+++ b/spec/models/concerns/database_event_tracking_spec.rb
@@ -3,6 +3,10 @@
require 'spec_helper'
RSpec.describe DatabaseEventTracking, :snowplow do
+ before do
+ allow(Gitlab::Tracking).to receive(:database_event).and_call_original
+ end
+
let(:test_class) do
Class.new(ActiveRecord::Base) do
include DatabaseEventTracking
@@ -17,7 +21,7 @@ RSpec.describe DatabaseEventTracking, :snowplow do
context 'if event emmiter failed' do
before do
- allow(Gitlab::Tracking).to receive(:event).and_raise(StandardError) # rubocop:disable RSpec/ExpectGitlabTracking
+ allow(Gitlab::Tracking).to receive(:database_event).and_raise(StandardError) # rubocop:disable RSpec/ExpectGitlabTracking
end
it 'tracks the exception' do
@@ -35,7 +39,7 @@ RSpec.describe DatabaseEventTracking, :snowplow do
it 'does not track the event' do
create_test_class_record
- expect_no_snowplow_event
+ expect_no_snowplow_event(tracking_method: :database_event)
end
end
@@ -46,24 +50,48 @@ RSpec.describe DatabaseEventTracking, :snowplow do
it 'when created' do
create_test_class_record
- expect_snowplow_event(category: category, action: "#{event}_create", label: 'application_setting_terms',
- property: 'create', namespace: nil, "id" => 1)
+ expect_snowplow_event(
+ tracking_method: :database_event,
+ category: category,
+ action: "#{event}_create",
+ label: 'application_setting_terms',
+ property: 'create',
+ namespace: nil,
+ project: nil,
+ "id" => 1
+ )
end
it 'when updated' do
create_test_class_record
test_class.first.update!(id: 3)
- expect_snowplow_event(category: category, action: "#{event}_update", label: 'application_setting_terms',
- property: 'update', namespace: nil, "id" => 3)
+ expect_snowplow_event(
+ tracking_method: :database_event,
+ category: category,
+ action: "#{event}_update",
+ label: 'application_setting_terms',
+ property: 'update',
+ namespace: nil,
+ project: nil,
+ "id" => 3
+ )
end
it 'when destroyed' do
create_test_class_record
test_class.first.destroy!
- expect_snowplow_event(category: category, action: "#{event}_destroy", label: 'application_setting_terms',
- property: 'destroy', namespace: nil, "id" => 1)
+ expect_snowplow_event(
+ tracking_method: :database_event,
+ category: category,
+ action: "#{event}_destroy",
+ label: 'application_setting_terms',
+ property: 'destroy',
+ namespace: nil,
+ project: nil,
+ "id" => 1
+ )
end
end
end
diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb
index bd1afe844ac..9b086e9785e 100644
--- a/spec/models/concerns/deployment_platform_spec.rb
+++ b/spec/models/concerns/deployment_platform_spec.rb
@@ -56,13 +56,23 @@ RSpec.describe DeploymentPlatform do
context 'when project does not have a cluster but has group clusters' do
let!(:default_cluster) do
- create(:cluster, :provided_by_user,
- cluster_type: :group_type, groups: [group], environment_scope: '*')
+ create(
+ :cluster,
+ :provided_by_user,
+ cluster_type: :group_type,
+ groups: [group],
+ environment_scope: '*'
+ )
end
let!(:cluster) do
- create(:cluster, :provided_by_user,
- cluster_type: :group_type, environment_scope: 'review/*', groups: [group])
+ create(
+ :cluster,
+ :provided_by_user,
+ cluster_type: :group_type,
+ environment_scope: 'review/*',
+ groups: [group]
+ )
end
let(:environment) { 'review/name' }
@@ -99,8 +109,13 @@ RSpec.describe DeploymentPlatform do
context 'when parent_group has a cluster with default scope' do
let!(:parent_group_cluster) do
- create(:cluster, :provided_by_user,
- cluster_type: :group_type, environment_scope: '*', groups: [parent_group])
+ create(
+ :cluster,
+ :provided_by_user,
+ cluster_type: :group_type,
+ environment_scope: '*',
+ groups: [parent_group]
+ )
end
it_behaves_like 'matching environment scope'
@@ -108,8 +123,13 @@ RSpec.describe DeploymentPlatform do
context 'when parent_group has a cluster that is an exact match' do
let!(:parent_group_cluster) do
- create(:cluster, :provided_by_user,
- cluster_type: :group_type, environment_scope: 'review/name', groups: [parent_group])
+ create(
+ :cluster,
+ :provided_by_user,
+ cluster_type: :group_type,
+ environment_scope: 'review/name',
+ groups: [parent_group]
+ )
end
it_behaves_like 'matching environment scope'
@@ -160,8 +180,13 @@ RSpec.describe DeploymentPlatform do
let!(:cluster) { create(:cluster, :provided_by_user, environment_scope: 'review/*', projects: [project]) }
let!(:group_default_cluster) do
- create(:cluster, :provided_by_user,
- cluster_type: :group_type, groups: [group], environment_scope: '*')
+ create(
+ :cluster,
+ :provided_by_user,
+ cluster_type: :group_type,
+ groups: [group],
+ environment_scope: '*'
+ )
end
let(:environment) { 'review/name' }
diff --git a/spec/models/concerns/each_batch_spec.rb b/spec/models/concerns/each_batch_spec.rb
index 2c75d4d5c41..75c5cac899b 100644
--- a/spec/models/concerns/each_batch_spec.rb
+++ b/spec/models/concerns/each_batch_spec.rb
@@ -171,4 +171,36 @@ RSpec.describe EachBatch do
end
end
end
+
+ describe '.each_batch_count' do
+ let_it_be(:users) { create_list(:user, 5, updated_at: 1.day.ago) }
+
+ it 'counts the records' do
+ count, last_value = User.each_batch_count
+
+ expect(count).to eq(5)
+ expect(last_value).to eq(nil)
+ end
+
+ context 'when using a different column' do
+ it 'returns correct count' do
+ count, _ = User.each_batch_count(column: :email, of: 2)
+
+ expect(count).to eq(5)
+ end
+ end
+
+ context 'when stopping and resuming the counting' do
+ it 'returns the correct count' do
+ count, last_value = User.each_batch_count(of: 1) do |current_count, _current_value|
+ current_count == 3 # stop when count reaches 3
+ end
+
+ expect(count).to eq(3)
+
+ final_count, _ = User.each_batch_count(of: 1, last_value: last_value, last_count: count)
+ expect(final_count).to eq(5)
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/expirable_spec.rb b/spec/models/concerns/expirable_spec.rb
index 50dfb138ac9..68a25917ce1 100644
--- a/spec/models/concerns/expirable_spec.rb
+++ b/spec/models/concerns/expirable_spec.rb
@@ -3,40 +3,52 @@
require 'spec_helper'
RSpec.describe Expirable do
- describe 'ProjectMember' do
- let_it_be(:no_expire) { create(:project_member) }
- let_it_be(:expire_later) { create(:project_member, expires_at: 8.days.from_now) }
- let_it_be(:expired) { create(:project_member, expires_at: 1.day.from_now) }
+ let_it_be(:no_expire) { create(:project_member) }
+ let_it_be(:expire_later) { create(:project_member, expires_at: 8.days.from_now) }
+ let_it_be(:expired) { create(:project_member, expires_at: 1.day.from_now) }
- before do
- travel_to(3.days.from_now)
- end
+ before do
+ travel_to(3.days.from_now)
+ end
- describe '.expired' do
- it { expect(ProjectMember.expired).to match_array([expired]) }
- end
+ describe '.expired' do
+ it { expect(ProjectMember.expired).to match_array([expired]) }
- describe '.not_expired' do
- it { expect(ProjectMember.not_expired).to include(no_expire, expire_later) }
- it { expect(ProjectMember.not_expired).not_to include(expired) }
- end
+ it 'scopes the query when multiple models are expirable' do
+ expired_access_token = create(:personal_access_token, :expired, user: no_expire.user)
- describe '#expired?' do
- it { expect(no_expire.expired?).to eq(false) }
- it { expect(expire_later.expired?).to eq(false) }
- it { expect(expired.expired?).to eq(true) }
+ expect(PersonalAccessToken.expired.joins(user: :members)).to match_array([expired_access_token])
+ expect(PersonalAccessToken.joins(user: :members).merge(ProjectMember.expired)).to eq([])
end
- describe '#expires?' do
- it { expect(no_expire.expires?).to eq(false) }
- it { expect(expire_later.expires?).to eq(true) }
- it { expect(expired.expires?).to eq(true) }
- end
+ it 'works with a timestamp expired_at field', time_travel_to: '2022-03-14T11:30:00Z' do
+ expired_deploy_token = create(:deploy_token, expires_at: 5.minutes.ago.iso8601)
- describe '#expires_soon?' do
- it { expect(no_expire.expires_soon?).to eq(false) }
- it { expect(expire_later.expires_soon?).to eq(true) }
- it { expect(expired.expires_soon?).to eq(true) }
+ # Here verify that `expires_at` in the SQL uses `Time.current` instead of `Date.current`
+ expect(DeployToken.expired).to match_array([expired_deploy_token])
end
end
+
+ describe '.not_expired' do
+ it { expect(ProjectMember.not_expired).to include(no_expire, expire_later) }
+ it { expect(ProjectMember.not_expired).not_to include(expired) }
+ end
+
+ describe '#expired?' do
+ it { expect(no_expire.expired?).to eq(false) }
+ it { expect(expire_later.expired?).to eq(false) }
+ it { expect(expired.expired?).to eq(true) }
+ end
+
+ describe '#expires?' do
+ it { expect(no_expire.expires?).to eq(false) }
+ it { expect(expire_later.expires?).to eq(true) }
+ it { expect(expired.expires?).to eq(true) }
+ end
+
+ describe '#expires_soon?' do
+ it { expect(no_expire.expires_soon?).to eq(false) }
+ it { expect(expire_later.expires_soon?).to eq(true) }
+ it { expect(expired.expires_soon?).to eq(true) }
+ end
end
diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb
index bd128112113..b5abd114f9a 100644
--- a/spec/models/concerns/has_user_type_spec.rb
+++ b/spec/models/concerns/has_user_type_spec.rb
@@ -2,18 +2,19 @@
require 'spec_helper'
-RSpec.describe User do
+RSpec.describe User, feature_category: :system_access do
specify 'types consistency checks', :aggregate_failures do
expect(described_class::USER_TYPES.keys)
- .to match_array(%w[human ghost alert_bot project_bot support_bot service_user security_bot visual_review_bot
- migration_bot automation_bot admin_bot suggested_reviewers_bot])
+ .to match_array(%w[human human_deprecated ghost alert_bot project_bot support_bot service_user security_bot
+ visual_review_bot migration_bot automation_bot security_policy_bot admin_bot suggested_reviewers_bot
+ service_account llm_bot])
expect(described_class::USER_TYPES).to include(*described_class::BOT_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::NON_INTERNAL_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::INTERNAL_USER_TYPES)
end
describe 'scopes & predicates' do
- User::USER_TYPES.keys.each do |type|
+ User::USER_TYPES.keys.each do |type| # rubocop:disable RSpec/UselessDynamicDefinition
let_it_be(type) { create(:user, username: type, user_type: type) }
end
let(:bots) { User::BOT_USER_TYPES.map { |type| public_send(type) } }
@@ -22,7 +23,13 @@ RSpec.describe User do
describe '.humans' do
it 'includes humans only' do
- expect(described_class.humans).to match_array([human])
+ expect(described_class.humans).to match_array([human, human_deprecated])
+ end
+ end
+
+ describe '.human' do
+ it 'includes humans only' do
+ expect(described_class.human).to match_array([human, human_deprecated])
end
end
@@ -69,6 +76,7 @@ RSpec.describe User do
describe '#human?' do
it 'is true for humans only' do
expect(human).to be_human
+ expect(human_deprecated).to be_human
expect(alert_bot).not_to be_human
expect(User.new).to be_human
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 206b3ae61cf..e11c7e98287 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -150,8 +150,10 @@ RSpec.describe Issuable do
end
it 'gives preference to state_id if present' do
- issuable = MergeRequest.new('state' => 'opened',
- 'state_id' => described_class::STATE_ID_MAP['merged'])
+ issuable = MergeRequest.new(
+ 'state' => 'opened',
+ 'state_id' => described_class::STATE_ID_MAP['merged']
+ )
expect(issuable.state).to eq('merged')
expect(issuable.state_id).to eq(described_class::STATE_ID_MAP['merged'])
@@ -858,22 +860,16 @@ RSpec.describe Issuable do
end
end
- describe '#first_contribution?' do
+ describe '#first_contribution?', feature_category: :code_review_workflow do
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:other_project) { create(:project) }
- let(:owner) { create(:owner) }
- let(:maintainer) { create(:user) }
- let(:reporter) { create(:user) }
let(:guest) { create(:user) }
let(:contributor) { create(:user) }
let(:first_time_contributor) { create(:user) }
before do
- group.add_owner(owner)
- project.add_maintainer(maintainer)
- project.add_reporter(reporter)
project.add_guest(guest)
project.add_guest(contributor)
project.add_guest(first_time_contributor)
@@ -884,24 +880,6 @@ RSpec.describe Issuable do
let(:merged_mr_other_project) { create(:merge_request, :merged, author: first_time_contributor, target_project: other_project, source_project: other_project) }
context "for merge requests" do
- it "is false for MAINTAINER" do
- mr = create(:merge_request, author: maintainer, target_project: project, source_project: project)
-
- expect(mr).not_to be_first_contribution
- end
-
- it "is false for OWNER" do
- mr = create(:merge_request, author: owner, target_project: project, source_project: project)
-
- expect(mr).not_to be_first_contribution
- end
-
- it "is false for REPORTER" do
- mr = create(:merge_request, author: reporter, target_project: project, source_project: project)
-
- expect(mr).not_to be_first_contribution
- end
-
it "is true when you don't have any merged MR" do
expect(open_mr).to be_first_contribution
expect(merged_mr).not_to be_first_contribution
@@ -998,7 +976,7 @@ RSpec.describe Issuable do
end
end
- describe '#incident?' do
+ describe '#incident_type_issue?' do
where(:issuable_type, :incident) do
:issue | false
:incident | true
@@ -1008,7 +986,7 @@ RSpec.describe Issuable do
with_them do
let(:issuable) { build_stubbed(issuable_type) }
- subject { issuable.incident? }
+ subject { issuable.incident_type_issue? }
it { is_expected.to eq(incident) }
end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 7bbbd10ec8d..d9e53fb7e9a 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -225,7 +225,7 @@ RSpec.describe Commit, 'Mentionable' do
end
context 'with external issue tracker' do
- let(:project) { create(:project, :with_jira_integration, :repository) }
+ let_it_be(:project) { create(:project, :with_jira_integration, :repository) }
it 'is true if external issues referenced' do
allow(commit.raw).to receive(:message).and_return 'JIRA-123'
diff --git a/spec/models/concerns/prometheus_adapter_spec.rb b/spec/models/concerns/prometheus_adapter_spec.rb
index d3a44ac8403..31ab8c23a84 100644
--- a/spec/models/concerns/prometheus_adapter_spec.rb
+++ b/spec/models/concerns/prometheus_adapter_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
include PrometheusHelpers
include ReactiveCachingHelpers
- let(:project) { create(:project, :with_prometheus_integration) }
+ let_it_be(:project) { create(:project, :with_prometheus_integration) }
let(:integration) { project.prometheus_integration }
let(:described_class) do
diff --git a/spec/models/concerns/protected_ref_access_spec.rb b/spec/models/concerns/protected_ref_access_spec.rb
deleted file mode 100644
index 750a5eba303..00000000000
--- a/spec/models/concerns/protected_ref_access_spec.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ProtectedRefAccess do
- include ExternalAuthorizationServiceHelpers
-
- subject(:protected_ref_access) do
- create(:protected_branch, :maintainers_can_push).push_access_levels.first
- end
-
- let(:project) { protected_ref_access.project }
-
- describe '#check_access' do
- it 'is always true for admins' do
- admin = create(:admin)
-
- expect(protected_ref_access.check_access(admin)).to be_truthy
- end
-
- it 'is true for maintainers' do
- maintainer = create(:user)
- project.add_maintainer(maintainer)
-
- expect(protected_ref_access.check_access(maintainer)).to be_truthy
- end
-
- it 'is for developers of the project' do
- developer = create(:user)
- project.add_developer(developer)
-
- expect(protected_ref_access.check_access(developer)).to be_falsy
- end
-
- context 'external authorization' do
- it 'is false if external authorization denies access' do
- maintainer = create(:user)
- project.add_maintainer(maintainer)
- external_service_deny_access(maintainer, project)
-
- expect(protected_ref_access.check_access(maintainer)).to be_falsey
- end
- end
- end
-end
diff --git a/spec/models/concerns/redis_cacheable_spec.rb b/spec/models/concerns/redis_cacheable_spec.rb
index c270f23defb..bf277b094ea 100644
--- a/spec/models/concerns/redis_cacheable_spec.rb
+++ b/spec/models/concerns/redis_cacheable_spec.rb
@@ -50,6 +50,58 @@ RSpec.describe RedisCacheable do
subject
end
+
+ context 'with existing cached attributes' do
+ before do
+ instance.cache_attributes({ existing_attr: 'value' })
+ end
+
+ it 'sets the cache attributes' do
+ Gitlab::Redis::Cache.with do |redis|
+ expect(redis).to receive(:set).with(cache_key, payload.to_json, anything).and_call_original
+ end
+
+ expect { subject }.to change { instance.cached_attribute(:existing_attr) }.from('value').to(nil)
+ end
+ end
+ end
+
+ describe '#merge_cache_attributes' do
+ subject { instance.merge_cache_attributes(payload) }
+
+ let(:existing_attributes) { { existing_attr: 'value', name: 'value' } }
+
+ before do
+ instance.cache_attributes(existing_attributes)
+ end
+
+ context 'with different attribute values' do
+ let(:payload) { { name: 'new_value' } }
+
+ it 'merges the cache attributes with existing values' do
+ Gitlab::Redis::Cache.with do |redis|
+ expect(redis).to receive(:set).with(cache_key, existing_attributes.merge(payload).to_json, anything)
+ .and_call_original
+ end
+
+ subject
+
+ expect(instance.cached_attribute(:existing_attr)).to eq 'value'
+ expect(instance.cached_attribute(:name)).to eq 'new_value'
+ end
+ end
+
+ context 'with no new or changed attribute values' do
+ let(:payload) { { name: 'value' } }
+
+ it 'does not try to set Redis key' do
+ Gitlab::Redis::Cache.with do |redis|
+ expect(redis).not_to receive(:set)
+ end
+
+ subject
+ end
+ end
end
describe '#cached_attr_reader', :clean_gitlab_redis_cache do
diff --git a/spec/models/concerns/require_email_verification_spec.rb b/spec/models/concerns/require_email_verification_spec.rb
index 0a6293f852e..1fb54e4276f 100644
--- a/spec/models/concerns/require_email_verification_spec.rb
+++ b/spec/models/concerns/require_email_verification_spec.rb
@@ -15,24 +15,20 @@ RSpec.describe RequireEmailVerification, feature_category: :insider_threat do
using RSpec::Parameterized::TableSyntax
- where(:feature_flag_enabled, :two_factor_enabled, :skipped, :overridden) do
- false | false | false | false
- false | false | true | false
- false | true | false | false
- false | true | true | false
- true | false | false | true
- true | false | true | false
- true | true | false | false
- true | true | true | false
- end
+ where(feature_flag_enabled: [true, false],
+ two_factor_enabled: [true, false],
+ oauth_user: [true, false],
+ skipped: [true, false])
with_them do
let(:instance) { model.new(id: 1) }
let(:another_instance) { model.new(id: 2) }
+ let(:overridden) { feature_flag_enabled && !two_factor_enabled && !oauth_user && !skipped }
before do
stub_feature_flags(require_email_verification: feature_flag_enabled ? instance : another_instance)
allow(instance).to receive(:two_factor_enabled?).and_return(two_factor_enabled)
+ allow(instance).to receive(:identities).and_return(oauth_user ? [:google] : [])
stub_feature_flags(skip_require_email_verification: skipped ? instance : another_instance)
end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index dc1002f3560..0bbe3dea812 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -74,6 +74,17 @@ RSpec.shared_examples 'routable resource with parent' do
describe '#full_name' do
it { expect(record.full_name).to eq "#{record.parent.human_name} / #{record.name}" }
+ context 'without route name' do
+ before do
+ stub_feature_flags(cached_route_lookups: true)
+ record.route.update_attribute(:name, nil)
+ end
+
+ it 'builds full name' do
+ expect(record.full_name).to eq("#{record.parent.human_name} / #{record.name}")
+ end
+ end
+
it 'hits the cache when not preloaded' do
forcibly_hit_cached_lookup(record, :full_name)
diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb
index a60a0a5e26d..80060802de8 100644
--- a/spec/models/concerns/subscribable_spec.rb
+++ b/spec/models/concerns/subscribable_spec.rb
@@ -215,5 +215,31 @@ RSpec.describe Subscribable, 'Subscribable' do
expect(lazy_queries.count).to eq(0)
expect(preloaded_queries.count).to eq(1)
end
+
+ context 'with work items' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:work_item) { create(:work_item, :task, project: project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ before do
+ [issue, work_item].each do |item|
+ create(:subscription, user: user, subscribable: item, subscribed: true, project: project)
+ end
+ end
+
+ it 'loads correct subscribable type' do
+ expect(issue).to receive(:subscribable_type).and_return('Issue')
+ issue.lazy_subscription(user, project)
+
+ expect(work_item).to receive(:subscribable_type).and_return('Issue')
+ work_item.lazy_subscription(user, project)
+ end
+
+ it 'matches existing subscription type' do
+ expect(issue.subscribed?(user, project)).to eq(true)
+ expect(work_item.subscribed?(user, project)).to eq(true)
+ end
+ end
end
end
diff --git a/spec/models/concerns/taskable_spec.rb b/spec/models/concerns/taskable_spec.rb
index 20de8995d13..dad0db2d898 100644
--- a/spec/models/concerns/taskable_spec.rb
+++ b/spec/models/concerns/taskable_spec.rb
@@ -27,6 +27,9 @@ RSpec.describe Taskable, feature_category: :team_planning do
+ [ ] Narrow no-break space (U+202F)
+ [ ] Thin space (U+2009)
+
+ 1. [ ] Numbered 1
+ 2) [x] Numbered 2
MARKDOWN
end
@@ -35,7 +38,9 @@ RSpec.describe Taskable, feature_category: :team_planning do
TaskList::Item.new('- [ ]', 'First item'),
TaskList::Item.new('- [x]', 'Second item'),
TaskList::Item.new('* [x]', 'First item'),
- TaskList::Item.new('* [ ]', 'Second item')
+ TaskList::Item.new('* [ ]', 'Second item'),
+ TaskList::Item.new('1. [ ]', 'Numbered 1'),
+ TaskList::Item.new('2) [x]', 'Numbered 2')
]
end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index e53fdafe3b1..7367577914c 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -130,10 +130,7 @@ RSpec.describe PersonalAccessToken, 'TokenAuthenticatable' do
let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
let(:user) { create(:user) }
let(:personal_access_token) do
- described_class.new(name: 'test-pat-01',
- user_id: user.id,
- scopes: [:api],
- token_digest: token_digest)
+ described_class.new(name: 'test-pat-01', user_id: user.id, scopes: [:api], token_digest: token_digest)
end
before do
diff --git a/spec/models/concerns/token_authenticatable_strategies/base_spec.rb b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
index 89ddc797a9d..2679bd7d93b 100644
--- a/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TokenAuthenticatableStrategies::Base do
+RSpec.describe TokenAuthenticatableStrategies::Base, feature_category: :system_access do
let(:instance) { double(:instance) }
let(:field) { double(:field) }
@@ -24,6 +24,41 @@ RSpec.describe TokenAuthenticatableStrategies::Base do
end
end
+ describe '#format_token' do
+ let(:strategy) { described_class.new(instance, field, options) }
+
+ let(:instance) { build(:ci_build, name: 'build_name_for_format_option', partition_id: partition_id) }
+ let(:partition_id) { 100 }
+ let(:field) { 'token' }
+ let(:options) { {} }
+
+ let(:token) { 'a_secret_token' }
+
+ it 'returns the origin token' do
+ expect(strategy.format_token(instance, token)).to eq(token)
+ end
+
+ context 'when format_with_prefix option is provided' do
+ context 'with symbol' do
+ let(:options) { { format_with_prefix: :partition_id_prefix_in_16_bit_encode } }
+ let(:partition_id_in_16_bit_encode_with_underscore) { "#{partition_id.to_s(16)}_" }
+ let(:formatted_token) { "#{partition_id_in_16_bit_encode_with_underscore}#{token}" }
+
+ it 'returns a formatted token from the format_with_prefix option' do
+ expect(strategy.format_token(instance, token)).to eq(formatted_token)
+ end
+ end
+
+ context 'with something else' do
+ let(:options) { { format_with_prefix: false } }
+
+ it 'raise not implemented' do
+ expect { strategy.format_token(instance, token) }.to raise_error(NotImplementedError)
+ end
+ end
+ end
+ end
+
describe '.fabricate' do
context 'when digest stragegy is specified' do
it 'fabricates digest strategy object' do
diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
index 2df86804f34..3bdb0647d62 100644
--- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
@@ -2,16 +2,20 @@
require 'spec_helper'
-RSpec.describe TokenAuthenticatableStrategies::Encrypted do
+RSpec.describe TokenAuthenticatableStrategies::Encrypted, feature_category: :system_access do
let(:model) { double(:model) }
let(:instance) { double(:instance) }
+ let(:original_token) { 'my-value' }
+ let(:resource) { double(:resource) }
+ let(:options) { other_options.merge(encrypted: encrypted_option) }
+ let(:other_options) { {} }
let(:encrypted) do
- TokenAuthenticatableStrategies::EncryptionHelper.encrypt_token('my-value')
+ TokenAuthenticatableStrategies::EncryptionHelper.encrypt_token(original_token)
end
let(:encrypted_with_static_iv) do
- Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value')
+ Gitlab::CryptoHelper.aes256_gcm_encrypt(original_token)
end
subject(:strategy) do
@@ -19,7 +23,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
end
describe '#token_fields' do
- let(:options) { { encrypted: :required } }
+ let(:encrypted_option) { :required }
it 'includes the encrypted field' do
expect(strategy.token_fields).to contain_exactly('some_field', 'some_field_encrypted')
@@ -27,50 +31,70 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
end
describe '#find_token_authenticatable' do
+ shared_examples 'finds the resource' do
+ it 'finds the resource by cleartext' do
+ expect(subject.find_token_authenticatable(original_token))
+ .to eq(resource)
+ end
+ end
+
+ shared_examples 'does not find any resource' do
+ it 'does not find any resource by cleartext' do
+ expect(subject.find_token_authenticatable(original_token))
+ .to be_nil
+ end
+ end
+
+ shared_examples 'finds the resource with/without setting require_prefix_for_validation' do
+ let(:standard_runner_token_prefix) { 'GR1348941' }
+ it_behaves_like 'finds the resource'
+
+ context 'when a require_prefix_for_validation is provided' do
+ let(:other_options) { { format_with_prefix: :format_with_prefix_method, require_prefix_for_validation: true } }
+
+ before do
+ allow(resource).to receive(:format_with_prefix_method).and_return(standard_runner_token_prefix)
+ end
+
+ it_behaves_like 'does not find any resource'
+
+ context 'when token starts with prefix' do
+ let(:original_token) { "#{standard_runner_token_prefix}plain_token" }
+
+ it_behaves_like 'finds the resource'
+ end
+ end
+ end
+
context 'when encryption is required' do
- let(:options) { { encrypted: :required } }
+ let(:encrypted_option) { :required }
+ let(:resource) { double(:encrypted_resource) }
- it 'finds the encrypted resource by cleartext' do
+ before do
allow(model).to receive(:where)
.and_return(model)
allow(model).to receive(:find_by)
.with('some_field_encrypted' => [encrypted, encrypted_with_static_iv])
- .and_return('encrypted resource')
-
- expect(subject.find_token_authenticatable('my-value'))
- .to eq 'encrypted resource'
+ .and_return(resource)
end
- context 'when a prefix is required' do
- let(:options) { { encrypted: :required, prefix: 'GR1348941' } }
-
- it 'finds the encrypted resource by cleartext' do
- allow(model).to receive(:where)
- .and_return(model)
- allow(model).to receive(:find_by)
- .with('some_field_encrypted' => [encrypted, encrypted_with_static_iv])
- .and_return('encrypted resource')
-
- expect(subject.find_token_authenticatable('my-value'))
- .to be_nil
- end
- end
+ it_behaves_like 'finds the resource with/without setting require_prefix_for_validation'
end
context 'when encryption is optional' do
- let(:options) { { encrypted: :optional } }
+ let(:encrypted_option) { :optional }
+ let(:resource) { double(:encrypted_resource) }
- it 'finds the encrypted resource by cleartext' do
+ before do
allow(model).to receive(:where)
.and_return(model)
allow(model).to receive(:find_by)
.with('some_field_encrypted' => [encrypted, encrypted_with_static_iv])
- .and_return('encrypted resource')
-
- expect(subject.find_token_authenticatable('my-value'))
- .to eq 'encrypted resource'
+ .and_return(resource)
end
+ it_behaves_like 'finds the resource with/without setting require_prefix_for_validation'
+
it 'uses insecure strategy when encrypted token cannot be found' do
allow(subject.send(:insecure_strategy))
.to receive(:find_token_authenticatable)
@@ -85,68 +109,27 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
expect(subject.find_token_authenticatable('my-value'))
.to eq 'plaintext resource'
end
-
- context 'when a prefix is required' do
- let(:options) { { encrypted: :optional, prefix: 'GR1348941' } }
-
- it 'finds the encrypted resource by cleartext' do
- allow(model).to receive(:where)
- .and_return(model)
- allow(model).to receive(:find_by)
- .with('some_field_encrypted' => [encrypted, encrypted_with_static_iv])
- .and_return('encrypted resource')
-
- expect(subject.find_token_authenticatable('my-value'))
- .to be_nil
- end
- end
end
context 'when encryption is migrating' do
- let(:options) { { encrypted: :migrating } }
+ let(:encrypted_option) { :migrating }
+ let(:resource) { double(:cleartext_resource) }
- it 'finds the cleartext resource by cleartext' do
+ before do
allow(model).to receive(:where)
.and_return(model)
allow(model).to receive(:find_by)
- .with('some_field' => 'my-value')
- .and_return('cleartext resource')
-
- expect(subject.find_token_authenticatable('my-value'))
- .to eq 'cleartext resource'
+ .with('some_field' => original_token)
+ .and_return(resource)
end
- it 'returns nil if resource cannot be found' do
- allow(model).to receive(:where)
- .and_return(model)
- allow(model).to receive(:find_by)
- .with('some_field' => 'my-value')
- .and_return(nil)
-
- expect(subject.find_token_authenticatable('my-value'))
- .to be_nil
- end
-
- context 'when a prefix is required' do
- let(:options) { { encrypted: :migrating, prefix: 'GR1348941' } }
-
- it 'finds the encrypted resource by cleartext' do
- allow(model).to receive(:where)
- .and_return(model)
- allow(model).to receive(:find_by)
- .with('some_field' => 'my-value')
- .and_return('cleartext resource')
-
- expect(subject.find_token_authenticatable('my-value'))
- .to be_nil
- end
- end
+ it_behaves_like 'finds the resource with/without setting require_prefix_for_validation'
end
end
describe '#get_token' do
context 'when encryption is required' do
- let(:options) { { encrypted: :required } }
+ let(:encrypted_option) { :required }
it 'returns decrypted token when an encrypted with static iv token is present' do
allow(instance).to receive(:read_attribute)
@@ -166,7 +149,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
end
context 'when encryption is optional' do
- let(:options) { { encrypted: :optional } }
+ let(:encrypted_option) { :optional }
it 'returns decrypted token when an encrypted token is present' do
allow(instance).to receive(:read_attribute)
@@ -198,7 +181,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
end
context 'when encryption is migrating' do
- let(:options) { { encrypted: :migrating } }
+ let(:encrypted_option) { :migrating }
it 'returns cleartext token when an encrypted token is present' do
allow(instance).to receive(:read_attribute)
@@ -228,7 +211,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
describe '#set_token' do
context 'when encryption is required' do
- let(:options) { { encrypted: :required } }
+ let(:encrypted_option) { :required }
it 'writes encrypted token and returns it' do
expect(instance).to receive(:[]=)
@@ -239,7 +222,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
end
context 'when encryption is optional' do
- let(:options) { { encrypted: :optional } }
+ let(:encrypted_option) { :optional }
it 'writes encrypted token and removes plaintext token and returns it' do
expect(instance).to receive(:[]=)
@@ -252,7 +235,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
end
context 'when encryption is migrating' do
- let(:options) { { encrypted: :migrating } }
+ let(:encrypted_option) { :migrating }
it 'writes encrypted token and writes plaintext token' do
expect(instance).to receive(:[]=)
diff --git a/spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb
index 671e51e3913..004298bbdd9 100644
--- a/spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb
@@ -6,96 +6,26 @@ RSpec.describe TokenAuthenticatableStrategies::EncryptionHelper do
let(:encrypted_token) { described_class.encrypt_token('my-value-my-value-my-value') }
describe '.encrypt_token' do
- context 'when dynamic_nonce feature flag is switched on' do
- it 'adds nonce identifier on the beginning' do
- expect(encrypted_token.first).to eq(described_class::DYNAMIC_NONCE_IDENTIFIER)
- end
-
- it 'adds nonce at the end' do
- nonce = encrypted_token.last(described_class::NONCE_SIZE)
-
- expect(nonce).to eq(::Digest::SHA256.hexdigest('my-value-my-value-my-value').bytes.take(described_class::NONCE_SIZE).pack('c*'))
- end
-
- it 'encrypts token' do
- expect(encrypted_token[1...-described_class::NONCE_SIZE]).not_to eq('my-value-my-value-my-value')
- end
+ it 'adds nonce identifier on the beginning' do
+ expect(encrypted_token.first).to eq(described_class::DYNAMIC_NONCE_IDENTIFIER)
end
- context 'when dynamic_nonce feature flag is switched off' do
- before do
- stub_feature_flags(dynamic_nonce: false)
- end
-
- it 'does not add nonce identifier on the beginning' do
- expect(encrypted_token.first).not_to eq(described_class::DYNAMIC_NONCE_IDENTIFIER)
- end
-
- it 'does not add nonce in the end' do
- nonce = encrypted_token.last(described_class::NONCE_SIZE)
-
- expect(nonce).not_to eq(::Digest::SHA256.hexdigest('my-value-my-value-my-value').bytes.take(described_class::NONCE_SIZE).pack('c*'))
- end
+ it 'adds nonce at the end' do
+ nonce = encrypted_token.last(described_class::NONCE_SIZE)
- it 'encrypts token with static iv' do
- token = Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value-my-value-my-value')
+ expect(nonce).to eq(::Digest::SHA256.hexdigest('my-value-my-value-my-value').bytes.take(described_class::NONCE_SIZE).pack('c*'))
+ end
- expect(encrypted_token).to eq(token)
- end
+ it 'encrypts token' do
+ expect(encrypted_token[1...-described_class::NONCE_SIZE]).not_to eq('my-value-my-value-my-value')
end
end
describe '.decrypt_token' do
- context 'with feature flag switched off' do
- before do
- stub_feature_flags(dynamic_nonce: false)
- end
-
- it 'decrypts token with static iv' do
- encrypted_token = described_class.encrypt_token('my-value')
-
- expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
- end
-
- it 'decrypts token if feature flag changed after encryption' do
- encrypted_token = described_class.encrypt_token('my-value')
-
- expect(encrypted_token).not_to eq('my-value')
-
- stub_feature_flags(dynamic_nonce: true)
-
- expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
- end
-
- it 'decrypts token with dynamic iv' do
- iv = ::Digest::SHA256.hexdigest('my-value').bytes.take(described_class::NONCE_SIZE).pack('c*')
- token = Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value', nonce: iv)
- encrypted_token = "#{described_class::DYNAMIC_NONCE_IDENTIFIER}#{token}#{iv}"
-
- expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
- end
- end
-
- context 'with feature flag switched on' do
- before do
- stub_feature_flags(dynamic_nonce: true)
- end
-
- it 'decrypts token with dynamic iv' do
- encrypted_token = described_class.encrypt_token('my-value')
-
- expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
- end
-
- it 'decrypts token if feature flag changed after encryption' do
- encrypted_token = described_class.encrypt_token('my-value')
-
- expect(encrypted_token).not_to eq('my-value')
-
- stub_feature_flags(dynamic_nonce: false)
+ it 'decrypts token with dynamic iv' do
+ encrypted_token = described_class.encrypt_token('my-value')
- expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
- end
+ expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
end
end
end
diff --git a/spec/models/concerns/uniquify_spec.rb b/spec/models/concerns/uniquify_spec.rb
deleted file mode 100644
index 9b79e4d4154..00000000000
--- a/spec/models/concerns/uniquify_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Uniquify do
- let(:uniquify) { described_class.new }
-
- describe "#string" do
- it 'returns the given string if it does not exist' do
- result = uniquify.string('test_string') { |s| false }
-
- expect(result).to eq('test_string')
- end
-
- it 'returns the given string with a counter attached if the string exists' do
- result = uniquify.string('test_string') { |s| s == 'test_string' }
-
- expect(result).to eq('test_string1')
- end
-
- it 'increments the counter for each candidate string that also exists' do
- result = uniquify.string('test_string') { |s| s == 'test_string' || s == 'test_string1' }
-
- expect(result).to eq('test_string2')
- end
-
- it 'allows to pass an initial value for the counter' do
- start_counting_from = 2
- uniquify = described_class.new(start_counting_from)
-
- result = uniquify.string('test_string') { |s| s == 'test_string' }
-
- expect(result).to eq('test_string2')
- end
-
- it 'allows passing in a base function that defines the location of the counter' do
- result = uniquify.string(-> (counter) { "test_#{counter}_string" }) do |s|
- s == 'test__string'
- end
-
- expect(result).to eq('test_1_string')
- end
- end
-end
diff --git a/spec/models/concerns/web_hooks/has_web_hooks_spec.rb b/spec/models/concerns/web_hooks/has_web_hooks_spec.rb
new file mode 100644
index 00000000000..afb2406a969
--- /dev/null
+++ b/spec/models/concerns/web_hooks/has_web_hooks_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WebHooks::HasWebHooks, feature_category: :integrations do
+ let(:minimal_test_class) do
+ Class.new do
+ include WebHooks::HasWebHooks
+
+ def id
+ 1
+ end
+ end
+ end
+
+ before do
+ stub_const('MinimalTestClass', minimal_test_class)
+ end
+
+ describe '#last_failure_redis_key' do
+ subject { MinimalTestClass.new.last_failure_redis_key }
+
+ it { is_expected.to eq('web_hooks:last_failure:minimal_test_class-1') }
+ end
+
+ describe 'last_webhook_failure', :clean_gitlab_redis_shared_state do
+ subject { MinimalTestClass.new.last_webhook_failure }
+
+ it { is_expected.to eq(nil) }
+
+ context 'when there was an older failure', :clean_gitlab_redis_shared_state do
+ let(:last_failure_date) { 1.month.ago.iso8601 }
+
+ before do
+ Gitlab::Redis::SharedState.with { |r| r.set('web_hooks:last_failure:minimal_test_class-1', last_failure_date) }
+ end
+
+ it { is_expected.to eq(last_failure_date) }
+ end
+ end
+end
diff --git a/spec/models/container_registry/data_repair_detail_spec.rb b/spec/models/container_registry/data_repair_detail_spec.rb
new file mode 100644
index 00000000000..4d2ac5fff42
--- /dev/null
+++ b/spec/models/container_registry/data_repair_detail_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::DataRepairDetail, type: :model, feature_category: :container_registry do
+ let_it_be(:project) { create(:project) }
+
+ subject { described_class.new(project: project) }
+
+ it { is_expected.to belong_to(:project).required }
+
+ it_behaves_like 'having unique enum values'
+
+ describe '.ongoing_since' do
+ let_it_be(:repair_detail1) { create(:container_registry_data_repair_detail, :ongoing, updated_at: 1.day.ago) }
+ let_it_be(:repair_detail2) { create(:container_registry_data_repair_detail, :ongoing, updated_at: 20.minutes.ago) }
+ let_it_be(:repair_detail3) do
+ create(:container_registry_data_repair_detail, :completed, updated_at: 20.minutes.ago)
+ end
+
+ let_it_be(:repair_detail4) do
+ create(:container_registry_data_repair_detail, :completed, updated_at: 31.minutes.ago)
+ end
+
+ subject { described_class.ongoing_since(30.minutes.ago) }
+
+ it { is_expected.to contain_exactly(repair_detail1) }
+ end
+end
diff --git a/spec/models/container_registry/event_spec.rb b/spec/models/container_registry/event_spec.rb
index 07ac35f7b6a..d5c3a5fc066 100644
--- a/spec/models/container_registry/event_spec.rb
+++ b/spec/models/container_registry/event_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe ContainerRegistry::Event do
using RSpec::Parameterized::TableSyntax
let_it_be(:group) { create(:group, name: 'group') }
- let_it_be(:project) { create(:project, name: 'test', namespace: group) }
+ let_it_be(:project) { create(:project, path: 'test', namespace: group) }
describe '#supported?' do
let(:raw_event) { { 'action' => action } }
@@ -225,6 +225,28 @@ RSpec.describe ContainerRegistry::Event do
end
end
+ context 'when it is a manifest delete event' do
+ let(:raw_event) { { 'action' => 'delete', 'target' => { 'digest' => 'x' }, 'actor' => {} } }
+
+ it 'calls the ContainerRegistryEventCounter' do
+ expect(::Gitlab::UsageDataCounters::ContainerRegistryEventCounter)
+ .to receive(:count).with('i_container_registry_delete_manifest')
+
+ subject
+ end
+ end
+
+ context 'when it is not a manifest delete event' do
+ let(:raw_event) { { 'action' => 'push', 'target' => { 'digest' => 'x' }, 'actor' => {} } }
+
+ it 'does not call the ContainerRegistryEventCounter' do
+ expect(::Gitlab::UsageDataCounters::ContainerRegistryEventCounter)
+ .not_to receive(:count).with('i_container_registry_delete_manifest')
+
+ subject
+ end
+ end
+
context 'without an actor name' do
let(:raw_event) { { 'action' => 'push', 'target' => {}, 'actor' => { 'user_type' => 'personal_access_token' } } }
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index da7b54644bd..d8019e74c71 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -13,9 +13,9 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
end
before do
- stub_container_registry_config(enabled: true,
- api_url: 'http://registry.gitlab',
- host_port: 'registry.gitlab')
+ stub_container_registry_config(
+ enabled: true, api_url: 'http://registry.gitlab', host_port: 'registry.gitlab'
+ )
stub_request(:get, "http://registry.gitlab/v2/group/test/my_image/tags/list?n=#{::ContainerRegistry::Client::DEFAULT_TAGS_PAGE_SIZE}")
.with(headers: { 'Accept' => ContainerRegistry::Client::ACCEPTED_TYPES.join(', ') })
@@ -92,7 +92,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
end
end
- shared_examples 'transitioning to pre_importing', skip_pre_import_success: true do
+ shared_examples 'transitioning to pre_importing' do
before do
repository.update_column(:migration_pre_import_done_at, Time.zone.now)
end
@@ -145,7 +145,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
end
end
- shared_examples 'transitioning to importing', skip_import_success: true do
+ shared_examples 'transitioning to importing' do
before do
repository.update_columns(migration_import_done_at: Time.zone.now)
end
@@ -219,9 +219,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
subject { repository.start_pre_import }
before do |example|
- unless example.metadata[:skip_pre_import_success]
- allow(repository).to receive(:migration_pre_import).and_return(:ok)
- end
+ allow(repository).to receive(:migration_pre_import).and_return(:ok)
end
it_behaves_like 'transitioning from allowed states', %w[default pre_importing importing import_aborted]
@@ -234,9 +232,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
subject { repository.retry_pre_import }
before do |example|
- unless example.metadata[:skip_pre_import_success]
- allow(repository).to receive(:migration_pre_import).and_return(:ok)
- end
+ allow(repository).to receive(:migration_pre_import).and_return(:ok)
end
it_behaves_like 'transitioning from allowed states', %w[pre_importing importing import_aborted]
@@ -264,9 +260,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
subject { repository.start_import }
before do |example|
- unless example.metadata[:skip_import_success]
- allow(repository).to receive(:migration_import).and_return(:ok)
- end
+ allow(repository).to receive(:migration_import).and_return(:ok)
end
it_behaves_like 'transitioning from allowed states', %w[pre_import_done pre_importing importing import_aborted]
@@ -279,9 +273,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
subject { repository.retry_import }
before do |example|
- unless example.metadata[:skip_import_success]
- allow(repository).to receive(:migration_import).and_return(:ok)
- end
+ allow(repository).to receive(:migration_import).and_return(:ok)
end
it_behaves_like 'transitioning from allowed states', %w[pre_importing importing import_aborted]
@@ -374,9 +366,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
subject { repository.finish_pre_import_and_start_import }
before do |example|
- unless example.metadata[:skip_import_success]
- allow(repository).to receive(:migration_import).and_return(:ok)
- end
+ allow(repository).to receive(:migration_import).and_return(:ok)
end
it_behaves_like 'transitioning from allowed states', %w[pre_importing importing import_aborted]
@@ -528,6 +518,10 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
describe '#each_tags_page' do
let(:page_size) { 100 }
+ before do
+ allow(repository).to receive(:migrated?).and_return(true)
+ end
+
shared_examples 'iterating through a page' do |expected_tags: true|
it 'iterates through one page' do
expect(repository.gitlab_api_client).to receive(:tags)
@@ -660,7 +654,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
context 'calling on a non migrated repository' do
before do
- repository.update!(created_at: described_class::MIGRATION_PHASE_1_ENDED_AT - 3.days)
+ allow(repository).to receive(:migrated?).and_return(false)
end
it 'raises an Argument error' do
@@ -695,9 +689,12 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
describe '#delete_tags!' do
let(:repository) do
- create(:container_repository, name: 'my_image',
- tags: { latest: '123', rc1: '234' },
- project: project)
+ create(
+ :container_repository,
+ name: 'my_image',
+ tags: { latest: '123', rc1: '234' },
+ project: project
+ )
end
context 'when action succeeds' do
@@ -725,9 +722,12 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
describe '#delete_tag_by_name' do
let(:repository) do
- create(:container_repository, name: 'my_image',
- tags: { latest: '123', rc1: '234' },
- project: project)
+ create(
+ :container_repository,
+ name: 'my_image',
+ tags: { latest: '123', rc1: '234' },
+ project: project
+ )
end
context 'when action succeeds' do
@@ -756,9 +756,11 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
describe '#location' do
context 'when registry is running on a custom port' do
before do
- stub_container_registry_config(enabled: true,
- api_url: 'http://registry.gitlab:5000',
- host_port: 'registry.gitlab:5000')
+ stub_container_registry_config(
+ enabled: true,
+ api_url: 'http://registry.gitlab:5000',
+ host_port: 'registry.gitlab:5000'
+ )
end
it 'returns a full location of the repository' do
@@ -795,6 +797,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
freeze_time do
expect { subject }
.to change { repository.expiration_policy_started_at }.from(nil).to(Time.zone.now)
+ .and change { repository.expiration_policy_cleanup_status }.from('cleanup_unscheduled').to('cleanup_ongoing')
.and change { repository.last_cleanup_deleted_tags_count }.from(10).to(nil)
end
end
@@ -1236,6 +1239,8 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
end
before do
+ allow(group).to receive(:first_project_with_container_registry_tags).and_return(nil)
+
group.parent = test_group
group.save!
end
@@ -1561,22 +1566,20 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
describe '#migrated?' do
subject { repository.migrated? }
- it { is_expected.to eq(true) }
-
- context 'with a created_at older than phase 1 ends' do
+ context 'on gitlab.com' do
before do
- repository.update!(created_at: described_class::MIGRATION_PHASE_1_ENDED_AT - 3.days)
+ allow(::Gitlab).to receive(:com?).and_return(true)
end
- it { is_expected.to eq(false) }
-
- context 'with migration state set to import_done' do
- before do
- repository.update!(migration_state: 'import_done')
- end
+ it { is_expected.to eq(true) }
+ end
- it { is_expected.to eq(true) }
+ context 'not on gitlab.com' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(false)
end
+
+ it { is_expected.to eq(false) }
end
end
@@ -1717,4 +1720,19 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
it { is_expected.to contain_exactly(*stale_migrations) }
end
+
+ describe '#registry' do
+ it 'caches the client' do
+ registry = repository.registry
+ registry1 = repository.registry
+ registry2 = nil
+
+ travel_to(Time.current + Gitlab::CurrentSettings.container_registry_token_expire_delay.minutes) do
+ registry2 = repository.registry
+ end
+
+ expect(registry1.object_id).to be(registry.object_id)
+ expect(registry2.object_id).not_to be(registry.object_id)
+ end
+ end
end
diff --git a/spec/models/customer_relations/contact_spec.rb b/spec/models/customer_relations/contact_spec.rb
index 487af404a7c..6beb5323f60 100644
--- a/spec/models/customer_relations/contact_spec.rb
+++ b/spec/models/customer_relations/contact_spec.rb
@@ -241,8 +241,8 @@ RSpec.describe CustomerRelations::Contact, type: :model do
end
describe 'sorting' do
- let_it_be(:organization_a) { create(:organization, name: 'a') }
- let_it_be(:organization_b) { create(:organization, name: 'b') }
+ let_it_be(:crm_organization_a) { create(:crm_organization, name: 'a') }
+ let_it_be(:crm_organization_b) { create(:crm_organization, name: 'b') }
let_it_be(:contact_a) { create(:contact, group: group, first_name: "c", last_name: "d") }
let_it_be(:contact_b) do
create(:contact,
@@ -250,7 +250,7 @@ RSpec.describe CustomerRelations::Contact, type: :model do
first_name: "a",
last_name: "b",
phone: "123",
- organization: organization_a)
+ organization: crm_organization_a)
end
let_it_be(:contact_c) do
@@ -259,7 +259,7 @@ RSpec.describe CustomerRelations::Contact, type: :model do
first_name: "e",
last_name: "d",
phone: "456",
- organization: organization_b)
+ organization: crm_organization_b)
end
describe '.sort_by_name' do
diff --git a/spec/models/customer_relations/organization_spec.rb b/spec/models/customer_relations/organization_spec.rb
index d19a0bdf6c7..7fab9fd0e80 100644
--- a/spec/models/customer_relations/organization_spec.rb
+++ b/spec/models/customer_relations/organization_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe CustomerRelations::Organization, type: :model do
end
describe 'validations' do
- subject { build(:organization) }
+ subject { build(:crm_organization) }
it { is_expected.to validate_presence_of(:group) }
it { is_expected.to validate_presence_of(:name) }
@@ -21,13 +21,13 @@ RSpec.describe CustomerRelations::Organization, type: :model do
describe '#root_group' do
context 'when root group' do
- subject { build(:organization, group: group) }
+ subject { build(:crm_organization, group: group) }
it { is_expected.to be_valid }
end
context 'when subgroup' do
- subject { build(:organization, group: create(:group, parent: group)) }
+ subject { build(:crm_organization, group: create(:group, parent: group)) }
it { is_expected.to be_invalid }
end
@@ -43,23 +43,25 @@ RSpec.describe CustomerRelations::Organization, type: :model do
end
describe '#find_by_name' do
- let!(:organiztion1) { create(:organization, group: group, name: 'Test') }
- let!(:organiztion2) { create(:organization, group: create(:group), name: 'Test') }
+ let!(:crm_organiztion1) { create(:crm_organization, group: group, name: 'Test') }
+ let!(:crm_organiztion2) { create(:crm_organization, group: create(:group), name: 'Test') }
it 'strips name' do
- expect(described_class.find_by_name(group.id, 'TEST')).to eq([organiztion1])
+ expect(described_class.find_by_name(group.id, 'TEST')).to eq([crm_organiztion1])
end
end
describe '#self.move_to_root_group' do
let!(:old_root_group) { create(:group) }
- let!(:organizations) { create_list(:organization, 4, group: old_root_group) }
+ let!(:crm_organizations) { create_list(:crm_organization, 4, group: old_root_group) }
let!(:new_root_group) { create(:group) }
- let!(:contact1) { create(:contact, group: new_root_group, organization: organizations[0]) }
- let!(:contact2) { create(:contact, group: new_root_group, organization: organizations[1]) }
+ let!(:contact1) { create(:contact, group: new_root_group, organization: crm_organizations[0]) }
+ let!(:contact2) { create(:contact, group: new_root_group, organization: crm_organizations[1]) }
- let!(:dupe_organization1) { create(:organization, group: new_root_group, name: organizations[1].name) }
- let!(:dupe_organization2) { create(:organization, group: new_root_group, name: organizations[3].name.upcase) }
+ let!(:dupe_crm_organization1) { create(:crm_organization, group: new_root_group, name: crm_organizations[1].name) }
+ let!(:dupe_crm_organization2) do
+ create(:crm_organization, group: new_root_group, name: crm_organizations[3].name.upcase)
+ end
before do
old_root_group.update!(parent: new_root_group)
@@ -67,22 +69,22 @@ RSpec.describe CustomerRelations::Organization, type: :model do
end
it 'moves organizations with unique names and deletes the rest' do
- expect(organizations[0].reload.group_id).to eq(new_root_group.id)
- expect(organizations[2].reload.group_id).to eq(new_root_group.id)
- expect { organizations[1].reload }.to raise_error(ActiveRecord::RecordNotFound)
- expect { organizations[3].reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(crm_organizations[0].reload.group_id).to eq(new_root_group.id)
+ expect(crm_organizations[2].reload.group_id).to eq(new_root_group.id)
+ expect { crm_organizations[1].reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { crm_organizations[3].reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'updates contact.organization_id for dupes and leaves the rest untouched' do
- expect(contact1.reload.organization_id).to eq(organizations[0].id)
- expect(contact2.reload.organization_id).to eq(dupe_organization1.id)
+ expect(contact1.reload.organization_id).to eq(crm_organizations[0].id)
+ expect(contact2.reload.organization_id).to eq(dupe_crm_organization1.id)
end
end
describe '.search' do
- let_it_be(:organization_a) do
+ let_it_be(:crm_organization_a) do
create(
- :organization,
+ :crm_organization,
group: group,
name: "DEF",
description: "ghi_st",
@@ -90,9 +92,9 @@ RSpec.describe CustomerRelations::Organization, type: :model do
)
end
- let_it_be(:organization_b) do
+ let_it_be(:crm_organization_b) do
create(
- :organization,
+ :crm_organization,
group: group,
name: "ABC_st",
description: "JKL",
@@ -106,7 +108,7 @@ RSpec.describe CustomerRelations::Organization, type: :model do
let(:search_term) { "" }
it 'returns all group organizations' do
- expect(found_organizations).to contain_exactly(organization_a, organization_b)
+ expect(found_organizations).to contain_exactly(crm_organization_a, crm_organization_b)
end
end
@@ -114,42 +116,42 @@ RSpec.describe CustomerRelations::Organization, type: :model do
context 'when searching for name' do
let(:search_term) { "aBc" }
- it { is_expected.to contain_exactly(organization_b) }
+ it { is_expected.to contain_exactly(crm_organization_b) }
end
context 'when searching for description' do
let(:search_term) { "ghI" }
- it { is_expected.to contain_exactly(organization_a) }
+ it { is_expected.to contain_exactly(crm_organization_a) }
end
context 'when searching for name and description' do
let(:search_term) { "_st" }
- it { is_expected.to contain_exactly(organization_a, organization_b) }
+ it { is_expected.to contain_exactly(crm_organization_a, crm_organization_b) }
end
end
end
describe '.search_by_state' do
- let_it_be(:organization_a) { create(:organization, group: group, state: "inactive") }
- let_it_be(:organization_b) { create(:organization, group: group, state: "active") }
+ let_it_be(:crm_organization_a) { create(:crm_organization, group: group, state: "inactive") }
+ let_it_be(:crm_organization_b) { create(:crm_organization, group: group, state: "active") }
context 'when searching for organizations state' do
it 'returns only inactive organizations' do
- expect(group.organizations.search_by_state(:inactive)).to contain_exactly(organization_a)
+ expect(group.organizations.search_by_state(:inactive)).to contain_exactly(crm_organization_a)
end
it 'returns only active organizations' do
- expect(group.organizations.search_by_state(:active)).to contain_exactly(organization_b)
+ expect(group.organizations.search_by_state(:active)).to contain_exactly(crm_organization_b)
end
end
end
describe '.counts_by_state' do
before do
- create_list(:organization, 3, group: group)
- create_list(:organization, 2, group: group, state: 'inactive')
+ create_list(:crm_organization, 3, group: group)
+ create_list(:crm_organization, 2, group: group, state: 'inactive')
end
it 'returns correct organization counts' do
@@ -168,20 +170,20 @@ RSpec.describe CustomerRelations::Organization, type: :model do
end
describe 'sorting' do
- let_it_be(:organization_a) { create(:organization, group: group, name: "c", description: "1") }
- let_it_be(:organization_b) { create(:organization, group: group, name: "a") }
- let_it_be(:organization_c) { create(:organization, group: group, name: "b", description: "2") }
+ let_it_be(:crm_organization_a) { create(:crm_organization, group: group, name: "c", description: "1") }
+ let_it_be(:crm_organization_b) { create(:crm_organization, group: group, name: "a") }
+ let_it_be(:crm_organization_c) { create(:crm_organization, group: group, name: "b", description: "2") }
describe '.sort_by_name' do
it 'sorts them by name in ascendent order' do
- expect(group.organizations.sort_by_name).to eq([organization_b, organization_c, organization_a])
+ expect(group.organizations.sort_by_name).to eq([crm_organization_b, crm_organization_c, crm_organization_a])
end
end
describe '.sort_by_field' do
it 'sorts them by description in descending order' do
expect(group.organizations.sort_by_field('description', :desc))
- .to eq([organization_c, organization_a, organization_b])
+ .to eq([crm_organization_c, crm_organization_a, crm_organization_b])
end
end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 46a1b4ce588..2a7a8d50895 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -170,13 +170,6 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
deployment.run!
end
end
-
- it 'does not execute Deployments::DropOlderDeploymentsWorker' do
- expect(Deployments::DropOlderDeploymentsWorker)
- .not_to receive(:perform_async).with(deployment.id)
-
- deployment.run!
- end
end
context 'when deployment succeeded' do
@@ -383,8 +376,14 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let_it_be(:commits) { project.repository.commits('master', limit: 2) }
let!(:deployment) do
- create(:deployment, :success, project: project, environment: environment,
- finished_at: 1.year.ago, sha: commits[0].sha)
+ create(
+ :deployment,
+ :success,
+ project: project,
+ environment: environment,
+ finished_at: 1.year.ago,
+ sha: commits[0].sha
+ )
end
let!(:last_deployment) do
@@ -1355,10 +1354,9 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
subject { deployment.tags }
it 'will return tags related to this deployment' do
- expect(project.repository).to receive(:refs_by_oid).with(oid: deployment.sha,
- limit: 100,
- ref_patterns: [Gitlab::Git::TAG_REF_PREFIX])
- .and_return(["#{Gitlab::Git::TAG_REF_PREFIX}test"])
+ expect(project.repository).to receive(:refs_by_oid).with(
+ oid: deployment.sha, limit: 100, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]
+ ).and_return(["#{Gitlab::Git::TAG_REF_PREFIX}test"])
is_expected.to match_array(['refs/tags/test'])
end
diff --git a/spec/models/design_management/design_collection_spec.rb b/spec/models/design_management/design_collection_spec.rb
index bc8330c7dd3..06596d37fde 100644
--- a/spec/models/design_management/design_collection_spec.rb
+++ b/spec/models/design_management/design_collection_spec.rb
@@ -128,7 +128,7 @@ RSpec.describe DesignManagement::DesignCollection do
describe "#repository" do
it "builds a design repository" do
- expect(collection.repository).to be_a(DesignManagement::Repository)
+ expect(collection.repository).to be_a(DesignManagement::GitRepository)
end
end
diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb
index 57e0d1cad8b..72c0d1d1a64 100644
--- a/spec/models/design_management/design_spec.rb
+++ b/spec/models/design_management/design_spec.rb
@@ -55,6 +55,7 @@ RSpec.describe DesignManagement::Design, feature_category: :design_management do
it { is_expected.to validate_presence_of(:issue) }
it { is_expected.to validate_presence_of(:filename) }
it { is_expected.to validate_length_of(:filename).is_at_most(255) }
+ it { is_expected.to validate_length_of(:description).is_at_most(Gitlab::Database::MAX_TEXT_SIZE_LIMIT) }
it { is_expected.to validate_uniqueness_of(:filename).scoped_to(:issue_id) }
it "validates that the extension is an image" do
@@ -462,7 +463,7 @@ RSpec.describe DesignManagement::Design, feature_category: :design_management do
it 'is a design repository' do
design = build(:design, issue: issue)
- expect(design.repository).to be_a(DesignManagement::Repository)
+ expect(design.repository).to be_a(DesignManagement::GitRepository)
end
end
@@ -512,11 +513,11 @@ RSpec.describe DesignManagement::Design, feature_category: :design_management do
end
describe '#to_reference' do
- let(:namespace) { build(:namespace, id: non_existing_record_id, path: 'sample-namespace') }
- let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
- let(:group) { create(:group, name: 'Group', path: 'sample-group') }
- let(:issue) { build(:issue, iid: 1, project: project) }
let(:filename) { 'homescreen.jpg' }
+ let(:namespace) { build(:namespace, id: non_existing_record_id) }
+ let(:project) { build(:project, namespace: namespace) }
+ let(:group) { build(:group) }
+ let(:issue) { build(:issue, iid: 1, project: project) }
let(:design) { build(:design, filename: filename, issue: issue, project: project) }
context 'when nil argument' do
@@ -535,7 +536,7 @@ RSpec.describe DesignManagement::Design, feature_category: :design_management do
design.to_reference(group, full: true)
]
- expect(refs).to all(eq 'sample-namespace/sample-project#1/designs[homescreen.jpg]')
+ expect(refs).to all(eq "#{project.full_path}#1/designs[homescreen.jpg]")
end
end
@@ -546,7 +547,7 @@ RSpec.describe DesignManagement::Design, feature_category: :design_management do
design.to_reference(group, full: false)
]
- expect(refs).to all(eq 'sample-namespace/sample-project#1[homescreen.jpg]')
+ expect(refs).to all(eq "#{project.full_path}#1[homescreen.jpg]")
end
end
@@ -595,7 +596,7 @@ RSpec.describe DesignManagement::Design, feature_category: :design_management do
'url_filename' => filename,
'issue' => issue.iid.to_s,
'namespace' => design.project.namespace.to_param,
- 'project' => design.project.name
+ 'project' => design.project.to_param
)
end
diff --git a/spec/models/design_management/git_repository_spec.rb b/spec/models/design_management/git_repository_spec.rb
new file mode 100644
index 00000000000..736f2ad45cf
--- /dev/null
+++ b/spec/models/design_management/git_repository_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe DesignManagement::GitRepository, feature_category: :design_management do
+ let_it_be(:container_repo) { DesignManagement::Repository.new(project: create(:project)) }
+ let(:git_repository) { container_repo.repository }
+
+ shared_examples 'returns parsed git attributes that enable LFS for all file types' do
+ it do
+ expect(subject.patterns).to be_a_kind_of(Hash)
+ expect(subject.patterns).to have_key('/designs/*')
+ expect(subject.patterns['/designs/*']).to eql(
+ { "filter" => "lfs", "diff" => "lfs", "merge" => "lfs", "text" => false }
+ )
+ end
+ end
+
+ describe '.container' do
+ it 'is of class DesignManagement::Repository' do
+ expect(git_repository.container).to be_a_kind_of(DesignManagement::Repository)
+ end
+ end
+
+ describe "#info_attributes" do
+ subject { git_repository.info_attributes }
+
+ include_examples 'returns parsed git attributes that enable LFS for all file types'
+ end
+
+ describe '#attributes_at' do
+ subject { git_repository.attributes_at }
+
+ include_examples 'returns parsed git attributes that enable LFS for all file types'
+ end
+
+ describe '#gitattribute' do
+ it 'returns a gitattribute when path has gitattributes' do
+ expect(git_repository.gitattribute('/designs/file.txt', 'filter')).to eq('lfs')
+ end
+
+ it 'returns nil when path has no gitattributes' do
+ expect(git_repository.gitattribute('/invalid/file.txt', 'filter')).to be_nil
+ end
+ end
+
+ describe '#copy_gitattributes' do
+ it 'always returns regardless of whether given a valid or invalid ref' do
+ expect(git_repository.copy_gitattributes('master')).to be true
+ expect(git_repository.copy_gitattributes('invalid')).to be true
+ end
+ end
+
+ describe '#attributes' do
+ it 'confirms that all files are LFS enabled' do
+ %w[png zip anything].each do |filetype|
+ path = "/#{DesignManagement.designs_directory}/file.#{filetype}"
+ attributes = git_repository.attributes(path)
+
+ expect(attributes['filter']).to eq('lfs')
+ end
+ end
+ end
+end
diff --git a/spec/models/design_management/repository_spec.rb b/spec/models/design_management/repository_spec.rb
index 0115e0c139c..74f393306dc 100644
--- a/spec/models/design_management/repository_spec.rb
+++ b/spec/models/design_management/repository_spec.rb
@@ -2,57 +2,24 @@
require 'spec_helper'
-RSpec.describe DesignManagement::Repository do
- let(:project) { create(:project) }
- let(:repository) { described_class.new(project) }
+RSpec.describe DesignManagement::Repository, feature_category: :design_management do
+ let_it_be(:project) { create(:project) }
+ let(:subject) { described_class.new({ project: project }) }
- shared_examples 'returns parsed git attributes that enable LFS for all file types' do
- it do
- expect(subject.patterns).to be_a_kind_of(Hash)
- expect(subject.patterns).to have_key('/designs/*')
- expect(subject.patterns['/designs/*']).to eql(
- { "filter" => "lfs", "diff" => "lfs", "merge" => "lfs", "text" => false }
- )
- end
+ describe 'associations' do
+ it { is_expected.to belong_to(:project).inverse_of(:design_management_repository) }
end
- describe "#info_attributes" do
- subject { repository.info_attributes }
-
- include_examples 'returns parsed git attributes that enable LFS for all file types'
- end
-
- describe '#attributes_at' do
- subject { repository.attributes_at }
-
- include_examples 'returns parsed git attributes that enable LFS for all file types'
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_uniqueness_of(:project) }
end
- describe '#gitattribute' do
- it 'returns a gitattribute when path has gitattributes' do
- expect(repository.gitattribute('/designs/file.txt', 'filter')).to eq('lfs')
- end
-
- it 'returns nil when path has no gitattributes' do
- expect(repository.gitattribute('/invalid/file.txt', 'filter')).to be_nil
- end
- end
-
- describe '#copy_gitattributes' do
- it 'always returns regardless of whether given a valid or invalid ref' do
- expect(repository.copy_gitattributes('master')).to be true
- expect(repository.copy_gitattributes('invalid')).to be true
- end
+ it "returns the project's full path" do
+ expect(subject.full_path).to eq(project.full_path + Gitlab::GlRepository::DESIGN.path_suffix)
end
- describe '#attributes' do
- it 'confirms that all files are LFS enabled' do
- %w(png zip anything).each do |filetype|
- path = "/#{DesignManagement.designs_directory}/file.#{filetype}"
- attributes = repository.attributes(path)
-
- expect(attributes['filter']).to eq('lfs')
- end
- end
+ it "returns the project's disk path" do
+ expect(subject.disk_path).to eq(project.disk_path + Gitlab::GlRepository::DESIGN.path_suffix)
end
end
diff --git a/spec/models/design_management/version_spec.rb b/spec/models/design_management/version_spec.rb
index 8c0d7e99ae5..748bcbffd29 100644
--- a/spec/models/design_management/version_spec.rb
+++ b/spec/models/design_management/version_spec.rb
@@ -247,10 +247,12 @@ RSpec.describe DesignManagement::Version do
context 'there are a bunch of different designs in a variety of states' do
let_it_be(:version) do
- create(:design_version,
- created_designs: create_list(:design, 3),
- modified_designs: create_list(:design, 4),
- deleted_designs: create_list(:design, 5))
+ create(
+ :design_version,
+ created_designs: create_list(:design, 3),
+ modified_designs: create_list(:design, 4),
+ deleted_designs: create_list(:design, 5)
+ )
end
it 'puts them in the right buckets' do
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index a526f91ddc1..0c2a0ce45d4 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -115,11 +115,13 @@ RSpec.describe DiffNote do
describe '#create_diff_file callback' do
context 'merge request' do
let(:position) do
- Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: nil,
- new_line: 9,
- diff_refs: merge_request.diff_refs)
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: merge_request.diff_refs
+ )
end
subject { build(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
@@ -131,10 +133,12 @@ RSpec.describe DiffNote do
let(:diff_file) do
diffs = merge_request.diffs
raw_diff = diffs.diffable.raw_diffs(diffs.diff_options.merge(paths: ['files/ruby/popen.rb'])).first
- Gitlab::Diff::File.new(raw_diff,
- repository: diffs.project.repository,
- diff_refs: diffs.diff_refs,
- fallback_diff_refs: diffs.fallback_diff_refs)
+ Gitlab::Diff::File.new(
+ raw_diff,
+ repository: diffs.project.repository,
+ diff_refs: diffs.diff_refs,
+ fallback_diff_refs: diffs.fallback_diff_refs
+ )
end
let(:diff_line) { diff_file.diff_lines.first }
@@ -188,10 +192,12 @@ RSpec.describe DiffNote do
end
it 'raises an error' do
- expect { subject.save! }.to raise_error(::DiffNote::NoteDiffFileCreationError,
- "Failed to find diff line for: #{diff_file.file_path}, "\
- "old_line: #{position.old_line}"\
- ", new_line: #{position.new_line}")
+ expect { subject.save! }.to raise_error(
+ ::DiffNote::NoteDiffFileCreationError,
+ "Failed to find diff line for: #{diff_file.file_path}, "\
+ "old_line: #{position.old_line}"\
+ ", new_line: #{position.new_line}"
+ )
end
end
@@ -383,8 +389,7 @@ RSpec.describe DiffNote do
subject { create(:diff_note_on_commit, project: project, position: position, commit_id: commit.id) }
it "doesn't update the position" do
- is_expected.to have_attributes(original_position: position,
- position: position)
+ is_expected.to have_attributes(original_position: position, position: position)
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index dfb7de34993..87beba680d8 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -126,7 +126,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
expect(environment.errors[:external_url].first).to eq(expected_error_message)
else
expect(environment.errors[:external_url]).to be_empty,
- "There were unexpected errors: #{environment.errors.full_messages}"
+ "There were unexpected errors: #{environment.errors.full_messages}"
expect(environment.external_url).to eq(source_external_url)
end
end
@@ -660,15 +660,17 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
let(:build) { create(:ci_build, :success) }
let!(:deployment) do
- create(:deployment, :success,
- environment: environment,
- deployable: build,
- on_stop: 'close_app')
+ create(
+ :deployment,
+ :success,
+ environment: environment,
+ deployable: build,
+ on_stop: 'close_app'
+ )
end
let!(:close_action) do
- create(:ci_build, :manual, pipeline: build.pipeline,
- name: 'close_app')
+ create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app')
end
context 'when environment is available' do
@@ -750,8 +752,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
before do
project.add_developer(user)
- create(:protected_branch, :developers_can_merge,
- name: 'master', project: project)
+ create(:protected_branch, :developers_can_merge, name: 'master', project: project)
end
context 'when action did not yet finish' do
@@ -774,8 +775,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
context 'if action did finish' do
let!(:close_action) do
- create(:ci_build, :manual, :success,
- pipeline: pipeline, name: 'close_app_a')
+ create(:ci_build, :manual, :success, pipeline: pipeline, name: 'close_app_a')
end
it 'returns a new action of the same type' do
@@ -1258,8 +1258,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
describe '#deployment_platform' do
context 'when there is a deployment platform for environment' do
let!(:cluster) do
- create(:cluster, :provided_by_gcp,
- environment_scope: '*', projects: [project])
+ create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project])
end
it 'finds a deployment platform' do
@@ -1387,7 +1386,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
context 'when the environment is available' do
context 'with a deployment service' do
- let(:project) { create(:project, :with_prometheus_integration, :repository) }
+ let_it_be(:project) { create(:project, :with_prometheus_integration, :repository) }
context 'and a deployment' do
let!(:deployment) { create(:deployment, environment: environment) }
@@ -1460,7 +1459,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
end
context 'when the environment is unavailable' do
- let(:project) { create(:project, :with_prometheus_integration) }
+ let_it_be(:project) { create(:project, :with_prometheus_integration) }
before do
environment.stop
@@ -1487,7 +1486,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
end
describe '#metrics' do
- let(:project) { create(:project, :with_prometheus_integration) }
+ let_it_be(:project) { create(:project, :with_prometheus_integration) }
subject { environment.metrics }
@@ -1523,7 +1522,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
end
describe '#additional_metrics' do
- let(:project) { create(:project, :with_prometheus_integration) }
+ let_it_be(:project) { create(:project, :with_prometheus_integration) }
let(:metric_params) { [] }
subject { environment.additional_metrics(*metric_params) }
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index 2f1edf9ab94..9814eed8b45 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe EnvironmentStatus do
it { is_expected.to delegate_method(:name).to(:environment) }
it { is_expected.to delegate_method(:deployed_at).to(:deployment) }
it { is_expected.to delegate_method(:status).to(:deployment) }
+ it { is_expected.to delegate_method(:deployable).to(:deployment) }
describe '#project' do
subject { environment_status.project }
@@ -177,11 +178,13 @@ RSpec.describe EnvironmentStatus do
let(:pipeline) { create(:ci_pipeline, sha: sha, project: forked) }
let(:merge_request) do
- create(:merge_request,
- source_project: forked,
- target_project: project,
- target_branch: 'master',
- head_pipeline: pipeline)
+ create(
+ :merge_request,
+ source_project: forked,
+ target_project: project,
+ target_branch: 'master',
+ head_pipeline: pipeline
+ )
end
it 'returns environment status' do
@@ -198,12 +201,14 @@ RSpec.describe EnvironmentStatus do
let(:pipeline) { create(:ci_pipeline, sha: sha, project: project) }
let(:merge_request) do
- create(:merge_request,
- source_project: project,
- source_branch: 'feature',
- target_project: project,
- target_branch: 'master',
- head_pipeline: pipeline)
+ create(
+ :merge_request,
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master',
+ head_pipeline: pipeline
+ )
end
it 'returns environment status' do
@@ -265,6 +270,7 @@ RSpec.describe EnvironmentStatus do
context 'when environment is stopped' do
before do
+ stub_feature_flags(review_apps_redeploy_mr_widget: false)
environment.stop!
end
@@ -272,6 +278,17 @@ RSpec.describe EnvironmentStatus do
expect(subject.count).to eq(0)
end
end
+
+ context 'when environment is stopped and review_apps_redeploy_mr_widget is turned on' do
+ before do
+ stub_feature_flags(review_apps_redeploy_mr_widget: true)
+ environment.stop!
+ end
+
+ it 'returns environment regardless of status' do
+ expect(subject.count).to eq(1)
+ end
+ end
end
end
end
diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
index 26795c0ea7f..34be6ec7fa9 100644
--- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
+RSpec.describe ErrorTracking::ProjectErrorTrackingSetting, feature_category: :error_tracking do
include ReactiveCachingHelpers
include Gitlab::Routing
@@ -93,9 +93,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
end
context 'with sentry backend' do
- before do
- subject.integrated = false
- end
+ subject { build(:project_error_tracking_setting, project: project) }
it 'does not create a new client key' do
expect { subject.save! }.not_to change { ErrorTracking::ClientKey.count }
@@ -302,7 +300,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
it { expect(result[:issue].gitlab_commit_path).to eq(nil) }
end
- context 'when repo commit matches first release version' do
+ context 'when repo commit matches first relase version' do
let(:commit) { instance_double(Commit, id: commit_id) }
let(:repository) { instance_double(Repository, commit: commit) }
@@ -341,18 +339,19 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
describe '#update_issue' do
let(:result) { subject.update_issue(**opts) }
- let(:opts) { { issue_id: 1, params: {} } }
+ let(:issue_id) { 1 }
+ let(:opts) { { issue_id: issue_id, params: {} } }
before do
allow(subject).to receive(:sentry_client).and_return(sentry_client)
allow(sentry_client).to receive(:issue_details)
- .with({ issue_id: 1 })
+ .with({ issue_id: issue_id })
.and_return(Gitlab::ErrorTracking::DetailedError.new(project_id: sentry_project_id))
end
context 'when sentry response is successful' do
before do
- allow(sentry_client).to receive(:update_issue).with(opts).and_return(true)
+ allow(sentry_client).to receive(:update_issue).with(**opts).and_return(true)
end
it 'returns the successful response' do
@@ -362,7 +361,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
context 'when sentry raises an error' do
before do
- allow(sentry_client).to receive(:update_issue).with(opts).and_raise(StandardError)
+ allow(sentry_client).to receive(:update_issue).with(**opts).and_raise(StandardError)
end
it 'returns the successful response' do
@@ -391,7 +390,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
setting.update!(sentry_project_id: nil)
allow(sentry_client).to receive(:projects).and_return(sentry_projects)
- allow(sentry_client).to receive(:update_issue).with(opts).and_return(true)
+ allow(sentry_client).to receive(:update_issue).with(**opts).and_return(true)
end
it 'tries to backfill it from sentry API' do
@@ -419,6 +418,25 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
end
end
end
+
+ describe 'passing parameters to sentry client' do
+ include SentryClientHelpers
+
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0' }
+ let(:sentry_request_url) { "#{sentry_url}/issues/#{issue_id}/" }
+ let(:token) { 'test-token' }
+ let(:sentry_client) { ErrorTracking::SentryClient.new(sentry_url, token) }
+
+ before do
+ stub_sentry_request(sentry_request_url, :put, body: true)
+
+ allow(sentry_client).to receive(:update_issue).and_call_original
+ end
+
+ it 'returns the successful response' do
+ expect(result).to eq(updated: true)
+ end
+ end
end
describe 'slugs' do
diff --git a/spec/models/event_collection_spec.rb b/spec/models/event_collection_spec.rb
index 13983dcfde3..cab2444c174 100644
--- a/spec/models/event_collection_spec.rb
+++ b/spec/models/event_collection_spec.rb
@@ -19,8 +19,7 @@ RSpec.describe EventCollection do
context 'with project events' do
let_it_be(:push_event_payloads) do
Array.new(9) do
- create(:push_event_payload,
- event: create(:push_event, project: project, author: user))
+ create(:push_event_payload, event: create(:push_event, project: project, author: user))
end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 931d12b7109..3e4fe57c59b 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -203,9 +203,7 @@ RSpec.describe Event, feature_category: :user_profile do
let(:target) { nil }
let(:event) do
- described_class.new(project: project,
- target: target,
- author_id: author.id)
+ described_class.new(project: project, target: target, author_id: author.id)
end
context 'for an issue' do
@@ -304,9 +302,7 @@ RSpec.describe Event, feature_category: :user_profile do
let(:note_on_design) { create(:note_on_design, author: author, noteable: design, project: project) }
let(:milestone_on_project) { create(:milestone, project: project) }
let(:event) do
- described_class.new(project: project,
- target: target,
- author_id: author.id)
+ described_class.new(project: project, target: target, author_id: author.id)
end
before do
@@ -902,8 +898,10 @@ RSpec.describe Event, feature_category: :user_profile do
it "deletes the redis key for if the project was inactive" do
Gitlab::Redis::SharedState.with do |redis|
- expect(redis).to receive(:hdel).with('inactive_projects_deletion_warning_email_notified',
- "project:#{project.id}")
+ expect(redis).to receive(:hdel).with(
+ 'inactive_projects_deletion_warning_email_notified',
+ "project:#{project.id}"
+ )
end
project.touch(:last_activity_at, time: 1.year.ago)
@@ -1138,11 +1136,13 @@ RSpec.describe Event, feature_category: :user_profile do
def create_push_event(project, user)
event = create(:push_event, project: project, author: user)
- create(:push_event_payload,
- event: event,
- commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
- commit_count: 0,
- ref: 'master')
+ create(
+ :push_event_payload,
+ event: event,
+ commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ commit_count: 0,
+ ref: 'master'
+ )
event
end
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index fe22b20ecf9..c477dec237f 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -8,8 +8,7 @@ RSpec.describe GenericCommitStatus do
let(:external_url) { 'http://example.gitlab.com/status' }
let(:generic_commit_status) do
- create(:generic_commit_status, pipeline: pipeline,
- target_url: external_url)
+ create(:generic_commit_status, pipeline: pipeline, target_url: external_url)
end
describe 'validations' do
diff --git a/spec/models/group_group_link_spec.rb b/spec/models/group_group_link_spec.rb
index eec8fe0ef71..780196e6c8c 100644
--- a/spec/models/group_group_link_spec.rb
+++ b/spec/models/group_group_link_spec.rb
@@ -5,9 +5,28 @@ require 'spec_helper'
RSpec.describe GroupGroupLink do
let_it_be(:group) { create(:group) }
let_it_be(:shared_group) { create(:group) }
- let_it_be(:group_group_link) do
- create(:group_group_link, shared_group: shared_group,
- shared_with_group: group)
+
+ describe 'validation' do
+ let_it_be(:group_group_link) do
+ create(:group_group_link, shared_group: shared_group, shared_with_group: group)
+ end
+
+ it { is_expected.to validate_presence_of(:shared_group) }
+
+ it do
+ is_expected.to(
+ validate_uniqueness_of(:shared_group_id)
+ .scoped_to(:shared_with_group_id)
+ .with_message('The group has already been shared with this group'))
+ end
+
+ it { is_expected.to validate_presence_of(:shared_with_group) }
+ it { is_expected.to validate_presence_of(:group_access) }
+
+ it do
+ is_expected.to(
+ validate_inclusion_of(:group_access).in_array(Gitlab::Access.values))
+ end
end
describe 'relations' do
@@ -16,42 +35,51 @@ RSpec.describe GroupGroupLink do
end
describe 'scopes' do
- describe '.non_guests' do
- let!(:group_group_link_reporter) { create :group_group_link, :reporter }
- let!(:group_group_link_maintainer) { create :group_group_link, :maintainer }
- let!(:group_group_link_owner) { create :group_group_link, :owner }
- let!(:group_group_link_guest) { create :group_group_link, :guest }
-
- it 'returns all records which are greater than Guests access' do
- expect(described_class.non_guests).to match_array([
- group_group_link_reporter, group_group_link,
- group_group_link_maintainer, group_group_link_owner
- ])
- end
- end
-
- describe '.with_owner_or_maintainer_access' do
+ context 'for scopes fetching records based on access levels' do
+ let_it_be(:group_group_link_guest) { create :group_group_link, :guest }
+ let_it_be(:group_group_link_reporter) { create :group_group_link, :reporter }
+ let_it_be(:group_group_link_developer) { create :group_group_link, :developer }
let_it_be(:group_group_link_maintainer) { create :group_group_link, :maintainer }
let_it_be(:group_group_link_owner) { create :group_group_link, :owner }
- let_it_be(:group_group_link_reporter) { create :group_group_link, :reporter }
- let_it_be(:group_group_link_guest) { create :group_group_link, :guest }
- it 'returns all records which have OWNER or MAINTAINER access' do
- expect(described_class.with_owner_or_maintainer_access).to match_array([
- group_group_link_maintainer,
- group_group_link_owner
- ])
+ describe '.non_guests' do
+ it 'returns all records which are greater than Guests access' do
+ expect(described_class.non_guests).to match_array([
+ group_group_link_reporter, group_group_link_developer,
+ group_group_link_maintainer, group_group_link_owner
+ ])
+ end
end
- end
- describe '.with_owner_access' do
- let_it_be(:group_group_link_maintainer) { create :group_group_link, :maintainer }
- let_it_be(:group_group_link_owner) { create :group_group_link, :owner }
- let_it_be(:group_group_link_reporter) { create :group_group_link, :reporter }
- let_it_be(:group_group_link_guest) { create :group_group_link, :guest }
+ describe '.with_owner_or_maintainer_access' do
+ it 'returns all records which have OWNER or MAINTAINER access' do
+ expect(described_class.with_owner_or_maintainer_access).to match_array([
+ group_group_link_maintainer,
+ group_group_link_owner
+ ])
+ end
+ end
- it 'returns all records which have OWNER access' do
- expect(described_class.with_owner_access).to match_array([group_group_link_owner])
+ describe '.with_owner_access' do
+ it 'returns all records which have OWNER access' do
+ expect(described_class.with_owner_access).to match_array([group_group_link_owner])
+ end
+ end
+
+ describe '.with_developer_access' do
+ it 'returns all records which have DEVELOPER access' do
+ expect(described_class.with_developer_access).to match_array([group_group_link_developer])
+ end
+ end
+
+ describe '.with_developer_maintainer_owner_access' do
+ it 'returns all records which have DEVELOPER, MAINTAINER or OWNER access' do
+ expect(described_class.with_developer_maintainer_owner_access).to match_array([
+ group_group_link_developer,
+ group_group_link_owner,
+ group_group_link_maintainer
+ ])
+ end
end
end
@@ -93,6 +121,15 @@ RSpec.describe GroupGroupLink do
let_it_be(:sub_shared_group) { create(:group, parent: shared_group) }
let_it_be(:other_group) { create(:group) }
+ let_it_be(:group_group_link_1) do
+ create(
+ :group_group_link,
+ shared_group: shared_group,
+ shared_with_group: group,
+ group_access: Gitlab::Access::DEVELOPER
+ )
+ end
+
let_it_be(:group_group_link_2) do
create(
:group_group_link,
@@ -125,7 +162,7 @@ RSpec.describe GroupGroupLink do
expect(described_class.all.count).to eq(4)
expect(distinct_group_group_links.count).to eq(2)
- expect(distinct_group_group_links).to include(group_group_link)
+ expect(distinct_group_group_links).to include(group_group_link_1)
expect(distinct_group_group_links).not_to include(group_group_link_2)
expect(distinct_group_group_links).not_to include(group_group_link_3)
expect(distinct_group_group_links).to include(group_group_link_4)
@@ -133,27 +170,9 @@ RSpec.describe GroupGroupLink do
end
end
- describe 'validation' do
- it { is_expected.to validate_presence_of(:shared_group) }
-
- it do
- is_expected.to(
- validate_uniqueness_of(:shared_group_id)
- .scoped_to(:shared_with_group_id)
- .with_message('The group has already been shared with this group'))
- end
-
- it { is_expected.to validate_presence_of(:shared_with_group) }
- it { is_expected.to validate_presence_of(:group_access) }
-
- it do
- is_expected.to(
- validate_inclusion_of(:group_access).in_array(Gitlab::Access.values))
- end
- end
-
describe '#human_access' do
it 'delegates to Gitlab::Access' do
+ group_group_link = create(:group_group_link, :reporter)
expect(Gitlab::Access).to receive(:human_access).with(group_group_link.group_access)
group_group_link.human_access
@@ -161,6 +180,8 @@ RSpec.describe GroupGroupLink do
end
describe 'search by group name' do
+ let_it_be(:group_group_link) { create(:group_group_link, :reporter, shared_with_group: group) }
+
it { expect(described_class.search(group.name)).to eq([group_group_link]) }
it { expect(described_class.search('not-a-group-name')).to be_empty }
end
diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb
index ec9244d5eb5..701348baf48 100644
--- a/spec/models/group_label_spec.rb
+++ b/spec/models/group_label_spec.rb
@@ -41,11 +41,13 @@ RSpec.describe GroupLabel do
context 'cross-project' do
let(:namespace) { build_stubbed(:namespace) }
- let(:source_project) { build_stubbed(:project, name: 'project-1', namespace: namespace) }
- let(:target_project) { build_stubbed(:project, name: 'project-2', namespace: namespace) }
+ let(:source_project) { build_stubbed(:project, namespace: namespace) }
+ let(:target_project) { build_stubbed(:project, namespace: namespace) }
it 'returns a String reference to the object' do
- expect(label.to_reference(source_project, target_project: target_project)).to eq %(project-1~#{label.id})
+ expect(label.to_reference(source_project, target_project: target_project)).to(
+ eq("#{source_project.path}~#{label.id}")
+ )
end
end
@@ -56,4 +58,39 @@ RSpec.describe GroupLabel do
end
end
end
+
+ describe '#preloaded_parent_container' do
+ let_it_be(:label) { create(:group_label) }
+
+ before do
+ label.reload # ensure associations are not loaded
+ end
+
+ context 'when group is loaded' do
+ it 'does not invoke a DB query' do
+ label.group
+
+ count = ActiveRecord::QueryRecorder.new { label.preloaded_parent_container }.count
+ expect(count).to eq(0)
+ expect(label.preloaded_parent_container).to eq(label.group)
+ end
+ end
+
+ context 'when parent_container is loaded' do
+ it 'does not invoke a DB query' do
+ label.parent_container
+
+ count = ActiveRecord::QueryRecorder.new { label.preloaded_parent_container }.count
+ expect(count).to eq(0)
+ expect(label.preloaded_parent_container).to eq(label.parent_container)
+ end
+ end
+
+ context 'when none of them are loaded' do
+ it 'invokes a DB query' do
+ count = ActiveRecord::QueryRecorder.new { label.preloaded_parent_container }.count
+ expect(count).to eq(1)
+ end
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 0a05c558d45..67e4e128019 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -40,7 +40,14 @@ RSpec.describe Group, feature_category: :subgroups do
it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::GroupDistribution').dependent(:destroy) }
it { is_expected.to have_many(:daily_build_group_report_results).class_name('Ci::DailyBuildGroupReportResult') }
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout').with_foreign_key(:group_id) }
+
it { is_expected.to have_many(:bulk_import_exports).class_name('BulkImports::Export') }
+
+ it do
+ is_expected.to have_many(:bulk_import_entities).class_name('BulkImports::Entity')
+ .with_foreign_key(:namespace_id).inverse_of(:group)
+ end
+
it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') }
it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') }
it { is_expected.to have_many(:protected_branches).inverse_of(:group).with_foreign_key(:namespace_id) }
@@ -448,6 +455,8 @@ RSpec.describe Group, feature_category: :subgroups do
it_behaves_like 'a BulkUsersByEmailLoad model'
+ it_behaves_like 'ensures runners_token is prefixed', :group
+
context 'after initialized' do
it 'has a group_feature' do
expect(described_class.new.group_feature).to be_present
@@ -955,6 +964,23 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
+ describe '.with_project_creation_levels' do
+ let_it_be(:group_1) { create(:group, project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS) }
+ let_it_be(:group_2) { create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
+ let_it_be(:group_3) { create(:group, project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS) }
+ let_it_be(:group_4) { create(:group, project_creation_level: nil) }
+
+ it 'returns groups with the specified project creation levels' do
+ result = described_class.with_project_creation_levels([
+ Gitlab::Access::NO_ONE_PROJECT_ACCESS,
+ Gitlab::Access::MAINTAINER_PROJECT_ACCESS
+ ])
+
+ expect(result).to include(group_1, group_3)
+ expect(result).not_to include(group_2, group_4)
+ end
+ end
+
describe '.project_creation_allowed' do
let_it_be(:group_1) { create(:group, project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS) }
let_it_be(:group_2) { create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
@@ -967,6 +993,22 @@ RSpec.describe Group, feature_category: :subgroups do
expect(result).to include(group_2, group_3, group_4)
expect(result).not_to include(group_1)
end
+
+ context 'when the application_setting is set to `NO_ONE_PROJECT_ACCESS`' do
+ before do
+ stub_application_setting(default_project_creation: Gitlab::Access::NO_ONE_PROJECT_ACCESS)
+ end
+
+ it 'only includes groups where project creation is allowed' do
+ result = described_class.project_creation_allowed
+
+ expect(result).to include(group_2, group_3)
+
+ # group_4 won't be included because it has `project_creation_level: nil`,
+ # and that means it behaves like the value of the application_setting will inherited.
+ expect(result).not_to include(group_1, group_4)
+ end
+ end
end
describe 'by_ids_or_paths' do
@@ -2409,8 +2451,7 @@ RSpec.describe Group, feature_category: :subgroups do
let(:shared_with_group) { create(:group, parent: group) }
before do
- create(:group_group_link, shared_group: nested_group,
- shared_with_group: shared_with_group)
+ create(:group_group_link, shared_group: nested_group, shared_with_group: shared_with_group)
end
subject(:related_group_ids) { nested_group.related_group_ids }
@@ -3115,11 +3156,11 @@ RSpec.describe Group, feature_category: :subgroups do
describe '.organizations' do
it 'returns organizations belonging to the group' do
- organization1 = create(:organization, group: group)
- create(:organization)
- organization3 = create(:organization, group: group)
+ crm_organization1 = create(:crm_organization, group: group)
+ create(:crm_organization)
+ crm_organization3 = create(:crm_organization, group: group)
- expect(group.organizations).to contain_exactly(organization1, organization3)
+ expect(group.organizations).to contain_exactly(crm_organization1, crm_organization3)
end
end
@@ -3584,6 +3625,13 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
+ describe '#content_editor_on_issues_feature_flag_enabled?' do
+ it_behaves_like 'checks self and root ancestor feature flag' do
+ let(:feature_flag) { :content_editor_on_issues }
+ let(:feature_flag_method) { :content_editor_on_issues_feature_flag_enabled? }
+ end
+ end
+
describe '#work_items_feature_flag_enabled?' do
it_behaves_like 'checks self and root ancestor feature flag' do
let(:feature_flag) { :work_items }
@@ -3696,7 +3744,7 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
- describe '#usage_quotas_enabled?', feature_category: :subscription_cost_management, unless: Gitlab.ee? do
+ describe '#usage_quotas_enabled?', feature_category: :consumables_cost_management, unless: Gitlab.ee? do
using RSpec::Parameterized::TableSyntax
where(:feature_enabled, :root_group, :result) do
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index ad5f01fe056..254b8c2520b 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -267,7 +267,7 @@ RSpec.describe WebHook, feature_category: :integrations do
end
context 'without url variables' do
- subject(:hook) { build_stubbed(:project_hook, project: project, url: 'http://example.com') }
+ subject(:hook) { build_stubbed(:project_hook, project: project, url: 'http://example.com', url_variables: nil) }
it 'does not reset url variables' do
hook.url = 'http://example.com/{one}/{two}'
@@ -346,7 +346,7 @@ RSpec.describe WebHook, feature_category: :integrations do
end
it 'is 10 minutes' do
- expect(hook.next_backoff).to eq(described_class::INITIAL_BACKOFF)
+ expect(hook.next_backoff).to eq(WebHooks::AutoDisabling::INITIAL_BACKOFF)
end
end
@@ -356,7 +356,7 @@ RSpec.describe WebHook, feature_category: :integrations do
end
it 'is twice the initial value' do
- expect(hook.next_backoff).to eq(2 * described_class::INITIAL_BACKOFF)
+ expect(hook.next_backoff).to eq(2 * WebHooks::AutoDisabling::INITIAL_BACKOFF)
end
end
@@ -366,7 +366,7 @@ RSpec.describe WebHook, feature_category: :integrations do
end
it 'grows exponentially' do
- expect(hook.next_backoff).to eq(2 * 2 * 2 * described_class::INITIAL_BACKOFF)
+ expect(hook.next_backoff).to eq(2 * 2 * 2 * WebHooks::AutoDisabling::INITIAL_BACKOFF)
end
end
@@ -376,7 +376,7 @@ RSpec.describe WebHook, feature_category: :integrations do
end
it 'does not exceed the max backoff value' do
- expect(hook.next_backoff).to eq(described_class::MAX_BACKOFF)
+ expect(hook.next_backoff).to eq(WebHooks::AutoDisabling::MAX_BACKOFF)
end
end
end
@@ -498,13 +498,13 @@ RSpec.describe WebHook, feature_category: :integrations do
end
it 'reduces to MAX_FAILURES' do
- expect { hook.backoff! }.to change(hook, :recent_failures).to(described_class::MAX_FAILURES)
+ expect { hook.backoff! }.to change(hook, :recent_failures).to(WebHooks::AutoDisabling::MAX_FAILURES)
end
end
context 'when the recent failure value is MAX_FAILURES' do
before do
- hook.update!(recent_failures: described_class::MAX_FAILURES, disabled_until: 1.hour.ago)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::MAX_FAILURES, disabled_until: 1.hour.ago)
end
it 'does not change recent_failures' do
@@ -514,7 +514,7 @@ RSpec.describe WebHook, feature_category: :integrations do
context 'when we have exhausted the grace period' do
before do
- hook.update!(recent_failures: described_class::FAILURE_THRESHOLD)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD)
end
it 'sets disabled_until to the next backoff' do
@@ -527,8 +527,8 @@ RSpec.describe WebHook, feature_category: :integrations do
context 'when we have backed off MAX_FAILURES times' do
before do
- stub_const("#{described_class}::MAX_FAILURES", 5)
- (described_class::FAILURE_THRESHOLD + 5).times { hook.backoff! }
+ stub_const("WebHooks::AutoDisabling::MAX_FAILURES", 5)
+ (WebHooks::AutoDisabling::FAILURE_THRESHOLD + 5).times { hook.backoff! }
end
it 'does not let the backoff count exceed the maximum failure count' do
@@ -544,7 +544,7 @@ RSpec.describe WebHook, feature_category: :integrations do
it 'changes disabled_until when it has elapsed', :skip_freeze_time do
travel_to(hook.disabled_until + 1.minute) do
expect { hook.backoff! }.to change { hook.disabled_until }
- expect(hook.backoff_count).to eq(described_class::MAX_FAILURES)
+ expect(hook.backoff_count).to eq(WebHooks::AutoDisabling::MAX_FAILURES)
end
end
end
@@ -567,7 +567,7 @@ RSpec.describe WebHook, feature_category: :integrations do
end
it 'does not update the hook if the the failure count exceeds the maximum value' do
- hook.recent_failures = described_class::MAX_FAILURES
+ hook.recent_failures = WebHooks::AutoDisabling::MAX_FAILURES
sql_count = ActiveRecord::QueryRecorder.new { hook.failed! }.count
diff --git a/spec/models/import_export_upload_spec.rb b/spec/models/import_export_upload_spec.rb
index e13f504b82a..9811dbf60e3 100644
--- a/spec/models/import_export_upload_spec.rb
+++ b/spec/models/import_export_upload_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe ImportExportUpload do
let(:after_commit_callbacks) { described_class._commit_callbacks.select { |cb| cb.kind == :after } }
def find_callback(callbacks, key)
- callbacks.find { |cb| cb.instance_variable_get(:@key) == key }
+ callbacks.find { |cb| cb.filter == key }
end
it 'export file is stored in after_commit callback' do
diff --git a/spec/models/import_failure_spec.rb b/spec/models/import_failure_spec.rb
index 9fee1b0ae7b..101da1212cf 100644
--- a/spec/models/import_failure_spec.rb
+++ b/spec/models/import_failure_spec.rb
@@ -8,9 +8,18 @@ RSpec.describe ImportFailure do
let_it_be(:correlation_id) { 'ABC' }
let_it_be(:hard_failure) { create(:import_failure, :hard_failure, project: project, correlation_id_value: correlation_id) }
let_it_be(:soft_failure) { create(:import_failure, :soft_failure, project: project, correlation_id_value: correlation_id) }
+ let_it_be(:github_import_failure) { create(:import_failure, :github_import_failure, project: project) }
let_it_be(:unrelated_failure) { create(:import_failure, project: project) }
- it 'returns hard failures given a correlation ID' do
+ it 'returns failures with external_identifiers' do
+ expect(ImportFailure.with_external_identifiers).to match_array([github_import_failure])
+ end
+
+ it 'returns failures for the given correlation ID' do
+ expect(ImportFailure.failures_by_correlation_id(correlation_id)).to match_array([hard_failure, soft_failure])
+ end
+
+ it 'returns hard failures for the given correlation ID' do
expect(ImportFailure.hard_failures_by_correlation_id(correlation_id)).to eq([hard_failure])
end
@@ -45,5 +54,21 @@ RSpec.describe ImportFailure do
it { is_expected.to validate_presence_of(:group) }
end
+
+ describe '#external_identifiers' do
+ it { is_expected.to allow_value({ note_id: 234, noteable_id: 345, noteable_type: 'MergeRequest' }).for(:external_identifiers) }
+ it { is_expected.not_to allow_value(nil).for(:external_identifiers) }
+ it { is_expected.not_to allow_value({ ids: [123] }).for(:external_identifiers) }
+
+ it 'allows up to 3 fields' do
+ is_expected.not_to allow_value({
+ note_id: 234,
+ noteable_id: 345,
+ noteable_type: 'MergeRequest',
+ object_type: 'pull_request',
+ extra_attribute: 'abc'
+ }).for(:external_identifiers)
+ end
+ end
end
end
diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb
index f57667cc5d6..7710a05820c 100644
--- a/spec/models/instance_configuration_spec.rb
+++ b/spec/models/instance_configuration_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe InstanceConfiguration do
it 'returns Settings.pages' do
gitlab_pages.delete(:ip_address)
- expect(gitlab_pages).to eq(Settings.pages.symbolize_keys)
+ expect(gitlab_pages).to eq(Settings.pages.to_hash.deep_symbolize_keys)
end
it 'returns the GitLab\'s pages host ip address' do
@@ -189,7 +189,6 @@ RSpec.describe InstanceConfiguration do
plan: plan1,
ci_pipeline_size: 1001,
ci_active_jobs: 1002,
- ci_active_pipelines: 1003,
ci_project_subscriptions: 1004,
ci_pipeline_schedules: 1005,
ci_needs_size_limit: 1006,
@@ -200,7 +199,6 @@ RSpec.describe InstanceConfiguration do
plan: plan2,
ci_pipeline_size: 1101,
ci_active_jobs: 1102,
- ci_active_pipelines: 1103,
ci_project_subscriptions: 1104,
ci_pipeline_schedules: 1105,
ci_needs_size_limit: 1106,
@@ -214,7 +212,6 @@ RSpec.describe InstanceConfiguration do
expect(ci_cd_size_limits[:Plan1]).to eq({
ci_active_jobs: 1002,
- ci_active_pipelines: 1003,
ci_needs_size_limit: 1006,
ci_pipeline_schedules: 1005,
ci_pipeline_size: 1001,
@@ -224,7 +221,6 @@ RSpec.describe InstanceConfiguration do
})
expect(ci_cd_size_limits[:Plan2]).to eq({
ci_active_jobs: 1102,
- ci_active_pipelines: 1103,
ci_needs_size_limit: 1106,
ci_pipeline_schedules: 1105,
ci_pipeline_size: 1101,
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index a247881899f..46c30074ae7 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -1092,7 +1092,7 @@ RSpec.describe Integration, feature_category: :integrations do
field :foo_dt, storage: :data_fields
field :bar, type: 'password'
- field :password
+ field :password, is_secret: true
field :webhook
diff --git a/spec/models/integrations/apple_app_store_spec.rb b/spec/models/integrations/apple_app_store_spec.rb
index 1a57f556895..70b32a15148 100644
--- a/spec/models/integrations/apple_app_store_spec.rb
+++ b/spec/models/integrations/apple_app_store_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
it { is_expected.to validate_presence_of :app_store_issuer_id }
it { is_expected.to validate_presence_of :app_store_key_id }
it { is_expected.to validate_presence_of :app_store_private_key }
+ it { is_expected.to validate_presence_of :app_store_private_key_file_name }
it { is_expected.to allow_value('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee').for(:app_store_issuer_id) }
it { is_expected.not_to allow_value('abcde').for(:app_store_issuer_id) }
it { is_expected.to allow_value(File.read('spec/fixtures/ssl_key.pem')).for(:app_store_private_key) }
@@ -28,8 +29,8 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
describe '#fields' do
it 'returns custom fields' do
- expect(apple_app_store_integration.fields.pluck(:name)).to eq(%w[app_store_issuer_id app_store_key_id
- app_store_private_key])
+ expect(apple_app_store_integration.fields.pluck(:name)).to match_array(%w[app_store_issuer_id app_store_key_id
+ app_store_private_key app_store_private_key_file_name])
end
end
@@ -40,8 +41,8 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
end
it 'returns false for an invalid request' do
- allow(AppStoreConnect::Client).to receive_message_chain(:new,
-:apps).and_return({ errors: [title: "error title"] })
+ allow(AppStoreConnect::Client).to receive_message_chain(:new, :apps)
+ .and_return({ errors: [title: "error title"] })
expect(apple_app_store_integration.test[:success]).to be false
end
end
@@ -80,6 +81,12 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
value: apple_app_store_integration.app_store_key_id,
masked: true,
public: false
+ },
+ {
+ key: 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64',
+ value: described_class::IS_KEY_CONTENT_BASE64,
+ masked: false,
+ public: false
}
]
diff --git a/spec/models/integrations/buildkite_spec.rb b/spec/models/integrations/buildkite_spec.rb
index 5f62c68bd2b..29c649af6c6 100644
--- a/spec/models/integrations/buildkite_spec.rb
+++ b/spec/models/integrations/buildkite_spec.rb
@@ -146,9 +146,10 @@ RSpec.describe Integrations::Buildkite, :use_clean_rails_memory_store_caching do
def stub_request(status: 200, body: nil)
body ||= %q({"status":"success"})
- stub_full_request(buildkite_full_url)
- .to_return(status: status,
- headers: { 'Content-Type' => 'application/json' },
- body: body)
+ stub_full_request(buildkite_full_url).to_return(
+ status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body
+ )
end
end
diff --git a/spec/models/integrations/campfire_spec.rb b/spec/models/integrations/campfire_spec.rb
index ae923cd38fc..38d3d89cdbf 100644
--- a/spec/models/integrations/campfire_spec.rb
+++ b/spec/models/integrations/campfire_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::Campfire do
+RSpec.describe Integrations::Campfire, feature_category: :integrations do
include StubRequests
it_behaves_like Integrations::ResetSecretFields do
@@ -88,4 +88,16 @@ RSpec.describe Integrations::Campfire do
expect(WebMock).not_to have_requested(:post, '*/room/.*/speak.json')
end
end
+
+ describe '#log_error' do
+ subject { described_class.new.log_error('error') }
+
+ it 'logs an error' do
+ expect(Gitlab::IntegrationsLogger).to receive(:error).with(
+ hash_including(integration_class: 'Integrations::Campfire', message: 'error')
+ ).and_call_original
+
+ is_expected.to be_truthy
+ end
+ end
end
diff --git a/spec/models/integrations/chat_message/deployment_message_spec.rb b/spec/models/integrations/chat_message/deployment_message_spec.rb
index 8da27ef5aa0..d16c191bd08 100644
--- a/spec/models/integrations/chat_message/deployment_message_spec.rb
+++ b/spec/models/integrations/chat_message/deployment_message_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Integrations::ChatMessage::DeploymentMessage do
let_it_be(:user) { create(:user, name: 'John Smith', username: 'smith') }
let_it_be(:namespace) { create(:namespace, name: 'myspace') }
- let_it_be(:project) { create(:project, :repository, namespace: namespace, name: 'myproject') }
+ let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'myproject') }
let_it_be(:commit) { project.commit('HEAD') }
let_it_be(:ci_build) { create(:ci_build, project: project) }
let_it_be(:environment) { create(:environment, name: 'myenvironment', project: project) }
diff --git a/spec/models/integrations/every_integration_spec.rb b/spec/models/integrations/every_integration_spec.rb
index 8666ef512fc..c39a3486eb4 100644
--- a/spec/models/integrations/every_integration_spec.rb
+++ b/spec/models/integrations/every_integration_spec.rb
@@ -11,9 +11,9 @@ RSpec.describe 'Every integration' do
let(:integration) { integration_class.new }
context 'secret fields', :aggregate_failures do
- it "uses type: 'password' for all secret fields, except when bypassed" do
+ it "uses type: 'password' for all secret fields" do
integration.fields.each do |field|
- next unless Integrations::Field::SECRET_NAME.match?(field[:name]) && field[:is_secret]
+ next unless field[:is_secret]
expect(field[:type]).to eq('password'),
"Field '#{field[:name]}' should use type 'password'"
diff --git a/spec/models/integrations/ewm_spec.rb b/spec/models/integrations/ewm_spec.rb
index dc48a2c982f..4f4ff038b19 100644
--- a/spec/models/integrations/ewm_spec.rb
+++ b/spec/models/integrations/ewm_spec.rb
@@ -31,27 +31,27 @@ RSpec.describe Integrations::Ewm do
describe "ReferencePatternValidation" do
it "extracts bug" do
- expect(described_class.reference_pattern.match("This is bug 123")[:issue]).to eq("bug 123")
+ expect(subject.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")
+ expect(subject.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")
+ expect(subject.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")
+ expect(subject.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")
+ expect(subject.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")
+ expect(subject.reference_pattern.match("This is rtcwi 123")[:issue]).to eq("rtcwi 123")
end
end
end
diff --git a/spec/models/integrations/field_spec.rb b/spec/models/integrations/field_spec.rb
index c30f9ef0d7b..ca71dd0e6d3 100644
--- a/spec/models/integrations/field_spec.rb
+++ b/spec/models/integrations/field_spec.rb
@@ -15,8 +15,8 @@ RSpec.describe ::Integrations::Field do
end
describe '#initialize' do
- it 'sets type password for secret names' do
- attrs[:name] = 'token'
+ it 'sets type password for secret fields' do
+ attrs[:is_secret] = true
attrs[:type] = 'text'
expect(field[:type]).to eq('password')
@@ -84,7 +84,7 @@ RSpec.describe ::Integrations::Field do
when :type
eq 'text'
when :is_secret
- eq true
+ eq false
else
be_nil
end
@@ -175,16 +175,6 @@ RSpec.describe ::Integrations::Field do
it { is_expected.to be_secret }
end
- %w[token api_token api_key secret_key secret_sauce password passphrase].each do |name|
- context "when named #{name}" do
- before do
- attrs[:name] = name
- end
-
- it { is_expected.to be_secret }
- end
- end
-
context "when named url" do
before do
attrs[:name] = :url
diff --git a/spec/models/integrations/gitlab_slack_application_spec.rb b/spec/models/integrations/gitlab_slack_application_spec.rb
new file mode 100644
index 00000000000..68476dde2a3
--- /dev/null
+++ b/spec/models/integrations/gitlab_slack_application_spec.rb
@@ -0,0 +1,337 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::GitlabSlackApplication, feature_category: :integrations do
+ include AfterNextHelpers
+
+ it_behaves_like Integrations::BaseSlackNotification, factory: :gitlab_slack_application_integration do
+ before do
+ stub_request(:post, "#{::Slack::API::BASE_URL}/chat.postMessage").to_return(body: '{"ok":true}')
+ end
+ end
+
+ describe 'validations' do
+ it { is_expected.not_to validate_presence_of(:webhook) }
+ end
+
+ describe 'default values' do
+ it { expect(subject.category).to eq(:chat) }
+
+ it { is_expected.not_to be_alert_events }
+ it { is_expected.not_to be_commit_events }
+ it { is_expected.not_to be_confidential_issues_events }
+ it { is_expected.not_to be_confidential_note_events }
+ it { is_expected.not_to be_deployment_events }
+ it { is_expected.not_to be_issues_events }
+ it { is_expected.not_to be_job_events }
+ it { is_expected.not_to be_merge_requests_events }
+ it { is_expected.not_to be_note_events }
+ it { is_expected.not_to be_pipeline_events }
+ it { is_expected.not_to be_push_events }
+ it { is_expected.not_to be_tag_push_events }
+ it { is_expected.not_to be_vulnerability_events }
+ it { is_expected.not_to be_wiki_page_events }
+ end
+
+ describe '#execute' do
+ let_it_be(:user) { build_stubbed(:user) }
+
+ let(:slack_integration) { build(:slack_integration) }
+ let(:data) { Gitlab::DataBuilder::Push.build_sample(integration.project, user) }
+ let(:slack_api_method_uri) { "#{::Slack::API::BASE_URL}/chat.postMessage" }
+
+ let(:mock_message) do
+ instance_double(Integrations::ChatMessage::PushMessage, attachments: ['foo'], pretext: 'bar')
+ end
+
+ subject(:integration) { build(:gitlab_slack_application_integration, slack_integration: slack_integration) }
+
+ before do
+ allow(integration).to receive(:get_message).and_return(mock_message)
+ allow(integration).to receive(:log_usage)
+ end
+
+ def stub_slack_request(channel: '#push_channel', success: true)
+ post_body = {
+ body: {
+ attachments: mock_message.attachments,
+ text: mock_message.pretext,
+ unfurl_links: false,
+ unfurl_media: false,
+ channel: channel
+ }
+ }
+
+ response = { ok: success }.to_json
+
+ stub_request(:post, slack_api_method_uri).with(post_body)
+ .to_return(body: response, headers: { 'Content-Type' => 'application/json; charset=utf-8' })
+ end
+
+ it 'notifies Slack' do
+ stub_slack_request
+
+ expect(integration.execute(data)).to be true
+ end
+
+ context 'when the integration is not configured for event' do
+ before do
+ integration.push_channel = nil
+ end
+
+ it 'does not notify Slack' do
+ expect(integration.execute(data)).to be false
+ end
+ end
+
+ context 'when Slack API responds with an error' do
+ it 'logs the error and API response' do
+ stub_slack_request(success: false)
+
+ expect(Gitlab::IntegrationsLogger).to receive(:error).with(
+ {
+ integration_class: described_class.name,
+ integration_id: integration.id,
+ project_id: integration.project_id,
+ project_path: kind_of(String),
+ message: 'Slack API error when notifying',
+ api_response: { 'ok' => false }
+ }
+ )
+ expect(integration.execute(data)).to be false
+ end
+ end
+
+ context 'when there is an HTTP error' do
+ it 'logs the error' do
+ expect_next(Slack::API).to receive(:post).and_raise(Net::ReadTimeout)
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(
+ kind_of(Net::ReadTimeout),
+ {
+ slack_integration_id: slack_integration.id,
+ integration_id: integration.id
+ }
+ )
+ expect(integration.execute(data)).to be false
+ end
+ end
+
+ context 'when configured to post to multiple Slack channels' do
+ before do
+ push_channels = '#first_channel, #second_channel'
+ integration.push_channel = push_channels
+ end
+
+ it 'posts to both Slack channels and returns true' do
+ stub_slack_request(channel: '#first_channel')
+ stub_slack_request(channel: '#second_channel')
+
+ expect(integration.execute(data)).to be true
+ end
+
+ context 'when one of the posts responds with an error' do
+ it 'posts to both channels and returns true' do
+ stub_slack_request(channel: '#first_channel', success: false)
+ stub_slack_request(channel: '#second_channel')
+
+ expect(Gitlab::IntegrationsLogger).to receive(:error).once
+ expect(integration.execute(data)).to be true
+ end
+ end
+
+ context 'when both of the posts respond with an error' do
+ it 'posts to both channels and returns false' do
+ stub_slack_request(channel: '#first_channel', success: false)
+ stub_slack_request(channel: '#second_channel', success: false)
+
+ expect(Gitlab::IntegrationsLogger).to receive(:error).twice
+ expect(integration.execute(data)).to be false
+ end
+ end
+
+ context 'when one of the posts raises an HTTP exception' do
+ it 'posts to one channel and returns true' do
+ stub_slack_request(channel: '#second_channel')
+
+ expect_next_instance_of(Slack::API) do |api_client|
+ expect(api_client).to receive(:post)
+ .with('chat.postMessage', hash_including(channel: '#first_channel')).and_raise(Net::ReadTimeout)
+ expect(api_client).to receive(:post)
+ .with('chat.postMessage', hash_including(channel: '#second_channel')).and_call_original
+ end
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).once
+ expect(integration.execute(data)).to be true
+ end
+ end
+
+ context 'when both of the posts raise an HTTP exception' do
+ it 'posts to one channel and returns true' do
+ stub_slack_request(channel: '#second_channel')
+
+ expect_next(Slack::API).to receive(:post).twice.and_raise(Net::ReadTimeout)
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).twice
+ expect(integration.execute(data)).to be false
+ end
+ end
+ end
+ end
+
+ describe '#test' do
+ let(:integration) { build(:gitlab_slack_application_integration) }
+
+ let(:slack_api_method_uri) { "#{::Slack::API::BASE_URL}/chat.postEphemeral" }
+ let(:response_failure) { { error: 'channel_not_found' } }
+ let(:response_success) { { error: 'user_not_in_channel' } }
+ let(:response_headers) { { 'Content-Type' => 'application/json; charset=utf-8' } }
+ let(:request_body) do
+ {
+ text: 'Test',
+ user: integration.bot_user_id
+ }
+ end
+
+ subject(:result) { integration.test({}) }
+
+ def stub_slack_request(channel:, success:)
+ response_body = success ? response_success : response_failure
+
+ stub_request(:post, slack_api_method_uri)
+ .with(body: request_body.merge(channel: channel))
+ .to_return(body: response_body.to_json, headers: response_headers)
+ end
+
+ context 'when all channels can be posted to' do
+ before do
+ stub_slack_request(channel: anything, success: true)
+ end
+
+ it 'is successful' do
+ is_expected.to eq({ success: true, result: nil })
+ end
+ end
+
+ context 'when the same channel is used for multiple events' do
+ let(:integration) do
+ build(:gitlab_slack_application_integration, all_channels: false, push_channel: '#foo', issue_channel: '#foo')
+ end
+
+ it 'only tests the channel once' do
+ stub_slack_request(channel: '#foo', success: true)
+
+ is_expected.to eq({ success: true, result: nil })
+ expect(WebMock).to have_requested(:post, slack_api_method_uri).once
+ end
+ end
+
+ context 'when there are channels that cannot be posted to' do
+ let(:unpostable_channels) { ['#push_channel', '#issue_channel'] }
+
+ before do
+ stub_slack_request(channel: anything, success: true)
+
+ unpostable_channels.each do |channel|
+ stub_slack_request(channel: channel, success: false)
+ end
+ end
+
+ it 'returns an error message informing which channels cannot be posted to' do
+ expected_message = "Unable to post to #{unpostable_channels.to_sentence}, " \
+ 'please add the GitLab Slack app to any private Slack channels'
+
+ is_expected.to eq({ success: false, result: expected_message })
+ end
+
+ context 'when integration is not configured for notifications' do
+ let_it_be(:integration) { build(:gitlab_slack_application_integration, all_channels: false) }
+
+ it 'is successful' do
+ is_expected.to eq({ success: true, result: nil })
+ end
+ end
+ end
+
+ context 'when integration is using legacy version of Slack app' do
+ before do
+ integration.slack_integration = build(:slack_integration, :legacy)
+ end
+
+ it 'returns an error to inform the user to update their integration' do
+ expected_message = 'GitLab for Slack app must be reinstalled to enable notifications'
+
+ is_expected.to eq({ success: false, result: expected_message })
+ end
+ end
+ end
+
+ context 'when the integration is active' do
+ before do
+ subject.active = true
+ end
+
+ it 'is editable, and presents editable fields' do
+ expect(subject).to be_editable
+ expect(subject.fields).not_to be_empty
+ expect(subject.configurable_events).not_to be_empty
+ end
+
+ it 'includes the expected sections' do
+ section_types = subject.sections.pluck(:type)
+
+ expect(section_types).to eq(
+ [
+ described_class::SECTION_TYPE_TRIGGER,
+ described_class::SECTION_TYPE_CONFIGURATION
+ ]
+ )
+ end
+ end
+
+ context 'when the integration is not active' do
+ before do
+ subject.active = false
+ end
+
+ it 'is not editable, and presents no editable fields' do
+ expect(subject).not_to be_editable
+ expect(subject.fields).to be_empty
+ expect(subject.configurable_events).to be_empty
+ end
+
+ it 'does not include sections' do
+ section_types = subject.sections.pluck(:type)
+
+ expect(section_types).to be_empty
+ end
+ end
+
+ describe '#description' do
+ specify { expect(subject.description).to be_present }
+ end
+
+ describe '#upgrade_needed?' do
+ context 'with all_features_supported' do
+ subject(:integration) { create(:gitlab_slack_application_integration, :all_features_supported) }
+
+ it 'is false' do
+ expect(integration).not_to be_upgrade_needed
+ end
+ end
+
+ context 'without all_features_supported' do
+ subject(:integration) { create(:gitlab_slack_application_integration) }
+
+ it 'is true' do
+ expect(integration).to be_upgrade_needed
+ end
+ end
+
+ context 'without slack_integration' do
+ subject(:integration) { create(:gitlab_slack_application_integration, slack_integration: nil) }
+
+ it 'is false' do
+ expect(integration).not_to be_upgrade_needed
+ end
+ end
+ end
+end
diff --git a/spec/models/integrations/google_play_spec.rb b/spec/models/integrations/google_play_spec.rb
new file mode 100644
index 00000000000..8349ac71bc9
--- /dev/null
+++ b/spec/models/integrations/google_play_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::GooglePlay, feature_category: :mobile_devops do
+ describe 'Validations' do
+ context 'when active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of :service_account_key_file_name }
+ it { is_expected.to validate_presence_of :service_account_key }
+ it { is_expected.to validate_presence_of :package_name }
+ it { is_expected.to allow_value(File.read('spec/fixtures/service_account.json')).for(:service_account_key) }
+ it { is_expected.not_to allow_value(File.read('spec/fixtures/group.json')).for(:service_account_key) }
+ it { is_expected.to allow_value('com.example.myapp').for(:package_name) }
+ it { is_expected.to allow_value('com.example.myorg.myapp').for(:package_name) }
+ it { is_expected.to allow_value('com_us.example.my_org.my_app').for(:package_name) }
+ it { is_expected.to allow_value('a.a.a').for(:package_name) }
+ it { is_expected.to allow_value('com.example').for(:package_name) }
+ it { is_expected.not_to allow_value('com').for(:package_name) }
+ it { is_expected.not_to allow_value('com.example.my app').for(:package_name) }
+ it { is_expected.not_to allow_value('1com.example.myapp').for(:package_name) }
+ it { is_expected.not_to allow_value('com.1example.myapp').for(:package_name) }
+ it { is_expected.not_to allow_value('com.example._myapp').for(:package_name) }
+ end
+ end
+
+ context 'when integration is enabled' do
+ let(:google_play_integration) { build(:google_play_integration) }
+
+ describe '#fields' do
+ it 'returns custom fields' do
+ expect(google_play_integration.fields.pluck(:name)).to match_array(%w[package_name service_account_key
+ service_account_key_file_name])
+ end
+ end
+
+ describe '#test' do
+ it 'returns true for a successful request' do
+ allow_next_instance_of(Google::Apis::AndroidpublisherV3::AndroidPublisherService) do |instance|
+ allow(instance).to receive(:list_reviews)
+ end
+ expect(google_play_integration.test[:success]).to be true
+ end
+
+ it 'returns false for an invalid request' do
+ allow_next_instance_of(Google::Apis::AndroidpublisherV3::AndroidPublisherService) do |instance|
+ allow(instance).to receive(:list_reviews).and_raise(Google::Apis::ClientError.new('error'))
+ end
+ expect(google_play_integration.test[:success]).to be false
+ end
+ end
+
+ describe '#help' do
+ it 'renders prompt information' do
+ expect(google_play_integration.help).not_to be_empty
+ end
+ end
+
+ describe '.to_param' do
+ it 'returns the name of the integration' do
+ expect(described_class.to_param).to eq('google_play')
+ end
+ end
+
+ describe '#ci_variables' do
+ let(:google_play_integration) { build_stubbed(:google_play_integration) }
+
+ it 'returns vars when the integration is activated' do
+ ci_vars = [
+ {
+ key: 'SUPPLY_PACKAGE_NAME',
+ value: google_play_integration.package_name,
+ masked: false,
+ public: false
+ },
+ {
+ key: 'SUPPLY_JSON_KEY_DATA',
+ value: google_play_integration.service_account_key,
+ masked: true,
+ public: false
+ }
+ ]
+
+ expect(google_play_integration.ci_variables).to match_array(ci_vars)
+ end
+ end
+ end
+
+ context 'when integration is disabled' do
+ let(:google_play_integration) { build_stubbed(:google_play_integration, active: false) }
+
+ describe '#ci_variables' do
+ it 'returns an empty array' do
+ expect(google_play_integration.ci_variables).to match_array([])
+ end
+ end
+ end
+end
diff --git a/spec/models/integrations/hangouts_chat_spec.rb b/spec/models/integrations/hangouts_chat_spec.rb
index 288478b494e..1ebf2ec3005 100644
--- a/spec/models/integrations/hangouts_chat_spec.rb
+++ b/spec/models/integrations/hangouts_chat_spec.rb
@@ -116,10 +116,13 @@ RSpec.describe Integrations::HangoutsChat do
context 'when commit comment event executed' do
let(:commit_note) do
- create(:note_on_commit, author: user,
- project: project,
- commit_id: project.repository.commit.id,
- note: 'a comment on a commit')
+ create(
+ :note_on_commit,
+ author: user,
+ project: project,
+ commit_id: project.repository.commit.id,
+ note: 'a comment on a commit'
+ )
end
it "adds thread key" do
@@ -135,8 +138,7 @@ RSpec.describe Integrations::HangoutsChat do
context 'when merge request comment event executed' do
let(:merge_request_note) do
- create(:note_on_merge_request, project: project,
- note: "merge request note")
+ create(:note_on_merge_request, project: project, note: "merge request note")
end
it "adds thread key" do
@@ -168,8 +170,7 @@ RSpec.describe Integrations::HangoutsChat do
context 'when snippet comment event executed' do
let(:snippet_note) do
- create(:note_on_project_snippet, project: project,
- note: "snippet note")
+ create(:note_on_project_snippet, project: project, note: "snippet note")
end
it "adds thread key" do
diff --git a/spec/models/integrations/harbor_spec.rb b/spec/models/integrations/harbor_spec.rb
index b4580028112..c4da876a0dd 100644
--- a/spec/models/integrations/harbor_spec.rb
+++ b/spec/models/integrations/harbor_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Integrations::Harbor do
before do
allow_next_instance_of(Gitlab::Harbor::Client) do |client|
- allow(client).to receive(:ping).and_return(test_response)
+ allow(client).to receive(:check_project_availability).and_return(test_response)
end
end
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index a4ccae459cf..d3cb386e8e0 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::Jira do
+RSpec.describe Integrations::Jira, feature_category: :integrations do
include AssetsHelpers
let_it_be(:project) { create(:project, :repository) }
@@ -11,6 +11,9 @@ RSpec.describe Integrations::Jira do
let(:url) { 'http://jira.example.com' }
let(:api_url) { 'http://api-jira.example.com' }
let(:username) { 'jira-username' }
+ let(:jira_auth_type) { 0 }
+ let(:jira_issue_prefix) { '' }
+ let(:jira_issue_regex) { '' }
let(:password) { 'jira-password' }
let(:project_key) { nil }
let(:transition_id) { 'test27' }
@@ -48,9 +51,39 @@ RSpec.describe Integrations::Jira do
it { is_expected.to validate_presence_of(:url) }
it { is_expected.to validate_presence_of(:username) }
it { is_expected.to validate_presence_of(:password) }
+ it { is_expected.to validate_presence_of(:jira_auth_type) }
+ it { is_expected.to validate_length_of(:jira_issue_regex).is_at_most(255) }
+ it { is_expected.to validate_length_of(:jira_issue_prefix).is_at_most(255) }
+ it { is_expected.to validate_inclusion_of(:jira_auth_type).in_array([0, 1]) }
it_behaves_like 'issue tracker integration URL attribute', :url
it_behaves_like 'issue tracker integration URL attribute', :api_url
+
+ context 'with personal_access_token_authorization' do
+ before do
+ jira_integration.jira_auth_type = 1
+ end
+
+ it { is_expected.not_to validate_presence_of(:username) }
+ end
+
+ context 'when URL is for Jira Cloud' do
+ before do
+ jira_integration.url = 'https://test.atlassian.net'
+ end
+
+ it 'is valid when jira_auth_type is basic' do
+ jira_integration.jira_auth_type = 0
+
+ expect(jira_integration).to be_valid
+ end
+
+ it 'is invalid when jira_auth_type is PAT' do
+ jira_integration.jira_auth_type = 1
+
+ expect(jira_integration).not_to be_valid
+ end
+ end
end
context 'when integration is inactive' do
@@ -62,6 +95,10 @@ RSpec.describe Integrations::Jira do
it { is_expected.not_to validate_presence_of(:url) }
it { is_expected.not_to validate_presence_of(:username) }
it { is_expected.not_to validate_presence_of(:password) }
+ it { is_expected.not_to validate_presence_of(:jira_auth_type) }
+ it { is_expected.not_to validate_length_of(:jira_issue_regex).is_at_most(255) }
+ it { is_expected.not_to validate_length_of(:jira_issue_prefix).is_at_most(255) }
+ it { is_expected.not_to validate_inclusion_of(:jira_auth_type).in_array([0, 1]) }
end
describe 'jira_issue_transition_id' do
@@ -167,7 +204,7 @@ RSpec.describe Integrations::Jira do
subject(:fields) { integration.fields }
it 'returns custom fields' do
- expect(fields.pluck(:name)).to eq(%w[url api_url username password jira_issue_transition_id])
+ expect(fields.pluck(:name)).to eq(%w[url api_url jira_auth_type username password jira_issue_regex jira_issue_prefix jira_issue_transition_id])
end
end
@@ -202,7 +239,7 @@ RSpec.describe Integrations::Jira do
end
end
- describe '.reference_pattern' do
+ describe '#reference_pattern' do
using RSpec::Parameterized::TableSyntax
where(:key, :result) do
@@ -216,11 +253,77 @@ RSpec.describe Integrations::Jira do
'3EXT_EXT-1234' | ''
'CVE-2022-123' | ''
'CVE-123' | 'CVE-123'
+ 'abc-JIRA-1234' | 'JIRA-1234'
end
with_them do
specify do
- expect(described_class.reference_pattern.match(key).to_s).to eq(result)
+ expect(jira_integration.reference_pattern.match(key).to_s).to eq(result)
+ end
+ end
+
+ context 'with match prefix' do
+ before do
+ jira_integration.jira_issue_prefix = 'jira#'
+ end
+
+ where(:key, :result, :issue_key) do
+ 'jira##123' | '' | ''
+ 'jira#1#23#12' | '' | ''
+ 'jira#JIRA-1234A' | 'jira#JIRA-1234' | 'JIRA-1234'
+ 'jira#JIRA-1234-some_tag' | 'jira#JIRA-1234' | 'JIRA-1234'
+ 'JIRA-1234A' | '' | ''
+ 'JIRA-1234-some_tag' | '' | ''
+ 'myjira#JIRA-1234-some_tag' | '' | ''
+ 'MYjira#JIRA-1234-some_tag' | '' | ''
+ 'my-jira#JIRA-1234-some_tag' | 'jira#JIRA-1234' | 'JIRA-1234'
+ end
+
+ with_them do
+ specify do
+ expect(jira_integration.reference_pattern.match(key).to_s).to eq(result)
+
+ expect(jira_integration.reference_pattern.match(key)[:issue]).to eq(issue_key) unless result.empty?
+ end
+ end
+ end
+
+ context 'with trailing space in jira_issue_prefix' do
+ before do
+ jira_integration.jira_issue_prefix = 'Jira# '
+ end
+
+ it 'leaves the trailing space' do
+ expect(jira_integration.jira_issue_prefix).to eq('Jira# ')
+ end
+
+ it 'pulls the issue ID without a prefix' do
+ expect(jira_integration.reference_pattern.match('Jira# FOO-123')[:issue]).to eq('FOO-123')
+ end
+ end
+
+ context 'with custom issue pattern' do
+ before do
+ jira_integration.jira_issue_regex = '[A-Z][0-9]-[0-9]+'
+ end
+
+ where(:key, :result) do
+ 'J1-123' | 'J1-123'
+ 'AAbJ J1-123' | 'J1-123'
+ '#A1-123' | 'A1-123'
+ 'J1-1234-some_tag' | 'J1-1234'
+ 'J1-1234A' | 'J1-1234'
+ 'J1-1234-some_tag' | 'J1-1234'
+ 'JI1-123' | ''
+ 'J1I-123' | ''
+ 'JI-123' | ''
+ '#123' | ''
+ end
+
+ with_them do
+ specify do
+ expect(jira_integration.reference_pattern.match(key).to_s).to eq(result)
+ end
end
end
end
@@ -251,7 +354,10 @@ RSpec.describe Integrations::Jira do
project: project,
url: url,
api_url: api_url,
+ jira_auth_type: jira_auth_type,
username: username, password: password,
+ jira_issue_regex: jira_issue_regex,
+ jira_issue_prefix: jira_issue_prefix,
jira_issue_transition_id: transition_id
}
end
@@ -265,8 +371,11 @@ RSpec.describe Integrations::Jira do
it 'stores data in data_fields correctly' do
expect(integration.jira_tracker_data.url).to eq(url)
expect(integration.jira_tracker_data.api_url).to eq(api_url)
+ expect(integration.jira_tracker_data.jira_auth_type).to eq(jira_auth_type)
expect(integration.jira_tracker_data.username).to eq(username)
expect(integration.jira_tracker_data.password).to eq(password)
+ expect(integration.jira_tracker_data.jira_issue_regex).to eq(jira_issue_regex)
+ expect(integration.jira_tracker_data.jira_issue_prefix).to eq(jira_issue_prefix)
expect(integration.jira_tracker_data.jira_issue_transition_id).to eq(transition_id)
expect(integration.jira_tracker_data.deployment_cloud?).to be_truthy
end
@@ -469,15 +578,54 @@ RSpec.describe Integrations::Jira do
end
describe '#client' do
+ before do
+ stub_request(:get, 'http://jira.example.com/foo')
+ end
+
it 'uses the default GitLab::HTTP timeouts' do
timeouts = Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS
- stub_request(:get, 'http://jira.example.com/foo')
expect(Gitlab::HTTP).to receive(:httparty_perform_request)
.with(Net::HTTP::Get, '/foo', hash_including(timeouts)).and_call_original
jira_integration.client.get('/foo')
end
+
+ context 'with basic auth' do
+ before do
+ jira_integration.jira_auth_type = 0
+ end
+
+ it 'uses correct authorization options' do
+ expect_next_instance_of(JIRA::Client) do |instance|
+ expect(instance.request_client.options).to include(
+ additional_cookies: ['OBBasicAuth=fromDialog'],
+ auth_type: :basic,
+ use_cookies: true,
+ password: jira_integration.password,
+ username: jira_integration.username
+ )
+ end
+
+ jira_integration.client.get('/foo')
+ end
+ end
+
+ context 'with personal access token auth' do
+ before do
+ jira_integration.jira_auth_type = 1
+ end
+
+ it 'uses correct authorization options' do
+ expect_next_instance_of(JIRA::Client) do |instance|
+ expect(instance.request_client.options).to include(
+ default_headers: { "Authorization" => "Bearer #{password}" }
+ )
+ end
+
+ jira_integration.client.get('/foo')
+ end
+ end
end
describe '#find_issue' do
@@ -590,7 +738,6 @@ RSpec.describe Integrations::Jira do
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
subject { close_issue }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { 'Integrations::Jira' }
let(:action) { 'perform_integrations_action' }
let(:namespace) { project.namespace }
@@ -944,7 +1091,6 @@ RSpec.describe Integrations::Jira do
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { 'Integrations::Jira' }
let(:action) { 'perform_integrations_action' }
let(:namespace) { project.namespace }
@@ -1095,9 +1241,7 @@ RSpec.describe Integrations::Jira do
expect(integration.web_url).to eq('')
end
- it 'includes Atlassian referrer for gitlab.com' do
- allow(Gitlab).to receive(:com?).and_return(true)
-
+ it 'includes Atlassian referrer for SaaS', :saas do
expect(integration.web_url).to eq("http://jira.test.com/path?#{described_class::ATLASSIAN_REFERRER_GITLAB_COM.to_query}")
allow(Gitlab).to receive(:staging?).and_return(true)
diff --git a/spec/models/integrations/mattermost_slash_commands_spec.rb b/spec/models/integrations/mattermost_slash_commands_spec.rb
index 070adb9ba93..e393a905f45 100644
--- a/spec/models/integrations/mattermost_slash_commands_spec.rb
+++ b/spec/models/integrations/mattermost_slash_commands_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::MattermostSlashCommands do
+RSpec.describe Integrations::MattermostSlashCommands, feature_category: :integrations do
it_behaves_like Integrations::BaseSlashCommands
describe 'Mattermost API' do
@@ -123,12 +123,5 @@ RSpec.describe Integrations::MattermostSlashCommands do
end
end
end
-
- describe '#chat_responder' do
- it 'returns the responder to use for Mattermost' do
- expect(described_class.new.chat_responder)
- .to eq(Gitlab::Chat::Responder::Mattermost)
- end
- end
end
end
diff --git a/spec/models/integrations/prometheus_spec.rb b/spec/models/integrations/prometheus_spec.rb
index aa248abd3bb..8aa9b12c4f0 100644
--- a/spec/models/integrations/prometheus_spec.rb
+++ b/spec/models/integrations/prometheus_spec.rb
@@ -90,37 +90,6 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
end
end
end
-
- context 'with self-monitoring project and internal Prometheus' do
- before do
- integration.api_url = 'http://localhost:9090'
-
- stub_application_setting(self_monitoring_project_id: project.id)
- stub_config(prometheus: { enable: true, server_address: 'localhost:9090' })
- end
-
- it 'allows self-monitoring project to connect to internal Prometheus' do
- aggregate_failures do
- ['127.0.0.1', '192.168.2.3'].each do |url|
- allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)])
-
- expect(integration.can_query?).to be true
- end
- end
- end
-
- it 'does not allow self-monitoring project to connect to other local URLs' do
- integration.api_url = 'http://localhost:8000'
-
- aggregate_failures do
- ['127.0.0.1', '192.168.2.3'].each do |url|
- allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)])
-
- expect(integration.can_query?).to be false
- end
- end
- end
- end
end
end
@@ -218,23 +187,6 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
it 'blocks local requests' do
expect(integration.prometheus_client).to be_nil
end
-
- context 'with self-monitoring project and internal Prometheus URL' do
- before do
- stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
- stub_application_setting(self_monitoring_project_id: project.id)
-
- stub_config(prometheus: {
- enable: true,
- server_address: api_url
- })
- end
-
- it 'allows local requests' do
- expect(integration.prometheus_client).not_to be_nil
- expect { integration.prometheus_client.ping }.not_to raise_error
- end
- end
end
context 'behind IAP' do
@@ -332,7 +284,7 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
context 'cluster belongs to projects group' do
let_it_be(:group) { create(:group) }
- let(:project) { create(:project, :with_prometheus_integration, group: group) }
+ let_it_be(:project) { create(:project, :with_prometheus_integration, group: group) }
let_it_be(:cluster) { create(:cluster_for_group, groups: [group]) }
it 'returns true' do
diff --git a/spec/models/integrations/redmine_spec.rb b/spec/models/integrations/redmine_spec.rb
index 59997d2b6f6..8785fc8a1ed 100644
--- a/spec/models/integrations/redmine_spec.rb
+++ b/spec/models/integrations/redmine_spec.rb
@@ -38,11 +38,11 @@ RSpec.describe Integrations::Redmine do
end
end
- describe '.reference_pattern' do
+ describe '#reference_pattern' do
it_behaves_like 'allows project key on reference pattern'
it 'does allow # on the reference' do
- expect(described_class.reference_pattern.match('#123')[:issue]).to eq('123')
+ expect(subject.reference_pattern.match('#123')[:issue]).to eq('123')
end
end
end
diff --git a/spec/models/integrations/slack_slash_commands_spec.rb b/spec/models/integrations/slack_slash_commands_spec.rb
index 22cbaa777cd..f373fc2a2de 100644
--- a/spec/models/integrations/slack_slash_commands_spec.rb
+++ b/spec/models/integrations/slack_slash_commands_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::SlackSlashCommands do
+RSpec.describe Integrations::SlackSlashCommands, feature_category: :integrations do
it_behaves_like Integrations::BaseSlashCommands
describe '#trigger' do
@@ -40,11 +40,4 @@ RSpec.describe Integrations::SlackSlashCommands do
end
end
end
-
- describe '#chat_responder' do
- it 'returns the responder to use for Slack' do
- expect(described_class.new.chat_responder)
- .to eq(Gitlab::Chat::Responder::Slack)
- end
- end
end
diff --git a/spec/models/integrations/slack_workspace/api_scope_spec.rb b/spec/models/integrations/slack_workspace/api_scope_spec.rb
new file mode 100644
index 00000000000..92052983242
--- /dev/null
+++ b/spec/models/integrations/slack_workspace/api_scope_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackWorkspace::ApiScope, feature_category: :integrations do
+ describe '.find_or_initialize_by_names' do
+ it 'acts as insert into a global set of scope names' do
+ expect { described_class.find_or_initialize_by_names(%w[foo bar baz]) }
+ .to change { described_class.count }.by(3)
+
+ expect { described_class.find_or_initialize_by_names(%w[bar baz foo buzz]) }
+ .to change { described_class.count }.by(1)
+
+ expect { described_class.find_or_initialize_by_names(%w[baz foo]) }
+ .to change { described_class.count }.by(0)
+
+ expect(described_class.pluck(:name)).to match_array(%w[foo bar baz buzz])
+ end
+ end
+end
diff --git a/spec/models/integrations/squash_tm_spec.rb b/spec/models/integrations/squash_tm_spec.rb
new file mode 100644
index 00000000000..f12e37dae6d
--- /dev/null
+++ b/spec/models/integrations/squash_tm_spec.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SquashTm, feature_category: :integrations do
+ it_behaves_like Integrations::HasWebHook do
+ let_it_be(:project) { create(:project) }
+
+ let(:integration) { build(:squash_tm_integration, project: project) }
+ let(:hook_url) { "#{integration.url}?token={token}" }
+ end
+
+ it_behaves_like Integrations::ResetSecretFields do
+ let(:integration) { subject }
+ end
+
+ describe 'Validations' do
+ context 'when integration is active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of(:url) }
+ it { is_expected.to allow_value('https://example.com').for(:url) }
+ it { is_expected.not_to allow_value(nil).for(:url) }
+ it { is_expected.not_to allow_value('').for(:url) }
+ it { is_expected.not_to allow_value('foo').for(:url) }
+ it { is_expected.not_to allow_value('example.com').for(:url) }
+
+ it { is_expected.not_to validate_presence_of(:token) }
+ it { is_expected.to validate_length_of(:token).is_at_most(255) }
+ it { is_expected.to allow_value(nil).for(:token) }
+ it { is_expected.to allow_value('foo').for(:token) }
+ end
+
+ context 'when integration is inactive' do
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:url) }
+ it { is_expected.not_to validate_presence_of(:token) }
+ end
+ end
+
+ describe '#execute' do
+ let(:integration) { build(:squash_tm_integration, project: build(:project)) }
+
+ let(:squash_tm_hook_url) do
+ "#{integration.url}?token=#{integration.token}"
+ end
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:issue) { create(:issue) }
+ let(:data) { issue.to_hook_data(user) }
+
+ before do
+ stub_request(:post, squash_tm_hook_url)
+ end
+
+ it 'calls Squash TM API' do
+ integration.execute(data)
+
+ expect(a_request(:post, squash_tm_hook_url)).to have_been_made.once
+ end
+ end
+
+ describe '#test' do
+ let(:integration) { build(:squash_tm_integration) }
+
+ let(:squash_tm_hook_url) do
+ "#{integration.url}?token=#{integration.token}"
+ end
+
+ subject(:result) { integration.test({}) }
+
+ context 'when server is responding' do
+ let(:body) { 'OK' }
+ let(:status) { 200 }
+
+ before do
+ stub_request(:post, squash_tm_hook_url)
+ .to_return(status: status, body: body)
+ end
+
+ it { is_expected.to eq(success: true, result: 'OK') }
+ end
+
+ context 'when server rejects the request' do
+ let(:body) { 'Unauthorized' }
+ let(:status) { 401 }
+
+ before do
+ stub_request(:post, squash_tm_hook_url)
+ .to_return(status: status, body: body)
+ end
+
+ it { is_expected.to eq(success: false, result: body) }
+ end
+
+ context 'when test request executes with errors' do
+ before do
+ allow(integration).to receive(:execute_web_hook!)
+ .with({}, "Test Configuration Hook")
+ .and_raise(StandardError, 'error message')
+ end
+
+ it { is_expected.to eq(success: false, result: 'error message') }
+ end
+ end
+
+ describe '.default_test_event' do
+ subject { described_class.default_test_event }
+
+ it { is_expected.to eq('issue') }
+ end
+end
diff --git a/spec/models/integrations/youtrack_spec.rb b/spec/models/integrations/youtrack_spec.rb
index 618ebcbb76a..69dda244413 100644
--- a/spec/models/integrations/youtrack_spec.rb
+++ b/spec/models/integrations/youtrack_spec.rb
@@ -26,15 +26,15 @@ RSpec.describe Integrations::Youtrack do
end
end
- describe '.reference_pattern' do
+ describe '#reference_pattern' do
it_behaves_like 'allows project key on reference pattern'
it 'does allow project prefix on the reference' do
- expect(described_class.reference_pattern.match('YT-123')[:issue]).to eq('YT-123')
+ expect(subject.reference_pattern.match('YT-123')[:issue]).to eq('YT-123')
end
it 'allows lowercase project key on the reference' do
- expect(described_class.reference_pattern.match('yt-123')[:issue]).to eq('yt-123')
+ expect(subject.reference_pattern.match('yt-123')[:issue]).to eq('yt-123')
end
end
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index f0007e1203c..59ade8783e5 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe InternalId do
let(:usage) { :issues }
let(:issue) { build(:issue, project: project) }
let(:id_subject) { issue }
- let(:scope) { { project: project } }
+ let(:scope) { { namespace: project.project_namespace } }
let(:init) { ->(issue, scope) { issue&.project&.issues&.size || Issue.where(**scope).count } }
it_behaves_like 'having unique enum values'
@@ -17,7 +17,7 @@ RSpec.describe InternalId do
end
describe '.flush_records!' do
- subject { described_class.flush_records!(project: project) }
+ subject { described_class.flush_records!(namespace: project.project_namespace) }
let(:another_project) { create(:project) }
@@ -27,11 +27,11 @@ RSpec.describe InternalId do
end
it 'deletes all records for the given project' do
- expect { subject }.to change { described_class.where(project: project).count }.from(1).to(0)
+ expect { subject }.to change { described_class.where(namespace: project.project_namespace).count }.from(1).to(0)
end
it 'retains records for other projects' do
- expect { subject }.not_to change { described_class.where(project: another_project).count }
+ expect { subject }.not_to change { described_class.where(namespace: another_project.project_namespace).count }
end
it 'does not allow an empty filter' do
@@ -51,7 +51,7 @@ RSpec.describe InternalId do
subject
described_class.first.tap do |record|
- expect(record.project).to eq(project)
+ expect(record.namespace).to eq(project.project_namespace)
expect(record.usage).to eq(usage.to_s)
end
end
@@ -182,7 +182,7 @@ RSpec.describe InternalId do
subject
described_class.first.tap do |record|
- expect(record.project).to eq(project)
+ expect(record.namespace).to eq(project.project_namespace)
expect(record.usage).to eq(usage.to_s)
expect(record.last_value).to eq(value)
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index e29318a7e83..6ae33fe2642 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -12,7 +12,6 @@ RSpec.describe Issue, feature_category: :team_planning do
describe "Associations" do
it { is_expected.to belong_to(:milestone) }
- it { is_expected.to belong_to(:iteration) }
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:work_item_type).class_name('WorkItems::Type') }
it { is_expected.to belong_to(:moved_to).class_name('Issue') }
@@ -38,6 +37,7 @@ RSpec.describe Issue, feature_category: :team_planning do
it { is_expected.to have_many(:issue_customer_relations_contacts) }
it { is_expected.to have_many(:customer_relations_contacts) }
it { is_expected.to have_many(:incident_management_timeline_events) }
+ it { is_expected.to have_many(:assignment_events).class_name('ResourceEvents::IssueAssignmentEvent').inverse_of(:issue) }
describe 'versions.most_recent' do
it 'returns the most recent version' do
@@ -63,13 +63,18 @@ RSpec.describe Issue, feature_category: :team_planning do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:issue) }
- let(:scope) { :project }
- let(:scope_attrs) { { project: instance.project } }
+ let(:scope) { :namespace }
+ let(:scope_attrs) { { namespace: instance.project.project_namespace } }
let(:usage) { :issues }
end
end
describe 'validations' do
+ it { is_expected.not_to allow_value(nil).for(:confidential) }
+ it { is_expected.to allow_value(true, false).for(:confidential) }
+ end
+
+ describe 'custom validations' do
subject(:valid?) { issue.valid? }
describe 'due_date_after_start_date' do
@@ -156,7 +161,7 @@ RSpec.describe Issue, feature_category: :team_planning do
it 'is possible to change type only between selected types' do
issue = create(:issue, old_type, project: reusable_project)
- issue.work_item_type_id = WorkItems::Type.default_by_type(new_type).id
+ issue.assign_attributes(work_item_type: WorkItems::Type.default_by_type(new_type), issue_type: new_type)
expect(issue.valid?).to eq(is_valid)
end
@@ -215,7 +220,7 @@ RSpec.describe Issue, feature_category: :team_planning do
subject { create(:issue, project: reusable_project) }
describe 'callbacks' do
- describe '#ensure_metrics' do
+ describe '#ensure_metrics!' do
it 'creates metrics after saving' do
expect(subject.metrics).to be_persisted
expect(Issue::Metrics.count).to eq(1)
@@ -250,7 +255,7 @@ RSpec.describe Issue, feature_category: :team_planning do
describe '#ensure_work_item_type' do
let_it_be(:issue_type) { create(:work_item_type, :issue, :default) }
- let_it_be(:task_type) { create(:work_item_type, :issue, :default) }
+ let_it_be(:incident_type) { create(:work_item_type, :incident, :default) }
let_it_be(:project) { create(:project) }
context 'when a type was already set' do
@@ -267,9 +272,9 @@ RSpec.describe Issue, feature_category: :team_planning do
expect(issue.work_item_type_id).to eq(issue_type.id)
expect(WorkItems::Type).not_to receive(:default_by_type)
- issue.update!(work_item_type: task_type, issue_type: 'task')
+ issue.update!(work_item_type: incident_type, issue_type: :incident)
- expect(issue.work_item_type_id).to eq(task_type.id)
+ expect(issue.work_item_type_id).to eq(incident_type.id)
end
it 'ensures a work item type if updated to nil' do
@@ -296,13 +301,36 @@ RSpec.describe Issue, feature_category: :team_planning do
expect(issue.work_item_type_id).to be_nil
expect(WorkItems::Type).not_to receive(:default_by_type)
- issue.update!(work_item_type: task_type, issue_type: 'task')
+ issue.update!(work_item_type: incident_type, issue_type: :incident)
- expect(issue.work_item_type_id).to eq(task_type.id)
+ expect(issue.work_item_type_id).to eq(incident_type.id)
end
end
end
+ describe '#check_issue_type_in_sync' do
+ it 'raises an error if issue_type is out of sync' do
+ issue = build(:issue, issue_type: :issue, work_item_type: WorkItems::Type.default_by_type(:task))
+
+ expect do
+ issue.save!
+ end.to raise_error(Issue::IssueTypeOutOfSyncError)
+ end
+
+ it 'uses attributes to compare both issue_type values' do
+ issue_type = WorkItems::Type.default_by_type(:issue)
+ issue = build(:issue, issue_type: :issue, work_item_type: issue_type)
+
+ attributes = double(:attributes)
+ allow(issue).to receive(:attributes).and_return(attributes)
+
+ expect(attributes).to receive(:[]).with('issue_type').twice.and_return('issue')
+ expect(issue_type).to receive(:base_type).and_call_original
+
+ issue.save!
+ end
+ end
+
describe '#record_create_action' do
it 'records the creation action after saving' do
expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_created_action)
@@ -575,47 +603,47 @@ RSpec.describe Issue, feature_category: :team_planning do
end
describe '#to_reference' do
- let(:namespace) { build(:namespace, path: 'sample-namespace') }
- let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
- let(:issue) { build(:issue, iid: 1, project: project) }
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be(:project) { create(:project, namespace: namespace) }
+ let_it_be(:issue) { create(:issue, project: project) }
context 'when nil argument' do
it 'returns issue id' do
- expect(issue.to_reference).to eq "#1"
+ expect(issue.to_reference).to eq "##{issue.iid}"
end
it 'returns complete path to the issue with full: true' do
- expect(issue.to_reference(full: true)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(full: true)).to eq "#{project.full_path}##{issue.iid}"
end
end
context 'when argument is a project' do
context 'when same project' do
it 'returns issue id' do
- expect(issue.to_reference(project)).to eq("#1")
+ expect(issue.to_reference(project)).to eq("##{issue.iid}")
end
it 'returns full reference with full: true' do
- expect(issue.to_reference(project, full: true)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(project, full: true)).to eq "#{project.full_path}##{issue.iid}"
end
end
context 'when cross-project in same namespace' do
let(:another_project) do
- build(:project, name: 'another-project', namespace: project.namespace)
+ create(:project, namespace: project.namespace)
end
it 'returns a cross-project reference' do
- expect(issue.to_reference(another_project)).to eq "sample-project#1"
+ expect(issue.to_reference(another_project)).to eq "#{project.path}##{issue.iid}"
end
end
context 'when cross-project in different namespace' do
let(:another_namespace) { build(:namespace, id: non_existing_record_id, path: 'another-namespace') }
- let(:another_namespace_project) { build(:project, path: 'another-project', namespace: another_namespace) }
+ let(:another_namespace_project) { build(:project, namespace: another_namespace) }
it 'returns complete path to the issue' do
- expect(issue.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(another_namespace_project)).to eq "#{project.full_path}##{issue.iid}"
end
end
end
@@ -623,11 +651,11 @@ RSpec.describe Issue, feature_category: :team_planning do
context 'when argument is a namespace' do
context 'when same as issue' do
it 'returns path to the issue with the project name' do
- expect(issue.to_reference(namespace)).to eq 'sample-project#1'
+ expect(issue.to_reference(namespace)).to eq "#{project.path}##{issue.iid}"
end
it 'returns full reference with full: true' do
- expect(issue.to_reference(namespace, full: true)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(namespace, full: true)).to eq "#{project.full_path}##{issue.iid}"
end
end
@@ -635,12 +663,111 @@ RSpec.describe Issue, feature_category: :team_planning do
let(:group) { build(:group, name: 'Group', path: 'sample-group') }
it 'returns full path to the issue with full: true' do
- expect(issue.to_reference(group)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(group)).to eq "#{project.full_path}##{issue.iid}"
end
end
end
end
+ describe '#to_reference with table syntax' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_namespace) { user.namespace }
+
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent) }
+ let_it_be(:another_group) { create(:group) }
+
+ let_it_be(:project) { create(:project, namespace: group) }
+ let_it_be(:project_namespace) { project.project_namespace }
+ let_it_be(:same_namespace_project) { create(:project, namespace: group) }
+ let_it_be(:same_namespace_project_namespace) { same_namespace_project.project_namespace }
+
+ let_it_be(:another_namespace_project) { create(:project) }
+ let_it_be(:another_namespace_project_namespace) { another_namespace_project.project_namespace }
+
+ let_it_be(:project_issue) { build(:issue, project: project, iid: 123) }
+ let_it_be(:project_issue_full_reference) { "#{project.full_path}##{project_issue.iid}" }
+
+ let_it_be(:group_issue) { build(:issue, namespace: group, iid: 123) }
+ let_it_be(:group_issue_full_reference) { "#{group.full_path}##{group_issue.iid}" }
+
+ # this one is just theoretically possible, not smth to be supported for real
+ let_it_be(:user_issue) { build(:issue, namespace: user_namespace, iid: 123) }
+ let_it_be(:user_issue_full_reference) { "#{user_namespace.full_path}##{user_issue.iid}" }
+
+ # namespace would be group, project namespace or user namespace
+ where(:issue, :full, :from, :result) do
+ ref(:project_issue) | false | nil | lazy { "##{issue.iid}" }
+ ref(:project_issue) | true | nil | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:group) | lazy { "#{project.path}##{issue.iid}" }
+ ref(:project_issue) | true | ref(:group) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:parent) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:parent) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:project) | lazy { "##{issue.iid}" }
+ ref(:project_issue) | true | ref(:project) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:project_namespace) | lazy { "##{issue.iid}" }
+ ref(:project_issue) | true | ref(:project_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:same_namespace_project) | lazy { "#{project.path}##{issue.iid}" }
+ ref(:project_issue) | true | ref(:same_namespace_project) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:same_namespace_project_namespace) | lazy { "#{project.path}##{issue.iid}" }
+ ref(:project_issue) | true | ref(:same_namespace_project_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:another_group) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:another_group) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:another_namespace_project) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:another_namespace_project) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:another_namespace_project_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:another_namespace_project_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:user_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:user_namespace) | ref(:project_issue_full_reference)
+
+ ref(:group_issue) | false | nil | lazy { "##{issue.iid}" }
+ ref(:group_issue) | true | nil | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:user_namespace) | ref(:group_issue_full_reference)
+ ref(:group_issue) | true | ref(:user_namespace) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:group) | lazy { "##{issue.iid}" }
+ ref(:group_issue) | true | ref(:group) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:parent) | lazy { "#{group.path}##{issue.iid}" }
+ ref(:group_issue) | true | ref(:parent) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:project) | lazy { "#{group.path}##{issue.iid}" }
+ ref(:group_issue) | true | ref(:project) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:project_namespace) | lazy { "#{group.path}##{issue.iid}" }
+ ref(:group_issue) | true | ref(:project_namespace) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:another_group) | ref(:group_issue_full_reference)
+ ref(:group_issue) | true | ref(:another_group) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:another_namespace_project) | ref(:group_issue_full_reference)
+ ref(:group_issue) | true | ref(:another_namespace_project) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:another_namespace_project_namespace) | ref(:group_issue_full_reference)
+ ref(:group_issue) | true | ref(:another_namespace_project_namespace) | ref(:group_issue_full_reference)
+
+ ref(:user_issue) | false | nil | lazy { "##{issue.iid}" }
+ ref(:user_issue) | true | nil | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:user_namespace) | lazy { "##{issue.iid}" }
+ ref(:user_issue) | true | ref(:user_namespace) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:group) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:group) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:parent) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:parent) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:project) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:project) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:project_namespace) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:project_namespace) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:another_group) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:another_group) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:another_namespace_project) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:another_namespace_project) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:another_namespace_project_namespace) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:another_namespace_project_namespace) | ref(:user_issue_full_reference)
+ end
+
+ with_them do
+ it 'returns correct reference' do
+ expect(issue.to_reference(from, full: full)).to eq(result)
+ end
+ end
+ end
+
describe '#assignee_or_author?' do
let(:issue) { create(:issue, project: reusable_project) }
@@ -1637,7 +1764,8 @@ RSpec.describe Issue, feature_category: :team_planning do
end
context 'when project in user namespace' do
- let(:project) { build_stubbed(:project_empty_repo) }
+ let(:project_namespace) { build_stubbed(:project_namespace) }
+ let(:project) { build_stubbed(:project_empty_repo, project_namespace: project_namespace) }
let(:project_id) { project.id }
let(:namespace_id) { nil }
@@ -1646,7 +1774,8 @@ RSpec.describe Issue, feature_category: :team_planning do
context 'when project in a group namespace' do
let(:group) { create(:group) }
- let(:project) { build_stubbed(:project_empty_repo, group: group) }
+ let(:project_namespace) { build_stubbed(:project_namespace) }
+ let(:project) { build_stubbed(:project_empty_repo, group: group, project_namespace: project_namespace) }
let(:project_id) { nil }
let(:namespace_id) { group.id }
@@ -1662,6 +1791,46 @@ RSpec.describe Issue, feature_category: :team_planning do
end
end
+ describe '#issue_type' do
+ let_it_be(:issue) { create(:issue) }
+
+ context 'when the issue_type_uses_work_item_types_table feature flag is enabled' do
+ it 'gets the type field from the work_item_types table' do
+ expect(issue).to receive_message_chain(:work_item_type, :base_type)
+
+ issue.issue_type
+ end
+
+ context 'when the issue is not persisted' do
+ it 'uses the default work item type' do
+ non_persisted_issue = build(:issue, work_item_type: nil)
+
+ expect(non_persisted_issue.issue_type).to eq(described_class::DEFAULT_ISSUE_TYPE.to_s)
+ end
+ end
+ end
+
+ context 'when the issue_type_uses_work_item_types_table feature flag is disabled' do
+ before do
+ stub_feature_flags(issue_type_uses_work_item_types_table: false)
+ end
+
+ it 'does not get the value from the work_item_types table' do
+ expect(issue).not_to receive(:work_item_type)
+
+ issue.issue_type
+ end
+
+ context 'when the issue is not persisted' do
+ it 'uses the default work item type' do
+ non_persisted_issue = build(:issue, work_item_type: nil)
+
+ expect(non_persisted_issue.issue_type).to eq(described_class::DEFAULT_ISSUE_TYPE.to_s)
+ end
+ end
+ end
+ end
+
describe '#issue_type_supports?' do
let_it_be(:issue) { create(:issue) }
@@ -1670,6 +1839,17 @@ RSpec.describe Issue, feature_category: :team_planning do
end
end
+ describe '#supports_assignee?' do
+ Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter::WIDGETS_FOR_TYPE.each_pair do |base_type, widgets|
+ specify do
+ issue = build(:issue, base_type)
+ supports_assignee = widgets.include?(:assignees)
+
+ expect(issue.supports_assignee?).to eq(supports_assignee)
+ end
+ end
+ end
+
describe '#supports_time_tracking?' do
let_it_be(:project) { create(:project) }
let_it_be_with_refind(:issue) { create(:incident, project: project) }
@@ -1681,10 +1861,10 @@ RSpec.describe Issue, feature_category: :team_planning do
with_them do
before do
- issue.update!(issue_type: issue_type)
+ issue.update!(issue_type: issue_type, work_item_type: WorkItems::Type.default_by_type(issue_type))
end
- it do
+ specify do
expect(issue.supports_time_tracking?).to eq(supports_time_tracking)
end
end
@@ -1701,10 +1881,10 @@ RSpec.describe Issue, feature_category: :team_planning do
with_them do
before do
- issue.update!(issue_type: issue_type)
+ issue.update!(issue_type: issue_type, work_item_type: WorkItems::Type.default_by_type(issue_type))
end
- it do
+ specify do
expect(issue.supports_move_and_clone?).to eq(supports_move_and_clone)
end
end
@@ -1815,4 +1995,24 @@ RSpec.describe Issue, feature_category: :team_planning do
end
end
end
+
+ describe '#work_item_type_with_default' do
+ subject { Issue.new.work_item_type_with_default }
+
+ it { is_expected.to eq(WorkItems::Type.default_by_type(::Issue::DEFAULT_ISSUE_TYPE)) }
+ end
+
+ describe 'issue_type enum generated methods' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:issue) { create(:issue, project: reusable_project) }
+
+ where(issue_type: WorkItems::Type.base_types.keys)
+
+ with_them do
+ it 'raises an error if called' do
+ expect { issue.public_send("#{issue_type}?".to_sym) }.to raise_error(Issue::ForbiddenColumnUsed)
+ end
+ end
+ end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index f1bc7b41cee..7a46e5e7e53 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -165,7 +165,7 @@ RSpec.describe Key, :mailer do
.with(key)
.and_return(service)
- expect(service).to receive(:execute)
+ expect(service).to receive(:execute_async)
key.update_last_used_at
end
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index ff7ac0ebd2a..26a1edcbcff 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -257,4 +257,15 @@ RSpec.describe Label do
end
end
end
+
+ describe '.pluck_titles' do
+ subject(:pluck_titles) { described_class.pluck_titles }
+
+ it 'returns the audit event type of the event type filter' do
+ label1 = create(:label, title: "TITLE1")
+ label2 = create(:label, title: "TITLE2")
+
+ expect(pluck_titles).to contain_exactly(label1.title, label2.title)
+ end
+ end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 6a52f12553f..eea96e5e4ae 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -16,7 +16,6 @@ RSpec.describe Member, feature_category: :subgroups do
describe 'Associations' do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:member_namespace) }
- it { is_expected.to belong_to(:member_role) }
it { is_expected.to have_one(:member_task) }
end
@@ -173,96 +172,6 @@ RSpec.describe Member, feature_category: :subgroups do
end
end
end
-
- context 'member role access level' do
- let_it_be_with_reload(:member) { create(:group_member, access_level: Gitlab::Access::DEVELOPER) }
-
- context 'when no member role is associated' do
- it 'is valid' do
- expect(member).to be_valid
- end
- end
-
- context 'when member role is associated' do
- let!(:member_role) do
- create(
- :member_role,
- members: [member],
- base_access_level: Gitlab::Access::DEVELOPER,
- namespace: member.member_namespace
- )
- end
-
- context 'when member role matches access level' do
- it 'is valid' do
- expect(member).to be_valid
- end
- end
-
- context 'when member role does not match access level' do
- it 'is invalid' do
- member_role.base_access_level = Gitlab::Access::MAINTAINER
-
- expect(member).not_to be_valid
- end
- end
-
- context 'when access_level is changed' do
- it 'is invalid' do
- member.access_level = Gitlab::Access::MAINTAINER
-
- expect(member).not_to be_valid
- expect(member.errors[:access_level]).to include(
- _("cannot be changed since member is associated with a custom role")
- )
- end
- end
- end
- end
-
- context 'member role namespace' do
- let_it_be_with_reload(:member) { create(:group_member) }
-
- context 'when no member role is associated' do
- it 'is valid' do
- expect(member).to be_valid
- end
- end
-
- context 'when member role is associated' do
- let_it_be(:member_role) do
- create(:member_role, members: [member], namespace: member.group, base_access_level: member.access_level)
- end
-
- context 'when member#member_namespace is a group within hierarchy of member_role#namespace' do
- it 'is valid' do
- member.member_namespace = create(:group, parent: member_role.namespace)
-
- expect(member).to be_valid
- end
- end
-
- context 'when member#member_namespace is a project within hierarchy of member_role#namespace' do
- it 'is valid' do
- project = create(:project, group: member_role.namespace)
- member.member_namespace = Namespace.find(project.parent_id)
-
- expect(member).to be_valid
- end
- end
-
- context 'when member#member_namespace is outside hierarchy of member_role#namespace' do
- it 'is invalid' do
- member.member_namespace = create(:group)
-
- expect(member).not_to be_valid
- expect(member.errors[:member_namespace]).to include(
- _("must be in same hierarchy as custom role's namespace")
- )
- end
- end
- end
- end
end
describe 'Scopes & finders' do
@@ -774,6 +683,24 @@ RSpec.describe Member, feature_category: :subgroups do
end
end
+ describe '.filter_by_user_type' do
+ let_it_be(:service_account) { create(:user, :service_account) }
+ let_it_be(:service_account_member) { create(:group_member, user: service_account) }
+ let_it_be(:other_member) { create(:group_member) }
+
+ context 'when the user type is valid' do
+ it 'returns service accounts' do
+ expect(described_class.filter_by_user_type('service_account')).to match_array([service_account_member])
+ end
+ end
+
+ context 'when the user type is invalid' do
+ it 'returns nil' do
+ expect(described_class.filter_by_user_type('invalid_type')).to eq(nil)
+ end
+ end
+ end
+
describe '#accept_request' do
let(:member) { create(:project_member, requested_at: Time.current.utc) }
@@ -971,6 +898,14 @@ RSpec.describe Member, feature_category: :subgroups do
end
end
+ describe '.pluck_user_ids' do
+ let(:member) { create(:group_member) }
+
+ it 'plucks the user ids' do
+ expect(described_class.where(id: member).pluck_user_ids).to match([member.user_id])
+ end
+ end
+
describe '#send_invitation_reminder' do
subject { member.send_invitation_reminder(0) }
diff --git a/spec/models/members/member_role_spec.rb b/spec/models/members/member_role_spec.rb
deleted file mode 100644
index 4bf33eb1fce..00000000000
--- a/spec/models/members/member_role_spec.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MemberRole, feature_category: :authentication_and_authorization do
- describe 'associations' do
- it { is_expected.to belong_to(:namespace) }
- it { is_expected.to have_many(:members) }
- end
-
- describe 'validation' do
- subject(:member_role) { build(:member_role) }
-
- it { is_expected.to validate_presence_of(:namespace) }
- it { is_expected.to validate_presence_of(:base_access_level) }
-
- context 'for attributes_locked_after_member_associated' do
- context 'when assigned to member' do
- it 'cannot be changed' do
- member_role.save!
- member_role.members << create(:project_member)
-
- expect(member_role).not_to be_valid
- expect(member_role.errors.messages[:base]).to include(
- s_("MemberRole|cannot be changed because it is already assigned to a user. "\
- "Please create a new Member Role instead")
- )
- end
- end
-
- context 'when not assigned to member' do
- it 'can be changed' do
- expect(member_role).to be_valid
- end
- end
- end
-
- context 'when for namespace' do
- let_it_be(:root_group) { create(:group) }
-
- context 'when namespace is a subgroup' do
- it 'is invalid' do
- subgroup = create(:group, parent: root_group)
- member_role.namespace = subgroup
-
- expect(member_role).to be_invalid
- expect(member_role.errors[:namespace]).to include(
- s_("MemberRole|must be top-level namespace")
- )
- end
- end
-
- context 'when namespace is a root group' do
- it 'is valid' do
- member_role.namespace = root_group
-
- expect(member_role).to be_valid
- end
- end
-
- context 'when namespace is not present' do
- it 'is invalid with a different error message' do
- member_role.namespace = nil
-
- expect(member_role).to be_invalid
- expect(member_role.errors[:namespace]).to include(_("can't be blank"))
- end
- end
-
- context 'when namespace is outside hierarchy of member' do
- it 'creates a validation error' do
- member_role.save!
- member_role.namespace = create(:group)
-
- expect(member_role).not_to be_valid
- expect(member_role.errors[:namespace]).to include(s_("MemberRole|can't be changed"))
- end
- end
- end
- end
-
- describe 'callbacks' do
- context 'for preventing deletion after member is associated' do
- let_it_be(:member_role) { create(:member_role) }
-
- subject(:destroy_member_role) { member_role.destroy } # rubocop: disable Rails/SaveBang
-
- it 'allows deletion without any member associated' do
- expect(destroy_member_role).to be_truthy
- end
-
- it 'prevent deletion when member is associated' do
- create(:group_member, { group: member_role.namespace,
- access_level: Gitlab::Access::DEVELOPER,
- member_role: member_role })
- member_role.members.reload
-
- expect(destroy_member_role).to be_falsey
- expect(member_role.errors.messages[:base])
- .to(
- include(s_("MemberRole|cannot be deleted because it is already assigned to a user. "\
- "Please disassociate the member role from all users before deletion."))
- )
- end
- end
- end
-end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index f0069b89494..20dae056646 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe ProjectMember do
describe 'validations' do
it { is_expected.to allow_value('Project').for(:source_type) }
- it { is_expected.not_to allow_value('project').for(:source_type) }
+ it { is_expected.not_to allow_value('Group').for(:source_type) }
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
end
@@ -136,24 +136,6 @@ RSpec.describe ProjectMember do
end
end
- describe '.add_members_to_projects' do
- it 'adds the given users to the given projects' do
- projects = create_list(:project, 2)
- users = create_list(:user, 2)
-
- described_class.add_members_to_projects(
- [projects.first.id, projects.second.id],
- [users.first.id, users.second],
- described_class::MAINTAINER)
-
- expect(projects.first.users).to include(users.first)
- expect(projects.first.users).to include(users.second)
-
- expect(projects.second.users).to include(users.first)
- expect(projects.second.users).to include(users.second)
- end
- end
-
describe '.truncate_teams' do
before do
@project_1 = create(:project)
diff --git a/spec/models/merge_request/diff_llm_summary_spec.rb b/spec/models/merge_request/diff_llm_summary_spec.rb
new file mode 100644
index 00000000000..a94adae9fa5
--- /dev/null
+++ b/spec/models/merge_request/diff_llm_summary_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::MergeRequest::DiffLlmSummary, feature_category: :code_review_workflow do
+ let_it_be_with_reload(:project) { create(:project, :repository) }
+
+ subject(:merge_request_diff_llm_summary) { build(:merge_request_diff_llm_summary) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:merge_request_diff) }
+ it { is_expected.to belong_to(:user).optional }
+ it { is_expected.to validate_presence_of(:content) }
+ it { is_expected.to validate_length_of(:content).is_at_most(2056) }
+ it { is_expected.to validate_presence_of(:provider) }
+ end
+end
diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb
index 8d1d503b323..b1c2a9b1111 100644
--- a/spec/models/merge_request/metrics_spec.rb
+++ b/spec/models/merge_request/metrics_spec.rb
@@ -93,4 +93,12 @@ RSpec.describe MergeRequest::Metrics do
end
end
end
+
+ it_behaves_like 'database events tracking batch 2' do
+ let(:merge_request) { create(:merge_request) }
+
+ let(:record) { merge_request.metrics }
+ let(:namespace) { nil }
+ let(:update_params) { { pipeline_id: 1, updated_at: Date.tomorrow } }
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index a3c0c9a0a74..a5e68829c5d 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -20,12 +20,16 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
it { is_expected.to belong_to(:target_project).class_name('Project') }
it { is_expected.to belong_to(:source_project).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
+
+ it do
+ is_expected.to belong_to(:head_pipeline).class_name('Ci::Pipeline').inverse_of(:merge_requests_as_head_pipeline)
+ end
+
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) }
- it { is_expected.to belong_to(:iteration) }
it { is_expected.to have_many(:resource_milestone_events) }
it { is_expected.to have_many(:resource_state_events) }
it { is_expected.to have_many(:draft_notes) }
@@ -33,6 +37,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
it { is_expected.to have_many(:reviewed_by_users).through(:reviews).source(:author) }
it { is_expected.to have_one(:cleanup_schedule).inverse_of(:merge_request) }
it { is_expected.to have_many(:created_environments).class_name('Environment').inverse_of(:merge_request) }
+ it { is_expected.to have_many(:assignment_events).class_name('ResourceEvents::MergeRequestAssignmentEvent').inverse_of(:merge_request) }
context 'for forks' do
let!(:project) { create(:project) }
@@ -393,8 +398,8 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
instance1 = MergeRequest.find(merge_request.id)
instance2 = MergeRequest.find(merge_request.id)
- instance1.ensure_metrics
- instance2.ensure_metrics
+ instance1.ensure_metrics!
+ instance2.ensure_metrics!
metrics_records = MergeRequest::Metrics.where(merge_request_id: merge_request.id)
expect(metrics_records.size).to eq(1)
@@ -760,6 +765,23 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
+ context 'when scoped with :merged_before and :merged_after' do
+ before do
+ mr2.metrics.update!(merged_at: mr1.metrics.merged_at - 1.week)
+ mr3.metrics.update!(merged_at: mr1.metrics.merged_at + 1.week)
+ end
+
+ it 'excludes merge requests outside of the date range' do
+ expect(
+ project.merge_requests.merge(
+ MergeRequest::Metrics
+ .merged_before(mr1.metrics.merged_at + 1.day)
+ .merged_after(mr1.metrics.merged_at - 1.day)
+ ).total_time_to_merge
+ ).to be_within(1).of(expected_total_time([mr1]))
+ end
+ end
+
def expected_total_time(mrs)
mrs = mrs.reject { |mr| mr.merged_at.nil? }
mrs.reduce(0.0) do |sum, mr|
@@ -1027,7 +1049,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
describe '#to_reference' do
- let(:project) { build(:project, name: 'sample-project') }
+ let(:project) { build(:project) }
let(:merge_request) { build(:merge_request, target_project: project, iid: 1) }
it 'returns a String reference to the object' do
@@ -1035,12 +1057,12 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
it 'supports a cross-project reference' do
- another_project = build(:project, name: 'another-project', namespace: project.namespace)
- expect(merge_request.to_reference(another_project)).to eq "sample-project!1"
+ another_project = build(:project, namespace: project.namespace)
+ expect(merge_request.to_reference(another_project)).to eq "#{project.path}!1"
end
it 'returns a String reference with the full path' do
- expect(merge_request.to_reference(full: true)).to eq(project.full_path + '!1')
+ expect(merge_request.to_reference(full: true)).to eq("#{project.full_path}!1")
end
end
@@ -4024,26 +4046,11 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
describe '#use_merge_base_pipeline_for_comparison?' do
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, :with_codequality_reports, source_project: project) }
+ let(:service_class) { Ci::CompareReportsBaseService }
subject { merge_request.use_merge_base_pipeline_for_comparison?(service_class) }
- context 'when service class is Ci::CompareMetricsReportsService' do
- let(:service_class) { 'Ci::CompareMetricsReportsService' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when service class is Ci::CompareCodequalityReportsService' do
- let(:service_class) { 'Ci::CompareCodequalityReportsService' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when service class is different' do
- let(:service_class) { 'Ci::GenerateCoverageReportsService' }
-
- it { is_expected.to be_falsey }
- end
+ it { is_expected.to eq(false) }
end
describe '#comparison_base_pipeline' do
@@ -4051,6 +4058,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, :with_codequality_reports, source_project: project) }
+ let(:service_class) { ::Ci::CompareReportsBaseService }
let!(:base_pipeline) do
create(:ci_pipeline,
:with_test_reports,
@@ -4060,20 +4068,24 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
)
end
- context 'when service class is Ci::CompareCodequalityReportsService' do
- let(:service_class) { 'Ci::CompareCodequalityReportsService' }
+ before do
+ allow(merge_request).to receive(:use_merge_base_pipeline_for_comparison?)
+ .with(service_class).and_return(uses_merge_base)
+ end
+
+ context 'when service class uses merge base pipeline' do
+ let(:uses_merge_base) { true }
context 'when merge request has a merge request pipeline' do
let(:merge_request) do
- create(:merge_request, :with_merge_request_pipeline)
+ create(:merge_request, :with_merge_request_pipeline, source_project: project)
end
- let(:merge_base_pipeline) do
- create(:ci_pipeline, ref: merge_request.target_branch, sha: merge_request.target_branch_sha)
+ let!(:merge_base_pipeline) do
+ create(:ci_pipeline, project: project, ref: merge_request.target_branch, sha: merge_request.target_branch_sha)
end
before do
- merge_base_pipeline
merge_request.update_head_pipeline
end
@@ -4082,19 +4094,27 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
- context 'when merge does not have a merge request pipeline' do
- it 'returns the base_pipeline' do
- expect(pipeline).to eq(base_pipeline)
- end
+ it 'returns the base_pipeline when merge does not have a merge request pipeline' do
+ expect(pipeline).to eq(base_pipeline)
end
end
- context 'when service_class is different' do
- let(:service_class) { 'Ci::GenerateCoverageReportsService' }
+ context 'when service_class does not use merge base pipeline' do
+ let(:uses_merge_base) { false }
it 'returns the base_pipeline' do
expect(pipeline).to eq(base_pipeline)
end
+
+ context 'when merge request has a merge request pipeline' do
+ let(:merge_request) do
+ create(:merge_request, :with_merge_request_pipeline, source_project: project)
+ end
+
+ it 'returns the base pipeline' do
+ expect(pipeline).to eq(base_pipeline)
+ end
+ end
end
end
@@ -4279,8 +4299,11 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
it 'refreshes the number of open merge requests of the target project' do
project = subject.target_project
- expect { subject.destroy! }
- .to change { project.open_merge_requests_count }.from(1).to(0)
+ expect do
+ subject.destroy!
+
+ BatchLoader::Executor.clear_current
+ end.to change { project.open_merge_requests_count }.from(1).to(0)
end
end
@@ -4306,6 +4329,14 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
transition!
end
+ context 'when skip_merge_status_trigger is set to true' do
+ before do
+ subject.skip_merge_status_trigger = true
+ end
+
+ it_behaves_like 'transition not triggering mergeRequestMergeStatusUpdated GraphQL subscription'
+ end
+
context 'when transaction is not committed' do
it_behaves_like 'transition not triggering mergeRequestMergeStatusUpdated GraphQL subscription' do
def transition!
@@ -4459,7 +4490,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
let(:expected_merge_status) { 'checking' }
include_examples 'for a valid state transition'
- it_behaves_like 'transition triggering mergeRequestMergeStatusUpdated GraphQL subscription'
+ it_behaves_like 'transition not triggering mergeRequestMergeStatusUpdated GraphQL subscription'
end
context 'when the status is checking' do
@@ -4479,7 +4510,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
let(:expected_merge_status) { 'cannot_be_merged_rechecking' }
include_examples 'for a valid state transition'
- it_behaves_like 'transition triggering mergeRequestMergeStatusUpdated GraphQL subscription'
+ it_behaves_like 'transition not triggering mergeRequestMergeStatusUpdated GraphQL subscription'
end
context 'when the status is cannot_be_merged' do
@@ -4615,6 +4646,29 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
.from(true)
.to(false)
end
+
+ context 'when it is a first contribution' do
+ let(:new_user) { create(:user) }
+
+ before do
+ subject.update!(author: new_user)
+ end
+
+ it 'sets first_contribution' do
+ subject.mark_as_merged
+
+ expect(subject.state).to eq('merged')
+ expect(subject.reload.first_contribution?).to be_truthy
+ end
+
+ it "doesn't set first_contribution not first contribution" do
+ create(:merged_merge_request, author: new_user)
+
+ subject.mark_as_merged
+
+ expect(subject.first_contribution?).to be_falsey
+ end
+ end
end
describe 'transition to cannot_be_merged' do
@@ -4697,9 +4751,9 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
[:closed, :merged].each do |state|
- let(:state) { state }
-
context state do
+ let(:state) { state }
+
it 'does not notify' do
expect(notification_service).not_to receive(:merge_request_unmergeable)
expect(todo_service).not_to receive(:merge_request_became_unmergeable)
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 9f6b1f8016b..1c43eafb576 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -439,8 +439,8 @@ RSpec.describe Milestone do
describe '#to_reference' do
let(:group) { build_stubbed(:group) }
- let(:project) { build_stubbed(:project, name: 'sample-project') }
- let(:another_project) { build_stubbed(:project, name: 'another-project', namespace: project.namespace) }
+ let(:project) { build_stubbed(:project, path: 'sample-project') }
+ let(:another_project) { build_stubbed(:project, path: 'another-project', namespace: project.namespace) }
context 'for a project milestone' do
let(:milestone) { build_stubbed(:milestone, iid: 1, project: project, name: 'milestone') }
@@ -724,4 +724,12 @@ RSpec.describe Milestone do
end
end
end
+
+ describe '#lock_version' do
+ let_it_be(:milestone) { create(:milestone, project: project) }
+
+ it 'ensures that lock_version and optimistic locking is enabled' do
+ expect(milestone.lock_version).to be_present
+ end
+ end
end
diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb
index 374e49aea01..fa19b723ee2 100644
--- a/spec/models/ml/candidate_spec.rb
+++ b/spec/models/ml/candidate_spec.rb
@@ -3,48 +3,74 @@
require 'spec_helper'
RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops do
- let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params, name: 'candidate0') }
+ let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params, :with_artifact, name: 'candidate0') }
let_it_be(:candidate2) do
create(:ml_candidates, experiment: candidate.experiment, user: create(:user), name: 'candidate2')
end
- let_it_be(:candidate_artifact) do
- FactoryBot.create(:generic_package,
- name: candidate.package_name,
- version: candidate.package_version,
- project: candidate.project)
- end
-
- let(:project) { candidate.experiment.project }
+ let(:project) { candidate.project }
describe 'associations' do
it { is_expected.to belong_to(:experiment) }
+ it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:package) }
+ it { is_expected.to belong_to(:ci_build).class_name('Ci::Build') }
it { is_expected.to have_many(:params) }
it { is_expected.to have_many(:metrics) }
it { is_expected.to have_many(:metadata) }
end
+ describe 'modules' do
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :internal_id }
+ let(:instance) { build(:ml_candidates, experiment: candidate.experiment) }
+ let(:scope) { :project }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { :ml_candidates }
+ end
+ end
+
describe 'default values' do
- it { expect(described_class.new.iid).to be_present }
+ it { expect(described_class.new.eid).to be_present }
end
- describe '.artifact_root' do
- subject { candidate.artifact_root }
+ describe '.destroy' do
+ let_it_be(:candidate_to_destroy) do
+ create(:ml_candidates, :with_metrics_and_params, :with_metadata, :with_artifact)
+ end
- it { is_expected.to eq("/ml_candidate_#{candidate.id}/-/") }
+ it 'destroys metrics, params and metadata, but not the artifact', :aggregate_failures do
+ expect { candidate_to_destroy.destroy! }
+ .to change { Ml::CandidateMetadata.count }.by(-2)
+ .and change { Ml::CandidateParam.count }.by(-2)
+ .and change { Ml::CandidateMetric.count }.by(-2)
+ .and not_change { Packages::Package.count }
+ end
end
- describe '.package_name' do
- subject { candidate.package_name }
+ describe '.artifact_root' do
+ subject { candidate.artifact_root }
- it { is_expected.to eq("ml_candidate_#{candidate.id}") }
+ it { is_expected.to eq("/#{candidate.package_name}/#{candidate.iid}/") }
end
describe '.package_version' do
subject { candidate.package_version }
- it { is_expected.to eq('-') }
+ it { is_expected.to eq(candidate.iid) }
+ end
+
+ describe '.eid' do
+ let_it_be(:eid) { SecureRandom.uuid }
+
+ let_it_be(:candidate3) do
+ build(:ml_candidates, :with_metrics_and_params, name: 'candidate0', eid: eid)
+ end
+
+ subject { candidate3.eid }
+
+ it { is_expected.to eq(eid) }
end
describe '.artifact' do
@@ -52,10 +78,6 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d
subject { tested_candidate.artifact }
- before do
- candidate_artifact
- end
-
context 'when has logged artifacts' do
it 'returns the package' do
expect(subject.name).to eq(tested_candidate.package_name)
@@ -69,21 +91,26 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d
end
end
- describe '.artifact_lazy' do
- context 'when candidates have same the same iid' do
- before do
- BatchLoader::Executor.clear_current
- end
+ describe '#by_project_id_and_eid' do
+ let(:project_id) { candidate.experiment.project_id }
+ let(:eid) { candidate.eid }
- it 'loads the correct artifacts', :aggregate_failures do
- candidate.artifact_lazy
- candidate2.artifact_lazy
+ subject { described_class.with_project_id_and_eid(project_id, eid) }
- expect(Packages::Package).to receive(:joins).once.and_call_original # Only one database call
+ context 'when eid exists', 'and belongs to project' do
+ it { is_expected.to eq(candidate) }
+ end
- expect(candidate.artifact.name).to eq(candidate.package_name)
- expect(candidate2.artifact).to be_nil
- end
+ context 'when eid exists', 'and does not belong to project' do
+ let(:project_id) { non_existing_record_id }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when eid does not exist' do
+ let(:eid) { 'a' }
+
+ it { is_expected.to be_nil }
end
end
@@ -93,18 +120,18 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d
subject { described_class.with_project_id_and_iid(project_id, iid) }
- context 'when iid exists', 'and belongs to project' do
+ context 'when internal_id exists', 'and belongs to project' do
it { is_expected.to eq(candidate) }
end
- context 'when iid exists', 'and does not belong to project' do
+ context 'when internal_id exists', 'and does not belong to project' do
let(:project_id) { non_existing_record_id }
it { is_expected.to be_nil }
end
- context 'when iid does not exist' do
- let(:iid) { 'a' }
+ context 'when internal_id does not exist' do
+ let(:iid) { non_existing_record_id }
it { is_expected.to be_nil }
end
@@ -130,6 +157,9 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d
expect(subject.association_cached?(:latest_metrics)).to be(true)
expect(subject.association_cached?(:params)).to be(true)
expect(subject.association_cached?(:user)).to be(true)
+ expect(subject.association_cached?(:project)).to be(true)
+ expect(subject.association_cached?(:package)).to be(true)
+ expect(subject.association_cached?(:ci_build)).to be(true)
end
end
@@ -161,6 +191,22 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d
end
end
+ describe 'from_ci?' do
+ subject { candidate }
+
+ it 'is false if candidate does not have ci_build_id' do
+ allow(candidate).to receive(:ci_build_id).and_return(nil)
+
+ is_expected.not_to be_from_ci
+ end
+
+ it 'is true if candidate does has ci_build_id' do
+ allow(candidate).to receive(:ci_build_id).and_return(1)
+
+ is_expected.to be_from_ci
+ end
+ end
+
describe '#order_by_metric' do
let_it_be(:auc_metrics) do
create(:ml_candidate_metrics, name: 'auc', value: 0.4, candidate: candidate)
@@ -183,4 +229,11 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d
end
end
end
+
+ context 'with loose foreign key on ml_candidates.ci_build_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:ci_build) }
+ let!(:model) { create(:ml_candidates, ci_build: parent) }
+ end
+ end
end
diff --git a/spec/models/ml/experiment_spec.rb b/spec/models/ml/experiment_spec.rb
index c75331a2ab5..9738a88b5b8 100644
--- a/spec/models/ml/experiment_spec.rb
+++ b/spec/models/ml/experiment_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ml::Experiment do
+RSpec.describe Ml::Experiment, feature_category: :mlops do
let_it_be(:exp) { create(:ml_experiments) }
let_it_be(:exp2) { create(:ml_experiments, project: exp.project) }
@@ -16,6 +16,12 @@ RSpec.describe Ml::Experiment do
it { is_expected.to have_many(:metadata) }
end
+ describe '.package_name' do
+ describe '.package_name' do
+ it { expect(exp.package_name).to eq("ml_experiment_#{exp.iid}") }
+ end
+ end
+
describe '#by_project_id_and_iid' do
subject { described_class.by_project_id_and_iid(exp.project_id, iid) }
@@ -74,4 +80,22 @@ RSpec.describe Ml::Experiment do
expect(subject[exp3.id]).to eq(3)
end
end
+
+ describe '#package_for_experiment?' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.package_for_experiment?(package_name) }
+
+ where(:package_name, :id) do
+ 'ml_experiment_1234' | true
+ 'ml_experiment_1234abc' | false
+ 'ml_experiment_abc' | false
+ 'ml_experiment_' | false
+ 'blah' | false
+ end
+
+ with_them do
+ it { is_expected.to be(id) }
+ end
+ end
end
diff --git a/spec/models/namespace/aggregation_schedule_spec.rb b/spec/models/namespace/aggregation_schedule_spec.rb
index 45b66fa12dd..0289e4a5462 100644
--- a/spec/models/namespace/aggregation_schedule_spec.rb
+++ b/spec/models/namespace/aggregation_schedule_spec.rb
@@ -5,27 +5,33 @@ require 'spec_helper'
RSpec.describe Namespace::AggregationSchedule, :clean_gitlab_redis_shared_state, type: :model do
include ExclusiveLeaseHelpers
- let(:default_timeout) { described_class.default_lease_timeout }
+ let(:namespace) { create(:namespace) }
+ let(:aggregation_schedule) { namespace.build_aggregation_schedule }
+ let(:default_timeout) { aggregation_schedule.default_lease_timeout }
it { is_expected.to belong_to :namespace }
describe "#default_lease_timeout" do
- subject(:default_lease_timeout) { default_timeout }
-
- it { is_expected.to eq 30.minutes.to_i }
+ before do
+ aggregation_schedule.save!
+ end
- context 'when remove_namespace_aggregator_delay FF is disabled' do
- before do
- stub_feature_flags(remove_namespace_aggregator_delay: false)
+ context 'when reduce_aggregation_schedule_lease FF is enabled' do
+ it 'is 2 minutes' do
+ stub_feature_flags(reduce_aggregation_schedule_lease: true)
+ expect(aggregation_schedule.default_lease_timeout).to eq 2.minutes.to_i
end
+ end
- it { is_expected.to eq 1.hour.to_i }
+ context 'when reduce_aggregation_schedule_lease FF is disabled' do
+ it 'is 30 minutes' do
+ stub_feature_flags(reduce_aggregation_schedule_lease: false)
+ expect(aggregation_schedule.default_lease_timeout).to eq 30.minutes.to_i
+ end
end
end
describe '#schedule_root_storage_statistics' do
- let(:namespace) { create(:namespace) }
- let(:aggregation_schedule) { namespace.build_aggregation_schedule }
let(:lease_key) { "namespace:namespaces_root_statistics:#{namespace.id}" }
context "when we can't obtain the lease" do
diff --git a/spec/models/namespace/package_setting_spec.rb b/spec/models/namespace/package_setting_spec.rb
index 2584fa597ad..fca929600a4 100644
--- a/spec/models/namespace/package_setting_spec.rb
+++ b/spec/models/namespace/package_setting_spec.rb
@@ -47,28 +47,30 @@ RSpec.describe Namespace::PackageSetting do
context 'package types with package_settings' do
# As more package types gain settings they will be added to this list
[:maven_package, :generic_package].each do |format|
- let_it_be(:package) { create(format, name: 'foo', version: 'beta') } # rubocop:disable Rails/SaveBang
- let_it_be(:package_type) { package.package_type }
- let_it_be(:package_setting) { package.project.namespace.package_settings }
-
- where(:duplicates_allowed, :duplicate_exception_regex, :result) do
- true | '' | true
- false | '' | false
- false | '.*' | true
- false | 'fo.*' | true
- false | 'be.*' | true
- end
+ context "with package_type:#{format}" do
+ let_it_be(:package) { create(format, name: 'foo', version: 'beta') } # rubocop:disable Rails/SaveBang
+ let_it_be(:package_type) { package.package_type }
+ let_it_be(:package_setting) { package.project.namespace.package_settings }
+
+ where(:duplicates_allowed, :duplicate_exception_regex, :result) do
+ true | '' | true
+ false | '' | false
+ false | '.*' | true
+ false | 'fo.*' | true
+ false | 'be.*' | true
+ end
- with_them do
- context "for #{format}" do
- before do
- package_setting.update!(
- "#{package_type}_duplicates_allowed" => duplicates_allowed,
- "#{package_type}_duplicate_exception_regex" => duplicate_exception_regex
- )
- end
+ with_them do
+ context "for #{format}" do
+ before do
+ package_setting.update!(
+ "#{package_type}_duplicates_allowed" => duplicates_allowed,
+ "#{package_type}_duplicate_exception_regex" => duplicate_exception_regex
+ )
+ end
- it { is_expected.to be(result) }
+ it { is_expected.to be(result) }
+ end
end
end
end
@@ -76,11 +78,13 @@ RSpec.describe Namespace::PackageSetting do
context 'package types without package_settings' do
[:npm_package, :conan_package, :nuget_package, :pypi_package, :composer_package, :golang_package, :debian_package].each do |format|
- let_it_be(:package) { create(format) } # rubocop:disable Rails/SaveBang
- let_it_be(:package_setting) { package.project.namespace.package_settings }
+ context "with package_type:#{format}" do
+ let_it_be(:package) { create(format) } # rubocop:disable Rails/SaveBang
+ let_it_be(:package_setting) { package.project.namespace.package_settings }
- it 'raises an error' do
- expect { subject }.to raise_error(Namespace::PackageSetting::PackageSettingNotImplemented)
+ it 'raises an error' do
+ expect { subject }.to raise_error(Namespace::PackageSetting::PackageSettingNotImplemented)
+ end
end
end
end
diff --git a/spec/models/namespace/root_storage_statistics_spec.rb b/spec/models/namespace/root_storage_statistics_spec.rb
index 14ac08b545a..c2a0c8c8a7c 100644
--- a/spec/models/namespace/root_storage_statistics_spec.rb
+++ b/spec/models/namespace/root_storage_statistics_spec.rb
@@ -22,15 +22,13 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
end
describe '#recalculate!' do
- let(:namespace) { create(:group) }
+ let_it_be(:namespace) { create(:group) }
+
let(:root_storage_statistics) { create(:namespace_root_storage_statistics, namespace: namespace) }
let(:project1) { create(:project, namespace: namespace) }
let(:project2) { create(:project, namespace: namespace) }
- let!(:project_stat1) { create(:project_statistics, project: project1, with_data: true, size_multiplier: 100) }
- let!(:project_stat2) { create(:project_statistics, project: project2, with_data: true, size_multiplier: 200) }
-
shared_examples 'project data refresh' do
it 'aggregates project statistics' do
root_storage_statistics.recalculate!
@@ -96,8 +94,13 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
end
end
- it_behaves_like 'project data refresh'
- it_behaves_like 'does not include personal snippets'
+ context 'with project statistics' do
+ let!(:project_stat1) { create(:project_statistics, project: project1, with_data: true, size_multiplier: 100) }
+ let!(:project_stat2) { create(:project_statistics, project: project2, with_data: true, size_multiplier: 200) }
+
+ it_behaves_like 'project data refresh'
+ it_behaves_like 'does not include personal snippets'
+ end
context 'with subgroups' do
let(:subgroup1) { create(:group, parent: namespace) }
@@ -106,6 +109,9 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
let(:project1) { create(:project, namespace: subgroup1) }
let(:project2) { create(:project, namespace: subgroup2) }
+ let!(:project_stat1) { create(:project_statistics, project: project1, with_data: true, size_multiplier: 100) }
+ let!(:project_stat2) { create(:project_statistics, project: project2, with_data: true, size_multiplier: 200) }
+
it_behaves_like 'project data refresh'
it_behaves_like 'does not include personal snippets'
end
@@ -122,6 +128,9 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
let(:namespace) { root_group }
+ let!(:project_stat1) { create(:project_statistics, project: project1, with_data: true, size_multiplier: 100) }
+ let!(:project_stat2) { create(:project_statistics, project: project2, with_data: true, size_multiplier: 200) }
+
it 'aggregates namespace statistics' do
# This group is not a descendant of the root_group so it shouldn't be included in the final stats.
other_group = create(:group)
@@ -168,6 +177,9 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
let(:namespace) { user.namespace }
+ let!(:project_stat1) { create(:project_statistics, project: project1, with_data: true, size_multiplier: 100) }
+ let!(:project_stat2) { create(:project_statistics, project: project2, with_data: true, size_multiplier: 200) }
+
it_behaves_like 'project data refresh'
it 'does not aggregate namespace statistics' do
@@ -210,5 +222,143 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
end
end
end
+
+ context 'with forks of projects' do
+ it 'aggregates total private forks size' do
+ project = create_project(visibility_level: :private, size_multiplier: 150)
+ project_fork = create_fork(project, size_multiplier: 100)
+
+ root_storage_statistics.recalculate!
+
+ expect(root_storage_statistics.reload.private_forks_storage_size).to eq(project_fork.statistics.storage_size)
+ end
+
+ it 'aggregates total public forks size' do
+ project = create_project(visibility_level: :public, size_multiplier: 250)
+ project_fork = create_fork(project, size_multiplier: 200)
+
+ root_storage_statistics.recalculate!
+
+ expect(root_storage_statistics.reload.public_forks_storage_size).to eq(project_fork.statistics.storage_size)
+ end
+
+ it 'aggregates total internal forks size' do
+ project = create_project(visibility_level: :internal, size_multiplier: 70)
+ project_fork = create_fork(project, size_multiplier: 50)
+
+ root_storage_statistics.recalculate!
+
+ expect(root_storage_statistics.reload.internal_forks_storage_size).to eq(project_fork.statistics.storage_size)
+ end
+
+ it 'aggregates multiple forks' do
+ project = create_project(size_multiplier: 175)
+ fork_a = create_fork(project, size_multiplier: 50)
+ fork_b = create_fork(project, size_multiplier: 60)
+
+ root_storage_statistics.recalculate!
+
+ total_size = fork_a.statistics.storage_size + fork_b.statistics.storage_size
+ expect(root_storage_statistics.reload.private_forks_storage_size).to eq(total_size)
+ end
+
+ it 'aggregates only forks in the namespace' do
+ other_namespace = create(:group)
+ project = create_project(size_multiplier: 175)
+ fork_a = create_fork(project, size_multiplier: 50)
+ create_fork(project, size_multiplier: 50, namespace: other_namespace)
+
+ root_storage_statistics.recalculate!
+
+ expect(root_storage_statistics.reload.private_forks_storage_size).to eq(fork_a.statistics.storage_size)
+ end
+
+ it 'aggregates forks in subgroups' do
+ subgroup = create(:group, parent: namespace)
+ project = create_project(size_multiplier: 100)
+ project_fork = create_fork(project, namespace: subgroup, size_multiplier: 300)
+
+ root_storage_statistics.recalculate!
+
+ expect(root_storage_statistics.reload.private_forks_storage_size).to eq(project_fork.statistics.storage_size)
+ end
+
+ it 'aggregates forks along with total storage size' do
+ project = create_project(size_multiplier: 240)
+ project_fork = create_fork(project, size_multiplier: 100)
+
+ root_storage_statistics.recalculate!
+
+ root_storage_statistics.reload
+ expect(root_storage_statistics.private_forks_storage_size).to eq(project_fork.statistics.storage_size)
+ expect(root_storage_statistics.storage_size).to eq(project.statistics.storage_size + project_fork.statistics.storage_size)
+ end
+
+ it 'sets the public forks storage size back to zero' do
+ root_storage_statistics.update!(public_forks_storage_size: 200)
+
+ root_storage_statistics.recalculate!
+
+ expect(root_storage_statistics.reload.public_forks_storage_size).to eq(0)
+ end
+
+ it 'sets the private forks storage size back to zero' do
+ root_storage_statistics.update!(private_forks_storage_size: 100)
+
+ root_storage_statistics.recalculate!
+
+ expect(root_storage_statistics.reload.private_forks_storage_size).to eq(0)
+ end
+
+ it 'sets the internal forks storage size back to zero' do
+ root_storage_statistics.update!(internal_forks_storage_size: 50)
+
+ root_storage_statistics.recalculate!
+
+ expect(root_storage_statistics.reload.internal_forks_storage_size).to eq(0)
+ end
+
+ context 'when the feature flag is off' do
+ before do
+ stub_feature_flags(root_storage_statistics_calculate_forks: false)
+ end
+
+ it 'does not aggregate fork storage sizes' do
+ project = create_project(size_multiplier: 150)
+ create_fork(project, size_multiplier: 100)
+
+ root_storage_statistics.recalculate!
+
+ expect(root_storage_statistics.reload.private_forks_storage_size).to eq(0)
+ end
+
+ it 'aggregates fork sizes for enabled namespaces' do
+ stub_feature_flags(root_storage_statistics_calculate_forks: namespace)
+ project = create_project(size_multiplier: 150)
+ project_fork = create_fork(project, size_multiplier: 100)
+
+ root_storage_statistics.recalculate!
+
+ expect(root_storage_statistics.reload.private_forks_storage_size).to eq(project_fork.statistics.storage_size)
+ end
+ end
+ end
+ end
+
+ def create_project(size_multiplier:, visibility_level: :private)
+ project = create(:project, visibility_level, namespace: namespace)
+ create(:project_statistics, project: project, with_data: true, size_multiplier: size_multiplier)
+
+ project
+ end
+
+ def create_fork(project, size_multiplier:, namespace: nil)
+ fork_namespace = namespace || project.namespace
+ project_fork = create(:project, namespace: fork_namespace, visibility_level: project.visibility_level)
+ create(:project_statistics, project: project_fork, with_data: true, size_multiplier: size_multiplier)
+ fork_network = project.fork_network || (create(:fork_network, root_project: project) && project.reload.fork_network)
+ create(:fork_network_member, project: project_fork, fork_network: fork_network)
+
+ project_fork
end
end
diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb
index b7cc59b5af3..ba0ce7d6f7f 100644
--- a/spec/models/namespace_setting_spec.rb
+++ b/spec/models/namespace_setting_spec.rb
@@ -15,6 +15,8 @@ RSpec.describe NamespaceSetting, feature_category: :subgroups, type: :model do
it { is_expected.to define_enum_for(:enabled_git_access_protocol).with_values([:all, :ssh, :http]).with_suffix }
describe "validations" do
+ it { is_expected.to validate_inclusion_of(:code_suggestions).in_array([true, false]) }
+
describe "#default_branch_name_content" do
let_it_be(:group) { create(:group) }
@@ -192,7 +194,7 @@ RSpec.describe NamespaceSetting, feature_category: :subgroups, type: :model do
context 'when a group has parent groups' do
let(:grandparent) { create(:group, namespace_settings: settings) }
let(:parent) { create(:group, parent: grandparent) }
- let!(:group) { create(:group, parent: parent) }
+ let!(:group) { create(:group, parent: parent) }
context "when a parent group has disabled diff previews" do
let(:settings) { create(:namespace_settings, show_diff_preview_in_email: false) }
@@ -212,61 +214,87 @@ RSpec.describe NamespaceSetting, feature_category: :subgroups, type: :model do
end
end
- describe '#runner_registration_enabled?' do
- context 'when not a subgroup' do
- let_it_be(:settings) { create(:namespace_settings) }
- let_it_be(:group) { create(:group, namespace_settings: settings) }
+ context 'runner registration settings' do
+ shared_context 'with runner registration settings changing in hierarchy' do
+ context 'when there are no parents' do
+ let_it_be(:group) { create(:group) }
- before do
- group.update!(runner_registration_enabled: runner_registration_enabled)
+ it { is_expected.to be_truthy }
+
+ context 'when no group can register runners' do
+ before do
+ stub_application_setting(valid_runner_registrars: [])
+ end
+
+ it { is_expected.to be_falsey }
+ end
end
- context 'when :runner_registration_enabled is false' do
- let(:runner_registration_enabled) { false }
+ context 'when there are parents' do
+ let_it_be(:grandparent) { create(:group) }
+ let_it_be(:parent) { create(:group, parent: grandparent) }
+ let_it_be(:group) { create(:group, parent: parent) }
- it 'returns false' do
- expect(group.runner_registration_enabled?).to be_falsey
+ before do
+ grandparent.update!(runner_registration_enabled: grandparent_runner_registration_enabled)
end
- it 'does not query the db' do
- expect { group.runner_registration_enabled? }.not_to exceed_query_limit(0)
+ context 'when a parent group has runner registration disabled' do
+ let(:grandparent_runner_registration_enabled) { false }
+
+ it { is_expected.to be_falsey }
end
- end
- context 'when :runner_registration_enabled is true' do
- let(:runner_registration_enabled) { true }
+ context 'when all parent groups have runner registration enabled' do
+ let(:grandparent_runner_registration_enabled) { true }
- it 'returns true' do
- expect(group.runner_registration_enabled?).to be_truthy
+ it { is_expected.to be_truthy }
end
end
end
- context 'when a group has parent groups' do
- let_it_be(:grandparent) { create(:group) }
- let_it_be(:parent) { create(:group, parent: grandparent) }
- let_it_be(:group) { create(:group, parent: parent) }
+ describe '#runner_registration_enabled?' do
+ subject(:group_setting) { group.runner_registration_enabled? }
+
+ let_it_be(:settings) { create(:namespace_settings) }
+ let_it_be(:group) { create(:group, namespace_settings: settings) }
before do
- grandparent.update!(runner_registration_enabled: runner_registration_enabled)
+ group.update!(runner_registration_enabled: group_runner_registration_enabled)
end
- context 'when a parent group has runner registration disabled' do
- let(:runner_registration_enabled) { false }
+ context 'when runner registration is enabled' do
+ let(:group_runner_registration_enabled) { true }
- it 'returns false' do
- expect(group.runner_registration_enabled?).to be_falsey
- end
+ it { is_expected.to be_truthy }
+
+ it_behaves_like 'with runner registration settings changing in hierarchy'
end
- context 'when all parent groups have runner registration enabled' do
- let(:runner_registration_enabled) { true }
+ context 'when runner registration is disabled' do
+ let(:group_runner_registration_enabled) { false }
- it 'returns true' do
- expect(group.runner_registration_enabled?).to be_truthy
+ it { is_expected.to be_falsey }
+
+ it 'does not query the db' do
+ expect { group.runner_registration_enabled? }.not_to exceed_query_limit(0)
+ end
+
+ context 'when group runner registration is disallowed' do
+ before do
+ stub_application_setting(valid_runner_registrars: [])
+ end
+
+ it { is_expected.to be_falsey }
end
end
end
+
+ describe '#all_ancestors_have_runner_registration_enabled?' do
+ subject(:group_setting) { group.all_ancestors_have_runner_registration_enabled? }
+
+ it_behaves_like 'with runner registration settings changing in hierarchy'
+ end
end
describe '#allow_runner_registration_token?' do
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index a0698ac30f5..3ff49938de5 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -31,7 +31,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
it { is_expected.to have_many :pending_builds }
it { is_expected.to have_one :namespace_route }
it { is_expected.to have_many :namespace_members }
- it { is_expected.to have_many :member_roles }
it { is_expected.to have_one :cluster_enabled_grant }
it { is_expected.to have_many(:work_items) }
it { is_expected.to have_many :achievements }
@@ -91,7 +90,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
context 'validating the parent of a namespace' do
using RSpec::Parameterized::TableSyntax
- # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
where(:parent_type, :child_type, :error) do
nil | ref(:user_sti_name) | nil
nil | ref(:group_sti_name) | nil
@@ -106,7 +104,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
ref(:user_sti_name) | ref(:group_sti_name) | 'user namespace cannot be the parent of another namespace'
ref(:user_sti_name) | ref(:project_sti_name) | nil
end
- # rubocop:enable Lint/BinaryOperatorWithIdenticalOperands
with_them do
it 'validates namespace parent' do
@@ -171,19 +168,26 @@ RSpec.describe Namespace, feature_category: :subgroups do
let_it_be(:parent) { create(:namespace) }
- # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
where(:namespace_type, :path, :valid) do
- ref(:project_sti_name) | 'j' | true
- ref(:project_sti_name) | 'path.' | true
- ref(:project_sti_name) | 'blob' | false
- ref(:group_sti_name) | 'j' | false
- ref(:group_sti_name) | 'path.' | false
- ref(:group_sti_name) | 'blob' | true
- ref(:user_sti_name) | 'j' | false
- ref(:user_sti_name) | 'path.' | false
- ref(:user_sti_name) | 'blob' | true
- end
- # rubocop:enable Lint/BinaryOperatorWithIdenticalOperands
+ ref(:project_sti_name) | 'j' | true
+ ref(:project_sti_name) | 'path.' | false
+ ref(:project_sti_name) | '.path' | false
+ ref(:project_sti_name) | 'path.git' | false
+ ref(:project_sti_name) | 'namespace__path' | false
+ ref(:project_sti_name) | 'blob' | false
+ ref(:group_sti_name) | 'j' | false
+ ref(:group_sti_name) | 'path.' | false
+ ref(:group_sti_name) | '.path' | false
+ ref(:group_sti_name) | 'path.git' | false
+ ref(:group_sti_name) | 'namespace__path' | false
+ ref(:group_sti_name) | 'blob' | true
+ ref(:user_sti_name) | 'j' | false
+ ref(:user_sti_name) | 'path.' | false
+ ref(:user_sti_name) | '.path' | false
+ ref(:user_sti_name) | 'path.git' | false
+ ref(:user_sti_name) | 'namespace__path' | false
+ ref(:user_sti_name) | 'blob' | true
+ end
with_them do
it 'validates namespace path' do
@@ -193,6 +197,26 @@ RSpec.describe Namespace, feature_category: :subgroups do
expect(namespace.valid?).to be(valid)
end
end
+
+ context 'when path starts or ends with a special character' do
+ it 'does not raise validation error for path for existing namespaces' do
+ parent.update_attribute(:path, '_path_')
+
+ expect { parent.update!(name: 'Foo') }.not_to raise_error
+ end
+ end
+
+ context 'when restrict_special_characters_in_namespace_path feature flag is disabled' do
+ before do
+ stub_feature_flags(restrict_special_characters_in_namespace_path: false)
+ end
+
+ it 'allows special character at the start or end of project namespace path' do
+ namespace = build(:namespace, type: project_sti_name, parent: parent, path: '_path_')
+
+ expect(namespace).to be_valid
+ end
+ end
end
describe '1 char path length' do
@@ -237,6 +261,117 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
end
+ describe "ReferencePatternValidation" do
+ subject { described_class.reference_pattern }
+
+ it { is_expected.to match("@group1") }
+ it { is_expected.to match("@group1/group2/group3") }
+ it { is_expected.to match("@1234/1234/1234") }
+ it { is_expected.to match("@.q-w_e") }
+ end
+
+ describe '#to_reference_base' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_namespace) { user.namespace }
+
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent) }
+ let_it_be(:another_group) { create(:group) }
+
+ let_it_be(:project) { create(:project, namespace: group) }
+ let_it_be(:project_namespace) { project.project_namespace }
+
+ let_it_be(:another_namespace_project) { create(:project) }
+ let_it_be(:another_namespace_project_namespace) { another_namespace_project.project_namespace }
+
+ # testing references with namespace being: group, project namespace and user namespace
+ where(:namespace, :full, :from, :result) do
+ ref(:parent) | false | nil | nil
+ ref(:parent) | true | nil | lazy { parent.full_path }
+ ref(:parent) | false | ref(:group) | lazy { parent.path }
+ ref(:parent) | true | ref(:group) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:parent) | nil
+ ref(:parent) | true | ref(:parent) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:project) | lazy { parent.path }
+ ref(:parent) | true | ref(:project) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:project_namespace) | lazy { parent.path }
+ ref(:parent) | true | ref(:project_namespace) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:another_group) | lazy { parent.full_path }
+ ref(:parent) | true | ref(:another_group) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:another_namespace_project) | lazy { parent.full_path }
+ ref(:parent) | true | ref(:another_namespace_project) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:another_namespace_project_namespace) | lazy { parent.full_path }
+ ref(:parent) | true | ref(:another_namespace_project_namespace) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:user_namespace) | lazy { parent.full_path }
+ ref(:parent) | true | ref(:user_namespace) | lazy { parent.full_path }
+
+ ref(:group) | false | nil | nil
+ ref(:group) | true | nil | lazy { group.full_path }
+ ref(:group) | false | ref(:group) | nil
+ ref(:group) | true | ref(:group) | lazy { group.full_path }
+ ref(:group) | false | ref(:parent) | lazy { group.path }
+ ref(:group) | true | ref(:parent) | lazy { group.full_path }
+ ref(:group) | false | ref(:project) | lazy { group.path }
+ ref(:group) | true | ref(:project) | lazy { group.full_path }
+ ref(:group) | false | ref(:project_namespace) | lazy { group.path }
+ ref(:group) | true | ref(:project_namespace) | lazy { group.full_path }
+ ref(:group) | false | ref(:another_group) | lazy { group.full_path }
+ ref(:group) | true | ref(:another_group) | lazy { group.full_path }
+ ref(:group) | false | ref(:another_namespace_project) | lazy { group.full_path }
+ ref(:group) | true | ref(:another_namespace_project) | lazy { group.full_path }
+ ref(:group) | false | ref(:another_namespace_project_namespace) | lazy { group.full_path }
+ ref(:group) | true | ref(:another_namespace_project_namespace) | lazy { group.full_path }
+ ref(:group) | false | ref(:user_namespace) | lazy { group.full_path }
+ ref(:group) | true | ref(:user_namespace) | lazy { group.full_path }
+
+ ref(:project_namespace) | false | nil | nil
+ ref(:project_namespace) | true | nil | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:group) | lazy { project_namespace.path }
+ ref(:project_namespace) | true | ref(:group) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:parent) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:parent) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:project) | nil
+ ref(:project_namespace) | true | ref(:project) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:project_namespace) | nil
+ ref(:project_namespace) | true | ref(:project_namespace) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:another_group) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:another_group) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:another_namespace_project) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:another_namespace_project) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:another_namespace_project_namespace) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:another_namespace_project_namespace) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:user_namespace) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:user_namespace) | lazy { project_namespace.full_path }
+
+ ref(:user_namespace) | false | nil | nil
+ ref(:user_namespace) | true | nil | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:user_namespace) | nil
+ ref(:user_namespace) | true | ref(:user_namespace) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:group) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:group) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:parent) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:parent) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:project) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:project) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:project_namespace) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:project_namespace) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:another_group) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:another_group) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:another_namespace_project) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:another_namespace_project) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:another_namespace_project_namespace) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:another_namespace_project_namespace) | lazy { user_namespace.full_path }
+ end
+
+ with_them do
+ it 'returns correct path' do
+ expect(namespace.to_reference_base(from, full: full)).to eq(result)
+ end
+ end
+ end
+
describe 'handling STI', :aggregate_failures do
let(:namespace_type) { nil }
let(:parent) { nil }
@@ -369,7 +504,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
it { is_expected.to delegate_method(:runner_registration_enabled).to(:namespace_settings) }
it { is_expected.to delegate_method(:runner_registration_enabled?).to(:namespace_settings) }
it { is_expected.to delegate_method(:allow_runner_registration_token).to(:namespace_settings) }
- it { is_expected.to delegate_method(:allow_runner_registration_token?).to(:namespace_settings) }
it { is_expected.to delegate_method(:maven_package_requests_forwarding).to(:package_settings) }
it { is_expected.to delegate_method(:pypi_package_requests_forwarding).to(:package_settings) }
it { is_expected.to delegate_method(:npm_package_requests_forwarding).to(:package_settings) }
@@ -388,6 +522,38 @@ RSpec.describe Namespace, feature_category: :subgroups do
is_expected.to delegate_method(:allow_runner_registration_token=).to(:namespace_settings)
.with_arguments(:args)
end
+
+ describe '#allow_runner_registration_token?' do
+ subject { namespace.allow_runner_registration_token? }
+
+ context 'when namespace_settings is nil' do
+ let_it_be(:namespace) { create(:namespace) }
+
+ it { is_expected.to eq false }
+ end
+
+ context 'when namespace_settings is not nil' do
+ let_it_be(:namespace) { create(:namespace, :with_namespace_settings) }
+
+ it { is_expected.to eq true }
+
+ context 'when namespace_settings.allow_runner_registration_token? is false' do
+ before do
+ namespace.allow_runner_registration_token = false
+ end
+
+ it { is_expected.to eq false }
+ end
+
+ context 'when namespace_settings.allow_runner_registration_token? is true' do
+ before do
+ namespace.allow_runner_registration_token = true
+ end
+
+ it { is_expected.to eq true }
+ end
+ end
+ end
end
describe "Respond to" do
@@ -436,6 +602,14 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
end
+ context 'when parent is nil' do
+ let(:namespace) { build(:group, parent: nil) }
+
+ it 'returns []' do
+ expect(namespace.traversal_ids).to eq []
+ end
+ end
+
context 'when made a child group' do
let!(:namespace) { create(:group) }
let!(:parent_namespace) { create(:group, children: [namespace]) }
@@ -458,6 +632,17 @@ RSpec.describe Namespace, feature_category: :subgroups do
expect(namespace.root_ancestor).to eq new_root
end
end
+
+ context 'within a transaction' do
+ # We would like traversal_ids to be defined within a transaction, but it's not possible yet.
+ # This spec exists to assert that the behavior is known.
+ it 'is not defined yet' do
+ Namespace.transaction do
+ group = create(:group)
+ expect(group.traversal_ids).to be_empty
+ end
+ end
+ end
end
context 'traversal scopes' do
@@ -542,17 +727,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
it { expect(child.traversal_ids).to eq [parent.id, child.id] }
it { expect(parent.sync_events.count).to eq 1 }
it { expect(child.sync_events.count).to eq 1 }
-
- context 'when set_traversal_ids_on_save feature flag is disabled' do
- before do
- stub_feature_flags(set_traversal_ids_on_save: false)
- end
-
- it 'only sets traversal_ids on reload' do
- expect { parent.reload }.to change(parent, :traversal_ids).from([]).to([parent.id])
- expect { child.reload }.to change(child, :traversal_ids).from([]).to([parent.id, child.id])
- end
- end
end
context 'traversal_ids on update' do
@@ -565,18 +739,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
it 'sets the traversal_ids attribute' do
expect { subject }.to change { namespace1.traversal_ids }.from([namespace1.id]).to([namespace2.id, namespace1.id])
end
-
- context 'when set_traversal_ids_on_save feature flag is disabled' do
- before do
- stub_feature_flags(set_traversal_ids_on_save: false)
- end
-
- it 'sets traversal_ids after reload' do
- subject
-
- expect { namespace1.reload }.to change(namespace1, :traversal_ids).from([]).to([namespace2.id, namespace1.id])
- end
- end
end
it 'creates a Namespaces::SyncEvent using triggers' do
@@ -677,24 +839,41 @@ RSpec.describe Namespace, feature_category: :subgroups do
describe '#any_project_has_container_registry_tags?' do
subject { namespace.any_project_has_container_registry_tags? }
- let!(:project_without_registry) { create(:project, namespace: namespace) }
+ let(:project) { create(:project, namespace: namespace) }
+
+ it 'returns true if there is a project with container registry tags' do
+ expect(namespace).to receive(:first_project_with_container_registry_tags).and_return(project)
- context 'without tags' do
- it { is_expected.to be_falsey }
+ expect(subject).to be_truthy
end
- context 'with tags' do
- before do
- repositories = create_list(:container_repository, 3)
- create(:project, namespace: namespace, container_repositories: repositories)
+ it 'returns false if there is no project with container registry tags' do
+ expect(namespace).to receive(:first_project_with_container_registry_tags).and_return(nil)
+
+ expect(subject).to be_falsey
+ end
+ end
+ describe '#first_project_with_container_registry_tags' do
+ let(:container_repository) { create(:container_repository) }
+ let!(:project) { create(:project, namespace: namespace, container_repositories: [container_repository]) }
+
+ context 'when Gitlab API is not supported' do
+ before do
stub_container_registry_config(enabled: true)
+ allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(false)
end
- it 'finds tags' do
+ it 'returns the project' do
stub_container_registry_tags(repository: :any, tags: ['tag'])
- is_expected.to be_truthy
+ expect(namespace.first_project_with_container_registry_tags).to eq(project)
+ end
+
+ it 'returns no project' do
+ stub_container_registry_tags(repository: :any, tags: nil)
+
+ expect(namespace.first_project_with_container_registry_tags).to be_nil
end
it 'does not cause N+1 query in fetching registries' do
@@ -704,29 +883,24 @@ RSpec.describe Namespace, feature_category: :subgroups do
other_repositories = create_list(:container_repository, 2)
create(:project, namespace: namespace, container_repositories: other_repositories)
- expect { namespace.any_project_has_container_registry_tags? }.not_to exceed_query_limit(control_count + 1)
+ expect { namespace.first_project_with_container_registry_tags }.not_to exceed_query_limit(control_count + 1)
end
end
- end
-
- describe '#first_project_with_container_registry_tags' do
- let(:container_repository) { create(:container_repository) }
- let!(:project) { create(:project, namespace: namespace, container_repositories: [container_repository]) }
- before do
- stub_container_registry_config(enabled: true)
- end
-
- it 'returns the project' do
- stub_container_registry_tags(repository: :any, tags: ['tag'])
-
- expect(namespace.first_project_with_container_registry_tags).to eq(project)
- end
+ context 'when Gitlab API is supported' do
+ before do
+ allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(true)
+ stub_container_registry_config(enabled: true, api_url: 'http://container-registry', key: 'spec/fixtures/x509_certificate_pk.key')
+ end
- it 'returns no project' do
- stub_container_registry_tags(repository: :any, tags: nil)
+ it 'calls and returns GitlabApiClient.one_project_with_container_registry_tag' do
+ expect(ContainerRegistry::GitlabApiClient)
+ .to receive(:one_project_with_container_registry_tag)
+ .with(namespace.full_path)
+ .and_return(project)
- expect(namespace.first_project_with_container_registry_tags).to be_nil
+ expect(namespace.first_project_with_container_registry_tags).to eq(project)
+ end
end
end
@@ -755,6 +929,7 @@ RSpec.describe Namespace, feature_category: :subgroups do
with_them do
before do
+ allow(ContainerRegistry::GitlabApiClient).to receive(:one_project_with_container_registry_tag).and_return(nil)
stub_container_registry_config(enabled: true, api_url: 'http://container-registry', key: 'spec/fixtures/x509_certificate_pk.key')
allow(Gitlab).to receive(:com?).and_return(true)
allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(gitlab_api_supported)
@@ -975,43 +1150,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
end
- describe '.find_by_pages_host' do
- it 'finds namespace by GitLab Pages host and is case-insensitive' do
- namespace = create(:namespace, name: 'topNAMEspace', path: 'topNAMEspace')
- create(:namespace, name: 'annother_namespace')
- host = "TopNamespace.#{Settings.pages.host.upcase}"
-
- expect(described_class.find_by_pages_host(host)).to eq(namespace)
- end
-
- context 'when there is non-top-level group with searched name' do
- before do
- create(:group, :nested, path: 'pages')
- end
-
- it 'ignores this group' do
- host = "pages.#{Settings.pages.host.upcase}"
-
- expect(described_class.find_by_pages_host(host)).to be_nil
- end
-
- it 'finds right top level group' do
- group = create(:group, path: 'pages')
-
- host = "pages.#{Settings.pages.host.upcase}"
-
- expect(described_class.find_by_pages_host(host)).to eq(group)
- end
- end
-
- it "returns no result if the provided host is not subdomain of the Pages host" do
- create(:namespace, name: 'namespace.io')
- host = "namespace.io"
-
- expect(described_class.find_by_pages_host(host)).to eq(nil)
- end
- end
-
describe '.top_most' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:group) { create(:group) }
@@ -1037,6 +1175,7 @@ RSpec.describe Namespace, feature_category: :subgroups do
allow(namespace).to receive(:path_was).and_return(namespace.path)
allow(namespace).to receive(:path).and_return('new_path')
+ allow(namespace).to receive(:first_project_with_container_registry_tags).and_return(project)
end
it 'raises an error about not movable project' do
@@ -1447,30 +1586,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
end
- describe '#use_traversal_ids_for_root_ancestor?' do
- let_it_be(:namespace, reload: true) { create(:namespace) }
-
- subject { namespace.use_traversal_ids_for_root_ancestor? }
-
- context 'when use_traversal_ids_for_root_ancestor feature flag is true' do
- before do
- stub_feature_flags(use_traversal_ids_for_root_ancestor: true)
- end
-
- it { is_expected.to eq true }
-
- it_behaves_like 'disabled feature flag when traversal_ids is blank'
- end
-
- context 'when use_traversal_ids_for_root_ancestor feature flag is false' do
- before do
- stub_feature_flags(use_traversal_ids_for_root_ancestor: false)
- end
-
- it { is_expected.to eq false }
- end
- end
-
describe '#use_traversal_ids_for_ancestors?' do
let_it_be(:namespace, reload: true) { create(:namespace) }
@@ -1618,14 +1733,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
describe '#all_projects' do
- context 'when recursive approach is disabled' do
- before do
- stub_feature_flags(recursive_approach_for_all_projects: false)
- end
-
- include_examples '#all_projects'
- end
-
context 'with use_traversal_ids feature flag enabled' do
before do
stub_feature_flags(use_traversal_ids: true)
@@ -1917,6 +2024,62 @@ RSpec.describe Namespace, feature_category: :subgroups do
expect(very_deep_nested_group.root_ancestor).to eq(root_group)
end
end
+
+ context 'when parent is changed' do
+ let(:group) { create(:group) }
+ let(:new_parent) { create(:group) }
+
+ shared_examples 'updates root_ancestor' do
+ it do
+ expect { subject }.to change { group.root_ancestor }.from(group).to(new_parent)
+ end
+ end
+
+ context 'by object' do
+ subject { group.parent = new_parent }
+
+ include_examples 'updates root_ancestor'
+ end
+
+ context 'by id' do
+ subject { group.parent_id = new_parent.id }
+
+ include_examples 'updates root_ancestor'
+ end
+ end
+
+ context 'within a transaction' do
+ context 'with a persisted parent' do
+ let(:parent) { create(:group) }
+
+ it do
+ Namespace.transaction do
+ group = create(:group, parent: parent)
+ expect(group.root_ancestor).to eq parent
+ end
+ end
+ end
+
+ context 'with a non-persisted parent' do
+ let(:parent) { build(:group) }
+
+ it do
+ Namespace.transaction do
+ group = create(:group, parent: parent)
+ expect(group.root_ancestor).to eq parent
+ end
+ end
+ end
+
+ context 'without a parent' do
+ it do
+ Namespace.transaction do
+ group = create(:group)
+ expect(group.root_ancestor).to eq group
+ end
+ end
+ end
+ end
end
describe '#full_path_before_last_save' do
@@ -2105,34 +2268,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
end
- describe '#pages_virtual_domain' do
- let(:project) { create(:project, namespace: namespace) }
- let(:virtual_domain) { namespace.pages_virtual_domain }
-
- before do
- project.mark_pages_as_deployed
- project.update_pages_deployment!(create(:pages_deployment, project: project))
- end
-
- it 'returns the virual domain' do
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{namespace.root_ancestor.id}_/)
- end
-
- context 'when :cache_pages_domain_api is disabled' do
- before do
- stub_feature_flags(cache_pages_domain_api: false)
- end
-
- it 'returns the virual domain' do
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- expect(virtual_domain.cache_key).to be_nil
- end
- 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
@@ -2505,28 +2640,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
end
- describe 'storage_enforcement_date', :freeze_time do
- let_it_be(:namespace) { create(:group) }
-
- before do
- stub_feature_flags(namespace_storage_limit_bypass_date_check: false)
- end
-
- it 'returns correct date' do
- expect(namespace.storage_enforcement_date).to eql(3.months.from_now.to_date)
- end
-
- context 'when :storage_banner_bypass_date_check is enabled' do
- before do
- stub_feature_flags(namespace_storage_limit_bypass_date_check: true)
- end
-
- it 'returns the current date' do
- expect(namespace.storage_enforcement_date).to eq(Date.current)
- end
- end
- end
-
describe 'serialization' do
let(:object) { build(:namespace) }
diff --git a/spec/models/namespaces/randomized_suffix_path_spec.rb b/spec/models/namespaces/randomized_suffix_path_spec.rb
index a2484030f3c..fc5ccd95ce6 100644
--- a/spec/models/namespaces/randomized_suffix_path_spec.rb
+++ b/spec/models/namespaces/randomized_suffix_path_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespaces::RandomizedSuffixPath, feature_category: :not_owned do
+RSpec.describe Namespaces::RandomizedSuffixPath, feature_category: :shared do
let(:path) { 'backintime' }
subject(:suffixed_path) { described_class.new(path) }
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 013070f7be5..f722415d428 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Note do
+RSpec.describe Note, feature_category: :team_planning do
include RepoHelpers
describe 'associations' do
@@ -11,6 +11,7 @@ RSpec.describe Note do
it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to have_many(:todos) }
+ it { is_expected.to have_one(:note_metadata).inverse_of(:note).class_name('Notes::NoteMetadata') }
it { is_expected.to belong_to(:review).inverse_of(:notes) }
end
@@ -799,20 +800,22 @@ RSpec.describe Note do
describe '#system_note_with_references?' do
it 'falsey for user-generated notes' do
- note = create(:note, system: false)
+ note = build_stubbed(:note, system: false)
expect(note.system_note_with_references?).to be_falsy
end
context 'when the note might contain cross references' do
SystemNoteMetadata.new.cross_reference_types.each do |type|
- let(:note) { create(:note, :system) }
- let!(:metadata) { create(:system_note_metadata, note: note, action: type) }
+ context "with #{type}" do
+ let(:note) { build_stubbed(:note, :system) }
+ let!(:metadata) { build_stubbed(:system_note_metadata, note: note, action: type) }
- it 'delegates to the cross-reference regex' do
- expect(note).to receive(:matches_cross_reference_regex?).and_return(false)
+ it 'delegates to the cross-reference regex' do
+ expect(note).to receive(:matches_cross_reference_regex?).and_return(false)
- note.system_note_with_references?
+ note.system_note_with_references?
+ end
end
end
end
@@ -1100,6 +1103,16 @@ RSpec.describe Note do
end
end
+ describe '#for_work_item?' do
+ it 'returns true for a work item' do
+ expect(build(:note_on_work_item).for_work_item?).to be true
+ end
+
+ it 'returns false for an issue' do
+ expect(build(:note_on_issue).for_work_item?).to be false
+ end
+ end
+
describe '#for_project_snippet?' do
it 'returns true for a project snippet note' do
expect(build(:note_on_project_snippet).for_project_snippet?).to be true
@@ -1656,6 +1669,32 @@ RSpec.describe Note do
end
end
end
+
+ describe '.without_hidden' do
+ subject { described_class.without_hidden }
+
+ context 'when a note with a banned author exists' do
+ let_it_be(:banned_user) { create(:banned_user).user }
+ let_it_be(:banned_note) { create(:note, author: banned_user) }
+
+ context 'when the :hidden_notes feature is disabled' do
+ before do
+ stub_feature_flags(hidden_notes: false)
+ end
+
+ it { is_expected.to include(banned_note, note1) }
+ end
+
+ context 'when the :hidden_notes feature is enabled' do
+ before do
+ stub_feature_flags(hidden_notes: true)
+ end
+
+ it { is_expected.not_to include(banned_note) }
+ it { is_expected.to include(note1) }
+ end
+ end
+ end
end
describe 'banzai_render_context' do
diff --git a/spec/models/notes/note_metadata_spec.rb b/spec/models/notes/note_metadata_spec.rb
new file mode 100644
index 00000000000..d1b35e0cdf9
--- /dev/null
+++ b/spec/models/notes/note_metadata_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Notes::NoteMetadata, feature_category: :team_planning do
+ describe 'associations' do
+ it { is_expected.to belong_to(:note) }
+ end
+
+ describe 'callbacks' do
+ let_it_be(:note) { create(:note) }
+ let_it_be(:email) { "#{'a' * 255}@example.com" }
+
+ context 'with before_save :ensure_email_participant_length' do
+ let(:note_metadata) { create(:note_metadata, note: note, email_participant: email) }
+
+ context 'when email length is > 255' do
+ let(:expected_email) { "#{'a' * 252}..." }
+
+ it 'rewrites the email within max length' do
+ expect(note_metadata.email_participant.length).to eq(255)
+ expect(note.note_metadata.email_participant).to eq(expected_email)
+ end
+ end
+
+ context 'when email is within permissible length' do
+ let(:email) { 'email@example.com' }
+
+ it 'saves the email as-is' do
+ expect(note_metadata.email_participant).to eq(email)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/oauth_access_token_spec.rb b/spec/models/oauth_access_token_spec.rb
index fc53d926dd6..5fa590eab58 100644
--- a/spec/models/oauth_access_token_spec.rb
+++ b/spec/models/oauth_access_token_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe OauthAccessToken do
it 'uses the expires_in value' do
token = OauthAccessToken.new(expires_in: 1.minute)
- expect(token.expires_in).to eq 1.minute
+ expect(token).to be_valid
end
end
@@ -67,7 +67,7 @@ RSpec.describe OauthAccessToken do
it 'uses default value' do
token = OauthAccessToken.new(expires_in: nil)
- expect(token.expires_in).to eq 2.hours
+ expect(token).to be_invalid
end
end
end
diff --git a/spec/models/onboarding/completion_spec.rb b/spec/models/onboarding/completion_spec.rb
index e1fad4255bc..dd7648f7799 100644
--- a/spec/models/onboarding/completion_spec.rb
+++ b/spec/models/onboarding/completion_spec.rb
@@ -2,24 +2,24 @@
require 'spec_helper'
-RSpec.describe Onboarding::Completion do
+RSpec.describe Onboarding::Completion, feature_category: :onboarding do
+ let(:completed_actions) { {} }
+ let(:project) { build(:project, namespace: namespace) }
+ let!(:onboarding_progress) { create(:onboarding_progress, namespace: namespace, **completed_actions) }
+
+ let_it_be(:namespace) { create(:namespace) }
+
describe '#percentage' do
- let(:completed_actions) { {} }
- let!(:onboarding_progress) { create(:onboarding_progress, namespace: namespace, **completed_actions) }
let(:tracked_action_columns) do
- [
- *described_class::ACTION_ISSUE_IDS.keys,
- *described_class::ACTION_PATHS,
- :security_scan_enabled
- ].map { |key| ::Onboarding::Progress.column_name(key) }
+ [*described_class::ACTION_PATHS, :security_scan_enabled].map do |key|
+ ::Onboarding::Progress.column_name(key)
+ end
end
- let_it_be(:namespace) { create(:namespace) }
-
- subject { described_class.new(namespace).percentage }
+ subject(:percentage) { described_class.new(project).percentage }
context 'when no onboarding_progress exists' do
- subject { described_class.new(build(:namespace)).percentage }
+ subject(:percentage) { described_class.new(build(:project)).percentage }
it { is_expected.to eq(0) }
end
@@ -29,30 +29,55 @@ RSpec.describe Onboarding::Completion do
end
context 'when all tracked actions have been completed' do
+ let(:project) { build(:project, :stubbed_commit_count, namespace: namespace) }
+
let(:completed_actions) do
tracked_action_columns.index_with { Time.current }
end
it { is_expected.to eq(100) }
end
+ end
+
+ describe '#completed?' do
+ subject(:completed?) { described_class.new(project).completed?(column) }
+
+ context 'when code_added' do
+ let(:column) { :code_added }
+
+ context 'when commit_count > 1' do
+ let(:project) { build(:project, :stubbed_commit_count, namespace: namespace) }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when branch_count > 1' do
+ let(:project) { build(:project, :stubbed_branch_count, namespace: namespace) }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when empty repository' do
+ let(:project) { build(:project, namespace: namespace) }
+
+ it { is_expected.to eq(false) }
+ end
+ end
- context 'with security_actions_continuous_onboarding experiment' do
- let(:completed_actions) { Hash[tracked_action_columns.first, Time.current] }
+ context 'when secure_dast_run' do
+ let(:column) { :secure_dast_run_at }
+ let(:completed_actions) { { secure_dast_run_at: secure_dast_run_at } }
- context 'when control' do
- before do
- stub_experiments(security_actions_continuous_onboarding: :control)
- end
+ context 'when is completed' do
+ let(:secure_dast_run_at) { Time.current }
- it { is_expected.to eq(11) }
+ it { is_expected.to eq(true) }
end
- context 'when candidate' do
- before do
- stub_experiments(security_actions_continuous_onboarding: :candidate)
- end
+ context 'when is not completed' do
+ let(:secure_dast_run_at) { nil }
- it { is_expected.to eq(9) }
+ it { is_expected.to eq(false) }
end
end
end
diff --git a/spec/models/onboarding/progress_spec.rb b/spec/models/onboarding/progress_spec.rb
index 9d91af2487a..7d169464462 100644
--- a/spec/models/onboarding/progress_spec.rb
+++ b/spec/models/onboarding/progress_spec.rb
@@ -187,7 +187,7 @@ RSpec.describe Onboarding::Progress do
end
context 'for multiple actions' do
- let(:action1) { :security_scan_enabled }
+ let(:action1) { :secure_dast_run }
let(:action2) { :secure_dependency_scanning_run }
let(:actions) { [action1, action2] }
@@ -206,11 +206,11 @@ RSpec.describe Onboarding::Progress do
it 'does not override timestamp', :aggregate_failures do
described_class.register(namespace, [action1])
- expect(described_class.find_by_namespace_id(namespace.id).security_scan_enabled_at).not_to be_nil
+ expect(described_class.find_by_namespace_id(namespace.id).secure_dast_run_at).not_to be_nil
expect(described_class.find_by_namespace_id(namespace.id).secure_dependency_scanning_run_at).to be_nil
expect { described_class.register(namespace, [action1, action2]) }.not_to change {
- described_class.find_by_namespace_id(namespace.id).security_scan_enabled_at
+ described_class.find_by_namespace_id(namespace.id).secure_dast_run_at
}
expect(described_class.find_by_namespace_id(namespace.id).secure_dependency_scanning_run_at).not_to be_nil
end
diff --git a/spec/models/operations/feature_flag_spec.rb b/spec/models/operations/feature_flag_spec.rb
index dd1ff95a16d..4a39c5dc234 100644
--- a/spec/models/operations/feature_flag_spec.rb
+++ b/spec/models/operations/feature_flag_spec.rb
@@ -37,8 +37,8 @@ RSpec.describe Operations::FeatureFlag do
end
describe '#to_reference' do
- let(:namespace) { build(:namespace, path: 'sample-namespace') }
- let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
+ let(:namespace) { build(:namespace) }
+ let(:project) { build(:project, namespace: namespace) }
let(:feature_flag) { build(:operations_feature_flag, iid: 1, project: project) }
it 'returns feature flag id' do
@@ -46,7 +46,7 @@ RSpec.describe Operations::FeatureFlag do
end
it 'returns complete path to the feature flag with full: true' do
- expect(feature_flag.to_reference(full: true)).to eq '[feature_flag:sample-namespace/sample-project/1]'
+ expect(feature_flag.to_reference(full: true)).to eq "[feature_flag:#{project.full_path}/1]"
end
end
diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb
new file mode 100644
index 00000000000..e1aac88e640
--- /dev/null
+++ b/spec/models/organization_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Organization, type: :model, feature_category: :cell do
+ let_it_be(:organization) { create(:organization) }
+ let_it_be(:default_organization) { create(:organization, :default) }
+
+ describe 'validations' do
+ subject { create(:organization) }
+
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).case_insensitive }
+ it { is_expected.to validate_length_of(:name).is_at_most(255) }
+ end
+
+ context 'when using scopes' do
+ describe '.without_default' do
+ it 'excludes default organization' do
+ expect(described_class.without_default).not_to include(default_organization)
+ end
+
+ it 'includes other organizations organization' do
+ expect(described_class.without_default).to include(organization)
+ end
+ end
+ end
+
+ describe '#id' do
+ context 'when organization is default' do
+ it 'has id 1' do
+ expect(default_organization.id).to eq(1)
+ end
+ end
+
+ context 'when organization is not default' do
+ it 'does not have id 1' do
+ expect(organization.id).not_to eq(1)
+ end
+ end
+ end
+
+ describe '#destroy!' do
+ context 'when trying to delete the default organization' do
+ it 'raises an error' do
+ expect do
+ default_organization.destroy!
+ end.to raise_error(ActiveRecord::RecordNotDestroyed, _('Cannot delete the default organization'))
+ end
+ end
+
+ context 'when trying to delete a non-default organization' do
+ let(:to_be_removed) { create(:organization) }
+
+ it 'does not raise error' do
+ expect { to_be_removed.destroy! }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#destroy' do
+ context 'when trying to delete the default organization' do
+ it 'returns false' do
+ expect(default_organization.destroy).to eq(false)
+ end
+ end
+
+ context 'when trying to delete a non-default organization' do
+ let(:to_be_removed) { create(:organization) }
+
+ it 'returns true' do
+ expect(to_be_removed.destroy).to eq(to_be_removed)
+ end
+ end
+ end
+
+ describe '#default?' do
+ context 'when organization is default' do
+ it 'returns true' do
+ expect(default_organization.default?).to eq(true)
+ end
+ end
+
+ context 'when organization is not default' do
+ it 'returns false' do
+ expect(organization.default?).to eq(false)
+ end
+ end
+ end
+
+ describe '#name' do
+ context 'when organization is default' do
+ it 'returns Default' do
+ expect(default_organization.name).to eq('Default')
+ end
+ end
+ end
+end
diff --git a/spec/models/packages/debian/file_metadatum_spec.rb b/spec/models/packages/debian/file_metadatum_spec.rb
index 1215adfa6a1..e86c0a71c9a 100644
--- a/spec/models/packages/debian/file_metadatum_spec.rb
+++ b/spec/models/packages/debian/file_metadatum_spec.rb
@@ -2,15 +2,15 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::FileMetadatum, type: :model do
- RSpec.shared_context 'Debian file metadatum' do |factory, trait|
- let_it_be_with_reload(:debian_package_file) { create(factory, trait) }
+RSpec.describe Packages::Debian::FileMetadatum, type: :model, feature_category: :package_registry do
+ RSpec.shared_context 'with Debian file metadatum' do |package_file_trait|
+ let_it_be_with_reload(:debian_package_file) { create(:debian_package_file, package_file_trait) }
let(:debian_file_metadatum) { debian_package_file.debian_file_metadatum }
subject { debian_file_metadatum }
end
- RSpec.shared_examples 'Test Debian file metadatum' do |has_component, has_architecture, has_fields, has_outdated|
+ RSpec.shared_examples 'Test Debian file metadatum' do |has_component, has_architecture, has_fields|
describe 'relationships' do
it { is_expected.to belong_to(:package_file) }
end
@@ -51,8 +51,8 @@ RSpec.describe Packages::Debian::FileMetadatum, type: :model do
describe '#fields' do
if has_fields
it { is_expected.to validate_presence_of(:fields) }
- it { is_expected.to allow_value({ 'a': 'b' }).for(:fields) }
- it { is_expected.not_to allow_value({ 'a': { 'b': 'c' } }).for(:fields) }
+ it { is_expected.to allow_value({ a: 'b' }).for(:fields) }
+ it { is_expected.not_to allow_value({ a: { b: 'c' } }).for(:fields) }
else
it { is_expected.to validate_absence_of(:fields) }
end
@@ -69,23 +69,35 @@ RSpec.describe Packages::Debian::FileMetadatum, type: :model do
end
end
end
+
+ describe 'scopes' do
+ describe '.with_file_type' do
+ subject { described_class.with_file_type(package_file_trait) }
+
+ it 'returns the matching file metadatum' do
+ expect(subject).to match_array([debian_file_metadatum])
+ end
+ end
+ end
end
using RSpec::Parameterized::TableSyntax
- where(:factory, :trait, :has_component, :has_architecture, :has_fields) do
- :debian_package_file | :unknown | false | false | false
- :debian_package_file | :source | true | false | false
- :debian_package_file | :dsc | true | false | true
- :debian_package_file | :deb | true | true | true
- :debian_package_file | :udeb | true | true | true
- :debian_package_file | :buildinfo | true | false | true
- :debian_package_file | :changes | false | false | true
+ where(:package_file_trait, :has_component, :has_architecture, :has_fields) do
+ :unknown | false | false | false
+ :source | true | false | false
+ :dsc | true | false | true
+ :deb | true | true | true
+ :udeb | true | true | true
+ :ddeb | true | true | true
+ :buildinfo | true | false | true
+ :changes | false | false | true
end
with_them do
- include_context 'Debian file metadatum', params[:factory], params[:trait] do
- it_behaves_like 'Test Debian file metadatum', params[:has_component], params[:has_architecture], params[:has_fields], params[:has_outdated]
+ include_context 'with Debian file metadatum', params[:package_file_trait] do
+ it_behaves_like 'Test Debian file metadatum',
+ params[:has_component], params[:has_architecture], params[:has_fields]
end
end
end
diff --git a/spec/models/packages/debian/group_distribution_spec.rb b/spec/models/packages/debian/group_distribution_spec.rb
index 90fb0d0e7d8..1af23ad3ac0 100644
--- a/spec/models/packages/debian/group_distribution_spec.rb
+++ b/spec/models/packages/debian/group_distribution_spec.rb
@@ -2,6 +2,9 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::GroupDistribution do
- it_behaves_like 'Debian Distribution', :debian_group_distribution, :group, false
+RSpec.describe Packages::Debian::GroupDistribution, feature_category: :package_registry do
+ include_context 'for Debian Distribution', :debian_group_distribution, false
+
+ it_behaves_like 'Debian Distribution for common behavior'
+ it_behaves_like 'Debian Distribution with group container'
end
diff --git a/spec/models/packages/debian/project_distribution_spec.rb b/spec/models/packages/debian/project_distribution_spec.rb
index 5f4041ad9fe..2b79cdb539f 100644
--- a/spec/models/packages/debian/project_distribution_spec.rb
+++ b/spec/models/packages/debian/project_distribution_spec.rb
@@ -2,6 +2,9 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::ProjectDistribution do
- it_behaves_like 'Debian Distribution', :debian_project_distribution, :project, true
+RSpec.describe Packages::Debian::ProjectDistribution, feature_category: :package_registry do
+ include_context 'for Debian Distribution', :debian_project_distribution, true
+
+ it_behaves_like 'Debian Distribution for common behavior'
+ it_behaves_like 'Debian Distribution with project container'
end
diff --git a/spec/models/packages/dependency_spec.rb b/spec/models/packages/dependency_spec.rb
index 1575dec98c9..80ec7f77fda 100644
--- a/spec/models/packages/dependency_spec.rb
+++ b/spec/models/packages/dependency_spec.rb
@@ -1,7 +1,11 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Dependency, type: :model do
+RSpec.describe Packages::Dependency, type: :model, feature_category: :package_registry do
+ describe 'included modules' do
+ it { is_expected.to include_module(EachBatch) }
+ end
+
describe 'relationships' do
it { is_expected.to have_many(:dependency_links) }
end
@@ -110,6 +114,19 @@ RSpec.describe Packages::Dependency, type: :model do
end
end
+ describe '.orphaned' do
+ let_it_be(:orphaned_dependencies) { create_list(:packages_dependency, 2) }
+ let_it_be(:linked_dependency) do
+ create(:packages_dependency).tap do |dependency|
+ create(:packages_dependency_link, dependency: dependency)
+ end
+ end
+
+ it 'returns orphaned dependency records' do
+ expect(described_class.orphaned).to contain_exactly(*orphaned_dependencies)
+ end
+ end
+
def build_names_and_version_patterns(*package_dependencies)
result = Hash.new { |h, dependency| h[dependency.name] = dependency.version_pattern }
package_dependencies.each { |dependency| result[dependency] }
diff --git a/spec/models/packages/event_spec.rb b/spec/models/packages/event_spec.rb
new file mode 100644
index 00000000000..58c1c1e6e92
--- /dev/null
+++ b/spec/models/packages/event_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Event, feature_category: :package_registry do
+ let(:event_type) { :push_package }
+ let(:event_scope) { :npm }
+ let(:originator_type) { :deploy_token }
+
+ shared_examples 'handle forbidden event type' do |result: []|
+ let(:event_type) { :search }
+
+ it { is_expected.to eq(result) }
+ end
+
+ describe '.event_allowed?' do
+ subject { described_class.event_allowed?(event_type) }
+
+ it { is_expected.to eq(true) }
+
+ it_behaves_like 'handle forbidden event type', result: false
+ end
+
+ describe '.unique_counters_for' do
+ subject { described_class.unique_counters_for(event_scope, event_type, originator_type) }
+
+ it { is_expected.to contain_exactly('i_package_npm_deploy_token') }
+
+ it_behaves_like 'handle forbidden event type'
+
+ context 'when an originator type is quest' do
+ let(:originator_type) { :guest }
+
+ it { is_expected.to eq([]) }
+ end
+ end
+
+ describe '.counters_for' do
+ subject { described_class.counters_for(event_scope, event_type, originator_type) }
+
+ it do
+ is_expected.to contain_exactly(
+ 'i_package_push_package',
+ 'i_package_push_package_by_deploy_token',
+ 'i_package_npm_push_package'
+ )
+ end
+
+ it_behaves_like 'handle forbidden event type'
+ end
+end
diff --git a/spec/models/packages/npm/metadata_cache_spec.rb b/spec/models/packages/npm/metadata_cache_spec.rb
new file mode 100644
index 00000000000..5e7a710baf8
--- /dev/null
+++ b/spec/models/packages/npm/metadata_cache_spec.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Npm::MetadataCache, type: :model, feature_category: :package_registry do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:package_name) { '@root/test' }
+
+ it { is_expected.to be_a FileStoreMounter }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project).inverse_of(:npm_metadata_caches) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:file) }
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:size) }
+
+ describe '#package_name' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, package_name: package_name, project: project) }
+
+ it { is_expected.to validate_presence_of(:package_name) }
+
+ describe 'uniqueness' do
+ it 'ensures the package name is unique within a given project' do
+ expect do
+ create(:npm_metadata_cache, package_name: package_name, project: project)
+ end.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Package name has already been taken')
+ end
+
+ it 'allows duplicate file names in different projects' do
+ expect do
+ create(:npm_metadata_cache, package_name: package_name, project: create(:project))
+ end.not_to raise_error
+ end
+ end
+
+ describe 'format' do
+ it { is_expected.to allow_value('my.app-11.07.2018').for(:package_name) }
+ it { is_expected.to allow_value('@group-1/package').for(:package_name) }
+ it { is_expected.to allow_value('@any-scope/package').for(:package_name) }
+ it { is_expected.to allow_value('unscoped-package').for(:package_name) }
+
+ it { is_expected.not_to allow_value('my(dom$$$ain)com.my-app').for(:package_name) }
+ it { is_expected.not_to allow_value('@inv@lid-scope/package').for(:package_name) }
+ it { is_expected.not_to allow_value('@scope/../../package').for(:package_name) }
+ it { is_expected.not_to allow_value('@scope%2e%2e%fpackage').for(:package_name) }
+ it { is_expected.not_to allow_value('@scope/sub/package').for(:package_name) }
+ end
+ end
+ end
+
+ describe '.find_or_build' do
+ subject { described_class.find_or_build(package_name: package_name, project_id: project.id) }
+
+ context 'when a metadata cache exists' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, package_name: package_name, project: project) }
+
+ it 'finds an existing metadata cache' do
+ expect(subject).to eq(npm_metadata_cache)
+ end
+ end
+
+ context 'when a metadata cache not found' do
+ let(:package_name) { 'not_existing' }
+
+ it 'builds a new instance', :aggregate_failures do
+ expect(subject).not_to be_persisted
+ expect(subject.package_name).to eq(package_name)
+ expect(subject.project_id).to eq(project.id)
+ end
+ end
+ end
+
+ describe 'save callbacks' do
+ describe 'object_storage_key' do
+ let(:object_storage_key) do
+ Gitlab::HashedPath.new(
+ 'packages', 'metadata_caches', 'npm', OpenSSL::Digest::SHA256.hexdigest(package_name),
+ root_hash: project.id
+ )
+ end
+
+ before do
+ allow(Gitlab::HashedPath).to receive(:new).and_return(object_storage_key)
+ end
+
+ context 'when the record is created' do
+ let(:npm_metadata_cache) { build(:npm_metadata_cache, package_name: package_name, project: project) }
+
+ it 'sets object_storage_key' do
+ npm_metadata_cache.save!
+
+ expect(npm_metadata_cache.object_storage_key).to eq(object_storage_key.to_s)
+ end
+
+ context 'when using `update!`' do
+ let(:metadata_content) { {}.to_json }
+
+ it 'sets object_storage_key' do
+ npm_metadata_cache.update!(
+ file: CarrierWaveStringFile.new(metadata_content),
+ size: metadata_content.bytesize
+ )
+
+ expect(npm_metadata_cache.object_storage_key).to eq(object_storage_key.to_s)
+ end
+ end
+ end
+
+ context 'when the record is updated' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, package_name: package_name, project: project) }
+
+ let(:existing_object_storage_key) { npm_metadata_cache.object_storage_key }
+ let(:new_package_name) { 'updated_package_name' }
+
+ it 'does not update object_storage_key' do
+ existing_object_storage_key = npm_metadata_cache.object_storage_key
+
+ npm_metadata_cache.update!(package_name: new_package_name)
+
+ expect(npm_metadata_cache.object_storage_key).to eq(existing_object_storage_key)
+ end
+ end
+ end
+ end
+
+ describe 'readonly attributes' do
+ describe 'object_storage_key' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache) }
+
+ it 'sets object_storage_key' do
+ expect(npm_metadata_cache.object_storage_key).to be_present
+ end
+
+ context 'when the record is persisted' do
+ let(:new_object_storage_key) { 'object/storage/updated_key' }
+
+ it 'does not re-set object_storage_key' do
+ npm_metadata_cache.object_storage_key = new_object_storage_key
+
+ npm_metadata_cache.save!
+
+ expect(npm_metadata_cache.object_storage_key).not_to eq(new_object_storage_key)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/packages/npm/metadatum_spec.rb b/spec/models/packages/npm/metadatum_spec.rb
index ff8cce5310e..92daddded7e 100644
--- a/spec/models/packages/npm/metadatum_spec.rb
+++ b/spec/models/packages/npm/metadatum_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Npm::Metadatum, type: :model do
+RSpec.describe Packages::Npm::Metadatum, type: :model, feature_category: :package_registry do
describe 'relationships' do
it { is_expected.to belong_to(:package).inverse_of(:npm_metadatum) }
end
@@ -47,4 +47,16 @@ RSpec.describe Packages::Npm::Metadatum, type: :model do
end
end
end
+
+ describe 'scopes' do
+ describe '.package_id_in' do
+ let_it_be(:package) { create(:npm_package) }
+ let_it_be(:metadatum_1) { create(:npm_metadatum, package: package) }
+ let_it_be(:metadatum_2) { create(:npm_metadatum) }
+
+ it 'returns metadatums with the given package ids' do
+ expect(described_class.package_id_in([package.id])).to contain_exactly(metadatum_1)
+ end
+ end
+ end
end
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index 9b341034aaa..c9db1efc64a 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -153,6 +153,7 @@ RSpec.describe Packages::PackageFile, type: :model do
let_it_be(:debian_changes) { debian_package.package_files.last }
let_it_be(:debian_deb) { create(:debian_package_file, package: debian_package) }
let_it_be(:debian_udeb) { create(:debian_package_file, :udeb, package: debian_package) }
+ let_it_be(:debian_ddeb) { create(:debian_package_file, :ddeb, package: debian_package) }
let_it_be(:debian_contrib) do
create(:debian_package_file, package: debian_package).tap do |pf|
@@ -177,6 +178,17 @@ RSpec.describe Packages::PackageFile, type: :model do
describe '#with_debian_architecture_name' do
it { expect(described_class.with_debian_architecture_name('mipsel')).to contain_exactly(debian_mipsel) }
end
+
+ describe '#with_debian_unknown_since' do
+ let_it_be(:incoming) { create(:debian_incoming, project: project) }
+
+ before do
+ incoming.package_files.first.debian_file_metadatum.update! updated_at: 1.day.ago
+ incoming.package_files.second.update! updated_at: 1.day.ago, status: :error
+ end
+
+ it { expect(described_class.with_debian_unknown_since(1.hour.ago)).to contain_exactly(incoming.package_files.first) }
+ end
end
describe '.for_helm_with_channel' do
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 992cc5c4354..e79459e0c7c 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -682,24 +682,20 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
end
end
- describe "#unique_debian_package_name" do
+ describe "uniqueness for package type debian" do
let!(:package) { create(:debian_package) }
- it "will allow a Debian package with same project, name and version, but different distribution" do
- new_package = build(:debian_package, project: package.project, name: package.name, version: package.version)
- expect(new_package).to be_valid
- end
-
it "will not allow a Debian package with same project, name, version and distribution" do
new_package = build(:debian_package, project: package.project, name: package.name, version: package.version)
new_package.debian_publication.distribution = package.debian_publication.distribution
expect(new_package).not_to be_valid
- expect(new_package.errors.to_a).to include('Debian package already exists in Distribution')
+ expect(new_package.errors.to_a).to include('Name has already been taken')
end
- it "will allow a Debian package with same project, name, version, but no distribution" do
+ it "will not allow a Debian package with same project, name, version, but no distribution" do
new_package = build(:debian_package, project: package.project, name: package.name, version: package.version, published_in: nil)
- expect(new_package).to be_valid
+ expect(new_package).not_to be_valid
+ expect(new_package.errors.to_a).to include('Name has already been taken')
end
context 'with pending_destruction package' do
@@ -713,7 +709,7 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
end
end
- Packages::Package.package_types.keys.without('conan', 'debian').each do |pt|
+ Packages::Package.package_types.keys.without('conan').each do |pt|
context "project id, name, version and package type uniqueness for package type #{pt}" do
let(:package) { create("#{pt}_package") }
@@ -722,6 +718,15 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
expect(new_package).not_to be_valid
expect(new_package.errors.to_a).to include("Name has already been taken")
end
+
+ context 'with pending_destruction package' do
+ let!(:package) { create("#{pt}_package", :pending_destruction) }
+
+ it "will allow a #{pt} package with same project, name, version and package_type" do
+ new_package = build("#{pt}_package", project: package.project, name: package.name, version: package.version)
+ expect(new_package).to be_valid
+ end
+ end
end
end
end
@@ -842,6 +847,14 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
is_expected.to match_array([package])
end
end
+
+ describe '.preload_conan_metadatum' do
+ subject { described_class.preload_conan_metadatum }
+
+ it 'loads conan metadatum' do
+ expect(subject.first.association(:conan_metadatum)).to be_loaded
+ end
+ end
end
describe '.without_nuget_temporary_name' do
@@ -1235,8 +1248,18 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
let_it_be(:first_build_info) { create(:package_build_info, :with_pipeline, package: package) }
let_it_be(:second_build_info) { create(:package_build_info, :with_pipeline, package: package) }
- it 'returns the first build info' do
- expect(package.original_build_info).to eq(first_build_info)
+ it 'returns the last build info' do
+ expect(package.original_build_info).to eq(second_build_info)
+ end
+
+ context 'with packages_display_last_pipeline disabled' do
+ before do
+ stub_feature_flags(packages_display_last_pipeline: false)
+ end
+
+ it 'returns the first build info' do
+ expect(package.original_build_info).to eq(first_build_info)
+ end
end
end
end
@@ -1402,4 +1425,36 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
.to change(package, :last_downloaded_at).from(nil).to(instance_of(ActiveSupport::TimeWithZone))
end
end
+
+ describe "#publish_creation_event" do
+ let_it_be(:project) { create(:project) }
+
+ let(:version) { '-' }
+ let(:package_type) { :generic }
+
+ subject { described_class.create!(project: project, name: 'incoming', version: version, package_type: package_type) }
+
+ context 'when package is generic' do
+ it 'publishes an event' do
+ expect { subject }
+ .to publish_event(::Packages::PackageCreatedEvent)
+ .with({
+ project_id: project.id,
+ id: kind_of(Numeric),
+ name: "incoming",
+ version: "-",
+ package_type: 'generic'
+ })
+ end
+ end
+
+ context 'when package is not generic' do
+ let(:package_type) { :debian }
+ let(:version) { 1 }
+
+ it 'does not create event' do
+ expect { subject }.not_to publish_event(::Packages::PackageCreatedEvent)
+ end
+ end
+ end
end
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index ef79ba28d5d..88fd1bd9e56 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -61,15 +61,13 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
it 'uses deployment from object storage' do
freeze_time do
- expect(source).to(
- eq({
- type: 'zip',
- path: deployment.file.url(expire_at: 1.day.from_now),
- global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
- sha256: deployment.file_sha256,
- file_size: deployment.size,
- file_count: deployment.file_count
- })
+ expect(source).to eq(
+ type: 'zip',
+ path: deployment.file.url(expire_at: 1.day.from_now),
+ global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
+ sha256: deployment.file_sha256,
+ file_size: deployment.size,
+ file_count: deployment.file_count
)
end
end
@@ -87,15 +85,13 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
it 'uses file protocol' do
freeze_time do
- expect(source).to(
- eq({
- type: 'zip',
- path: 'file://' + deployment.file.path,
- global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
- sha256: deployment.file_sha256,
- file_size: deployment.size,
- file_count: deployment.file_count
- })
+ expect(source).to eq(
+ type: 'zip',
+ path: "file://#{deployment.file.path}",
+ global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
+ sha256: deployment.file_sha256,
+ file_size: deployment.size,
+ file_count: deployment.file_count
)
end
end
@@ -108,15 +104,13 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
it 'uses deployment from object storage' do
freeze_time do
- expect(source).to(
- eq({
- type: 'zip',
- path: deployment.file.url(expire_at: 1.day.from_now),
- global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
- sha256: deployment.file_sha256,
- file_size: deployment.size,
- file_count: deployment.file_count
- })
+ expect(source).to eq(
+ type: 'zip',
+ path: deployment.file.url(expire_at: 1.day.from_now),
+ global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
+ sha256: deployment.file_sha256,
+ file_size: deployment.size,
+ file_count: deployment.file_count
)
end
end
@@ -143,4 +137,48 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
expect(lookup_path.prefix).to eq('/myproject/')
end
end
+
+ describe '#unique_host' do
+ let(:project) { build(:project) }
+
+ context 'when unique domain is disabled' do
+ it 'returns nil' do
+ project.project_setting.pages_unique_domain_enabled = false
+
+ expect(lookup_path.unique_host).to be_nil
+ end
+ end
+
+ context 'when unique domain is enabled' do
+ it 'returns the project unique domain' do
+ project.project_setting.pages_unique_domain_enabled = true
+ project.project_setting.pages_unique_domain = 'unique-domain'
+
+ expect(lookup_path.unique_host).to eq('unique-domain.example.com')
+ end
+ end
+ end
+
+ describe '#root_directory' do
+ subject(:lookup_path) { described_class.new(project) }
+
+ context 'when there is no deployment' do
+ it 'returns nil' do
+ expect(lookup_path.root_directory).to be_nil
+ end
+ end
+
+ context 'when there is a deployment' do
+ let(:deployment) { create(:pages_deployment, project: project, root_directory: 'foo') }
+
+ before do
+ project.mark_pages_as_deployed
+ project.pages_metadatum.update!(pages_deployment: deployment)
+ end
+
+ it 'returns the deployment\'s root_directory' do
+ expect(lookup_path.root_directory).to eq('foo')
+ end
+ end
+ end
end
diff --git a/spec/models/pages_deployment_spec.rb b/spec/models/pages_deployment_spec.rb
index 268c5006a88..767db511d85 100644
--- a/spec/models/pages_deployment_spec.rb
+++ b/spec/models/pages_deployment_spec.rb
@@ -59,6 +59,66 @@ RSpec.describe PagesDeployment, feature_category: :pages do
end
end
+ context 'when uploading the file' do
+ before do
+ stub_pages_object_storage(::Pages::DeploymentUploader)
+ end
+
+ describe '#store_after_commit?' do
+ context 'when feature flag pages_deploy_upload_file_outside_transaction is disabled' do
+ it 'returns false' do
+ Feature.disable(:pages_deploy_upload_file_outside_transaction)
+
+ deployment = create(:pages_deployment, project: project)
+ expect(deployment.store_after_commit?).to eq(false)
+ end
+ end
+
+ context 'when feature flag pages_deploy_upload_file_outside_transaction is enabled' do
+ it 'returns true' do
+ deployment = create(:pages_deployment, project: project)
+ expect(deployment.store_after_commit?).to eq(true)
+ end
+ end
+ end
+
+ context 'when feature flag pages_deploy_upload_file_outside_transaction is disabled' do
+ before do
+ Feature.disable(:pages_deploy_upload_file_outside_transaction)
+ end
+
+ it 'stores the file within the transaction' do
+ expect_next_instance_of(PagesDeployment) do |deployment|
+ expect(deployment).not_to receive(:store_file_now!)
+ end
+
+ create(:pages_deployment, project: project)
+ end
+ end
+
+ context 'when feature flag pages_deploy_upload_file_outside_transaction is enabled' do
+ before do
+ Feature.enable(:pages_deploy_upload_file_outside_transaction)
+ end
+
+ it 'stores the file outsize of the transaction' do
+ expect_next_instance_of(PagesDeployment) do |deployment|
+ expect(deployment).to receive(:store_file_now!)
+ end
+
+ create(:pages_deployment, project: project)
+ end
+
+ it 'does nothing when the file did not change' do
+ deployment = create(:pages_deployment, project: project)
+
+ expect(deployment).not_to receive(:store_file_now!)
+
+ deployment.touch
+ end
+ end
+ end
+
describe '#migrated?' do
it 'returns false for normal deployment' do
deployment = create(:pages_deployment)
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index f054fde78e7..b218d4dce09 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -9,7 +9,6 @@ RSpec.describe PagesDomain do
describe 'associations' do
it { is_expected.to belong_to(:project) }
- it { is_expected.to have_many(:serverless_domain_clusters) }
end
describe '.for_project' do
@@ -546,44 +545,6 @@ RSpec.describe PagesDomain do
end
end
- describe '#pages_virtual_domain' do
- let(:project) { create(:project) }
- let(:pages_domain) { create(:pages_domain, project: project) }
-
- context 'when there are no pages deployed for the project' do
- it 'returns nil' do
- expect(pages_domain.pages_virtual_domain).to be_nil
- end
- end
-
- context 'when there are pages deployed for the project' do
- let(:virtual_domain) { pages_domain.pages_virtual_domain }
-
- before do
- project.mark_pages_as_deployed
- project.update_pages_deployment!(create(:pages_deployment, project: project))
- end
-
- it 'returns the virual domain when there are pages deployed for the project' do
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- expect(virtual_domain.cache_key).to match(/pages_domain_for_domain_#{pages_domain.id}_/)
- end
-
- context 'when :cache_pages_domain_api is disabled' do
- before do
- stub_feature_flags(cache_pages_domain_api: false)
- end
-
- it 'returns the virual domain when there are pages deployed for the project' do
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- expect(virtual_domain.cache_key).to be_nil
- end
- end
- end
- end
-
describe '#validate_custom_domain_count_per_project' do
let_it_be(:project) { create(:project) }
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 2320ff669d0..bd6a7c156c4 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PersonalAccessToken, feature_category: :authentication_and_authorization do
+RSpec.describe PersonalAccessToken, feature_category: :system_access do
subject { described_class }
describe '.build' do
@@ -267,6 +267,41 @@ RSpec.describe PersonalAccessToken, feature_category: :authentication_and_author
expect(personal_access_token).not_to be_valid
expect(personal_access_token.errors[:scopes].first).to eq "can only contain available scopes"
end
+
+ context 'validates expires_at' do
+ let(:max_expiration_date) { described_class::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now }
+
+ context 'when default_pat_expiration feature flag is true' do
+ context 'when expires_in is less than MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS days' do
+ it 'is valid' do
+ personal_access_token.expires_at = max_expiration_date - 1.day
+
+ expect(personal_access_token).to be_valid
+ end
+ end
+
+ context 'when expires_in is more than MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS days' do
+ it 'is invalid' do
+ personal_access_token.expires_at = max_expiration_date + 1.day
+
+ expect(personal_access_token).not_to be_valid
+ expect(personal_access_token.errors[:expires_at].first).to eq('must expire in 365 days')
+ end
+ end
+ end
+
+ context 'when default_pat_expiration feature flag is false' do
+ before do
+ stub_feature_flags(default_pat_expiration: false)
+ end
+
+ it 'allows any expires_at value' do
+ personal_access_token.expires_at = max_expiration_date + 1.day
+
+ expect(personal_access_token).to be_valid
+ end
+ end
+ end
end
describe 'scopes' do
@@ -289,7 +324,7 @@ RSpec.describe PersonalAccessToken, feature_category: :authentication_and_author
let_it_be(:revoked_token) { create(:personal_access_token, revoked: true) }
let_it_be(:valid_token_and_notified) { create(:personal_access_token, expires_at: 2.days.from_now, expire_notification_delivered: true) }
let_it_be(:valid_token) { create(:personal_access_token, expires_at: 2.days.from_now) }
- let_it_be(:long_expiry_token) { create(:personal_access_token, expires_at: '999999-12-31'.to_date) }
+ let_it_be(:long_expiry_token) { create(:personal_access_token, expires_at: described_class::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now) }
context 'in one day' do
it "doesn't have any tokens" do
@@ -405,4 +440,58 @@ RSpec.describe PersonalAccessToken, feature_category: :authentication_and_author
end
end
end
+
+ describe 'token format' do
+ let(:personal_access_token) { described_class.new }
+
+ it 'generates a token' do
+ expect { personal_access_token.ensure_token }
+ .to change { personal_access_token.token }.from(nil).to(a_string_starting_with(described_class.token_prefix))
+ end
+
+ context 'when there is an existing token' do
+ let(:token) { 'an_existing_secret_token' }
+
+ before do
+ personal_access_token.set_token(token)
+ end
+
+ it 'does not change the existing token' do
+ expect { personal_access_token.ensure_token }
+ .not_to change { personal_access_token.token }.from(token)
+ end
+ end
+ end
+
+ describe '#expires_at=' do
+ let(:personal_access_token) { described_class.new }
+
+ context 'when default_pat_expiration feature flag is true' do
+ context 'expires_at set to empty value' do
+ [nil, ""].each do |expires_in_value|
+ it 'defaults to PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS' do
+ personal_access_token.expires_at = expires_in_value
+
+ freeze_time do
+ expect(personal_access_token.expires_at).to eq(
+ PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now.to_date
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context 'when default_pat_expiration feature flag is false' do
+ before do
+ stub_feature_flags(default_pat_expiration: false)
+ end
+
+ it 'does not set a default' do
+ personal_access_token.expires_at = nil
+
+ expect(personal_access_token.expires_at).to eq(nil)
+ end
+ end
+ end
end
diff --git a/spec/models/plan_limits_spec.rb b/spec/models/plan_limits_spec.rb
index 3705cab7ef5..962bb21d761 100644
--- a/spec/models/plan_limits_spec.rb
+++ b/spec/models/plan_limits_spec.rb
@@ -206,6 +206,8 @@ RSpec.describe PlanLimits do
]
end
+ # Remove ci_active_pipelines when db column is removed
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/408141
let(:columns_with_zero) do
%w[
ci_active_pipelines
@@ -221,6 +223,7 @@ RSpec.describe PlanLimits do
security_policy_scan_execution_schedules
enforcement_limit
notification_limit
+ project_access_token_limit
] + disabled_max_artifact_size_columns
end
diff --git a/spec/models/preloaders/labels_preloader_spec.rb b/spec/models/preloaders/labels_preloader_spec.rb
index 07f148a0a6c..3d2a5edc8f0 100644
--- a/spec/models/preloaders/labels_preloader_spec.rb
+++ b/spec/models/preloaders/labels_preloader_spec.rb
@@ -18,14 +18,24 @@ RSpec.describe Preloaders::LabelsPreloader do
context 'project labels' do
let_it_be(:projects) { create_list(:project, 3, :public, :repository) }
- let_it_be(:labels) { projects.each { |p| create(:label, project: p) } }
+ let_it_be(:labels) { projects.map { |p| create(:label, project: p) } }
it_behaves_like 'an efficient database query'
+
+ it 'preloads the max access level', :request_store do
+ labels_with_preloaded_data
+
+ query_count = ActiveRecord::QueryRecorder.new do
+ projects.first.team.max_member_access_for_user_ids([user.id])
+ end.count
+
+ expect(query_count).to eq(0)
+ end
end
context 'group labels' do
let_it_be(:groups) { create_list(:group, 3) }
- let_it_be(:labels) { groups.each { |g| create(:group_label, group: g) } }
+ let_it_be(:labels) { groups.map { |g| create(:group_label, group: g) } }
it_behaves_like 'an efficient database query'
end
diff --git a/spec/models/preloaders/runner_manager_policy_preloader_spec.rb b/spec/models/preloaders/runner_manager_policy_preloader_spec.rb
new file mode 100644
index 00000000000..1977e2c5787
--- /dev/null
+++ b/spec/models/preloaders/runner_manager_policy_preloader_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Preloaders::RunnerManagerPolicyPreloader, feature_category: :runner_fleet do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:runner1) { create(:ci_runner) }
+ let_it_be(:runner2) { create(:ci_runner) }
+ let_it_be(:runner_manager1) { create(:ci_runner_machine, runner: runner1) }
+ let_it_be(:runner_manager2) { create(:ci_runner_machine, runner: runner2) }
+
+ let(:base_runner_managers) do
+ Project.where(id: [runner_manager1, runner_manager2])
+ end
+
+ it 'avoids N+1 queries when authorizing a list of runner managers', :request_store do
+ preload_runner_managers_for_policy(user)
+ control = ActiveRecord::QueryRecorder.new { authorize_all_runner_managers(user) }
+
+ new_runner1 = create(:ci_runner)
+ new_runner2 = create(:ci_runner)
+ new_runner_manager1 = create(:ci_runner_machine, runner: new_runner1)
+ new_runner_manager2 = create(:ci_runner_machine, runner: new_runner2)
+
+ pristine_runner_managers = Project.where(id: base_runner_managers + [new_runner_manager1, new_runner_manager2])
+
+ preload_runner_managers_for_policy(user, pristine_runner_managers)
+ expect { authorize_all_runner_managers(user, pristine_runner_managers) }.not_to exceed_query_limit(control)
+ end
+
+ def authorize_all_runner_managers(current_user, runner_manager_list = base_runner_managers)
+ runner_manager_list.each { |runner_manager| current_user.can?(:read_runner_manager, runner_manager) }
+ end
+
+ def preload_runner_managers_for_policy(current_user, runner_manager_list = base_runner_managers)
+ described_class.new(runner_manager_list, current_user).execute
+ end
+end
diff --git a/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb b/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
index 7d04817b621..3fba2ac003b 100644
--- a/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
+++ b/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
@@ -66,22 +66,6 @@ RSpec.describe Preloaders::UserMaxAccessLevelInGroupsPreloader do
create(:group_group_link, :guest, shared_with_group: group1, shared_group: group4)
end
- context 'when `include_memberships_from_group_shares_in_preloader` feature flag is disabled' do
- before do
- stub_feature_flags(include_memberships_from_group_shares_in_preloader: false)
- end
-
- it 'sets access_level to `NO_ACCESS` in cache for groups arising from group shares' do
- described_class.new(groups, user).execute
-
- groups.each do |group|
- cached_access_level = group.max_member_access_for_user(user)
-
- expect(cached_access_level).to eq(Gitlab::Access::NO_ACCESS)
- end
- end
- end
-
it 'sets the right access level in cache for groups arising from group shares' do
described_class.new(groups, user).execute
diff --git a/spec/models/preloaders/users_max_access_level_by_project_preloader_spec.rb b/spec/models/preloaders/users_max_access_level_by_project_preloader_spec.rb
new file mode 100644
index 00000000000..f5bc0c8c2f8
--- /dev/null
+++ b/spec/models/preloaders/users_max_access_level_by_project_preloader_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Preloaders::UsersMaxAccessLevelByProjectPreloader, feature_category: :projects do
+ let_it_be(:user_1) { create(:user) }
+ let_it_be(:user_2) { create(:user) }
+ let_it_be(:user_with_no_access) { create(:user) } # ensures we correctly cache NO_ACCESS
+
+ let_it_be(:project_1) { create(:project) }
+ let_it_be(:project_2) { create(:project) }
+ let_it_be(:project_3) { create(:project) }
+
+ before do
+ project_1.add_developer(user_1)
+ project_1.add_developer(user_2)
+
+ project_2.add_developer(user_1)
+ project_2.add_developer(user_2)
+
+ project_3.add_developer(user_1)
+ project_3.add_developer(user_2)
+ end
+
+ describe '#execute', :request_store do
+ let(:project_users) do
+ {
+ project_1 => [user_1, user_with_no_access],
+ project_2 => user_2
+ }
+ end
+
+ it 'avoids N+1 queries' do
+ control_input = project_users
+ control = ActiveRecord::QueryRecorder.new do
+ described_class.new(project_users: control_input).execute
+ end
+
+ sample_input = control_input.merge(project_3 => user_2)
+ sample = ActiveRecord::QueryRecorder.new do
+ described_class.new(project_users: sample_input).execute
+ end
+
+ expect(sample).not_to exceed_query_limit(control)
+ end
+
+ it 'preloads the max access level used by project policies' do
+ described_class.new(project_users: project_users).execute
+
+ policy_queries = ActiveRecord::QueryRecorder.new do
+ project_users.each do |project, users|
+ Array.wrap(users).each do |user|
+ user.can?(:read_project, project)
+ end
+ end
+ end
+
+ expect(policy_queries).not_to exceed_query_limit(0)
+ end
+ end
+end
diff --git a/spec/models/preloaders/users_max_access_level_in_projects_preloader_spec.rb b/spec/models/preloaders/users_max_access_level_in_projects_preloader_spec.rb
deleted file mode 100644
index 7ecb6bb9861..00000000000
--- a/spec/models/preloaders/users_max_access_level_in_projects_preloader_spec.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-RSpec.describe Preloaders::UsersMaxAccessLevelInProjectsPreloader do
- let_it_be(:user1) { create(:user) }
- let_it_be(:user2) { create(:user) }
-
- let_it_be(:project_1) { create(:project) }
- let_it_be(:project_2) { create(:project) }
- let_it_be(:project_3) { create(:project) }
-
- let(:projects) { [project_1, project_2, project_3] }
- let(:users) { [user1, user2] }
-
- before do
- project_1.add_developer(user1)
- project_1.add_developer(user2)
-
- project_2.add_developer(user1)
- project_2.add_developer(user2)
-
- project_3.add_developer(user1)
- project_3.add_developer(user2)
- end
-
- context 'preload maximum access level to avoid querying project_authorizations', :request_store do
- it 'avoids N+1 queries', :request_store do
- Preloaders::UsersMaxAccessLevelInProjectsPreloader.new(projects: projects, users: users).execute
-
- expect(count_queries).to eq(0)
- end
-
- it 'runs N queries without preloading' do
- query_count_without_preload = count_queries
-
- Preloaders::UsersMaxAccessLevelInProjectsPreloader.new(projects: projects, users: users).execute
- count_queries_with_preload = count_queries
-
- expect(count_queries_with_preload).to be < query_count_without_preload
- end
- end
-
- def count_queries
- ActiveRecord::QueryRecorder.new do
- projects.each do |project|
- user1.can?(:read_project, project)
- user2.can?(:read_project, project)
- end
- end.count
- end
-end
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 2c490c33747..0a818147bfc 100644
--- a/spec/models/project_ci_cd_setting_spec.rb
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -27,22 +27,8 @@ RSpec.describe ProjectCiCdSetting do
end
end
- describe '#set_default_for_inbound_job_token_scope_enabled' do
- context 'when feature flag ci_inbound_job_token_scope is enabled' do
- before do
- stub_feature_flags(ci_inbound_job_token_scope: true)
- end
-
- it { is_expected.to be_inbound_job_token_scope_enabled }
- end
-
- context 'when feature flag ci_inbound_job_token_scope is disabled' do
- before do
- stub_feature_flags(ci_inbound_job_token_scope: false)
- end
-
- it { is_expected.not_to be_inbound_job_token_scope_enabled }
- end
+ describe '#default_for_inbound_job_token_scope_enabled' do
+ it { is_expected.to be_inbound_job_token_scope_enabled }
end
describe '#default_git_depth' do
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index fe0b46c3117..87bfdd15773 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -288,7 +288,6 @@ RSpec.describe ProjectFeature, feature_category: :projects do
end
context 'sync packages_enabled' do
- # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
where(:initial_value, :new_value, :expected_result) do
ProjectFeature::DISABLED | ProjectFeature::DISABLED | false
ProjectFeature::DISABLED | ProjectFeature::ENABLED | true
@@ -300,7 +299,6 @@ RSpec.describe ProjectFeature, feature_category: :projects do
ProjectFeature::PUBLIC | ProjectFeature::ENABLED | true
ProjectFeature::PUBLIC | ProjectFeature::PUBLIC | true
end
- # rubocop:enable Lint/BinaryOperatorWithIdenticalOperands
with_them do
it 'set correct value' do
@@ -314,6 +312,40 @@ RSpec.describe ProjectFeature, feature_category: :projects do
end
end
+ describe '#public_packages?' do
+ let_it_be(:public_project) { create(:project, :public) }
+
+ context 'with packages config enabled' do
+ context 'when project is private' do
+ it 'returns false' do
+ expect(project.project_feature.public_packages?).to eq(false)
+ end
+
+ context 'with package_registry_access_level set to public' do
+ before do
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ end
+
+ it 'returns true' do
+ expect(project.project_feature.public_packages?).to eq(true)
+ end
+ end
+ end
+
+ context 'when project is public' do
+ it 'returns true' do
+ expect(public_project.project_feature.public_packages?).to eq(true)
+ end
+ end
+ end
+
+ it 'returns false if packages config is not enabled' do
+ stub_config(packages: { enabled: false })
+
+ expect(public_project.project_feature.public_packages?).to eq(false)
+ end
+ end
+
# rubocop:disable Gitlab/FeatureAvailableUsage
describe '#feature_available?' do
let(:features) { ProjectFeature::FEATURES }
diff --git a/spec/models/project_label_spec.rb b/spec/models/project_label_spec.rb
index f451c2905e6..ba9ea759c6a 100644
--- a/spec/models/project_label_spec.rb
+++ b/spec/models/project_label_spec.rb
@@ -119,4 +119,39 @@ RSpec.describe ProjectLabel do
end
end
end
+
+ describe '#preloaded_parent_container' do
+ let_it_be(:label) { create(:label) }
+
+ before do
+ label.reload # ensure associations are not loaded
+ end
+
+ context 'when project is loaded' do
+ it 'does not invoke a DB query' do
+ label.project
+
+ count = ActiveRecord::QueryRecorder.new { label.preloaded_parent_container }.count
+ expect(count).to eq(0)
+ expect(label.preloaded_parent_container).to eq(label.project)
+ end
+ end
+
+ context 'when parent_container is loaded' do
+ it 'does not invoke a DB query' do
+ label.parent_container
+
+ count = ActiveRecord::QueryRecorder.new { label.preloaded_parent_container }.count
+ expect(count).to eq(0)
+ expect(label.preloaded_parent_container).to eq(label.parent_container)
+ end
+ end
+
+ context 'when none of them are loaded' do
+ it 'invokes a DB query' do
+ count = ActiveRecord::QueryRecorder.new { label.preloaded_parent_container }.count
+ expect(count).to eq(1)
+ end
+ end
+ end
end
diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb
index feb5985818b..0a2ead0aa6b 100644
--- a/spec/models/project_setting_spec.rb
+++ b/spec/models/project_setting_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectSetting, type: :model do
+RSpec.describe ProjectSetting, type: :model, feature_category: :projects do
using RSpec::Parameterized::TableSyntax
it { is_expected.to belong_to(:project) }
@@ -39,6 +39,44 @@ RSpec.describe ProjectSetting, type: :model do
[nil, 'not_allowed', :invalid].each do |invalid_value|
it { is_expected.not_to allow_value([invalid_value]).for(:target_platforms) }
end
+
+ context "when pages_unique_domain is required", feature_category: :pages do
+ it "is not required if pages_unique_domain_enabled is false" do
+ project_setting = build(:project_setting, pages_unique_domain_enabled: false)
+
+ expect(project_setting).to be_valid
+ expect(project_setting.errors.full_messages).not_to include("Pages unique domain can't be blank")
+ end
+
+ it "is required when pages_unique_domain_enabled is true" do
+ project_setting = build(:project_setting, pages_unique_domain_enabled: true)
+
+ expect(project_setting).not_to be_valid
+ expect(project_setting.errors.full_messages).to include("Pages unique domain can't be blank")
+ end
+
+ it "is required if it is already saved in the database" do
+ project_setting = create(
+ :project_setting,
+ pages_unique_domain: "random-unique-domain-here",
+ pages_unique_domain_enabled: true
+ )
+
+ project_setting.pages_unique_domain = nil
+
+ expect(project_setting).not_to be_valid
+ expect(project_setting.errors.full_messages).to include("Pages unique domain can't be blank")
+ end
+ end
+
+ it "validates uniqueness of pages_unique_domain", feature_category: :pages do
+ create(:project_setting, pages_unique_domain: "random-unique-domain-here")
+
+ project_setting = build(:project_setting, pages_unique_domain: "random-unique-domain-here")
+
+ expect(project_setting).not_to be_valid
+ expect(project_setting.errors.full_messages).to include("Pages unique domain has already been taken")
+ end
end
describe 'target_platforms=' do
@@ -169,4 +207,34 @@ RSpec.describe ProjectSetting, type: :model do
end
end
end
+
+ describe '#runner_registration_enabled' do
+ let_it_be(:settings) { create(:project_setting) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, project_setting: settings, group: group) }
+
+ it 'returns true' do
+ expect(project.runner_registration_enabled).to eq true
+ end
+
+ context 'when project has runner registration disabled' do
+ before do
+ project.update!(runner_registration_enabled: false)
+ end
+
+ it 'returns false' do
+ expect(project.runner_registration_enabled).to eq false
+ end
+ end
+
+ context 'when all projects have runner registration disabled' do
+ before do
+ stub_application_setting(valid_runner_registrars: ['group'])
+ end
+
+ it 'returns false' do
+ expect(project.runner_registration_enabled).to eq false
+ end
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index dfc8919e19d..e9bb01f4b23 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -14,6 +14,8 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it_behaves_like 'having unique enum values'
+ it_behaves_like 'ensures runners_token is prefixed', :project
+
describe 'associations' do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:namespace) }
@@ -25,6 +27,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to have_many(:merge_requests) }
it { is_expected.to have_many(:merge_request_metrics).class_name('MergeRequest::Metrics') }
it { is_expected.to have_many(:issues) }
+ it { is_expected.to have_many(:work_items) }
it { is_expected.to have_many(:incident_management_issuable_escalation_statuses).through(:issues).inverse_of(:project).class_name('IncidentManagement::IssuableEscalationStatus') }
it { is_expected.to have_many(:milestones) }
it { is_expected.to have_many(:project_members).dependent(:delete_all) }
@@ -40,7 +43,9 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to have_many(:protected_branches) }
it { is_expected.to have_many(:exported_protected_branches) }
it { is_expected.to have_one(:wiki_repository).class_name('Projects::WikiRepository').inverse_of(:project) }
+ it { is_expected.to have_one(:design_management_repository).class_name('DesignManagement::Repository').inverse_of(:project) }
it { is_expected.to have_one(:slack_integration) }
+ it { is_expected.to have_one(:catalog_resource) }
it { is_expected.to have_one(:microsoft_teams_integration) }
it { is_expected.to have_one(:mattermost_integration) }
it { is_expected.to have_one(:hangouts_chat_integration) }
@@ -50,6 +55,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to have_one(:packagist_integration) }
it { is_expected.to have_one(:pushover_integration) }
it { is_expected.to have_one(:apple_app_store_integration) }
+ it { is_expected.to have_one(:google_play_integration) }
it { is_expected.to have_one(:asana_integration) }
it { is_expected.to have_many(:boards) }
it { is_expected.to have_one(:campfire_integration) }
@@ -88,6 +94,8 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to have_one(:alerting_setting).class_name('Alerting::ProjectAlertingSetting') }
it { is_expected.to have_one(:mock_ci_integration) }
it { is_expected.to have_one(:mock_monitoring_integration) }
+ it { is_expected.to have_one(:service_desk_custom_email_verification).class_name('ServiceDesk::CustomEmailVerification') }
+ it { is_expected.to have_one(:container_registry_data_repair_detail).class_name('ContainerRegistry::DataRepairDetail') }
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:ci_pipelines) }
it { is_expected.to have_many(:ci_refs) }
@@ -135,7 +143,9 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to have_many(:reviews).inverse_of(:project) }
it { is_expected.to have_many(:packages).class_name('Packages::Package') }
it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile') }
+ it { is_expected.to have_many(:rpm_repository_files).class_name('Packages::Rpm::RepositoryFile').inverse_of(:project).dependent(:destroy) }
it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::ProjectDistribution').dependent(:destroy) }
+ it { is_expected.to have_many(:npm_metadata_caches).class_name('Packages::Npm::MetadataCache') }
it { is_expected.to have_one(:packages_cleanup_policy).class_name('Packages::Cleanup::Policy').inverse_of(:project) }
it { is_expected.to have_many(:pipeline_artifacts).dependent(:restrict_with_error) }
it { is_expected.to have_many(:terraform_states).class_name('Terraform::State').inverse_of(:project) }
@@ -780,7 +790,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
let(:new_project) do
build(:project,
- name: project_pending_deletion.name,
+ path: project_pending_deletion.path,
namespace: project_pending_deletion.namespace)
end
@@ -874,46 +884,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- describe '#has_packages?' do
- let_it_be(:project) { create(:project, :public) }
-
- subject { project.has_packages?(package_type) }
-
- shared_examples 'returning true examples' do
- let!(:package) { create("#{package_type}_package", project: project) }
-
- it { is_expected.to be true }
- end
-
- shared_examples 'returning false examples' do
- it { is_expected.to be false }
- end
-
- context 'with maven packages' do
- it_behaves_like 'returning true examples' do
- let(:package_type) { :maven }
- end
- end
-
- context 'with npm packages' do
- it_behaves_like 'returning true examples' do
- let(:package_type) { :npm }
- end
- end
-
- context 'with conan packages' do
- it_behaves_like 'returning true examples' do
- let(:package_type) { :conan }
- end
- end
-
- context 'with no package type' do
- it_behaves_like 'returning false examples' do
- let(:package_type) { nil }
- end
- end
- end
-
describe '#ci_pipelines' do
let_it_be(:project) { create(:project) }
@@ -939,6 +909,17 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#commit_notes' do
+ let_it_be(:project) { create(:project) }
+
+ it "returns project's commit notes" do
+ note_1 = create(:note_on_commit, project: project, commit_id: 'commit_id_1')
+ note_2 = create(:note_on_commit, project: project, commit_id: 'commit_id_2')
+
+ expect(project.commit_notes).to match_array([note_1, note_2])
+ end
+ end
+
describe '#personal_namespace_holder?' do
let_it_be(:group) { create(:group) }
let_it_be(:namespace_user) { create(:user) }
@@ -1115,7 +1096,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
'restrict_user_defined_variables' => '',
'runner_token_expiration_interval' => '',
'separated_caches' => 'ci_',
- 'opt_in_jwt' => 'ci_',
'allow_fork_pipelines_to_run_in_parent_project' => 'ci_',
'inbound_job_token_scope_enabled' => 'ci_',
'job_token_scope_enabled' => 'ci_outbound_'
@@ -1151,7 +1131,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
describe '#ci_inbound_job_token_scope_enabled?' do
- it_behaves_like 'a ci_cd_settings predicate method', prefix: 'ci_' do
+ it_behaves_like 'a ci_cd_settings predicate method', prefix: 'ci_', default: true do
let(:delegated_method) { :inbound_job_token_scope_enabled? }
end
end
@@ -1380,6 +1360,60 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#to_reference_base' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_namespace) { user.namespace }
+
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent) }
+ let_it_be(:another_group) { create(:group) }
+
+ let_it_be(:project1) { create(:project, namespace: group) }
+ let_it_be(:project_namespace) { project1.project_namespace }
+
+ # different project same group
+ let_it_be(:project2) { create(:project, namespace: group) }
+ let_it_be(:project_namespace2) { project2.project_namespace }
+
+ # different project from different group
+ let_it_be(:project3) { create(:project) }
+ let_it_be(:project_namespace3) { project3.project_namespace }
+
+ # testing references with namespace being: group, project namespace and user namespace
+ where(:project, :full, :from, :result) do
+ ref(:project1) | false | nil | nil
+ ref(:project1) | true | nil | lazy { project.full_path }
+ ref(:project1) | false | ref(:group) | lazy { project.path }
+ ref(:project1) | true | ref(:group) | lazy { project.full_path }
+ ref(:project1) | false | ref(:parent) | lazy { project.full_path }
+ ref(:project1) | true | ref(:parent) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project1) | nil
+ ref(:project1) | true | ref(:project1) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project_namespace) | nil
+ ref(:project1) | true | ref(:project_namespace) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project2) | lazy { project.path }
+ ref(:project1) | true | ref(:project2) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project_namespace2) | lazy { project.path }
+ ref(:project1) | true | ref(:project_namespace2) | lazy { project.full_path }
+ ref(:project1) | false | ref(:another_group) | lazy { project.full_path }
+ ref(:project1) | true | ref(:another_group) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project3) | lazy { project.full_path }
+ ref(:project1) | true | ref(:project3) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project_namespace3) | lazy { project.full_path }
+ ref(:project1) | true | ref(:project_namespace3) | lazy { project.full_path }
+ ref(:project1) | false | ref(:user_namespace) | lazy { project.full_path }
+ ref(:project1) | true | ref(:user_namespace) | lazy { project.full_path }
+ end
+
+ with_them do
+ it 'returns correct path' do
+ expect(project.to_reference_base(from, full: full)).to eq(result)
+ end
+ end
+ end
+
describe '#merge_method' do
where(:ff, :rebase, :method) do
true | true | :ff
@@ -1619,7 +1653,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
context 'with namespace' do
before do
@group = create(:group, name: 'gitlab')
- @project = create(:project, name: 'gitlabhq', namespace: @group)
+ @project = create(:project, path: 'gitlabhq', namespace: @group)
end
it { expect(@project.to_param).to eq('gitlabhq') }
@@ -2213,8 +2247,8 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
subject(:project) { build(:project, :private, namespace: namespace, service_desk_enabled: true) }
before do
- allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
- allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?).and_return(true)
+ allow(Gitlab::Email::IncomingEmail).to receive(:enabled?).and_return(true)
+ allow(Gitlab::Email::IncomingEmail).to receive(:supports_wildcard?).and_return(true)
end
it 'is enabled' do
@@ -2254,7 +2288,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
context 'when service_desk_email is disabled' do
before do
- allow(::Gitlab::ServiceDeskEmail).to receive(:enabled?).and_return(false)
+ allow(::Gitlab::Email::ServiceDeskEmail).to receive(:enabled?).and_return(false)
end
it_behaves_like 'with incoming email address'
@@ -2263,7 +2297,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
context 'when service_desk_email is enabled' do
before do
config = double(enabled: true, address: 'foo+%{key}@bar.com')
- allow(::Gitlab::ServiceDeskEmail).to receive(:config).and_return(config)
+ allow(::Gitlab::Email::ServiceDeskEmail).to receive(:config).and_return(config)
end
context 'when project_key is set' do
@@ -2666,7 +2700,11 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
describe '#pages_url', feature_category: :pages do
+ let(:group_name) { 'group' }
+ let(:project_name) { 'project' }
+
let(:group) { create(:group, name: group_name) }
+ let(:nested_group) { create(:group, parent: group) }
let(:project_path) { project_name.downcase }
let(:project) do
@@ -2689,101 +2727,168 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
.and_return(['http://example.com', port].compact.join(':'))
end
- context 'group page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'group.example.com' }
-
- it { is_expected.to eq("http://group.example.com") }
+ context 'when not using pages_unique_domain' do
+ subject { project.pages_url(with_unique_domain: false) }
- context 'mixed case path' do
- let(:project_path) { 'Group.example.com' }
+ context 'when pages_unique_domain feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: false)
+ end
- it { is_expected.to eq("http://group.example.com") }
+ it { is_expected.to eq('http://group.example.com/project') }
end
- end
- context 'project page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'Project' }
+ context 'when pages_unique_domain feature flag is enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
- it { is_expected.to eq("http://group.example.com/project") }
+ project.project_setting.update!(
+ pages_unique_domain_enabled: pages_unique_domain_enabled,
+ pages_unique_domain: 'unique-domain'
+ )
+ end
- context 'mixed case path' do
- let(:project_path) { 'Project' }
+ context 'when pages_unique_domain_enabled is false' do
+ let(:pages_unique_domain_enabled) { false }
+
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
- it { is_expected.to eq("http://group.example.com/Project") }
+ context 'when pages_unique_domain_enabled is true' do
+ let(:pages_unique_domain_enabled) { true }
+
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
end
end
- context 'when there is an explicit port' do
- let(:port) { 3000 }
+ context 'when using pages_unique_domain' do
+ subject { project.pages_url(with_unique_domain: true) }
- context 'when not in dev mode' do
+ context 'when pages_unique_domain feature flag is disabled' do
before do
- stub_rails_env('production')
+ stub_feature_flags(pages_unique_domain: false)
end
- context 'group page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'group.example.com' }
-
- it { is_expected.to eq('http://group.example.com:3000/group.example.com') }
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Group.example.com' }
+ context 'when pages_unique_domain feature flag is enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
- it { is_expected.to eq('http://group.example.com:3000/Group.example.com') }
- end
+ project.project_setting.update!(
+ pages_unique_domain_enabled: pages_unique_domain_enabled,
+ pages_unique_domain: 'unique-domain'
+ )
end
- context 'project page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'Project' }
+ context 'when pages_unique_domain_enabled is false' do
+ let(:pages_unique_domain_enabled) { false }
- it { is_expected.to eq("http://group.example.com:3000/project") }
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Project' }
+ context 'when pages_unique_domain_enabled is true' do
+ let(:pages_unique_domain_enabled) { true }
- it { is_expected.to eq("http://group.example.com:3000/Project") }
- end
+ it { is_expected.to eq('http://unique-domain.example.com') }
end
end
+ end
- context 'when in dev mode' do
- before do
- stub_rails_env('development')
- end
+ context 'with nested group' do
+ let(:project) { create(:project, namespace: nested_group, name: project_name) }
+ let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" }
- context 'group page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'group.example.com' }
+ context 'group page' do
+ let(:project_name) { 'group.example.com' }
- it { is_expected.to eq('http://group.example.com:3000') }
+ it { is_expected.to eq(expected_url) }
+ end
+
+ context 'project page' do
+ let(:project_name) { 'Project' }
- context 'mixed case path' do
- let(:project_path) { 'Group.example.com' }
+ it { is_expected.to eq(expected_url) }
+ end
+ end
- it { is_expected.to eq('http://group.example.com:3000') }
- end
- end
+ context 'when the project matches its namespace url' do
+ let(:project_name) { 'group.example.com' }
+
+ it { is_expected.to eq('http://group.example.com') }
- context 'project page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'Project' }
+ context 'with different group name capitalization' do
+ let(:group_name) { 'Group' }
- it { is_expected.to eq("http://group.example.com:3000/project") }
+ it { is_expected.to eq("http://group.example.com") }
+ end
+
+ context 'with different project path capitalization' do
+ let(:project_path) { 'Group.example.com' }
+
+ it { is_expected.to eq("http://group.example.com") }
+ end
+
+ context 'with different project name capitalization' do
+ let(:project_name) { 'Project' }
- context 'mixed case path' do
- let(:project_path) { 'Project' }
+ it { is_expected.to eq("http://group.example.com/project") }
+ end
+
+ context 'when there is an explicit port' do
+ let(:port) { 3000 }
- it { is_expected.to eq("http://group.example.com:3000/Project") }
+ context 'when not in dev mode' do
+ before do
+ stub_rails_env('production')
end
+
+ it { is_expected.to eq('http://group.example.com:3000/group.example.com') }
+ end
+
+ context 'when in dev mode' do
+ before do
+ stub_rails_env('development')
+ end
+
+ it { is_expected.to eq('http://group.example.com:3000') }
end
end
end
end
+ describe '#pages_unique_url', feature_category: :pages do
+ let(:project_settings) { create(:project_setting, pages_unique_domain: 'unique-domain') }
+ let(:project) { build(:project, project_setting: project_settings) }
+ let(:domain) { 'example.com' }
+
+ before do
+ allow(Settings.pages).to receive(:host).and_return(domain)
+ allow(Gitlab.config.pages).to receive(:url).and_return("http://#{domain}")
+ end
+
+ it 'returns the pages unique url' do
+ expect(project.pages_unique_url).to eq('http://unique-domain.example.com')
+ end
+ end
+
+ describe '#pages_unique_host', feature_category: :pages do
+ let(:project_settings) { create(:project_setting, pages_unique_domain: 'unique-domain') }
+ let(:project) { build(:project, project_setting: project_settings) }
+ let(:domain) { 'example.com' }
+
+ before do
+ allow(Settings.pages).to receive(:host).and_return(domain)
+ allow(Gitlab.config.pages).to receive(:url).and_return("http://#{domain}")
+ end
+
+ it 'returns the pages unique url' do
+ expect(project.pages_unique_host).to eq('unique-domain.example.com')
+ end
+ end
+
describe '#pages_namespace_url', feature_category: :pages do
let(:group) { create(:group, name: group_name) }
let(:project) { create(:project, namespace: group, name: project_name) }
@@ -3030,6 +3135,12 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
expect(project.create_repository).to eq(false)
expect(project.errors).not_to be_empty
end
+
+ it 'passes through default branch' do
+ expect(project.repository).to receive(:create_repository).with('pineapple')
+
+ expect(project.create_repository(default_branch: 'pineapple')).to eq(true)
+ end
end
context 'using a forked repository' do
@@ -3565,6 +3676,44 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#beautified_import_status_name' do
+ context 'when import not finished' do
+ it 'returns the right beautified import status' do
+ project = create(:project, :import_started)
+
+ expect(project.beautified_import_status_name).to eq('started')
+ end
+ end
+
+ context 'when import is finished' do
+ context 'when import is partially completed' do
+ it 'returns partially completed' do
+ project = create(:project)
+
+ create(:import_state, project: project, status: 'finished', checksums: {
+ 'fetched' => { 'labels' => 10 },
+ 'imported' => { 'labels' => 9 }
+ })
+
+ expect(project.beautified_import_status_name).to eq('partially completed')
+ end
+ end
+
+ context 'when import is fully completed' do
+ it 'returns completed' do
+ project = create(:project)
+
+ create(:import_state, project: project, status: 'finished', checksums: {
+ 'fetched' => { 'labels' => 10 },
+ 'imported' => { 'labels' => 10 }
+ })
+
+ expect(project.beautified_import_status_name).to eq('completed')
+ end
+ end
+ end
+ end
+
describe '#add_import_job' do
let(:import_jid) { '123' }
@@ -3843,21 +3992,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
describe '#ancestors' do
- context 'with linear_project_ancestors feature flag enabled' do
- before do
- stub_feature_flags(linear_project_ancestors: true)
- end
-
- include_examples '#ancestors'
- end
-
- context 'with linear_project_ancestors feature flag disabled' do
- before do
- stub_feature_flags(linear_project_ancestors: false)
- end
-
- include_examples '#ancestors'
- end
+ include_examples '#ancestors'
end
describe '#ancestors_upto' do
@@ -4643,52 +4778,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- describe '#pages_url' do
- let(:group) { create(:group, name: 'Group') }
- let(:nested_group) { create(:group, parent: group) }
- let(:domain) { 'Example.com' }
-
- subject { project.pages_url }
-
- before do
- allow(Settings.pages).to receive(:host).and_return(domain)
- allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com')
- end
-
- context 'top-level group' do
- let(:project) { create(:project, namespace: group, name: project_name) }
-
- context 'group page' do
- let(:project_name) { 'group.example.com' }
-
- it { is_expected.to eq("http://group.example.com") }
- end
-
- context 'project page' do
- let(:project_name) { 'Project' }
-
- it { is_expected.to eq("http://group.example.com/project") }
- end
- end
-
- context 'nested group' do
- let(:project) { create(:project, namespace: nested_group, name: project_name) }
- let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" }
-
- context 'group page' do
- let(:project_name) { 'group.example.com' }
-
- it { is_expected.to eq(expected_url) }
- end
-
- context 'project page' do
- let(:project_name) { 'Project' }
-
- it { is_expected.to eq(expected_url) }
- end
- end
- end
-
describe '#lfs_http_url_to_repo' do
let(:project) { create(:project) }
@@ -5740,8 +5829,19 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
let_it_be(:project) { create(:project) }
it 'exposes API v4 URL' do
- expect(project.api_variables.first[:key]).to eq 'CI_API_V4_URL'
- expect(project.api_variables.first[:value]).to include '/api/v4'
+ v4_variable = project.api_variables.find { |variable| variable[:key] == "CI_API_V4_URL" }
+
+ expect(v4_variable).not_to be_nil
+ expect(v4_variable[:key]).to eq 'CI_API_V4_URL'
+ expect(v4_variable[:value]).to end_with '/api/v4'
+ end
+
+ it 'exposes API GraphQL URL' do
+ graphql_variable = project.api_variables.find { |variable| variable[:key] == "CI_API_GRAPHQL_URL" }
+
+ expect(graphql_variable).not_to be_nil
+ expect(graphql_variable[:key]).to eq 'CI_API_GRAPHQL_URL'
+ expect(graphql_variable[:value]).to end_with '/api/graphql'
end
it 'contains a URL variable for every supported API version' do
@@ -5756,7 +5856,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
expect(project.api_variables.map { |variable| variable[:key] })
- .to contain_exactly(*required_variables)
+ .to include(*required_variables)
end
end
@@ -5854,7 +5954,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
expect(project).to receive(:after_create_default_branch)
expect(project).to receive(:refresh_markdown_cache!)
expect(InternalId).to receive(:flush_records!).with(project: project)
- expect(ProjectCacheWorker).to receive(:perform_async).with(project.id, [], [:repository_size])
+ expect(ProjectCacheWorker).to receive(:perform_async).with(project.id, [], [:repository_size, :wiki_size])
expect(DetectRepositoryLanguagesWorker).to receive(:perform_async).with(project.id)
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:perform_async).with(project.id)
expect(project).to receive(:set_full_path)
@@ -6049,7 +6149,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it 'executes hooks which were backed off and are no longer backed off' do
project = create(:project)
hook = create(:project_hook, project: project, push_events: true)
- WebHook::FAILURE_THRESHOLD.succ.times { hook.backoff! }
+ WebHooks::AutoDisabling::FAILURE_THRESHOLD.succ.times { hook.backoff! }
expect_any_instance_of(ProjectHook).to receive(:async_execute).once
@@ -6701,6 +6801,19 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '.pending_data_repair_analysis' do
+ it 'returns projects that are not in ContainerRegistry::DataRepairDetail' do
+ project_1 = create(:project)
+ project_2 = create(:project)
+
+ expect(described_class.pending_data_repair_analysis).to match_array([project_1, project_2])
+
+ create(:container_registry_data_repair_detail, project: project_1)
+
+ expect(described_class.pending_data_repair_analysis).to match_array([project_2])
+ end
+ end
+
describe '.deployments' do
subject { project.deployments }
@@ -7305,20 +7418,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- describe 'with integrations and chat names' do
- subject { create(:project) }
-
- let(:integration) { create(:integration, project: subject) }
-
- before do
- create_list(:chat_name, 5, integration: integration)
- end
-
- it 'does not remove chat names on removal' do
- expect { subject.destroy! }.not_to change { ChatName.count }
- end
- end
-
describe 'with_issues_or_mrs_available_for_user' do
before do
Project.delete_all
@@ -7380,6 +7479,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
context 'when feature flag `group_protected_branches` enabled' do
before do
stub_feature_flags(group_protected_branches: true)
+ stub_feature_flags(allow_protected_branches_for_group: true)
end
it 'return all protected branches' do
@@ -7390,6 +7490,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
context 'when feature flag `group_protected_branches` disabled' do
before do
stub_feature_flags(group_protected_branches: false)
+ stub_feature_flags(allow_protected_branches_for_group: false)
end
it 'return only project-level protected branches' do
@@ -7463,24 +7564,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- describe '#self_monitoring?' do
- let_it_be(:project) { create(:project) }
-
- subject { project.self_monitoring? }
-
- context 'when the project is instance self-monitoring' do
- before do
- stub_application_setting(self_monitoring_project_id: project.id)
- end
-
- it { is_expected.to be true }
- end
-
- context 'when the project is not self-monitoring' do
- it { is_expected.to be false }
- end
- end
-
describe '#add_export_job' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
@@ -7578,48 +7661,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- describe '#has_packages?' do
- let(:project) { create(:project, :public) }
-
- subject { project.has_packages?(package_type) }
-
- shared_examples 'has_package' do
- context 'package of package_type exists' do
- let!(:package) { create("#{package_type}_package", project: project) }
-
- it { is_expected.to be true }
- end
-
- context 'package of package_type does not exist' do
- it { is_expected.to be false }
- end
- end
-
- context 'with maven packages' do
- it_behaves_like 'has_package' do
- let(:package_type) { :maven }
- end
- end
-
- context 'with npm packages' do
- it_behaves_like 'has_package' do
- let(:package_type) { :npm }
- end
- end
-
- context 'with conan packages' do
- it_behaves_like 'has_package' do
- let(:package_type) { :conan }
- end
- end
-
- context 'calling has_package? with nil' do
- let(:package_type) { nil }
-
- it { is_expected.to be false }
- end
- end
-
describe 'with Debian Distributions' do
subject { create(:project) }
@@ -7740,6 +7781,29 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#has_namespaced_npm_packages?' do
+ let_it_be(:namespace) { create(:namespace, path: 'test') }
+ let_it_be(:project) { create(:project, :public, namespace: namespace) }
+
+ subject { project.has_namespaced_npm_packages? }
+
+ context 'with scope of the namespace path' do
+ let_it_be(:package) { create(:npm_package, project: project, name: "@#{namespace.path}/foo") }
+
+ it { is_expected.to be true }
+ end
+
+ context 'without scope of the namespace path' do
+ let_it_be(:package) { create(:npm_package, project: project, name: "@someotherscope/foo") }
+
+ it { is_expected.to be false }
+ end
+
+ context 'without packages' do
+ it { is_expected.to be false }
+ end
+ end
+
describe '#package_already_taken?' do
let_it_be(:namespace) { create(:namespace, path: 'test') }
let_it_be(:project) { create(:project, :public, namespace: namespace) }
@@ -7913,7 +7977,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
describe '#activity_path' do
it 'returns the project activity_path' do
- expected_path = "/#{project.namespace.path}/#{project.name}/activity"
+ expected_path = "/#{project.full_path}/activity"
expect(project.activity_path).to eq(expected_path)
end
@@ -7952,8 +8016,8 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
using RSpec::Parameterized::TableSyntax
where(:topic_list, :expected_result) do
- ['topicA', 'topicB'] | %w[topicA topicB] # rubocop:disable Style/WordArray, Lint/BinaryOperatorWithIdenticalOperands
- ['topicB', 'topicA'] | %w[topicB topicA] # rubocop:disable Style/WordArray, Lint/BinaryOperatorWithIdenticalOperands
+ ['topicA', 'topicB'] | %w[topicA topicB] # rubocop:disable Style/WordArray
+ ['topicB', 'topicA'] | %w[topicB topicA] # rubocop:disable Style/WordArray
[' topicC ', ' topicD '] | %w[topicC topicD]
['topicE', 'topicF', 'topicE'] | %w[topicE topicF] # rubocop:disable Style/WordArray
['topicE ', 'topicF', ' topicE'] | %w[topicE topicF]
@@ -8028,7 +8092,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
using RSpec::Parameterized::TableSyntax
- # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
where(:initial_visibility, :new_visibility, :new_topic_list, :expected_count_changes) do
ref(:private) | nil | 't2, t3' | [0, 0, 0]
ref(:internal) | nil | 't2, t3' | [-1, 0, 1]
@@ -8052,7 +8115,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
ref(:public) | ref(:internal) | 't2, t3' | [-1, 0, 1]
ref(:public) | ref(:private) | 't2, t3' | [-1, -1, 0]
end
- # rubocop:enable Lint/BinaryOperatorWithIdenticalOperands
with_them do
it 'increments or decrements counters of topics' do
@@ -8519,6 +8581,16 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#content_editor_on_issues_feature_flag_enabled?' do
+ let_it_be(:group_project) { create(:project, :in_subgroup) }
+
+ it_behaves_like 'checks parent group feature flag' do
+ let(:feature_flag_method) { :content_editor_on_issues_feature_flag_enabled? }
+ let(:feature_flag) { :content_editor_on_issues }
+ let(:subject_project) { group_project }
+ end
+ end
+
describe '#work_items_mvc_feature_flag_enabled?' do
let_it_be(:group_project) { create(:project, :in_subgroup) }
@@ -8842,6 +8914,32 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe 'deprecated project attributes' do
+ where(:project_attr, :project_method, :project_feature_attr) do
+ :wiki_enabled | :wiki_enabled? | :wiki_access_level
+ :builds_enabled | :builds_enabled? | :builds_access_level
+ :merge_requests_enabled | :merge_requests_enabled? | :merge_requests_access_level
+ :issues_enabled | :issues_enabled? | :issues_access_level
+ :snippets_enabled | :snippets_enabled? | :snippets_access_level
+ end
+
+ with_them do
+ it 'delegates the attributes to project feature' do
+ project = Project.new(project_attr => false)
+
+ expect(project.public_send(project_method)).to eq(false)
+ expect(project.project_feature.public_send(project_feature_attr)).to eq(ProjectFeature::DISABLED)
+ end
+
+ it 'sets the default value' do
+ project = Project.new
+
+ expect(project.public_send(project_method)).to eq(true)
+ expect(project.project_feature.public_send(project_feature_attr)).to eq(ProjectFeature::ENABLED)
+ end
+ end
+ end
+
private
def finish_job(export_job)
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index c04fc70deca..efa5403dad9 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectWiki do
+RSpec.describe ProjectWiki, feature_category: :wiki do
it_behaves_like 'wiki model' do
let(:wiki_container) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_container_without_repo) { create(:project, namespace: user.namespace) }
@@ -18,6 +18,33 @@ RSpec.describe ProjectWiki do
end
end
+ describe '#create_wiki_repository' do
+ context 'when a project_wiki_repositories record does not exist' do
+ let_it_be(:wiki_container) { create(:project) }
+
+ it 'creates a new record' do
+ expect { subject.create_wiki_repository }.to change { wiki_container.wiki_repository }
+ .from(nil).to(kind_of(Projects::WikiRepository))
+ end
+
+ context 'on a read-only instance' do
+ before do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ end
+
+ it 'does not attempt to create a new record' do
+ expect { subject.create_wiki_repository }.not_to change { wiki_container.wiki_repository }
+ end
+ end
+ end
+
+ context 'when a project_wiki_repositories record exists' do
+ it 'does not create a new record in the database' do
+ expect { subject.create_wiki_repository }.not_to change { wiki_container.wiki_repository }
+ end
+ end
+ end
+
describe '#after_wiki_activity' do
it 'updates project activity' do
wiki_container.update!(
diff --git a/spec/models/projects/data_transfer_spec.rb b/spec/models/projects/data_transfer_spec.rb
index 6d3ddbdd74e..49be35662c8 100644
--- a/spec/models/projects/data_transfer_spec.rb
+++ b/spec/models/projects/data_transfer_spec.rb
@@ -7,12 +7,24 @@ RSpec.describe Projects::DataTransfer, feature_category: :source_code_management
it { expect(subject).to be_valid }
+ # tests DataTransferCounterAttribute with the appropiate attributes
+ it_behaves_like CounterAttribute,
+ %i[repository_egress artifacts_egress packages_egress registry_egress] do
+ let(:model) { create(:project_data_transfer, project: project) }
+ end
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:namespace) }
end
describe 'scopes' do
+ let(:dates) { %w[2023-01-01 2023-02-01 2023-03-01] }
+
+ before do
+ dates.each { |date| create(:project_data_transfer, project: project, date: date) }
+ end
+
describe '.current_month' do
subject { described_class.current_month }
@@ -25,6 +37,26 @@ RSpec.describe Projects::DataTransfer, feature_category: :source_code_management
end
end
end
+
+ describe '.with_project_between_dates' do
+ subject do
+ described_class.with_project_between_dates(project, Date.new(2023, 2, 1), Date.new(2023, 3, 1))
+ end
+
+ it 'returns the correct number of results' do
+ expect(subject.size).to eq(2)
+ end
+ end
+
+ describe '.with_namespace_between_dates' do
+ subject do
+ described_class.with_namespace_between_dates(project.namespace, Date.new(2023, 2, 1), Date.new(2023, 3, 1))
+ end
+
+ it 'returns the correct number of results' do
+ expect(subject.select(:namespace_id).to_a.size).to eq(2)
+ end
+ end
end
describe '.beginning_of_month' do
diff --git a/spec/models/projects/forks/divergence_counts_spec.rb b/spec/models/projects/forks/details_spec.rb
index fd69cc0f3e7..4c0a2e3453a 100644
--- a/spec/models/projects/forks/divergence_counts_spec.rb
+++ b/spec/models/projects/forks/details_spec.rb
@@ -2,24 +2,29 @@
require 'spec_helper'
-RSpec.describe Projects::Forks::DivergenceCounts, feature_category: :source_code_management do
+RSpec.describe Projects::Forks::Details, feature_category: :source_code_management do
+ include ExclusiveLeaseHelpers
include ProjectForksHelper
let_it_be(:user) { create(:user) }
+ let_it_be(:source_repo) { create(:project, :repository, :public).repository }
+ let_it_be(:fork_repo) { fork_project(source_repo.project, user, { repository: true }).repository }
- describe '#counts', :use_clean_rails_redis_caching do
- let(:source_repo) { create(:project, :repository, :public).repository }
- let(:fork_repo) { fork_project(source_repo.project, user, { repository: true }).repository }
- let(:fork_branch) { 'fork-branch' }
- let(:cache_key) { ['project_forks', fork_repo.project.id, fork_branch, 'divergence_counts'] }
+ let(:fork_branch) { 'fork-branch' }
+ let(:cache_key) { ['project_fork_details', fork_repo.project.id, fork_branch].join(':') }
+ describe '#counts', :use_clean_rails_redis_caching do
def expect_cached_counts(value)
counts = described_class.new(fork_repo.project, fork_branch).counts
ahead, behind = value
expect(counts).to eq({ ahead: ahead, behind: behind })
- cached_value = [source_repo.commit.sha, fork_repo.commit(fork_branch).sha, value]
+ cached_value = {
+ source_sha: source_repo.commit.sha,
+ sha: fork_repo.commit(fork_branch).sha,
+ counts: value
+ }
expect(Rails.cache.read(cache_key)).to eq(cached_value)
end
@@ -72,6 +77,9 @@ RSpec.describe Projects::Forks::DivergenceCounts, feature_category: :source_code
end
context 'when counts calculated from a branch that exists upstream' do
+ let_it_be(:source_repo) { create(:project, :repository, :public).repository }
+ let_it_be(:fork_repo) { fork_project(source_repo.project, user, { repository: true }).repository }
+
let(:fork_branch) { 'feature' }
it 'compares the fork branch to upstream default branch' do
@@ -94,5 +102,61 @@ RSpec.describe Projects::Forks::DivergenceCounts, feature_category: :source_code
expect_cached_counts([2, 30])
end
end
+
+ context 'when specified branch does not exist' do
+ it 'returns nils as counts' do
+ counts = described_class.new(fork_repo.project, 'non-existent-branch').counts
+ expect(counts).to eq({ ahead: nil, behind: nil })
+ end
+ end
+ end
+
+ describe '#update!', :use_clean_rails_redis_caching do
+ it 'updates the cache with the specified value' do
+ value = { source_sha: source_repo.commit.sha, sha: fork_repo.commit.sha, counts: [0, 0], has_conflicts: true }
+
+ described_class.new(fork_repo.project, fork_branch).update!(value)
+
+ expect(Rails.cache.read(cache_key)).to eq(value)
+ end
+ end
+
+ describe '#has_conflicts', :use_clean_rails_redis_caching do
+ it 'returns whether merge for the stored commits failed due to conflicts' do
+ details = described_class.new(fork_repo.project, fork_branch)
+
+ expect do
+ value = { source_sha: source_repo.commit.sha, sha: fork_repo.commit.sha, counts: [0, 0], has_conflicts: true }
+
+ details.update!(value)
+ end.to change { details.has_conflicts? }.from(false).to(true)
+ end
+ end
+
+ describe '#exclusive_lease' do
+ it 'returns exclusive lease to the details' do
+ key = ['project_details', fork_repo.project.id, fork_branch].join(':')
+ uuid = SecureRandom.uuid
+ details = described_class.new(fork_repo.project, fork_branch)
+
+ expect(Gitlab::ExclusiveLease).to receive(:get_uuid).with(key).and_return(uuid)
+ expect(Gitlab::ExclusiveLease).to receive(:new).with(
+ key, uuid: uuid, timeout: described_class::LEASE_TIMEOUT
+ ).and_call_original
+
+ expect(details.exclusive_lease).to be_a(Gitlab::ExclusiveLease)
+ end
+ end
+
+ describe 'syncing?', :use_clean_rails_redis_caching do
+ it 'returns whether there is a sync in progress' do
+ details = described_class.new(fork_repo.project, fork_branch)
+
+ expect(details.exclusive_lease.try_obtain).to be_present
+ expect(details.syncing?).to eq(true)
+
+ details.exclusive_lease.cancel
+ expect(details.syncing?).to eq(false)
+ end
end
end
diff --git a/spec/models/projects/import_export/relation_export_spec.rb b/spec/models/projects/import_export/relation_export_spec.rb
index 8643fbc7b46..c724f30250d 100644
--- a/spec/models/projects/import_export/relation_export_spec.rb
+++ b/spec/models/projects/import_export/relation_export_spec.rb
@@ -2,9 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ImportExport::RelationExport, type: :model do
- subject { create(:project_relation_export) }
-
+RSpec.describe Projects::ImportExport::RelationExport, type: :model, feature_category: :importers do
describe 'associations' do
it { is_expected.to belong_to(:project_export_job) }
it { is_expected.to have_one(:upload) }
@@ -13,12 +11,16 @@ RSpec.describe Projects::ImportExport::RelationExport, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:project_export_job) }
it { is_expected.to validate_presence_of(:relation) }
- it { is_expected.to validate_uniqueness_of(:relation).scoped_to(:project_export_job_id) }
it { is_expected.to validate_presence_of(:status) }
it { is_expected.to validate_numericality_of(:status).only_integer }
it { is_expected.to validate_length_of(:relation).is_at_most(255) }
it { is_expected.to validate_length_of(:jid).is_at_most(255) }
it { is_expected.to validate_length_of(:export_error).is_at_most(300) }
+
+ it 'validates uniquness of the relation attribute' do
+ create(:project_relation_export)
+ expect(subject).to validate_uniqueness_of(:relation).scoped_to(:project_export_job_id)
+ end
end
describe '.by_relation' do
@@ -52,4 +54,16 @@ RSpec.describe Projects::ImportExport::RelationExport, type: :model do
expect(described_class.relation_names_list).not_to include('events', 'notes')
end
end
+
+ describe '#mark_as_failed' do
+ it 'sets status to failed and sets the export error', :aggregate_failures do
+ relation_export = create(:project_relation_export)
+
+ relation_export.mark_as_failed("Error message")
+ relation_export.reload
+
+ expect(relation_export.failed?).to eq(true)
+ expect(relation_export.export_error).to eq("Error message")
+ end
+ end
end
diff --git a/spec/models/protected_branch/merge_access_level_spec.rb b/spec/models/protected_branch/merge_access_level_spec.rb
index b6c2d527d1b..92aa5fa3eee 100644
--- a/spec/models/protected_branch/merge_access_level_spec.rb
+++ b/spec/models/protected_branch/merge_access_level_spec.rb
@@ -2,6 +2,7 @@
require 'spec_helper'
-RSpec.describe ProtectedBranch::MergeAccessLevel do
- it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MAINTAINER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) }
+RSpec.describe ProtectedBranch::MergeAccessLevel, feature_category: :source_code_management do
+ include_examples 'protected branch access'
+ include_examples 'protected ref access allowed_access_levels'
end
diff --git a/spec/models/protected_branch/push_access_level_spec.rb b/spec/models/protected_branch/push_access_level_spec.rb
index 008ae6275f0..e56ff2241b1 100644
--- a/spec/models/protected_branch/push_access_level_spec.rb
+++ b/spec/models/protected_branch/push_access_level_spec.rb
@@ -2,8 +2,9 @@
require 'spec_helper'
-RSpec.describe ProtectedBranch::PushAccessLevel do
- it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MAINTAINER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) }
+RSpec.describe ProtectedBranch::PushAccessLevel, feature_category: :source_code_management do
+ include_examples 'protected branch access'
+ include_examples 'protected ref access allowed_access_levels'
describe 'associations' do
it { is_expected.to belong_to(:deploy_key) }
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index 71e22f848cc..d14a7dd1a7e 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProtectedBranch do
+RSpec.describe ProtectedBranch, feature_category: :source_code_management do
subject { build_stubbed(:protected_branch) }
describe 'Associations' do
@@ -13,6 +13,30 @@ RSpec.describe ProtectedBranch do
describe 'Validation' do
it { is_expected.to validate_presence_of(:name) }
+ context 'uniqueness' do
+ let(:protected_branch) { build(:protected_branch) }
+
+ subject { protected_branch }
+
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to([:project_id, :namespace_id]) }
+
+ context 'when the protected_branch was saved previously' do
+ before do
+ protected_branch.save!
+ end
+
+ it { is_expected.not_to validate_uniqueness_of(:name) }
+
+ context 'and name is changed' do
+ before do
+ protected_branch.name = "#{protected_branch.name} + something else"
+ end
+
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to([:project_id, :namespace_id]) }
+ end
+ end
+ end
+
describe '#validate_either_project_or_top_group' do
context 'when protected branch does not have project or group association' do
it 'validate failed' do
@@ -214,85 +238,52 @@ RSpec.describe ProtectedBranch do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:protected_branch) { create(:protected_branch, project: project, name: "“jawn”") }
- let(:rely_on_new_cache) { true }
-
- shared_examples_for 'hash based cache implementation' do
- it 'calls only hash based cache implementation' do
- expect_next_instance_of(ProtectedBranches::CacheService) do |instance|
- expect(instance).to receive(:fetch).with('missing-branch', anything).and_call_original
- end
-
- expect(Rails.cache).not_to receive(:fetch)
-
- described_class.protected?(project, 'missing-branch')
- end
- end
-
before do
- stub_feature_flags(rely_on_protected_branches_cache: rely_on_new_cache)
allow(described_class).to receive(:matching).and_call_original
# the original call works and warms the cache
described_class.protected?(project, protected_branch.name)
end
- context 'Dry-run: true (rely_on_protected_branches_cache is off, new hash-based is used)' do
- let(:rely_on_new_cache) { false }
+ it 'correctly invalidates a cache' do
+ expect(described_class).to receive(:matching).with(protected_branch.name, protected_refs: anything).exactly(3).times.and_call_original
- it 'recalculates a fresh value every time in order to check the cache is not returning stale data' do
- expect(described_class).to receive(:matching).with(protected_branch.name, protected_refs: anything).twice
+ create_params = { name: 'bar', merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }] }
+ branch = ProtectedBranches::CreateService.new(project, project.owner, create_params).execute
+ expect(described_class.protected?(project, protected_branch.name)).to eq(true)
- 2.times { described_class.protected?(project, protected_branch.name) }
- end
+ ProtectedBranches::UpdateService.new(project, project.owner, name: 'ber').execute(branch)
+ expect(described_class.protected?(project, protected_branch.name)).to eq(true)
- it_behaves_like 'hash based cache implementation'
+ ProtectedBranches::DestroyService.new(project, project.owner).execute(branch)
+ expect(described_class.protected?(project, protected_branch.name)).to eq(true)
end
- context 'Dry-run: false (rely_on_protected_branches_cache is enabled, new hash-based cache is used)' do
- let(:rely_on_new_cache) { true }
-
- it 'correctly invalidates a cache' do
- expect(described_class).to receive(:matching).with(protected_branch.name, protected_refs: anything).exactly(3).times.and_call_original
-
- create_params = { name: 'bar', merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }] }
- branch = ProtectedBranches::CreateService.new(project, project.owner, create_params).execute
- expect(described_class.protected?(project, protected_branch.name)).to eq(true)
-
- ProtectedBranches::UpdateService.new(project, project.owner, name: 'ber').execute(branch)
- expect(described_class.protected?(project, protected_branch.name)).to eq(true)
-
- ProtectedBranches::DestroyService.new(project, project.owner).execute(branch)
- expect(described_class.protected?(project, protected_branch.name)).to eq(true)
- end
-
- it_behaves_like 'hash based cache implementation'
-
- context 'when project is updated' do
- it 'does not invalidate a cache' do
- expect(described_class).not_to receive(:matching).with(protected_branch.name, protected_refs: anything)
+ context 'when project is updated' do
+ it 'does not invalidate a cache' do
+ expect(described_class).not_to receive(:matching).with(protected_branch.name, protected_refs: anything)
- project.touch
+ project.touch
- described_class.protected?(project, protected_branch.name)
- end
+ described_class.protected?(project, protected_branch.name)
end
+ end
- context 'when other project protected branch is updated' do
- it 'does not invalidate the current project cache' do
- expect(described_class).not_to receive(:matching).with(protected_branch.name, protected_refs: anything)
+ context 'when other project protected branch is updated' do
+ it 'does not invalidate the current project cache' do
+ expect(described_class).not_to receive(:matching).with(protected_branch.name, protected_refs: anything)
- another_project = create(:project)
- ProtectedBranches::CreateService.new(another_project, another_project.owner, name: 'bar').execute
+ another_project = create(:project)
+ ProtectedBranches::CreateService.new(another_project, another_project.owner, name: 'bar').execute
- described_class.protected?(project, protected_branch.name)
- end
+ described_class.protected?(project, protected_branch.name)
end
+ end
- it 'correctly uses the cached version' do
- expect(described_class).not_to receive(:matching)
+ it 'correctly uses the cached version' do
+ expect(described_class).not_to receive(:matching)
- expect(described_class.protected?(project, protected_branch.name)).to eq(true)
- end
+ expect(described_class.protected?(project, protected_branch.name)).to eq(true)
end
end
end
@@ -344,6 +335,7 @@ RSpec.describe ProtectedBranch do
context "when feature flag disabled" do
before do
stub_feature_flags(group_protected_branches: false)
+ stub_feature_flags(allow_protected_branches_for_group: false)
end
let(:subject_branch) { create(:protected_branch, allow_force_push: allow_force_push, name: "foo") }
@@ -383,6 +375,7 @@ RSpec.describe ProtectedBranch do
with_them do
before do
stub_feature_flags(group_protected_branches: true)
+ stub_feature_flags(allow_protected_branches_for_group: true)
unless group_level_value.nil?
create(:protected_branch, allow_force_push: group_level_value, name: "foo", project: nil, group: group)
@@ -436,6 +429,7 @@ RSpec.describe ProtectedBranch do
context 'when feature flag enabled' do
before do
stub_feature_flags(group_protected_branches: true)
+ stub_feature_flags(allow_protected_branches_for_group: true)
end
it 'call `all_protected_branches`' do
@@ -448,6 +442,7 @@ RSpec.describe ProtectedBranch do
context 'when feature flag disabled' do
before do
stub_feature_flags(group_protected_branches: false)
+ stub_feature_flags(allow_protected_branches_for_group: false)
end
it 'call `protected_branches`' do
@@ -458,6 +453,62 @@ RSpec.describe ProtectedBranch do
end
end
+ describe '.protected_ref_accessible_to?' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:admin) { create(:user, :admin) }
+
+ before do
+ project.add_guest(guest)
+ project.add_reporter(reporter)
+ project.add_developer(developer)
+ project.add_maintainer(maintainer)
+ project.add_owner(owner)
+ end
+
+ subject { described_class.protected_ref_accessible_to?(anything, current_user, project: project, action: :push) }
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with admin' do
+ let(:current_user) { admin }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
describe '.by_name' do
let!(:protected_branch) { create(:protected_branch, name: 'master') }
let!(:another_protected_branch) { create(:protected_branch, name: 'stable') }
diff --git a/spec/models/protected_tag/create_access_level_spec.rb b/spec/models/protected_tag/create_access_level_spec.rb
index 566f8695388..8eeccdc9b34 100644
--- a/spec/models/protected_tag/create_access_level_spec.rb
+++ b/spec/models/protected_tag/create_access_level_spec.rb
@@ -3,6 +3,9 @@
require 'spec_helper'
RSpec.describe ProtectedTag::CreateAccessLevel, feature_category: :source_code_management do
+ include_examples 'protected tag access'
+ include_examples 'protected ref access allowed_access_levels'
+
describe 'associations' do
it { is_expected.to belong_to(:deploy_key) }
end
@@ -10,16 +13,6 @@ RSpec.describe ProtectedTag::CreateAccessLevel, feature_category: :source_code_m
describe 'validations', :aggregate_failures do
let_it_be(:protected_tag) { create(:protected_tag) }
- it 'verifies access levels' do
- is_expected.to validate_inclusion_of(:access_level).in_array(
- [
- Gitlab::Access::MAINTAINER,
- Gitlab::Access::DEVELOPER,
- Gitlab::Access::NO_ACCESS
- ]
- )
- end
-
context 'when deploy key enabled for the project' do
let(:deploy_key) { create(:deploy_key, projects: [protected_tag.project]) }
diff --git a/spec/models/releases/link_spec.rb b/spec/models/releases/link_spec.rb
index 4910de61c22..c4c9fba32d9 100644
--- a/spec/models/releases/link_spec.rb
+++ b/spec/models/releases/link_spec.rb
@@ -80,15 +80,6 @@ RSpec.describe Releases::Link do
end
end
- describe '#external?' do
- subject { link.external? }
-
- let(:link) { build(:release_link, release: release, url: url) }
- let(:url) { 'https://google.com/-/jobs/140463678/artifacts/download' }
-
- it { is_expected.to be_truthy }
- end
-
describe 'supported protocols' do
where(:protocol) do
%w(http https ftp)
diff --git a/spec/models/releases/source_spec.rb b/spec/models/releases/source_spec.rb
index 227085951c0..fadc2fd6f53 100644
--- a/spec/models/releases/source_spec.rb
+++ b/spec/models/releases/source_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Releases::Source do
- let_it_be(:project) { create(:project, :repository, name: 'finance-cal') }
+ let_it_be(:project) { create(:project, :repository) }
let(:tag_name) { 'v1.0' }
@@ -27,7 +27,7 @@ RSpec.describe Releases::Source do
it 'returns zip archived source url' do
is_expected
- .to eq("#{project.web_url}/-/archive/v1.0/finance-cal-v1.0.zip")
+ .to eq("#{project.web_url}/-/archive/v1.0/#{project.path}-v1.0.zip")
end
context 'when ref is directory structure' do
@@ -35,7 +35,7 @@ RSpec.describe Releases::Source do
it 'converts slash to dash' do
is_expected
- .to eq("#{project.web_url}/-/archive/beta/v1.0/finance-cal-beta-v1.0.zip")
+ .to eq("#{project.web_url}/-/archive/beta/v1.0/#{project.path}-beta-v1.0.zip")
end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 44fcf87526a..aa2ac52a9ab 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -20,10 +20,12 @@ RSpec.describe Repository, feature_category: :source_code_management do
let(:merge_commit) do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
- merge_commit_id = repository.merge(user,
- merge_request.diff_head_sha,
- merge_request,
- message)
+ merge_commit_id = repository.merge(
+ user,
+ merge_request.diff_head_sha,
+ merge_request,
+ message
+ )
repository.commit(merge_commit_id)
end
@@ -413,6 +415,27 @@ RSpec.describe Repository, feature_category: :source_code_management do
repository.commits('master', limit: 1)
end
end
+
+ context 'when include_referenced_by is passed' do
+ context 'when commit has references' do
+ let(:ref) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
+ let(:include_referenced_by) { ['refs/tags'] }
+
+ subject { repository.commits(ref, limit: 1, include_referenced_by: include_referenced_by).first }
+
+ it 'returns commits with referenced_by excluding that match the patterns' do
+ expect(subject.referenced_by).to match_array(['refs/tags/v1.1.0'])
+ end
+
+ context 'when matching multiple references' do
+ let(:include_referenced_by) { ['refs/tags', 'refs/heads'] }
+
+ it 'returns commits with referenced_by that match the patterns' do
+ expect(subject.referenced_by).to match_array(['refs/tags/v1.1.0', 'refs/heads/improve/awesome', 'refs/heads/merge-test'])
+ end
+ end
+ end
+ end
end
context "when 'author' is set" do
@@ -576,6 +599,15 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
describe '#list_commits_by' do
+ it 'returns commits when no filter is applied' do
+ commit_ids = repository.list_commits_by(nil, 'master', limit: 2).map(&:id)
+
+ expect(commit_ids).to include(
+ 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
+ '498214de67004b1da3d820901307bed2a68a8ef6'
+ )
+ end
+
it 'returns commits with messages containing a given string' do
commit_ids = repository.list_commits_by('test text', 'master').map(&:id)
@@ -682,6 +714,14 @@ RSpec.describe Repository, feature_category: :source_code_management do
it { is_expected.to be_nil }
end
+
+ context 'when root reference is empty' do
+ subject { empty_repo.merged_to_root_ref?('master') }
+
+ let(:empty_repo) { build(:project, :empty_repo).repository }
+
+ it { is_expected.to be_nil }
+ end
end
describe "#root_ref_sha" do
@@ -690,7 +730,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
subject { repository.root_ref_sha }
before do
- allow(repository).to receive(:commit).with(repository.root_ref) { commit }
+ allow(repository).to receive(:head_commit) { commit }
end
it { is_expected.to eq(commit.sha) }
@@ -925,9 +965,11 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe "#create_file" do
it 'commits new file successfully' do
expect do
- repository.create_file(user, 'NEWCHANGELOG', 'Changelog!',
- message: 'Create changelog',
- branch_name: 'master')
+ repository.create_file(
+ user, 'NEWCHANGELOG', 'Changelog!',
+ message: 'Create changelog',
+ branch_name: 'master'
+ )
end.to change { repository.count_commits(ref: 'master') }.by(1)
blob = repository.blob_at('master', 'NEWCHANGELOG')
@@ -937,9 +979,11 @@ RSpec.describe Repository, feature_category: :source_code_management do
it 'creates new file and dir when file_path has a forward slash' do
expect do
- repository.create_file(user, 'new_dir/new_file.txt', 'File!',
- message: 'Create new_file with new_dir',
- branch_name: 'master')
+ repository.create_file(
+ user, 'new_dir/new_file.txt', 'File!',
+ message: 'Create new_file with new_dir',
+ branch_name: 'master'
+ )
end.to change { repository.count_commits(ref: 'master') }.by(1)
expect(repository.tree('master', 'new_dir').path).to eq('new_dir')
@@ -947,9 +991,11 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
it 'respects the autocrlf setting' do
- repository.create_file(user, 'hello.txt', "Hello,\r\nWorld",
- message: 'Add hello world',
- branch_name: 'master')
+ repository.create_file(
+ user, 'hello.txt', "Hello,\r\nWorld",
+ message: 'Add hello world',
+ branch_name: 'master'
+ )
blob = repository.blob_at('master', 'hello.txt')
@@ -959,11 +1005,13 @@ RSpec.describe Repository, feature_category: :source_code_management do
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
expect do
- repository.create_file(user, 'NEWREADME', 'README!',
- message: 'Add README',
- branch_name: 'master',
- author_email: author_email,
- author_name: author_name)
+ repository.create_file(
+ user, 'NEWREADME', 'README!',
+ message: 'Add README',
+ branch_name: 'master',
+ author_email: author_email,
+ author_name: author_name
+ )
end.to change { repository.count_commits(ref: 'master') }.by(1)
last_commit = repository.commit
@@ -977,9 +1025,11 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe "#update_file" do
it 'updates file successfully' do
expect do
- repository.update_file(user, 'CHANGELOG', 'Changelog!',
- message: 'Update changelog',
- branch_name: 'master')
+ repository.update_file(
+ user, 'CHANGELOG', 'Changelog!',
+ message: 'Update changelog',
+ branch_name: 'master'
+ )
end.to change { repository.count_commits(ref: 'master') }.by(1)
blob = repository.blob_at('master', 'CHANGELOG')
@@ -989,10 +1039,12 @@ RSpec.describe Repository, feature_category: :source_code_management do
it 'updates filename successfully' do
expect do
- repository.update_file(user, 'NEWLICENSE', 'Copyright!',
- branch_name: 'master',
- previous_path: 'LICENSE',
- message: 'Changes filename')
+ repository.update_file(
+ user, 'NEWLICENSE', 'Copyright!',
+ branch_name: 'master',
+ previous_path: 'LICENSE',
+ message: 'Changes filename'
+ )
end.to change { repository.count_commits(ref: 'master') }.by(1)
files = repository.ls_files('master')
@@ -1004,12 +1056,14 @@ RSpec.describe Repository, feature_category: :source_code_management do
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
expect do
- repository.update_file(user, 'README', 'Updated README!',
- branch_name: 'master',
- previous_path: 'README',
- message: 'Update README',
- author_email: author_email,
- author_name: author_name)
+ repository.update_file(
+ user, 'README', 'Updated README!',
+ branch_name: 'master',
+ previous_path: 'README',
+ message: 'Update README',
+ author_email: author_email,
+ author_name: author_name
+ )
end.to change { repository.count_commits(ref: 'master') }.by(1)
last_commit = repository.commit
@@ -1020,13 +1074,45 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
end
+ describe "#move_dir_files" do
+ it 'move directory files successfully' do
+ expect do
+ repository.move_dir_files(
+ user, 'files/new_js', 'files/js',
+ branch_name: 'master',
+ message: 'move directory images to new_images',
+ author_email: author_email,
+ author_name: author_name
+ )
+ end.to change { repository.count_commits(ref: 'master') }.by(1)
+ files = repository.ls_files('master')
+
+ expect(files).not_to include('files/js/application.js')
+ expect(files).to include('files/new_js/application.js')
+ end
+
+ it 'skips commit with same path' do
+ expect do
+ repository.move_dir_files(
+ user, 'files/js', 'files/js',
+ branch_name: 'master',
+ message: 'no commit',
+ author_email: author_email,
+ author_name: author_name
+ )
+ end.to change { repository.count_commits(ref: 'master') }.by(0)
+ end
+ end
+
describe "#delete_file" do
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
it 'removes file successfully' do
expect do
- repository.delete_file(user, 'README',
- message: 'Remove README', branch_name: 'master')
+ repository.delete_file(
+ user, 'README',
+ message: 'Remove README', branch_name: 'master'
+ )
end.to change { repository.count_commits(ref: 'master') }.by(1)
expect(repository.blob_at('master', 'README')).to be_nil
@@ -1035,9 +1121,11 @@ RSpec.describe Repository, feature_category: :source_code_management do
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
expect do
- repository.delete_file(user, 'README',
+ repository.delete_file(
+ user, 'README',
message: 'Remove README', branch_name: 'master',
- author_email: author_email, author_name: author_name)
+ author_email: author_email, author_name: author_name
+ )
end.to change { repository.count_commits(ref: 'master') }.by(1)
last_commit = repository.commit
@@ -1155,10 +1243,12 @@ RSpec.describe Repository, feature_category: :source_code_management do
let(:path) { '*.md' }
it 'returns files matching the path in the root folder' do
- expect(result).to contain_exactly('CONTRIBUTING.md',
- 'MAINTENANCE.md',
- 'PROCESS.md',
- 'README.md')
+ expect(result).to contain_exactly(
+ 'CONTRIBUTING.md',
+ 'MAINTENANCE.md',
+ 'PROCESS.md',
+ 'README.md'
+ )
end
end
@@ -1166,12 +1256,14 @@ RSpec.describe Repository, feature_category: :source_code_management do
let(:path) { '**.md' }
it 'returns all matching files in all folders' do
- expect(result).to contain_exactly('CONTRIBUTING.md',
- 'MAINTENANCE.md',
- 'PROCESS.md',
- 'README.md',
- 'files/markdown/ruby-style-guide.md',
- 'with space/README.md')
+ expect(result).to contain_exactly(
+ 'CONTRIBUTING.md',
+ 'MAINTENANCE.md',
+ 'PROCESS.md',
+ 'README.md',
+ 'files/markdown/ruby-style-guide.md',
+ 'with space/README.md'
+ )
end
end
@@ -1203,10 +1295,12 @@ RSpec.describe Repository, feature_category: :source_code_management do
let(:path) { '**/*.rb' }
it 'returns all matched files in all subfolders' do
- expect(result).to contain_exactly('encoding/russian.rb',
- 'files/ruby/popen.rb',
- 'files/ruby/regex.rb',
- 'files/ruby/version_info.rb')
+ expect(result).to contain_exactly(
+ 'encoding/russian.rb',
+ 'files/ruby/popen.rb',
+ 'files/ruby/regex.rb',
+ 'files/ruby/version_info.rb'
+ )
end
end
@@ -1422,46 +1516,47 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
end
- [true, false].each do |ff|
- context "with feature flag license_from_gitaly=#{ff}" do
- before do
- stub_feature_flags(license_from_gitaly: ff)
- end
-
- describe '#license', :use_clean_rails_memory_store_caching, :clean_gitlab_redis_cache do
- let(:project) { create(:project, :repository) }
+ describe '#license', :use_clean_rails_memory_store_caching, :clean_gitlab_redis_cache do
+ let(:project) { create(:project, :repository) }
- before do
- repository.delete_file(user, 'LICENSE',
- message: 'Remove LICENSE', branch_name: 'master')
- end
+ before do
+ repository.delete_file(
+ user, 'LICENSE',
+ message: 'Remove LICENSE',
+ branch_name: 'master'
+ )
+ end
- it 'returns nil when no license is detected' do
- expect(repository.license).to be_nil
- end
+ it 'returns nil when no license is detected' do
+ expect(repository.license).to be_nil
+ end
- it 'returns nil when the repository does not exist' do
- expect(repository).to receive(:exists?).and_return(false)
+ it 'returns nil when the repository does not exist' do
+ expect(repository).to receive(:exists?).and_return(false)
- expect(repository.license).to be_nil
- end
+ expect(repository.license).to be_nil
+ end
- it 'returns other when the content is not recognizable' do
- repository.create_file(user, 'LICENSE', 'Gitlab B.V.',
- message: 'Add LICENSE', branch_name: 'master')
+ it 'returns other when the content is not recognizable' do
+ repository.create_file(
+ user, 'LICENSE', 'Gitlab B.V.',
+ message: 'Add LICENSE',
+ branch_name: 'master'
+ )
- expect(repository.license_key).to eq('other')
- end
+ expect(repository.license_key).to eq('other')
+ end
- it 'returns the license' do
- license = Licensee::License.new('mit')
- repository.create_file(user, 'LICENSE',
- license.content,
- message: 'Add LICENSE', branch_name: 'master')
+ it 'returns the license' do
+ license = Licensee::License.new('mit')
+ repository.create_file(
+ user, 'LICENSE',
+ license.content,
+ message: 'Add LICENSE',
+ branch_name: 'master'
+ )
- expect(repository.license_key).to eq(license.key)
- end
- end
+ expect(repository.license_key).to eq(license.key)
end
end
@@ -1987,19 +2082,23 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe '#merge_to_ref' do
let(:merge_request) do
- create(:merge_request, source_branch: 'feature',
- target_branch: 'master',
- source_project: project)
+ create(
+ :merge_request,
+ source_branch: 'feature',
+ target_branch: 'master',
+ source_project: project
+ )
end
it 'writes merge of source SHA and first parent ref to MR merge_ref_path' do
- merge_commit_id =
- repository.merge_to_ref(user,
- source_sha: merge_request.diff_head_sha,
- branch: merge_request.target_branch,
- target_ref: merge_request.merge_ref_path,
- message: 'Custom message',
- first_parent_ref: merge_request.target_branch_ref)
+ merge_commit_id = repository.merge_to_ref(
+ user,
+ source_sha: merge_request.diff_head_sha,
+ branch: merge_request.target_branch,
+ target_ref: merge_request.merge_ref_path,
+ message: 'Custom message',
+ first_parent_ref: merge_request.target_branch_ref
+ )
merge_commit = repository.commit(merge_commit_id)
@@ -2021,11 +2120,13 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
it 'merges the code and return the commit id' do
- merge_commit_id = repository.ff_merge(user,
- merge_request.diff_head_sha,
- merge_request.target_branch,
- target_sha: repository.commit(merge_request.target_branch).sha,
- merge_request: merge_request)
+ merge_commit_id = repository.ff_merge(
+ user,
+ merge_request.diff_head_sha,
+ merge_request.target_branch,
+ target_sha: repository.commit(merge_request.target_branch).sha,
+ merge_request: merge_request
+ )
merge_commit = repository.commit(merge_commit_id)
expect(merge_commit).to be_present
@@ -2033,11 +2134,13 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
- merge_commit_id = repository.ff_merge(user,
- merge_request.diff_head_sha,
- merge_request.target_branch,
- target_sha: repository.commit(merge_request.target_branch).sha,
- merge_request: merge_request)
+ merge_commit_id = repository.ff_merge(
+ user,
+ merge_request.diff_head_sha,
+ merge_request.target_branch,
+ target_sha: repository.commit(merge_request.target_branch).sha,
+ merge_request: merge_request
+ )
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
@@ -2302,7 +2405,6 @@ RSpec.describe Repository, feature_category: :source_code_management do
:contribution_guide,
:changelog,
:license_blob,
- :license_licensee,
:license_gitaly,
:gitignore,
:gitlab_ci_yml,
@@ -2957,11 +3059,10 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(readme_path license_blob license_licensee license_gitaly))
+ .with(%i(readme_path license_blob license_gitaly))
expect(repository).to receive(:readme_path)
expect(repository).to receive(:license_blob)
- expect(repository).to receive(:license_licensee)
expect(repository).to receive(:license_gitaly)
repository.refresh_method_caches(%i(readme license))
diff --git a/spec/models/resource_events/abuse_report_event_spec.rb b/spec/models/resource_events/abuse_report_event_spec.rb
new file mode 100644
index 00000000000..1c709ae4f21
--- /dev/null
+++ b/spec/models/resource_events/abuse_report_event_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ResourceEvents::AbuseReportEvent, feature_category: :instance_resiliency, type: :model do
+ subject(:event) { build(:abuse_report_event) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:abuse_report).required }
+ it { is_expected.to belong_to(:user).optional }
+ end
+
+ describe 'validations' do
+ it { is_expected.to be_valid }
+ it { is_expected.to validate_presence_of(:action) }
+ end
+end
diff --git a/spec/models/resource_events/issue_assignment_event_spec.rb b/spec/models/resource_events/issue_assignment_event_spec.rb
new file mode 100644
index 00000000000..bc217da2812
--- /dev/null
+++ b/spec/models/resource_events/issue_assignment_event_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ResourceEvents::IssueAssignmentEvent, feature_category: :value_stream_management, type: :model do
+ subject(:event) { build(:issue_assignment_event) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:issue) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to be_valid }
+ it { is_expected.to validate_presence_of(:issue) }
+ end
+end
diff --git a/spec/models/resource_events/merge_request_assignment_event_spec.rb b/spec/models/resource_events/merge_request_assignment_event_spec.rb
new file mode 100644
index 00000000000..15f4c088333
--- /dev/null
+++ b/spec/models/resource_events/merge_request_assignment_event_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ResourceEvents::MergeRequestAssignmentEvent, feature_category: :value_stream_management, type: :model do
+ subject(:event) { build(:merge_request_assignment_event) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:merge_request) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to be_valid }
+ it { is_expected.to validate_presence_of(:merge_request) }
+ end
+end
diff --git a/spec/models/resource_state_event_spec.rb b/spec/models/resource_state_event_spec.rb
index a6d6b507b69..de101107268 100644
--- a/spec/models/resource_state_event_spec.rb
+++ b/spec/models/resource_state_event_spec.rb
@@ -45,8 +45,11 @@ RSpec.describe ResourceStateEvent, feature_category: :team_planning, type: :mode
describe '#issue_usage_metrics' do
describe 'when an issue is closed' do
subject(:close_issue) do
- create(described_class.name.underscore.to_sym, issue: issue,
- state: described_class.states[:closed])
+ create(
+ described_class.name.underscore.to_sym,
+ issue: issue,
+ state: described_class.states[:closed]
+ )
end
it 'tracks closed issues' do
@@ -65,8 +68,11 @@ RSpec.describe ResourceStateEvent, feature_category: :team_planning, type: :mode
describe 'when an issue is reopened' do
subject(:reopen_issue) do
- create(described_class.name.underscore.to_sym, issue: issue,
- state: described_class.states[:reopened])
+ create(
+ described_class.name.underscore.to_sym,
+ issue: issue,
+ state: described_class.states[:reopened]
+ )
end
it 'tracks reopened issues' do
diff --git a/spec/models/serverless/domain_cluster_spec.rb b/spec/models/serverless/domain_cluster_spec.rb
deleted file mode 100644
index 487385c62c1..00000000000
--- a/spec/models/serverless/domain_cluster_spec.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ::Serverless::DomainCluster do
- subject { create(:serverless_domain_cluster) }
-
- describe 'default values' do
- subject(:domain_cluster) { build(:serverless_domain_cluster) }
-
- before do
- allow(::Serverless::Domain).to receive(:generate_uuid).and_return('randomtoken')
- end
-
- it { expect(domain_cluster.uuid).to eq('randomtoken') }
- end
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:pages_domain) }
- it { is_expected.to validate_presence_of(:knative) }
-
- it { is_expected.to validate_presence_of(:uuid) }
- it { is_expected.to validate_length_of(:uuid).is_equal_to(::Serverless::Domain::UUID_LENGTH) }
- it { is_expected.to validate_uniqueness_of(:uuid) }
-
- it 'validates that uuid has only hex characters' do
- subject = build(:serverless_domain_cluster, uuid: 'z1234567890123')
- subject.valid?
-
- expect(subject.errors[:uuid]).to include('only allows hex characters')
- end
- end
-
- describe 'associations' do
- it { is_expected.to belong_to(:pages_domain) }
- it { is_expected.to belong_to(:knative) }
- it { is_expected.to belong_to(:creator).optional }
- end
-
- describe 'uuid' do
- context 'when nil' do
- it 'generates a value by default' do
- attributes = build(:serverless_domain_cluster).attributes.merge(uuid: nil)
- expect(::Serverless::Domain).to receive(:generate_uuid).and_call_original
-
- subject = Serverless::DomainCluster.new(attributes)
-
- expect(subject.uuid).not_to be_blank
- end
- end
-
- context 'when not nil' do
- it 'does not override the existing value' do
- uuid = 'abcd1234567890'
- expect(build(:serverless_domain_cluster, uuid: uuid).uuid).to eq(uuid)
- end
- end
- end
-
- describe 'cluster' do
- it { is_expected.to respond_to(:cluster) }
- end
-
- describe 'domain' do
- it { is_expected.to respond_to(:domain) }
- end
-
- describe 'certificate' do
- it { is_expected.to respond_to(:certificate) }
- end
-
- describe 'key' do
- it { is_expected.to respond_to(:key) }
- end
-end
diff --git a/spec/models/serverless/domain_spec.rb b/spec/models/serverless/domain_spec.rb
deleted file mode 100644
index f997b28b149..00000000000
--- a/spec/models/serverless/domain_spec.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ::Serverless::Domain do
- let(:function_name) { 'test-function' }
- let(:pages_domain_name) { 'serverless.gitlab.io' }
- let(:pages_domain) { create(:pages_domain, :instance_serverless, domain: pages_domain_name) }
- let!(:serverless_domain_cluster) { create(:serverless_domain_cluster, uuid: 'abcdef12345678', pages_domain: pages_domain) }
- let(:valid_cluster_uuid) { 'aba1cdef123456f278' }
- let(:invalid_cluster_uuid) { 'aba1cdef123456f178' }
- let!(:environment) { create(:environment, name: 'test') }
-
- let(:valid_uri) { "https://#{function_name}-#{valid_cluster_uuid}#{"%x" % environment.id}-#{environment.slug}.#{pages_domain_name}" }
- let(:valid_fqdn) { "#{function_name}-#{valid_cluster_uuid}#{"%x" % environment.id}-#{environment.slug}.#{pages_domain_name}" }
- let(:invalid_uri) { "https://#{function_name}-#{invalid_cluster_uuid}#{"%x" % environment.id}-#{environment.slug}.#{pages_domain_name}" }
-
- shared_examples 'a valid Domain' do
- describe '#uri' do
- it 'matches valid URI' do
- expect(subject.uri.to_s).to eq valid_uri
- end
- end
-
- describe '#function_name' do
- it 'returns function_name' do
- expect(subject.function_name).to eq function_name
- end
- end
-
- describe '#serverless_domain_cluster' do
- it 'returns serverless_domain_cluster' do
- expect(subject.serverless_domain_cluster).to eq serverless_domain_cluster
- end
- end
-
- describe '#environment' do
- it 'returns environment' do
- expect(subject.environment).to eq environment
- end
- end
- end
-
- describe '.new' do
- context 'with valid arguments' do
- subject do
- described_class.new(
- function_name: function_name,
- serverless_domain_cluster: serverless_domain_cluster,
- environment: environment
- )
- end
-
- it_behaves_like 'a valid Domain'
- end
-
- context 'with invalid arguments' do
- subject do
- described_class.new(
- function_name: function_name,
- environment: environment
- )
- end
-
- it { is_expected.not_to be_valid }
- end
-
- context 'with nil cluster argument' do
- subject do
- described_class.new(
- function_name: function_name,
- serverless_domain_cluster: nil,
- environment: environment
- )
- end
-
- it { is_expected.not_to be_valid }
- end
- end
-
- describe '.generate_uuid' do
- it 'has 14 characters' do
- expect(described_class.generate_uuid.length).to eq(described_class::UUID_LENGTH)
- end
-
- it 'consists of only hexadecimal characters' do
- expect(described_class.generate_uuid).to match(/\A\h+\z/)
- end
-
- it 'uses random characters' do
- uuid = 'abcd1234567890'
-
- expect(SecureRandom).to receive(:hex).with(described_class::UUID_LENGTH / 2).and_return(uuid)
- expect(described_class.generate_uuid).to eq(uuid)
- end
- end
-end
diff --git a/spec/models/serverless/function_spec.rb b/spec/models/serverless/function_spec.rb
deleted file mode 100644
index 632f5eba5c3..00000000000
--- a/spec/models/serverless/function_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ::Serverless::Function do
- let(:project) { create(:project) }
- let(:func) { described_class.new(project, 'test', 'test-ns') }
-
- it 'has a proper id' do
- expect(func.id).to eql("#{project.id}/test/test-ns")
- expect(func.name).to eql("test")
- expect(func.namespace).to eql("test-ns")
- end
-
- it 'can decode an identifier' do
- f = described_class.find_by_id("#{project.id}/testfunc/dummy-ns")
-
- expect(f.name).to eql("testfunc")
- expect(f.namespace).to eql("dummy-ns")
- end
-end
diff --git a/spec/models/service_desk/custom_email_credential_spec.rb b/spec/models/service_desk/custom_email_credential_spec.rb
new file mode 100644
index 00000000000..a990b77128e
--- /dev/null
+++ b/spec/models/service_desk/custom_email_credential_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ServiceDesk::CustomEmailCredential, feature_category: :service_desk do
+ let(:project) { build_stubbed(:project) }
+ let(:credential) { build_stubbed(:service_desk_custom_email_credential, project: project) }
+ let(:smtp_username) { "user@example.com" }
+ let(:smtp_password) { "supersecret" }
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+
+ it { is_expected.to validate_presence_of(:smtp_address) }
+ it { is_expected.to validate_length_of(:smtp_address).is_at_most(255) }
+ it { is_expected.to allow_value('smtp.gmail.com').for(:smtp_address) }
+ it { is_expected.to allow_value('1.1.1.1').for(:smtp_address) }
+ it { is_expected.to allow_value('199.1.1.1').for(:smtp_address) }
+ it { is_expected.not_to allow_value('https://example.com').for(:smtp_address) }
+ it { is_expected.not_to allow_value('file://example').for(:smtp_address) }
+ it { is_expected.not_to allow_value('/example').for(:smtp_address) }
+ it { is_expected.not_to allow_value('localhost').for(:smtp_address) }
+ it { is_expected.not_to allow_value('127.0.0.1').for(:smtp_address) }
+ it { is_expected.not_to allow_value('192.168.12.12').for(:smtp_address) } # disallow local network
+
+ it { is_expected.to validate_presence_of(:smtp_port) }
+ it { is_expected.to validate_numericality_of(:smtp_port).only_integer.is_greater_than(0) }
+
+ it { is_expected.to validate_presence_of(:smtp_username) }
+ it { is_expected.to validate_length_of(:smtp_username).is_at_most(255) }
+
+ it { is_expected.to validate_presence_of(:smtp_password) }
+ it { is_expected.to validate_length_of(:smtp_password).is_at_least(8).is_at_most(128) }
+ end
+
+ describe 'encrypted #smtp_username' do
+ subject { build_stubbed(:service_desk_custom_email_credential, smtp_username: smtp_username) }
+
+ it 'saves and retrieves the encrypted smtp username and iv correctly' do
+ expect(subject.encrypted_smtp_username).not_to be_nil
+ expect(subject.encrypted_smtp_username_iv).not_to be_nil
+
+ expect(subject.smtp_username).to eq(smtp_username)
+ end
+ end
+
+ describe 'encrypted #smtp_password' do
+ subject { build_stubbed(:service_desk_custom_email_credential, smtp_password: smtp_password) }
+
+ it 'saves and retrieves the encrypted smtp password and iv correctly' do
+ expect(subject.encrypted_smtp_password).not_to be_nil
+ expect(subject.encrypted_smtp_password_iv).not_to be_nil
+
+ expect(subject.smtp_password).to eq(smtp_password)
+ end
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+
+ it 'can access service desk setting from project' do
+ setting = build_stubbed(:service_desk_setting, project: project)
+
+ expect(credential.service_desk_setting).to eq(setting)
+ end
+ end
+end
diff --git a/spec/models/service_desk/custom_email_verification_spec.rb b/spec/models/service_desk/custom_email_verification_spec.rb
new file mode 100644
index 00000000000..f114367cfbf
--- /dev/null
+++ b/spec/models/service_desk/custom_email_verification_spec.rb
@@ -0,0 +1,170 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ServiceDesk::CustomEmailVerification, feature_category: :service_desk do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:generate_token_pattern) { /\A\p{Alnum}{12}\z/ }
+
+ describe '.generate_token' do
+ it 'matches expected output' do
+ expect(described_class.generate_token).to match(generate_token_pattern)
+ end
+ end
+
+ describe 'validations' do
+ subject { build(:service_desk_custom_email_verification, project: project) }
+
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:state) }
+
+ context 'when status is :started' do
+ before do
+ subject.mark_as_started!(user)
+ end
+
+ it { is_expected.to validate_presence_of(:token) }
+ it { is_expected.to validate_length_of(:token).is_equal_to(12) }
+
+ it 'matches .generate_token pattern' do
+ expect(subject.token).to match(generate_token_pattern)
+ end
+
+ it { is_expected.to validate_presence_of(:triggerer) }
+ it { is_expected.to validate_presence_of(:triggered_at) }
+ it { is_expected.to validate_absence_of(:error) }
+ end
+
+ context 'when status is :finished' do
+ before do
+ subject.mark_as_started!(user)
+ subject.mark_as_finished!
+ end
+
+ it { is_expected.to validate_absence_of(:token) }
+ it { is_expected.to validate_absence_of(:error) }
+ end
+
+ context 'when status is :failed' do
+ before do
+ subject.mark_as_started!(user)
+ subject.mark_as_failed!(:smtp_host_issue)
+ end
+
+ it { is_expected.to validate_presence_of(:error) }
+ it { is_expected.to validate_absence_of(:token) }
+ end
+ end
+
+ describe 'status state machine' do
+ subject { build(:service_desk_custom_email_verification, project: project) }
+
+ describe 'transitioning to started' do
+ it 'records the started at time and generates token' do
+ subject.mark_as_started!(user)
+
+ is_expected.to be_started
+ expect(subject.token).to be_present
+ expect(subject.triggered_at).to be_present
+ expect(subject.triggerer).to eq(user)
+ end
+ end
+
+ describe 'transitioning to finished' do
+ it 'removes the generated token' do
+ subject.mark_as_started!(user)
+ subject.mark_as_finished!
+
+ is_expected.to be_finished
+ expect(subject.token).not_to be_present
+ end
+ end
+
+ describe 'transitioning to failed' do
+ let(:error) { :smtp_host_issue }
+
+ it 'removes the generated token' do
+ subject.mark_as_started!(user)
+ subject.mark_as_failed!(error)
+
+ is_expected.to be_failed
+ expect(subject.token).not_to be_present
+ expect(subject.error).to eq(error.to_s)
+ end
+ end
+ end
+
+ describe '#accepted_until' do
+ it 'returns nil' do
+ expect(subject.accepted_until).to be_nil
+ end
+
+ context 'when state is :started and successfully transitioned' do
+ let(:triggered_at) { 2.minutes.ago }
+
+ before do
+ subject.project = project
+ subject.mark_as_started!(user)
+ end
+
+ it 'returns correct timeframe end time' do
+ expect(subject.accepted_until).to eq(described_class::TIMEFRAME.since(subject.triggered_at))
+ end
+
+ context 'when triggered_at is not set' do
+ it 'returns nil' do
+ subject.triggered_at = nil
+ expect(subject.accepted_until).to be nil
+ end
+ end
+ end
+ end
+
+ describe '#in_timeframe?' do
+ it { is_expected.not_to be_in_timeframe }
+
+ context 'when state is :started and successfully transitioned' do
+ before do
+ subject.project = project
+ subject.mark_as_started!(user)
+ end
+
+ it { is_expected.to be_in_timeframe }
+
+ context 'and timeframe was missed' do
+ before do
+ subject.triggered_at = (described_class::TIMEFRAME + 1).ago
+ end
+
+ it { is_expected.not_to be_in_timeframe }
+ end
+ end
+ end
+
+ describe 'encrypted #token' do
+ let(:token) { 'XXXXXXXXXXXX' }
+
+ subject { build_stubbed(:service_desk_custom_email_verification, token: token) }
+
+ it 'saves and retrieves the encrypted token and iv correctly' do
+ expect(subject.encrypted_token).not_to be_nil
+ expect(subject.encrypted_token_iv).not_to be_nil
+
+ expect(subject.token).to eq(token)
+ end
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:triggerer) }
+
+ it 'can access service desk setting from project' do
+ subject.project = project
+ setting = build_stubbed(:service_desk_setting, project: subject.project)
+
+ expect(subject.service_desk_setting).to eq(setting)
+ end
+ end
+end
diff --git a/spec/models/service_desk_setting_spec.rb b/spec/models/service_desk_setting_spec.rb
index 32c36375a3d..b9679b82bd0 100644
--- a/spec/models/service_desk_setting_spec.rb
+++ b/spec/models/service_desk_setting_spec.rb
@@ -3,6 +3,9 @@
require 'spec_helper'
RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
+ let(:verification) { build(:service_desk_custom_email_verification) }
+ let(:project) { build(:project) }
+
describe 'validations' do
subject(:service_desk_setting) { create(:service_desk_setting) }
@@ -13,8 +16,6 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
it { is_expected.not_to allow_value('abc 12').for(:project_key).with_message("can contain only lowercase letters, digits, and '_'.") }
it { is_expected.not_to allow_value('Big val').for(:project_key) }
it { is_expected.to validate_length_of(:custom_email).is_at_most(255) }
- it { is_expected.to validate_length_of(:custom_email_smtp_address).is_at_most(255) }
- it { is_expected.to validate_length_of(:custom_email_smtp_username).is_at_most(255) }
describe '#custom_email_enabled' do
it { expect(subject.custom_email_enabled).to be_falsey }
@@ -23,6 +24,7 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
context 'when custom_email_enabled is true' do
before do
+ # Test without ServiceDesk::CustomEmailVerification for simplicity
subject.custom_email_enabled = true
end
@@ -42,20 +44,9 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
it { is_expected.not_to allow_value('"><script>alert(1);</script>"@example.org').for(:custom_email) }
it { is_expected.not_to allow_value('file://example').for(:custom_email) }
it { is_expected.not_to allow_value('no email at all').for(:custom_email) }
-
- it { is_expected.to validate_presence_of(:custom_email_smtp_username) }
-
- it { is_expected.to validate_presence_of(:custom_email_smtp_port) }
- it { is_expected.to validate_numericality_of(:custom_email_smtp_port).only_integer.is_greater_than(0) }
-
- it { is_expected.to validate_presence_of(:custom_email_smtp_address) }
- it { is_expected.to allow_value('smtp.gmail.com').for(:custom_email_smtp_address) }
- it { is_expected.not_to allow_value('https://example.com').for(:custom_email_smtp_address) }
- it { is_expected.not_to allow_value('file://example').for(:custom_email_smtp_address) }
- it { is_expected.not_to allow_value('/example').for(:custom_email_smtp_address) }
end
- describe '.valid_issue_template' do
+ describe '#valid_issue_template' do
let_it_be(:project) { create(:project, :custom_repo, files: { '.gitlab/issue_templates/service_desk.md' => 'template' }) }
it 'is not valid if template does not exist' do
@@ -73,13 +64,26 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
end
end
- describe '.valid_project_key' do
+ describe '#custom_email_address_for_verification' do
+ it 'returns nil' do
+ expect(subject.custom_email_address_for_verification).to be_nil
+ end
+
+ context 'when custom_email exists' do
+ it 'returns correct verification address' do
+ subject.custom_email = 'support@example.com'
+ expect(subject.custom_email_address_for_verification).to eq('support+verify@example.com')
+ end
+ end
+ end
+
+ describe '#valid_project_key' do
# Creates two projects with same full path slug
# group1/test/one and group1/test-one will both have 'group-test-one' slug
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group, name: 'test') }
- let_it_be(:project1) { create(:project, name: 'test-one', group: group) }
- let_it_be(:project2) { create(:project, name: 'one', group: subgroup) }
+ let_it_be(:project1) { create(:project, path: 'test-one', group: group) }
+ let_it_be(:project2) { create(:project, path: 'one', group: subgroup) }
let_it_be(:project_key) { 'key' }
let!(:setting) do
create(:service_desk_setting, project: project1, project_key: project_key)
@@ -109,28 +113,21 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
end
end
- describe 'encrypted password' do
- let_it_be(:settings) do
- create(
+ describe 'associations' do
+ let(:custom_email_settings) do
+ build_stubbed(
:service_desk_setting,
- custom_email_enabled: true,
- custom_email: 'supersupport@example.com',
- custom_email_smtp_address: 'smtp.example.com',
- custom_email_smtp_port: 587,
- custom_email_smtp_username: 'supersupport@example.com',
- custom_email_smtp_password: 'supersecret'
+ custom_email: 'support@example.com'
)
end
- it 'saves and retrieves the encrypted custom email smtp password and iv correctly' do
- expect(settings.encrypted_custom_email_smtp_password).not_to be_nil
- expect(settings.encrypted_custom_email_smtp_password_iv).not_to be_nil
+ it { is_expected.to belong_to(:project) }
- expect(settings.custom_email_smtp_password).to eq('supersecret')
- end
- end
+ it 'can access custom email verification from project' do
+ project.service_desk_custom_email_verification = verification
+ custom_email_settings.project = project
- describe 'associations' do
- it { is_expected.to belong_to(:project) }
+ expect(custom_email_settings.custom_email_verification).to eq(verification)
+ end
end
end
diff --git a/spec/models/slack_integration_spec.rb b/spec/models/slack_integration_spec.rb
new file mode 100644
index 00000000000..41beeee598c
--- /dev/null
+++ b/spec/models/slack_integration_spec.rb
@@ -0,0 +1,147 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe SlackIntegration, feature_category: :integrations do
+ describe "Associations" do
+ it { is_expected.to belong_to(:integration) }
+ end
+
+ describe 'authorized_scope_names' do
+ subject(:slack_integration) { create(:slack_integration) }
+
+ it 'accepts assignment to nil' do
+ slack_integration.update!(authorized_scope_names: nil)
+
+ expect(slack_integration.authorized_scope_names).to be_empty
+ end
+
+ it 'accepts assignment to a string' do
+ slack_integration.update!(authorized_scope_names: 'foo')
+
+ expect(slack_integration.authorized_scope_names).to contain_exactly('foo')
+ end
+
+ it 'accepts assignment to an array of strings' do
+ slack_integration.update!(authorized_scope_names: %w[foo bar])
+
+ expect(slack_integration.authorized_scope_names).to contain_exactly('foo', 'bar')
+ end
+
+ it 'accepts assignment to a comma-separated string' do
+ slack_integration.update!(authorized_scope_names: 'foo,bar')
+
+ expect(slack_integration.authorized_scope_names).to contain_exactly('foo', 'bar')
+ end
+
+ it 'strips white-space' do
+ slack_integration.update!(authorized_scope_names: 'foo , bar,baz')
+
+ expect(slack_integration.authorized_scope_names).to contain_exactly('foo', 'bar', 'baz')
+ end
+ end
+
+ describe 'all_features_supported?/upgrade_needed?' do
+ subject(:slack_integration) { create(:slack_integration) }
+
+ context 'with enough scopes' do
+ before do
+ slack_integration.update!(authorized_scope_names: %w[chat:write.public chat:write commands])
+ end
+
+ it { is_expected.to be_all_features_supported }
+ it { is_expected.not_to be_upgrade_needed }
+ end
+
+ %w[chat:write.public chat:write].each do |scope_name|
+ context "without #{scope_name}" do
+ before do
+ scopes = %w[chat:write.public chat:write] - [scope_name]
+ slack_integration.update!(authorized_scope_names: scopes)
+ end
+
+ it { is_expected.not_to be_all_features_supported }
+ it { is_expected.to be_upgrade_needed }
+ end
+ end
+ end
+
+ describe 'feature_available?' do
+ subject(:slack_integration) { create(:slack_integration) }
+
+ context 'without any scopes' do
+ it 'is always true for :commands' do
+ expect(slack_integration).to be_feature_available(:commands)
+ end
+
+ it 'is always false for others' do
+ expect(slack_integration).not_to be_feature_available(:notifications)
+ expect(slack_integration).not_to be_feature_available(:foo)
+ end
+ end
+
+ context 'with enough scopes for notifications' do
+ before do
+ slack_integration.update!(authorized_scope_names: %w[chat:write.public chat:write foo])
+ end
+
+ it 'only has the correct features' do
+ expect(slack_integration).to be_feature_available(:commands)
+ expect(slack_integration).to be_feature_available(:notifications)
+ expect(slack_integration).not_to be_feature_available(:foo)
+ end
+ end
+
+ context 'with enough scopes for commands' do
+ before do
+ slack_integration.update!(authorized_scope_names: %w[commands foo])
+ end
+
+ it 'only has the correct features' do
+ expect(slack_integration).to be_feature_available(:commands)
+ expect(slack_integration).not_to be_feature_available(:notifications)
+ expect(slack_integration).not_to be_feature_available(:foo)
+ end
+ end
+
+ context 'with all scopes' do
+ before do
+ slack_integration.update!(authorized_scope_names: %w[commands chat:write chat:write.public])
+ end
+
+ it 'only has the correct features' do
+ expect(slack_integration).to be_feature_available(:commands)
+ expect(slack_integration).to be_feature_available(:notifications)
+ expect(slack_integration).not_to be_feature_available(:foo)
+ end
+ end
+ end
+
+ describe 'Scopes' do
+ let_it_be(:slack_integration) { create(:slack_integration) }
+ let_it_be(:legacy_slack_integration) { create(:slack_integration, :legacy) }
+
+ describe '#with_bot' do
+ it 'returns records with bot data' do
+ expect(described_class.with_bot).to contain_exactly(slack_integration)
+ end
+ end
+
+ describe '#by_team' do
+ it 'returns records with shared team_id' do
+ team_id = slack_integration.team_id
+ team_slack_integration = create(:slack_integration, team_id: team_id)
+
+ expect(described_class.by_team(team_id)).to contain_exactly(slack_integration, team_slack_integration)
+ end
+ end
+ end
+
+ describe 'Validations' do
+ it { is_expected.to validate_presence_of(:team_id) }
+ it { is_expected.to validate_presence_of(:team_name) }
+ it { is_expected.to validate_presence_of(:alias) }
+ it { is_expected.to validate_presence_of(:user_id) }
+ it { is_expected.to validate_presence_of(:integration) }
+ end
+end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index da1f2653676..6a5456fce3f 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -41,8 +41,8 @@ RSpec.describe Snippet do
is_expected
.to validate_length_of(:content)
- .is_at_most(Gitlab::CurrentSettings.snippet_size_limit)
- .with_message("is too long (2 Bytes). The maximum size is 1 Byte.")
+ .is_at_most(Gitlab::CurrentSettings.snippet_size_limit)
+ .with_message("is too long (2 Bytes). The maximum size is 1 Byte.")
end
context 'content validations' do
@@ -145,7 +145,7 @@ RSpec.describe Snippet do
describe '#to_reference' do
context 'when snippet belongs to a project' do
- let(:project) { build(:project, name: 'sample-project') }
+ let(:project) { build(:project) }
let(:snippet) { build(:snippet, id: 1, project: project) }
it 'returns a String reference to the object' do
@@ -153,8 +153,8 @@ RSpec.describe Snippet do
end
it 'supports a cross-project reference' do
- another_project = build(:project, name: 'another-project', namespace: project.namespace)
- expect(snippet.to_reference(another_project)).to eq "sample-project$1"
+ another_project = build(:project, namespace: project.namespace)
+ expect(snippet.to_reference(another_project)).to eq "#{project.path}$1"
end
end
@@ -166,7 +166,7 @@ RSpec.describe Snippet do
end
it 'still returns shortest reference when project arg present' do
- another_project = build(:project, name: 'another-project')
+ another_project = build(:project)
expect(snippet.to_reference(another_project)).to eq "$1"
end
end
@@ -492,17 +492,21 @@ RSpec.describe Snippet do
let_it_be(:snippet) { create(:snippet, content: 'foo', project: project) }
let_it_be(:note1) do
- create(:note_on_project_snippet,
- noteable: snippet,
- project: project,
- note: 'a')
+ create(
+ :note_on_project_snippet,
+ noteable: snippet,
+ project: project,
+ note: 'a'
+ )
end
let_it_be(:note2) do
- create(:note_on_project_snippet,
- noteable: snippet,
- project: project,
- note: 'b')
+ create(
+ :note_on_project_snippet,
+ noteable: snippet,
+ project: project,
+ note: 'b'
+ )
end
it 'includes the snippet author and note authors' do
diff --git a/spec/models/spam_log_spec.rb b/spec/models/spam_log_spec.rb
index 564710b31d0..549a4e92ce0 100644
--- a/spec/models/spam_log_spec.rb
+++ b/spec/models/spam_log_spec.rb
@@ -30,8 +30,7 @@ RSpec.describe SpamLog do
end
expect(
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin)
+ Users::GhostUserMigration.where(user: user, initiator_user: admin)
).to be_exists
end
end
diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb
index 533e6e4bd7b..fc0a6432149 100644
--- a/spec/models/terraform/state_spec.rb
+++ b/spec/models/terraform/state_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Terraform::State do
+RSpec.describe Terraform::State, feature_category: :infrastructure_as_code do
subject { create(:terraform_state, :with_version) }
it { is_expected.to belong_to(:project) }
diff --git a/spec/models/terraform/state_version_spec.rb b/spec/models/terraform/state_version_spec.rb
index 477041117cb..a476b9e79ae 100644
--- a/spec/models/terraform/state_version_spec.rb
+++ b/spec/models/terraform/state_version_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe Terraform::StateVersion do
+RSpec.describe Terraform::StateVersion, feature_category: :infrastructure_as_code do
it { is_expected.to be_a FileStoreMounter }
it { is_expected.to be_a EachBatch }
- it { is_expected.to belong_to(:terraform_state).required }
+ it { is_expected.to belong_to(:terraform_state).required.touch }
it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
it { is_expected.to belong_to(:build).class_name('Ci::Build').optional }
diff --git a/spec/models/u2f_registration_spec.rb b/spec/models/u2f_registration_spec.rb
deleted file mode 100644
index 1fab3882c2a..00000000000
--- a/spec/models/u2f_registration_spec.rb
+++ /dev/null
@@ -1,141 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe U2fRegistration do
- let_it_be(:user) { create(:user) }
-
- let(:u2f_registration_name) { 'u2f_device' }
- let(:app_id) { FFaker::BaconIpsum.characters(5) }
- let(:device) { U2F::FakeU2F.new(app_id) }
-
- describe '.authenticate' do
- context 'when registration is found' do
- it 'returns true' do
- create_u2f_registration
- device_challenge = U2F.urlsafe_encode64(SecureRandom.random_bytes(32))
- sign_response_json = device.sign_response(device_challenge)
-
- response = U2fRegistration.authenticate(
- user,
- app_id,
- sign_response_json,
- device_challenge
- )
-
- expect(response).to eq true
- end
- end
-
- context 'when registration not found' do
- it 'returns nil' do
- device_challenge = U2F.urlsafe_encode64(SecureRandom.random_bytes(32))
- sign_response_json = device.sign_response(device_challenge)
-
- # data is valid but user does not have any u2f_registrations
- response = U2fRegistration.authenticate(
- user,
- app_id,
- sign_response_json,
- device_challenge
- )
-
- expect(response).to eq nil
- end
- end
-
- context 'when args passed in are invalid' do
- it 'returns false' do
- some_app_id = 123
- invalid_json = 'invalid JSON'
- challenges = 'whatever'
-
- response = U2fRegistration.authenticate(
- user,
- some_app_id,
- invalid_json,
- challenges
- )
-
- expect(response).to eq false
- end
- end
- end
-
- describe 'callbacks' do
- describe 'after create' do
- shared_examples_for 'creates webauthn registration' do
- it 'creates webauthn registration' do
- u2f_registration = create_u2f_registration
- webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id)
- expect(webauthn_registration).to exist
- end
- end
-
- it_behaves_like 'creates webauthn registration'
-
- context 'when the u2f_registration has a blank name' do
- let(:u2f_registration_name) { '' }
-
- it_behaves_like 'creates webauthn registration'
- end
-
- context 'when the u2f_registration has the name as `nil`' do
- let(:u2f_registration_name) { nil }
-
- it_behaves_like 'creates webauthn registration'
- end
-
- it 'logs error' do
- allow(Gitlab::Auth::U2fWebauthnConverter).to receive(:new).and_raise('boom!')
-
- allow_next_instance_of(U2fRegistration) do |u2f_registration|
- allow(u2f_registration).to receive(:id).and_return(123)
- end
-
- expect(Gitlab::ErrorTracking).to(
- receive(:track_exception).with(kind_of(StandardError),
- u2f_registration_id: 123))
-
- create_u2f_registration
- end
- end
-
- describe 'after update' do
- context 'when counter is updated' do
- it 'updates the webauthn registration counter to be the same value' do
- u2f_registration = create_u2f_registration
- new_counter = u2f_registration.counter + 1
- webauthn_registration = WebauthnRegistration.find_by(u2f_registration_id: u2f_registration.id)
-
- u2f_registration.update!(counter: new_counter)
-
- expect(u2f_registration.reload.counter).to eq(new_counter)
- expect(webauthn_registration.reload.counter).to eq(new_counter)
- end
- end
-
- context 'when sign count of registration is not updated' do
- it 'does not update the counter' do
- u2f_registration = create_u2f_registration
- webauthn_registration = WebauthnRegistration.find_by(u2f_registration_id: u2f_registration.id)
-
- expect do
- u2f_registration.update!(name: 'a new name')
- end.not_to change { webauthn_registration.counter }
- end
- end
- end
- end
-
- def create_u2f_registration
- create(
- :u2f_registration,
- name: u2f_registration_name,
- user: user,
- certificate: Base64.strict_encode64(device.cert_raw),
- key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
- public_key: Base64.strict_encode64(device.origin_public_key_raw)
- )
- end
-end
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index cdf73b203af..27e2060a94b 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -96,9 +96,11 @@ RSpec.describe Upload do
describe '#calculate_checksum!' do
let(:upload) do
- described_class.new(path: __FILE__,
- size: described_class::CHECKSUM_THRESHOLD - 1.megabyte,
- store: ObjectStorage::Store::LOCAL)
+ described_class.new(
+ path: __FILE__,
+ size: described_class::CHECKSUM_THRESHOLD - 1.megabyte,
+ store: ObjectStorage::Store::LOCAL
+ )
end
it 'sets `checksum` to SHA256 sum of the file' do
diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb
index 7d433896cf8..428fd5470c3 100644
--- a/spec/models/user_detail_spec.rb
+++ b/spec/models/user_detail_spec.rb
@@ -91,15 +91,17 @@ RSpec.describe UserDetail do
describe '#save' do
let(:user_detail) do
- create(:user_detail,
- bio: 'bio',
- discord: '1234567890123456789',
- linkedin: 'linkedin',
- location: 'location',
- organization: 'organization',
- skype: 'skype',
- twitter: 'twitter',
- website_url: 'https://example.com')
+ create(
+ :user_detail,
+ bio: 'bio',
+ discord: '1234567890123456789',
+ linkedin: 'linkedin',
+ location: 'location',
+ organization: 'organization',
+ skype: 'skype',
+ twitter: 'twitter',
+ website_url: 'https://example.com'
+ )
end
shared_examples 'prevents `nil` value' do |attr|
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
index a6f64c90657..1d7ecb724bf 100644
--- a/spec/models/user_preference_spec.rb
+++ b/spec/models/user_preference_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UserPreference do
+RSpec.describe UserPreference, feature_category: :user_profile do
let_it_be(:user) { create(:user) }
let(:user_preference) { create(:user_preference, user: user) }
@@ -48,11 +48,21 @@ RSpec.describe UserPreference do
end
end
- describe 'use_legacy_web_ide' do
- it { is_expected.to allow_value(true).for(:use_legacy_web_ide) }
- it { is_expected.to allow_value(false).for(:use_legacy_web_ide) }
- it { is_expected.not_to allow_value(nil).for(:use_legacy_web_ide) }
- it { is_expected.not_to allow_value("").for(:use_legacy_web_ide) }
+ describe 'pass_user_identities_to_ci_jwt' do
+ it { is_expected.to allow_value(true).for(:pass_user_identities_to_ci_jwt) }
+ it { is_expected.to allow_value(false).for(:pass_user_identities_to_ci_jwt) }
+ it { is_expected.not_to allow_value(nil).for(:pass_user_identities_to_ci_jwt) }
+ it { is_expected.not_to allow_value("").for(:pass_user_identities_to_ci_jwt) }
+ end
+
+ describe 'visibility_pipeline_id_type' do
+ it 'is set to 0 by default' do
+ pref = described_class.new
+
+ expect(pref.visibility_pipeline_id_type).to eq('id')
+ end
+
+ it { is_expected.to define_enum_for(:visibility_pipeline_id_type).with_values(id: 0, iid: 1) }
end
end
@@ -215,47 +225,6 @@ RSpec.describe UserPreference do
end
end
- describe '#time_format_in_24h' do
- it 'is set to false by default' do
- pref = described_class.new
-
- expect(pref.time_format_in_24h).to eq(false)
- end
-
- it 'returns default value when assigning nil' do
- pref = described_class.new(time_format_in_24h: nil)
-
- expect(pref.time_format_in_24h).to eq(false)
- end
-
- it 'returns default value when the value is NULL' do
- pref = create(:user_preference, user: user)
- pref.update_column(:time_format_in_24h, nil)
-
- expect(pref.reload.time_format_in_24h).to eq(false)
- end
-
- it 'returns assigned value' do
- pref = described_class.new(time_format_in_24h: true)
-
- expect(pref.time_format_in_24h).to eq(true)
- end
- end
-
- describe '#time_format_in_24h=' do
- it 'sets to default value when nil' do
- pref = described_class.new(time_format_in_24h: nil)
-
- expect(pref.read_attribute(:time_format_in_24h)).to eq(false)
- end
-
- it 'sets user values' do
- pref = described_class.new(time_format_in_24h: true)
-
- expect(pref.read_attribute(:time_format_in_24h)).to eq(true)
- end
- end
-
describe '#render_whitespace_in_code' do
it 'is set to false by default' do
pref = described_class.new
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index e87667d9604..c73dac7251e 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -42,9 +42,6 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to delegate_method(:time_display_relative).to(:user_preference) }
it { is_expected.to delegate_method(:time_display_relative=).to(:user_preference).with_arguments(:args) }
- it { is_expected.to delegate_method(:time_format_in_24h).to(:user_preference) }
- it { is_expected.to delegate_method(:time_format_in_24h=).to(:user_preference).with_arguments(:args) }
-
it { is_expected.to delegate_method(:show_whitespace_in_diffs).to(:user_preference) }
it { is_expected.to delegate_method(:show_whitespace_in_diffs=).to(:user_preference).with_arguments(:args) }
@@ -78,12 +75,15 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to delegate_method(:diffs_addition_color).to(:user_preference) }
it { is_expected.to delegate_method(:diffs_addition_color=).to(:user_preference).with_arguments(:args) }
- it { is_expected.to delegate_method(:use_legacy_web_ide).to(:user_preference) }
- it { is_expected.to delegate_method(:use_legacy_web_ide=).to(:user_preference).with_arguments(:args) }
-
it { is_expected.to delegate_method(:use_new_navigation).to(:user_preference) }
it { is_expected.to delegate_method(:use_new_navigation=).to(:user_preference).with_arguments(:args) }
+ it { is_expected.to delegate_method(:pinned_nav_items).to(:user_preference) }
+ it { is_expected.to delegate_method(:pinned_nav_items=).to(:user_preference).with_arguments(:args) }
+
+ it { is_expected.to delegate_method(:achievements_enabled).to(:user_preference) }
+ it { is_expected.to delegate_method(:achievements_enabled=).to(:user_preference).with_arguments(:args) }
+
it { is_expected.to delegate_method(:job_title).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:job_title=).to(:user_detail).with_arguments(:args).allow_nil }
@@ -99,9 +99,6 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to delegate_method(:registration_objective).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:registration_objective=).to(:user_detail).with_arguments(:args).allow_nil }
- it { is_expected.to delegate_method(:requires_credit_card_verification).to(:user_detail).allow_nil }
- it { is_expected.to delegate_method(:requires_credit_card_verification=).to(:user_detail).with_arguments(:args).allow_nil }
-
it { is_expected.to delegate_method(:discord).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:discord=).to(:user_detail).with_arguments(:args).allow_nil }
@@ -174,6 +171,14 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to have_many(:revoked_user_achievements).class_name('Achievements::UserAchievement').with_foreign_key('revoked_by_user_id').inverse_of(:revoked_by_user) }
it { is_expected.to have_many(:achievements).through(:user_achievements).class_name('Achievements::Achievement').inverse_of(:users) }
it { is_expected.to have_many(:namespace_commit_emails).class_name('Users::NamespaceCommitEmail') }
+ it { is_expected.to have_many(:audit_events).with_foreign_key(:author_id).inverse_of(:user) }
+ it { is_expected.to have_many(:abuse_trust_scores).class_name('Abuse::TrustScore') }
+ it { is_expected.to have_many(:issue_assignment_events).class_name('ResourceEvents::IssueAssignmentEvent') }
+ it { is_expected.to have_many(:merge_request_assignment_events).class_name('ResourceEvents::MergeRequestAssignmentEvent') }
+
+ it do
+ is_expected.to have_many(:alert_assignees).class_name('::AlertManagement::AlertAssignee').inverse_of(:assignee)
+ end
describe 'default values' do
let(:user) { described_class.new }
@@ -188,6 +193,7 @@ RSpec.describe User, feature_category: :user_profile do
it { expect(user.notified_of_own_activity).to be_falsey }
it { expect(user.preferred_language).to eq(Gitlab::CurrentSettings.default_preferred_language) }
it { expect(user.theme_id).to eq(described_class.gitlab_config.default_theme) }
+ it { expect(user.color_scheme_id).to eq(Gitlab::CurrentSettings.default_syntax_highlighting_theme) }
end
describe '#user_detail' do
@@ -282,6 +288,31 @@ RSpec.describe User, feature_category: :user_profile do
end
end
+ describe '#abuse_metadata' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:contribution_calendar) { Gitlab::ContributionsCalendar.new(user) }
+
+ before do
+ allow(Gitlab::ContributionsCalendar).to receive(:new).and_return(contribution_calendar)
+ allow(contribution_calendar).to receive(:activity_dates).and_return({ first: 3, second: 5, third: 4 })
+
+ allow(user).to receive_messages(
+ account_age_in_days: 10,
+ two_factor_enabled?: true
+ )
+ end
+
+ it 'returns the expected hash' do
+ abuse_metadata = user.abuse_metadata
+
+ expect(abuse_metadata.length).to eq 2
+ expect(abuse_metadata).to include(
+ account_age: 10,
+ two_factor_enabled: 1
+ )
+ end
+ end
+
describe '#group_members' do
it 'does not include group memberships for which user is a requester' do
user = create(:user)
@@ -474,6 +505,37 @@ RSpec.describe User, feature_category: :user_profile do
expect(user).to be_valid
end
end
+
+ context 'namespace_move_dir_allowed' do
+ context 'when the user is not a new record' do
+ before do
+ expect(user.new_record?).to eq(false)
+ end
+
+ it 'checks when username changes' do
+ expect(user).to receive(:namespace_move_dir_allowed)
+
+ user.username = 'newuser'
+ user.validate
+ end
+
+ it 'does not check if the username did not change' do
+ expect(user).not_to receive(:namespace_move_dir_allowed)
+ expect(user.username_changed?).to eq(false)
+
+ user.validate
+ end
+ end
+
+ it 'does not check if the user is a new record' do
+ user = User.new(username: 'newuser')
+
+ expect(user.new_record?).to eq(true)
+ expect(user).not_to receive(:namespace_move_dir_allowed)
+
+ user.validate
+ end
+ end
end
describe 'name' do
@@ -978,10 +1040,6 @@ RSpec.describe User, feature_category: :user_profile do
end
end
- describe 'and U2F' do
- it_behaves_like "returns the right users", :two_factor_via_u2f
- end
-
describe 'and WebAuthn' do
it_behaves_like "returns the right users", :two_factor_via_webauthn
end
@@ -997,26 +1055,6 @@ RSpec.describe User, feature_category: :user_profile do
expect(users_without_two_factor).not_to include(user_with_2fa.id)
end
- 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)
- 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
-
describe 'and webauthn' do
it 'excludes users with 2fa enabled via WebAuthn' do
user_with_2fa = create(:user, :two_factor_via_webauthn)
@@ -2076,7 +2114,7 @@ RSpec.describe User, feature_category: :user_profile do
let_it_be(:incoming_email_token) { 'ilqx6jm1u945macft4eff0nw' }
it 'returns incoming email token when supported' do
- allow(Gitlab::IncomingEmail).to receive(:supports_issue_creation?).and_return(true)
+ allow(Gitlab::Email::IncomingEmail).to receive(:supports_issue_creation?).and_return(true)
user = create(:user, incoming_email_token: incoming_email_token)
@@ -2084,7 +2122,7 @@ RSpec.describe User, feature_category: :user_profile do
end
it 'returns `nil` when not supported' do
- allow(Gitlab::IncomingEmail).to receive(:supports_issue_creation?).and_return(false)
+ allow(Gitlab::Email::IncomingEmail).to receive(:supports_issue_creation?).and_return(false)
user = create(:user, incoming_email_token: incoming_email_token)
@@ -2112,6 +2150,59 @@ RSpec.describe User, feature_category: :user_profile do
end
end
+ describe '#remember_me!' do
+ let(:user) { create(:user) }
+
+ context 'when remember me application setting is enabled' do
+ before do
+ stub_application_setting(remember_me_enabled: true)
+ end
+
+ it 'sets rememberable attributes' do
+ expect(user.remember_created_at).to be_nil
+
+ user.remember_me!
+
+ expect(user.remember_created_at).not_to be_nil
+ end
+ end
+
+ context 'when remember me application setting is not enabled' do
+ before do
+ stub_application_setting(remember_me_enabled: false)
+ end
+
+ it 'sets rememberable attributes' do
+ expect(user.remember_created_at).to be_nil
+
+ user.remember_me!
+
+ expect(user.remember_created_at).to be_nil
+ end
+ end
+ end
+
+ describe '#forget_me!' do
+ let(:user) { create(:user) }
+
+ context 'when remember me application setting is disabled' do
+ before do
+ stub_application_setting(remember_me_enabled: true)
+ end
+
+ it 'allows user to be forgotten when previously remembered' do
+ user.remember_me!
+
+ expect(user.remember_created_at).not_to be_nil
+
+ stub_application_setting(remember_me_enabled: false)
+ user.forget_me!
+
+ expect(user.remember_created_at).to be_nil
+ end
+ end
+ end
+
describe '#disable_two_factor!' do
it 'clears all 2FA-related fields' do
user = create(:user, :two_factor)
@@ -2174,6 +2265,24 @@ RSpec.describe User, feature_category: :user_profile do
end
end
+ context 'Duo Auth' do
+ context 'when enabled via GitLab settings' do
+ before do
+ allow(::Gitlab.config.duo_auth).to receive(:enabled).and_return(true)
+ end
+
+ it { expect(user.two_factor_otp_enabled?).to eq(true) }
+ end
+
+ context 'when disabled via GitLab settings' do
+ before do
+ allow(::Gitlab.config.duo_auth).to receive(:enabled).and_return(false)
+ end
+
+ it { expect(user.two_factor_otp_enabled?).to eq(false) }
+ end
+ end
+
context 'FortiTokenCloud' do
context 'when enabled via GitLab settings' do
before do
@@ -2207,54 +2316,6 @@ RSpec.describe User, feature_category: :user_profile do
end
end
- context 'two_factor_u2f_enabled?' do
- let_it_be(:user) { create(:user, :two_factor) }
-
- context 'when webauthn feature flag is enabled' do
- context 'user has no U2F registration' do
- it { expect(user.two_factor_u2f_enabled?).to eq(false) }
- end
-
- context 'user has existing U2F registration' do
- it 'returns false' do
- device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
- create(:u2f_registration,
- name: 'my u2f device',
- user: user,
- certificate: Base64.strict_encode64(device.cert_raw),
- key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
- public_key: Base64.strict_encode64(device.origin_public_key_raw))
-
- expect(user.two_factor_u2f_enabled?).to eq(false)
- end
- end
- end
-
- context 'when webauthn feature flag is disabled' do
- before do
- stub_feature_flags(webauthn: false)
- end
-
- context 'user has no U2F registration' do
- it { expect(user.two_factor_u2f_enabled?).to eq(false) }
- end
-
- context 'user has existing U2F registration' do
- it 'returns true' do
- device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
- create(:u2f_registration,
- name: 'my u2f device',
- user: user,
- certificate: Base64.strict_encode64(device.cert_raw),
- key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
- public_key: Base64.strict_encode64(device.origin_public_key_raw))
-
- expect(user.two_factor_u2f_enabled?).to eq(true)
- end
- end
- end
- end
-
describe 'needs_new_otp_secret?', :freeze_time do
let(:user) { create(:user) }
@@ -2380,6 +2441,20 @@ RSpec.describe User, feature_category: :user_profile do
expect(user.forkable_namespaces).to contain_exactly(user.namespace, group, subgroup, developer_group)
end
+
+ it 'includes groups where the user has access via group shares to create projects' do
+ shared_group = create(:group)
+ create(
+ :group_group_link,
+ :maintainer,
+ shared_with_group: group,
+ shared_group: shared_group
+ )
+
+ expect(user.forkable_namespaces).to contain_exactly(
+ user.namespace, group, subgroup, shared_group
+ )
+ end
end
describe '#manageable_groups' do
@@ -2396,14 +2471,6 @@ RSpec.describe User, feature_category: :user_profile do
end
it_behaves_like 'manageable groups examples'
-
- context 'when feature flag :linear_user_manageable_groups is disabled' do
- before do
- stub_feature_flags(linear_user_manageable_groups: false)
- end
-
- it_behaves_like 'manageable groups examples'
- end
end
end
end
@@ -3847,6 +3914,54 @@ RSpec.describe User, feature_category: :user_profile do
expect(user.following?(followee)).to be_falsey
end
+
+ it 'does not follow if user disabled following' do
+ user = create(:user)
+ user.enabled_following = false
+
+ followee = create(:user)
+
+ expect(user.follow(followee)).to eq(false)
+
+ expect(user.following?(followee)).to be_falsey
+ end
+
+ it 'does not follow if followee user disabled following' do
+ user = create(:user)
+
+ followee = create(:user)
+ followee.enabled_following = false
+
+ expect(user.follow(followee)).to eq(false)
+
+ expect(user.following?(followee)).to be_falsey
+ end
+
+ context 'when disable_follow_users feature flag is off' do
+ before do
+ stub_feature_flags(disable_follow_users: false)
+ end
+
+ it 'follows user even if user disabled following' do
+ user = create(:user)
+ user.enabled_following = false
+
+ followee = create(:user)
+
+ expect(user.follow(followee)).to be_truthy
+ expect(user.following?(followee)).to be_truthy
+ end
+
+ it 'follows user even if followee user disabled following' do
+ user = create(:user)
+
+ followee = create(:user)
+ followee.enabled_following = false
+
+ expect(user.follow(followee)).to be_truthy
+ expect(user.following?(followee)).to be_truthy
+ end
+ end
end
describe '#unfollow' do
@@ -3889,6 +4004,39 @@ RSpec.describe User, feature_category: :user_profile do
end
end
+ describe '#following_users_allowed?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:followee) { create(:user) }
+
+ where(:user_enabled_following, :followee_enabled_following, :feature_flag_status, :result) do
+ true | true | false | true
+ true | false | false | true
+ true | true | true | true
+ true | false | true | false
+ false | true | false | true
+ false | true | true | false
+ false | false | false | true
+ false | false | true | false
+ end
+
+ with_them do
+ before do
+ user.enabled_following = user_enabled_following
+ followee.enabled_following = followee_enabled_following
+ followee.save!
+ stub_feature_flags(disable_follow_users: feature_flag_status)
+ end
+
+ it { expect(user.following_users_allowed?(followee)).to eq result }
+ end
+
+ it 'is false when user and followee is the same user' do
+ expect(user.following_users_allowed?(user)).to eq(false)
+ end
+ end
+
describe '#notification_email_or_default' do
let(:email) { 'gonzo@muppets.com' }
@@ -5225,13 +5373,13 @@ RSpec.describe User, feature_category: :user_profile do
end
describe '#source_groups_of_two_factor_authentication_requirement' do
- let_it_be(:group_not_requiring_2FA) { create :group }
+ let_it_be(:group_not_requiring_2fa) { create :group }
let(:user) { create :user }
before do
group.add_member(user, GroupMember::OWNER)
- group_not_requiring_2FA.add_member(user, GroupMember::OWNER)
+ group_not_requiring_2fa.add_member(user, GroupMember::OWNER)
end
context 'when user is direct member of group requiring 2FA' do
@@ -5713,6 +5861,34 @@ RSpec.describe User, feature_category: :user_profile do
expect(user).not_to be_blocked
end
+
+ context 'when target user is the same as deleted_by' do
+ let(:deleted_by) { user }
+
+ it 'blocks the user and schedules the record for deletion with the correct delay' do
+ freeze_time do
+ expect(DeleteUserWorker).to receive(:perform_in).with(7.days, user.id, user.id, {})
+
+ user.delete_async(deleted_by: deleted_by)
+
+ expect(user).to be_blocked
+ end
+ end
+
+ context 'when delay_delete_own_user feature flag is disabled' do
+ before do
+ stub_feature_flags(delay_delete_own_user: false)
+ end
+
+ it 'schedules user for deletion without blocking them' do
+ expect(DeleteUserWorker).to receive(:perform_async).with(user.id, user.id, {})
+
+ user.delete_async(deleted_by: deleted_by)
+
+ expect(user).not_to be_blocked
+ end
+ end
+ end
end
describe '#max_member_access_for_project_ids' do
@@ -5777,8 +5953,10 @@ RSpec.describe User, feature_category: :user_profile do
all_projects = projects + [second_maintainer_project.id, second_developer_project.id]
- expected_all = expected.merge(second_maintainer_project.id => Gitlab::Access::MAINTAINER,
- second_developer_project.id => Gitlab::Access::DEVELOPER)
+ expected_all = expected.merge(
+ second_maintainer_project.id => Gitlab::Access::MAINTAINER,
+ second_developer_project.id => Gitlab::Access::DEVELOPER
+ )
access_levels(projects)
@@ -5856,8 +6034,10 @@ RSpec.describe User, feature_category: :user_profile do
all_groups = groups + [second_maintainer_group.id, second_developer_group.id]
- expected_all = expected.merge(second_maintainer_group.id => Gitlab::Access::MAINTAINER,
- second_developer_group.id => Gitlab::Access::DEVELOPER)
+ expected_all = expected.merge(
+ second_maintainer_group.id => Gitlab::Access::MAINTAINER,
+ second_developer_group.id => Gitlab::Access::DEVELOPER
+ )
access_levels(groups)
@@ -6566,11 +6746,13 @@ RSpec.describe User, feature_category: :user_profile do
context 'when dismissed callout exists' do
before_all do
- create(:group_callout,
- user: user,
- group_id: group.id,
- feature_name: feature_name,
- dismissed_at: 4.months.ago)
+ create(
+ :group_callout,
+ user: user,
+ group_id: group.id,
+ feature_name: feature_name,
+ dismissed_at: 4.months.ago
+ )
end
it 'returns true when no ignore_dismissal_earlier_than provided' do
@@ -6600,11 +6782,13 @@ RSpec.describe User, feature_category: :user_profile do
context 'when dismissed callout exists' do
before_all do
- create(:project_callout,
- user: user,
- project_id: project.id,
- feature_name: feature_name,
- dismissed_at: 4.months.ago)
+ create(
+ :project_callout,
+ user: user,
+ project_id: project.id,
+ feature_name: feature_name,
+ dismissed_at: 4.months.ago
+ )
end
it 'returns true when no ignore_dismissal_earlier_than provided' do
@@ -6840,7 +7024,8 @@ RSpec.describe User, feature_category: :user_profile do
{ user_type: :support_bot },
{ user_type: :security_bot },
{ user_type: :automation_bot },
- { user_type: :admin_bot }
+ { user_type: :admin_bot },
+ { user_type: :llm_bot }
]
end
@@ -7002,6 +7187,14 @@ RSpec.describe User, feature_category: :user_profile do
it_behaves_like 'does not require password to be present'
end
+
+ context 'when user is a security_policy bot user' do
+ before do
+ user.update!(user_type: 'security_policy_bot')
+ end
+
+ it_behaves_like 'does not require password to be present'
+ end
end
describe 'can_trigger_notifications?' do
@@ -7104,42 +7297,103 @@ RSpec.describe User, feature_category: :user_profile do
context 'when user is confirmed' do
let(:user) { create(:user) }
- it 'is falsey' do
- expect(user.confirmed?).to be_truthy
- expect(subject).to be_falsey
+ it 'is false' do
+ expect(user.confirmed?).to be(true)
+ expect(subject).to be(false)
end
end
context 'when user is not confirmed' do
let_it_be(:user) { build_stubbed(:user, :unconfirmed, confirmation_sent_at: Time.current) }
- it 'is truthy when soft_email_confirmation feature is disabled' do
- stub_feature_flags(soft_email_confirmation: false)
- expect(subject).to be_truthy
+ context 'when email confirmation setting is set to `off`' do
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'off')
+ end
+
+ it { is_expected.to be(false) }
end
- context 'when soft_email_confirmation feature is enabled' do
+ context 'when email confirmation setting is set to `soft`' do
before do
- stub_feature_flags(soft_email_confirmation: true)
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
end
- it 'is falsey when confirmation period is valid' do
- expect(subject).to be_falsey
+ context 'when confirmation period is valid' do
+ it { is_expected.to be(false) }
end
- it 'is truthy when confirmation period is expired' do
- travel_to(User.allow_unconfirmed_access_for.from_now + 1.day) do
- expect(subject).to be_truthy
+ context 'when confirmation period is expired' do
+ before do
+ travel_to(User.allow_unconfirmed_access_for.from_now + 1.day)
end
+
+ it { is_expected.to be(true) }
end
context 'when user has no confirmation email sent' do
let(:user) { build(:user, :unconfirmed, confirmation_sent_at: nil) }
- it 'is truthy' do
- expect(subject).to be_truthy
- end
+ it { is_expected.to be(true) }
+ end
+ end
+
+ context 'when email confirmation setting is set to `hard`' do
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
+ end
+
+ it { is_expected.to be(true) }
+ end
+ end
+ end
+
+ describe '#confirmation_period_valid?' do
+ subject { user.send(:confirmation_period_valid?) }
+
+ let_it_be(:user) { create(:user) }
+
+ context 'when email confirmation setting is set to `off`' do
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'off')
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when email confirmation setting is set to `soft`' do
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
+ end
+
+ context 'when within confirmation window' do
+ before do
+ user.update!(confirmation_sent_at: Date.today)
end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when outside confirmation window' do
+ before do
+ user.update!(confirmation_sent_at: Date.today - described_class.confirm_within - 7.days)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
+
+ context 'when email confirmation setting is set to `hard`' do
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ describe '#in_confirmation_period?' do
+ it 'is expected to be an alias' do
+ expect(user.method(:in_confirmation_period?).original_name).to eq(:confirmation_period_valid?)
end
end
end
@@ -7433,9 +7687,11 @@ RSpec.describe User, feature_category: :user_profile do
context 'with a defined project namespace_commit_email' do
it 'returns the defined namespace_commit_email' do
- project_commit_email = create(:namespace_commit_email,
- user: user,
- namespace: project.project_namespace)
+ project_commit_email = create(
+ :namespace_commit_email,
+ user: user,
+ namespace: project.project_namespace
+ )
expect(emails).to eq(project_commit_email)
end
@@ -7455,9 +7711,11 @@ RSpec.describe User, feature_category: :user_profile do
context 'with a defined project namespace_commit_email' do
it 'returns the defined namespace_commit_email' do
- project_commit_email = create(:namespace_commit_email,
- user: user,
- namespace: project.project_namespace)
+ project_commit_email = create(
+ :namespace_commit_email,
+ user: user,
+ namespace: project.project_namespace
+ )
expect(emails).to eq(project_commit_email)
end
@@ -7476,9 +7734,11 @@ RSpec.describe User, feature_category: :user_profile do
context 'with a defined project namespace_commit_email' do
it 'returns the defined namespace_commit_email' do
- project_commit_email = create(:namespace_commit_email,
- user: user,
- namespace: project.project_namespace)
+ project_commit_email = create(
+ :namespace_commit_email,
+ user: user,
+ namespace: project.project_namespace
+ )
expect(emails).to eq(project_commit_email)
end
diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb
index 58b529ff18a..4db3683c057 100644
--- a/spec/models/users/credit_card_validation_spec.rb
+++ b/spec/models/users/credit_card_validation_spec.rb
@@ -51,4 +51,107 @@ RSpec.describe Users::CreditCardValidation do
end
end
end
+
+ describe 'scopes' do
+ describe '.by_banned_user' do
+ let(:banned_user) { create(:banned_user) }
+ let!(:credit_card) { create(:credit_card_validation) }
+ let!(:banned_user_credit_card) { create(:credit_card_validation, user: banned_user.user) }
+
+ it 'returns only records associated to banned users' do
+ expect(described_class.by_banned_user).to match_array([banned_user_credit_card])
+ end
+ end
+
+ describe '.similar_by_holder_name' do
+ let!(:credit_card) { create(:credit_card_validation, holder_name: 'CARD MCHODLER') }
+ let!(:credit_card2) { create(:credit_card_validation, holder_name: 'RICHIE RICH') }
+
+ it 'returns only records that case-insensitive match the given holder name' do
+ expect(described_class.similar_by_holder_name('card mchodler')).to match_array([credit_card])
+ end
+
+ context 'when given holder name is falsey' do
+ it 'returns [] when given holder name is ""' do
+ expect(described_class.similar_by_holder_name('')).to match_array([])
+ end
+
+ it 'returns [] when given holder name is nil' do
+ expect(described_class.similar_by_holder_name(nil)).to match_array([])
+ end
+ end
+ end
+
+ describe '.similar_to' do
+ let(:credit_card) { create(:credit_card_validation) }
+
+ let!(:credit_card2) do
+ create(:credit_card_validation,
+ expiration_date: credit_card.expiration_date,
+ last_digits: credit_card.last_digits,
+ network: credit_card.network
+ )
+ end
+
+ let!(:credit_card3) do
+ create(:credit_card_validation,
+ expiration_date: credit_card.expiration_date,
+ last_digits: credit_card.last_digits,
+ network: 'UnknownCCNetwork'
+ )
+ end
+
+ it 'returns only records with similar expiration_date, last_digits, and network attribute values' do
+ expect(described_class.similar_to(credit_card)).to match_array([credit_card, credit_card2])
+ end
+ end
+ end
+
+ describe '#used_by_banned_user?' do
+ let(:credit_card_details) do
+ {
+ holder_name: 'Christ McLovin',
+ expiration_date: 2.years.from_now.end_of_month,
+ last_digits: 4242,
+ network: 'Visa'
+ }
+ end
+
+ let!(:credit_card) { create(:credit_card_validation, credit_card_details) }
+
+ subject { credit_card }
+
+ context 'when there is a similar credit card associated to a banned user' do
+ let_it_be(:banned_user) { create(:banned_user) }
+
+ let(:attrs) { credit_card_details.merge({ user: banned_user.user }) }
+ let!(:similar_credit_card) { create(:credit_card_validation, attrs) }
+
+ it { is_expected.to be_used_by_banned_user }
+
+ context 'when holder names do not match' do
+ let!(:similar_credit_card) do
+ create(:credit_card_validation, attrs.merge({ holder_name: 'Mary Goody' }))
+ end
+
+ it { is_expected.not_to be_used_by_banned_user }
+ end
+
+ context 'when .similar_to returns nothing' do
+ let!(:similar_credit_card) do
+ create(:credit_card_validation, attrs.merge({ network: 'DifferentNetwork' }))
+ end
+
+ it { is_expected.not_to be_used_by_banned_user }
+ end
+ end
+
+ context 'when there is a similar credit card not associated to a banned user' do
+ let!(:similar_credit_card) do
+ create(:credit_card_validation, credit_card_details)
+ end
+
+ it { is_expected.not_to be_used_by_banned_user }
+ end
+ end
end
diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb
index 1b177934ace..c30e79f79ce 100644
--- a/spec/models/wiki_directory_spec.rb
+++ b/spec/models/wiki_directory_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe WikiDirectory do
let_it_be(:toplevel1) { build(:wiki_page, title: 'aaa-toplevel1') }
let_it_be(:toplevel2) { build(:wiki_page, title: 'zzz-toplevel2') }
let_it_be(:toplevel3) { build(:wiki_page, title: 'zzz-toplevel3') }
+ let_it_be(:home) { build(:wiki_page, title: 'home') }
+ let_it_be(:homechild) { build(:wiki_page, title: 'Home/homechild') }
let_it_be(:parent1) { build(:wiki_page, title: 'parent1') }
let_it_be(:parent2) { build(:wiki_page, title: 'parent2') }
let_it_be(:child1) { build(:wiki_page, title: 'parent1/child1') }
@@ -24,13 +26,18 @@ RSpec.describe WikiDirectory do
it 'returns a nested array of entries' do
entries = described_class.group_pages(
- [toplevel1, toplevel2, toplevel3,
+ [toplevel1, toplevel2, toplevel3, home, homechild,
parent1, parent2, child1, child2, child3,
subparent, grandchild1, grandchild2].sort_by(&:title)
)
expect(entries).to match(
[
+ a_kind_of(WikiDirectory).and(
+ having_attributes(
+ slug: 'Home', entries: [homechild]
+ )
+ ),
toplevel1,
a_kind_of(WikiDirectory).and(
having_attributes(
diff --git a/spec/models/wiki_page/meta_spec.rb b/spec/models/wiki_page/meta_spec.rb
index 4d1a2dc1c98..5808be128e0 100644
--- a/spec/models/wiki_page/meta_spec.rb
+++ b/spec/models/wiki_page/meta_spec.rb
@@ -167,10 +167,12 @@ RSpec.describe WikiPage::Meta do
end
def create_previous_version(title: old_title, slug: last_known_slug, date: wiki_page.version.commit.committed_date)
- create(:wiki_page_meta,
- title: title, project: project,
- created_at: date, updated_at: date,
- canonical_slug: slug)
+ create(
+ :wiki_page_meta,
+ title: title, project: project,
+ created_at: date, updated_at: date,
+ canonical_slug: slug
+ )
end
def create_context
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 21da06a222f..efade74688a 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -588,6 +588,20 @@ RSpec.describe WikiPage do
expect(page.content).to eq new_content
end
+ context 'when page combine with directory' do
+ it 'moving the file and directory' do
+ wiki.create_page('testpage/testtitle', 'content')
+ wiki.create_page('testpage', 'content')
+
+ page = wiki.find_page('testpage')
+ page.update(title: 'testfolder/testpage')
+
+ page = wiki.find_page('testfolder/testpage/testtitle')
+
+ expect(page.slug).to eq 'testfolder/testpage/testtitle'
+ end
+ end
+
describe 'in subdir' do
it 'moves the page to the root folder if the title is preceded by /' do
page = create_wiki_page(container, title: 'foo/Existing Page')
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index 6aacaa3c119..5a525d83c3b 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe WorkItem, feature_category: :portfolio_management do
+ using RSpec::Parameterized::TableSyntax
+
let_it_be(:reusable_project) { create(:project) }
describe 'associations' do
@@ -99,15 +101,28 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
- describe '#supports_assignee?' do
- let(:work_item) { build(:work_item, :task) }
+ describe '#get_widget' do
+ let(:work_item) { build(:work_item, description: 'foo') }
- before do
- allow(work_item.work_item_type).to receive(:supports_assignee?).and_return(false)
+ it 'returns widget object' do
+ expect(work_item.get_widget(:description)).to be_an_instance_of(WorkItems::Widgets::Description)
end
- it 'delegates the call to its work item type' do
- expect(work_item.supports_assignee?).to be(false)
+ context 'when widget does not exist' do
+ it 'returns nil' do
+ expect(work_item.get_widget(:nop)).to be_nil
+ end
+ end
+ end
+
+ describe '#supports_assignee?' do
+ Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter::WIDGETS_FOR_TYPE.each_pair do |base_type, widgets|
+ specify do
+ work_item = build(:work_item, base_type)
+ supports_assignee = widgets.include?(:assignees)
+
+ expect(work_item.supports_assignee?).to eq(supports_assignee)
+ end
end
end
@@ -117,7 +132,7 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
subject { work_item.supported_quick_action_commands }
it 'returns quick action commands supported for all work items' do
- is_expected.to include(:title, :reopen, :close, :cc, :tableflip, :shrug)
+ is_expected.to include(:title, :reopen, :close, :cc, :tableflip, :shrug, :type)
end
context 'when work item supports the assignee widget' do
@@ -127,7 +142,7 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
context 'when work item does not the assignee widget' do
- let(:work_item) { build(:work_item, :incident) }
+ let(:work_item) { build(:work_item, :test_case) }
it 'omits assignee related quick action commands' do
is_expected.not_to include(:assign, :unassign, :reassign)
@@ -163,6 +178,30 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
+ describe 'transform_quick_action_params' do
+ let(:work_item) { build(:work_item, :task) }
+
+ subject(:transformed_params) do
+ work_item.transform_quick_action_params({
+ title: 'bar',
+ assignee_ids: ['foo']
+ })
+ end
+
+ it 'correctly separates widget params from regular params' do
+ expect(transformed_params).to eq({
+ common: {
+ title: 'bar'
+ },
+ widgets: {
+ assignees_widget: {
+ assignee_ids: ['foo']
+ }
+ }
+ })
+ end
+ end
+
describe 'callbacks' do
describe 'record_create_action' do
it 'records the creation action after saving' do
@@ -290,6 +329,20 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
+ describe '#link_reference_pattern' do
+ let(:match_data) { described_class.link_reference_pattern.match(link_reference_url) }
+
+ context 'with work item url' do
+ let(:link_reference_url) { 'http://localhost/namespace/project/-/work_items/1' }
+
+ it 'matches with expected attributes' do
+ expect(match_data['namespace']).to eq('namespace')
+ expect(match_data['project']).to eq('project')
+ expect(match_data['work_item']).to eq('1')
+ end
+ end
+ end
+
context 'with hierarchy' do
let_it_be(:type1) { create(:work_item_type, namespace: reusable_project.namespace) }
let_it_be(:type2) { create(:work_item_type, namespace: reusable_project.namespace) }
@@ -344,4 +397,179 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
end
+
+ describe '#allowed_work_item_type_change' do
+ let_it_be(:all_types) { WorkItems::Type::BASE_TYPES.keys }
+
+ it 'is possible to change between all types', :aggregate_failures do
+ all_types.each do |type|
+ work_item = build(:work_item, type, project: reusable_project)
+
+ (all_types - [type]).each do |new_type|
+ work_item.work_item_type_id = WorkItems::Type.default_by_type(new_type).id
+
+ expect(work_item).to be_valid, "#{type} to #{new_type}"
+ end
+ end
+ end
+
+ context 'with ParentLink relation' do
+ let_it_be(:old_type) { create(:work_item_type) }
+ let_it_be(:new_type) { create(:work_item_type) }
+
+ context 'with hierarchy restrictions' do
+ let_it_be(:child_type) { create(:work_item_type) }
+
+ let_it_be_with_reload(:parent) { create(:work_item, work_item_type: old_type, project: reusable_project) }
+ let_it_be_with_reload(:child) { create(:work_item, work_item_type: child_type, project: reusable_project) }
+
+ let_it_be(:hierarchy_restriction) do
+ create(:hierarchy_restriction, parent_type: old_type, child_type: child_type)
+ end
+
+ let_it_be(:link) { create(:parent_link, work_item_parent: parent, work_item: child) }
+
+ context 'when child items restrict the type change' do
+ before do
+ parent.work_item_type = new_type
+ end
+
+ context 'when child items are compatible with the new type' do
+ let_it_be(:hierarchy_restriction_new_type) do
+ create(:hierarchy_restriction, parent_type: new_type, child_type: child_type)
+ end
+
+ it 'allows to change types' do
+ expect(parent).to be_valid
+ expect(parent.errors).to be_empty
+ end
+ end
+
+ context 'when child items are not compatible with the new type' do
+ it 'does not allow to change types' do
+ expect(parent).not_to be_valid
+ expect(parent.errors[:work_item_type_id])
+ .to include("cannot be changed to #{new_type.name} with these child item types.")
+ end
+ end
+ end
+
+ context 'when the parent restricts the type change' do
+ before do
+ child.work_item_type = new_type
+ end
+
+ it 'does not allow to change types' do
+ expect(child.valid?).to eq(false)
+ expect(child.errors[:work_item_type_id])
+ .to include("cannot be changed to #{new_type.name} with #{parent.work_item_type.name} as parent type.")
+ end
+ end
+ end
+
+ context 'with hierarchy depth restriction' do
+ let_it_be_with_reload(:item1) { create(:work_item, work_item_type: new_type, project: reusable_project) }
+ let_it_be_with_reload(:item2) { create(:work_item, work_item_type: new_type, project: reusable_project) }
+ let_it_be_with_reload(:item3) { create(:work_item, work_item_type: new_type, project: reusable_project) }
+ let_it_be_with_reload(:item4) { create(:work_item, work_item_type: new_type, project: reusable_project) }
+
+ let_it_be(:hierarchy_restriction1) do
+ create(:hierarchy_restriction, parent_type: old_type, child_type: new_type)
+ end
+
+ let_it_be(:hierarchy_restriction2) do
+ create(:hierarchy_restriction, parent_type: new_type, child_type: old_type)
+ end
+
+ let_it_be_with_reload(:hierarchy_restriction3) do
+ create(:hierarchy_restriction, parent_type: new_type, child_type: new_type, maximum_depth: 4)
+ end
+
+ let_it_be(:link1) { create(:parent_link, work_item_parent: item1, work_item: item2) }
+ let_it_be(:link2) { create(:parent_link, work_item_parent: item2, work_item: item3) }
+ let_it_be(:link3) { create(:parent_link, work_item_parent: item3, work_item: item4) }
+
+ before do
+ hierarchy_restriction3.update!(maximum_depth: maximum_depth)
+ end
+
+ shared_examples 'validates the depth correctly' do
+ before do
+ work_item.update!(work_item_type: old_type)
+ end
+
+ context 'when it is valid' do
+ let(:maximum_depth) { 4 }
+
+ it 'allows to change types' do
+ work_item.work_item_type = new_type
+
+ expect(work_item).to be_valid
+ end
+ end
+
+ context 'when it is not valid' do
+ let(:maximum_depth) { 3 }
+
+ it 'does not allow to change types' do
+ work_item.work_item_type = new_type
+
+ expect(work_item).not_to be_valid
+ expect(work_item.errors[:work_item_type_id]).to include("reached maximum depth")
+ end
+ end
+ end
+
+ context 'with the highest ancestor' do
+ let_it_be_with_reload(:work_item) { item1 }
+
+ it_behaves_like 'validates the depth correctly'
+ end
+
+ context 'with a child item' do
+ let_it_be_with_reload(:work_item) { item2 }
+
+ it_behaves_like 'validates the depth correctly'
+ end
+
+ context 'with the last child item' do
+ let_it_be_with_reload(:work_item) { item4 }
+
+ it_behaves_like 'validates the depth correctly'
+ end
+
+ context 'when ancestor is still the old type' do
+ let_it_be(:hierarchy_restriction4) do
+ create(:hierarchy_restriction, parent_type: old_type, child_type: old_type)
+ end
+
+ before do
+ item1.update!(work_item_type: old_type)
+ item2.update!(work_item_type: old_type)
+ end
+
+ context 'when it exceeds maximum depth' do
+ let(:maximum_depth) { 2 }
+
+ it 'does not allow to change types' do
+ item2.work_item_type = new_type
+
+ expect(item2).not_to be_valid
+ expect(item2.errors[:work_item_type_id]).to include("reached maximum depth")
+ end
+ end
+
+ context 'when it does not exceed maximum depth' do
+ let(:maximum_depth) { 3 }
+
+ it 'does allow to change types' do
+ item2.work_item_type = new_type
+
+ expect(item2).to be_valid
+ end
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/work_items/resource_link_event_spec.rb b/spec/models/work_items/resource_link_event_spec.rb
new file mode 100644
index 00000000000..67ca9e72bbc
--- /dev/null
+++ b/spec/models/work_items/resource_link_event_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::ResourceLinkEvent, type: :model, feature_category: :team_planning do
+ it_behaves_like 'a resource event'
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:work_item) }
+ it { is_expected.to belong_to(:child_work_item) }
+ end
+
+ describe 'validation' do
+ it { is_expected.to validate_presence_of(:child_work_item) }
+ end
+end
diff --git a/spec/models/work_items/widget_definition_spec.rb b/spec/models/work_items/widget_definition_spec.rb
index 08f8f4d9663..a33e08a1bf2 100644
--- a/spec/models/work_items/widget_definition_spec.rb
+++ b/spec/models/work_items/widget_definition_spec.rb
@@ -11,7 +11,10 @@ RSpec.describe WorkItems::WidgetDefinition, feature_category: :team_planning do
::WorkItems::Widgets::Assignees,
::WorkItems::Widgets::StartAndDueDate,
::WorkItems::Widgets::Milestone,
- ::WorkItems::Widgets::Notes
+ ::WorkItems::Widgets::Notes,
+ ::WorkItems::Widgets::Notifications,
+ ::WorkItems::Widgets::CurrentUserTodos,
+ ::WorkItems::Widgets::AwardEmoji
]
if Gitlab.ee?
diff --git a/spec/models/work_items/widgets/award_emoji_spec.rb b/spec/models/work_items/widgets/award_emoji_spec.rb
new file mode 100644
index 00000000000..bb61aa41669
--- /dev/null
+++ b/spec/models/work_items/widgets/award_emoji_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::AwardEmoji, feature_category: :team_planning do
+ let_it_be(:work_item) { create(:work_item) }
+ let_it_be(:emoji1) { create(:award_emoji, name: 'star', awardable: work_item) }
+ let_it_be(:emoji2) { create(:award_emoji, :upvote, awardable: work_item) }
+ let_it_be(:emoji3) { create(:award_emoji, :downvote, awardable: work_item) }
+
+ describe '.type' do
+ it { expect(described_class.type).to eq(:award_emoji) }
+ end
+
+ describe '#type' do
+ it { expect(described_class.new(work_item).type).to eq(:award_emoji) }
+ end
+
+ describe '#downvotes' do
+ it { expect(described_class.new(work_item).downvotes).to eq(1) }
+ end
+
+ describe '#upvotes' do
+ it { expect(described_class.new(work_item).upvotes).to eq(1) }
+ end
+
+ describe '#award_emoji' do
+ it { expect(described_class.new(work_item).award_emoji).to match_array([emoji1, emoji2, emoji3]) }
+ end
+end
diff --git a/spec/models/work_items/widgets/notifications_spec.rb b/spec/models/work_items/widgets/notifications_spec.rb
new file mode 100644
index 00000000000..2942c149660
--- /dev/null
+++ b/spec/models/work_items/widgets/notifications_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::Notifications, feature_category: :team_planning do
+ let_it_be(:work_item) { create(:work_item) }
+
+ describe '.type' do
+ it { expect(described_class.type).to eq(:notifications) }
+ end
+
+ describe '#type' do
+ it { expect(described_class.new(work_item).type).to eq(:notifications) }
+ end
+
+ describe '#subscribed?' do
+ it { expect(described_class.new(work_item).subscribed?(work_item.author, work_item.project)).to eq(true) }
+ end
+end