diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-19 15:57:54 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-19 15:57:54 +0300 |
commit | 419c53ec62de6e97a517abd5fdd4cbde3a942a34 (patch) | |
tree | 1f43a548b46bca8a5fb8fe0c31cef1883d49c5b6 /spec/lib/gitlab | |
parent | 1da20d9135b3ad9e75e65b028bffc921aaf8deb7 (diff) |
Add latest changes from gitlab-org/gitlab@16-5-stable-eev16.5.0-rc42
Diffstat (limited to 'spec/lib/gitlab')
204 files changed, 5714 insertions, 4252 deletions
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb index b0ec46a3a0e..95199ae18de 100644 --- a/spec/lib/gitlab/auth/auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/auth_finders_spec.rb @@ -1149,4 +1149,75 @@ RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :system_access do end end end + + describe '#authentication_token_present?' do + subject { authentication_token_present? } + + context 'no auth header/param/oauth' do + before do + request.headers['Random'] = 'Something' + set_param(:random, 'something') + end + + it { is_expected.to be(false) } + end + + context 'with auth header' do + before do + request.headers[header] = 'invalid' + end + + context 'with private-token' do + let(:header) { 'Private-Token' } + + it { is_expected.to be(true) } + end + + context 'with job-token' do + let(:header) { 'Job-Token' } + + it { is_expected.to be(true) } + end + + context 'with deploy-token' do + let(:header) { 'Deploy-Token' } + + it { is_expected.to be(true) } + end + end + + context 'with authorization bearer (oauth token)' do + before do + request.headers['Authorization'] = 'Bearer invalid' + end + + it { is_expected.to be(true) } + end + + context 'with auth param' do + context 'with private_token' do + it 'returns true' do + set_param(:private_token, 'invalid') + + expect(subject).to be(true) + end + end + + context 'with job_token' do + it 'returns true' do + set_param(:job_token, 'invalid') + + expect(subject).to be(true) + end + end + + context 'with token' do + it 'returns true' do + set_param(:token, 'invalid') + + expect(subject).to be(true) + end + end + end + end end diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb index 160fd78b2b9..48039b58216 100644 --- a/spec/lib/gitlab/auth/ldap/config_spec.rb +++ b/spec/lib/gitlab/auth/ldap/config_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Auth::Ldap::Config do +RSpec.describe Gitlab::Auth::Ldap::Config, feature_category: :system_access do include LdapHelpers before do @@ -362,6 +362,19 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK expect(config.omniauth_options.keys).not_to include(:bind_dn, :password) end + it 'defaults to plain encryption when not configured' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 386, + 'base' => 'ou=users,dc=example,dc=com', + 'uid' => 'uid' + } + ) + + expect(config.omniauth_options).to include(encryption: 'plain') + end + it 'includes authentication options when auth is configured' do stub_ldap_config( options: { diff --git a/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb index 8c50b2acac6..5d01f09df41 100644 --- a/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Auth::OAuth::AuthHash, feature_category: :user_management do - let(:provider) { 'ldap' } + let(:provider) { 'openid_connect' } let(:auth_hash) do described_class.new( OmniAuth::AuthHash.new( @@ -19,7 +19,6 @@ RSpec.describe Gitlab::Auth::OAuth::AuthHash, feature_category: :user_management ) end - let(:provider_config) { { 'args' => { 'gitlab_username_claim' => 'first_name' } } } let(:uid_raw) do +"CN=Onur K\xC3\xBC\xC3\xA7\xC3\xBCk,OU=Test,DC=example,DC=net" end @@ -90,6 +89,22 @@ RSpec.describe Gitlab::Auth::OAuth::AuthHash, feature_category: :user_management end end + context 'when username claim is in email format' do + let(:info_hash) do + { + email: nil, + name: 'GitLab test', + nickname: 'GitLab@gitlabsandbox.onmicrosoft.com', + uid: uid_ascii + } + end + + it 'creates proper email and username fields' do + expect(auth_hash.username).to eql 'GitLab' + expect(auth_hash.email).to eql 'temp-email-for-oauth-GitLab@gitlab.localhost' + end + end + context 'name not provided' do before do info_hash.delete(:name) @@ -101,8 +116,17 @@ RSpec.describe Gitlab::Auth::OAuth::AuthHash, feature_category: :user_management end context 'custom username field provided' do + let(:provider_config) do + GitlabSettings::Options.build( + { + name: provider, + args: { 'gitlab_username_claim' => 'first_name' } + } + ) + end + before do - allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for).and_return(provider_config) + stub_omniauth_setting(providers: [provider_config]) end it 'uses the custom field for the username within info' do diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb index 78e0df91103..8a9182f6457 100644 --- a/spec/lib/gitlab/auth/o_auth/user_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb @@ -535,6 +535,37 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do end end + context "and a corresponding LDAP person with some values being nil" do + before do + allow(ldap_user).to receive(:uid) { uid } + allow(ldap_user).to receive(:username) { uid } + allow(ldap_user).to receive(:name) { nil } + allow(ldap_user).to receive(:email) { nil } + allow(ldap_user).to receive(:dn) { dn } + + allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user) + + oauth_user.save # rubocop:disable Rails/SaveBang + end + + it "creates the user correctly" do + expect(gl_user).to be_valid + expect(gl_user.username).to eq(uid) + expect(gl_user.name).to eq(info_hash[:name]) + expect(gl_user.email).to eq(info_hash[:email]) + end + + it "does not have the attributes not provided by LDAP set as synced" do + expect(gl_user.user_synced_attributes_metadata.name_synced).to be_falsey + expect(gl_user.user_synced_attributes_metadata.email_synced).to be_falsey + end + + it "does not have the attributes not provided by LDAP set as read-only" do + expect(gl_user.read_only_attribute?(:name)).to be_falsey + expect(gl_user.read_only_attribute?(:email)).to be_falsey + end + end + context 'and a corresponding LDAP person with a non-default username' do before do allow(ldap_user).to receive(:uid) { uid } diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 8da617175ca..f5b9555916c 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -34,7 +34,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate end end - context 'available_scopes' do + describe 'available_scopes' do before do stub_container_registry_config(enabled: true) end @@ -43,26 +43,26 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate expect(subject.all_available_scopes).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode read_observability write_observability create_runner k8s_proxy ai_features] end - it 'contains for non-admin user all non-default scopes without ADMIN access and without observability scopes' do + it 'contains for non-admin user all non-default scopes without ADMIN access and without observability scopes and ai_features' do user = build_stubbed(:user, admin: false) - expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy ai_features] + expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy] end - it 'contains for admin user all non-default scopes with ADMIN access and without observability scopes' do + it 'contains for admin user all non-default scopes with ADMIN access and without observability scopes and ai_features' do user = build_stubbed(:user, admin: true) - expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode create_runner k8s_proxy ai_features] + expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode create_runner k8s_proxy] end - it 'contains for project all resource bot scopes without observability scopes' do - expect(subject.available_scopes_for(project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy ai_features] + it 'contains for project all resource bot scopes without ai_features' do + expect(subject.available_scopes_for(project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner k8s_proxy] end it 'contains for group all resource bot scopes' do - group = build_stubbed(:group) + group = build_stubbed(:group).tap { |g| g.namespace_settings = build_stubbed(:namespace_settings, namespace: g) } - expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner k8s_proxy ai_features] + expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner k8s_proxy] end it 'contains for unsupported type no scopes' do @@ -73,44 +73,101 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate expect(subject.optional_scopes).to match_array %i[read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode openid profile email read_observability write_observability create_runner k8s_proxy ai_features] end - context 'with observability_group_tab feature flag' do + describe 'ai_features scope' do + let(:resource) { nil } + + subject { described_class.available_scopes_for(resource) } + + context 'when resource is user', 'and user has a group with ai features' do + let(:resource) { build_stubbed(:user) } + + it { is_expected.not_to include(:ai_features) } + end + + context 'when resource is project' do + let(:resource) { build_stubbed(:project) } + + it 'does not include ai_features scope' do + is_expected.not_to include(:ai_features) + end + end + + context 'when resource is group' do + let(:resource) { build_stubbed(:group) } + + it 'does not include ai_features scope' do + is_expected.not_to include(:ai_features) + end + end + end + + context 'with observability_tracing feature flag' do context 'when disabled' do before do - stub_feature_flags(observability_group_tab: false) + stub_feature_flags(observability_tracing: false) end it 'contains for group all resource bot scopes without observability scopes' do - group = build_stubbed(:group) + group = build_stubbed(:group).tap do |g| + g.namespace_settings = build_stubbed(:namespace_settings, namespace: g) + end - expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy ai_features] + expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy] + end + + it 'contains for project all resource bot scopes without observability scopes' do + group = build_stubbed(:group).tap do |g| + g.namespace_settings = build_stubbed(:namespace_settings, namespace: g) + end + project = build_stubbed(:project, namespace: group) + + expect(subject.available_scopes_for(project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy] end end - context 'when enabled for specific group' do - let(:group) { build_stubbed(:group) } + context 'when enabled for specific root group' do + let(:parent) { build_stubbed(:group) } + let(:group) do + build_stubbed(:group, parent: parent).tap { |g| g.namespace_settings = build_stubbed(:namespace_settings, namespace: g) } + end + + let(:project) { build_stubbed(:project, namespace: group) } before do - stub_feature_flags(observability_group_tab: group) + stub_feature_flags(observability_tracing: parent) end - it 'contains for other group all resource bot scopes including observability scopes' do - expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner k8s_proxy ai_features] + it 'contains for group all resource bot scopes including observability scopes' do + expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner k8s_proxy] end it 'contains for admin user all non-default scopes with ADMIN access and without observability scopes' do user = build_stubbed(:user, admin: true) - expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode create_runner k8s_proxy ai_features] + expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode create_runner k8s_proxy] end - it 'contains for project all resource bot scopes without observability scopes' do - expect(subject.available_scopes_for(project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy ai_features] + it 'contains for project all resource bot scopes including observability scopes' do + expect(subject.available_scopes_for(project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner k8s_proxy] end it 'contains for other group all resource bot scopes without observability scopes' do - other_group = build_stubbed(:group) + other_parent = build_stubbed(:group) + other_group = build_stubbed(:group, parent: other_parent).tap do |g| + g.namespace_settings = build_stubbed(:namespace_settings, namespace: g) + end + + expect(subject.available_scopes_for(other_group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy] + end + + it 'contains for other project all resource bot scopes without observability scopes' do + other_parent = build_stubbed(:group) + other_group = build_stubbed(:group, parent: other_parent).tap do |g| + g.namespace_settings = build_stubbed(:namespace_settings, namespace: g) + end + other_project = build_stubbed(:project, namespace: other_group) - expect(subject.available_scopes_for(other_group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy ai_features] + expect(subject.available_scopes_for(other_project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy] end end end diff --git a/spec/lib/gitlab/background_migration/backfill_finding_id_in_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/backfill_finding_id_in_vulnerabilities_spec.rb new file mode 100644 index 00000000000..3dbb1b34726 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_finding_id_in_vulnerabilities_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' +RSpec.describe Gitlab::BackgroundMigration::BackfillFindingIdInVulnerabilities, schema: 20230912105945, feature_category: :vulnerability_management do # rubocop:disable Layout/LineLength + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:users) { table(:users) } + let(:members) { table(:members) } + let(:vulnerability_identifiers) { table(:vulnerability_identifiers) } + let(:vulnerability_scanners) { table(:vulnerability_scanners) } + let(:vulnerability_findings) { table(:vulnerability_occurrences) } + let(:vulnerabilities) { table(:vulnerabilities) } + let!(:user) { create_user(email: "test1@example.com", username: "test1") } + let!(:namespace) { namespaces.create!(name: "test-1", path: "test-1", owner_id: user.id) } + let!(:project) do + projects.create!( + id: 9999, namespace_id: namespace.id, + project_namespace_id: namespace.id, + creator_id: user.id + ) + end + + let!(:membership) do + members.create!(access_level: 50, source_id: project.id, source_type: "Project", user_id: user.id, state: 0, + notification_level: 3, type: "ProjectMember", member_namespace_id: namespace.id) + end + + let(:migration_attrs) do + { + start_id: vulnerabilities.first.id, + end_id: vulnerabilities.last.id, + batch_table: :vulnerabilities, + batch_column: :id, + sub_batch_size: 100, + pause_ms: 0, + connection: ApplicationRecord.connection + } + end + + describe "#perform" do + subject(:background_migration) { described_class.new(**migration_attrs).perform } + + # This scenario is what usually happens because we first create a Vulnerabilities::Finding record, then create + # a Vulnerability record and populate the Vulnerabilities::Finding#vulnerability_id + let(:vulnerabilities_finding_1) { create_finding(project, vulnerability_id: vulnerability_without_finding_id.id) } + let(:vulnerability_without_finding_id) { create_vulnerability } + + # This scenario can occur because we have modified our Vulnerabilities ingestion pipeline to populate + # vulnerabilities.finding_id as soon as possible + let(:vulnerabilities_finding_2) { create_finding(project) } + let(:vulnerability_with_finding_id) { create_vulnerability(finding_id: vulnerabilities_finding_2.id) } + + it 'backfills finding_id column in the vulnerabilities table' do + expect { background_migration }.to change { vulnerability_without_finding_id.reload.finding_id } + .from(nil).to(vulnerabilities_finding_1.id) + end + + it 'does not affect rows with finding_id populated' do + expect { background_migration }.not_to change { vulnerability_with_finding_id.reload.finding_id } + end + end + + private + + def create_scanner(project, overrides = {}) + attrs = { + project_id: project.id, + external_id: "test_vulnerability_scanner", + name: "Test Vulnerabilities::Scanner" + }.merge(overrides) + + vulnerability_scanners.create!(attrs) + end + + def create_identifier(project, overrides = {}) + attrs = { + project_id: project.id, + external_id: "CVE-2018-1234", + external_type: "CVE", + name: "CVE-2018-1234", + fingerprint: SecureRandom.hex(20) + }.merge(overrides) + + vulnerability_identifiers.create!(attrs) + end + + def create_finding(project, overrides = {}) + attrs = { + project_id: project.id, + scanner_id: create_scanner(project).id, + severity: 5, # medium + confidence: 2, # unknown, + report_type: 99, # generic + primary_identifier_id: create_identifier(project).id, + project_fingerprint: SecureRandom.hex(20), + location_fingerprint: SecureRandom.hex(20), + uuid: SecureRandom.uuid, + name: "CVE-2018-1234", + raw_metadata: "{}", + metadata_version: "test:1.0" + }.merge(overrides) + + vulnerability_findings.create!(attrs) + end + + def create_vulnerability(overrides = {}) + attrs = { + project_id: project.id, + author_id: user.id, + title: 'test', + severity: 1, + confidence: 1, + report_type: 1, + state: 1, + detected_at: Time.zone.now + }.merge(overrides) + + vulnerabilities.create!(attrs) + end + + def create_user(overrides = {}) + attrs = { + email: "test@example.com", + notification_email: "test@example.com", + name: "test", + username: "test", + state: "active", + projects_limit: 10 + }.merge(overrides) + + users.create!(attrs) + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_has_remediations_of_vulnerability_reads_spec.rb b/spec/lib/gitlab/background_migration/backfill_has_remediations_of_vulnerability_reads_spec.rb new file mode 100644 index 00000000000..0e7a0210758 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_has_remediations_of_vulnerability_reads_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillHasRemediationsOfVulnerabilityReads, + feature_category: :database do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:users) { table(:users) } + let(:scanners) { table(:vulnerability_scanners) } + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerability_reads) { table(:vulnerability_reads) } + + let(:namespace) { namespaces.create!(name: 'user', path: 'user') } + let(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id) } + let(:user) { users.create!(username: 'john_doe', email: 'johndoe@gitlab.com', projects_limit: 10) } + let(:scanner) { scanners.create!(project_id: project.id, external_id: 'external_id', name: 'Test Scanner') } + + let(:vulnerability_1) { create_vulnerability(title: 'vulnerability 1') } + let(:vulnerability_2) { create_vulnerability(title: 'vulnerability 2') } + + let!(:vulnerability_read_1) { create_vulnerability_read(vulnerability_id: vulnerability_1.id) } + let!(:vulnerability_read_2) { create_vulnerability_read(vulnerability_id: vulnerability_2.id) } + + let(:vulnerability_findings) { table(:vulnerability_occurrences) } + let(:vulnerability_findings_remediations) { table(:vulnerability_findings_remediations) } + let(:vulnerability_remediations) { table(:vulnerability_remediations) } + let(:vulnerability_identifiers) { table(:vulnerability_identifiers) } + + subject(:perform_migration) do + described_class.new( + start_id: vulnerability_reads.first.vulnerability_id, + end_id: vulnerability_reads.last.vulnerability_id, + batch_table: :vulnerability_reads, + batch_column: :vulnerability_id, + sub_batch_size: vulnerability_reads.count, + pause_ms: 0, + connection: ActiveRecord::Base.connection + ).perform + end + + it 'updates vulnerability_reads records which has remediations' do + vuln_remediation = create_remediation + vuln_finding = create_finding(vulnerability_id: vulnerability_1.id) + vulnerability_findings_remediations.create!( + vulnerability_occurrence_id: vuln_finding.id, + vulnerability_remediation_id: vuln_remediation.id + ) + + expect { perform_migration }.to change { vulnerability_read_1.reload.has_remediations }.from(false).to(true) + .and not_change { vulnerability_read_2.reload.has_remediations }.from(false) + end + + it 'does not modify has_remediations of vulnerabilities which do not have remediations' do + expect { perform_migration }.to not_change { vulnerability_read_1.reload.has_remediations }.from(false) + .and not_change { vulnerability_read_2.reload.has_remediations }.from(false) + end + + private + + def create_vulnerability(overrides = {}) + attrs = { + project_id: project.id, + author_id: user.id, + title: 'test', + severity: 1, + confidence: 1, + report_type: 1 + }.merge(overrides) + + vulnerabilities.create!(attrs) + end + + def create_vulnerability_read(overrides = {}) + attrs = { + project_id: project.id, + vulnerability_id: 1, + scanner_id: scanner.id, + severity: 1, + report_type: 1, + state: 1, + uuid: SecureRandom.uuid + }.merge(overrides) + + vulnerability_reads.create!(attrs) + end + + def create_finding(overrides = {}) + attrs = { + project_id: project.id, + scanner_id: scanner.id, + severity: 5, # medium + confidence: 2, # unknown, + report_type: 99, # generic + primary_identifier_id: create_identifier.id, + project_fingerprint: SecureRandom.hex(20), + location_fingerprint: SecureRandom.hex(20), + uuid: SecureRandom.uuid, + name: "CVE-2018-1234", + raw_metadata: "{}", + metadata_version: "test:1.0" + }.merge(overrides) + + vulnerability_findings.create!(attrs) + end + + def create_remediation(overrides = {}) + remediation_hash = { summary: 'summary', diff: "ZGlmZiAtLWdp" } + + attrs = { + project_id: project.id, + summary: remediation_hash[:summary], + checksum: checksum(remediation_hash[:diff]), + file: Tempfile.new.path + }.merge(overrides) + + vulnerability_remediations.create!(attrs) + end + + def create_identifier(overrides = {}) + attrs = { + project_id: project.id, + external_id: "CVE-2018-1234", + external_type: "CVE", + name: "CVE-2018-1234", + fingerprint: SecureRandom.hex(20) + }.merge(overrides) + + vulnerability_identifiers.create!(attrs) + end + + def checksum(value) + sha = Digest::SHA256.hexdigest(value) + Gitlab::Database::ShaAttribute.new.serialize(sha) + end +end diff --git a/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules2_spec.rb b/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules2_spec.rb new file mode 100644 index 00000000000..81dd37f0fe9 --- /dev/null +++ b/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules2_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::DeleteOrphansApprovalMergeRequestRules2, feature_category: :security_policy_management do + describe '#perform' do + let(:batch_table) { :approval_merge_request_rules } + let(:batch_column) { :id } + let(:sub_batch_size) { 1 } + let(:pause_ms) { 0 } + let(:connection) { ApplicationRecord.connection } + + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:approval_project_rules) { table(:approval_project_rules) } + let(:approval_merge_request_rules) { table(:approval_merge_request_rules) } + let(:approval_merge_request_rule_sources) { table(:approval_merge_request_rule_sources) } + let(:security_orchestration_policy_configurations) { table(:security_orchestration_policy_configurations) } + let(:namespace) { namespaces.create!(name: 'name', path: 'path') } + let(:project) do + projects + .create!(name: "project", path: "project", namespace_id: namespace.id, project_namespace_id: namespace.id) + end + + let(:namespace_2) { namespaces.create!(name: 'name_2', path: 'path_2') } + let(:security_project) do + projects + .create!(name: "security_project", path: "security_project", namespace_id: namespace_2.id, + project_namespace_id: namespace_2.id) + end + + let!(:security_orchestration_policy_configuration) do + security_orchestration_policy_configurations + .create!(project_id: project.id, security_policy_management_project_id: security_project.id) + end + + let(:merge_request) do + table(:merge_requests).create!(target_project_id: project.id, target_branch: 'main', source_branch: 'feature') + end + + let!(:approval_rule) do + approval_merge_request_rules.create!( + name: 'rule', + merge_request_id: merge_request.id, + report_type: 4, + security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id) + end + + let!(:approval_rule_other_report_type) do + approval_merge_request_rules.create!( + name: 'rule 2', + merge_request_id: merge_request.id, + report_type: 1, + security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id) + end + + let!(:approval_rule_license_scanning) do + approval_merge_request_rules.create!( + name: 'rule 4', + merge_request_id: merge_request.id, + report_type: 2, + security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id) + end + + let!(:approval_rule_license_scanning_without_policy_id) do + approval_merge_request_rules.create!(name: 'rule 5', merge_request_id: merge_request.id, report_type: 2) + end + + let!(:approval_rule_last) do + approval_merge_request_rules.create!(name: 'rule 3', merge_request_id: merge_request.id, report_type: 4) + end + + subject do + described_class.new( + start_id: approval_rule.id, + end_id: approval_rule_last.id, + batch_table: batch_table, + batch_column: batch_column, + sub_batch_size: sub_batch_size, + pause_ms: pause_ms, + connection: connection + ).perform + end + + it 'delete only approval rules without association with the security project and report_type equals to 4 or 2' do + expect { subject }.to change { approval_merge_request_rules.all }.to( + contain_exactly(approval_rule, + approval_rule_other_report_type, + approval_rule_license_scanning)) + end + + context 'with rule sources' do # rubocop: disable RSpec/MultipleMemoizedHelpers + let(:project_approval_rule_1) { approval_project_rules.create!(project_id: project.id, name: '1') } + let(:project_approval_rule_2) { approval_project_rules.create!(project_id: project.id, name: '2') } + + let!(:rule_source_1) do + approval_merge_request_rule_sources.create!( + approval_merge_request_rule_id: approval_rule_license_scanning_without_policy_id.id, + approval_project_rule_id: project_approval_rule_1.id) + end + + let!(:rule_source_2) do + approval_merge_request_rule_sources.create!( + approval_merge_request_rule_id: approval_rule_other_report_type.id, + approval_project_rule_id: project_approval_rule_2.id) + end + + it 'deletes referenced sources' do + # rubocop: disable CodeReuse/ActiveRecord + expect { subject }.to change { approval_merge_request_rule_sources.exists?(rule_source_1.id) }.to(false) + # rubocop: enable CodeReuse/ActiveRecord + end + + it 'does not delete unreferenced sources' do + # rubocop: disable CodeReuse/ActiveRecord + expect { subject }.not_to change { approval_merge_request_rule_sources.exists?(rule_source_2.id) } + # rubocop: enable CodeReuse/ActiveRecord + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules2_spec.rb b/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules2_spec.rb new file mode 100644 index 00000000000..c6563efe173 --- /dev/null +++ b/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules2_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::DeleteOrphansApprovalProjectRules2, feature_category: :security_policy_management do + describe '#perform' do + let(:batch_table) { :approval_project_rules } + let(:batch_column) { :id } + let(:sub_batch_size) { 1 } + let(:pause_ms) { 0 } + let(:connection) { ApplicationRecord.connection } + + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:approval_project_rules) { table(:approval_project_rules) } + let(:approval_merge_request_rules) { table(:approval_merge_request_rules) } + let(:approval_merge_request_rule_sources) { table(:approval_merge_request_rule_sources) } + let(:security_orchestration_policy_configurations) { table(:security_orchestration_policy_configurations) } + let(:namespace) { namespaces.create!(name: 'name', path: 'path') } + let(:project) do + projects + .create!(name: "project", path: "project", namespace_id: namespace.id, project_namespace_id: namespace.id) + end + + let(:merge_request) do + table(:merge_requests).create!(target_project_id: project.id, target_branch: 'main', source_branch: 'feature') + end + + let(:namespace_2) { namespaces.create!(name: 'name_2', path: 'path_2') } + let(:security_project) do + projects + .create!(name: "security_project", path: "security_project", namespace_id: namespace_2.id, + project_namespace_id: namespace_2.id) + end + + let!(:security_orchestration_policy_configuration) do + security_orchestration_policy_configurations + .create!(project_id: project.id, security_policy_management_project_id: security_project.id) + end + + let!(:project_rule) do + approval_project_rules.create!( + name: 'rule', + project_id: project.id, + report_type: 4, + security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id) + end + + let!(:project_rule_other_report_type) do + approval_project_rules.create!( + name: 'rule 2', + project_id: project.id, + report_type: 1, + security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id) + end + + let!(:project_rule_license_scanning) do + approval_project_rules.create!( + name: 'rule 4', + project_id: project.id, + report_type: 2, + security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id) + end + + let!(:project_rule_license_scanning_without_policy_id) do + approval_project_rules.create!(name: 'rule 5', project_id: project.id, report_type: 2) + end + + let!(:project_rule_last) do + approval_project_rules.create!(name: 'rule 3', project_id: project.id, report_type: 4) + end + + subject do + described_class.new( + start_id: project_rule.id, + end_id: project_rule_last.id, + batch_table: batch_table, + batch_column: batch_column, + sub_batch_size: sub_batch_size, + pause_ms: pause_ms, + connection: connection + ).perform + end + + it 'delete only approval rules without association with the security project and report_type equals to 4' do + expect { subject }.to change { approval_project_rules.all }.to( + contain_exactly(project_rule, + project_rule_other_report_type, + project_rule_license_scanning)) + end + + context 'with rule sources' do # rubocop: disable RSpec/MultipleMemoizedHelpers + let(:approval_merge_request_rule_1) do + approval_merge_request_rules.create!(merge_request_id: merge_request.id, name: '1') + end + + let(:approval_merge_request_rule_2) do + approval_merge_request_rules.create!(merge_request_id: merge_request.id, name: '2') + end + + let!(:rule_source_1) do + approval_merge_request_rule_sources.create!( + approval_merge_request_rule_id: approval_merge_request_rule_1.id, + approval_project_rule_id: project_rule_license_scanning_without_policy_id.id) + end + + let!(:rule_source_2) do + approval_merge_request_rule_sources.create!( + approval_merge_request_rule_id: approval_merge_request_rule_2.id, + approval_project_rule_id: project_rule_other_report_type.id) + end + + it 'deletes referenced sources' do + expect { subject }.to change { approval_merge_request_rule_sources.exists?(rule_source_1.id) }.to(false) + end + + it 'does not delete unreferenced sources' do + expect { subject }.not_to change { approval_merge_request_rule_sources.exists?(rule_source_2.id) } + end + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index 9786e7a364e..517d557d665 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -112,6 +112,7 @@ RSpec.describe Gitlab::BitbucketImport::Importer, :clean_gitlab_redis_cache, fea end let(:pull_request_author) { 'other' } + let(:comments) { [@inline_note, @reply] } let(:author_line) { "*Created by: someuser*\n\n" } @@ -145,8 +146,6 @@ RSpec.describe Gitlab::BitbucketImport::Importer, :clean_gitlab_redis_cache, fea has_parent?: true, parent_id: 2) - comments = [@inline_note, @reply] - allow(subject.client).to receive(:repo) allow(subject.client).to receive(:pull_requests).and_return([pull_request]) allow(subject.client).to receive(:pull_request_comments).with(anything, pull_request.iid).and_return(comments) @@ -202,6 +201,12 @@ RSpec.describe Gitlab::BitbucketImport::Importer, :clean_gitlab_redis_cache, fea end end + it 'calls RefConverter to convert Bitbucket refs to Gitlab refs' do + expect(subject.instance_values['ref_converter']).to receive(:convert_note).twice + + subject.execute + end + context 'when importing a pull request throws an exception' do before do allow(pull_request).to receive(:raw).and_return({ error: "broken" }) @@ -384,6 +389,12 @@ RSpec.describe Gitlab::BitbucketImport::Importer, :clean_gitlab_redis_cache, fea expect(label_after_import.attributes).to eq(existing_label.attributes) end end + + it 'raises an error if a label is not valid' do + stub_const("#{described_class}::LABELS", [{ title: nil, color: nil }]) + + expect { importer.create_labels }.to raise_error(StandardError, /Failed to create label/) + end end it 'maps statuses to open or closed' do @@ -444,26 +455,33 @@ RSpec.describe Gitlab::BitbucketImport::Importer, :clean_gitlab_redis_cache, fea end context 'with issue comments' do + let(:note) { 'Hello world' } let(:inline_note) do - instance_double(Bitbucket::Representation::Comment, note: 'Hello world', author: 'someuser', created_at: Time.now, updated_at: Time.now) + instance_double(Bitbucket::Representation::Comment, note: note, author: 'someuser', created_at: Time.now, updated_at: Time.now) end before do allow_next_instance_of(Bitbucket::Client) do |instance| allow(instance).to receive(:issue_comments).and_return([inline_note]) end + allow(importer).to receive(:import_wiki) end it 'imports issue comments' do - allow(importer).to receive(:import_wiki) importer.execute comment = project.notes.first expect(project.notes.size).to eq(7) - expect(comment.note).to include(inline_note.note) + expect(comment.note).to include(note) expect(comment.note).to include(inline_note.author) expect(importer.errors).to be_empty end + + it 'calls RefConverter to convert Bitbucket refs to Gitlab refs' do + expect(importer.instance_values['ref_converter']).to receive(:convert_note).exactly(7).times + + importer.execute + end end context 'when issue was already imported' do diff --git a/spec/lib/gitlab/bitbucket_import/importers/issue_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/issue_importer_spec.rb new file mode 100644 index 00000000000..8f79390d2d9 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/importers/issue_importer_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::Importers::IssueImporter, :clean_gitlab_redis_cache, feature_category: :importers do + include AfterNextHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:bitbucket_user) { create(:user) } + let_it_be(:identity) { create(:identity, user: bitbucket_user, extern_uid: 'bitbucket_user', provider: :bitbucket) } + let_it_be(:default_work_item_type) { create(:work_item_type) } + let_it_be(:label) { create(:label, project: project) } + + let(:hash) do + { + iid: 111, + title: 'title', + description: 'description', + state: 'closed', + author: 'bitbucket_user', + milestone: 'my milestone', + issue_type_id: default_work_item_type.id, + label_id: label.id, + created_at: Date.today, + updated_at: Date.today + } + end + + subject(:importer) { described_class.new(project, hash) } + + before do + allow(Gitlab::Git).to receive(:ref_name).and_return('refname') + end + + describe '#execute' do + it 'creates an issue' do + expect { importer.execute }.to change { project.issues.count }.from(0).to(1) + + issue = project.issues.first + + expect(issue.description).to eq('description') + expect(issue.author).to eq(bitbucket_user) + expect(issue.closed?).to be_truthy + expect(issue.milestone).to eq(project.milestones.first) + expect(issue.work_item_type).to eq(default_work_item_type) + expect(issue.labels).to eq([label]) + expect(issue.created_at).to eq(Date.today) + expect(issue.updated_at).to eq(Date.today) + end + + context 'when the author does not have a bitbucket identity' do + before do + identity.update!(provider: :github) + end + + it 'sets the author to the project creator and adds the author to the description' do + importer.execute + + issue = project.issues.first + + expect(issue.author).to eq(project.creator) + expect(issue.description).to eq("*Created by: bitbucket_user*\n\ndescription") + end + end + + context 'when a milestone with the same title exists' do + let_it_be(:milestone) { create(:milestone, project: project, title: 'my milestone') } + + it 'assigns the milestone and does not create a new milestone' do + expect { importer.execute }.not_to change { project.milestones.count } + + expect(project.issues.first.milestone).to eq(milestone) + end + end + + context 'when a milestone with the same title does not exist' do + it 'creates a new milestone and assigns it' do + expect { importer.execute }.to change { project.milestones.count }.from(0).to(1) + + expect(project.issues.first.milestone).to eq(project.milestones.first) + end + end + + context 'when an error is raised' do + it 'tracks the failure and does not fail' do + expect(Gitlab::Import::ImportFailureService).to receive(:track).once + + described_class.new(project, hash.except(:title)).execute + end + end + + it 'logs its progress' do + allow(Gitlab::Import::MergeRequestCreator).to receive_message_chain(:new, :execute) + + expect(Gitlab::BitbucketImport::Logger) + .to receive(:info).with(include(message: 'starting', iid: anything)).and_call_original + expect(Gitlab::BitbucketImport::Logger) + .to receive(:info).with(include(message: 'finished', iid: anything)).and_call_original + + importer.execute + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/importers/issue_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/issue_notes_importer_spec.rb new file mode 100644 index 00000000000..1a2a43d6877 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/importers/issue_notes_importer_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::Importers::IssueNotesImporter, :clean_gitlab_redis_cache, feature_category: :importers do + let_it_be(:project) do + create(:project, :import_started, import_source: 'namespace/repo', + import_data_attributes: { + credentials: { 'base_uri' => 'http://bitbucket.org/', 'user' => 'bitbucket', 'password' => 'password' } + } + ) + end + + let_it_be(:bitbucket_user) { create(:user) } + let_it_be(:identity) { create(:identity, user: bitbucket_user, extern_uid: 'bitbucket_user', provider: :bitbucket) } + let_it_be(:issue) { create(:issue, project: project) } + let(:hash) { { iid: issue.iid } } + let(:note_body) { 'body' } + let(:client) { Bitbucket::Client.new({}) } + + subject(:importer) { described_class.new(project, hash) } + + describe '#execute' do + let(:issue_comments_response) do + [ + Bitbucket::Representation::Comment.new({ + 'user' => { 'nickname' => 'bitbucket_user' }, + 'content' => { 'raw' => note_body }, + 'created_on' => Date.today, + 'updated_on' => Date.today + }) + ] + end + + before do + allow(Bitbucket::Client).to receive(:new).and_return(client) + allow(client).to receive(:issue_comments).and_return(issue_comments_response) + end + + it 'creates a new note with the correct attributes' do + expect { importer.execute }.to change { issue.notes.count }.from(0).to(1) + + note = issue.notes.first + + expect(note.project).to eq(project) + expect(note.note).to eq(note_body) + expect(note.author).to eq(bitbucket_user) + expect(note.created_at).to eq(Date.today) + expect(note.updated_at).to eq(Date.today) + end + + context 'when the author does not have a bitbucket identity' do + before do + identity.update!(provider: :github) + end + + it 'sets the author to the project creator and adds the author to the note' do + importer.execute + + note = issue.notes.first + + expect(note.author).to eq(project.creator) + expect(note.note).to eq("*Created by: bitbucket_user*\n\nbody") + end + end + + it 'calls RefConverter to convert Bitbucket refs to Gitlab refs' do + expect(importer.instance_values['ref_converter']).to receive(:convert_note).once + + importer.execute + end + + context 'when an error is raised' do + before do + allow(client).to receive(:issue_comments).and_raise(StandardError) + end + + it 'tracks the failure and does not fail' do + expect(Gitlab::Import::ImportFailureService).to receive(:track).once + + importer.execute + end + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb new file mode 100644 index 00000000000..a361a9343dd --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::Importers::IssuesImporter, feature_category: :importers do + let_it_be(:project) do + create(:project, :import_started, + import_data_attributes: { + data: { 'project_key' => 'key', 'repo_slug' => 'slug' }, + credentials: { 'base_uri' => 'http://bitbucket.org/', 'user' => 'bitbucket', 'password' => 'password' } + } + ) + end + + subject(:importer) { described_class.new(project) } + + describe '#execute', :clean_gitlab_redis_cache do + before do + allow_next_instance_of(Bitbucket::Client) do |client| + allow(client).to receive(:issues).and_return( + [ + Bitbucket::Representation::Issue.new({ 'id' => 1 }), + Bitbucket::Representation::Issue.new({ 'id' => 2 }) + ], + [] + ) + end + end + + it 'imports each issue in parallel', :aggregate_failures do + expect(Gitlab::BitbucketImport::ImportIssueWorker).to receive(:perform_in).twice + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(2) + expect(Gitlab::Cache::Import::Caching.values_from_set(importer.already_enqueued_cache_key)) + .to match_array(%w[1 2]) + end + + context 'when the client raises an error' do + before do + allow_next_instance_of(Bitbucket::Client) do |client| + allow(client).to receive(:issues).and_raise(StandardError) + end + end + + it 'tracks the failure and does not fail' do + expect(Gitlab::Import::ImportFailureService).to receive(:track).once + + importer.execute + end + end + + context 'when issue was already enqueued' do + before do + Gitlab::Cache::Import::Caching.set_add(importer.already_enqueued_cache_key, 1) + end + + it 'does not schedule job for enqueued issues', :aggregate_failures do + expect(Gitlab::BitbucketImport::ImportIssueWorker).to receive(:perform_in).once + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(2) + end + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb new file mode 100644 index 00000000000..043cd7f17b9 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::Importers::IssuesNotesImporter, feature_category: :importers do + let_it_be(:project) { create(:project, :import_started) } + # let_it_be(:merge_request_1) { create(:merge_request, source_project: project) } + # let_it_be(:merge_request_2) { create(:merge_request, source_project: project, source_branch: 'other-branch') } + let_it_be(:issue_1) { create(:issue, project: project) } + let_it_be(:issue_2) { create(:issue, project: project) } + + subject(:importer) { described_class.new(project) } + + describe '#execute', :clean_gitlab_redis_cache do + it 'imports the notes from each issue in parallel', :aggregate_failures do + expect(Gitlab::BitbucketImport::ImportIssueNotesWorker).to receive(:perform_in).twice + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(2) + expect(Gitlab::Cache::Import::Caching.values_from_set(importer.already_enqueued_cache_key)) + .to match_array(%w[1 2]) + end + + context 'when an error is raised' do + before do + allow(importer).to receive(:mark_as_enqueued).and_raise(StandardError) + end + + it 'tracks the failure and does not fail' do + expect(Gitlab::Import::ImportFailureService).to receive(:track).once + + importer.execute + end + end + + context 'when issue was already enqueued' do + before do + Gitlab::Cache::Import::Caching.set_add(importer.already_enqueued_cache_key, 2) + end + + it 'does not schedule job for enqueued issues', :aggregate_failures do + expect(Gitlab::BitbucketImport::ImportIssueNotesWorker).to receive(:perform_in).once + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(2) + end + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/importers/lfs_object_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/lfs_object_importer_spec.rb new file mode 100644 index 00000000000..4d56853032a --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/importers/lfs_object_importer_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::Importers::LfsObjectImporter, feature_category: :importers do + let_it_be(:project) { create(:project) } + let(:oid) { 'a' * 64 } + + let(:lfs_attributes) do + { + 'oid' => oid, + 'size' => 1, + 'link' => 'http://www.gitlab.com/lfs_objects/oid', + 'headers' => { 'X-Some-Header' => '456' } + } + end + + let(:importer) { described_class.new(project, lfs_attributes) } + + describe '#execute' do + it 'calls the LfsDownloadService with the lfs object attributes' do + expect_next_instance_of( + Projects::LfsPointers::LfsDownloadService, project, have_attributes(lfs_attributes) + ) do |service| + expect(service).to receive(:execute).and_return(ServiceResponse.success) + end + + importer.execute + end + + context 'when the object is not valid' do + let(:oid) { 'invalid' } + + it 'tracks the validation errors and does not continue' do + expect(Gitlab::Import::ImportFailureService).to receive(:track).once + + expect(Projects::LfsPointers::LfsDownloadService).not_to receive(:new) + + importer.execute + end + end + + context 'when an error is raised' do + let(:exception) { StandardError.new('messsage') } + + before do + allow_next_instance_of(Projects::LfsPointers::LfsDownloadService) do |service| + allow(service).to receive(:execute).and_raise(exception) + end + end + + it 'rescues and logs the exception' do + expect(Gitlab::Import::ImportFailureService) + .to receive(:track) + .with(hash_including(exception: exception)) + + importer.execute + end + end + + it 'logs its progress' do + allow_next_instance_of(Projects::LfsPointers::LfsDownloadService) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.success) + end + + common_log_message = { + oid: oid, + import_stage: 'import_lfs_object', + class: described_class.name, + project_id: project.id, + project_path: project.full_path + } + + expect(Gitlab::BitbucketImport::Logger) + .to receive(:info).with(common_log_message.merge(message: 'starting')).and_call_original + expect(Gitlab::BitbucketImport::Logger) + .to receive(:info).with(common_log_message.merge(message: 'finished')).and_call_original + + importer.execute + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/importers/lfs_objects_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/lfs_objects_importer_spec.rb new file mode 100644 index 00000000000..fbce8337264 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/importers/lfs_objects_importer_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::Importers::LfsObjectsImporter, feature_category: :importers do + let_it_be(:project) do + create(:project, :import_started, + import_data_attributes: { + data: { 'project_key' => 'key', 'repo_slug' => 'slug' }, + credentials: { 'token' => 'token' } + } + ) + end + + let(:lfs_attributes) do + { + oid: 'a' * 64, + size: 1, + link: 'http://www.gitlab.com/lfs_objects/oid', + headers: { 'X-Some-Header' => '456' } + } + end + + let(:lfs_download_object) { LfsDownloadObject.new(**lfs_attributes) } + + let(:common_log_messages) do + { + import_stage: 'import_lfs_objects', + class: described_class.name, + project_id: project.id, + project_path: project.full_path + } + end + + describe '#execute', :clean_gitlab_redis_cache do + context 'when lfs is enabled' do + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + end + + it 'imports each lfs object in parallel' do + importer = described_class.new(project) + + expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service| + expect(service).to receive(:each_list_item).and_yield(lfs_download_object) + end + + expect(Gitlab::BitbucketImport::ImportLfsObjectWorker).to receive(:perform_in) + .with(1.second, project.id, lfs_attributes.stringify_keys, start_with(Gitlab::JobWaiter::KEY_PREFIX)) + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(1) + end + + it 'logs its progress' do + importer = described_class.new(project) + + expect(Gitlab::BitbucketImport::Logger) + .to receive(:info).with(common_log_messages.merge(message: 'starting')).and_call_original + expect(Gitlab::BitbucketImport::Logger) + .to receive(:info).with(common_log_messages.merge(message: 'finished')).and_call_original + + importer.execute + end + + context 'when LFS list download fails' do + let(:exception) { StandardError.new('Invalid Project URL') } + + before do + allow_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service| + allow(service).to receive(:each_list_item).and_raise(exception) + end + end + + it 'rescues and logs the exception' do + importer = described_class.new(project) + + expect(Gitlab::Import::ImportFailureService) + .to receive(:track) + .with( + project_id: project.id, + exception: exception, + error_source: described_class.name + ).and_call_original + + expect(Gitlab::BitbucketImport::ImportLfsObjectWorker).not_to receive(:perform_in) + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(0) + end + end + end + + context 'when LFS is not enabled' do + before do + allow(project).to receive(:lfs_enabled?).and_return(false) + end + + it 'logs progress but does nothing' do + importer = described_class.new(project) + + expect(Gitlab::BitbucketImport::Logger).to receive(:info).twice + expect(Gitlab::BitbucketImport::ImportLfsObjectWorker).not_to receive(:perform_in) + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(0) + end + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer_spec.rb new file mode 100644 index 00000000000..4a30f225d66 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestNotesImporter, feature_category: :importers do + let_it_be(:project) do + create(:project, :import_started, + import_data_attributes: { + credentials: { 'base_uri' => 'http://bitbucket.org/', 'user' => 'bitbucket', 'password' => 'password' } + } + ) + end + + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + + let(:hash) { { iid: merge_request.iid } } + let(:importer_helper) { Gitlab::BitbucketImport::Importer.new(project) } + + subject(:importer) { described_class.new(project, hash) } + + before do + allow(Gitlab::BitbucketImport::Importer).to receive(:new).and_return(importer_helper) + end + + describe '#execute' do + it 'calls Importer.import_pull_request_comments' do + expect(importer_helper).to receive(:import_pull_request_comments).once + + importer.execute + end + + context 'when the merge request does not exist' do + let(:hash) { { iid: 'nonexistent' } } + + it 'does not call Importer.import_pull_request_comments' do + expect(importer_helper).not_to receive(:import_pull_request_comments) + + importer.execute + end + end + + context 'when the merge request exists but not for this project' do + let_it_be(:another_project) { create(:project) } + + before do + merge_request.update!(source_project: another_project, target_project: another_project) + end + + it 'does not call Importer.import_pull_request_comments' do + expect(importer_helper).not_to receive(:import_pull_request_comments) + + importer.execute + end + end + + context 'when an error is raised' do + before do + allow(importer_helper).to receive(:import_pull_request_comments).and_raise(StandardError) + end + + it 'tracks the failure and does not fail' do + expect(Gitlab::Import::ImportFailureService).to receive(:track).once + + importer.execute + end + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb new file mode 100644 index 00000000000..c44fc259c3b --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestsNotesImporter, feature_category: :importers do + let_it_be(:project) { create(:project, :import_started) } + let_it_be(:merge_request_1) { create(:merge_request, source_project: project) } + let_it_be(:merge_request_2) { create(:merge_request, source_project: project, source_branch: 'other-branch') } + + subject(:importer) { described_class.new(project) } + + describe '#execute', :clean_gitlab_redis_cache do + it 'imports the notes from each merge request in parallel', :aggregate_failures do + expect(Gitlab::BitbucketImport::ImportPullRequestNotesWorker).to receive(:perform_in).twice + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(2) + expect(Gitlab::Cache::Import::Caching.values_from_set(importer.already_enqueued_cache_key)) + .to match_array(%w[1 2]) + end + + context 'when an error is raised' do + before do + allow(importer).to receive(:mark_as_enqueued).and_raise(StandardError) + end + + it 'tracks the failure and does not fail' do + expect(Gitlab::Import::ImportFailureService).to receive(:track).once + + importer.execute + end + end + + context 'when merge request was already enqueued' do + before do + Gitlab::Cache::Import::Caching.set_add(importer.already_enqueued_cache_key, 2) + end + + it 'does not schedule job for enqueued merge requests', :aggregate_failures do + expect(Gitlab::BitbucketImport::ImportPullRequestNotesWorker).to receive(:perform_in).once + + waiter = importer.execute + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(2) + end + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/ref_converter_spec.rb b/spec/lib/gitlab/bitbucket_import/ref_converter_spec.rb new file mode 100644 index 00000000000..578b661d86b --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/ref_converter_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::RefConverter, feature_category: :importers do + let_it_be(:project_identifier) { 'namespace/repo' } + let_it_be(:project) { create(:project, import_source: project_identifier) } + let(:path) { project.full_path } + + let(:ref_converter) { described_class.new(project) } + + shared_examples 'converts the ref correctly' do + it 'converts the ref to a gitlab reference' do + actual = ref_converter.convert_note(note) + + expect(actual).to eq(expected) + end + end + + context 'when the note has an issue ref' do + let(:note) { "[https://bitbucket.org/namespace/repo/issues/1/first-issue](https://bitbucket.org/namespace/repo/issues/1/first-issue){: data-inline-card='' } " } + let(:expected) { "[http://localhost/#{path}/-/issues/1/](http://localhost/#{path}/-/issues/1/)" } + + it_behaves_like 'converts the ref correctly' + end + + context 'when the note has a pull request ref' do + let(:note) { "[https://bitbucket.org/namespace/repo/pull-requests/7](https://bitbucket.org/namespace/repo/pull-requests/7){: data-inline-card='' } " } + let(:expected) { "[http://localhost/#{path}/-/merge_requests/7](http://localhost/#{path}/-/merge_requests/7)" } + + it_behaves_like 'converts the ref correctly' + end + + context 'when the note has a reference to a branch' do + let(:note) { "[https://bitbucket.org/namespace/repo/src/master/](https://bitbucket.org/namespace/repo/src/master/){: data-inline-card='' } " } + let(:expected) { "[http://localhost/#{path}/-/blob/master/](http://localhost/#{path}/-/blob/master/)" } + + it_behaves_like 'converts the ref correctly' + end + + context 'when the note has a reference to a line in a file' do + let(:note) do + "[https://bitbucket.org/namespace/repo/src/0f16a22c21671421780980c9a7433eb8c986b9af/.gitignore#lines-6] \ + (https://bitbucket.org/namespace/repo/src/0f16a22c21671421780980c9a7433eb8c986b9af/.gitignore#lines-6) \ + {: data-inline-card='' }" + end + + let(:expected) do + "[http://localhost/#{path}/-/blob/0f16a22c21671421780980c9a7433eb8c986b9af/.gitignore#L6] \ + (http://localhost/#{path}/-/blob/0f16a22c21671421780980c9a7433eb8c986b9af/.gitignore#L6)" + end + + it_behaves_like 'converts the ref correctly' + end + + context 'when the note has a reference to a file' do + let(:note) { "[https://bitbucket.org/namespace/repo/src/master/.gitignore](https://bitbucket.org/namespace/repo/src/master/.gitignore){: data-inline-card='' } " } + let(:expected) { "[http://localhost/#{path}/-/blob/master/.gitignore](http://localhost/#{path}/-/blob/master/.gitignore)" } + + it_behaves_like 'converts the ref correctly' + end + + context 'when the note does not have a ref' do + let(:note) { 'Hello world' } + let(:expected) { 'Hello world' } + + it_behaves_like 'converts the ref correctly' + end +end diff --git a/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_importer_spec.rb index 3c84d888c92..1ae68f9efb8 100644 --- a/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_importer_spec.rb @@ -48,6 +48,68 @@ RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestImporter, fe end end + describe 'merge request diff head_commit_sha' do + before do + allow(pull_request).to receive(:source_branch_sha).and_return(source_branch_sha) + end + + context 'when a commit with the source_branch_sha exists' do + let(:source_branch_sha) { project.repository.head_commit.sha } + + it 'is equal to the source_branch_sha' do + importer.execute + + merge_request = project.merge_requests.find_by_iid(pull_request.iid) + + expect(merge_request.merge_request_diffs.first.head_commit_sha).to eq(source_branch_sha) + end + end + + context 'when a commit with the source_branch_sha does not exist' do + let(:source_branch_sha) { 'x' * Commit::MIN_SHA_LENGTH } + + it 'is nil' do + importer.execute + + merge_request = project.merge_requests.find_by_iid(pull_request.iid) + + expect(merge_request.merge_request_diffs.first.head_commit_sha).to be_nil + end + + context 'when a commit containing the sha in the message exists' do + let(:source_branch_sha) { project.repository.head_commit.sha } + + it 'is equal to the sha' do + message = " + Squashed commit of the following: + + commit #{source_branch_sha} + Author: John Smith <john@smith.com> + Date: Mon Sep 18 15:58:38 2023 +0200 + + My commit message + " + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: message, + file_path: 'files/lfs/ruby.rb', + file_content: 'testing' + ).execute + + importer.execute + + merge_request = project.merge_requests.find_by_iid(pull_request.iid) + + expect(merge_request.merge_request_diffs.first.head_commit_sha).to eq(source_branch_sha) + end + end + end + end + it 'logs its progress' do expect(Gitlab::BitbucketServerImport::Logger) .to receive(:info).with(include(message: 'starting', iid: pull_request.iid)).and_call_original diff --git a/spec/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer_spec.rb index b9a9c8dac29..af8a0202083 100644 --- a/spec/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestsImporter, feature_category: :importers do let_it_be(:project) do - create(:project, :import_started, + create(:project, :with_import_url, :import_started, :empty_repo, import_data_attributes: { data: { 'project_key' => 'key', 'repo_slug' => 'slug' }, credentials: { 'base_uri' => 'http://bitbucket.org/', 'user' => 'bitbucket', 'password' => 'password' } @@ -19,8 +19,30 @@ RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestsImporter, f allow_next_instance_of(BitbucketServer::Client) do |client| allow(client).to receive(:pull_requests).and_return( [ - BitbucketServer::Representation::PullRequest.new({ 'id' => 1 }), - BitbucketServer::Representation::PullRequest.new({ 'id' => 2 }) + BitbucketServer::Representation::PullRequest.new( + { + 'id' => 1, + 'state' => 'MERGED', + 'fromRef' => { 'latestCommit' => 'aaaa1' }, + 'toRef' => { 'latestCommit' => 'aaaa2' } + } + ), + BitbucketServer::Representation::PullRequest.new( + { + 'id' => 2, + 'state' => 'DECLINED', + 'fromRef' => { 'latestCommit' => 'bbbb1' }, + 'toRef' => { 'latestCommit' => 'bbbb2' } + } + ), + BitbucketServer::Representation::PullRequest.new( + { + 'id' => 3, + 'state' => 'OPEN', + 'fromRef' => { 'latestCommit' => 'cccc1' }, + 'toRef' => { 'latestCommit' => 'cccc2' } + } + ) ], [] ) @@ -28,14 +50,14 @@ RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestsImporter, f end it 'imports each pull request in parallel', :aggregate_failures do - expect(Gitlab::BitbucketServerImport::ImportPullRequestWorker).to receive(:perform_in).twice + expect(Gitlab::BitbucketServerImport::ImportPullRequestWorker).to receive(:perform_in).thrice waiter = importer.execute expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) - expect(waiter.jobs_remaining).to eq(2) + expect(waiter.jobs_remaining).to eq(3) expect(Gitlab::Cache::Import::Caching.values_from_set(importer.already_processed_cache_key)) - .to match_array(%w[1 2]) + .to match_array(%w[1 2 3]) end context 'when pull request was already processed' do @@ -44,12 +66,68 @@ RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestsImporter, f end it 'does not schedule job for processed pull requests', :aggregate_failures do - expect(Gitlab::BitbucketServerImport::ImportPullRequestWorker).to receive(:perform_in).once + expect(Gitlab::BitbucketServerImport::ImportPullRequestWorker).to receive(:perform_in).twice waiter = importer.execute expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) - expect(waiter.jobs_remaining).to eq(2) + expect(waiter.jobs_remaining).to eq(3) + end + end + + context 'when pull requests are in merged or declined status' do + it 'fetches latest commits from the remote repository' do + expect(project.repository).to receive(:fetch_remote).with( + project.import_url, + refmap: %w[aaaa1 aaaa2 bbbb1 bbbb2], + prune: false + ) + + importer.execute + end + + context 'when feature flag "fetch_commits_for_bitbucket_server" is disabled' do + before do + stub_feature_flags(fetch_commits_for_bitbucket_server: false) + end + + it 'does not fetch anything' do + expect(project.repository).not_to receive(:fetch_remote) + importer.execute + end + end + + context 'when there are no commits to process' do + before do + Gitlab::Cache::Import::Caching.set_add(importer.already_processed_cache_key, 1) + Gitlab::Cache::Import::Caching.set_add(importer.already_processed_cache_key, 2) + end + + it 'does not fetch anything' do + expect(project.repository).not_to receive(:fetch_remote) + + importer.execute + end + end + + context 'when fetch process is failed' do + let(:exception) { ArgumentError.new('blank or empty URL') } + + before do + allow(project.repository).to receive(:fetch_remote).and_raise(exception) + end + + it 'rescues and logs the exception' do + expect(Gitlab::Import::ImportFailureService) + .to receive(:track) + .with( + project_id: project.id, + exception: exception, + error_source: described_class.name + ).and_call_original + + importer.execute + end end end end diff --git a/spec/lib/gitlab/chat_spec.rb b/spec/lib/gitlab/chat_spec.rb deleted file mode 100644 index a9df35ace98..00000000000 --- a/spec/lib/gitlab/chat_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Chat, :use_clean_rails_memory_store_caching do - describe '.available?' do - it 'returns true when the chatops feature is available' do - stub_feature_flags(chatops: true) - - expect(described_class).to be_available - end - - it 'returns false when the chatops feature is not available' do - stub_feature_flags(chatops: false) - - expect(described_class).not_to be_available - end - end -end diff --git a/spec/lib/gitlab/checks/global_file_size_check_spec.rb b/spec/lib/gitlab/checks/global_file_size_check_spec.rb index a2b3ee0f761..db615053356 100644 --- a/spec/lib/gitlab/checks/global_file_size_check_spec.rb +++ b/spec/lib/gitlab/checks/global_file_size_check_spec.rb @@ -34,7 +34,10 @@ RSpec.describe Gitlab::Checks::GlobalFileSizeCheck, feature_category: :source_co end context 'when there are oversized blobs' do - let(:blob_double) { instance_double(Gitlab::Git::Blob, size: 10) } + let(:mock_blob_id) { "88acbfafb1b8fdb7c51db870babce21bd861ac4f" } + let(:mock_blob_size) { 300 * 1024 * 1024 } # 300 MiB + let(:size_msg) { "300.0" } # it is (mock_blob_size / 1024.0 / 1024.0).round(2).to_s + let(:blob_double) { instance_double(Gitlab::Git::Blob, size: mock_blob_size, id: mock_blob_id) } before do allow_next_instance_of(Gitlab::Checks::FileSizeCheck::HookEnvironmentAwareAnyOversizedBlobs, @@ -48,8 +51,15 @@ RSpec.describe Gitlab::Checks::GlobalFileSizeCheck, feature_category: :source_co it 'logs a message with blob size and raises an exception' do expect(Gitlab::AppJsonLogger).to receive(:info).with('Checking for blobs over the file size limit') - expect(Gitlab::AppJsonLogger).to receive(:info).with(message: 'Found blob over global limit', blob_sizes: [10]) - expect { subject.validate! }.to raise_exception(Gitlab::GitAccess::ForbiddenError) + expect(Gitlab::AppJsonLogger).to receive(:info).with( + message: 'Found blob over global limit', + blob_sizes: [mock_blob_size], + blob_details: { mock_blob_id => { "size" => mock_blob_size } } + ) + expect do + subject.validate! + end.to raise_exception(Gitlab::GitAccess::ForbiddenError, + /- #{mock_blob_id} \(#{size_msg} MiB\)/) end context 'when the enforce_global_file_size_limit feature flag is disabled' do diff --git a/spec/lib/gitlab/checks/tag_check_spec.rb b/spec/lib/gitlab/checks/tag_check_spec.rb index 60d3eb4bfb3..b5aafde006f 100644 --- a/spec/lib/gitlab/checks/tag_check_spec.rb +++ b/spec/lib/gitlab/checks/tag_check_spec.rb @@ -41,6 +41,36 @@ RSpec.describe Gitlab::Checks::TagCheck, feature_category: :source_code_manageme expect { subject.validate! }.not_to raise_error end end + + it "prohibits tag names that include characters incompatible with UTF-8" do + allow(subject).to receive(:tag_name).and_return("v6.0.0-\xCE.BETA") + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "Tag names must be valid when converted to UTF-8 encoding") + end + + it "doesn't prohibit UTF-8 compatible characters" do + allow(subject).to receive(:tag_name).and_return("v6.0.0-Ü.BETA") + + expect { subject.validate! }.not_to raise_error + end + + context "when prohibited_tag_name_encoding_check feature flag is disabled" do + before do + stub_feature_flags(prohibited_tag_name_encoding_check: false) + end + + it "doesn't prohibit tag names that include characters incompatible with UTF-8" do + allow(subject).to receive(:tag_name).and_return("v6.0.0-\xCE.BETA") + + expect { subject.validate! }.not_to raise_error + end + + it "doesn't prohibit UTF-8 compatible characters" do + allow(subject).to receive(:tag_name).and_return("v6.0.0-Ü.BETA") + + expect { subject.validate! }.not_to raise_error + end + end end context 'with protected tag' do diff --git a/spec/lib/gitlab/ci/build/context/build_spec.rb b/spec/lib/gitlab/ci/build/context/build_spec.rb index 6047eb1b1e0..fae02e140f2 100644 --- a/spec/lib/gitlab/ci/build/context/build_spec.rb +++ b/spec/lib/gitlab/ci/build/context/build_spec.rb @@ -4,7 +4,13 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_composition do let(:pipeline) { create(:ci_pipeline) } - let(:seed_attributes) { { 'name' => 'some-job' } } + let(:seed_attributes) do + { + name: 'some-job', + tag_list: %w[ruby docker postgresql], + needs_attributes: [{ name: 'setup-test-env', artifacts: true, optional: false }] + } + end subject(:context) { described_class.new(pipeline, seed_attributes) } @@ -23,7 +29,7 @@ RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_co end context 'when environment:name is provided' do - let(:seed_attributes) { { 'name' => 'some-job', 'environment' => 'test' } } + let(:seed_attributes) { { name: 'some-job', environment: 'test' } } it { is_expected.to include('CI_ENVIRONMENT_NAME' => 'test') } end @@ -35,6 +41,16 @@ RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_co it { expect(context.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) } it_behaves_like 'variables collection' + + context 'with FF disabled' do + before do + stub_feature_flags(reduced_build_attributes_list_for_rules: false) + end + + it { expect(context.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) } + + it_behaves_like 'variables collection' + end end describe '#variables_hash' do @@ -43,5 +59,15 @@ RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_co it { expect(context.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) } it_behaves_like 'variables collection' + + context 'with FF disabled' do + before do + stub_feature_flags(reduced_build_attributes_list_for_rules: false) + end + + it { expect(context.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) } + + it_behaves_like 'variables collection' + end end end diff --git a/spec/lib/gitlab/ci/build/duration_parser_spec.rb b/spec/lib/gitlab/ci/build/duration_parser_spec.rb index bc905aa0a35..7f5ff1eb0ee 100644 --- a/spec/lib/gitlab/ci/build/duration_parser_spec.rb +++ b/spec/lib/gitlab/ci/build/duration_parser_spec.rb @@ -25,8 +25,8 @@ RSpec.describe Gitlab::Ci::Build::DurationParser do it { is_expected.to be_truthy } it 'caches data' do - expect(ChronicDuration).to receive(:parse).with(value, use_complete_matcher: true).once.and_call_original - expect(ChronicDuration).to receive(:parse).with(other_value, use_complete_matcher: true).once.and_call_original + expect(ChronicDuration).to receive(:parse).with(value).once.and_call_original + expect(ChronicDuration).to receive(:parse).with(other_value).once.and_call_original 2.times do expect(described_class.validate_duration(value)).to eq(86400) @@ -41,7 +41,7 @@ RSpec.describe Gitlab::Ci::Build::DurationParser do it { is_expected.to be_falsy } it 'caches data' do - expect(ChronicDuration).to receive(:parse).with(value, use_complete_matcher: true).once.and_call_original + expect(ChronicDuration).to receive(:parse).with(value).once.and_call_original 2.times do expect(described_class.validate_duration(value)).to be_falsey diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb index 97843781891..0bdcfcfd546 100644 --- a/spec/lib/gitlab/ci/components/instance_path_spec.rb +++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline_composition do let_it_be(:user) { create(:user) } - let(:path) { described_class.new(address: address, content_filename: 'template.yml') } + let(:path) { described_class.new(address: address) } let(:settings) { GitlabSettings::Options.build({ 'component_fqdn' => current_host }) } let(:current_host) { 'acme.com/' } @@ -44,9 +44,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline context 'when the component is simple (single file template)' do it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine_1') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine_1') + expect(result.path).to eq('templates/secret-detection.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('templates/secret-detection.yml') expect(path.project).to eq(project) expect(path.sha).to eq(project.commit('master').id) end @@ -56,9 +57,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline let(:address) { "acme.com/#{project_path}/dast@#{version}" } it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine_2') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine_2') + expect(result.path).to eq('templates/dast/template.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('templates/dast/template.yml') expect(path.project).to eq(project) expect(path.sha).to eq(project.commit('master').id) end @@ -67,7 +69,8 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline let(:address) { "acme.com/#{project_path}/dast/another-folder@#{version}" } it 'returns nil' do - expect(path.fetch_content!(current_user: user)).to be_nil + result = path.fetch_content!(current_user: user) + expect(result.content).to be_nil end end @@ -75,7 +78,8 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline let(:address) { "acme.com/#{project_path}/dast/another-template@#{version}" } it 'returns nil' do - expect(path.fetch_content!(current_user: user)).to be_nil + result = path.fetch_content!(current_user: user) + expect(result.content).to be_nil end end end @@ -110,9 +114,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline end it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine_2') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine_2') + expect(result.path).to eq('templates/secret-detection.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('templates/secret-detection.yml') expect(path.project).to eq(project) expect(path.sha).to eq(latest_sha) end @@ -124,7 +129,6 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline it 'returns nil', :aggregate_failures do expect(path.fetch_content!(current_user: user)).to be_nil expect(path.host).to eq(current_host) - expect(path.project_file_path).to be_nil expect(path.project).to eq(project) expect(path.sha).to be_nil end @@ -135,9 +139,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline let(:current_host) { 'acme.com/gitlab/' } it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine_1') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine_1') + expect(result.path).to eq('templates/secret-detection.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('templates/secret-detection.yml') expect(path.project).to eq(project) expect(path.sha).to eq(project.commit('master').id) end @@ -164,9 +169,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline end it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine') + expect(result.path).to eq('component/template.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('component/template.yml') expect(path.project).to eq(project) expect(path.sha).to eq(project.commit('master').id) end @@ -184,9 +190,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline end it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine') + expect(result.path).to eq('component/template.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('component/template.yml') expect(path.project).to eq(project) expect(path.sha).to eq(project.commit('master').id) end @@ -197,9 +204,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline let(:current_host) { 'acme.com/gitlab/' } it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine') + expect(result.path).to eq('component/template.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('component/template.yml') expect(path.project).to eq(project) expect(path.sha).to eq(project.commit('master').id) end @@ -211,7 +219,6 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline it 'returns nil', :aggregate_failures do expect(path.fetch_content!(current_user: user)).to be_nil expect(path.host).to eq(current_host) - expect(path.project_file_path).to be_nil expect(path.project).to eq(project) expect(path.sha).to be_nil end diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb index 0f7b811b5df..88e272ac3fd 100644 --- a/spec/lib/gitlab/ci/config/external/file/component_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb @@ -99,7 +99,9 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: let(:response) do ServiceResponse.success(payload: { content: content, - path: instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345') + path: 'templates/component.yml', + project: project, + sha: '12345' }) end @@ -132,7 +134,9 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: let(:response) do ServiceResponse.success(payload: { content: content, - path: instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345') + path: 'templates/component.yml', + project: project, + sha: '12345' }) end @@ -158,15 +162,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: describe '#metadata' do subject(:metadata) { external_resource.metadata } - let(:component_path) do - instance_double(::Gitlab::Ci::Components::InstancePath, - project: project, - sha: '12345', - project_file_path: 'my-component/template.yml') - end - let(:response) do - ServiceResponse.success(payload: { path: component_path }) + ServiceResponse.success(payload: { path: 'my-component/template.yml', project: project, sha: '12345' }) end it 'returns the metadata' do @@ -183,14 +180,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: end describe '#expand_context' do - let(:component_path) do - instance_double(::Gitlab::Ci::Components::InstancePath, - project: project, - sha: '12345') - end - let(:response) do - ServiceResponse.success(payload: { path: component_path }) + ServiceResponse.success(payload: { path: 'templates/component.yml', project: project, sha: '12345' }) end subject { external_resource.send(:expand_context_attrs) } @@ -207,11 +198,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: describe '#to_hash' do context 'when interpolation is being used' do let(:response) do - ServiceResponse.success(payload: { content: content, path: path }) - end - - let(:path) do - instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345') + ServiceResponse.success(payload: { content: content, path: 'templates/component.yml', project: project, + sha: '12345' }) end let(:content) do diff --git a/spec/lib/gitlab/ci/config/header/input_spec.rb b/spec/lib/gitlab/ci/config/header/input_spec.rb index b5155dff6e8..5d1fa4a8e6e 100644 --- a/spec/lib/gitlab/ci/config/header/input_spec.rb +++ b/spec/lib/gitlab/ci/config/header/input_spec.rb @@ -46,6 +46,12 @@ RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_co it_behaves_like 'a valid input' end + context 'when has a description value' do + let(:input_hash) { { description: 'bar' } } + + it_behaves_like 'a valid input' + end + context 'when is a required input' do let(:input_hash) { nil } @@ -62,6 +68,12 @@ RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_co end end + context 'when the input has RegEx validation' do + let(:input_hash) { { regex: '\w+' } } + + it_behaves_like 'a valid input' + end + context 'when given an invalid type' do let(:input_hash) { { type: 'datetime' } } let(:expected_errors) { ['foo input type unknown value: datetime'] } @@ -84,4 +96,11 @@ RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_co it_behaves_like 'an invalid input' end + + context 'when RegEx validation value is not a string' do + let(:input_hash) { { regex: [] } } + let(:expected_errors) { ['foo input regex should be a string'] } + + it_behaves_like 'an invalid input' + end end diff --git a/spec/lib/gitlab/ci/config/interpolation/context_spec.rb b/spec/lib/gitlab/ci/config/interpolation/context_spec.rb index c90866c986a..56a572312eb 100644 --- a/spec/lib/gitlab/ci/config/interpolation/context_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/context_spec.rb @@ -17,6 +17,12 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Context, feature_category: :pi end end + describe '.new' do + it 'returns variables as a Variables::Collection object' do + expect(subject.variables.class).to eq(Gitlab::Ci::Variables::Collection) + end + end + describe '#to_h' do it 'returns the context hash' do expect(subject.to_h).to eq(ctx) diff --git a/spec/lib/gitlab/ci/config/interpolation/functions/base_spec.rb b/spec/lib/gitlab/ci/config/interpolation/functions/base_spec.rb index c193e88dbe2..a2b575afb6f 100644 --- a/spec/lib/gitlab/ci/config/interpolation/functions/base_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/functions/base_spec.rb @@ -18,6 +18,6 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Functions::Base, feature_categ it 'defines an expected interface for child classes' do expect { described_class.function_expression_pattern }.to raise_error(NotImplementedError) expect { described_class.name }.to raise_error(NotImplementedError) - expect { custom_function_klass.new('test').execute('input') }.to raise_error(NotImplementedError) + expect { custom_function_klass.new('test', nil).execute('input') }.to raise_error(NotImplementedError) end end diff --git a/spec/lib/gitlab/ci/config/interpolation/functions/expand_vars_spec.rb b/spec/lib/gitlab/ci/config/interpolation/functions/expand_vars_spec.rb new file mode 100644 index 00000000000..2a627b435d3 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/functions/expand_vars_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::Functions::ExpandVars, feature_category: :pipeline_composition do + let(:variables) do + Gitlab::Ci::Variables::Collection.new([ + { key: 'VAR1', value: 'value1', masked: false }, + { key: 'VAR2', value: 'value2', masked: false }, + { key: 'NESTED_VAR', value: '$MY_VAR', masked: false }, + { key: 'MASKED_VAR', value: 'masked', masked: true } + ]) + end + + let(:function_expression) { 'expand_vars' } + let(:ctx) { Gitlab::Ci::Config::Interpolation::Context.new({}, variables: variables) } + + subject(:function) { described_class.new(function_expression, ctx) } + + describe '#execute' do + let(:input_value) { '$VAR1' } + + subject(:execute) { function.execute(input_value) } + + it 'expands the variable' do + expect(execute).to eq('value1') + expect(function).to be_valid + end + + context 'when the variable contains another variable' do + let(:input_value) { '$NESTED_VAR' } + + it 'does not expand the inner variable' do + expect(execute).to eq('$MY_VAR') + expect(function).to be_valid + end + end + + context 'when the variable is masked' do + let(:input_value) { '$MASKED_VAR' } + + it 'returns an error' do + expect(execute).to be_nil + expect(function).not_to be_valid + expect(function.errors).to contain_exactly( + 'error in `expand_vars` function: variable expansion error: masked variables cannot be expanded' + ) + end + end + + context 'when the variable is unknown' do + let(:input_value) { '$UNKNOWN_VAR' } + + it 'does not expand the variable' do + expect(execute).to eq('$UNKNOWN_VAR') + expect(function).to be_valid + end + end + + context 'when there are multiple variables' do + let(:input_value) { '${VAR1} $VAR2 %VAR1%' } + + it 'expands the variables' do + expect(execute).to eq('value1 value2 value1') + expect(function).to be_valid + end + end + + context 'when the input is not a string' do + let(:input_value) { 100 } + + it 'returns an error' do + expect(execute).to be_nil + expect(function).not_to be_valid + expect(function.errors).to contain_exactly( + 'error in `expand_vars` function: invalid input type: expand_vars can only be used with string inputs' + ) + end + end + end + + describe '.matches?' do + it 'matches exactly the expand_vars function with no arguments' do + expect(described_class.matches?('expand_vars')).to be_truthy + expect(described_class.matches?('expand_vars()')).to be_falsey + expect(described_class.matches?('expand_vars(1)')).to be_falsey + expect(described_class.matches?('unknown')).to be_falsey + end + end +end diff --git a/spec/lib/gitlab/ci/config/interpolation/functions/truncate_spec.rb b/spec/lib/gitlab/ci/config/interpolation/functions/truncate_spec.rb index c521eff9811..93e5d4ef48c 100644 --- a/spec/lib/gitlab/ci/config/interpolation/functions/truncate_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/functions/truncate_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Functions::Truncate, feature_c end it 'truncates the given input' do - function = described_class.new('truncate(1,2)') + function = described_class.new('truncate(1,2)', nil) output = function.execute('test') @@ -22,7 +22,7 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Functions::Truncate, feature_c context 'when given a non-string input' do it 'returns an error' do - function = described_class.new('truncate(1,2)') + function = described_class.new('truncate(1,2)', nil) function.execute(100) diff --git a/spec/lib/gitlab/ci/config/interpolation/functions_stack_spec.rb b/spec/lib/gitlab/ci/config/interpolation/functions_stack_spec.rb index 881f092c440..9ac0ef05c61 100644 --- a/spec/lib/gitlab/ci/config/interpolation/functions_stack_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/functions_stack_spec.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Config::Interpolation::FunctionsStack, feature_category: :pipeline_composition do let(:functions) { ['truncate(0,4)', 'truncate(1,2)'] } let(:input_value) { 'test_input_value' } - subject { described_class.new(functions).evaluate(input_value) } + subject { described_class.new(functions, nil).evaluate(input_value) } it 'modifies the given input value according to the function expressions' do expect(subject).to be_success diff --git a/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb b/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb index ea06f181fa4..b0618081207 100644 --- a/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb @@ -7,130 +7,303 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs, feature_category: :pip let(:specs) { { foo: { default: 'bar' } } } let(:args) { {} } - context 'when inputs are valid' do - where(:specs, :args, :merged) do - [ - [ - { foo: { default: 'bar' } }, {}, - { foo: 'bar' } - ], - [ - { foo: { default: 'bar' } }, { foo: 'test' }, - { foo: 'test' } - ], - [ - { foo: nil }, { foo: 'bar' }, - { foo: 'bar' } - ], - [ - { foo: { type: 'string' } }, { foo: 'bar' }, - { foo: 'bar' } - ], - [ - { foo: { type: 'string', default: 'bar' } }, { foo: 'test' }, - { foo: 'test' } - ], - [ - { foo: { type: 'string', default: 'bar' } }, {}, - { foo: 'bar' } - ], - [ - { foo: { default: 'bar' }, baz: nil }, { baz: 'test' }, - { foo: 'bar', baz: 'test' } - ], - [ - { number_input: { type: 'number' } }, - { number_input: 8 }, - { number_input: 8 } - ], - [ - { default_number_input: { default: 9, type: 'number' } }, - {}, - { default_number_input: 9 } - ], - [ - { true_input: { type: 'boolean' }, false_input: { type: 'boolean' } }, - { true_input: true, false_input: false }, - { true_input: true, false_input: false } - ], - [ - { default_boolean_input: { default: true, type: 'boolean' } }, - {}, - { default_boolean_input: true } - ] - ] - end - - with_them do - it 'contains the merged inputs' do + context 'when given unrecognized inputs' do + let(:specs) { { foo: nil } } + let(:args) { { foo: 'bar', test: 'bar' } } + + it 'is invalid' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly('unknown input arguments: test') + end + end + + context 'when given unrecognized configuration keywords' do + let(:specs) { { foo: 123 } } + let(:args) { {} } + + it 'is invalid' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly( + 'unknown input specification for `foo` (valid types: boolean, number, string)' + ) + end + end + + context 'when the inputs have multiple errors' do + let(:specs) { { foo: nil } } + let(:args) { { test: 'bar', gitlab: '1' } } + + it 'reports all of them' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly( + 'unknown input arguments: test, gitlab', + '`foo` input: required value has not been provided' + ) + end + end + + describe 'required inputs' do + let(:specs) { { foo: nil } } + + context 'when a value is given' do + let(:args) { { foo: 'bar' } } + + it 'is valid' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(foo: 'bar') + end + end + + context 'when no value is given' do + let(:args) { {} } + + it 'is invalid' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly('`foo` input: required value has not been provided') + end + end + end + + describe 'inputs with a default value' do + let(:specs) { { foo: { default: 'bar' } } } + + context 'when a value is given' do + let(:args) { { foo: 'test' } } + + it 'uses the given value' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(foo: 'test') + end + end + + context 'when no value is given' do + let(:args) { {} } + + it 'uses the default value' do expect(inputs).to be_valid - expect(inputs.to_hash).to eq(merged) + expect(inputs.to_hash).to eq(foo: 'bar') end end end - context 'when inputs are invalid' do - where(:specs, :args, :errors) do - [ - [ - { foo: nil }, { foo: 'bar', test: 'bar' }, - ['unknown input arguments: test'] - ], - [ - { foo: nil }, { test: 'bar', gitlab: '1' }, - ['unknown input arguments: test, gitlab', '`foo` input: required value has not been provided'] - ], - [ - { foo: 123 }, {}, - ['unknown input specification for `foo` (valid types: boolean, number, string)'] - ], - [ - { a: nil, foo: 123 }, { a: '123' }, - ['unknown input specification for `foo` (valid types: boolean, number, string)'] - ], - [ - { foo: nil }, {}, - ['`foo` input: required value has not been provided'] - ], - [ - { foo: { default: 123 } }, { foo: 'test' }, - ['`foo` input: default value is not a string'] - ], - [ - { foo: { default: 'test' } }, { foo: 123 }, - ['`foo` input: provided value is not a string'] - ], - [ - { foo: nil }, { foo: 123 }, - ['`foo` input: provided value is not a string'] - ], - [ - { number_input: { type: 'number' } }, - { number_input: 'NaN' }, - ['`number_input` input: provided value is not a number'] - ], - [ - { default_number_input: { default: 'NaN', type: 'number' } }, - {}, - ['`default_number_input` input: default value is not a number'] - ], - [ - { boolean_input: { type: 'boolean' } }, - { boolean_input: 'string' }, - ['`boolean_input` input: provided value is not a boolean'] - ], - [ - { default_boolean_input: { default: 'string', type: 'boolean' } }, - {}, - ['`default_boolean_input` input: default value is not a boolean'] - ] - ] - end - - with_them do - it 'contains the merged inputs', :aggregate_failures do + describe 'inputs with type validation' do + describe 'string validation' do + let(:specs) { { a_input: nil, b_input: { default: 'test' }, c_input: { default: 123 } } } + let(:args) { { a_input: 123, b_input: 123, c_input: 'test' } } + + it 'is the default type' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly( + '`a_input` input: provided value is not a string', + '`b_input` input: provided value is not a string', + '`c_input` input: default value is not a string' + ) + end + + context 'when the value is a string' do + let(:specs) { { foo: { type: 'string' } } } + let(:args) { { foo: 'bar' } } + + it 'is valid' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(foo: 'bar') + end + end + + context 'when the default is a string' do + let(:specs) { { foo: { type: 'string', default: 'bar' } } } + let(:args) { {} } + + it 'is valid' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(foo: 'bar') + end + end + + context 'when the value is not a string' do + let(:specs) { { foo: { type: 'string' } } } + let(:args) { { foo: 123 } } + + it 'is invalid' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly('`foo` input: provided value is not a string') + end + end + + context 'when the default is not a string' do + let(:specs) { { foo: { default: 123, type: 'string' } } } + let(:args) { {} } + + it 'is invalid' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly('`foo` input: default value is not a string') + end + end + end + + describe 'number validation' do + let(:specs) { { integer: { type: 'number' }, float: { type: 'number' } } } + + context 'when the value is a float or integer' do + let(:args) { { integer: 6, float: 6.6 } } + + it 'is valid' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(integer: 6, float: 6.6) + end + end + + context 'when the default is a float or integer' do + let(:specs) { { integer: { default: 6, type: 'number' }, float: { default: 6.6, type: 'number' } } } + + it 'is valid' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(integer: 6, float: 6.6) + end + end + + context 'when the value is not a number' do + let(:specs) { { number_input: { type: 'number' } } } + let(:args) { { number_input: 'NaN' } } + + it 'is invalid' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly('`number_input` input: provided value is not a number') + end + end + + context 'when the default is not a number' do + let(:specs) { { number_input: { default: 'NaN', type: 'number' } } } + let(:args) { {} } + + it 'is invalid' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly('`number_input` input: default value is not a number') + end + end + end + + describe 'boolean validation' do + context 'when the value is true or false' do + let(:specs) { { truthy: { type: 'boolean' }, falsey: { type: 'boolean' } } } + let(:args) { { truthy: true, falsey: false } } + + it 'is valid' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(truthy: true, falsey: false) + end + end + + context 'when the default is true or false' do + let(:specs) { { truthy: { default: true, type: 'boolean' }, falsey: { default: false, type: 'boolean' } } } + let(:args) { {} } + + it 'is valid' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(truthy: true, falsey: false) + end + end + + context 'when the value is not a boolean' do + let(:specs) { { boolean_input: { type: 'boolean' } } } + let(:args) { { boolean_input: 'string' } } + + it 'is invalid' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly('`boolean_input` input: provided value is not a boolean') + end + end + + context 'when the default is not a boolean' do + let(:specs) { { boolean_input: { default: 'string', type: 'boolean' } } } + let(:args) { {} } + + it 'is invalid' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly('`boolean_input` input: default value is not a boolean') + end + end + end + + context 'when given an unknown type' do + let(:specs) { { unknown: { type: 'datetime' } } } + let(:args) { { unknown: '2023-10-31' } } + + it 'is invalid' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly( + 'unknown input specification for `unknown` (valid types: boolean, number, string)' + ) + end + end + end + + describe 'inputs with RegEx validation' do + context 'when given a value that matches the pattern' do + let(:specs) { { test_input: { regex: '^input_value$' } } } + let(:args) { { test_input: 'input_value' } } + + it 'is valid' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(test_input: 'input_value') + end + end + + context 'when given a default that matches the pattern' do + let(:specs) { { test_input: { default: 'input_value', regex: '^input_value$' } } } + let(:args) { {} } + + it 'is valid' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(test_input: 'input_value') + end + end + + context 'when given a value that does not match the pattern' do + let(:specs) { { test_input: { regex: '^input_value$' } } } + let(:args) { { test_input: 'input' } } + + it 'is invalid' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly( + '`test_input` input: provided value does not match required RegEx pattern' + ) + end + end + + context 'when given a default that does not match the pattern' do + let(:specs) { { test_input: { default: 'input', regex: '^input_value$' } } } + let(:args) { {} } + + it 'is invalid' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly( + '`test_input` input: default value does not match required RegEx pattern' + ) + end + end + + context 'when used with any type other than `string`' do + let(:specs) { { test_input: { regex: '^input_value$', type: 'number' } } } + let(:args) { { test_input: 999 } } + + it 'is invalid' do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly( + '`test_input` input: RegEx validation can only be used with string inputs' + ) + end + end + + context 'when the pattern is unsafe' do + let(:specs) { { test_input: { regex: 'a++' } } } + let(:args) { { test_input: 'aaaaaaaaaaaaaaaaaaaaa' } } + + it 'is invalid' do expect(inputs).not_to be_valid - expect(inputs.errors).to contain_exactly(*errors) + expect(inputs.errors).to contain_exactly( + '`test_input` input: invalid regular expression' + ) end end end diff --git a/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb b/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb index 804164c933a..c924323837b 100644 --- a/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Interpolator, feature_category let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(config: [header, content]) } - subject { described_class.new(result, arguments) } + subject { described_class.new(result, arguments, []) } context 'when input data is valid' do let(:header) do diff --git a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb index 57a9a47d699..684da1df43b 100644 --- a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb @@ -58,4 +58,36 @@ RSpec.describe ::Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_c end end end + + describe '#load_uninterpolated_yaml' do + let(:yaml) do + <<~YAML + --- + spec: + inputs: + test_input: + --- + test_job: + script: + - echo "$[[ inputs.test_input ]]" + YAML + end + + subject(:result) { described_class.new(yaml).load_uninterpolated_yaml } + + it 'returns the config' do + expected_content = { test_job: { script: ["echo \"$[[ inputs.test_input ]]\""] } } + expect(result).to be_valid + expect(result.content).to eq(expected_content) + end + + context 'when there is a format error in the yaml' do + let(:yaml) { 'invalid: yaml: all the time' } + + it 'returns an error' do + expect(result).not_to be_valid + expect(result.error).to include('mapping values are not allowed in this context') + end + end + end end diff --git a/spec/lib/gitlab/ci/config/yaml/result_spec.rb b/spec/lib/gitlab/ci/config/yaml/result_spec.rb index a66c630dfc9..5e9dee02190 100644 --- a/spec/lib/gitlab/ci/config/yaml/result_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml/result_spec.rb @@ -3,12 +3,44 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Yaml::Result, feature_category: :pipeline_composition do + it 'raises an error when reading a header when there is none' do + result = described_class.new(config: { b: 2 }) + + expect { result.header }.to raise_error(ArgumentError) + end + + it 'stores an error / exception when initialized with it' do + result = described_class.new(error: ArgumentError.new('abc')) + + expect(result).not_to be_valid + expect(result.error).to be_a ArgumentError + end + it 'does not have a header when config is a single hash' do result = described_class.new(config: { a: 1, b: 2 }) expect(result).not_to have_header end + describe '#inputs' do + it 'returns the value of the spec inputs' do + result = described_class.new(config: [{ spec: { inputs: { website: nil } } }, { b: 2 }]) + + expect(result).to have_header + expect(result.inputs).to eq({ website: nil }) + end + end + + describe '#interpolated?' do + it 'defaults to false' do + expect(described_class.new).not_to be_interpolated + end + + it 'returns the value passed to the initializer' do + expect(described_class.new(interpolated: true)).to be_interpolated + end + end + context 'when config is an array of hashes' do context 'when first document matches the header schema' do it 'has a header' do @@ -38,27 +70,4 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Result, feature_category: :pipeline_com expect(result.content).to be_empty end end - - it 'raises an error when reading a header when there is none' do - result = described_class.new(config: { b: 2 }) - - expect { result.header }.to raise_error(ArgumentError) - end - - it 'stores an error / exception when initialized with it' do - result = described_class.new(error: ArgumentError.new('abc')) - - expect(result).not_to be_valid - expect(result.error).to be_a ArgumentError - end - - describe '#interpolated?' do - it 'defaults to false' do - expect(described_class.new).not_to be_interpolated - end - - it 'returns the value passed to the initializer' do - expect(described_class.new(interpolated: true)).to be_interpolated - end - end end diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index 4196aad2db4..1637d084c42 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -7,8 +7,18 @@ RSpec.describe Gitlab::Ci::Lint, feature_category: :pipeline_composition do let_it_be(:user) { create(:user) } let(:sha) { nil } + let(:verify_project_sha) { nil } let(:ref) { project.default_branch } - let(:lint) { described_class.new(project: project, current_user: user, sha: sha) } + let(:kwargs) do + { + project: project, + current_user: user, + sha: sha, + verify_project_sha: verify_project_sha + }.compact + end + + let(:lint) { described_class.new(**kwargs) } describe '#validate' do subject { lint.validate(content, dry_run: dry_run, ref: ref) } @@ -252,6 +262,19 @@ RSpec.describe Gitlab::Ci::Lint, feature_category: :pipeline_composition do subject end + shared_examples 'when sha is not provided' do + it 'runs YamlProcessor with verify_project_sha: false' do + expect(Gitlab::Ci::YamlProcessor) + .to receive(:new) + .with(content, a_hash_including(verify_project_sha: false)) + .and_call_original + + subject + end + end + + it_behaves_like 'when sha is not provided' + context 'when sha is provided' do let(:sha) { project.commit.sha } @@ -288,20 +311,16 @@ RSpec.describe Gitlab::Ci::Lint, feature_category: :pipeline_composition do context 'when a project ref does not contain the sha' do it 'returns an error' do expect(subject).not_to be_valid - expect(subject.errors).to include(/Could not validate configuration/) + expect(subject.errors).to include( + /configuration originates from an external project or a commit not associated with a Git reference/) end end end - end - context 'when sha is not provided' do - it 'runs YamlProcessor with verify_project_sha: false' do - expect(Gitlab::Ci::YamlProcessor) - .to receive(:new) - .with(content, a_hash_including(verify_project_sha: false)) - .and_call_original + context 'when verify_project_sha is false' do + let(:verify_project_sha) { false } - subject + it_behaves_like 'when sha is not provided' end end end @@ -468,7 +487,7 @@ RSpec.describe Gitlab::Ci::Lint, feature_category: :pipeline_composition do end context 'when project is not provided' do - let(:project) { nil } + let(:lint) { described_class.new(project: nil, **kwargs) } let(:project_nil_loggable_data) do expected_data.except('project_id') diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb index 9470d59f502..648b8ac2db9 100644 --- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb @@ -370,6 +370,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common, feature_category: :vulnera end end + describe 'setting CVSS' do + let(:cvss_vectors) { report.findings.filter_map(&:cvss).reject(&:empty?) } + + it 'ingests the provided CVSS vectors' do + expect(cvss_vectors.count).to eq(1) + end + end + describe 'setting the uuid' do let(:finding_uuids) { report.findings.map(&:uuid) } let(:uuid_1) do diff --git a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb index 821a5057d2e..1bab27c877d 100644 --- a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb +++ b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb @@ -111,7 +111,21 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do it_behaves_like '<testcase> XML parser', ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED, - 'Some failure' + "System Err:\n\nSome failure" + end + + context 'and has failure with message, system-out and system-err' do + let(:testcase_content) do + <<-EOF.strip_heredoc + <failure>Some failure</failure> + <system-out>This is the system output</system-out> + <system-err>This is the system err</system-err> + EOF + end + + it_behaves_like '<testcase> XML parser', + ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED, + "Some failure\n\nSystem Out:\n\nThis is the system output\n\nSystem Err:\n\nThis is the system err" end context 'and has error' do @@ -132,7 +146,21 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do it_behaves_like '<testcase> XML parser', ::Gitlab::Ci::Reports::TestCase::STATUS_ERROR, - 'Some error' + "System Err:\n\nSome error" + end + + context 'and has error with message, system-out and system-err' do + let(:testcase_content) do + <<-EOF.strip_heredoc + <error>Some error</error> + <system-out>This is the system output</system-out> + <system-err>This is the system err</system-err> + EOF + end + + it_behaves_like '<testcase> XML parser', + ::Gitlab::Ci::Reports::TestCase::STATUS_ERROR, + "Some error\n\nSystem Out:\n\nThis is the system output\n\nSystem Err:\n\nThis is the system err" end context 'and has skipped' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb index c3516c467d4..2a26747f65a 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb @@ -92,7 +92,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities, feature_categor it 'adds an error about imports' do expect(pipeline.errors.to_a) - .to include /Import in progress/ + .to include /before project import is complete/ end it 'breaks the pipeline builder chain' do diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern/regular_expression_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern/regular_expression_spec.rb new file mode 100644 index 00000000000..145777a9476 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern/regular_expression_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern::RegularExpression, feature_category: :continuous_integration do + describe '#initialize' do + it 'initializes the pattern' do + pattern = described_class.new('/foo/') + + expect(pattern.value).to eq('/foo/') + end + end + + describe '#valid?' do + subject { described_class.new(pattern).valid? } + + context 'with valid expressions' do + let(:pattern) { '/foo\\/bar/' } + + it { is_expected.to be_truthy } + end + + context 'when the value is not a valid regular expression' do + let(:pattern) { 'foo' } + + it { is_expected.to be_falsey } + end + end + + describe '#expression' do + subject { described_class.new(pattern).expression } + + context 'with valid expressions' do + let(:pattern) { '/bar/' } + + it { is_expected.to eq Gitlab::UntrustedRegexp.new('bar') } + end + + context 'when the value is not a valid regular expression' do + let(:pattern) { 'foo' } + + it { expect { subject }.to raise_error(RegexpError) } + end + + context 'when the request store is activated', :request_store do + let(:pattern) { '/foo\\/bar/' } + + it 'fabricates once' do + expect(Gitlab::UntrustedRegexp::RubySyntax).to receive(:fabricate!).once.and_call_original + + 2.times do + expect(described_class.new(pattern).expression).to be_a(Gitlab::UntrustedRegexp) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb index be205395b69..09899cb9fc4 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do +RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern, feature_category: :continuous_integration do describe '#initialize' do context 'when the value is a valid regular expression' do it 'initializes the pattern' do @@ -164,14 +164,5 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do expect(regexp.evaluate).to eq Gitlab::UntrustedRegexp.new('abc') end - - it 'raises error if evaluated regexp is not valid' do - allow(Gitlab::UntrustedRegexp::RubySyntax).to receive(:valid?).and_return(true) - - regexp = described_class.new('/invalid ( .*/') - - expect { regexp.evaluate } - .to raise_error(Gitlab::Ci::Pipeline::Expression::RuntimeError) - end end end diff --git a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb index 040c3ec7f6e..ca1b00e2f5b 100644 --- a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb @@ -22,7 +22,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|created') + expect(status.text).to eq s_('CiStatusText|Created') expect(status.icon).to eq 'status_created' expect(status.favicon).to eq 'favicon_status_created' expect(status.label).to eq 'created' @@ -49,11 +49,11 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|failed') + expect(status.text).to eq s_('CiStatusText|Failed') expect(status.icon).to eq 'status_failed' expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq 'failed' - expect(status.status_tooltip).to eq "#{s_('CiStatusText|failed')} - (unknown failure)" + expect(status.status_tooltip).to eq "#{s_('CiStatusLabel|failed')} - (unknown failure)" expect(status).not_to have_details expect(status).to have_action end @@ -67,7 +67,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou it 'fabricates correct status_tooltip' do expect(status.status_tooltip).to eq( - "#{s_('CiStatusText|failed')} - (downstream pipeline can not be created, Pipeline will not run for the selected trigger. " \ + "#{s_('CiStatusLabel|failed')} - (downstream pipeline can not be created, Pipeline will not run for the selected trigger. " \ "The rules configuration prevented any jobs from being added to the pipeline., other error)" ) end @@ -93,7 +93,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|manual') + expect(status.text).to eq s_('CiStatusText|Manual') expect(status.group).to eq 'manual' expect(status.icon).to eq 'status_manual' expect(status.favicon).to eq 'favicon_status_manual' @@ -128,7 +128,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou end it 'fabricates status with correct details' do - expect(status.text).to eq 'waiting' + expect(status.text).to eq 'Waiting' expect(status.group).to eq 'waiting-for-resource' expect(status.icon).to eq 'status_pending' expect(status.favicon).to eq 'favicon_status_pending' @@ -154,7 +154,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|passed') + expect(status.text).to eq s_('CiStatusText|Passed') expect(status.icon).to eq 'status_success' expect(status.favicon).to eq 'favicon_status_success' expect(status).to have_action diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index f71f3d47452..1d043966321 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|passed') + expect(status.text).to eq s_('CiStatusText|Passed') expect(status.icon).to eq 'status_success' expect(status.favicon).to eq 'favicon_status_success' expect(status.label).to eq s_('CiStatusLabel|passed') @@ -58,7 +58,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|passed') + expect(status.text).to eq s_('CiStatusText|Passed') expect(status.icon).to eq 'status_success' expect(status.favicon).to eq 'favicon_status_success' expect(status.label).to eq s_('CiStatusLabel|passed') @@ -86,11 +86,11 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|failed') + expect(status.text).to eq s_('CiStatusText|Failed') expect(status.icon).to eq 'status_failed' expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq s_('CiStatusLabel|failed') - expect(status.status_tooltip).to eq "#{s_('CiStatusText|failed')} - (unknown failure)" + expect(status.status_tooltip).to eq "#{s_('CiStatusLabel|failed')} - (unknown failure)" expect(status).to have_details expect(status).to have_action end @@ -115,7 +115,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|failed') + expect(status.text).to eq s_('CiStatusText|Failed') expect(status.icon).to eq 'status_warning' expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq 'failed (allowed to fail)' @@ -144,7 +144,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|failed') + expect(status.text).to eq s_('CiStatusText|Failed') expect(status.icon).to eq 'status_failed' expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq s_('CiStatusLabel|failed') @@ -173,7 +173,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|canceled') + expect(status.text).to eq s_('CiStatusText|Canceled') expect(status.icon).to eq 'status_canceled' expect(status.favicon).to eq 'favicon_status_canceled' expect(status.illustration).to include(:image, :size, :title) @@ -200,10 +200,10 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatus|running') + expect(status.text).to eq s_('CiStatusText|Running') expect(status.icon).to eq 'status_running' expect(status.favicon).to eq 'favicon_status_running' - expect(status.label).to eq s_('CiStatus|running') + expect(status.label).to eq s_('CiStatusLabel|running') expect(status).to have_details expect(status).to have_action end @@ -226,7 +226,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|pending') + expect(status.text).to eq s_('CiStatusText|Pending') expect(status.icon).to eq 'status_pending' expect(status.favicon).to eq 'favicon_status_pending' expect(status.illustration).to include(:image, :size, :title, :content) @@ -252,7 +252,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|skipped') + expect(status.text).to eq s_('CiStatusText|Skipped') expect(status.icon).to eq 'status_skipped' expect(status.favicon).to eq 'favicon_status_skipped' expect(status.illustration).to include(:image, :size, :title) @@ -282,7 +282,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|manual') + expect(status.text).to eq s_('CiStatusText|Manual') expect(status.group).to eq 'manual' expect(status.icon).to eq 'status_manual' expect(status.favicon).to eq 'favicon_status_manual' @@ -339,7 +339,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|manual') + expect(status.text).to eq s_('CiStatusText|Manual') expect(status.group).to eq 'manual' expect(status.icon).to eq 'status_manual' expect(status.favicon).to eq 'favicon_status_manual' @@ -370,7 +370,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do end it 'fabricates status with correct details' do - expect(status.text).to eq s_('CiStatusText|scheduled') + expect(status.text).to eq s_('CiStatusText|Scheduled') expect(status.group).to eq 'scheduled' expect(status.icon).to eq 'status_scheduled' expect(status.favicon).to eq 'favicon_status_scheduled' diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index 7fae76f61ea..ddb8b7ecff9 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Status::Canceled do end describe '#text' do - it { expect(subject.text).to eq 'canceled' } + it { expect(subject.text).to eq 'Canceled' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb index 1e54d1ed8c5..19fecbb33b9 100644 --- a/spec/lib/gitlab/ci/status/created_spec.rb +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Status::Created do end describe '#text' do - it { expect(subject.text).to eq 'created' } + it { expect(subject.text).to eq 'Created' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb index 94a6255f1e2..277b440a21d 100644 --- a/spec/lib/gitlab/ci/status/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/factory_spec.rb @@ -74,7 +74,7 @@ RSpec.describe Gitlab::Ci::Status::Factory do end it 'delegates to core status' do - expect(fabricated_status.text).to eq 'passed' + expect(fabricated_status.text).to eq 'Passed' end it 'latest matches status becomes a status name' do @@ -104,7 +104,7 @@ RSpec.describe Gitlab::Ci::Status::Factory do end it 'delegates to core status' do - expect(fabricated_status.text).to eq 'passed' + expect(fabricated_status.text).to eq 'Passed' end it 'matches correct core status' do diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb index f3f3304b04d..48df3e99855 100644 --- a/spec/lib/gitlab/ci/status/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Status::Failed do end describe '#text' do - it { expect(subject.text).to eq 'failed' } + it { expect(subject.text).to eq 'Failed' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb index a9203438898..6e02772f670 100644 --- a/spec/lib/gitlab/ci/status/manual_spec.rb +++ b/spec/lib/gitlab/ci/status/manual_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Status::Manual do end describe '#text' do - it { expect(subject.text).to eq 'manual' } + it { expect(subject.text).to eq 'Manual' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb index 1c062a0133d..82ea987e4c9 100644 --- a/spec/lib/gitlab/ci/status/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Status::Pending do end describe '#text' do - it { expect(subject.text).to eq 'pending' } + it { expect(subject.text).to eq 'Pending' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb b/spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb index 8fd974972e4..8948d83f9cb 100644 --- a/spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::Ci::Status::Pipeline::Blocked do describe '#text' do it 'overrides status text' do - expect(subject.text).to eq 'blocked' + expect(subject.text).to eq 'Blocked' end end diff --git a/spec/lib/gitlab/ci/status/pipeline/delayed_spec.rb b/spec/lib/gitlab/ci/status/pipeline/delayed_spec.rb index 1302c2069ff..072ea642e70 100644 --- a/spec/lib/gitlab/ci/status/pipeline/delayed_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/delayed_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::Ci::Status::Pipeline::Delayed do describe '#text' do it 'overrides status text' do - expect(subject.text).to eq 'delayed' + expect(subject.text).to eq 'Delayed' end end diff --git a/spec/lib/gitlab/ci/status/preparing_spec.rb b/spec/lib/gitlab/ci/status/preparing_spec.rb index ec1850c1959..f9033bce5f2 100644 --- a/spec/lib/gitlab/ci/status/preparing_spec.rb +++ b/spec/lib/gitlab/ci/status/preparing_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Status::Preparing do end describe '#text' do - it { expect(subject.text).to eq 'preparing' } + it { expect(subject.text).to eq 'Preparing' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb index e40d696ee4d..aefc7e90e85 100644 --- a/spec/lib/gitlab/ci/status/running_spec.rb +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Status::Running do end describe '#text' do - it { expect(subject.text).to eq 'running' } + it { expect(subject.text).to eq 'Running' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/scheduled_spec.rb b/spec/lib/gitlab/ci/status/scheduled_spec.rb index df72455d3c1..1a8e48052ec 100644 --- a/spec/lib/gitlab/ci/status/scheduled_spec.rb +++ b/spec/lib/gitlab/ci/status/scheduled_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Status::Scheduled, feature_category: :continuous_inte end describe '#text' do - it { expect(subject.text).to eq 'scheduled' } + it { expect(subject.text).to eq 'Scheduled' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb index ac3c2f253f7..da674df2090 100644 --- a/spec/lib/gitlab/ci/status/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Status::Skipped do end describe '#text' do - it { expect(subject.text).to eq 'skipped' } + it { expect(subject.text).to eq 'Skipped' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb index f2069334abd..c6567684ac0 100644 --- a/spec/lib/gitlab/ci/status/success_spec.rb +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Status::Success do end describe '#text' do - it { expect(subject.text).to eq 'passed' } + it { expect(subject.text).to eq 'Passed' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb index 1725f90a0cf..4a669da358e 100644 --- a/spec/lib/gitlab/ci/status/success_warning_spec.rb +++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb @@ -9,8 +9,8 @@ RSpec.describe Gitlab::Ci::Status::SuccessWarning, feature_category: :continuous described_class.new(status) end - describe '#test' do - it { expect(subject.text).to eq 'warning' } + describe '#text' do + it { expect(subject.text).to eq 'Warning' } end describe '#label' do @@ -25,6 +25,10 @@ RSpec.describe Gitlab::Ci::Status::SuccessWarning, feature_category: :continuous it { expect(subject.group).to eq 'success-with-warnings' } end + describe '#name' do + it { expect(subject.name).to eq 'SUCCESS_WITH_WARNINGS' } + end + describe '.matches?' do let(:matchable) { double('matchable') } diff --git a/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb index 6f5ab77a358..bd9663fb80f 100644 --- a/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb +++ b/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Status::WaitingForResource do end describe '#text' do - it { expect(subject.text).to eq 'waiting' } + it { expect(subject.text).to eq 'Waiting' } end describe '#label' do @@ -27,6 +27,10 @@ RSpec.describe Gitlab::Ci::Status::WaitingForResource do it { expect(subject.group).to eq 'waiting-for-resource' } end + describe '#name' do + it { expect(subject.name).to eq 'WAITING_FOR_RESOURCE' } + end + describe '#details_path' do it { expect(subject.details_path).to be_nil } end diff --git a/spec/lib/gitlab/ci/variables/builder/group_spec.rb b/spec/lib/gitlab/ci/variables/builder/group_spec.rb index c3743ebd2d7..004e63f424f 100644 --- a/spec/lib/gitlab/ci/variables/builder/group_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder/group_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Variables::Builder::Group do +RSpec.describe Gitlab::Ci::Variables::Builder::Group, feature_category: :secrets_management do let_it_be(:group) { create(:group) } let(:builder) { described_class.new(group) } @@ -185,21 +185,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Group do end end - context 'recursive' do - before do - stub_feature_flags(use_traversal_ids: false) - end - - include_examples 'correct ancestor order' - end - - context 'linear' do - before do - stub_feature_flags(use_traversal_ids: true) - end - - include_examples 'correct ancestor order' - end + include_examples 'correct ancestor order' end end end diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb index f7c6f7f51df..d96c8f1bd0c 100644 --- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Variables::Collection::Item do +RSpec.describe Gitlab::Ci::Variables::Collection::Item, feature_category: :secrets_management do let(:variable_key) { 'VAR' } let(:variable_value) { 'something' } let(:expected_value) { variable_value } @@ -217,6 +217,25 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do end end + describe '#masked?' do + let(:variable_hash) { { key: variable_key, value: variable_value } } + let(:item) { described_class.new(**variable_hash) } + + context 'when :masked is not specified' do + it 'returns false' do + expect(item.masked?).to eq(false) + end + end + + context 'when :masked is specified as true' do + let(:variable_hash) { { key: variable_key, value: variable_value, masked: true } } + + it 'returns true' do + expect(item.masked?).to eq(true) + end + end + end + describe '#to_runner_variable' do context 'when variable is not a file-related' do it 'returns a runner-compatible hash representation' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 5cfd8d9b9fb..81bc8c7ab9a 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -794,28 +794,6 @@ module Gitlab it_behaves_like 'returns errors', 'test_job_1 has the following needs duplicated: test_job_2.' end - - context 'when needed job name is too long' do - let(:job_name) { 'a' * (::Ci::BuildNeed::MAX_JOB_NAME_LENGTH + 1) } - - let(:config) do - <<-EOYML - lint_job: - script: 'echo lint_job' - rules: - - if: $var == null - needs: [#{job_name}] - #{job_name}: - script: 'echo job' - EOYML - end - - it 'returns an error' do - expect(subject.errors).to include( - "lint_job job: need `#{job_name}` name is too long (maximum is #{::Ci::BuildNeed::MAX_JOB_NAME_LENGTH} characters)" - ) - end - end end context 'rule needs as hash' do @@ -3659,7 +3637,8 @@ module Gitlab context 'when a project ref does not contain the forked commit sha' do it 'returns an error' do is_expected.not_to be_valid - expect(subject.errors).to include(/Could not validate configuration/) + expect(subject.errors).to include( + /configuration originates from an external project or a commit not associated with a Git reference/) end it_behaves_like 'when the processor is executed twice consecutively' diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb index 3682a654181..9e2f3bda14c 100644 --- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb +++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb @@ -577,17 +577,6 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :s end end - context 'when browsersdk_tracking is disabled' do - before do - stub_feature_flags(browsersdk_tracking: false) - stub_env('GITLAB_ANALYTICS_URL', analytics_url) - end - - it 'does not add GITLAB_ANALYTICS_URL to connect-src' do - expect(connect_src).not_to include(analytics_url) - end - end - context 'when GITLAB_ANALYTICS_URL is not set' do before do stub_env('GITLAB_ANALYTICS_URL', nil) diff --git a/spec/lib/gitlab/database/click_house_client_spec.rb b/spec/lib/gitlab/database/click_house_client_spec.rb index 6e63ae56557..271500ed3f6 100644 --- a/spec/lib/gitlab/database/click_house_client_spec.rb +++ b/spec/lib/gitlab/database/click_house_client_spec.rb @@ -112,6 +112,28 @@ RSpec.describe 'ClickHouse::Client', :click_house, feature_category: :database d results = ClickHouse::Client.select(select_query, :main) expect(results).to be_empty + + # Async, lazy deletion + # Set the `deleted` field to 1 and update the `updated_at` timestamp. + # Based on the highest version of the given row (updated_at), CH will eventually remove the row. + # See: https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replacingmergetree#is_deleted + soft_delete_query = ClickHouse::Client::Query.new( + raw_query: %{ + INSERT INTO events (id, deleted, updated_at) + VALUES ({id:UInt64}, 1, {updated_at:DateTime64(6, 'UTC')}) + }, + placeholders: { id: event2.id, updated_at: (event2.updated_at + 2.hours).utc.to_f } + ) + + ClickHouse::Client.execute(soft_delete_query, :main) + + select_query = ClickHouse::Client::Query.new( + raw_query: 'SELECT * FROM events FINAL WHERE id = {id:UInt64}', + placeholders: { id: event2.id } + ) + + results = ClickHouse::Client.select(select_query, :main) + expect(results).to be_empty end end end diff --git a/spec/lib/gitlab/database/gitlab_schema_spec.rb b/spec/lib/gitlab/database/gitlab_schema_spec.rb index e402014df90..a6de695c345 100644 --- a/spec/lib/gitlab/database/gitlab_schema_spec.rb +++ b/spec/lib/gitlab/database/gitlab_schema_spec.rb @@ -226,57 +226,83 @@ RSpec.describe Gitlab::Database::GitlabSchema, feature_category: :database do allow_cross_joins: %i[gitlab_shared], allow_cross_transactions: %i[gitlab_internal gitlab_shared], allow_cross_foreign_keys: %i[] + ), + Gitlab::Database::GitlabSchemaInfo.new( + name: "gitlab_main_cell", + allow_cross_joins: [ + :gitlab_shared, + :gitlab_main, + { gitlab_main_clusterwide: { specific_tables: %w[plans] } } + ], + allow_cross_transactions: [ + :gitlab_internal, + :gitlab_shared, + :gitlab_main, + { gitlab_main_clusterwide: { specific_tables: %w[plans] } } + ], + allow_cross_foreign_keys: [ + { gitlab_main_clusterwide: { specific_tables: %w[plans] } } + ] ) ].index_by(&:name) ) end describe '.cross_joins_allowed?' do - where(:schemas, :result) do - %i[] | true - %i[gitlab_main_clusterwide gitlab_main] | true - %i[gitlab_main_clusterwide gitlab_ci] | false - %i[gitlab_main_clusterwide gitlab_main gitlab_ci] | false - %i[gitlab_main_clusterwide gitlab_internal] | false - %i[gitlab_main gitlab_ci] | false - %i[gitlab_main_clusterwide gitlab_main gitlab_shared] | true - %i[gitlab_main_clusterwide gitlab_shared] | true + where(:schemas, :tables, :result) do + %i[] | %i[] | true + %i[gitlab_main] | %i[] | true + %i[gitlab_main_clusterwide gitlab_main] | %i[] | true + %i[gitlab_main_clusterwide gitlab_ci] | %i[] | false + %i[gitlab_main_clusterwide gitlab_main gitlab_ci] | %i[] | false + %i[gitlab_main_clusterwide gitlab_internal] | %i[] | false + %i[gitlab_main gitlab_ci] | %i[] | false + %i[gitlab_main_clusterwide gitlab_main gitlab_shared] | %i[] | true + %i[gitlab_main_clusterwide gitlab_shared] | %i[] | true + %i[gitlab_main_clusterwide gitlab_main_cell] | %w[users namespaces] | false + %i[gitlab_main_clusterwide gitlab_main_cell] | %w[plans namespaces] | true end with_them do - it { expect(described_class.cross_joins_allowed?(schemas)).to eq(result) } + it { expect(described_class.cross_joins_allowed?(schemas, tables)).to eq(result) } end end describe '.cross_transactions_allowed?' do - where(:schemas, :result) do - %i[] | true - %i[gitlab_main_clusterwide gitlab_main] | true - %i[gitlab_main_clusterwide gitlab_ci] | false - %i[gitlab_main_clusterwide gitlab_main gitlab_ci] | false - %i[gitlab_main_clusterwide gitlab_internal] | true - %i[gitlab_main gitlab_ci] | false - %i[gitlab_main_clusterwide gitlab_main gitlab_shared] | true - %i[gitlab_main_clusterwide gitlab_shared] | true + where(:schemas, :tables, :result) do + %i[] | %i[] | true + %i[gitlab_main] | %i[] | true + %i[gitlab_main_clusterwide gitlab_main] | %i[] | true + %i[gitlab_main_clusterwide gitlab_ci] | %i[] | false + %i[gitlab_main_clusterwide gitlab_main gitlab_ci] | %i[] | false + %i[gitlab_main_clusterwide gitlab_internal] | %i[] | true + %i[gitlab_main gitlab_ci] | %i[] | false + %i[gitlab_main_clusterwide gitlab_main gitlab_shared] | %i[] | true + %i[gitlab_main_clusterwide gitlab_shared] | %i[] | true + %i[gitlab_main_clusterwide gitlab_main_cell] | %w[users namespaces] | false + %i[gitlab_main_clusterwide gitlab_main_cell] | %w[plans namespaces] | true end with_them do - it { expect(described_class.cross_transactions_allowed?(schemas)).to eq(result) } + it { expect(described_class.cross_transactions_allowed?(schemas, tables)).to eq(result) } end end describe '.cross_foreign_key_allowed?' do - where(:schemas, :result) do - %i[] | false - %i[gitlab_main_clusterwide gitlab_main] | true - %i[gitlab_main_clusterwide gitlab_ci] | false - %i[gitlab_main_clusterwide gitlab_internal] | false - %i[gitlab_main gitlab_ci] | false - %i[gitlab_main_clusterwide gitlab_shared] | false + where(:schemas, :tables, :result) do + %i[] | %i[] | false + %i[gitlab_main] | %i[] | true + %i[gitlab_main_clusterwide gitlab_main] | %i[] | true + %i[gitlab_main_clusterwide gitlab_ci] | %i[] | false + %i[gitlab_main_clusterwide gitlab_internal] | %i[] | false + %i[gitlab_main gitlab_ci] | %i[] | false + %i[gitlab_main_clusterwide gitlab_shared] | %i[] | false + %i[gitlab_main_clusterwide gitlab_main_cell] | %w[users namespaces] | false + %i[gitlab_main_clusterwide gitlab_main_cell] | %w[plans namespaces] | true end with_them do - it { expect(described_class.cross_foreign_key_allowed?(schemas)).to eq(result) } + it { expect(described_class.cross_foreign_key_allowed?(schemas, tables)).to eq(result) } end end end diff --git a/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb b/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb index 7197b99fe33..442fa678d4e 100644 --- a/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb @@ -194,7 +194,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery, feature_catego describe '#replace_hosts' do before do - stub_env('LOAD_BALANCER_PARALLEL_DISCONNECT', 'true') allow(service) .to receive(:load_balancer) .and_return(load_balancer) @@ -257,26 +256,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery, feature_catego service.replace_hosts([address_foo, address_bar]) end end - - context 'when LOAD_BALANCER_PARALLEL_DISCONNECT is false' do - before do - stub_env('LOAD_BALANCER_PARALLEL_DISCONNECT', 'false') - end - - it 'disconnects them sequentially' do - host = load_balancer.host_list.hosts.first - - allow(service) - .to receive(:disconnect_timeout) - .and_return(2) - - expect(host) - .to receive(:disconnect!) - .with(timeout: 2) - - service.replace_hosts([address_bar]) - end - end end describe '#addresses_from_dns' do diff --git a/spec/lib/gitlab/database/migration_helpers/swapping_spec.rb b/spec/lib/gitlab/database/migration_helpers/swapping_spec.rb new file mode 100644 index 00000000000..0940c6f4c30 --- /dev/null +++ b/spec/lib/gitlab/database/migration_helpers/swapping_spec.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::MigrationHelpers::Swapping, feature_category: :database do + let(:connection) { ApplicationRecord.connection } + let(:migration_context) do + ActiveRecord::Migration + .new + .extend(described_class) + .extend(Gitlab::Database::MigrationHelpers) + end + + let(:service_instance) { instance_double('Gitlab::Database::Migrations::SwapColumns', execute: nil) } + + describe '#reset_trigger_function' do + let(:trigger_function_name) { 'existing_trigger_function' } + + before do + connection.execute(<<~SQL) + CREATE FUNCTION #{trigger_function_name}() RETURNS trigger + LANGUAGE plpgsql + AS $$ + BEGIN + NEW."bigint_column" := NEW."integer_column"; + RETURN NEW; + END; + $$; + SQL + end + + it 'resets' do + recorder = ActiveRecord::QueryRecorder.new do + migration_context.reset_trigger_function(trigger_function_name) + end + expect(recorder.log).to include(/ALTER FUNCTION "existing_trigger_function" RESET ALL/) + end + end + + describe '#swap_columns' do + let(:table) { :ci_pipeline_variables } + let(:column1) { :pipeline_id } + let(:column2) { :pipeline_id_convert_to_bigint } + + it 'calls service' do + expect(::Gitlab::Database::Migrations::SwapColumns).to receive(:new).with( + migration_context: migration_context, + table: table, + column1: column1, + column2: column2 + ).and_return(service_instance) + + migration_context.swap_columns(table, column1, column2) + end + end + + describe '#swap_columns_default' do + let(:table) { :_test_table } + let(:column1) { :pipeline_id } + let(:column2) { :pipeline_id_convert_to_bigint } + + it 'calls service' do + expect(::Gitlab::Database::Migrations::SwapColumnsDefault).to receive(:new).with( + migration_context: migration_context, + table: table, + column1: column1, + column2: column2 + ).and_return(service_instance) + + migration_context.swap_columns_default(table, column1, column2) + end + end + + describe '#swap_foreign_keys' do + let(:table) { :_test_swap_foreign_keys } + let(:referenced_table) { "#{table}_referenced" } + let(:foreign_key1) { :fkey_on_integer_column } + let(:foreign_key2) { :fkey_on_bigint_column } + + before do + connection.execute(<<~SQL) + CREATE TABLE #{table} ( + integer_column integer NOT NULL, + bigint_column bigint DEFAULT 0 NOT NULL + ); + CREATE TABLE #{referenced_table} ( + id bigint NOT NULL + ); + + ALTER TABLE ONLY #{referenced_table} + ADD CONSTRAINT pk PRIMARY KEY (id); + + ALTER TABLE ONLY #{table} + ADD CONSTRAINT #{foreign_key1} + FOREIGN KEY (integer_column) REFERENCES #{referenced_table}(id) ON DELETE SET NULL; + + ALTER TABLE ONLY #{table} + ADD CONSTRAINT #{foreign_key2} + FOREIGN KEY (bigint_column) REFERENCES #{referenced_table}(id) ON DELETE SET NULL; + SQL + end + + shared_examples_for 'swapping foreign keys correctly' do + specify do + expect { migration_context.swap_foreign_keys(table, foreign_key1, foreign_key2) } + .to change { + find_foreign_key_by(foreign_key1).options[:column] + }.from('integer_column').to('bigint_column') + .and change { + find_foreign_key_by(foreign_key2).options[:column] + }.from('bigint_column').to('integer_column') + end + end + + it_behaves_like 'swapping foreign keys correctly' + + context 'when foreign key names are 63 bytes' do + let(:foreign_key1) { :f1_012345678901234567890123456789012345678901234567890123456789 } + let(:foreign_key2) { :f2_012345678901234567890123456789012345678901234567890123456789 } + + it_behaves_like 'swapping foreign keys correctly' + end + + private + + def find_foreign_key_by(name) + connection.foreign_keys(table).find { |k| k.options[:name].to_s == name.to_s } + end + end + + describe '#swap_indexes' do + let(:table) { :_test_swap_indexes } + let(:index1) { :index_on_integer } + let(:index2) { :index_on_bigint } + + before do + connection.execute(<<~SQL) + CREATE TABLE #{table} ( + integer_column integer NOT NULL, + bigint_column bigint DEFAULT 0 NOT NULL + ); + + CREATE INDEX #{index1} ON #{table} USING btree (integer_column); + + CREATE INDEX #{index2} ON #{table} USING btree (bigint_column); + SQL + end + + shared_examples_for 'swapping indexes correctly' do + specify do + expect { migration_context.swap_indexes(table, index1, index2) } + .to change { find_index_by(index1).columns }.from(['integer_column']).to(['bigint_column']) + .and change { find_index_by(index2).columns }.from(['bigint_column']).to(['integer_column']) + end + end + + it_behaves_like 'swapping indexes correctly' + + context 'when index names are 63 bytes' do + let(:index1) { :i1_012345678901234567890123456789012345678901234567890123456789 } + let(:index2) { :i2_012345678901234567890123456789012345678901234567890123456789 } + + it_behaves_like 'swapping indexes correctly' + end + + private + + def find_index_by(name) + connection.indexes(table).find { |c| c.name == name.to_s } + end + end +end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index f3c181db3aa..dd51cca688c 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -1774,6 +1774,35 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d end describe '#copy_indexes' do + context 'when index name is too long' do + it 'does not fail' do + index = double(:index, + columns: %w(uuid), + name: 'index_vuln_findings_on_uuid_including_vuln_id_1', + using: nil, + where: nil, + opclasses: {}, + unique: true, + lengths: [], + orders: []) + + allow(model).to receive(:indexes_for).with(:vulnerability_occurrences, 'uuid') + .and_return([index]) + + expect(model).to receive(:add_concurrent_index) + .with(:vulnerability_occurrences, + %w(tmp_undo_cleanup_column_8cbf300838), + { + unique: true, + name: 'idx_copy_191a1af1a0', + length: [], + order: [] + }) + + model.copy_indexes(:vulnerability_occurrences, :uuid, :tmp_undo_cleanup_column_8cbf300838) + end + end + context 'using a regular index using a single column' do it 'copies the index' do index = double(:index, @@ -2326,6 +2355,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d end describe '#revert_initialize_conversion_of_integer_to_bigint' do + let(:setup_table) { true } let(:table) { :_test_table } before do @@ -2334,7 +2364,18 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d t.integer :other_id end - model.initialize_conversion_of_integer_to_bigint(table, columns) + model.initialize_conversion_of_integer_to_bigint(table, columns) if setup_table + end + + context 'when column and trigger do not exist' do + let(:setup_table) { false } + let(:columns) { :id } + + it 'does not raise an error' do + expect do + model.revert_initialize_conversion_of_integer_to_bigint(table, columns) + end.not_to raise_error + end end context 'when single column is given' do @@ -2906,4 +2947,20 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d it { expect(recorder.log).to be_empty } end end + + describe '#lock_tables' do + let(:lock_statement) do + /LOCK TABLE ci_builds, ci_pipelines IN ACCESS EXCLUSIVE MODE/ + end + + subject(:recorder) do + ActiveRecord::QueryRecorder.new do + model.lock_tables(:ci_builds, :ci_pipelines) + end + end + + it 'locks the tables' do + expect(recorder.log).to include(lock_statement) + end + end end diff --git a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb index 158497b1fef..f1271f2434c 100644 --- a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers do +RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers, feature_category: :database do let(:migration_class) do Class.new(ActiveRecord::Migration[6.1]) .include(described_class) @@ -70,39 +70,54 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d end end - it 'creates the database record for the migration' do - expect(Gitlab::Database::PgClass).to receive(:for_table).with(:projects).and_return(pgclass_info) + context "when the migration doesn't exist already" do + before do + allow(Gitlab::Database::PgClass).to receive(:for_table).with(:projects).and_return(pgclass_info) + end - expect do + subject(:enqueue_batched_background_migration) do migration.queue_batched_background_migration( job_class.name, :projects, :id, job_interval: 5.minutes, + queued_migration_version: format("%.14d", 123), batch_min_value: 5, batch_max_value: 1000, batch_class_name: 'MyBatchClass', batch_size: 100, max_batch_size: 10000, sub_batch_size: 10, - gitlab_schema: :gitlab_ci) - end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) - - expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to have_attributes( - job_class_name: 'MyJobClass', - table_name: 'projects', - column_name: 'id', - interval: 300, - min_value: 5, - max_value: 1000, - batch_class_name: 'MyBatchClass', - batch_size: 100, - max_batch_size: 10000, - sub_batch_size: 10, - job_arguments: %w[], - status_name: :active, - total_tuple_count: pgclass_info.cardinality_estimate, - gitlab_schema: 'gitlab_ci') + gitlab_schema: :gitlab_ci + ) + end + + it 'enqueues exactly one batched migration' do + expect { enqueue_batched_background_migration } + .to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) + end + + it 'creates the database record for the migration' do + batched_background_migration = enqueue_batched_background_migration + + expect(batched_background_migration.reload).to have_attributes( + job_class_name: 'MyJobClass', + table_name: 'projects', + column_name: 'id', + interval: 300, + min_value: 5, + max_value: 1000, + batch_class_name: 'MyBatchClass', + batch_size: 100, + max_batch_size: 10000, + sub_batch_size: 10, + job_arguments: %w[], + status_name: :active, + total_tuple_count: pgclass_info.cardinality_estimate, + gitlab_schema: 'gitlab_ci', + queued_migration_version: format("%.14d", 123) + ) + end end context 'when the job interval is lower than the minimum' do diff --git a/spec/lib/gitlab/database/migrations/milestone_mixin_spec.rb b/spec/lib/gitlab/database/migrations/milestone_mixin_spec.rb new file mode 100644 index 00000000000..e375af494a2 --- /dev/null +++ b/spec/lib/gitlab/database/migrations/milestone_mixin_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Migrations::MilestoneMixin, feature_category: :database do + let(:migration_no_mixin) do + Class.new(Gitlab::Database::Migration[2.1]) do + def change + # no-op here to make rubocop happy + end + end + end + + let(:migration_mixin) do + Class.new(Gitlab::Database::Migration[2.1]) do + include Gitlab::Database::Migrations::MilestoneMixin + end + end + + let(:migration_mixin_version) do + Class.new(Gitlab::Database::Migration[2.1]) do + include Gitlab::Database::Migrations::MilestoneMixin + milestone '16.4' + end + end + + context 'when the mixin is not included' do + it 'does not raise an error' do + expect { migration_no_mixin.new(4, 4) }.not_to raise_error + end + end + + context 'when the mixin is included' do + context 'when a milestone is not specified' do + it "raises MilestoneNotSetError" do + expect { migration_mixin.new(4, 4, :regular) }.to raise_error( + "#{described_class}::MilestoneNotSetError".constantize + ) + end + end + + context 'when a milestone is specified' do + it "does not raise an error" do + expect { migration_mixin_version.new(4, 4, :regular) }.not_to raise_error + end + end + end +end diff --git a/spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb b/spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb index 66de25d65bb..330c9d18fb2 100644 --- a/spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb +++ b/spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb @@ -41,7 +41,13 @@ RSpec.describe Gitlab::Database::Migrations::Observers::QueryStatistics do let(:result) { double } let(:pgss_query) do <<~SQL - SELECT query, calls, total_time, max_time, mean_time, rows + SELECT + query, + calls, + total_exec_time + total_plan_time AS total_time, + max_exec_time + max_plan_time AS max_time, + mean_exec_time + mean_plan_time AS mean_time, + "rows" FROM pg_stat_statements WHERE pg_get_userbyid(userid) = current_user ORDER BY total_time DESC diff --git a/spec/lib/gitlab/database/migrations/swap_columns_default_spec.rb b/spec/lib/gitlab/database/migrations/swap_columns_default_spec.rb new file mode 100644 index 00000000000..e53480d453e --- /dev/null +++ b/spec/lib/gitlab/database/migrations/swap_columns_default_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Migrations::SwapColumnsDefault, feature_category: :database do + describe '#execute' do + let(:connection) { ApplicationRecord.connection } + let(:migration_context) do + Gitlab::Database::Migration[2.1] + .new('name', 'version') + .extend(Gitlab::Database::MigrationHelpers::Swapping) + end + + let(:table) { :_test_swap_columns_and_defaults } + let(:column1) { :integer_column } + let(:column2) { :bigint_column } + + subject(:execute_service) do + described_class.new( + migration_context: migration_context, + table: table, + column1: column1, + column2: column2 + ).execute + end + + before do + connection.execute(sql) + end + + context 'when defaults are static values' do + let(:sql) do + <<~SQL + CREATE TABLE #{table} ( + id integer NOT NULL, + #{column1} integer DEFAULT 8 NOT NULL, + #{column2} bigint DEFAULT 100 NOT NULL + ); + SQL + end + + it 'swaps the default correctly' do + expect { execute_service } + .to change { find_column_by(column1).default }.to('100') + .and change { find_column_by(column2).default }.to('8') + .and not_change { find_column_by(column1).default_function }.from(nil) + .and not_change { find_column_by(column2).default_function }.from(nil) + end + end + + context 'when default is sequence' do + let(:sql) do + <<~SQL + CREATE TABLE #{table} ( + id integer NOT NULL, + #{column1} integer NOT NULL, + #{column2} bigint DEFAULT 100 NOT NULL + ); + + CREATE SEQUENCE #{table}_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + ALTER SEQUENCE #{table}_seq OWNED BY #{table}.#{column1}; + ALTER TABLE ONLY #{table} ALTER COLUMN #{column1} SET DEFAULT nextval('#{table}_seq'::regclass); + SQL + end + + it 'swaps the default correctly' do + recorder = nil + expect { recorder = ActiveRecord::QueryRecorder.new { execute_service } } + .to change { find_column_by(column1).default }.to('100') + .and change { find_column_by(column1).default_function }.to(nil) + .and change { find_column_by(column2).default }.to(nil) + .and change { + find_column_by(column2).default_function + }.to("nextval('_test_swap_columns_and_defaults_seq'::regclass)") + expect(recorder.log).to include( + /SEQUENCE "_test_swap_columns_and_defaults_seq" OWNED BY "_test_swap_columns_and_defaults"."bigint_column"/ + ) + expect(recorder.log).to include( + /COLUMN "bigint_column" SET DEFAULT nextval\('_test_swap_columns_and_defaults_seq'::regclass\)/ + ) + end + end + + context 'when defaults are the same' do + let(:sql) do + <<~SQL + CREATE TABLE #{table} ( + id integer NOT NULL, + #{column1} integer DEFAULT 100 NOT NULL, + #{column2} bigint DEFAULT 100 NOT NULL + ); + SQL + end + + it 'does nothing' do + recorder = nil + expect { recorder = ActiveRecord::QueryRecorder.new { execute_service } } + .to not_change { find_column_by(column1).default } + .and not_change { find_column_by(column1).default_function } + .and not_change { find_column_by(column2).default } + .and not_change { find_column_by(column2).default_function } + expect(recorder.log).not_to include(/ALTER TABLE/) + end + end + + private + + def find_column_by(name) + connection.columns(table).find { |c| c.name == name.to_s } + end + end +end diff --git a/spec/lib/gitlab/database/migrations/swap_columns_spec.rb b/spec/lib/gitlab/database/migrations/swap_columns_spec.rb new file mode 100644 index 00000000000..a119b23dda4 --- /dev/null +++ b/spec/lib/gitlab/database/migrations/swap_columns_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Migrations::SwapColumns, feature_category: :database do + describe '#execute' do + let(:connection) { ApplicationRecord.connection } + let(:sql) do + <<~SQL + CREATE TABLE #{table} ( + id integer NOT NULL, + #{column1} integer DEFAULT 8 NOT NULL, + #{column2} bigint DEFAULT 100 NOT NULL + ); + SQL + end + + let(:migration_context) do + Gitlab::Database::Migration[2.1] + .new('name', 'version') + .extend(Gitlab::Database::MigrationHelpers::Swapping) + end + + let(:table) { :_test_swap_columns_and_defaults } + let(:column1) { :integer_column } + let(:column2) { :bigint_column } + + subject(:execute_service) do + described_class.new( + migration_context: migration_context, + table: table, + column1: column1, + column2: column2 + ).execute + end + + before do + connection.execute(sql) + end + + shared_examples_for 'swapping columns correctly' do + specify do + expect { execute_service } + .to change { find_column_by(column1).sql_type }.from('integer').to('bigint') + .and change { find_column_by(column2).sql_type }.from('bigint').to('integer') + end + end + + it_behaves_like 'swapping columns correctly' + + context 'when column names are 63 bytes' do + let(:column1) { :int012345678901234567890123456789012345678901234567890123456789 } + let(:column2) { :big012345678901234567890123456789012345678901234567890123456789 } + + it_behaves_like 'swapping columns correctly' + end + + private + + def find_column_by(name) + connection.columns(table).find { |c| c.name == name.to_s } + end + end +end diff --git a/spec/lib/gitlab/database/migrations/version_spec.rb b/spec/lib/gitlab/database/migrations/version_spec.rb new file mode 100644 index 00000000000..821a2156539 --- /dev/null +++ b/spec/lib/gitlab/database/migrations/version_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Migrations::Version, feature_category: :database do + let(:test_versions) do + [ + 4, + 5, + described_class.new(6, Gitlab::VersionInfo.parse_from_milestone('10.3'), :regular), + 7, + described_class.new(8, Gitlab::VersionInfo.parse_from_milestone('10.3'), :regular), + described_class.new(9, Gitlab::VersionInfo.parse_from_milestone('10.4'), :regular), + described_class.new(10, Gitlab::VersionInfo.parse_from_milestone('10.3'), :post), + described_class.new(11, Gitlab::VersionInfo.parse_from_milestone('10.3'), :regular) + ] + end + + describe "#<=>" do + it 'sorts by existence of milestone, then by milestone, then by type, then by timestamp when sorted by version' do + expect(test_versions.sort.map(&:to_i)).to eq [4, 5, 7, 6, 8, 11, 10, 9] + end + end + + describe 'initialize' do + context 'when the type is :post or :regular' do + it 'does not raise an error' do + expect { described_class.new(4, 4, :regular) }.not_to raise_error + expect { described_class.new(4, 4, :post) }.not_to raise_error + end + end + + context 'when the type is anything else' do + it 'does not raise an error' do + expect { described_class.new(4, 4, 'foo') }.to raise_error("#{described_class}::InvalidTypeError".constantize) + end + end + end + + describe 'eql?' do + where(:version1, :version2, :expected_equality) do + [ + [ + described_class.new(4, Gitlab::VersionInfo.parse_from_milestone('10.3'), :regular), + described_class.new(4, Gitlab::VersionInfo.parse_from_milestone('10.3'), :regular), + true + ], + [ + described_class.new(4, Gitlab::VersionInfo.parse_from_milestone('10.3'), :regular), + described_class.new(4, Gitlab::VersionInfo.parse_from_milestone('10.4'), :regular), + false + ], + [ + described_class.new(4, Gitlab::VersionInfo.parse_from_milestone('10.3'), :regular), + described_class.new(4, Gitlab::VersionInfo.parse_from_milestone('10.3'), :post), + false + ], + [ + described_class.new(4, Gitlab::VersionInfo.parse_from_milestone('10.3'), :regular), + described_class.new(5, Gitlab::VersionInfo.parse_from_milestone('10.3'), :regular), + false + ] + ] + end + + with_them do + it 'correctly evaluates deep equality' do + expect(version1.eql?(version2)).to eq(expected_equality) + end + + it 'correctly evaluates deep equality using ==' do + expect(version1 == version2).to eq(expected_equality) + end + end + end + + describe 'type' do + subject { described_class.new(4, Gitlab::VersionInfo.parse_from_milestone('10.3'), migration_type) } + + context 'when the migration is regular' do + let(:migration_type) { :regular } + + it 'correctly identifies the migration type' do + expect(subject.type).to eq(:regular) + expect(subject.regular?).to eq(true) + expect(subject.post_deployment?).to eq(false) + end + end + + context 'when the migration is post_deployment' do + let(:migration_type) { :post } + + it 'correctly identifies the migration type' do + expect(subject.type).to eq(:post) + expect(subject.regular?).to eq(false) + expect(subject.post_deployment?).to eq(true) + end + end + end + + describe 'to_s' do + subject { described_class.new(4, Gitlab::VersionInfo.parse_from_milestone('10.3'), :regular) } + + it 'returns the given timestamp value as a string' do + expect(subject.to_s).to eql('4') + end + end + + describe 'hash' do + subject { described_class.new(4, Gitlab::VersionInfo.parse_from_milestone('10.3'), :regular) } + + let(:expected_hash) { subject.hash } + + it 'deterministically returns a hash of the timestamp, milestone, and type value' do + 3.times do + expect(subject.hash).to eq(expected_hash) + end + end + end +end diff --git a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb index 2fa4c9e562f..c6cd5e55754 100644 --- a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb +++ b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb @@ -23,8 +23,6 @@ RSpec.describe 'cross-database foreign keys' do 'merge_requests.merge_user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080 'merge_requests.author_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080 'project_authorizations.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422044 - 'projects.creator_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/421844 - 'projects.marked_for_deletion_by_user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/421844 'user_group_callouts.user_id' # https://gitlab.com/gitlab-org/gitlab/-/issues/421287 ] end @@ -34,9 +32,11 @@ RSpec.describe 'cross-database foreign keys' do end def is_cross_db?(fk_record) - table_schemas = Gitlab::Database::GitlabSchema.table_schemas!([fk_record.from_table, fk_record.to_table]) + tables = [fk_record.from_table, fk_record.to_table] - !Gitlab::Database::GitlabSchema.cross_foreign_key_allowed?(table_schemas) + table_schemas = Gitlab::Database::GitlabSchema.table_schemas!(tables) + + !Gitlab::Database::GitlabSchema.cross_foreign_key_allowed?(table_schemas, tables) end it 'onlies have allowed list of cross-database foreign keys', :aggregate_failures do diff --git a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb index c41228777ca..80ffa708d8a 100644 --- a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb +++ b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb @@ -322,74 +322,33 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager, feature_categor allow(connection).to receive(:select_value).and_return(nil, Time.current, Time.current) end - context 'when feature flag database_analyze_on_partitioned_tables is enabled' do - before do - stub_feature_flags(database_analyze_on_partitioned_tables: true) - end - - it_behaves_like 'run only once analyze within interval' + it_behaves_like 'run only once analyze within interval' - context 'when analyze is false' do - let(:analyze) { false } + context 'when analyze is false' do + let(:analyze) { false } - it_behaves_like 'not to run the analyze at all' - end + it_behaves_like 'not to run the analyze at all' + end - context 'when model does not set analyze_interval' do - let(:my_model) do - Class.new(ApplicationRecord) do - include PartitionedTable + context 'when model does not set analyze_interval' do + let(:my_model) do + Class.new(ApplicationRecord) do + include PartitionedTable - partitioned_by :partition_id, - strategy: :ci_sliding_list, - next_partition_if: proc { false }, - detach_partition_if: proc { false } - end + partitioned_by :partition_id, + strategy: :ci_sliding_list, + next_partition_if: proc { false }, + detach_partition_if: proc { false } end - - it_behaves_like 'not to run the analyze at all' - end - - context 'when no partition is created' do - let(:create_partition) { false } - - it_behaves_like 'run only once analyze within interval' - end - end - - context 'when feature flag database_analyze_on_partitioned_tables is disabled' do - before do - stub_feature_flags(database_analyze_on_partitioned_tables: false) end it_behaves_like 'not to run the analyze at all' + end - context 'when analyze is false' do - let(:analyze) { false } - - it_behaves_like 'not to run the analyze at all' - end - - context 'when model does not set analyze_interval' do - let(:my_model) do - Class.new(ApplicationRecord) do - include PartitionedTable - - partitioned_by :partition_id, - strategy: :ci_sliding_list, - next_partition_if: proc { false }, - detach_partition_if: proc { false } - end - end - - it_behaves_like 'not to run the analyze at all' - end - - context 'when no partition is created' do - let(:create_partition) { false } + context 'when no partition is created' do + let(:create_partition) { false } - it_behaves_like 'not to run the analyze at all' - end + it_behaves_like 'run only once analyze within interval' end end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb deleted file mode 100644 index 370d03b495c..00000000000 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb +++ /dev/null @@ -1,292 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete, feature_category: :groups_and_projects do - let(:migration) { FakeRenameReservedPathMigrationV1.new } - let(:subject) { described_class.new(['the-path'], migration) } - - before do - allow(migration).to receive(:say) - TestEnv.clean_test_path - end - - def migration_namespace(namespace) - Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses:: - Namespace.find(namespace.id) - end - - def migration_project(project) - Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses:: - Project.find(project.id) - end - - describe "#remove_last_occurrence" do - it "removes only the last occurrence of a string" do - input = "this/is/a-word-to-replace/namespace/with/a-word-to-replace" - - expect(subject.remove_last_occurrence(input, "a-word-to-replace")) - .to eq("this/is/a-word-to-replace/namespace/with/") - end - end - - describe '#remove_cached_html_for_projects' do - let(:project) { create(:project, description_html: 'Project description') } - - it 'removes description_html from projects' do - subject.remove_cached_html_for_projects([project.id]) - - expect(project.reload.description_html).to be_nil - end - - it 'removes issue descriptions' do - issue = create(:issue, project: project, description_html: 'Issue description') - - subject.remove_cached_html_for_projects([project.id]) - - expect(issue.reload.description_html).to be_nil - end - - it 'removes merge request descriptions' do - merge_request = create(:merge_request, - source_project: project, - target_project: project, - description_html: 'MergeRequest description') - - subject.remove_cached_html_for_projects([project.id]) - - expect(merge_request.reload.description_html).to be_nil - end - - it 'removes note html' do - note = create(:note, - project: project, - noteable: create(:issue, project: project), - note_html: 'note description') - - subject.remove_cached_html_for_projects([project.id]) - - expect(note.reload.note_html).to be_nil - end - - it 'removes milestone description' do - milestone = create(:milestone, - project: project, - description_html: 'milestone description') - - subject.remove_cached_html_for_projects([project.id]) - - expect(milestone.reload.description_html).to be_nil - end - end - - describe '#rename_path_for_routable' do - context 'for personal namespaces' do - let(:namespace) { create(:namespace, path: 'the-path') } - - it "renames namespaces called the-path" do - subject.rename_path_for_routable(migration_namespace(namespace)) - - expect(namespace.reload.path).to eq("the-path0") - end - - it "renames the route to the namespace" do - subject.rename_path_for_routable(migration_namespace(namespace)) - - expect(Namespace.find(namespace.id).full_path).to eq("the-path0") - end - - it "renames the route for projects of the namespace" do - project = create(:project, :repository, path: "project-path", namespace: namespace) - - subject.rename_path_for_routable(migration_namespace(namespace)) - - expect(project.route.reload.path).to eq("the-path0/project-path") - end - - it 'returns the old & the new path' do - old_path, new_path = subject.rename_path_for_routable(migration_namespace(namespace)) - - expect(old_path).to eq('the-path') - expect(new_path).to eq('the-path0') - end - - it "doesn't rename routes that start with a similar name" do - other_namespace = create(:namespace, path: 'the-path-but-not-really') - project = create(:project, path: 'the-project', namespace: other_namespace) - - subject.rename_path_for_routable(migration_namespace(namespace)) - - expect(project.route.reload.path).to eq('the-path-but-not-really/the-project') - end - end - - context 'for groups' do - context "the-path group -> subgroup -> the-path0 project" do - it "updates the route of the project correctly" do - group = create(:group, path: 'the-path') - subgroup = create(:group, path: "subgroup", parent: group) - project = create(:project, :repository, path: "the-path0", namespace: subgroup) - - subject.rename_path_for_routable(migration_namespace(group)) - - expect(project.route.reload.path).to eq("the-path0/subgroup/the-path0") - end - end - end - - context 'for projects' do - let(:parent) { create(:namespace, path: 'the-parent') } - let(:project) { create(:project, path: 'the-path', namespace: parent) } - - it 'renames the project called `the-path`' do - subject.rename_path_for_routable(migration_project(project)) - - expect(project.reload.path).to eq('the-path0') - end - - it 'renames the route for the project' do - subject.rename_path_for_routable(project) - - expect(project.reload.route.path).to eq('the-parent/the-path0') - end - - it 'returns the old & new path' do - old_path, new_path = subject.rename_path_for_routable(migration_project(project)) - - expect(old_path).to eq('the-parent/the-path') - expect(new_path).to eq('the-parent/the-path0') - end - end - end - - describe '#perform_rename' do - context 'for personal namespaces' do - it 'renames the path' do - namespace = create(:namespace, path: 'the-path') - - subject.perform_rename(migration_namespace(namespace), 'the-path', 'renamed') - - expect(namespace.reload.path).to eq('renamed') - expect(namespace.reload.route.path).to eq('renamed') - end - end - - context 'for groups' do - it 'renames all the routes for the group' do - group = create(:group, path: 'the-path') - child = create(:group, path: 'child', parent: group) - project = create(:project, :repository, namespace: child, path: 'the-project') - other_one = create(:group, path: 'the-path-is-similar') - - subject.perform_rename(migration_namespace(group), 'the-path', 'renamed') - - expect(group.reload.route.path).to eq('renamed') - expect(child.reload.route.path).to eq('renamed/child') - expect(project.reload.route.path).to eq('renamed/child/the-project') - expect(other_one.reload.route.path).to eq('the-path-is-similar') - end - end - end - - describe '#move_pages' do - it 'moves the pages directory' do - expect(subject).to receive(:move_folders) - .with(TestEnv.pages_path, 'old-path', 'new-path') - - subject.move_pages('old-path', 'new-path') - end - end - - describe "#move_uploads" do - let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') } - let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') } - - it 'moves subdirectories in the uploads folder' do - expect(subject).to receive(:uploads_dir).and_return(uploads_dir) - expect(subject).to receive(:move_folders).with(uploads_dir, 'old_path', 'new_path') - - subject.move_uploads('old_path', 'new_path') - end - - it "doesn't move uploads when they are stored in object storage" do - expect(subject).to receive(:file_storage?).and_return(false) - expect(subject).not_to receive(:move_folders) - - subject.move_uploads('old_path', 'new_path') - end - end - - describe '#move_folders' do - let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') } - let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') } - - before do - FileUtils.remove_dir(test_dir) if File.directory?(test_dir) - FileUtils.mkdir_p(uploads_dir) - allow(subject).to receive(:uploads_dir).and_return(uploads_dir) - end - - it 'moves a folder with files' do - source = File.join(uploads_dir, 'parent-group', 'sub-group') - FileUtils.mkdir_p(source) - destination = File.join(uploads_dir, 'parent-group', 'moved-group') - FileUtils.touch(File.join(source, 'test.txt')) - expected_file = File.join(destination, 'test.txt') - - subject.move_folders(uploads_dir, File.join('parent-group', 'sub-group'), File.join('parent-group', 'moved-group')) - - expect(File.exist?(expected_file)).to be(true) - end - end - - describe '#track_rename', :redis do - it 'tracks a rename in redis' do - key = 'rename:FakeRenameReservedPathMigrationV1:namespace' - - subject.track_rename('namespace', 'path/to/namespace', 'path/to/renamed') - - old_path = nil - new_path = nil - Gitlab::Redis::SharedState.with do |redis| - rename_info = redis.lpop(key) - old_path, new_path = Gitlab::Json.parse(rename_info) - end - - expect(old_path).to eq('path/to/namespace') - expect(new_path).to eq('path/to/renamed') - end - end - - describe '#reverts_for_type', :redis do - it 'yields for each tracked rename' do - subject.track_rename('project', 'old_path', 'new_path') - subject.track_rename('project', 'old_path2', 'new_path2') - subject.track_rename('namespace', 'namespace_path', 'new_namespace_path') - - expect { |b| subject.reverts_for_type('project', &b) } - .to yield_successive_args(%w(old_path2 new_path2), %w(old_path new_path)) - expect { |b| subject.reverts_for_type('namespace', &b) } - .to yield_with_args('namespace_path', 'new_namespace_path') - end - - it 'keeps the revert in redis if it failed' do - subject.track_rename('project', 'old_path', 'new_path') - - subject.reverts_for_type('project') do - raise 'whatever happens, keep going!' - end - - key = 'rename:FakeRenameReservedPathMigrationV1:project' - stored_renames = nil - rename_count = 0 - Gitlab::Redis::SharedState.with do |redis| - stored_renames = redis.lrange(key, 0, 1) - rename_count = redis.llen(key) - end - - expect(rename_count).to eq(1) - expect(Gitlab::Json.parse(stored_renames.first)).to eq(%w(old_path new_path)) - end - end -end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb deleted file mode 100644 index b00a1d4a9e1..00000000000 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb +++ /dev/null @@ -1,313 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :delete, -feature_category: :groups_and_projects do - let(:migration) { FakeRenameReservedPathMigrationV1.new } - let(:subject) { described_class.new(['the-path'], migration) } - let(:namespace) { create(:group, name: 'the-path') } - - before do - allow(migration).to receive(:say) - TestEnv.clean_test_path - end - - def migration_namespace(namespace) - Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses:: - Namespace.find(namespace.id) - end - - describe '#namespaces_for_paths' do - context 'nested namespaces' do - let(:subject) { described_class.new(['parent/the-Path'], migration) } - - it 'includes the namespace' do - parent = create(:group, path: 'parent') - child = create(:group, path: 'the-path', parent: parent) - - found_ids = subject.namespaces_for_paths(type: :child) - .map(&:id) - - expect(found_ids).to contain_exactly(child.id) - end - end - - context 'for child namespaces' do - it 'only returns child namespaces with the correct path' do - _root_namespace = create(:group, path: 'THE-path') - _other_path = create(:group, - path: 'other', - parent: create(:group)) - namespace = create(:group, - path: 'the-path', - parent: create(:group)) - - found_ids = subject.namespaces_for_paths(type: :child) - .map(&:id) - - expect(found_ids).to contain_exactly(namespace.id) - end - - it 'has no namespaces that look the same' do - _root_namespace = create(:group, path: 'THE-path') - _similar_path = create(:group, - path: 'not-really-the-path', - parent: create(:group)) - namespace = create(:group, - path: 'the-path', - parent: create(:group)) - - found_ids = subject.namespaces_for_paths(type: :child) - .map(&:id) - - expect(found_ids).to contain_exactly(namespace.id) - end - end - - context 'for top levelnamespaces' do - it 'only returns child namespaces with the correct path' do - root_namespace = create(:group, path: 'the-path') - _other_path = create(:group, path: 'other') - _child_namespace = create(:group, - path: 'the-path', - parent: create(:group)) - - found_ids = subject.namespaces_for_paths(type: :top_level) - .map(&:id) - - expect(found_ids).to contain_exactly(root_namespace.id) - end - - it 'has no namespaces that just look the same' do - root_namespace = create(:group, path: 'the-path') - _similar_path = create(:group, path: 'not-really-the-path') - _child_namespace = create(:group, - path: 'the-path', - parent: create(:group)) - - found_ids = subject.namespaces_for_paths(type: :top_level) - .map(&:id) - - expect(found_ids).to contain_exactly(root_namespace.id) - end - end - end - - describe '#move_repositories' do - let(:namespace) { create(:group, name: 'hello-group') } - - it 'moves a project for a namespace' do - project = create(:project, :repository, :legacy_storage, namespace: namespace, path: 'hello-project') - expected_repository = Gitlab::Git::Repository.new( - project.repository_storage, - 'bye-group/hello-project.git', - nil, - nil - ) - - subject.move_repositories(namespace, 'hello-group', 'bye-group') - - expect(expected_repository).to exist - end - - it 'moves a namespace in a subdirectory correctly' do - child_namespace = create(:group, name: 'sub-group', parent: namespace) - project = create(:project, :repository, :legacy_storage, namespace: child_namespace, path: 'hello-project') - - expected_repository = Gitlab::Git::Repository.new( - project.repository_storage, - 'hello-group/renamed-sub-group/hello-project.git', - nil, - nil - ) - - subject.move_repositories(child_namespace, 'hello-group/sub-group', 'hello-group/renamed-sub-group') - - expect(expected_repository).to exist - end - - it 'moves a parent namespace with subdirectories' do - child_namespace = create(:group, name: 'sub-group', parent: namespace) - project = create(:project, :repository, :legacy_storage, namespace: child_namespace, path: 'hello-project') - expected_repository = Gitlab::Git::Repository.new( - project.repository_storage, - 'renamed-group/sub-group/hello-project.git', - nil, - nil - ) - - subject.move_repositories(child_namespace, 'hello-group', 'renamed-group') - - expect(expected_repository).to exist - end - end - - describe "#child_ids_for_parent" do - it "collects child ids for all levels" do - parent = create(:group) - first_child = create(:group, parent: parent) - second_child = create(:group, parent: parent) - third_child = create(:group, parent: second_child) - all_ids = [parent.id, first_child.id, second_child.id, third_child.id] - - collected_ids = subject.child_ids_for_parent(parent, ids: [parent.id]) - - expect(collected_ids).to contain_exactly(*all_ids) - end - end - - describe "#rename_namespace" do - it 'renames paths & routes for the namespace' do - expect(subject).to receive(:rename_path_for_routable) - .with(namespace) - .and_call_original - - subject.rename_namespace(namespace) - - expect(namespace.reload.path).to eq('the-path0') - end - - it 'tracks the rename' do - expect(subject).to receive(:track_rename) - .with('namespace', 'the-path', 'the-path0') - - subject.rename_namespace(namespace) - end - - it 'renames things related to the namespace' do - expect(subject).to receive(:rename_namespace_dependencies) - .with(namespace, 'the-path', 'the-path0') - - subject.rename_namespace(namespace) - end - end - - describe '#rename_namespace_dependencies' do - it "moves the repository for a project in the namespace" do - project = create(:project, :repository, :legacy_storage, namespace: namespace, path: "the-path-project") - expected_repository = Gitlab::Git::Repository.new( - project.repository_storage, - "the-path0/the-path-project.git", - nil, - nil - ) - - subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') - - expect(expected_repository).to exist - end - - it "moves the uploads for the namespace" do - expect(subject).to receive(:move_uploads).with("the-path", "the-path0") - - subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') - end - - it "moves the pages for the namespace" do - expect(subject).to receive(:move_pages).with("the-path", "the-path0") - - subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') - end - - it 'invalidates the markdown cache of related projects' do - project = create(:project, :legacy_storage, namespace: namespace, path: "the-path-project") - - expect(subject).to receive(:remove_cached_html_for_projects).with([project.id]) - - subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') - end - - it "doesn't rename users for other namespaces" do - expect(subject).not_to receive(:rename_user) - - subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') - end - - it 'renames the username of a namespace for a user' do - user = create(:user, username: 'the-path') - - expect(subject).to receive(:rename_user).with('the-path', 'the-path0') - - subject.rename_namespace_dependencies(user.namespace, 'the-path', 'the-path0') - end - end - - describe '#rename_user' do - it 'renames a username' do - subject = described_class.new([], migration) - user = create(:user, username: 'broken') - - subject.rename_user('broken', 'broken0') - - expect(user.reload.username).to eq('broken0') - end - end - - describe '#rename_namespaces' do - let!(:top_level_namespace) { create(:group, path: 'the-path') } - let!(:child_namespace) do - create(:group, path: 'the-path', parent: create(:group)) - end - - it 'renames top level namespaces the namespace' do - expect(subject).to receive(:rename_namespace) - .with(migration_namespace(top_level_namespace)) - - subject.rename_namespaces(type: :top_level) - end - - it 'renames child namespaces' do - expect(subject).to receive(:rename_namespace) - .with(migration_namespace(child_namespace)) - - subject.rename_namespaces(type: :child) - end - end - - describe '#revert_renames', :redis do - it 'renames the routes back to the previous values' do - project = create(:project, :legacy_storage, :repository, path: 'a-project', namespace: namespace) - subject.rename_namespace(namespace) - - expect(subject).to receive(:perform_rename) - .with( - kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Namespace), - 'the-path0', - 'the-path' - ).and_call_original - - subject.revert_renames - - expect(namespace.reload.path).to eq('the-path') - expect(namespace.reload.route.path).to eq('the-path') - expect(project.reload.route.path).to eq('the-path/a-project') - end - - it 'moves the repositories back to their original place' do - project = create(:project, :repository, :legacy_storage, path: 'a-project', namespace: namespace) - project.create_repository - subject.rename_namespace(namespace) - - expected_repository = Gitlab::Git::Repository.new(project.repository_storage, 'the-path/a-project.git', nil, nil) - - expect(subject).to receive(:rename_namespace_dependencies) - .with( - kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Namespace), - 'the-path0', - 'the-path' - ).and_call_original - - subject.revert_renames - - expect(expected_repository).to exist - end - - it "doesn't break when the namespace was renamed" do - subject.rename_namespace(namespace) - namespace.update!(path: 'renamed-afterwards') - - expect { subject.revert_renames }.not_to raise_error - end - end -end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb deleted file mode 100644 index d2665664fb0..00000000000 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb +++ /dev/null @@ -1,190 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :delete, -feature_category: :groups_and_projects do - let(:migration) { FakeRenameReservedPathMigrationV1.new } - let(:subject) { described_class.new(['the-path'], migration) } - let(:project) do - create(:project, - :legacy_storage, - path: 'the-path', - namespace: create(:namespace, path: 'known-parent' )) - end - - before do - allow(migration).to receive(:say) - TestEnv.clean_test_path - end - - describe '#projects_for_paths' do - it 'searches using nested paths' do - namespace = create(:namespace, path: 'hello') - project = create(:project, :legacy_storage, path: 'THE-path', namespace: namespace) - - result_ids = described_class.new(['Hello/the-path'], migration) - .projects_for_paths.map(&:id) - - expect(result_ids).to contain_exactly(project.id) - end - - it 'includes the correct projects' do - project = create(:project, :legacy_storage, path: 'THE-path') - _other_project = create(:project, :legacy_storage) - - result_ids = subject.projects_for_paths.map(&:id) - - expect(result_ids).to contain_exactly(project.id) - end - end - - describe '#rename_projects' do - let!(:projects) { create_list(:project, 2, :legacy_storage, path: 'the-path') } - - it 'renames each project' do - expect(subject).to receive(:rename_project).twice - - subject.rename_projects - end - - it 'invalidates the markdown cache of related projects' do - expect(subject).to receive(:remove_cached_html_for_projects) - .with(a_collection_containing_exactly(*projects.map(&:id))) - - subject.rename_projects - end - end - - describe '#rename_project' do - it 'renames path & route for the project' do - expect(subject).to receive(:rename_path_for_routable) - .with(project) - .and_call_original - - subject.rename_project(project) - - expect(project.reload.path).to eq('the-path0') - end - - it 'tracks the rename' do - expect(subject).to receive(:track_rename) - .with('project', 'known-parent/the-path', 'known-parent/the-path0') - - subject.rename_project(project) - end - - it 'renames the folders for the project' do - expect(subject).to receive(:move_project_folders).with(project, 'known-parent/the-path', 'known-parent/the-path0') - - subject.rename_project(project) - end - end - - describe '#move_project_folders' do - it 'moves the wiki & the repo' do - expect(subject).to receive(:move_repository) - .with(project, 'known-parent/the-path.wiki', 'known-parent/the-path0.wiki') - expect(subject).to receive(:move_repository) - .with(project, 'known-parent/the-path', 'known-parent/the-path0') - - subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') - end - - it 'does not move the repositories when hashed storage is enabled' do - project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) - - expect(subject).not_to receive(:move_repository) - - subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') - end - - it 'moves uploads' do - expect(subject).to receive(:move_uploads) - .with('known-parent/the-path', 'known-parent/the-path0') - - subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') - end - - it 'does not move uploads when hashed storage is enabled for attachments' do - project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:attachments]) - - expect(subject).not_to receive(:move_uploads) - - subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') - end - - it 'moves pages' do - expect(subject).to receive(:move_pages) - .with('known-parent/the-path', 'known-parent/the-path0') - - subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') - end - end - - describe '#move_repository' do - let(:known_parent) { create(:namespace, path: 'known-parent') } - let(:project) { create(:project, :repository, :legacy_storage, path: 'the-path', namespace: known_parent) } - - it 'moves the repository for a project' do - expected_repository = Gitlab::Git::Repository.new( - project.repository_storage, - 'known-parent/new-repo.git', - nil, - nil - ) - - subject.move_repository(project, 'known-parent/the-path', 'known-parent/new-repo') - - expect(expected_repository).to exist - end - end - - describe '#revert_renames', :redis do - it 'renames the routes back to the previous values' do - subject.rename_project(project) - - expect(subject).to receive(:perform_rename) - .with( - kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Project), - 'known-parent/the-path0', - 'known-parent/the-path' - ).and_call_original - - subject.revert_renames - - expect(project.reload.path).to eq('the-path') - expect(project.route.path).to eq('known-parent/the-path') - end - - it 'moves the repositories back to their original place' do - project.create_repository - subject.rename_project(project) - - expected_repository = Gitlab::Git::Repository.new( - project.repository_storage, - 'known-parent/the-path.git', - nil, - nil - ) - - expect(subject).to receive(:move_project_folders) - .with( - kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Project), - 'known-parent/the-path0', - 'known-parent/the-path' - ).and_call_original - - subject.revert_renames - - expect(expected_repository).to exist - end - - it "doesn't break when the project was renamed" do - subject.rename_project(project) - project.update!(path: 'renamed-afterwards') - - expect { subject.revert_renames }.not_to raise_error - end - end -end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb deleted file mode 100644 index 3b2d3ab1354..00000000000 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.shared_examples 'renames child namespaces' do |type| - it 'renames namespaces' do - rename_namespaces = double - expect(described_class::RenameNamespaces) - .to receive(:new).with(%w[first-path second-path], subject) - .and_return(rename_namespaces) - expect(rename_namespaces).to receive(:rename_namespaces) - .with(type: :child) - - subject.rename_wildcard_paths(%w[first-path second-path]) - end -end - -RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1, :delete do - let(:subject) { FakeRenameReservedPathMigrationV1.new } - - before do - allow(subject).to receive(:say) - end - - describe '#rename_child_paths' do - it_behaves_like 'renames child namespaces' - end - - describe '#rename_wildcard_paths' do - it_behaves_like 'renames child namespaces' - - it 'renames projects' do - rename_projects = double - expect(described_class::RenameProjects) - .to receive(:new).with(['the-path'], subject) - .and_return(rename_projects) - - expect(rename_projects).to receive(:rename_projects) - - subject.rename_wildcard_paths(['the-path']) - end - end - - describe '#rename_root_paths' do - it 'renames namespaces' do - rename_namespaces = double - expect(described_class::RenameNamespaces) - .to receive(:new).with(['the-path'], subject) - .and_return(rename_namespaces) - expect(rename_namespaces).to receive(:rename_namespaces) - .with(type: :top_level) - - subject.rename_root_paths('the-path') - end - end - - describe '#revert_renames' do - it 'renames namespaces' do - rename_namespaces = double - expect(described_class::RenameNamespaces) - .to receive(:new).with([], subject) - .and_return(rename_namespaces) - expect(rename_namespaces).to receive(:revert_renames) - - subject.revert_renames - end - - it 'renames projects' do - rename_projects = double - expect(described_class::RenameProjects) - .to receive(:new).with([], subject) - .and_return(rename_projects) - expect(rename_projects).to receive(:revert_renames) - - subject.revert_renames - end - end -end diff --git a/spec/lib/gitlab/database_importers/work_items/related_links_restrictions_importer_spec.rb b/spec/lib/gitlab/database_importers/work_items/related_links_restrictions_importer_spec.rb new file mode 100644 index 00000000000..39d02922acc --- /dev/null +++ b/spec/lib/gitlab/database_importers/work_items/related_links_restrictions_importer_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::DatabaseImporters::WorkItems::RelatedLinksRestrictionsImporter, + feature_category: :portfolio_management do + subject { described_class.upsert_restrictions } + + it_behaves_like 'work item related links restrictions importer' +end diff --git a/spec/lib/gitlab/deploy_key_access_spec.rb b/spec/lib/gitlab/deploy_key_access_spec.rb index e32858cc13f..0a85fc5d967 100644 --- a/spec/lib/gitlab/deploy_key_access_spec.rb +++ b/spec/lib/gitlab/deploy_key_access_spec.rb @@ -23,16 +23,6 @@ RSpec.describe Gitlab::DeployKeyAccess, feature_category: :source_code_managemen it 'returns false' do expect(access.can_create_tag?('v0.1.2')).to be_falsey end - - context 'when deploy_key_for_protected_tags FF is disabled' do - before do - stub_feature_flags(deploy_key_for_protected_tags: false) - end - - it 'allows to push the tag' do - expect(access.can_create_tag?('v0.1.2')).to be_truthy - end - end end context 'push tag that matches a protected tag pattern via a deploy key' do diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 4aa4f160fc9..059058c5499 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -116,5 +116,71 @@ RSpec.describe Gitlab::Diff::PositionTracer do expect(diff_refs.head_sha).to eq(new_diff_refs.head_sha) end end + + describe 'when requesting diffs' do + shared_examples 'it does not call diff stats' do + it 'does not call diff stats' do + expect_next_instance_of(Gitlab::GitalyClient::CommitService) do |instance| + expect(instance).not_to receive(:diff_stats) + end + + diff_files + end + end + + shared_examples 'it calls diff stats' do + it 'calls diff stats' do + expect_next_instance_of(Gitlab::GitalyClient::CommitService) do |instance| + expect(instance).to receive(:diff_stats).and_call_original + end + + diff_files + end + end + + context 'when remove_request_stats_for_tracing is true' do + context 'ac diffs' do + let(:diff_files) { subject.ac_diffs.diff_files } + + it_behaves_like 'it does not call diff stats' + end + + context 'bd diffs' do + let(:diff_files) { subject.bd_diffs.diff_files } + + it_behaves_like 'it does not call diff stats' + end + + context 'cd diffs' do + let(:diff_files) { subject.cd_diffs.diff_files } + + it_behaves_like 'it does not call diff stats' + end + end + + context 'when remove_request_stats_for_tracing is false' do + before do + stub_feature_flags(remove_request_stats_for_tracing: false) + end + + context 'ac diffs' do + let(:diff_files) { subject.ac_diffs.diff_files } + + it_behaves_like 'it calls diff stats' + end + + context 'bd diffs' do + let(:diff_files) { subject.bd_diffs.diff_files } + + it_behaves_like 'it calls diff stats' + end + + context 'cd diffs' do + let(:diff_files) { subject.cd_diffs.diff_files } + + it_behaves_like 'it calls diff stats' + end + end + end end end diff --git a/spec/lib/gitlab/doctor/reset_tokens_spec.rb b/spec/lib/gitlab/doctor/reset_tokens_spec.rb new file mode 100644 index 00000000000..0cc947efdb4 --- /dev/null +++ b/spec/lib/gitlab/doctor/reset_tokens_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Doctor::ResetTokens, feature_category: :runner_fleet do + let(:logger) { instance_double('Logger') } + let(:model_names) { %w[Project Group] } + let(:token_names) { %w[runners_token] } + let(:dry_run) { false } + let(:doctor) { described_class.new(logger, model_names: model_names, token_names: token_names, dry_run: dry_run) } + + let_it_be(:functional_project) { create(:project).tap(&:runners_token) } + let_it_be(:functional_group) { create(:group).tap(&:runners_token) } + + let(:broken_project) { create(:project).tap { |project| project.update_columns(runners_token_encrypted: 'aaa') } } + let(:project_with_cipher_error) do + create(:project).tap do |project| + project.update_columns( + runners_token_encrypted: '|rXs75DSHXPE9MGAIgyxcut8pZc72gaa/2ojU0GS1+R+cXNqkbUB13Vb5BaMwf47d98980fc1') + end + end + + let(:broken_group) { create(:group, runners_token_encrypted: 'aaa') } + + subject(:run!) do + expect(logger).to receive(:info).with( + "Resetting #{token_names.join(', ')} on #{model_names.join(', ')} if they can not be read" + ) + expect(logger).to receive(:info).with('Done!') + doctor.run! + end + + before do + allow(logger).to receive(:info).with(%r{Checked \d/\d Projects}) + allow(logger).to receive(:info).with(%r{Checked \d Projects}) + allow(logger).to receive(:info).with(%r{Checked \d/\d Groups}) + allow(logger).to receive(:info).with(%r{Checked \d Groups}) + end + + it 'fixes broken project and not the functional project' do + expect(logger).to receive(:debug).with("> Fix Project[#{broken_project.id}].runners_token") + + expect { run! }.to change { broken_project.reload.runners_token_encrypted }.from('aaa') + .and not_change { functional_project.reload.runners_token_encrypted } + expect { broken_project.runners_token }.not_to raise_error + end + + it 'fixes project with cipher error' do + expect { project_with_cipher_error.runners_token }.to raise_error(OpenSSL::Cipher::CipherError) + expect(logger).to receive(:debug).with("> Fix Project[#{project_with_cipher_error.id}].runners_token") + + expect { run! }.to change { project_with_cipher_error.reload.runners_token_encrypted } + expect { project_with_cipher_error.runners_token }.not_to raise_error + end + + it 'fixes broken group and not the functional group' do + expect(logger).to receive(:debug).with("> Fix Group[#{broken_group.id}].runners_token") + + expect { run! }.to change { broken_group.reload.runners_token_encrypted }.from('aaa') + .and not_change { functional_group.reload.runners_token_encrypted } + + expect { broken_group.runners_token }.not_to raise_error + end + + context 'when one model specified' do + let(:model_names) { %w[Project] } + + it 'fixes broken project' do + expect(logger).to receive(:debug).with("> Fix Project[#{broken_project.id}].runners_token") + + expect { run! }.to change { broken_project.reload.runners_token_encrypted }.from('aaa') + expect { broken_project.runners_token }.not_to raise_error + end + + it 'does not fix other models' do + expect { run! }.not_to change { broken_group.reload.runners_token_encrypted }.from('aaa') + end + end + + context 'when non-existing token field is given' do + let(:token_names) { %w[nonexisting_token] } + + it 'does not fix anything' do + expect { run! }.not_to change { broken_project.reload.runners_token_encrypted }.from('aaa') + end + end + + context 'when executing in a dry-run mode' do + let(:dry_run) { true } + + it 'prints info about fixed project, but does not actually do anything' do + expect(logger).to receive(:info).with('Executing in DRY RUN mode, no records will actually be updated') + expect(logger).to receive(:debug).with("> Fix Project[#{broken_project.id}].runners_token") + + expect { run! }.not_to change { broken_project.reload.runners_token_encrypted }.from('aaa') + expect { broken_project.runners_token }.to raise_error(TypeError) + end + end + + it 'prints progress along the way' do + stub_const('Gitlab::Doctor::ResetTokens::PRINT_PROGRESS_EVERY', 1) + + broken_project + project_with_cipher_error + + expect(logger).to receive(:info).with( + "Resetting #{token_names.join(', ')} on #{model_names.join(', ')} if they can not be read" + ) + expect(logger).to receive(:info).with('Checked 1/3 Projects') + expect(logger).to receive(:debug).with("> Fix Project[#{broken_project.id}].runners_token") + expect(logger).to receive(:info).with('Checked 2/3 Projects') + expect(logger).to receive(:debug).with("> Fix Project[#{project_with_cipher_error.id}].runners_token") + expect(logger).to receive(:info).with('Checked 3/3 Projects') + expect(logger).to receive(:info).with('Done!') + + doctor.run! + end + + it "prints 'Something went wrong' error when encounters unexpected exception, but continues" do + broken_project + project_with_cipher_error + + expect(logger).to receive(:debug).with( + "> Something went wrong for Project[#{broken_project.id}].runners_token: Error message") + expect(logger).to receive(:debug).with("> Fix Project[#{project_with_cipher_error.id}].runners_token") + + expect(broken_project).to receive(:runners_token).and_raise("Error message") + expect(Project).to receive(:find_each).and_return([broken_project, project_with_cipher_error].each) + + expect { run! }.to not_change { broken_project.reload.runners_token_encrypted }.from('aaa') + .and change { project_with_cipher_error.reload.runners_token_encrypted } + end +end diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb index 6941ebd2e11..e6fff939632 100644 --- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb @@ -321,7 +321,7 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se end end - context 'when using custom service desk address' do + context 'when using additional service desk alias address' do let(:receiver) { Gitlab::Email::ServiceDeskReceiver.new(email_raw) } before do @@ -587,6 +587,16 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se end end + context 'when there is no to address' do + before do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:to_address).and_return(nil) + end + end + + it_behaves_like 'a new issue request' + end + context 'when there is no from address' do before do allow_next_instance_of(described_class) do |instance| diff --git a/spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb b/spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb deleted file mode 100644 index 4b77b2f7192..00000000000 --- a/spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Email::Message::BuildIosAppGuide, :saas do - subject(:message) { described_class.new } - - it 'contains the correct message', :aggregate_failures do - expect(message.subject_line).to eq 'Get set up to build for iOS' - expect(message.title).to eq "Building for iOS? We've got you covered." - expect(message.body_line1).to eq "Want to get your iOS app up and running, including " \ - "publishing all the way to TestFlight? Follow our guide to set up GitLab and fastlane to publish iOS apps to " \ - "the App Store." - expect(message.cta_text).to eq 'Learn how to build for iOS' - expect(message.cta2_text).to eq 'Watch iOS building in action.' - expect(message.logo_path).to eq 'mailers/in_product_marketing/create-0.png' - expect(message.unsubscribe).to include('%tag_unsubscribe_url%') - end -end diff --git a/spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb deleted file mode 100644 index a3c2d1b428e..00000000000 --- a/spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Email::Message::InProductMarketing::Helper do - describe 'unsubscribe_message' do - include Gitlab::Routing - - let(:dummy_class_with_helper) do - Class.new do - include Gitlab::Email::Message::InProductMarketing::Helper - include Gitlab::Routing - - def initialize(format = :html) - @format = format - end - - def default_url_options - {} - end - - attr_accessor :format - end - end - - let(:format) { :html } - - subject(:class_with_helper) { dummy_class_with_helper.new(format) } - - context 'for SaaS', :saas do - context 'format is HTML' do - it 'returns the correct HTML' do - message = "If you no longer wish to receive marketing emails from us, " \ - "you may <a href=\"%tag_unsubscribe_url%\">unsubscribe</a> at any time." - expect(class_with_helper.unsubscribe_message).to match message - end - end - - context 'format is text' do - let(:format) { :text } - - it 'returns the correct string' do - message = "If you no longer wish to receive marketing emails from us, " \ - "you may unsubscribe (%tag_unsubscribe_url%) at any time." - expect(class_with_helper.unsubscribe_message.squish).to match message - end - end - end - - context 'self-managed' do - context 'format is HTML' do - it 'returns the correct HTML' do - preferences_link = "http://example.com/preferences" - message = "To opt out of these onboarding emails, " \ - "<a href=\"#{profile_notifications_url}\">unsubscribe</a>. " \ - "If you don't want to receive marketing emails directly from GitLab, #{preferences_link}." - expect(class_with_helper.unsubscribe_message(preferences_link)) - .to match message - end - end - - context 'format is text' do - let(:format) { :text } - - it 'returns the correct string' do - preferences_link = "http://example.com/preferences" - message = "To opt out of these onboarding emails, " \ - "unsubscribe (#{profile_notifications_url}). " \ - "If you don't want to receive marketing emails directly from GitLab, #{preferences_link}." - expect(class_with_helper.unsubscribe_message(preferences_link).squish).to match message - end - end - end - end -end diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index ee836fc2129..f8084d24850 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Gitlab::Email::Receiver do metadata = receiver.mail_metadata - expect(metadata.keys).to match_array(%i(mail_uid from_address to_address mail_key references delivered_to envelope_to x_envelope_to meta received_recipients)) + expect(metadata.keys).to match_array(%i(mail_uid from_address to_address mail_key references delivered_to envelope_to x_envelope_to meta received_recipients cc_address)) expect(metadata[:meta]).to include(client_id: client_id, project: project.full_path) expect(metadata[meta_key]).to eq(meta_value) end @@ -112,6 +112,24 @@ RSpec.describe Gitlab::Email::Receiver do it_behaves_like 'successful receive' end end + + context 'when in a Cc header' do + let(:email_raw) do + <<~EMAIL + From: jake@example.com + To: to@example.com + Cc: incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com + Subject: Issue titile + + Issue description + EMAIL + end + + let(:meta_key) { :cc_address } + let(:meta_value) { ["incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com"] } + + it_behaves_like 'successful receive' + end end context 'when we cannot find a capable handler' do diff --git a/spec/lib/gitlab/email/service_desk_receiver_spec.rb b/spec/lib/gitlab/email/service_desk_receiver_spec.rb index c249a5422ff..4b67020471a 100644 --- a/spec/lib/gitlab/email/service_desk_receiver_spec.rb +++ b/spec/lib/gitlab/email/service_desk_receiver_spec.rb @@ -7,6 +7,12 @@ RSpec.describe Gitlab::Email::ServiceDeskReceiver do let(:receiver) { described_class.new(email) } context 'when the email contains a valid email address' do + shared_examples 'received successfully' do + it 'finds the service desk key' do + expect { receiver.execute }.not_to raise_error + end + end + before do stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com') @@ -21,34 +27,41 @@ RSpec.describe Gitlab::Email::ServiceDeskReceiver do end context 'when in a To header' do - it 'finds the service desk key' do - receiver.execute - end + it_behaves_like 'received successfully' end context 'when the email contains a valid email address in a header' do context 'when in a Delivered-To header' do let(:email) { fixture_file('emails/service_desk_custom_address_reply.eml') } - it 'finds the service desk key' do - receiver.execute - end + it_behaves_like 'received successfully' end context 'when in a Envelope-To header' do let(:email) { fixture_file('emails/service_desk_custom_address_envelope_to.eml') } - it 'finds the service desk key' do - receiver.execute - end + it_behaves_like 'received successfully' end context 'when in a X-Envelope-To header' do let(:email) { fixture_file('emails/service_desk_custom_address_x_envelope_to.eml') } - it 'finds the service desk key' do - receiver.execute + it_behaves_like 'received successfully' + end + + context 'when in a Cc header' do + let(:email) do + <<~EMAIL + From: from@example.com + To: to@example.com + Cc: support+project_slug-project_key@example.com + Subject: Issue titile + + Issue description + EMAIL end + + it_behaves_like 'received successfully' end end end diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index bc72d1a67d6..1b7c11dfef6 100644 --- a/spec/lib/gitlab/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe Gitlab::EncodingHelper do +RSpec.describe Gitlab::EncodingHelper, feature_category: :shared do using RSpec::Parameterized::TableSyntax let(:ext_class) { Class.new { extend Gitlab::EncodingHelper } } @@ -291,4 +291,39 @@ RSpec.describe Gitlab::EncodingHelper do expect(described_class.strip_bom("BOM at the end\xEF\xBB\xBF")).to eq("BOM at the end\xEF\xBB\xBF") end end + + # This cop's alternative to .dup doesn't work in this context for some reason. + # rubocop: disable Performance/UnfreezeString + describe "#force_encode_utf8" do + let(:stringish) do + Class.new(String) do + undef :force_encoding + end + end + + it "raises an ArgumentError if the argument can't force encoding" do + expect { described_class.force_encode_utf8(stringish.new("foo")) }.to raise_error(ArgumentError) + end + + it "returns the message if already UTF-8 and valid encoding" do + string = "føø".dup + + expect(string).not_to receive(:force_encoding).and_call_original + expect(described_class.force_encode_utf8(string)).to eq("føø") + end + + it "forcibly encodes a string to UTF-8" do + string = "føø".dup.force_encoding("ISO-8859-1") + + expect(string).to receive(:force_encoding).with("UTF-8").and_call_original + expect(described_class.force_encode_utf8(string)).to eq("føø") + end + + it "forcibly encodes a frozen string to UTF-8" do + string = "bår".dup.force_encoding("ISO-8859-1").freeze + + expect(described_class.force_encode_utf8(string)).to eq("bår") + end + end + # rubocop: enable Performance/UnfreezeString end diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb index c8325c5b359..80154c729e3 100644 --- a/spec/lib/gitlab/exclusive_lease_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::ExclusiveLease, :request_store, :clean_gitlab_redis_shared_state, +RSpec.describe Gitlab::ExclusiveLease, :request_store, :clean_gitlab_redis_cluster_shared_state, feature_category: :shared do let(:unique_key) { SecureRandom.hex(10) } @@ -20,67 +20,6 @@ RSpec.describe Gitlab::ExclusiveLease, :request_store, :clean_gitlab_redis_share sleep(2 * timeout) # lease should have expired now expect(lease.try_obtain).to be_present end - - context 'when migrating across stores' do - let(:lease) { described_class.new(unique_key, timeout: 3600) } - - before do - stub_feature_flags(use_cluster_shared_state_for_exclusive_lease: false) - allow(lease).to receive(:same_store).and_return(false) - end - - it 'acquires 2 locks' do - # stub first SETNX - Gitlab::Redis::SharedState.with { |r| expect(r).to receive(:set).and_return(true) } - Gitlab::Redis::ClusterSharedState.with { |r| expect(r).to receive(:set).and_call_original } - - expect(lease.try_obtain).to be_truthy - end - - it 'rollback first lock if second lock is not acquired' do - Gitlab::Redis::ClusterSharedState.with do |r| - expect(r).to receive(:set).and_return(false) - expect(r).to receive(:eval).and_call_original - end - - Gitlab::Redis::SharedState.with do |r| - expect(r).to receive(:set).and_call_original - expect(r).to receive(:eval).and_call_original - end - - expect(lease.try_obtain).to be_falsey - end - end - - context 'when cutting over to ClusterSharedState' do - context 'when lock is not acquired' do - it 'waits for existing holder to yield the lock' do - Gitlab::Redis::ClusterSharedState.with { |r| expect(r).to receive(:set).and_call_original } - Gitlab::Redis::SharedState.with { |r| expect(r).not_to receive(:set) } - - lease = described_class.new(unique_key, timeout: 3600) - expect(lease.try_obtain).to be_truthy - end - end - - context 'when lock is still acquired' do - let(:lease) { described_class.new(unique_key, timeout: 3600) } - - before do - # simulates cutover where some application's feature-flag has not updated - stub_feature_flags(use_cluster_shared_state_for_exclusive_lease: false) - lease.try_obtain - stub_feature_flags(use_cluster_shared_state_for_exclusive_lease: true) - end - - it 'waits for existing holder to yield the lock' do - Gitlab::Redis::ClusterSharedState.with { |r| expect(r).not_to receive(:set) } - Gitlab::Redis::SharedState.with { |r| expect(r).not_to receive(:set) } - - expect(lease.try_obtain).to be_falsey - end - end - end end describe '.redis_shared_state_key' do @@ -104,159 +43,131 @@ RSpec.describe Gitlab::ExclusiveLease, :request_store, :clean_gitlab_redis_share end end - shared_examples 'write operations' do - describe '#renew' do - it 'returns true when we have the existing lease' do - lease = described_class.new(unique_key, timeout: 3600) - expect(lease.try_obtain).to be_present - expect(lease.renew).to be_truthy - end + describe '#renew' do + it 'returns true when we have the existing lease' do + lease = described_class.new(unique_key, timeout: 3600) + expect(lease.try_obtain).to be_present + expect(lease.renew).to be_truthy + end - it 'returns false when we dont have a lease' do - lease = described_class.new(unique_key, timeout: 3600) - expect(lease.renew).to be_falsey - end + it 'returns false when we dont have a lease' do + lease = described_class.new(unique_key, timeout: 3600) + expect(lease.renew).to be_falsey end + end - describe 'cancellation' do - def new_lease(key) - described_class.new(key, timeout: 3600) - end + describe 'cancellation' do + def new_lease(key) + described_class.new(key, timeout: 3600) + end - shared_examples 'cancelling a lease' do - let(:lease) { new_lease(unique_key) } + shared_examples 'cancelling a lease' do + let(:lease) { new_lease(unique_key) } - it 'releases the held lease' do - uuid = lease.try_obtain - expect(uuid).to be_present - expect(new_lease(unique_key).try_obtain).to eq(false) + it 'releases the held lease' do + uuid = lease.try_obtain + expect(uuid).to be_present + expect(new_lease(unique_key).try_obtain).to eq(false) - cancel_lease(uuid) + cancel_lease(uuid) - expect(new_lease(unique_key).try_obtain).to be_present - end + expect(new_lease(unique_key).try_obtain).to be_present end + end - describe '.cancel' do - def cancel_lease(uuid) - described_class.cancel(release_key, uuid) - end + describe '.cancel' do + def cancel_lease(uuid) + described_class.cancel(release_key, uuid) + end - context 'when called with the unprefixed key' do - it_behaves_like 'cancelling a lease' do - let(:release_key) { unique_key } - end + context 'when called with the unprefixed key' do + it_behaves_like 'cancelling a lease' do + let(:release_key) { unique_key } end + end - context 'when called with the prefixed key' do - it_behaves_like 'cancelling a lease' do - let(:release_key) { described_class.redis_shared_state_key(unique_key) } - end + context 'when called with the prefixed key' do + it_behaves_like 'cancelling a lease' do + let(:release_key) { described_class.redis_shared_state_key(unique_key) } end + end - it 'does not raise errors when given a nil key' do - expect { described_class.cancel(nil, nil) }.not_to raise_error - end + it 'does not raise errors when given a nil key' do + expect { described_class.cancel(nil, nil) }.not_to raise_error end + end - describe '#cancel' do - def cancel_lease(_uuid) - lease.cancel - end + describe '#cancel' do + def cancel_lease(_uuid) + lease.cancel + end - it_behaves_like 'cancelling a lease' + it_behaves_like 'cancelling a lease' - it 'is safe to call even if the lease was never obtained' do - lease = new_lease(unique_key) + it 'is safe to call even if the lease was never obtained' do + lease = new_lease(unique_key) - lease.cancel + lease.cancel - expect(new_lease(unique_key).try_obtain).to be_present - end + expect(new_lease(unique_key).try_obtain).to be_present end end + end - describe '.reset_all!' do - it 'removes all existing lease keys from redis' do - uuid = described_class.new(unique_key, timeout: 3600).try_obtain + describe '.reset_all!' do + it 'removes all existing lease keys from redis' do + uuid = described_class.new(unique_key, timeout: 3600).try_obtain - expect(described_class.get_uuid(unique_key)).to eq(uuid) + expect(described_class.get_uuid(unique_key)).to eq(uuid) - described_class.reset_all! + described_class.reset_all! - expect(described_class.get_uuid(unique_key)).to be_falsey - end + expect(described_class.get_uuid(unique_key)).to be_falsey end end - shared_examples 'read operations' do - describe '#exists?' do - it 'returns true for an existing lease' do - lease = described_class.new(unique_key, timeout: 3600) - lease.try_obtain - - expect(lease.exists?).to eq(true) - end - - it 'returns false for a lease that does not exist' do - lease = described_class.new(unique_key, timeout: 3600) + describe '#exists?' do + it 'returns true for an existing lease' do + lease = described_class.new(unique_key, timeout: 3600) + lease.try_obtain - expect(lease.exists?).to eq(false) - end + expect(lease.exists?).to eq(true) end - describe '.get_uuid' do - it 'gets the uuid if lease with the key associated exists' do - uuid = described_class.new(unique_key, timeout: 3600).try_obtain - - expect(described_class.get_uuid(unique_key)).to eq(uuid) - end + it 'returns false for a lease that does not exist' do + lease = described_class.new(unique_key, timeout: 3600) - it 'returns false if the lease does not exist' do - expect(described_class.get_uuid(unique_key)).to be false - end + expect(lease.exists?).to eq(false) end + end - describe '#ttl' do - it 'returns the TTL of the Redis key' do - lease = described_class.new('kittens', timeout: 100) - lease.try_obtain - - expect(lease.ttl <= 100).to eq(true) - end + describe '.get_uuid' do + it 'gets the uuid if lease with the key associated exists' do + uuid = described_class.new(unique_key, timeout: 3600).try_obtain - it 'returns nil when the lease does not exist' do - lease = described_class.new('kittens', timeout: 10) + expect(described_class.get_uuid(unique_key)).to eq(uuid) + end - expect(lease.ttl).to be_nil - end + it 'returns false if the lease does not exist' do + expect(described_class.get_uuid(unique_key)).to be false end end - context 'when migrating across stores' do - before do - stub_feature_flags(use_cluster_shared_state_for_exclusive_lease: false) + describe '#ttl' do + it 'returns the TTL of the Redis key' do + lease = described_class.new('kittens', timeout: 100) + lease.try_obtain + + expect(lease.ttl <= 100).to eq(true) end - it_behaves_like 'read operations' - it_behaves_like 'write operations' - end + it 'returns nil when the lease does not exist' do + lease = described_class.new('kittens', timeout: 10) - context 'when feature flags are all disabled' do - before do - stub_feature_flags( - use_cluster_shared_state_for_exclusive_lease: false, - enable_exclusive_lease_double_lock_rw: false - ) + expect(lease.ttl).to be_nil end - - it_behaves_like 'read operations' - it_behaves_like 'write operations' end - it_behaves_like 'read operations' - it_behaves_like 'write operations' - describe '.throttle' do it 'prevents repeated execution of the block' do number = 0 @@ -310,8 +221,8 @@ RSpec.describe Gitlab::ExclusiveLease, :request_store, :clean_gitlab_redis_share it 'allows count to be specified' do expect(described_class) .to receive(:new) - .with(anything, hash_including(timeout: 15.minutes.to_i)) - .and_call_original + .with(anything, hash_including(timeout: 15.minutes.to_i)) + .and_call_original described_class.throttle(1, count: 4) {} end @@ -319,8 +230,8 @@ RSpec.describe Gitlab::ExclusiveLease, :request_store, :clean_gitlab_redis_share it 'allows period to be specified' do expect(described_class) .to receive(:new) - .with(anything, hash_including(timeout: 1.day.to_i)) - .and_call_original + .with(anything, hash_including(timeout: 1.day.to_i)) + .and_call_original described_class.throttle(1, period: 1.day) {} end @@ -328,80 +239,10 @@ RSpec.describe Gitlab::ExclusiveLease, :request_store, :clean_gitlab_redis_share it 'allows period and count to be specified' do expect(described_class) .to receive(:new) - .with(anything, hash_including(timeout: 30.minutes.to_i)) - .and_call_original + .with(anything, hash_including(timeout: 30.minutes.to_i)) + .and_call_original described_class.throttle(1, count: 48, period: 1.day) {} end end - - describe 'transitions between feature-flag toggles' do - shared_examples 'retains behaviours across transitions' do |flag| - it 'retains read behaviour' do - lease = described_class.new(unique_key, timeout: 3600) - uuid = lease.try_obtain - - expect(lease.ttl).not_to eq(nil) - expect(lease.exists?).to be_truthy - expect(described_class.get_uuid(unique_key)).to eq(uuid) - - # simulates transition - stub_feature_flags({ flag => true }) - Gitlab::SafeRequestStore.clear! - - expect(lease.ttl).not_to eq(nil) - expect(lease.exists?).to be_truthy - expect(described_class.get_uuid(unique_key)).to eq(uuid) - end - - it 'retains renew behaviour' do - lease = described_class.new(unique_key, timeout: 3600) - lease.try_obtain - - expect(lease.renew).to be_truthy - - # simulates transition - stub_feature_flags({ flag => true }) - Gitlab::SafeRequestStore.clear! - - expect(lease.renew).to be_truthy - end - - it 'retains renew behaviour' do - lease = described_class.new(unique_key, timeout: 3600) - uuid = lease.try_obtain - lease.cancel - - # proves successful cancellation - expect(lease.try_obtain).to eq(uuid) - - # simulates transition - stub_feature_flags({ flag => true }) - Gitlab::SafeRequestStore.clear! - - expect(lease.try_obtain).to be_falsey - lease.cancel - expect(lease.try_obtain).to eq(uuid) - end - end - - context 'when enabling enable_exclusive_lease_double_lock_rw' do - before do - stub_feature_flags( - enable_exclusive_lease_double_lock_rw: false, - use_cluster_shared_state_for_exclusive_lease: false - ) - end - - it_behaves_like 'retains behaviours across transitions', :enable_exclusive_lease_double_lock_rw - end - - context 'when enabling use_cluster_shared_state_for_exclusive_lease' do - before do - stub_feature_flags(use_cluster_shared_state_for_exclusive_lease: false) - end - - it_behaves_like 'retains behaviours across transitions', :use_cluster_shared_state_for_exclusive_lease - end - end end diff --git a/spec/lib/gitlab/experiment/rollout/feature_spec.rb b/spec/lib/gitlab/experiment/rollout/feature_spec.rb index cd46e7b3386..6d01b7a175f 100644 --- a/spec/lib/gitlab/experiment/rollout/feature_spec.rb +++ b/spec/lib/gitlab/experiment/rollout/feature_spec.rb @@ -2,16 +2,15 @@ require 'spec_helper' -RSpec.describe Gitlab::Experiment::Rollout::Feature, :experiment do +RSpec.describe Gitlab::Experiment::Rollout::Feature, :experiment, feature_category: :acquisition do subject { described_class.new(subject_experiment) } let(:subject_experiment) { experiment('namespaced/stub') } - describe "#enabled?" do + describe "#enabled?", :saas do before do stub_feature_flags(gitlab_experiment: true) allow(subject).to receive(:feature_flag_defined?).and_return(true) - allow(Gitlab).to receive(:com?).and_return(true) allow(subject).to receive(:feature_flag_instance).and_return(double(state: :on)) end @@ -45,6 +44,18 @@ RSpec.describe Gitlab::Experiment::Rollout::Feature, :experiment do end describe "#execute_assignment" do + let(:variants) do + ->(e) do + # rubocop:disable Lint/EmptyBlock + e.control {} + e.variant(:red) {} + e.variant(:blue) {} + # rubocop:enable Lint/EmptyBlock + end + end + + let(:subject_experiment) { experiment('namespaced/stub', &variants) } + before do allow(Feature).to receive(:enabled?).with('namespaced_stub', any_args).and_return(true) end @@ -60,9 +71,82 @@ RSpec.describe Gitlab::Experiment::Rollout::Feature, :experiment do end it "returns an assigned name" do - allow(subject).to receive(:behavior_names).and_return([:variant1, :variant2]) + expect(subject.execute_assignment).to eq(:blue) + end + + context "when there are no behaviors" do + let(:variants) { ->(e) { e.control {} } } # rubocop:disable Lint/EmptyBlock + + it "does not raise an error" do + expect { subject.execute_assignment }.not_to raise_error + end + end + + context "for even rollout to non-control", :saas do + let(:counts) { Hash.new(0) } + let(:subject_experiment) { experiment('namespaced/stub') } + + before do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:enabled?).and_return(true) + end + + subject_experiment.variant(:variant1) {} # rubocop:disable Lint/EmptyBlock + subject_experiment.variant(:variant2) {} # rubocop:disable Lint/EmptyBlock + end + + it "rolls out relatively evenly to 2 behaviors" do + 100.times { |i| run_cycle(subject_experiment, value: i) } + + expect(counts).to eq(variant1: 54, variant2: 46) + end + + it "rolls out relatively evenly to 3 behaviors" do + subject_experiment.variant(:variant3) {} # rubocop:disable Lint/EmptyBlock + + 100.times { |i| run_cycle(subject_experiment, value: i) } + + expect(counts).to eq(variant1: 31, variant2: 29, variant3: 40) + end + + context "when distribution is specified as an array" do + before do + subject_experiment.rollout(described_class, distribution: [32, 25, 43]) + end + + it "rolls out with the expected distribution" do + subject_experiment.variant(:variant3) {} # rubocop:disable Lint/EmptyBlock + + 100.times { |i| run_cycle(subject_experiment, value: i) } + + expect(counts).to eq(variant1: 39, variant2: 24, variant3: 37) + end + end + + context "when distribution is specified as a hash" do + before do + subject_experiment.rollout(described_class, distribution: { variant1: 90, variant2: 10 }) + end + + it "rolls out with the expected distribution" do + 100.times { |i| run_cycle(subject_experiment, value: i) } + + expect(counts).to eq(variant1: 95, variant2: 5) + end + end + + def run_cycle(experiment, **context) + experiment.instance_variable_set(:@_assigned_variant_name, nil) + experiment.context(context) if context + + begin + experiment.cache.delete + rescue StandardError + nil + end - expect(subject.execute_assignment).to eq(:variant2) + counts[experiment.assigned.name] += 1 + end end end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 4d78e194da8..6b3630d7a1f 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe Gitlab::Git::Diff do +RSpec.describe Gitlab::Git::Diff, feature_category: :source_code_management do let_it_be(:project) { create(:project, :repository) } let_it_be(:repository) { project.repository } @@ -336,6 +336,121 @@ EOT end end + describe '#unidiff' do + let_it_be(:project) { create(:project, :empty_repo) } + let_it_be(:repository) { project.repository } + let_it_be(:user) { project.first_owner } + + let(:commits) { repository.commits('master', limit: 10) } + let(:diffs) { commits.map(&:diffs).map(&:diffs).flat_map(&:to_a).reverse } + + before_all do + create_commit( + project, + user, + commit_message: "Create file", + actions: [{ action: 'create', content: 'foo', file_path: 'a.txt' }] + ) + + create_commit( + project, + user, + commit_message: "Update file", + actions: [{ action: 'update', content: 'foo2', file_path: 'a.txt' }] + ) + + create_commit( + project, + user, + commit_message: "Rename file without change", + actions: [{ action: 'move', previous_path: 'a.txt', file_path: 'b.txt' }] + ) + + create_commit( + project, + user, + commit_message: "Rename file with change", + actions: [{ action: 'move', content: 'foo3', previous_path: 'b.txt', file_path: 'c.txt' }] + ) + + create_commit( + project, + user, + commit_message: "Delete file", + actions: [{ action: 'delete', file_path: 'c.txt' }] + ) + + create_commit( + project, + user, + commit_message: "Create empty file", + actions: [{ action: 'create', file_path: 'empty.txt' }] + ) + + create_commit( + project, + user, + commit_message: "Create binary file", + actions: [{ action: 'create', content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=', file_path: 'test%2Ebin', encoding: 'base64' }] + ) + end + + context 'when file was created' do + it 'returns a correct header' do + diff = diffs[0] + + expect(diff.unidiff).to start_with("--- /dev/null\n+++ b/a.txt\n") + end + end + + context 'when file was changed' do + it 'returns a correct header' do + diff = diffs[1] + + expect(diff.unidiff).to start_with("--- a/a.txt\n+++ b/a.txt\n") + end + end + + context 'when file was moved without content change' do + it 'returns an empty header' do + diff = diffs[2] + + expect(diff.unidiff).to eq('') + end + end + + context 'when file was moved with content change' do + it 'returns a correct header' do + expect(diffs[3].unidiff).to start_with("--- /dev/null\n+++ b/c.txt\n") + expect(diffs[4].unidiff).to start_with("--- a/b.txt\n+++ /dev/null\n") + end + end + + context 'when file was deleted' do + it 'returns a correct header' do + diff = diffs[5] + + expect(diff.unidiff).to start_with("--- a/c.txt\n+++ /dev/null\n") + end + end + + context 'when empty file was created' do + it 'returns an empty header' do + diff = diffs[6] + + expect(diff.unidiff).to eq('') + end + end + + context 'when file is binary' do + it 'returns a binary files message' do + diff = diffs[7] + + expect(diff.unidiff).to eq("Binary files /dev/null and b/test%2Ebin differ\n") + end + end + end + describe '#submodule?' do let(:gitaly_submodule_diff) do Gitlab::GitalyClient::Diff.new( @@ -445,4 +560,9 @@ EOT # rugged will not detect this as binary, but we can fake it described_class.between(project.repository, 'add-pdf-text-binary', 'add-pdf-text-binary^').first end + + def create_commit(project, user, params) + params = { start_branch: 'master', branch_name: 'master' }.merge(params) + Files::MultiService.new(project, user, params).execute.fetch(:result) + end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 18a090a00be..47b5986cfd8 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -203,25 +203,6 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen expect(metadata['CommitId']).to eq(expected_commit_id) end end - - context 'when resolve_ambiguous_archives is disabled' do - before do - stub_feature_flags(resolve_ambiguous_archives: false) - end - - where(:ref, :expected_commit_id, :desc) do - 'refs/heads/branch-merged' | ref(:branch_merged_commit_id) | 'when tag looks like a branch (difference!)' - 'branch-merged' | ref(:branch_master_commit_id) | 'when tag has the same name as a branch' - ref(:branch_merged_commit_id) | ref(:branch_merged_commit_id) | 'when tag looks like a commit id' - 'v0.0.0' | ref(:branch_master_commit_id) | 'when tag looks like a normal tag' - end - - with_them do - it 'selects the correct commit' do - expect(metadata['CommitId']).to eq(expected_commit_id) - end - end - end end context 'when branch is ambiguous' do @@ -241,25 +222,6 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen expect(metadata['CommitId']).to eq(expected_commit_id) end end - - context 'when resolve_ambiguous_archives is disabled' do - before do - stub_feature_flags(resolve_ambiguous_archives: false) - end - - where(:ref, :expected_commit_id, :desc) do - 'refs/tags/v1.0.0' | ref(:tag_1_0_0_commit_id) | 'when branch looks like a tag (difference!)' - 'v1.0.0' | ref(:tag_1_0_0_commit_id) | 'when branch has the same name as a tag' - ref(:branch_merged_commit_id) | ref(:branch_merged_commit_id) | 'when branch looks like a commit id' - 'just-a-normal-branch' | ref(:branch_master_commit_id) | 'when branch looks like a normal branch' - end - - with_them do - it 'selects the correct commit' do - expect(metadata['CommitId']).to eq(expected_commit_id) - end - end - end end context 'when ref is HEAD' do @@ -2650,21 +2612,6 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen end end - describe '#rename' do - let(:repository) { mutable_repository } - - it 'moves the repository' do - checksum = repository.checksum - new_relative_path = "rename_test/relative/path" - renamed_repository = Gitlab::Git::Repository.new(repository.storage, new_relative_path, nil, nil) - - repository.rename(new_relative_path) - - expect(renamed_repository.checksum).to eq(checksum) - expect(repository.exists?).to be false - end - end - describe '#remove' do let(:repository) { mutable_repository } @@ -2833,4 +2780,14 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen end end end + + describe '#get_file_attributes' do + let(:rev) { 'master' } + let(:paths) { ['file.txt'] } + let(:attrs) { ['text'] } + + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :get_file_attributes do + subject { repository.get_file_attributes(rev, paths, attrs) } + end + end end diff --git a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb index d320b9c4091..d5a0ab3d5e0 100644 --- a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb +++ b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb @@ -1,18 +1,11 @@ # frozen_string_literal: true require 'spec_helper' -require 'json' -require 'tempfile' RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, feature_category: :gitaly do let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:feature_flag_name) { wrapper.rugged_feature_keys.first } - let(:temp_gitaly_metadata_file) { create_temporary_gitaly_metadata_file } - - before_all do - create_gitaly_metadata_file - end subject(:wrapper) do klazz = Class.new do @@ -24,11 +17,6 @@ RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, feature_category: :gitaly do klazz.new end - before do - allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_call_original - Gitlab::GitalyClient.instance_variable_set(:@can_use_disk, {}) - end - describe '#execute_rugged_call', :request_store do let(:args) { ['refs/heads/master', 1] } @@ -46,83 +34,9 @@ RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, feature_category: :gitaly do end end - context 'when feature flag is not persisted', stub_feature_flags: false do - context 'when running puma with multiple threads' do - before do - allow(subject).to receive(:running_puma_with_multiple_threads?).and_return(true) - end - - it 'returns false' do - expect(subject.use_rugged?(repository, feature_flag_name)).to be false - end - end - - context 'when skip_rugged_auto_detect feature flag is enabled' do - context 'when not running puma with multiple threads' do - before do - allow(subject).to receive(:running_puma_with_multiple_threads?).and_return(false) - stub_feature_flags(feature_flag_name => nil) - stub_feature_flags(skip_rugged_auto_detect: true) - end - - it 'returns false' do - expect(subject.use_rugged?(repository, feature_flag_name)).to be false - end - end - end - - context 'when skip_rugged_auto_detect feature flag is disabled' do - before do - stub_feature_flags(skip_rugged_auto_detect: false) - end - - context 'when not running puma with multiple threads' do - before do - allow(subject).to receive(:running_puma_with_multiple_threads?).and_return(false) - end - - it 'returns true when gitaly matches disk' do - expect(subject.use_rugged?(repository, feature_flag_name)).to be true - end - - it 'returns false when disk access fails' do - allow(Gitlab::GitalyClient).to receive(:storage_metadata_file_path).and_return("/fake/path/doesnt/exist") - - expect(subject.use_rugged?(repository, feature_flag_name)).to be false - end - - it "returns false when gitaly doesn't match disk" do - allow(Gitlab::GitalyClient).to receive(:storage_metadata_file_path).and_return(temp_gitaly_metadata_file) - - expect(subject.use_rugged?(repository, feature_flag_name)).to be_falsey - - File.delete(temp_gitaly_metadata_file) - end - - it "doesn't lead to a second rpc call because gitaly client should use the cached value" do - expect(subject.use_rugged?(repository, feature_flag_name)).to be true - - expect(Gitlab::GitalyClient).not_to receive(:filesystem_id) - - subject.use_rugged?(repository, feature_flag_name) - end - end - end - end - - context 'when feature flag is persisted' do - it 'returns false when the feature flag is off' do - Feature.disable(feature_flag_name) - - expect(subject.use_rugged?(repository, feature_flag_name)).to be_falsey - end - - it "returns true when feature flag is on" do - Feature.enable(feature_flag_name) - - allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(false) - - expect(subject.use_rugged?(repository, feature_flag_name)).to be true + describe '#use_rugged?' do + it 'returns false' do + expect(subject.use_rugged?(repository, feature_flag_name)).to be false end end @@ -184,7 +98,7 @@ RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, feature_category: :gitaly do context 'all features are enabled' do let(:feature_keys) { [:feature_key_1, :feature_key_2] } - it { is_expected.to be_truthy } + it { is_expected.to be_falsey } end context 'all features are not enabled' do @@ -196,28 +110,7 @@ RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, feature_category: :gitaly do context 'some feature is enabled' do let(:feature_keys) { [:feature_key_4, :feature_key_2] } - it { is_expected.to be_truthy } - end - end - - def create_temporary_gitaly_metadata_file - tmp = Tempfile.new('.gitaly-metadata') - gitaly_metadata = { - "gitaly_filesystem_id" => "some-value" - } - tmp.write(gitaly_metadata.to_json) - tmp.flush - tmp.close - tmp.path - end - - def create_gitaly_metadata_file - metadata_filename = File.join(TestEnv.repos_path, '.gitaly-metadata') - File.open(metadata_filename, 'w+') do |f| - gitaly_metadata = { - "gitaly_filesystem_id" => SecureRandom.uuid - } - f.write(gitaly_metadata.to_json) + it { is_expected.to be_falsey } end end end diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 84ab8376fe1..9675e48a77f 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -2,11 +2,11 @@ require "spec_helper" -RSpec.describe Gitlab::Git::Tree do +RSpec.describe Gitlab::Git::Tree, feature_category: :source_code_management do let_it_be(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:repository) { project.repository.raw } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:repository) { project.repository.raw } shared_examples 'repo' do subject(:tree) { Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, pagination_params) } @@ -95,6 +95,8 @@ RSpec.describe Gitlab::Git::Tree do end context :flat_path do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw } let(:filename) { 'files/flat/path/correct/content.txt' } let(:path) { 'files/flat' } # rubocop: disable Rails/FindBy @@ -192,9 +194,9 @@ RSpec.describe Gitlab::Git::Tree do end describe '.where with Rugged enabled', :enable_rugged do - it 'calls out to the Rugged implementation' do + it 'does not call to the Rugged implementation' do allow_next_instance_of(Rugged) do |instance| - allow(instance).to receive(:lookup).with(SeedRepo::Commit::ID) + allow(instance).not_to receive(:lookup) end described_class.where(repository, SeedRepo::Commit::ID, 'files', false, false) @@ -214,10 +216,10 @@ RSpec.describe Gitlab::Git::Tree do context 'when limit is equal to number of entries' do let(:entries_count) { entries.count } - it 'returns all entries without a cursor' do + it 'returns all entries with a cursor' do result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: entries_count, page_token: nil }) - expect(cursor).to be_nil + expect(cursor).to eq(Gitaly::PaginationCursor.new) expect(result.entries.count).to eq(entries_count) end end @@ -234,9 +236,9 @@ RSpec.describe Gitlab::Git::Tree do context 'when limit is missing' do let(:pagination_params) { { limit: nil, page_token: nil } } - it 'returns empty result' do - expect(entries).to eq([]) - expect(cursor).to be_nil + it 'returns all entries' do + expect(entries.count).to be < 20 + expect(cursor).to eq(Gitaly::PaginationCursor.new) end end @@ -247,7 +249,7 @@ RSpec.describe Gitlab::Git::Tree do result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: -1, page_token: nil }) expect(result.count).to eq(entries_count) - expect(cursor).to be_nil + expect(cursor).to eq(Gitaly::PaginationCursor.new) end context 'when token is provided' do @@ -258,7 +260,7 @@ RSpec.describe Gitlab::Git::Tree do result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: -1, page_token: token }) expect(result.count).to eq(entries.count - 2) - expect(cursor).to be_nil + expect(cursor).to eq(Gitaly::PaginationCursor.new) end end end @@ -276,7 +278,7 @@ RSpec.describe Gitlab::Git::Tree do it 'returns only available entries' do expect(entries.count).to be < 20 - expect(cursor).to be_nil + expect(cursor).to eq(Gitaly::PaginationCursor.new) end end diff --git a/spec/lib/gitlab/git_audit_event_spec.rb b/spec/lib/gitlab/git_audit_event_spec.rb new file mode 100644 index 00000000000..c533b39f550 --- /dev/null +++ b/spec/lib/gitlab/git_audit_event_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GitAuditEvent, feature_category: :source_code_management do + let_it_be(:player) { create(:user) } + let_it_be(:group) { create(:group, :public) } + let_it_be(:project) { create(:project) } + + subject { described_class.new(player, project) } + + describe '#send_audit_event' do + let(:msg) { 'valid_msg' } + + context 'with successfully sending' do + let_it_be(:project) { create(:project, namespace: group) } + + before do + allow(::Gitlab::Audit::Auditor).to receive(:audit) + end + + context 'when player is a regular user' do + it 'sends git audit event' do + expect(::Gitlab::Audit::Auditor).to receive(:audit).with(a_hash_including( + name: 'repository_git_operation', + stream_only: true, + author: player, + scope: project, + target: project, + message: msg + )).once + + subject.send_audit_event(msg) + end + end + + context 'when player is ::API::Support::GitAccessActor' do + let_it_be(:user) { player } + let_it_be(:key) { create(:key, user: user) } + let_it_be(:git_access_actor) { ::API::Support::GitAccessActor.new(user: user, key: key) } + + subject { described_class.new(git_access_actor, project) } + + it 'sends git audit event' do + expect(::Gitlab::Audit::Auditor).to receive(:audit).with(a_hash_including( + name: 'repository_git_operation', + stream_only: true, + author: git_access_actor.deploy_key_or_user, + scope: project, + target: project, + message: msg + )).once + + subject.send_audit_event(msg) + end + end + end + + context 'when user is blank' do + let_it_be(:player) { nil } + + it 'does not send git audit event' do + expect(::Gitlab::Audit::Auditor).not_to receive(:audit) + + subject.send_audit_event(msg) + end + end + + context 'when project is blank' do + let_it_be(:project) { nil } + + it 'does not send git audit event' do + expect(::Gitlab::Audit::Auditor).not_to receive(:audit) + + subject.send_audit_event(msg) + end + end + end +end diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index 8e0e4525729..283a9cb45dc 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -369,17 +369,6 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService, feature_category: :gital end end - describe '#rename' do - it 'sends a rename_repository message' do - expect_any_instance_of(Gitaly::RepositoryService::Stub) - .to receive(:rename_repository) - .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) - .and_return(double(value: true)) - - client.rename('some/new/path') - end - end - describe '#remove' do it 'sends a remove_repository message' do expect_any_instance_of(Gitaly::RepositoryService::Stub) @@ -451,4 +440,19 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService, feature_category: :gital client.object_pool end end + + describe '#get_file_attributes' do + let(:rev) { 'master' } + let(:paths) { ['file.txt'] } + let(:attrs) { ['text'] } + + it 'sends a get_file_attributes message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:get_file_attributes) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_call_original + + expect(client.get_file_attributes(rev, paths, attrs)).to be_a Gitaly::GetFileAttributesResponse + end + end end diff --git a/spec/lib/gitlab/github_gists_import/importer/gist_importer_spec.rb b/spec/lib/gitlab/github_gists_import/importer/gist_importer_spec.rb index cbcd9b83c15..b098a151660 100644 --- a/spec/lib/gitlab/github_gists_import/importer/gist_importer_spec.rb +++ b/spec/lib/gitlab/github_gists_import/importer/gist_importer_spec.rb @@ -174,9 +174,9 @@ RSpec.describe Gitlab::GithubGistsImport::Importer::GistImporter, feature_catego .to receive(:validate!) .with(url, ports: [80, 443], schemes: %w[http https git], allow_localhost: true, allow_local_network: true) - .and_raise(Gitlab::UrlBlocker::BlockedUrlError) + .and_raise(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) - expect { subject.execute }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError) + expect { subject.execute }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end @@ -191,9 +191,9 @@ RSpec.describe Gitlab::GithubGistsImport::Importer::GistImporter, feature_catego .to receive(:validate!) .with(url, ports: [80, 443], schemes: %w[http https git], allow_localhost: false, allow_local_network: false) - .and_raise(Gitlab::UrlBlocker::BlockedUrlError) + .and_raise(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) - expect { subject.execute }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError) + expect { subject.execute }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end end diff --git a/spec/lib/gitlab/github_import/bulk_importing_spec.rb b/spec/lib/gitlab/github_import/bulk_importing_spec.rb index 28fbd4d883f..6b4984ceaf2 100644 --- a/spec/lib/gitlab/github_import/bulk_importing_spec.rb +++ b/spec/lib/gitlab/github_import/bulk_importing_spec.rb @@ -47,10 +47,9 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers .with(object) .and_return(false) - expect(Gitlab::Import::Logger) + expect(Gitlab::GithubImport::Logger) .to receive(:info) .with( - import_type: :github, project_id: 1, importer: 'MyImporter', message: '1 object_types fetched' @@ -82,10 +81,9 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers .with(object) .and_return(true) - expect(Gitlab::Import::Logger) + expect(Gitlab::GithubImport::Logger) .to receive(:info) .with( - import_type: :github, project_id: 1, importer: 'MyImporter', message: '0 object_types fetched' @@ -145,14 +143,13 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers } ) - expect(Gitlab::Import::Logger) + expect(Gitlab::GithubImport::Logger) .to receive(:error) .with( - import_type: :github, project_id: 1, importer: 'MyImporter', message: ['Title is invalid'], - github_identifiers: { id: 12345, title: 'bug,bug', object_type: :object_type } + external_identifiers: { id: 12345, title: 'bug,bug', object_type: :object_type } ) expect(Gitlab::GithubImport::ObjectCounter) @@ -172,7 +169,7 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers expect(errors).not_to be_empty expect(errors[0][:validation_errors].full_messages).to match_array(['Title is invalid']) - expect(errors[0][:github_identifiers]).to eq({ id: 12345, title: 'bug,bug', object_type: :object_type }) + expect(errors[0][:external_identifiers]).to eq({ id: 12345, title: 'bug,bug', object_type: :object_type }) end end end @@ -182,11 +179,10 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers it 'bulk inserts rows into the database' do rows = [{ title: 'Foo' }] * 10 - expect(Gitlab::Import::Logger) + expect(Gitlab::GithubImport::Logger) .to receive(:info) .twice .with( - import_type: :github, project_id: 1, importer: 'MyImporter', message: '5 object_types imported' @@ -243,7 +239,7 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers importer.bulk_insert_failures([{ validation_errors: error, - github_identifiers: { id: 123456 } + external_identifiers: { id: 123456 } }]) end end diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index 4b0d61e3188..5f321a15de9 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -316,7 +316,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do allow_retry expect(client).to receive(:requests_remaining?).twice.and_return(true) - expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once + expect(Gitlab::GithubImport::Logger).to receive(:info).with(hash_including(info_params)).once expect(client.with_rate_limit(&block_to_rate_limit)).to eq({}) end @@ -337,7 +337,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do it 'retries on error and succeeds' do allow_retry - expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once + expect(Gitlab::GithubImport::Logger).to receive(:info).with(hash_including(info_params)).once expect(client.with_rate_limit(&block_to_rate_limit)).to eq({}) end @@ -723,7 +723,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do it 'retries on error and succeeds' do allow_retry(:post) - expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once + expect(Gitlab::GithubImport::Logger).to receive(:info).with(hash_including(info_params)).once expect(client.search_repos_by_name_graphql('test')).to eq({}) end diff --git a/spec/lib/gitlab/github_import/clients/proxy_spec.rb b/spec/lib/gitlab/github_import/clients/proxy_spec.rb index 7b2a8fa9d74..99fd98d2ed4 100644 --- a/spec/lib/gitlab/github_import/clients/proxy_spec.rb +++ b/spec/lib/gitlab/github_import/clients/proxy_spec.rb @@ -3,10 +3,9 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Clients::Proxy, :manage, feature_category: :importers do - subject(:client) { described_class.new(access_token, client_options) } + subject(:client) { described_class.new(access_token) } let(:access_token) { 'test_token' } - let(:client_options) { { foo: :bar } } it { expect(client).to delegate_method(:each_object).to(:client) } it { expect(client).to delegate_method(:user).to(:client) } @@ -15,124 +14,67 @@ RSpec.describe Gitlab::GithubImport::Clients::Proxy, :manage, feature_category: describe '#repos' do let(:search_text) { 'search text' } let(:pagination_options) { { limit: 10 } } - - context 'when remove_legacy_github_client FF is enabled' do - let(:client_stub) { instance_double(Gitlab::GithubImport::Client) } - - let(:client_response) do - { - data: { - search: { - nodes: [{ name: 'foo' }, { name: 'bar' }], - pageInfo: { startCursor: 'foo', endCursor: 'bar' }, - repositoryCount: 2 - } + let(:client_stub) { instance_double(Gitlab::GithubImport::Client) } + let(:client_response) do + { + data: { + search: { + nodes: [{ name: 'foo' }, { name: 'bar' }], + pageInfo: { startCursor: 'foo', endCursor: 'bar' }, + repositoryCount: 2 } } - end - - it 'fetches repos with Gitlab::GithubImport::Client (GraphQL API)' do - expect(Gitlab::GithubImport::Client) - .to receive(:new).with(access_token).and_return(client_stub) - expect(client_stub) - .to receive(:search_repos_by_name_graphql) - .with(search_text, pagination_options).and_return(client_response) - - expect(client.repos(search_text, pagination_options)).to eq( - { - repos: [{ name: 'foo' }, { name: 'bar' }], - page_info: { startCursor: 'foo', endCursor: 'bar' }, - count: 2 - } - ) - end + } end - context 'when remove_legacy_github_client FF is disabled' do - let(:client_stub) { instance_double(Gitlab::LegacyGithubImport::Client) } - let(:search_text) { nil } - - before do - stub_feature_flags(remove_legacy_github_client: false) - end - - it 'fetches repos with Gitlab::LegacyGithubImport::Client' do - expect(Gitlab::LegacyGithubImport::Client) - .to receive(:new).with(access_token, client_options).and_return(client_stub) - expect(client_stub).to receive(:repos) - .and_return([{ name: 'foo' }, { name: 'bar' }]) - - expect(client.repos(search_text, pagination_options)) - .to eq({ repos: [{ name: 'foo' }, { name: 'bar' }] }) - end - - context 'with filter params' do - let(:search_text) { 'fo' } + it 'fetches repos with Gitlab::GithubImport::Client (GraphQL API)' do + expect(Gitlab::GithubImport::Client) + .to receive(:new).with(access_token).and_return(client_stub) + expect(client_stub) + .to receive(:search_repos_by_name_graphql) + .with(search_text, pagination_options).and_return(client_response) - it 'fetches repos with Gitlab::LegacyGithubImport::Client' do - expect(Gitlab::LegacyGithubImport::Client) - .to receive(:new).with(access_token, client_options).and_return(client_stub) - expect(client_stub).to receive(:repos) - .and_return([{ name: 'FOO' }, { name: 'bAr' }]) - - expect(client.repos(search_text, pagination_options)) - .to eq({ repos: [{ name: 'FOO' }] }) - end - end + expect(client.repos(search_text, pagination_options)).to eq( + { + repos: [{ name: 'foo' }, { name: 'bar' }], + page_info: { startCursor: 'foo', endCursor: 'bar' }, + count: 2 + } + ) end end describe '#count_by', :clean_gitlab_redis_cache do - context 'when remove_legacy_github_client FF is enabled' do - let(:client_stub) { instance_double(Gitlab::GithubImport::Client) } - let(:client_response) { { data: { search: { repositoryCount: 1 } } } } + let(:client_stub) { instance_double(Gitlab::GithubImport::Client) } + let(:client_response) { { data: { search: { repositoryCount: 1 } } } } + context 'when value is cached' do before do - stub_feature_flags(remove_legacy_github_client: true) + Gitlab::Cache::Import::Caching.write('github-importer/provider-repo-count/owned/user_id', 3) end - context 'when value is cached' do - before do - Gitlab::Cache::Import::Caching.write('github-importer/provider-repo-count/owned/user_id', 3) - end - - it 'returns repository count from cache' do - expect(Gitlab::GithubImport::Client) - .to receive(:new).with(access_token).and_return(client_stub) - expect(client_stub) - .not_to receive(:count_repos_by_relation_type_graphql) - .with({ relation_type: 'owned' }) - expect(client.count_repos_by('owned', 'user_id')).to eq(3) - end - end - - context 'when value is not cached' do - it 'returns repository count' do - expect(Gitlab::GithubImport::Client) - .to receive(:new).with(access_token).and_return(client_stub) - expect(client_stub) - .to receive(:count_repos_by_relation_type_graphql) - .with({ relation_type: 'owned' }).and_return(client_response) - expect(Gitlab::Cache::Import::Caching) - .to receive(:write) - .with('github-importer/provider-repo-count/owned/user_id', 1, timeout: 5.minutes) - .and_call_original - expect(client.count_repos_by('owned', 'user_id')).to eq(1) - end + it 'returns repository count from cache' do + expect(Gitlab::GithubImport::Client) + .to receive(:new).with(access_token).and_return(client_stub) + expect(client_stub) + .not_to receive(:count_repos_by_relation_type_graphql) + .with({ relation_type: 'owned' }) + expect(client.count_repos_by('owned', 'user_id')).to eq(3) end end - context 'when remove_legacy_github_client FF is disabled' do - let(:client_stub) { instance_double(Gitlab::LegacyGithubImport::Client) } - - before do - stub_feature_flags(remove_legacy_github_client: false) - end - - it 'returns nil' do - expect(Gitlab::LegacyGithubImport::Client) - .to receive(:new).with(access_token, client_options).and_return(client_stub) - expect(client.count_repos_by('owned', 'user_id')).to be_nil + context 'when value is not cached' do + it 'returns repository count' do + expect(Gitlab::GithubImport::Client) + .to receive(:new).with(access_token).and_return(client_stub) + expect(client_stub) + .to receive(:count_repos_by_relation_type_graphql) + .with({ relation_type: 'owned' }).and_return(client_response) + expect(Gitlab::Cache::Import::Caching) + .to receive(:write) + .with('github-importer/provider-repo-count/owned/user_id', 1, timeout: 5.minutes) + .and_call_original + expect(client.count_repos_by('owned', 'user_id')).to eq(1) end end end diff --git a/spec/lib/gitlab/github_import/importer/attachments/issues_importer_spec.rb b/spec/lib/gitlab/github_import/importer/attachments/issues_importer_spec.rb index 7890561bf2d..b44f1ec85f3 100644 --- a/spec/lib/gitlab/github_import/importer/attachments/issues_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/attachments/issues_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Importer::Attachments::IssuesImporter do +RSpec.describe Gitlab::GithubImport::Importer::Attachments::IssuesImporter, feature_category: :importers do subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project) } @@ -17,6 +17,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::IssuesImporter do let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] } it 'imports each project issue attachments' do + expect(project.issues).to receive(:id_not_in).with([]).and_return(project.issues) expect(project.issues).to receive(:select).with(:id, :description, :iid).and_call_original expect_next_instances_of( @@ -32,6 +33,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::IssuesImporter do it "doesn't import this issue attachments" do importer.mark_as_imported(issue_1) + expect(project.issues).to receive(:id_not_in).with([issue_1.id.to_s]).and_call_original expect_next_instance_of( Gitlab::GithubImport::Importer::NoteAttachmentsImporter, *importer_attrs ) do |note_attachments_importer| diff --git a/spec/lib/gitlab/github_import/importer/attachments/merge_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/attachments/merge_requests_importer_spec.rb index e5aa17dd81e..381cb17bb52 100644 --- a/spec/lib/gitlab/github_import/importer/attachments/merge_requests_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/attachments/merge_requests_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter do +RSpec.describe Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter, feature_category: :importers do subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project) } @@ -17,6 +17,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporte let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] } it 'imports each project merge request attachments' do + expect(project.merge_requests).to receive(:id_not_in).with([]).and_return(project.merge_requests) expect(project.merge_requests).to receive(:select).with(:id, :description, :iid).and_call_original expect_next_instances_of( @@ -32,6 +33,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporte it "doesn't import this merge request attachments" do importer.mark_as_imported(merge_request_1) + expect(project.merge_requests).to receive(:id_not_in).with([merge_request_1.id.to_s]).and_call_original expect_next_instance_of( Gitlab::GithubImport::Importer::NoteAttachmentsImporter, *importer_attrs ) do |note_attachments_importer| diff --git a/spec/lib/gitlab/github_import/importer/attachments/notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/attachments/notes_importer_spec.rb index 7ed353e1b71..5b3ad032702 100644 --- a/spec/lib/gitlab/github_import/importer/attachments/notes_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/attachments/notes_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Importer::Attachments::NotesImporter do +RSpec.describe Gitlab::GithubImport::Importer::Attachments::NotesImporter, feature_category: :importers do subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project) } @@ -18,6 +18,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::NotesImporter do let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] } it 'imports each project user note' do + expect(project.notes).to receive(:id_not_in).with([]).and_call_original expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new) .with(*importer_attrs).twice.and_return(importer_stub) expect(importer_stub).to receive(:execute).twice @@ -29,6 +30,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::NotesImporter do it "doesn't import this note" do importer.mark_as_imported(note_1) + expect(project.notes).to receive(:id_not_in).with([note_1.id.to_s]).and_call_original expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new) .with(*importer_attrs).once.and_return(importer_stub) expect(importer_stub).to receive(:execute).once diff --git a/spec/lib/gitlab/github_import/importer/attachments/releases_importer_spec.rb b/spec/lib/gitlab/github_import/importer/attachments/releases_importer_spec.rb index e1b009c3eeb..c1c19c40afb 100644 --- a/spec/lib/gitlab/github_import/importer/attachments/releases_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/attachments/releases_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Importer::Attachments::ReleasesImporter do +RSpec.describe Gitlab::GithubImport::Importer::Attachments::ReleasesImporter, feature_category: :importers do subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project) } @@ -17,6 +17,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::ReleasesImporter do let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] } it 'imports each project release' do + expect(project.releases).to receive(:id_not_in).with([]).and_return(project.releases) expect(project.releases).to receive(:select).with(:id, :description, :tag).and_call_original expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new) @@ -30,6 +31,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::ReleasesImporter do it "doesn't import this release" do importer.mark_as_imported(release_1) + expect(project.releases).to receive(:id_not_in).with([release_1.id.to_s]).and_call_original expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new) .with(*importer_attrs).once.and_return(importer_stub) expect(importer_stub).to receive(:execute).once diff --git a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb index 0f35c7ee0dc..7668451ad4e 100644 --- a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_failures do +RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_failures, feature_category: :importers do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } @@ -80,17 +80,6 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_fail expect(note.author_id).to eq(project.creator_id) expect(note.note).to eq("*Created by: #{user.username}*\n\nHello") end - - it 'does not import the note when a foreign key error is raised' do - stub_user_finder(project.creator_id, false) - - expect(ApplicationRecord) - .to receive(:legacy_bulk_insert) - .and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key') - - expect { subject.execute } - .not_to change(LegacyDiffNote, :count) - end end describe '#execute' do @@ -143,6 +132,7 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_fail expect(note.noteable_type).to eq('MergeRequest') expect(note.noteable_id).to eq(merge_request.id) expect(note.project_id).to eq(project.id) + expect(note.namespace_id).to eq(project.project_namespace_id) expect(note.author_id).to eq(user.id) expect(note.system).to eq(false) expect(note.discussion_id).to eq(discussion_id) diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb index bf2ffda3bf1..1c453436f9f 100644 --- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cache do +RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cache, feature_category: :importers do let_it_be(:work_item_type_id) { ::WorkItems::Type.default_issue_type.id } let(:project) { create(:project) } @@ -77,6 +77,27 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redi importer.execute end + + it 'caches the created issue ID even if importer later fails' do + error = StandardError.new('mocked error') + + allow_next_instance_of(described_class) do |importer| + allow(importer) + .to receive(:create_issue) + .and_return(10) + allow(importer) + .to receive(:create_assignees) + .and_raise(error) + end + + expect_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + expect(finder) + .to receive(:cache_database_id) + .with(10) + end + + expect { importer.execute }.to raise_error(error) + end end describe '#create_issue' do @@ -162,21 +183,6 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redi end end - context 'when the import fails due to a foreign key error' do - it 'does not raise any errors' do - allow(importer.user_finder) - .to receive(:author_id_for) - .with(issue) - .and_return([user.id, true]) - - expect(importer) - .to receive(:insert_and_return_id) - .and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key') - - expect { importer.create_issue }.not_to raise_error - end - end - it 'produces a valid Issue' do allow(importer.user_finder) .to receive(:author_id_for) diff --git a/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb index fc8d9cee066..0328a36b646 100644 --- a/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb @@ -50,13 +50,12 @@ feature_category: :importers do label = { id: 1, name: 'bug,bug', color: 'ffffff' } expect(importer).to receive(:each_label).and_return([label]) - expect(Gitlab::Import::Logger).to receive(:error) + expect(Gitlab::GithubImport::Logger).to receive(:error) .with( - import_type: :github, project_id: project.id, importer: described_class.name, message: ['Title is invalid'], - github_identifiers: { title: 'bug,bug', object_type: :label } + external_identifiers: { title: 'bug,bug', object_type: :label } ) rows, errors = importer.build_labels diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb index cf44d510c80..fa7283d210b 100644 --- a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb @@ -80,13 +80,12 @@ RSpec.describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab .to receive(:each_milestone) .and_return([milestone]) - expect(Gitlab::Import::Logger).to receive(:error) + expect(Gitlab::GithubImport::Logger).to receive(:error) .with( - import_type: :github, project_id: project.id, importer: described_class.name, message: ["Title can't be blank"], - github_identifiers: { iid: 2, object_type: :milestone, title: nil } + external_identifiers: { iid: 2, object_type: :milestone, title: nil } ) rows, errors = importer.build_milestones diff --git a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb index 5ac50578b6a..91311a8e90f 100644 --- a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do +RSpec.describe Gitlab::GithubImport::Importer::NoteImporter, feature_category: :importers do let(:client) { double(:client) } let(:project) { create(:project) } let(:user) { create(:user) } @@ -12,13 +12,13 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do let(:github_note) do Gitlab::GithubImport::Representation::Note.new( + note_id: 100, noteable_id: 1, noteable_type: 'Issue', author: Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice'), note: note_body, created_at: created_at, - updated_at: updated_at, - github_id: 1 + updated_at: updated_at ) end @@ -50,6 +50,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do noteable_type: 'Issue', noteable_id: issue_row.id, project_id: project.id, + namespace_id: project.project_namespace_id, author_id: user.id, note: 'This is my note', discussion_id: match(/\A[0-9a-f]{40}\z/), @@ -81,6 +82,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do noteable_type: 'Issue', noteable_id: issue_row.id, project_id: project.id, + namespace_id: project.project_namespace_id, author_id: project.creator_id, note: "*Created by: alice*\n\nThis is my note", discussion_id: match(/\A[0-9a-f]{40}\z/), @@ -126,34 +128,20 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do expect { importer.execute }.to raise_error(ActiveRecord::RecordInvalid) end end - end - - context 'when the noteable does not exist' do - it 'does not import the note' do - expect(ApplicationRecord).not_to receive(:legacy_bulk_insert) - - importer.execute - end - end - - context 'when the import fails due to a foreign key error' do - it 'does not raise any errors' do - issue_row = create(:issue, project: project, iid: 1) - - allow(importer) - .to receive(:find_noteable_id) - .and_return(issue_row.id) - allow(importer.user_finder) - .to receive(:author_id_for) - .with(github_note) - .and_return([user.id, true]) - - expect(ApplicationRecord) - .to receive(:legacy_bulk_insert) - .and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key') + context 'when noteble_id can not be found' do + before do + allow(importer) + .to receive(:find_noteable_id) + .and_return(nil) + end - expect { importer.execute }.not_to raise_error + it 'raises NoteableNotFound' do + expect { importer.execute }.to raise_error( + ::Gitlab::GithubImport::Exceptions::NoteableNotFound, + 'Error to find noteable_id for note' + ) + end end end @@ -173,13 +161,6 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do expect(project.notes.take).to be_valid end - - # rubocop:disable RSpec/AnyInstanceOf - it 'skips markdown field cache callback' do - expect_any_instance_of(Note).not_to receive(:refresh_markdown_cache) - importer.execute - end - # rubocop:enable RSpec/AnyInstanceOf end describe '#find_noteable_id' do diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb index dd73b6879e0..52c91d91eff 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redis_cache do +RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redis_cache, feature_category: :importers do let(:project) { create(:project, :repository) } let(:client) { double(:client) } let(:user) { create(:user) } @@ -42,9 +42,9 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitla let(:importer) { described_class.new(pull_request, project, client) } describe '#execute' do - it 'imports the pull request' do - mr = double(:merge_request, id: 10, merged?: false) + let(:mr) { double(:merge_request, id: 10, merged?: false) } + it 'imports the pull request' do expect(importer) .to receive(:create_merge_request) .and_return([mr, false]) @@ -63,6 +63,27 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitla importer.execute end + + it 'caches the created MR ID even if importer later fails' do + error = StandardError.new('mocked error') + + allow_next_instance_of(described_class) do |importer| + allow(importer) + .to receive(:create_merge_request) + .and_return([mr, false]) + allow(importer) + .to receive(:set_merge_request_assignees) + .and_raise(error) + end + + expect_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + expect(finder) + .to receive(:cache_database_id) + .with(mr.id) + end + + expect { importer.execute }.to raise_error(error) + end end describe '#create_merge_request' do diff --git a/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb index 9e9d6c6e9cd..d0145ba1120 100644 --- a/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb @@ -54,6 +54,9 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor expect(note_attachments_importer).to receive(:execute) end + expect(Gitlab::GithubImport::ObjectCounter) + .to receive(:increment).twice.with(project, :pull_request_review_request, :fetched) + importer.sequential_import end @@ -72,6 +75,9 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor expect(note_attachments_importer).to receive(:execute) end + expect(Gitlab::GithubImport::ObjectCounter) + .to receive(:increment).once.with(project, :pull_request_review_request, :fetched) + importer.sequential_import end end @@ -115,6 +121,9 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker) .to receive(:perform_in).with(1.second, *expected_worker_payload.second) + expect(Gitlab::GithubImport::ObjectCounter) + .to receive(:increment).twice.with(project, :pull_request_review_request, :fetched) + importer.parallel_import end @@ -130,6 +139,9 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker) .to receive(:perform_in).with(1.second, *expected_worker_payload.second) + expect(Gitlab::GithubImport::ObjectCounter) + .to receive(:increment).once.with(project, :pull_request_review_request, :fetched) + importer.parallel_import end end diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb index eddde272d2c..cfd75fba849 100644 --- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb @@ -149,7 +149,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter, feature_cat it 'updates the repository' do importer = described_class.new(project, client) - expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect_next_instance_of(Gitlab::GithubImport::Logger) do |logger| expect(logger) .to receive(:info) .with(an_instance_of(Hash)) diff --git a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb index a3d20af22c7..1cfbe8e20ae 100644 --- a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb @@ -148,7 +148,7 @@ RSpec.describe Gitlab::GithubImport::Importer::ReleasesImporter, feature_categor expect(errors[0][:validation_errors].full_messages).to match_array( ['Description is too long (maximum is 1000000 characters)'] ) - expect(errors[0][:github_identifiers]).to eq({ tag: '1.0', object_type: :release }) + expect(errors[0][:external_identifiers]).to eq({ tag: '1.0', object_type: :release }) end end diff --git a/spec/lib/gitlab/github_import/settings_spec.rb b/spec/lib/gitlab/github_import/settings_spec.rb index d670aaea482..de497bc6689 100644 --- a/spec/lib/gitlab/github_import/settings_spec.rb +++ b/spec/lib/gitlab/github_import/settings_spec.rb @@ -62,17 +62,20 @@ RSpec.describe Gitlab::GithubImport::Settings, feature_category: :importers do collaborators_import: false, foo: :bar }, + timeout_strategy: "optimistic", additional_access_tokens: %w[foo bar] }.stringify_keys end - it 'puts optional steps & access tokens into projects import_data' do - project.create_or_update_import_data(credentials: { user: 'token' }) + it 'puts optional steps, timeout strategy & access tokens into projects import_data' do + project.build_or_assign_import_data(credentials: { user: 'token' }) settings.write(data_input) expect(project.import_data.data['optional_stages']) .to eq optional_stages.stringify_keys + expect(project.import_data.data['timeout_strategy']) + .to eq("optimistic") expect(project.import_data.credentials.fetch(:additional_access_tokens)) .to eq(data_input['additional_access_tokens']) end @@ -80,7 +83,7 @@ RSpec.describe Gitlab::GithubImport::Settings, feature_category: :importers do describe '#enabled?' do it 'returns is enabled or not specific optional stage' do - project.create_or_update_import_data(data: { optional_stages: optional_stages }) + project.build_or_assign_import_data(data: { optional_stages: optional_stages }) expect(settings.enabled?(:single_endpoint_issue_events_import)).to eq true expect(settings.enabled?(:single_endpoint_notes_import)).to eq false @@ -91,7 +94,7 @@ RSpec.describe Gitlab::GithubImport::Settings, feature_category: :importers do describe '#disabled?' do it 'returns is disabled or not specific optional stage' do - project.create_or_update_import_data(data: { optional_stages: optional_stages }) + project.build_or_assign_import_data(data: { optional_stages: optional_stages }) expect(settings.disabled?(:single_endpoint_issue_events_import)).to eq false expect(settings.disabled?(:single_endpoint_notes_import)).to eq true diff --git a/spec/lib/gitlab/gon_helper_spec.rb b/spec/lib/gitlab/gon_helper_spec.rb index fc722402917..e4684597ddf 100644 --- a/spec/lib/gitlab/gon_helper_spec.rb +++ b/spec/lib/gitlab/gon_helper_spec.rb @@ -206,6 +206,7 @@ RSpec.describe Gitlab::GonHelper do context 'when feature flag is false' do before do stub_feature_flags(browsersdk_tracking: false) + stub_feature_flags(gl_analytics_tracking: false) end it "doesn't set the analytics_url and analytics_id" do diff --git a/spec/lib/gitlab/graphql/deprecations/deprecation_spec.rb b/spec/lib/gitlab/graphql/deprecations/deprecation_spec.rb index 55650b0480e..4db9c1da418 100644 --- a/spec/lib/gitlab/graphql/deprecations/deprecation_spec.rb +++ b/spec/lib/gitlab/graphql/deprecations/deprecation_spec.rb @@ -175,6 +175,23 @@ RSpec.describe ::Gitlab::Graphql::Deprecations::Deprecation, feature_category: : expect(desc).to be_nil end + + it 'strips any leading or trailing spaces' do + desc = deprecation.edit_description(" Some description. \n") + + expect(desc).to eq('Some description. Deprecated in 10.10: This was renamed.') + end + + it 'strips any leading or trailing spaces in heredoc string literals' do + description = <<~DESC + Lorem ipsum + dolor sit amet. + DESC + + desc = deprecation.edit_description(description) + + expect(desc).to eq("Lorem ipsum\ndolor sit amet. Deprecated in 10.10: This was renamed.") + end end describe '#original_description' do diff --git a/spec/lib/gitlab/graphql/pagination/array_connection_spec.rb b/spec/lib/gitlab/graphql/pagination/array_connection_spec.rb index 03cf53bb990..28885d0379b 100644 --- a/spec/lib/gitlab/graphql/pagination/array_connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/array_connection_spec.rb @@ -3,9 +3,10 @@ require 'spec_helper' RSpec.describe ::Gitlab::Graphql::Pagination::ArrayConnection do + let(:context) { instance_double(GraphQL::Query::Context, schema: GitlabSchema) } let(:nodes) { (1..10) } - subject(:connection) { described_class.new(nodes, max_page_size: 100) } + subject(:connection) { described_class.new(nodes, context: context, max_page_size: 100) } it_behaves_like 'a connection with collection methods' diff --git a/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb b/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb index d2475d1edb9..e3ae6732ebb 100644 --- a/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection do + let(:context) { instance_double(GraphQL::Query::Context, schema: GitlabSchema) } let(:prev_cursor) { 1 } let(:next_cursor) { 6 } let(:values) { [2, 3, 4, 5] } @@ -10,7 +11,7 @@ RSpec.describe Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection d let(:arguments) { {} } subject(:connection) do - described_class.new(all_nodes, **{ max_page_size: values.size }.merge(arguments)) + described_class.new(all_nodes, **{ context: context, max_page_size: values.size }.merge(arguments)) end it_behaves_like 'a connection with collection methods' diff --git a/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb b/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb index 1ca7c1c3c69..a8babaf8d3b 100644 --- a/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb @@ -3,18 +3,20 @@ require 'spec_helper' RSpec.describe Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection do + let(:context) { instance_double(GraphQL::Query::Context, schema: GitlabSchema) } + it 'subclasses from GraphQL::Relay::RelationConnection' do expect(described_class.superclass).to eq GraphQL::Pagination::ActiveRecordRelationConnection end it_behaves_like 'a connection with collection methods' do - let(:connection) { described_class.new(Project.all) } + let(:connection) { described_class.new(Project.all, context: context) } end it_behaves_like 'a redactable connection' do let_it_be(:users) { create_list(:user, 2) } - let(:connection) { described_class.new(User.all, max_page_size: 10) } + let(:connection) { described_class.new(User.all, context: context, max_page_size: 10) } let(:unwanted) { users.second } end end diff --git a/spec/lib/gitlab/graphql/timeout_spec.rb b/spec/lib/gitlab/graphql/timeout_spec.rb index 999840019d2..fd27def6973 100644 --- a/spec/lib/gitlab/graphql/timeout_spec.rb +++ b/spec/lib/gitlab/graphql/timeout_spec.rb @@ -8,10 +8,9 @@ RSpec.describe Gitlab::Graphql::Timeout do end it 'sends the error to our GraphQL logger' do - parent_type = double(graphql_name: 'parent_type') - field = double(graphql_name: 'field') + field = double(path: 'parent_type.field') query = double(query_string: 'query_string', provided_variables: 'provided_variables') - error = GraphQL::Schema::Timeout::TimeoutError.new(parent_type, field) + error = GraphQL::Schema::Timeout::TimeoutError.new(field) expect(Gitlab::GraphqlLogger) .to receive(:error) diff --git a/spec/lib/gitlab/group_search_results_spec.rb b/spec/lib/gitlab/group_search_results_spec.rb index 314759fb8a4..84a2a0549d5 100644 --- a/spec/lib/gitlab/group_search_results_spec.rb +++ b/spec/lib/gitlab/group_search_results_spec.rb @@ -59,7 +59,7 @@ RSpec.describe Gitlab::GroupSearchResults, feature_category: :global_search do let(:query) { 'foo' } let(:scope) { 'milestones' } - include_examples 'search results filtered by archived', 'search_milestones_hide_archived_projects' + include_examples 'search results filtered by archived' end describe '#projects' do diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb deleted file mode 100644 index f4f15cab05a..00000000000 --- a/spec/lib/gitlab/hashed_storage/migrator_spec.rb +++ /dev/null @@ -1,247 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::HashedStorage::Migrator, :redis do - describe '#bulk_schedule_migration' do - it 'schedules job to HashedStorage::MigratorWorker' do - Sidekiq::Testing.fake! do - expect { subject.bulk_schedule_migration(start: 1, finish: 5) }.to change(HashedStorage::MigratorWorker.jobs, :size).by(1) - end - end - end - - describe '#bulk_schedule_rollback' do - it 'schedules job to HashedStorage::RollbackerWorker' do - Sidekiq::Testing.fake! do - expect { subject.bulk_schedule_rollback(start: 1, finish: 5) }.to change(HashedStorage::RollbackerWorker.jobs, :size).by(1) - end - end - end - - describe '#bulk_migrate' do - let(:projects) { create_list(:project, 2, :legacy_storage, :empty_repo) } - let(:ids) { projects.map(&:id) } - - it 'enqueue jobs to HashedStorage::ProjectMigrateWorker' do - Sidekiq::Testing.fake! do - expect { subject.bulk_migrate(start: ids.min, finish: ids.max) }.to change(HashedStorage::ProjectMigrateWorker.jobs, :size).by(2) - end - end - - it 'rescues and log exceptions' do - allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError) - expect { subject.bulk_migrate(start: ids.min, finish: ids.max) }.not_to raise_error - end - - it 'delegates each project in specified range to #migrate' do - projects.each do |project| - expect(subject).to receive(:migrate).with(project) - end - - subject.bulk_migrate(start: ids.min, finish: ids.max) - end - - it 'has all projects migrated and set as writable', :sidekiq_might_not_need_inline do - perform_enqueued_jobs do - subject.bulk_migrate(start: ids.min, finish: ids.max) - end - - projects.each do |project| - project.reload - - expect(project.hashed_storage?(:repository)).to be_truthy - expect(project.repository_read_only?).to be_falsey - end - end - end - - describe '#bulk_rollback' do - let(:projects) { create_list(:project, 2, :empty_repo) } - let(:ids) { projects.map(&:id) } - - it 'enqueue jobs to HashedStorage::ProjectRollbackWorker' do - Sidekiq::Testing.fake! do - expect { subject.bulk_rollback(start: ids.min, finish: ids.max) }.to change(HashedStorage::ProjectRollbackWorker.jobs, :size).by(2) - end - end - - it 'rescues and log exceptions' do - allow_any_instance_of(Project).to receive(:rollback_to_legacy_storage!).and_raise(StandardError) - expect { subject.bulk_rollback(start: ids.min, finish: ids.max) }.not_to raise_error - end - - it 'delegates each project in specified range to #rollback' do - projects.each do |project| - expect(subject).to receive(:rollback).with(project) - end - - subject.bulk_rollback(start: ids.min, finish: ids.max) - end - - it 'has all projects rolledback and set as writable', :sidekiq_might_not_need_inline do - perform_enqueued_jobs do - subject.bulk_rollback(start: ids.min, finish: ids.max) - end - - projects.each do |project| - project.reload - - expect(project.legacy_storage?).to be_truthy - expect(project.repository_read_only?).to be_falsey - end - end - end - - describe '#migrate' do - let(:project) { create(:project, :legacy_storage, :empty_repo) } - - it 'enqueues project migration job' do - Sidekiq::Testing.fake! do - expect { subject.migrate(project) }.to change(HashedStorage::ProjectMigrateWorker.jobs, :size).by(1) - end - end - - it 'rescues and log exceptions' do - allow(project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError) - - expect { subject.migrate(project) }.not_to raise_error - end - - it 'migrates project storage', :sidekiq_might_not_need_inline do - perform_enqueued_jobs do - subject.migrate(project) - end - - expect(project.reload.hashed_storage?(:attachments)).to be_truthy - end - - it 'has migrated project set as writable' do - perform_enqueued_jobs do - subject.migrate(project) - end - - expect(project.reload.repository_read_only?).to be_falsey - end - - context 'when project is already on hashed storage' do - let(:project) { create(:project, :empty_repo) } - - it 'doesnt enqueue any migration job' do - Sidekiq::Testing.fake! do - expect { subject.migrate(project) }.not_to change(HashedStorage::ProjectMigrateWorker.jobs, :size) - end - end - - it 'returns false' do - expect(subject.migrate(project)).to be_falsey - end - end - end - - describe '#rollback' do - let(:project) { create(:project, :empty_repo) } - - it 'enqueues project rollback job' do - Sidekiq::Testing.fake! do - expect { subject.rollback(project) }.to change(HashedStorage::ProjectRollbackWorker.jobs, :size).by(1) - end - end - - it 'rescues and log exceptions' do - allow(project).to receive(:rollback_to_hashed_storage!).and_raise(StandardError) - - expect { subject.rollback(project) }.not_to raise_error - end - - it 'rolls-back project storage', :sidekiq_might_not_need_inline do - perform_enqueued_jobs do - subject.rollback(project) - end - - expect(project.reload.legacy_storage?).to be_truthy - end - - it 'has rolled-back project set as writable' do - perform_enqueued_jobs do - subject.rollback(project) - end - - expect(project.reload.repository_read_only?).to be_falsey - end - - context 'when project is already on legacy storage' do - let(:project) { create(:project, :legacy_storage, :empty_repo) } - - it 'doesnt enqueue any rollback job' do - Sidekiq::Testing.fake! do - expect { subject.rollback(project) }.not_to change(HashedStorage::ProjectRollbackWorker.jobs, :size) - end - end - - it 'returns false' do - expect(subject.rollback(project)).to be_falsey - end - end - end - - describe 'migration_pending?' do - let_it_be(:project) { create(:project, :empty_repo) } - - it 'returns true when there are MigratorWorker jobs scheduled' do - Sidekiq::Testing.disable! do - ::HashedStorage::MigratorWorker.perform_async(1, 5) - - expect(subject.migration_pending?).to be_truthy - end - end - - it 'returns true when there are ProjectMigrateWorker jobs scheduled' do - Sidekiq::Testing.disable! do - ::HashedStorage::ProjectMigrateWorker.perform_async(1) - - expect(subject.migration_pending?).to be_truthy - end - end - - it 'returns false when queues are empty' do - expect(subject.migration_pending?).to be_falsey - end - end - - describe 'rollback_pending?' do - let_it_be(:project) { create(:project, :empty_repo) } - - it 'returns true when there are RollbackerWorker jobs scheduled' do - Sidekiq::Testing.disable! do - ::HashedStorage::RollbackerWorker.perform_async(1, 5) - - expect(subject.rollback_pending?).to be_truthy - end - end - - it 'returns true when there are jobs scheduled' do - Sidekiq::Testing.disable! do - ::HashedStorage::ProjectRollbackWorker.perform_async(1) - - expect(subject.rollback_pending?).to be_truthy - end - end - - it 'returns false when queues are empty' do - expect(subject.rollback_pending?).to be_falsey - end - end - - describe 'abort_rollback!' do - let_it_be(:project) { create(:project, :empty_repo) } - - it 'removes any rollback related scheduled job' do - Sidekiq::Testing.disable! do - ::HashedStorage::RollbackerWorker.perform_async(1, 5) - - expect { subject.abort_rollback! }.to change { subject.rollback_pending? }.from(true).to(false) - end - end - end -end diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb index 9d89167bf81..a9e0c6a3b92 100644 --- a/spec/lib/gitlab/http_spec.rb +++ b/spec/lib/gitlab/http_spec.rb @@ -2,441 +2,102 @@ require 'spec_helper' -RSpec.describe Gitlab::HTTP do - include StubRequests - - let(:default_options) { described_class::DEFAULT_TIMEOUT_OPTIONS } - - context 'when allow_local_requests' do - it 'sends the request to the correct URI' do - stub_full_request('https://example.org:8080', ip_address: '8.8.8.8').to_return(status: 200) - - described_class.get('https://example.org:8080', allow_local_requests: false) - - expect(WebMock).to have_requested(:get, 'https://8.8.8.8:8080').once - end +RSpec.describe Gitlab::HTTP, feature_category: :shared do + let(:default_options) do + { + allow_local_requests: false, + deny_all_requests_except_allowed: false, + dns_rebinding_protection_enabled: true, + outbound_local_requests_allowlist: [], + silent_mode_enabled: false + } end - context 'when not allow_local_requests' do - it 'sends the request to the correct URI' do - stub_full_request('https://example.org:8080') - - described_class.get('https://example.org:8080', allow_local_requests: true) - - expect(WebMock).to have_requested(:get, 'https://8.8.8.9:8080').once - end - end - - context 'when reading the response is too slow' do - before_all do - # Override Net::HTTP to add a delay between sending each response chunk - mocked_http = Class.new(Net::HTTP) do - def request(*) - super do |response| - response.instance_eval do - def read_body(*) - mock_stream = @body.split(' ') - mock_stream.each do |fragment| - sleep 0.002.seconds - - yield fragment if block_given? - end - - @body - end - end - - yield response if block_given? - - response - end - end - end - - @original_net_http = Net.send(:remove_const, :HTTP) - @webmock_net_http = WebMock::HttpLibAdapters::NetHttpAdapter.instance_variable_get(:@webMockNetHTTP) - - Net.send(:const_set, :HTTP, mocked_http) - WebMock::HttpLibAdapters::NetHttpAdapter.instance_variable_set(:@webMockNetHTTP, mocked_http) + describe '.get' do + it 'calls Gitlab::HTTP_V2.get with default options' do + expect(Gitlab::HTTP_V2).to receive(:get).with('/path', default_options) - # Reload Gitlab::NetHttpAdapter - Gitlab.send(:remove_const, :NetHttpAdapter) - load "#{Rails.root}/lib/gitlab/net_http_adapter.rb" + described_class.get('/path') end - before do - stub_const("#{described_class}::DEFAULT_READ_TOTAL_TIMEOUT", 0.001.seconds) - - WebMock.stub_request(:post, /.*/).to_return do - { body: "chunk-1 chunk-2", status: 200 } - end - end - - after(:all) do - Net.send(:remove_const, :HTTP) - Net.send(:const_set, :HTTP, @original_net_http) - WebMock::HttpLibAdapters::NetHttpAdapter.instance_variable_set(:@webMockNetHTTP, @webmock_net_http) - - # Reload Gitlab::NetHttpAdapter - Gitlab.send(:remove_const, :NetHttpAdapter) - load "#{Rails.root}/lib/gitlab/net_http_adapter.rb" - end - - let(:options) { {} } - - subject(:request_slow_responder) { described_class.post('http://example.org', **options) } - - it 'raises an error' do - expect { request_slow_responder }.to raise_error(Gitlab::HTTP::ReadTotalTimeout, /Request timed out after ?([0-9]*[.])?[0-9]+ seconds/) - end - - context 'and timeout option is greater than DEFAULT_READ_TOTAL_TIMEOUT' do - let(:options) { { timeout: 10.seconds } } - - it 'does not raise an error' do - expect { request_slow_responder }.not_to raise_error - end - end - - context 'and stream_body option is truthy' do - let(:options) { { stream_body: true } } - - it 'does not raise an error' do - expect { request_slow_responder }.not_to raise_error - end - end - end - - it 'calls a block' do - WebMock.stub_request(:post, /.*/) - - expect { |b| described_class.post('http://example.org', &b) }.to yield_with_args - end - - describe 'allow_local_requests_from_web_hooks_and_services is' do - before do - WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success') - end - - context 'disabled' do + context 'when passing allow_object_storage:true' do before do - allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_web_hooks_and_services?).and_return(false) - end - - it 'deny requests to localhost' do - expect { described_class.get('http://localhost:3003') }.to raise_error(Gitlab::HTTP::BlockedUrlError) - end - - it 'deny requests to private network' do - expect { described_class.get('http://192.168.1.2:3003') }.to raise_error(Gitlab::HTTP::BlockedUrlError) + allow(ObjectStoreSettings).to receive(:enabled_endpoint_uris).and_return([URI('http://example.com')]) end - context 'if allow_local_requests set to true' do - it 'override the global value and allow requests to localhost or private network' do - stub_full_request('http://localhost:3003') + it 'calls Gitlab::HTTP_V2.get with default options and extra_allowed_uris' do + expect(Gitlab::HTTP_V2).to receive(:get) + .with('/path', default_options.merge(extra_allowed_uris: [URI('http://example.com')])) - expect { described_class.get('http://localhost:3003', allow_local_requests: true) }.not_to raise_error - end + described_class.get('/path', allow_object_storage: true) end end - - context 'enabled' do - before do - allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_web_hooks_and_services?).and_return(true) - end - - it 'allow requests to localhost' do - stub_full_request('http://localhost:3003') - - expect { described_class.get('http://localhost:3003') }.not_to raise_error - end - - it 'allow requests to private network' do - expect { described_class.get('http://192.168.1.2:3003') }.not_to raise_error - end - - context 'if allow_local_requests set to false' do - it 'override the global value and ban requests to localhost or private network' do - expect { described_class.get('http://localhost:3003', allow_local_requests: false) }.to raise_error(Gitlab::HTTP::BlockedUrlError) - end - end - end - end - - describe 'handle redirect loops' do - before do - stub_full_request("http://example.org", method: :any).to_raise(HTTParty::RedirectionTooDeep.new("Redirection Too Deep")) - end - - it 'handles GET requests' do - expect { described_class.get('http://example.org') }.to raise_error(Gitlab::HTTP::RedirectionTooDeep) - end - - it 'handles POST requests' do - expect { described_class.post('http://example.org') }.to raise_error(Gitlab::HTTP::RedirectionTooDeep) - end - - it 'handles PUT requests' do - expect { described_class.put('http://example.org') }.to raise_error(Gitlab::HTTP::RedirectionTooDeep) - end - - it 'handles DELETE requests' do - expect { described_class.delete('http://example.org') }.to raise_error(Gitlab::HTTP::RedirectionTooDeep) - end - - it 'handles HEAD requests' do - expect { described_class.head('http://example.org') }.to raise_error(Gitlab::HTTP::RedirectionTooDeep) - end end - describe 'setting default timeouts' do - before do - stub_full_request('http://example.org', method: :any) - end - - context 'when no timeouts are set' do - it 'sets default open and read and write timeouts' do - expect(described_class).to receive(:httparty_perform_request).with( - Net::HTTP::Get, 'http://example.org', default_options - ).and_call_original - - described_class.get('http://example.org') - end - end - - context 'when :timeout is set' do - it 'does not set any default timeouts' do - expect(described_class).to receive(:httparty_perform_request).with( - Net::HTTP::Get, 'http://example.org', { timeout: 1 } - ).and_call_original - - described_class.get('http://example.org', { timeout: 1 }) - end - end - - context 'when :open_timeout is set' do - it 'only sets default read and write timeout' do - expect(described_class).to receive(:httparty_perform_request).with( - Net::HTTP::Get, 'http://example.org', default_options.merge(open_timeout: 1) - ).and_call_original + describe '.try_get' do + it 'calls .get' do + expect(described_class).to receive(:get).with('/path', {}) - described_class.get('http://example.org', open_timeout: 1) - end + described_class.try_get('/path') end - context 'when :read_timeout is set' do - it 'only sets default open and write timeout' do - expect(described_class).to receive(:httparty_perform_request).with( - Net::HTTP::Get, 'http://example.org', default_options.merge(read_timeout: 1) - ).and_call_original + it 'returns nil when .get raises an error' do + expect(described_class).to receive(:get).and_raise(SocketError) - described_class.get('http://example.org', read_timeout: 1) - end - end - - context 'when :write_timeout is set' do - it 'only sets default open and read timeout' do - expect(described_class).to receive(:httparty_perform_request).with( - Net::HTTP::Put, 'http://example.org', default_options.merge(write_timeout: 1) - ).and_call_original - - described_class.put('http://example.org', write_timeout: 1) - end + expect(described_class.try_get('/path')).to be_nil end end - describe '.try_get' do - let(:path) { 'http://example.org' } + describe '.perform_request' do + context 'when sending a GET request' do + it 'calls Gitlab::HTTP_V2.get with default options' do + expect(Gitlab::HTTP_V2).to receive(:get).with('/path', default_options) - let(:extra_log_info_proc) do - proc do |error, url, options| - { klass: error.class, url: url, options: options } + described_class.perform_request(Net::HTTP::Get, '/path', {}) end end - let(:request_options) do - default_options.merge({ - verify: false, - basic_auth: { username: 'user', password: 'pass' } - }) - end - - described_class::HTTP_ERRORS.each do |exception_class| - context "with #{exception_class}" do - let(:klass) { exception_class } - - context 'with path' do - before do - expect(described_class).to receive(:httparty_perform_request) - .with(Net::HTTP::Get, path, default_options) - .and_raise(klass) - end - - it 'handles requests without extra_log_info' do - expect(Gitlab::ErrorTracking) - .to receive(:log_exception) - .with(instance_of(klass), {}) - - expect(described_class.try_get(path)).to be_nil - end - - it 'handles requests with extra_log_info as hash' do - expect(Gitlab::ErrorTracking) - .to receive(:log_exception) - .with(instance_of(klass), { a: :b }) - - expect(described_class.try_get(path, extra_log_info: { a: :b })).to be_nil - end - - it 'handles requests with extra_log_info as proc' do - expect(Gitlab::ErrorTracking) - .to receive(:log_exception) - .with(instance_of(klass), { url: path, klass: klass, options: {} }) - - expect(described_class.try_get(path, extra_log_info: extra_log_info_proc)).to be_nil - end - end - - context 'with path and options' do - before do - expect(described_class).to receive(:httparty_perform_request) - .with(Net::HTTP::Get, path, request_options) - .and_raise(klass) - end - - it 'handles requests without extra_log_info' do - expect(Gitlab::ErrorTracking) - .to receive(:log_exception) - .with(instance_of(klass), {}) - - expect(described_class.try_get(path, request_options)).to be_nil - end - - it 'handles requests with extra_log_info as hash' do - expect(Gitlab::ErrorTracking) - .to receive(:log_exception) - .with(instance_of(klass), { a: :b }) - - expect(described_class.try_get(path, **request_options, extra_log_info: { a: :b })).to be_nil - end - - it 'handles requests with extra_log_info as proc' do - expect(Gitlab::ErrorTracking) - .to receive(:log_exception) - .with(instance_of(klass), { klass: klass, url: path, options: request_options }) - - expect(described_class.try_get(path, **request_options, extra_log_info: extra_log_info_proc)).to be_nil - end - end - - context 'with path, options, and block' do - let(:block) do - proc {} - end - - before do - expect(described_class).to receive(:httparty_perform_request) - .with(Net::HTTP::Get, path, request_options, &block) - .and_raise(klass) - end - - it 'handles requests without extra_log_info' do - expect(Gitlab::ErrorTracking) - .to receive(:log_exception) - .with(instance_of(klass), {}) - - expect(described_class.try_get(path, request_options, &block)).to be_nil - end - - it 'handles requests with extra_log_info as hash' do - expect(Gitlab::ErrorTracking) - .to receive(:log_exception) - .with(instance_of(klass), { a: :b }) - - expect(described_class.try_get(path, **request_options, extra_log_info: { a: :b }, &block)).to be_nil - end - - it 'handles requests with extra_log_info as proc' do - expect(Gitlab::ErrorTracking) - .to receive(:log_exception) - .with(instance_of(klass), { klass: klass, url: path, options: request_options }) - - expect(described_class.try_get(path, **request_options, extra_log_info: extra_log_info_proc, &block)).to be_nil - end - end + context 'when sending a LOCK request' do + it 'raises ArgumentError' do + expect do + described_class.perform_request(Net::HTTP::Lock, '/path', {}) + end.to raise_error(ArgumentError, "Unsupported HTTP method: 'lock'.") end end end - describe 'silent mode', feature_category: :geo_replication do + context 'when the FF use_gitlab_http_v2 is disabled' do before do - stub_full_request("http://example.org", method: :any) - stub_application_setting(silent_mode_enabled: silent_mode) + stub_feature_flags(use_gitlab_http_v2: false) end - context 'when silent mode is enabled' do - let(:silent_mode) { true } - - it 'allows GET requests' do - expect { described_class.get('http://example.org') }.not_to raise_error - end + describe '.get' do + it 'calls Gitlab::LegacyHTTP.get with default options' do + expect(Gitlab::LegacyHTTP).to receive(:get).with('/path', {}) - it 'allows HEAD requests' do - expect { described_class.head('http://example.org') }.not_to raise_error - end - - it 'allows OPTIONS requests' do - expect { described_class.options('http://example.org') }.not_to raise_error - end - - it 'blocks POST requests' do - expect { described_class.post('http://example.org') }.to raise_error(Gitlab::HTTP::SilentModeBlockedError) - end - - it 'blocks PUT requests' do - expect { described_class.put('http://example.org') }.to raise_error(Gitlab::HTTP::SilentModeBlockedError) - end - - it 'blocks DELETE requests' do - expect { described_class.delete('http://example.org') }.to raise_error(Gitlab::HTTP::SilentModeBlockedError) - end - - it 'logs blocked requests' do - expect(::Gitlab::AppJsonLogger).to receive(:info).with( - message: "Outbound HTTP request blocked", - outbound_http_request_method: 'Net::HTTP::Post', - silent_mode_enabled: true - ) - - expect { described_class.post('http://example.org') }.to raise_error(Gitlab::HTTP::SilentModeBlockedError) + described_class.get('/path') end end - context 'when silent mode is disabled' do - let(:silent_mode) { false } - - it 'allows GET requests' do - expect { described_class.get('http://example.org') }.not_to raise_error - end + describe '.try_get' do + it 'calls .get' do + expect(described_class).to receive(:get).with('/path', {}) - it 'allows HEAD requests' do - expect { described_class.head('http://example.org') }.not_to raise_error + described_class.try_get('/path') end - it 'allows OPTIONS requests' do - expect { described_class.options('http://example.org') }.not_to raise_error - end + it 'returns nil when .get raises an error' do + expect(described_class).to receive(:get).and_raise(SocketError) - it 'blocks POST requests' do - expect { described_class.post('http://example.org') }.not_to raise_error + expect(described_class.try_get('/path')).to be_nil end + end - it 'blocks PUT requests' do - expect { described_class.put('http://example.org') }.not_to raise_error - end + describe '.perform_request' do + it 'calls Gitlab::LegacyHTTP.perform_request with default options' do + expect(Gitlab::LegacyHTTP).to receive(:perform_request).with(Net::HTTP::Get, '/path', {}) - it 'blocks DELETE requests' do - expect { described_class.delete('http://example.org') }.not_to raise_error + described_class.perform_request(Net::HTTP::Get, '/path', {}) end end end diff --git a/spec/lib/gitlab/i18n_spec.rb b/spec/lib/gitlab/i18n_spec.rb index ee92831922d..fdd868acbb1 100644 --- a/spec/lib/gitlab/i18n_spec.rb +++ b/spec/lib/gitlab/i18n_spec.rb @@ -62,4 +62,18 @@ RSpec.describe Gitlab::I18n, feature_category: :internationalization do end end end + + describe '.trimmed_language_name' do + it 'trims the language name', :aggregate_failures do + expect(described_class.trimmed_language_name('en')).to eq('English') + expect(described_class.trimmed_language_name('bg')).to eq('Bulgarian') + expect(described_class.trimmed_language_name('id_ID')).to eq('Indonesian') + expect(described_class.trimmed_language_name('nb_NO')).to eq('Norwegian (Bokmål)') + expect(described_class.trimmed_language_name('zh_HK')).to eq('Chinese, Traditional (Hong Kong)') + end + + it 'return nil for unknown language code' do + expect(described_class.trimmed_language_name('_invalid_code_')).to be_nil + end + end end diff --git a/spec/lib/gitlab/import/errors_spec.rb b/spec/lib/gitlab/import/errors_spec.rb index 21d96601609..3b45af0618b 100644 --- a/spec/lib/gitlab/import/errors_spec.rb +++ b/spec/lib/gitlab/import/errors_spec.rb @@ -39,7 +39,6 @@ RSpec.describe Gitlab::Import::Errors, feature_category: :importers do "Noteable can't be blank", "Author can't be blank", "Project does not match noteable project", - "Namespace can't be blank", "User can't be blank", "Name is not a valid emoji name" ) diff --git a/spec/lib/gitlab/import/import_failure_service_spec.rb b/spec/lib/gitlab/import/import_failure_service_spec.rb index eb71b307b8d..a4682a9495e 100644 --- a/spec/lib/gitlab/import/import_failure_service_spec.rb +++ b/spec/lib/gitlab/import/import_failure_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures do +RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures, feature_category: :importers do let_it_be(:import_type) { 'import_type' } let_it_be(:project) { create(:project, :import_started, import_type: import_type) } @@ -10,15 +10,18 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures do let(:import_state) { nil } let(:fail_import) { false } let(:metrics) { false } + let(:external_identifiers) { {} } + let(:project_id) { project.id } let(:arguments) do { - project_id: project.id, + project_id: project_id, error_source: 'SomeImporter', exception: exception, fail_import: fail_import, metrics: metrics, - import_state: import_state + import_state: import_state, + external_identifiers: external_identifiers } end @@ -33,7 +36,8 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures do project_id: '_project_id_', error_source: '_error_source_', fail_import: '_fail_import_', - metrics: '_metrics_' + metrics: '_metrics_', + external_identifiers: { id: 1 } } end @@ -59,7 +63,7 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures do subject(:service) { described_class.new(**arguments) } shared_examples 'logs the exception and fails the import' do - it 'when the failure does not abort the import' do + specify do expect(Gitlab::ErrorTracking) .to receive(:track_exception) .with( @@ -67,7 +71,8 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures do { project_id: project.id, import_type: import_type, - source: 'SomeImporter' + source: 'SomeImporter', + external_identifiers: external_identifiers } ) @@ -76,10 +81,11 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures do .with( { message: 'importer failed', - 'error.message': 'some error', + 'exception.message': 'some error', project_id: project.id, import_type: import_type, - source: 'SomeImporter' + source: 'SomeImporter', + external_identifiers: external_identifiers } ) @@ -95,7 +101,7 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures do end shared_examples 'logs the exception and does not fail the import' do - it 'when the failure does not abort the import' do + specify do expect(Gitlab::ErrorTracking) .to receive(:track_exception) .with( @@ -103,7 +109,8 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures do { project_id: project.id, import_type: import_type, - source: 'SomeImporter' + source: 'SomeImporter', + external_identifiers: external_identifiers } ) @@ -112,10 +119,11 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures do .with( { message: 'importer failed', - 'error.message': 'some error', + 'exception.message': 'some error', project_id: project.id, import_type: import_type, - source: 'SomeImporter' + source: 'SomeImporter', + external_identifiers: external_identifiers } ) @@ -159,6 +167,7 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures do end context 'when using the import_state as reference' do + let(:project_id) { nil } let(:import_state) { project.import_state } context 'when it fails the import' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index d337a37c69f..cd899a79451 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -172,7 +172,6 @@ project_members: - user - source - project -- member_task - member_namespace - member_role member_roles: @@ -250,6 +249,7 @@ merge_requests: - created_environments - predictions - user_agent_detail +- scan_result_policy_violations external_pull_requests: - project merge_request_diff: @@ -668,6 +668,7 @@ project: - statistics - container_repositories - container_registry_data_repair_detail +- container_registry_protection_rules - uploads - file_uploads - import_state @@ -823,10 +824,12 @@ project: - design_management_repository_state - compliance_standards_adherence - scan_result_policy_reads +- scan_result_policy_violations - project_state - security_policy_bots - target_branch_rules - organization +- dora_performance_scores award_emoji: - awardable - user diff --git a/spec/lib/gitlab/import_export/attributes_finder_spec.rb b/spec/lib/gitlab/import_export/attributes_finder_spec.rb index f12cbe4f82f..fd9d609992d 100644 --- a/spec/lib/gitlab/import_export/attributes_finder_spec.rb +++ b/spec/lib/gitlab/import_export/attributes_finder_spec.rb @@ -131,19 +131,19 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder, feature_category: :import end it 'generates the correct hash for a relation with included attributes' do - setup_yaml(tree: { project: [:issues] }, - included_attributes: { issues: [:name, :description] }) + setup_yaml( + tree: { project: [:issues] }, + included_attributes: { issues: [:name, :description] } + ) is_expected.to match( - include: [{ issues: { include: [], - only: [:name, :description] } }], + include: [{ issues: { include: [], only: [:name, :description] } }], preload: { issues: nil } ) end it 'generates the correct hash for a relation with excluded attributes' do - setup_yaml(tree: { project: [:issues] }, - excluded_attributes: { issues: [:name] }) + setup_yaml(tree: { project: [:issues] }, excluded_attributes: { issues: [:name] }) is_expected.to match( include: [{ issues: { except: [:name], @@ -153,25 +153,23 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder, feature_category: :import end it 'generates the correct hash for a relation with both excluded and included attributes' do - setup_yaml(tree: { project: [:issues] }, - excluded_attributes: { issues: [:name] }, - included_attributes: { issues: [:description] }) + setup_yaml( + tree: { project: [:issues] }, + excluded_attributes: { issues: [:name] }, + included_attributes: { issues: [:description] } + ) is_expected.to match( - include: [{ issues: { except: [:name], - include: [], - only: [:description] } }], + include: [{ issues: { except: [:name], include: [], only: [:description] } }], preload: { issues: nil } ) end it 'generates the correct hash for a relation with custom methods' do - setup_yaml(tree: { project: [:issues] }, - methods: { issues: [:name] }) + setup_yaml(tree: { project: [:issues] }, methods: { issues: [:name] }) is_expected.to match( - include: [{ issues: { include: [], - methods: [:name] } }], + include: [{ issues: { include: [], methods: [:name] } }], preload: { issues: nil } ) end diff --git a/spec/lib/gitlab/import_export/base/object_builder_spec.rb b/spec/lib/gitlab/import_export/base/object_builder_spec.rb index 38c3b23db36..3c69a6a7746 100644 --- a/spec/lib/gitlab/import_export/base/object_builder_spec.rb +++ b/spec/lib/gitlab/import_export/base/object_builder_spec.rb @@ -4,11 +4,13 @@ require 'spec_helper' RSpec.describe Gitlab::ImportExport::Base::ObjectBuilder do let(:project) do - create(:project, :repository, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project') + create( + :project, :repository, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project' + ) end let(:klass) { Milestone } diff --git a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb index 4ef8f4b5d76..5e63804c51c 100644 --- a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb @@ -11,14 +11,16 @@ RSpec.describe Gitlab::ImportExport::Base::RelationFactory do let(:excluded_keys) { [] } subject do - described_class.create(relation_sym: relation_sym, # rubocop:disable Rails/SaveBang - relation_hash: relation_hash, - relation_index: 1, - object_builder: Gitlab::ImportExport::Project::ObjectBuilder, - members_mapper: members_mapper, - user: user, - importable: project, - excluded_keys: excluded_keys) + described_class.create( # rubocop:disable Rails/SaveBang + relation_sym: relation_sym, + relation_hash: relation_hash, + relation_index: 1, + object_builder: Gitlab::ImportExport::Project::ObjectBuilder, + members_mapper: members_mapper, + user: user, + importable: project, + excluded_keys: excluded_keys + ) end describe '#create' do diff --git a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb index 5ef9eb78d3b..144617055ab 100644 --- a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb @@ -12,9 +12,7 @@ RSpec.describe Gitlab::ImportExport::DesignRepoRestorer do let(:bundler) { Gitlab::ImportExport::DesignRepoSaver.new(exportable: project_with_design_repo, shared: shared) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.design_repo_bundle_filename) } let(:restorer) do - described_class.new(path_to_bundle: bundle_path, - shared: shared, - importable: project) + described_class.new(path_to_bundle: bundle_path, shared: shared, importable: project) end before do diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb index 02419267f0e..dfc7202194d 100644 --- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb +++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb @@ -217,17 +217,18 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer, :with_license, feature_ release = create(:release) group = create(:group) - project = create(:project, - :public, - :repository, - :issues_disabled, - :wiki_enabled, - :builds_private, - description: 'description', - releases: [release], - group: group, - approvals_before_merge: 1 - ) + project = create( + :project, + :public, + :repository, + :issues_disabled, + :wiki_enabled, + :builds_private, + description: 'description', + releases: [release], + group: group, + approvals_before_merge: 1 + ) issue = create(:issue, assignees: [user], project: project) snippet = create(:project_snippet, project: project) @@ -249,10 +250,7 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer, :with_license, feature_ create(:discussion_note, noteable: issue, project: project) create(:note, noteable: merge_request, project: project) create(:note, noteable: snippet, project: project) - create(:note_on_commit, - author: user, - project: project, - commit_id: ci_build.pipeline.sha) + create(:note_on_commit, author: user, project: project, commit_id: ci_build.pipeline.sha) create(:resource_label_event, label: project_label, issue: issue) create(:resource_label_event, label: group_label, merge_request: merge_request) diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb index 53d205850c8..a98080b682b 100644 --- a/spec/lib/gitlab/import_export/importer_spec.rb +++ b/spec/lib/gitlab/import_export/importer_spec.rb @@ -80,7 +80,7 @@ RSpec.describe Gitlab::ImportExport::Importer do context 'with sample_data_template' do it 'initializes the Sample::TreeRestorer' do - project.create_or_update_import_data(data: { sample_data: true }) + project.build_or_assign_import_data(data: { sample_data: true }) expect(Gitlab::ImportExport::Project::Sample::TreeRestorer).to receive(:new).and_call_original @@ -112,7 +112,7 @@ RSpec.describe Gitlab::ImportExport::Importer do end it 'sets the correct visibility_level when visibility level is a string' do - project.create_or_update_import_data( + project.build_or_assign_import_data( data: { override_params: { visibility_level: Gitlab::VisibilityLevel::PRIVATE.to_s } } ) diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb index 3ca9f727033..17d416b0f0a 100644 --- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -16,10 +16,12 @@ RSpec.describe Gitlab::ImportExport::MergeRequestParser do let(:diff_head_sha) { SecureRandom.hex(20) } let(:parsed_merge_request) do - described_class.new(project, - diff_head_sha, - merge_request, - merge_request.as_json).parse! + described_class.new( + project, + diff_head_sha, + merge_request, + merge_request.as_json + ).parse! end after do diff --git a/spec/lib/gitlab/import_export/project/export_task_spec.rb b/spec/lib/gitlab/import_export/project/export_task_spec.rb index 0837874526a..8eb3c76302a 100644 --- a/spec/lib/gitlab/import_export/project/export_task_spec.rb +++ b/spec/lib/gitlab/import_export/project/export_task_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rake_helper' +require 'spec_helper' RSpec.describe Gitlab::ImportExport::Project::ExportTask, :silence_stdout, feature_category: :importers do let_it_be(:username) { 'root' } diff --git a/spec/lib/gitlab/import_export/project/import_task_spec.rb b/spec/lib/gitlab/import_export/project/import_task_spec.rb index 693f1984ce8..d38905992d9 100644 --- a/spec/lib/gitlab/import_export/project/import_task_spec.rb +++ b/spec/lib/gitlab/import_export/project/import_task_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rake_helper' +require 'spec_helper' RSpec.describe Gitlab::ImportExport::Project::ImportTask, :request_store, :silence_stdout, feature_category: :importers do let(:username) { 'root' } diff --git a/spec/lib/gitlab/import_export/project/object_builder_spec.rb b/spec/lib/gitlab/import_export/project/object_builder_spec.rb index 43794ce01a3..20e176bf6fd 100644 --- a/spec/lib/gitlab/import_export/project/object_builder_spec.rb +++ b/spec/lib/gitlab/import_export/project/object_builder_spec.rb @@ -6,12 +6,15 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do let!(:group) { create(:group, :private) } let!(:subgroup) { create(:group, :private, parent: group) } let!(:project) do - create(:project, :repository, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project', - group: subgroup) + create( + :project, + :repository, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: subgroup + ) end let(:lru_cache) { subject.send(:lru_cache) } @@ -19,10 +22,7 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do context 'request store is not active' do subject do - described_class.new(Label, - 'title' => 'group label', - 'project' => project, - 'group' => project.group) + described_class.new(Label, 'title' => 'group label', 'project' => project, 'group' => project.group) end it 'ignore cache initialize' do @@ -33,10 +33,7 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do context 'request store is active', :request_store do subject do - described_class.new(Label, - 'title' => 'group label', - 'project' => project, - 'group' => project.group) + described_class.new(Label, 'title' => 'group label', 'project' => project, 'group' => project.group) end it 'initialize cache in memory' do @@ -71,27 +68,33 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do it 'finds the existing group label' do group_label = create(:group_label, name: 'group label', group: project.group) - expect(described_class.build(Label, - 'title' => 'group label', - 'project' => project, - 'group' => project.group)).to eq(group_label) + expect(described_class.build( + Label, + 'title' => 'group label', + 'project' => project, + 'group' => project.group + )).to eq(group_label) end it 'finds the existing group label in root ancestor' do group_label = create(:group_label, name: 'group label', group: group) - expect(described_class.build(Label, - 'title' => 'group label', - 'project' => project, - 'group' => group)).to eq(group_label) + expect(described_class.build( + Label, + 'title' => 'group label', + 'project' => project, + 'group' => group + )).to eq(group_label) end it 'creates a new project label' do - label = described_class.build(Label, - 'title' => 'group label', - 'project' => project, - 'group' => project.group, - 'group_id' => project.group.id) + label = described_class.build( + Label, + 'title' => 'group label', + 'project' => project, + 'group' => project.group, + 'group_id' => project.group.id + ) expect(label.persisted?).to be true expect(label).to be_an_instance_of(ProjectLabel) @@ -103,26 +106,32 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do it 'finds the existing group milestone' do milestone = create(:milestone, name: 'group milestone', group: project.group) - expect(described_class.build(Milestone, - 'title' => 'group milestone', - 'project' => project, - 'group' => project.group)).to eq(milestone) + expect(described_class.build( + Milestone, + 'title' => 'group milestone', + 'project' => project, + 'group' => project.group + )).to eq(milestone) end it 'finds the existing group milestone in root ancestor' do milestone = create(:milestone, name: 'group milestone', group: group) - expect(described_class.build(Milestone, - 'title' => 'group milestone', - 'project' => project, - 'group' => group)).to eq(milestone) + expect(described_class.build( + Milestone, + 'title' => 'group milestone', + 'project' => project, + 'group' => group + )).to eq(milestone) end it 'creates a new milestone' do - milestone = described_class.build(Milestone, - 'title' => 'group milestone', - 'project' => project, - 'group' => project.group) + milestone = described_class.build( + Milestone, + 'title' => 'group milestone', + 'project' => project, + 'group' => project.group + ) expect(milestone.persisted?).to be true end @@ -132,12 +141,14 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do clashing_iid = 1 create(:milestone, iid: clashing_iid, project: project) - milestone = described_class.build(Milestone, - 'iid' => clashing_iid, - 'title' => 'milestone', - 'project' => project, - 'group' => nil, - 'group_id' => nil) + milestone = described_class.build( + Milestone, + 'iid' => clashing_iid, + 'title' => 'milestone', + 'project' => project, + 'group' => nil, + 'group_id' => nil + ) expect(milestone.persisted?).to be true expect(Milestone.count).to eq(2) @@ -173,34 +184,45 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do context 'merge_request' do it 'finds the existing merge_request' do - merge_request = create(:merge_request, title: 'MergeRequest', iid: 7, target_project: project, source_project: project) - expect(described_class.build(MergeRequest, - 'title' => 'MergeRequest', - 'source_project_id' => project.id, - 'target_project_id' => project.id, - 'source_branch' => 'SourceBranch', - 'iid' => 7, - 'target_branch' => 'TargetBranch', - 'author_id' => project.creator.id)).to eq(merge_request) + merge_request = create( + :merge_request, + title: 'MergeRequest', + iid: 7, + target_project: project, + source_project: project + ) + + expect(described_class.build( + MergeRequest, + 'title' => 'MergeRequest', + 'source_project_id' => project.id, + 'target_project_id' => project.id, + 'source_branch' => 'SourceBranch', + 'iid' => 7, + 'target_branch' => 'TargetBranch', + 'author_id' => project.creator.id + )).to eq(merge_request) end it 'creates a new merge_request' do - merge_request = described_class.build(MergeRequest, - 'title' => 'MergeRequest', - 'iid' => 8, - 'source_project_id' => project.id, - 'target_project_id' => project.id, - 'source_branch' => 'SourceBranch', - 'target_branch' => 'TargetBranch', - 'author_id' => project.creator.id) + merge_request = described_class.build( + MergeRequest, + 'title' => 'MergeRequest', + 'iid' => 8, + 'source_project_id' => project.id, + 'target_project_id' => project.id, + 'source_branch' => 'SourceBranch', + 'target_branch' => 'TargetBranch', + 'author_id' => project.creator.id + ) + expect(merge_request.persisted?).to be true end end context 'merge request diff commit users' do it 'finds the existing user' do - user = MergeRequest::DiffCommitUser - .find_or_create('Alice', 'alice@example.com') + user = MergeRequest::DiffCommitUser.find_or_create('Alice', 'alice@example.com') found = described_class.build( MergeRequest::DiffCommitUser, diff --git a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb index 5e9fed32c4e..99959daa1fa 100644 --- a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb @@ -335,17 +335,19 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_ context 'pipeline_schedule' do let(:relation_sym) { :pipeline_schedules } + let(:value) { true } let(:relation_hash) do { - "id": 3, - "created_at": "2016-07-22T08:55:44.161Z", - "updated_at": "2016-07-22T08:55:44.161Z", - "description": "pipeline schedule", - "ref": "main", - "cron": "0 4 * * 0", - "cron_timezone": "UTC", - "active": value, - "project_id": project.id + 'id' => 3, + 'created_at' => '2016-07-22T08:55:44.161Z', + 'updated_at' => '2016-07-22T08:55:44.161Z', + 'description' => 'pipeline schedule', + 'ref' => 'main', + 'cron' => '0 4 * * 0', + 'cron_timezone' => 'UTC', + 'active' => value, + 'project_id' => project.id, + 'owner_id' => non_existing_record_id } end @@ -360,6 +362,10 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_ end end end + + it 'sets importer user as owner' do + expect(created_object.owner_id).to eq(importer_user.id) + end end # `project_id`, `described_class.USER_REFERENCES`, noteable_id, target_id, and some project IDs are already diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index b0bc31e366e..14af3028a6e 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -449,6 +449,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i expect(pipeline_schedule.cron).to eq('0 4 * * 0') expect(pipeline_schedule.cron_timezone).to eq('UTC') expect(pipeline_schedule.active).to eq(false) + expect(pipeline_schedule.owner_id).to eq(@user.id) end end @@ -853,12 +854,14 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i end let!(:project) do - create(:project, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project', - group: group) + create( + :project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: group + ) end before do @@ -889,12 +892,14 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i context 'with existing group models' do let(:group) { create(:group).tap { |g| g.add_maintainer(user) } } let!(:project) do - create(:project, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project', - group: group) + create( + :project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: group + ) end before do @@ -925,12 +930,14 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i context 'with clashing milestones on IID' do let(:group) { create(:group).tap { |g| g.add_maintainer(user) } } let!(:project) do - create(:project, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project', - group: group) + create( + :project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: group + ) end before do @@ -1142,8 +1149,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i let_it_be(:user) { create(:admin, email: 'user_1@gitlabexample.com') } let_it_be(:second_user) { create(:user, email: 'user_2@gitlabexample.com') } let_it_be(:project) do - create(:project, :builds_disabled, :issues_disabled, - { name: 'project', path: 'project' }) + create(:project, :builds_disabled, :issues_disabled, { name: 'project', path: 'project' }) end let(:shared) { project.import_export_shared } diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb index abb781b277b..1bf1e5b47e1 100644 --- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb @@ -309,8 +309,8 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_ context 'with pipeline schedules' do let(:relation_name) { :pipeline_schedules } - it 'has no owner_id' do - expect(subject.first['owner_id']).to be_nil + it 'has owner_id' do + expect(subject.first['owner_id']).to be_present end end end diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index 3da7af7509e..3c540eb45c9 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do subject { described_class.new(path_to_bundle: bundle_path, shared: shared, importable: project) } after do - Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path) + project.repository.remove end it 'restores the repo successfully', :aggregate_failures do @@ -66,7 +66,7 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do subject { described_class.new(path_to_bundle: bundle_path, shared: shared, importable: ProjectWiki.new(project)) } after do - Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path) + project.wiki.repository.remove end it 'restores the wiki repo successfully', :aggregate_failures do diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb index 408ed3a2176..37a59a68188 100644 --- a/spec/lib/gitlab/import_export/shared_spec.rb +++ b/spec/lib/gitlab/import_export/shared_spec.rb @@ -74,12 +74,12 @@ RSpec.describe Gitlab::ImportExport::Shared do expect(Gitlab::ErrorTracking) .to receive(:track_exception) .with(error, hash_including( - importer: 'Import/Export', - project_id: project.id, - project_name: project.name, - project_path: project.full_path, - import_jid: import_state.jid - )) + importer: 'Import/Export', + project_id: project.id, + project_name: project.name, + project_path: project.full_path, + import_jid: import_state.jid + )) subject.error(error) end diff --git a/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb index 2f39cb560d0..d7b1b180e2e 100644 --- a/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb @@ -10,10 +10,12 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do let(:shared) { project.import_export_shared } let(:exporter) { Gitlab::ImportExport::SnippetsRepoSaver.new(project: project, shared: shared, current_user: user) } let(:restorer) do - described_class.new(user: user, - shared: shared, - snippet: snippet, - path_to_bundle: snippet_bundle_path) + described_class.new( + user: user, + shared: shared, + snippet: snippet, + path_to_bundle: snippet_bundle_path + ) end after do diff --git a/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb index e348e8f7991..4a9a01475cb 100644 --- a/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb @@ -14,9 +14,7 @@ RSpec.describe Gitlab::ImportExport::SnippetsRepoRestorer, :clean_gitlab_redis_r let(:bundle_dir) { ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path) } let(:service) { instance_double(Gitlab::ImportExport::SnippetRepoRestorer) } let(:restorer) do - described_class.new(user: user, - shared: shared, - project: project) + described_class.new(user: user, shared: shared, project: project) end after do diff --git a/spec/lib/gitlab/internal_events/event_definitions_spec.rb b/spec/lib/gitlab/internal_events/event_definitions_spec.rb index 924845504ca..a00d1ab5ecb 100644 --- a/spec/lib/gitlab/internal_events/event_definitions_spec.rb +++ b/spec/lib/gitlab/internal_events/event_definitions_spec.rb @@ -3,7 +3,9 @@ require "spec_helper" RSpec.describe Gitlab::InternalEvents::EventDefinitions, feature_category: :product_analytics_data_management do - after(:all) do + around do |example| + described_class.instance_variable_set(:@events, nil) + example.run described_class.instance_variable_set(:@events, nil) end @@ -20,7 +22,6 @@ RSpec.describe Gitlab::InternalEvents::EventDefinitions, feature_category: :prod let(:events2) { { 'event2' => nil } } before do - allow(Gitlab::Usage::MetricDefinition).to receive(:metric_definitions_changed?).and_return(true) allow(Gitlab::Usage::MetricDefinition).to receive(:all).and_return([definition1, definition2]) allow(definition1).to receive(:available?).and_return(true) allow(definition2).to receive(:available?).and_return(true) @@ -58,9 +59,8 @@ RSpec.describe Gitlab::InternalEvents::EventDefinitions, feature_category: :prod end context 'when event does not have unique property' do - it 'unique fails' do - expect { described_class.unique_property('event1') } - .to raise_error(described_class::InvalidMetricConfiguration, /Unique property not defined for/) + it 'returns nil' do + expect(described_class.unique_property('event1')).to be_nil end end end diff --git a/spec/lib/gitlab/internal_events_spec.rb b/spec/lib/gitlab/internal_events_spec.rb index c2615e0f22c..20625add292 100644 --- a/spec/lib/gitlab/internal_events_spec.rb +++ b/spec/lib/gitlab/internal_events_spec.rb @@ -9,6 +9,8 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana before do allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) + allow(redis).to receive(:incr) + allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis) allow(Gitlab::Tracking).to receive(:tracker).and_return(fake_snowplow) allow(Gitlab::InternalEvents::EventDefinitions).to receive(:unique_property).and_return(:user) allow(fake_snowplow).to receive(:event) @@ -19,6 +21,12 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana .with(event_name, values: unique_value) end + def expect_redis_tracking(event_name) + expect(redis).to have_received(:incr) do |redis_key| + expect(redis_key).to end_with(event_name) + end + end + def expect_snowplow_tracking(event_name) service_ping_context = Gitlab::Tracking::ServicePingContext .new(data_source: :redis_hll, event: event_name) @@ -39,14 +47,16 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana let_it_be(:project) { build(:project, id: 2) } let_it_be(:namespace) { project.namespace } + let(:redis) { instance_double('Redis') } let(:fake_snowplow) { instance_double(Gitlab::Tracking::Destinations::Snowplow) } let(:event_name) { 'g_edit_by_web_ide' } let(:unique_value) { user.id } - it 'updates both RedisHLL and Snowplow', :aggregate_failures do + it 'updates Redis, RedisHLL and Snowplow', :aggregate_failures do params = { user: user, project: project, namespace: namespace } described_class.track_event(event_name, **params) + expect_redis_tracking(event_name) expect_redis_hll_tracking(event_name) expect_snowplow_tracking(event_name) # Add test for arguments end @@ -73,9 +83,10 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana expect { described_class.track_event('unknown_event') }.not_to raise_error end - it 'logs error on missing property' do + it 'logs error on missing property', :aggregate_failures do expect { described_class.track_event(event_name, merge_request_id: 1) }.not_to raise_error + expect_redis_tracking(event_name) expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception) .with(described_class::InvalidPropertyError, event_name: event_name, kwargs: { merge_request_id: 1 }) end @@ -86,9 +97,10 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana .and_raise(Gitlab::InternalEvents::EventDefinitions::InvalidMetricConfiguration) end - it 'fails on missing unique property' do + it 'logs error on missing unique property', :aggregate_failures do expect { described_class.track_event(event_name, merge_request_id: 1) }.not_to raise_error + expect_redis_tracking(event_name) expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception) end end @@ -107,6 +119,7 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana it 'is used when logging to RedisHLL', :aggregate_failures do described_class.track_event(event_name, user: user, project: project) + expect_redis_tracking(event_name) expect_redis_hll_tracking(event_name) expect_snowplow_tracking(event_name) end @@ -120,13 +133,42 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana end end - context 'when method does not exist on property' do + context 'when method does not exist on property', :aggregate_failures do it 'logs error on missing method' do expect { described_class.track_event(event_name, project: "a_string") }.not_to raise_error + expect_redis_tracking(event_name) expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception) .with(described_class::InvalidMethodError, event_name: event_name, kwargs: { project: 'a_string' }) end end + + context 'when send_snowplow_event is false' do + it 'logs to Redis and RedisHLL but not Snowplow' do + described_class.track_event(event_name, send_snowplow_event: false, user: user, project: project) + + expect_redis_tracking(event_name) + expect_redis_hll_tracking(event_name) + expect(fake_snowplow).not_to have_received(:event) + end + end + end + + context 'when unique key is not defined' do + let(:event_name) { 'p_ci_templates_terraform_base_latest' } + + before do + allow(Gitlab::InternalEvents::EventDefinitions).to receive(:unique_property) + .with(event_name) + .and_return(nil) + end + + it 'logs to Redis and Snowplow but not RedisHLL', :aggregate_failures do + described_class.track_event(event_name, user: user, project: project) + + expect_redis_tracking(event_name) + expect_snowplow_tracking(event_name) + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to have_received(:track_event) + end end end diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index d0b89afccdc..5fcbecfe6e1 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -106,7 +106,7 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do describe '#initialize' do shared_examples 'local address' do it 'blocks local addresses' do - expect { client }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError) + expect { client }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end context 'when local requests are allowed' do @@ -136,7 +136,7 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do let(:api_url) { 'ssh://192.168.1.2' } it 'raises an error' do - expect { client }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError) + expect { client }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end diff --git a/spec/lib/gitlab/legacy_http_spec.rb b/spec/lib/gitlab/legacy_http_spec.rb new file mode 100644 index 00000000000..07a30b194b6 --- /dev/null +++ b/spec/lib/gitlab/legacy_http_spec.rb @@ -0,0 +1,448 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::LegacyHTTP, feature_category: :shared do + include StubRequests + + let(:default_options) { Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS } + + context 'when allow_local_requests' do + it 'sends the request to the correct URI' do + stub_full_request('https://example.org:8080', ip_address: '8.8.8.8').to_return(status: 200) + + described_class.get('https://example.org:8080', allow_local_requests: false) + + expect(WebMock).to have_requested(:get, 'https://8.8.8.8:8080').once + end + end + + context 'when not allow_local_requests' do + it 'sends the request to the correct URI' do + stub_full_request('https://example.org:8080') + + described_class.get('https://example.org:8080', allow_local_requests: true) + + expect(WebMock).to have_requested(:get, 'https://8.8.8.9:8080').once + end + end + + context 'when reading the response is too slow' do + before_all do + # Override Net::HTTP to add a delay between sending each response chunk + mocked_http = Class.new(Net::HTTP) do + def request(*) + super do |response| + response.instance_eval do + def read_body(*) + mock_stream = @body.split(' ') + mock_stream.each do |fragment| + sleep 0.002.seconds + + yield fragment if block_given? + end + + @body + end + end + + yield response if block_given? + + response + end + end + end + + @original_net_http = Net.send(:remove_const, :HTTP) + @webmock_net_http = WebMock::HttpLibAdapters::NetHttpAdapter.instance_variable_get(:@webMockNetHTTP) + + Net.send(:const_set, :HTTP, mocked_http) + WebMock::HttpLibAdapters::NetHttpAdapter.instance_variable_set(:@webMockNetHTTP, mocked_http) + + # Reload Gitlab::NetHttpAdapter + Gitlab.send(:remove_const, :NetHttpAdapter) + load "#{Rails.root}/lib/gitlab/net_http_adapter.rb" + end + + before do + stub_const("Gitlab::HTTP::DEFAULT_READ_TOTAL_TIMEOUT", 0.001.seconds) + + WebMock.stub_request(:post, /.*/).to_return do + { body: "chunk-1 chunk-2", status: 200 } + end + end + + after(:all) do + Net.send(:remove_const, :HTTP) + Net.send(:const_set, :HTTP, @original_net_http) + WebMock::HttpLibAdapters::NetHttpAdapter.instance_variable_set(:@webMockNetHTTP, @webmock_net_http) + + # Reload Gitlab::NetHttpAdapter + Gitlab.send(:remove_const, :NetHttpAdapter) + load "#{Rails.root}/lib/gitlab/net_http_adapter.rb" + end + + let(:options) { {} } + + subject(:request_slow_responder) { described_class.post('http://example.org', **options) } + + it 'raises an error' do + expect { request_slow_responder }.to raise_error( + Gitlab::HTTP::ReadTotalTimeout, /Request timed out after ?([0-9]*[.])?[0-9]+ seconds/) + end + + context 'and timeout option is greater than DEFAULT_READ_TOTAL_TIMEOUT' do + let(:options) { { timeout: 10.seconds } } + + it 'does not raise an error' do + expect { request_slow_responder }.not_to raise_error + end + end + + context 'and stream_body option is truthy' do + let(:options) { { stream_body: true } } + + it 'does not raise an error' do + expect { request_slow_responder }.not_to raise_error + end + end + end + + it 'calls a block' do + WebMock.stub_request(:post, /.*/) + + expect { |b| described_class.post('http://example.org', &b) }.to yield_with_args + end + + describe 'allow_local_requests_from_web_hooks_and_services is' do + before do + WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success') + end + + context 'disabled' do + before do + allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_web_hooks_and_services?).and_return(false) + end + + it 'deny requests to localhost' do + expect { described_class.get('http://localhost:3003') }.to raise_error(Gitlab::HTTP::BlockedUrlError) + end + + it 'deny requests to private network' do + expect { described_class.get('http://192.168.1.2:3003') }.to raise_error(Gitlab::HTTP::BlockedUrlError) + end + + context 'if allow_local_requests set to true' do + it 'override the global value and allow requests to localhost or private network' do + stub_full_request('http://localhost:3003') + + expect { described_class.get('http://localhost:3003', allow_local_requests: true) }.not_to raise_error + end + end + end + + context 'enabled' do + before do + allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_web_hooks_and_services?).and_return(true) + end + + it 'allow requests to localhost' do + stub_full_request('http://localhost:3003') + + expect { described_class.get('http://localhost:3003') }.not_to raise_error + end + + it 'allow requests to private network' do + expect { described_class.get('http://192.168.1.2:3003') }.not_to raise_error + end + + context 'if allow_local_requests set to false' do + it 'override the global value and ban requests to localhost or private network' do + expect { described_class.get('http://localhost:3003', allow_local_requests: false) }.to raise_error( + Gitlab::HTTP::BlockedUrlError) + end + end + end + end + + describe 'handle redirect loops' do + before do + stub_full_request("http://example.org", method: :any).to_raise( + HTTParty::RedirectionTooDeep.new("Redirection Too Deep")) + end + + it 'handles GET requests' do + expect { described_class.get('http://example.org') }.to raise_error(Gitlab::HTTP::RedirectionTooDeep) + end + + it 'handles POST requests' do + expect { described_class.post('http://example.org') }.to raise_error(Gitlab::HTTP::RedirectionTooDeep) + end + + it 'handles PUT requests' do + expect { described_class.put('http://example.org') }.to raise_error(Gitlab::HTTP::RedirectionTooDeep) + end + + it 'handles DELETE requests' do + expect { described_class.delete('http://example.org') }.to raise_error(Gitlab::HTTP::RedirectionTooDeep) + end + + it 'handles HEAD requests' do + expect { described_class.head('http://example.org') }.to raise_error(Gitlab::HTTP::RedirectionTooDeep) + end + end + + describe 'setting default timeouts' do + before do + stub_full_request('http://example.org', method: :any) + end + + context 'when no timeouts are set' do + it 'sets default open and read and write timeouts' do + expect(described_class).to receive(:httparty_perform_request).with( + Net::HTTP::Get, 'http://example.org', default_options + ).and_call_original + + described_class.get('http://example.org') + end + end + + context 'when :timeout is set' do + it 'does not set any default timeouts' do + expect(described_class).to receive(:httparty_perform_request).with( + Net::HTTP::Get, 'http://example.org', { timeout: 1 } + ).and_call_original + + described_class.get('http://example.org', { timeout: 1 }) + end + end + + context 'when :open_timeout is set' do + it 'only sets default read and write timeout' do + expect(described_class).to receive(:httparty_perform_request).with( + Net::HTTP::Get, 'http://example.org', default_options.merge(open_timeout: 1) + ).and_call_original + + described_class.get('http://example.org', open_timeout: 1) + end + end + + context 'when :read_timeout is set' do + it 'only sets default open and write timeout' do + expect(described_class).to receive(:httparty_perform_request).with( + Net::HTTP::Get, 'http://example.org', default_options.merge(read_timeout: 1) + ).and_call_original + + described_class.get('http://example.org', read_timeout: 1) + end + end + + context 'when :write_timeout is set' do + it 'only sets default open and read timeout' do + expect(described_class).to receive(:httparty_perform_request).with( + Net::HTTP::Put, 'http://example.org', default_options.merge(write_timeout: 1) + ).and_call_original + + described_class.put('http://example.org', write_timeout: 1) + end + end + end + + describe '.try_get' do + let(:path) { 'http://example.org' } + + let(:extra_log_info_proc) do + proc do |error, url, options| + { klass: error.class, url: url, options: options } + end + end + + let(:request_options) do + default_options.merge({ + verify: false, + basic_auth: { username: 'user', password: 'pass' } + }) + end + + Gitlab::HTTP::HTTP_ERRORS.each do |exception_class| + context "with #{exception_class}" do + let(:klass) { exception_class } + + context 'with path' do + before do + expect(described_class).to receive(:httparty_perform_request) + .with(Net::HTTP::Get, path, default_options) + .and_raise(klass) + end + + it 'handles requests without extra_log_info' do + expect(Gitlab::ErrorTracking) + .to receive(:log_exception) + .with(instance_of(klass), {}) + + expect(described_class.try_get(path)).to be_nil + end + + it 'handles requests with extra_log_info as hash' do + expect(Gitlab::ErrorTracking) + .to receive(:log_exception) + .with(instance_of(klass), { a: :b }) + + expect(described_class.try_get(path, extra_log_info: { a: :b })).to be_nil + end + + it 'handles requests with extra_log_info as proc' do + expect(Gitlab::ErrorTracking) + .to receive(:log_exception) + .with(instance_of(klass), { url: path, klass: klass, options: {} }) + + expect(described_class.try_get(path, extra_log_info: extra_log_info_proc)).to be_nil + end + end + + context 'with path and options' do + before do + expect(described_class).to receive(:httparty_perform_request) + .with(Net::HTTP::Get, path, request_options) + .and_raise(klass) + end + + it 'handles requests without extra_log_info' do + expect(Gitlab::ErrorTracking) + .to receive(:log_exception) + .with(instance_of(klass), {}) + + expect(described_class.try_get(path, request_options)).to be_nil + end + + it 'handles requests with extra_log_info as hash' do + expect(Gitlab::ErrorTracking) + .to receive(:log_exception) + .with(instance_of(klass), { a: :b }) + + expect(described_class.try_get(path, **request_options, extra_log_info: { a: :b })).to be_nil + end + + it 'handles requests with extra_log_info as proc' do + expect(Gitlab::ErrorTracking) + .to receive(:log_exception) + .with(instance_of(klass), { klass: klass, url: path, options: request_options }) + + expect(described_class.try_get(path, **request_options, extra_log_info: extra_log_info_proc)).to be_nil + end + end + + context 'with path, options, and block' do + let(:block) do + proc {} + end + + before do + expect(described_class).to receive(:httparty_perform_request) + .with(Net::HTTP::Get, path, request_options, &block) + .and_raise(klass) + end + + it 'handles requests without extra_log_info' do + expect(Gitlab::ErrorTracking) + .to receive(:log_exception) + .with(instance_of(klass), {}) + + expect(described_class.try_get(path, request_options, &block)).to be_nil + end + + it 'handles requests with extra_log_info as hash' do + expect(Gitlab::ErrorTracking) + .to receive(:log_exception) + .with(instance_of(klass), { a: :b }) + + expect(described_class.try_get(path, **request_options, extra_log_info: { a: :b }, &block)).to be_nil + end + + it 'handles requests with extra_log_info as proc' do + expect(Gitlab::ErrorTracking) + .to receive(:log_exception) + .with(instance_of(klass), { klass: klass, url: path, options: request_options }) + + expect( + described_class.try_get(path, **request_options, extra_log_info: extra_log_info_proc, &block) + ).to be_nil + end + end + end + end + end + + describe 'silent mode', feature_category: :geo_replication do + before do + stub_full_request("http://example.org", method: :any) + stub_application_setting(silent_mode_enabled: silent_mode) + end + + context 'when silent mode is enabled' do + let(:silent_mode) { true } + + it 'allows GET requests' do + expect { described_class.get('http://example.org') }.not_to raise_error + end + + it 'allows HEAD requests' do + expect { described_class.head('http://example.org') }.not_to raise_error + end + + it 'allows OPTIONS requests' do + expect { described_class.options('http://example.org') }.not_to raise_error + end + + it 'blocks POST requests' do + expect { described_class.post('http://example.org') }.to raise_error(Gitlab::HTTP::SilentModeBlockedError) + end + + it 'blocks PUT requests' do + expect { described_class.put('http://example.org') }.to raise_error(Gitlab::HTTP::SilentModeBlockedError) + end + + it 'blocks DELETE requests' do + expect { described_class.delete('http://example.org') }.to raise_error(Gitlab::HTTP::SilentModeBlockedError) + end + + it 'logs blocked requests' do + expect(::Gitlab::AppJsonLogger).to receive(:info).with( + message: "Outbound HTTP request blocked", + outbound_http_request_method: 'Net::HTTP::Post', + silent_mode_enabled: true + ) + + expect { described_class.post('http://example.org') }.to raise_error(Gitlab::HTTP::SilentModeBlockedError) + end + end + + context 'when silent mode is disabled' do + let(:silent_mode) { false } + + it 'allows GET requests' do + expect { described_class.get('http://example.org') }.not_to raise_error + end + + it 'allows HEAD requests' do + expect { described_class.head('http://example.org') }.not_to raise_error + end + + it 'allows OPTIONS requests' do + expect { described_class.options('http://example.org') }.not_to raise_error + end + + it 'blocks POST requests' do + expect { described_class.post('http://example.org') }.not_to raise_error + end + + it 'blocks PUT requests' do + expect { described_class.put('http://example.org') }.not_to raise_error + end + + it 'blocks DELETE requests' do + expect { described_class.delete('http://example.org') }.not_to raise_error + end + end + end +end diff --git a/spec/lib/gitlab/memory/instrumentation_spec.rb b/spec/lib/gitlab/memory/instrumentation_spec.rb index 3d58f28ec1e..f287edb7da3 100644 --- a/spec/lib/gitlab/memory/instrumentation_spec.rb +++ b/spec/lib/gitlab/memory/instrumentation_spec.rb @@ -38,7 +38,7 @@ RSpec.describe Gitlab::Memory::Instrumentation, feature_category: :application_p subject do described_class.with_memory_allocations do - Array.new(1000).map { '0' * 100 } + Array.new(1000).map { '0' * 1000 } end end @@ -52,7 +52,7 @@ RSpec.describe Gitlab::Memory::Instrumentation, feature_category: :application_p expect(result).to include( mem_objects: be > 1000, mem_mallocs: be > 1000, - mem_bytes: be > 100_000, # 100 items * 100 bytes each + mem_bytes: be > 1000_000, # 1000 items * 1000 bytes each mem_total_bytes: eq(result[:mem_bytes] + 40 * result[:mem_objects]) ) end diff --git a/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb index 4f437e57600..74aa3528328 100644 --- a/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb +++ b/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb @@ -137,4 +137,19 @@ RSpec.describe Gitlab::MergeRequests::Mergeability::CheckResult do end end end + + describe '#identifier' do + let(:payload) { { identifier: 'ci_must_pass' } } + + subject(:identifier) do + described_class + .new( + status: described_class::SUCCESS_STATUS, + payload: payload + ) + .identifier + end + + it { is_expected.to eq(:ci_must_pass) } + end end diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb index dc59fa804c4..ea98c8d7933 100644 --- a/spec/lib/gitlab/metrics/web_transaction_spec.rb +++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb @@ -64,7 +64,7 @@ RSpec.describe Gitlab::Metrics::WebTransaction do describe '#labels' do context 'when request goes to Grape endpoint' do before do - route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)') + route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)', origin: '/:version/projects/:id/archive') endpoint = double(:endpoint, route: route, options: { for: API::Projects, path: [":id/archive"] }, namespace: "/projects") @@ -76,7 +76,12 @@ RSpec.describe Gitlab::Metrics::WebTransaction do end it 'provides labels with the method and path of the route in the grape endpoint' do - expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive', feature_category: 'projects' }) + expect(transaction.labels).to eq({ + controller: 'Grape', + action: 'GET /projects/:id/archive', + feature_category: 'projects', + endpoint_id: 'GET /:version/projects/:id/archive' + }) end it 'contains only the labels defined for transactions' do @@ -103,7 +108,7 @@ RSpec.describe Gitlab::Metrics::WebTransaction do end it 'tags a transaction with the name and action of a controller' do - expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: ::Gitlab::FeatureCategories::FEATURE_CATEGORY_DEFAULT }) + expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: ::Gitlab::FeatureCategories::FEATURE_CATEGORY_DEFAULT, endpoint_id: 'TestController#show' }) end it 'contains only the labels defined for transactions' do @@ -114,7 +119,7 @@ RSpec.describe Gitlab::Metrics::WebTransaction do let(:request) { double(:request, format: double(:format, ref: :json)) } it 'appends the mime type to the transaction action' do - expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json', feature_category: ::Gitlab::FeatureCategories::FEATURE_CATEGORY_DEFAULT }) + expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json', feature_category: ::Gitlab::FeatureCategories::FEATURE_CATEGORY_DEFAULT, endpoint_id: 'TestController#show' }) end end @@ -122,7 +127,7 @@ RSpec.describe Gitlab::Metrics::WebTransaction do let(:request) { double(:request, format: double(:format, ref: 'http://example.com')) } it 'does not append the MIME type to the transaction action' do - expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: ::Gitlab::FeatureCategories::FEATURE_CATEGORY_DEFAULT }) + expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: ::Gitlab::FeatureCategories::FEATURE_CATEGORY_DEFAULT, endpoint_id: 'TestController#show' }) end end @@ -131,7 +136,7 @@ RSpec.describe Gitlab::Metrics::WebTransaction do # This is needed since we're not actually making a request, which would trigger the controller pushing to the context ::Gitlab::ApplicationContext.push(feature_category: 'source_code_management') - expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: "source_code_management" }) + expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: 'source_code_management', endpoint_id: 'TestController#show' }) end end end @@ -147,7 +152,7 @@ RSpec.describe Gitlab::Metrics::WebTransaction do let(:controller) { double(:controller, class: controller_class, action_name: 'show', request: request) } let(:transaction_obj) { described_class.new({ 'action_controller.instance' => controller }) } - let(:labels) { { controller: 'TestController', action: 'show', feature_category: 'projects' } } + let(:labels) { { controller: 'TestController', action: 'show', feature_category: 'projects', endpoint_id: 'TestController#show' } } before do ::Gitlab::ApplicationContext.push(feature_category: 'projects') diff --git a/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb b/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb index ed1440f23b6..7bc5fd853bf 100644 --- a/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb +++ b/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb @@ -58,6 +58,39 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do end end + context 'with POST request' do + let(:request_env) do + Rack::MockRequest.env_for( + '/', + method: 'POST', + input: input, + 'CONTENT_TYPE' => 'application/json' + ) + end + + let(:params) { { method: 'POST' } } + + context 'with valid JSON' do + let(:input) { %({"hello": "world"}) } + + it 'returns no error' do + env = request_env + + expect(subject.call(env)).not_to eq error_400 + end + end + + context 'with bad JSON' do + let(:input) { "{ bad json }" } + + it 'rejects bad JSON with 400 error' do + env = request_env + + expect(subject.call(env)).to eq error_400 + end + end + end + context 'in authorization headers' do let(:problematic_input) { null_byte } diff --git a/spec/lib/gitlab/middleware/path_traversal_check_spec.rb b/spec/lib/gitlab/middleware/path_traversal_check_spec.rb new file mode 100644 index 00000000000..3d334a60c49 --- /dev/null +++ b/spec/lib/gitlab/middleware/path_traversal_check_spec.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shared do + using RSpec::Parameterized::TableSyntax + + let(:fake_response) { [200, { 'Content-Type' => 'text/plain' }, ['OK']] } + let(:fake_app) { ->(_) { fake_response } } + let(:middleware) { described_class.new(fake_app) } + + describe '#call' do + let(:fullpath) { ::Rack::Request.new(env).fullpath } + let(:decoded_fullpath) { CGI.unescape(fullpath) } + + let(:env) do + Rack::MockRequest.env_for( + path, + method: method, + params: query_params + ) + end + + subject { middleware.call(env) } + + shared_examples 'no issue' do + it 'measures and logs the execution time' do + expect(::Gitlab::PathTraversal) + .to receive(:check_path_traversal!) + .with(decoded_fullpath, skip_decoding: true) + .and_call_original + expect(::Gitlab::AppLogger) + .to receive(:warn) + .with({ class_name: described_class.name, duration_ms: instance_of(Float) }) + .and_call_original + + expect(subject).to eq(fake_response) + end + + context 'with log_execution_time_path_traversal_middleware disabled' do + before do + stub_feature_flags(log_execution_time_path_traversal_middleware: false) + end + + it 'does nothing' do + expect(::Gitlab::PathTraversal) + .to receive(:check_path_traversal!) + .with(decoded_fullpath, skip_decoding: true) + .and_call_original + expect(::Gitlab::AppLogger) + .not_to receive(:warn) + + expect(subject).to eq(fake_response) + end + end + end + + shared_examples 'path traversal' do + it 'logs the problem and measures the execution time' do + expect(::Gitlab::PathTraversal) + .to receive(:check_path_traversal!) + .with(decoded_fullpath, skip_decoding: true) + .and_call_original + expect(::Gitlab::AppLogger) + .to receive(:warn) + .with({ message: described_class::PATH_TRAVERSAL_MESSAGE, path: instance_of(String) }) + expect(::Gitlab::AppLogger) + .to receive(:warn) + .with({ + class_name: described_class.name, + duration_ms: instance_of(Float), + message: described_class::PATH_TRAVERSAL_MESSAGE, + fullpath: fullpath + }).and_call_original + + expect(subject).to eq(fake_response) + end + + context 'with log_execution_time_path_traversal_middleware disabled' do + before do + stub_feature_flags(log_execution_time_path_traversal_middleware: false) + end + + it 'logs the problem without the execution time' do + expect(::Gitlab::PathTraversal) + .to receive(:check_path_traversal!) + .with(decoded_fullpath, skip_decoding: true) + .and_call_original + expect(::Gitlab::AppLogger) + .to receive(:warn) + .with({ message: described_class::PATH_TRAVERSAL_MESSAGE, path: instance_of(String) }) + expect(::Gitlab::AppLogger) + .to receive(:warn) + .with({ + class_name: described_class.name, + message: described_class::PATH_TRAVERSAL_MESSAGE, + fullpath: fullpath + }).and_call_original + + expect(subject).to eq(fake_response) + end + end + end + + # we use Rack request.full_path, this will dump the accessed path and + # the query string. The query string is only for GETs requests. + # Hence different expectation (when params are set) for GETs and + # the other methods (see below) + context 'when using get' do + let(:method) { 'get' } + + where(:path, :query_params, :shared_example_name) do + '/foo/bar' | {} | 'no issue' + '/foo/../bar' | {} | 'path traversal' + '/foo%2Fbar' | {} | 'no issue' + '/foo%2F..%2Fbar' | {} | 'path traversal' + '/foo%252F..%252Fbar' | {} | 'no issue' + '/foo/bar' | { x: 'foo' } | 'no issue' + '/foo/bar' | { x: 'foo/../bar' } | 'path traversal' + '/foo/bar' | { x: 'foo%2Fbar' } | 'no issue' + '/foo/bar' | { x: 'foo%2F..%2Fbar' } | 'no issue' + '/foo/bar' | { x: 'foo%252F..%252Fbar' } | 'no issue' + '/foo%2F..%2Fbar' | { x: 'foo%252F..%252Fbar' } | 'path traversal' + end + + with_them do + it_behaves_like params[:shared_example_name] + end + + context 'with a issues search path' do + let(:query_params) { {} } + let(:path) do + 'project/-/issues/?sort=updated_desc&milestone_title=16.0&search=Release%20%252525&first_page_size=20' + end + + it_behaves_like 'no issue' + end + end + + %w[post put post delete patch].each do |http_method| + context "when using #{http_method}" do + let(:method) { http_method } + + where(:path, :query_params, :shared_example_name) do + '/foo/bar' | {} | 'no issue' + '/foo/../bar' | {} | 'path traversal' + '/foo%2Fbar' | {} | 'no issue' + '/foo%2F..%2Fbar' | {} | 'path traversal' + '/foo%252F..%252Fbar' | {} | 'no issue' + '/foo/bar' | { x: 'foo' } | 'no issue' + '/foo/bar' | { x: 'foo/../bar' } | 'no issue' + '/foo/bar' | { x: 'foo%2Fbar' } | 'no issue' + '/foo/bar' | { x: 'foo%2F..%2Fbar' } | 'no issue' + '/foo/bar' | { x: 'foo%252F..%252Fbar' } | 'no issue' + '/foo%2F..%2Fbar' | { x: 'foo%252F..%252Fbar' } | 'path traversal' + end + + with_them do + it_behaves_like params[:shared_example_name] + end + end + end + + context 'with check_path_traversal_middleware disabled' do + before do + stub_feature_flags(check_path_traversal_middleware: false) + end + + where(:path, :query_params) do + '/foo/bar' | {} + '/foo/../bar' | {} + '/foo%2Fbar' | {} + '/foo%2F..%2Fbar' | {} + '/foo%252F..%252Fbar' | {} + '/foo/bar' | { x: 'foo' } + '/foo/bar' | { x: 'foo/../bar' } + '/foo/bar' | { x: 'foo%2Fbar' } + '/foo/bar' | { x: 'foo%2F..%2Fbar' } + '/foo/bar' | { x: 'foo%252F..%252Fbar' } + end + + with_them do + %w[get post put post delete patch].each do |http_method| + context "when using #{http_method}" do + let(:method) { http_method } + + it 'does not check for path traversals' do + expect(::Gitlab::PathTraversal).not_to receive(:check_path_traversal!) + + subject + end + end + end + end + end + end +end diff --git a/spec/lib/gitlab/observability_spec.rb b/spec/lib/gitlab/observability_spec.rb index 04c35f0ee3a..7af2daea11c 100644 --- a/spec/lib/gitlab/observability_spec.rb +++ b/spec/lib/gitlab/observability_spec.rb @@ -46,206 +46,54 @@ RSpec.describe Gitlab::Observability, feature_category: :error_tracking do it { is_expected.to eq("#{described_class.observability_url}/v3/tenant/#{project.id}") } end - describe '.build_full_url' do - let_it_be(:group) { build_stubbed(:group, id: 123) } - let(:observability_url) { described_class.observability_url } + describe '.should_enable_observability_auth_scopes?' do + subject { described_class.should_enable_observability_auth_scopes?(resource) } - it 'returns the full observability url for the given params' do - url = described_class.build_full_url(group, '/foo?bar=baz', '/') - expect(url).to eq("https://observe.gitlab.com/-/123/foo?bar=baz") - end - - it 'handles missing / from observability_path' do - url = described_class.build_full_url(group, 'foo?bar=baz', '/') - expect(url).to eq("https://observe.gitlab.com/-/123/foo?bar=baz") - end - - it 'sanitises observability_path' do - url = described_class.build_full_url(group, "/test?groupId=<script>alert('attack!')</script>", '/') - expect(url).to eq("https://observe.gitlab.com/-/123/test?groupId=alert('attack!')") - end - - context 'when observability_path is missing' do - it 'builds the url with the fallback_path' do - url = described_class.build_full_url(group, nil, '/fallback') - expect(url).to eq("https://observe.gitlab.com/-/123/fallback") - end - - it 'defaults to / if fallback_path is also missing' do - url = described_class.build_full_url(group, nil, nil) - expect(url).to eq("https://observe.gitlab.com/-/123/") + let(:parent) { build_stubbed(:group) } + let(:resource) do + build_stubbed(:group, parent: parent).tap do |g| + g.namespace_settings = build_stubbed(:namespace_settings, namespace: g) end end - end - describe '.embeddable_url' do before do - stub_config_setting(url: "https://www.gitlab.com") - # Can't use build/build_stubbed as we want the routes to be generated as well - create(:group, path: 'test-path', id: 123) - end - - context 'when URL is valid' do - where(:input, :expected) do - [ - [ - "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=%2Fexplore%3FgroupId%3D14485840%26left%3D%255B%2522now-1h%2522,%2522now%2522,%2522new-sentry.gitlab.net%2522,%257B%257D%255D", - "https://observe.gitlab.com/-/123/explore?groupId=14485840&left=%5B%22now-1h%22,%22now%22,%22new-sentry.gitlab.net%22,%7B%7D%5D" - ], - [ - "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=/goto/foo", - "https://observe.gitlab.com/-/123/goto/foo" - ] - ] - end - - with_them do - it 'returns an embeddable observability url' do - expect(described_class.embeddable_url(input)).to eq(expected) - end - end + stub_feature_flags(observability_tracing: parent) end - context 'when URL is invalid' do - where(:input) do - [ - # direct links to observe.gitlab.com - "https://observe.gitlab.com/-/123/explore", - 'https://observe.gitlab.com/v1/auth/start', - - # invalid GitLab URL - "not a link", - "https://foo.bar/groups/test-path/-/observability/explore?observability_path=/explore", - "http://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=/explore", - "https://www.gitlab.com:123/groups/test-path/-/observability/explore?observability_path=/explore", - "https://www.gitlab.com@example.com/groups/test-path/-/observability/explore?observability_path=/explore", - "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=@example.com", - - # invalid group/controller/actions - "https://www.gitlab.com/groups/INVALID_GROUP/-/observability/explore?observability_path=/explore", - "https://www.gitlab.com/groups/test-path/-/INVALID_CONTROLLER/explore?observability_path=/explore", - "https://www.gitlab.com/groups/test-path/-/observability/INVALID_ACTION?observability_path=/explore", - - # invalid observablity path - "https://www.gitlab.com/groups/test-path/-/observability/explore", - "https://www.gitlab.com/groups/test-path/-/observability/explore?missing_observability_path=/explore", - "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=/not_embeddable", - "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=/datasources", - "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=not a valid path" - ] + describe 'when resource is group' do + context 'if observability_tracing FF enabled' do + it { is_expected.to be true } end - with_them do - it 'returns nil' do - expect(described_class.embeddable_url(input)).to be_nil + context 'if observability_tracing FF disabled' do + before do + stub_feature_flags(observability_tracing: false) end - end - - it 'returns nil if the path detection throws an error' do - test_url = "https://www.gitlab.com/groups/test-path/-/observability/explore" - allow(Rails.application.routes).to receive(:recognize_path).with(test_url) { - raise ActionController::RoutingError, 'test' - } - expect(described_class.embeddable_url(test_url)).to be_nil - end - - it 'returns nil if parsing observaboility path throws an error' do - observability_path = 'some-path' - test_url = "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=#{observability_path}" - - allow(URI).to receive(:parse).and_call_original - allow(URI).to receive(:parse).with(observability_path) { - raise URI::InvalidURIError, 'test' - } - expect(described_class.embeddable_url(test_url)).to be_nil + it { is_expected.to be false } end end - end - - describe '.allowed_for_action?' do - let(:group) { build_stubbed(:group) } - let(:user) { build_stubbed(:user) } - - before do - allow(described_class).to receive(:allowed?).and_call_original - end - - it 'returns false if action is nil' do - expect(described_class.allowed_for_action?(user, group, nil)).to eq(false) - end - describe 'allowed? calls' do - using RSpec::Parameterized::TableSyntax + describe 'when resource is project' do + let(:resource) { build_stubbed(:project, namespace: parent) } - where(:action, :permission) do - :foo | :admin_observability - :explore | :read_observability - :datasources | :admin_observability - :manage | :admin_observability - :dashboards | :read_observability + context 'if observability_tracing FF enabled' do + it { is_expected.to be true } end - with_them do - it "calls allowed? with #{params[:permission]} when actions is #{params[:action]}" do - described_class.allowed_for_action?(user, group, action) - expect(described_class).to have_received(:allowed?).with(user, group, permission) + context 'if observability_tracing FF disabled' do + before do + stub_feature_flags(observability_tracing: false) end - end - end - end - - describe '.allowed?' do - let(:user) { build_stubbed(:user) } - let(:group) { build_stubbed(:group) } - let(:test_permission) { :read_observability } - - before do - allow(Ability).to receive(:allowed?).and_return(false) - end - - subject do - described_class.allowed?(user, group, test_permission) - end - - it 'checks if ability is allowed for the given user and group' do - allow(Ability).to receive(:allowed?).and_return(true) - - subject - - expect(Ability).to have_received(:allowed?).with(user, test_permission, group) - end - - it 'checks for admin_observability if permission is missing' do - described_class.allowed?(user, group) - - expect(Ability).to have_received(:allowed?).with(user, :admin_observability, group) - end - - it 'returns true if the ability is allowed' do - allow(Ability).to receive(:allowed?).and_return(true) - - expect(subject).to eq(true) - end - it 'returns false if the ability is not allowed' do - allow(Ability).to receive(:allowed?).and_return(false) - - expect(subject).to eq(false) - end - - it 'returns false if observability url is missing' do - allow(described_class).to receive(:observability_url).and_return("") - - expect(subject).to eq(false) + it { is_expected.to be false } + end end - it 'returns false if group is missing' do - expect(described_class.allowed?(user, nil, :read_observability)).to eq(false) - end + describe 'when resource is not a group or project' do + let(:resource) { build_stubbed(:user) } - it 'returns false if user is missing' do - expect(described_class.allowed?(nil, group, :read_observability)).to eq(false) + it { is_expected.to be false } end end end diff --git a/spec/lib/gitlab/octokit/middleware_spec.rb b/spec/lib/gitlab/octokit/middleware_spec.rb index f7063f2c4f2..07936de9e78 100644 --- a/spec/lib/gitlab/octokit/middleware_spec.rb +++ b/spec/lib/gitlab/octokit/middleware_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Gitlab::Octokit::Middleware, feature_category: :importers do shared_examples 'Blocked URL' do it 'raises an error' do - expect { middleware.call(env) }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError) + expect { middleware.call(env) }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end @@ -88,7 +88,7 @@ RSpec.describe Gitlab::Octokit::Middleware, feature_category: :importers do let(:env) { { url: 'ssh://172.16.0.0' } } it 'raises an error' do - expect { middleware.call(env) }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError) + expect { middleware.call(env) }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end end diff --git a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb index 4128f745ce7..effe767e41d 100644 --- a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb +++ b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb @@ -6,52 +6,24 @@ RSpec.describe Gitlab::Pagination::CursorBasedKeyset do subject { described_class } describe '.available_for_type?' do - context 'with api_keyset_pagination_multi_order FF disabled' do - before do - stub_feature_flags(api_keyset_pagination_multi_order: false) - end - - it 'returns true for Group' do - expect(subject.available_for_type?(Group.all)).to be_truthy - end - - it 'returns true for Ci::Build' do - expect(subject.available_for_type?(Ci::Build.all)).to be_truthy - end - - it 'returns true for Packages::BuildInfo' do - expect(subject.available_for_type?(Packages::BuildInfo.all)).to be_truthy - end - - it 'return false for User' do - expect(subject.available_for_type?(User.all)).to be_falsey - end + it 'returns true for Group' do + expect(subject.available_for_type?(Group.all)).to be_truthy end - context 'with api_keyset_pagination_multi_order FF enabled' do - before do - stub_feature_flags(api_keyset_pagination_multi_order: true) - end - - it 'returns true for Group' do - expect(subject.available_for_type?(Group.all)).to be_truthy - end - - it 'returns true for Ci::Build' do - expect(subject.available_for_type?(Ci::Build.all)).to be_truthy - end + it 'returns true for Ci::Build' do + expect(subject.available_for_type?(Ci::Build.all)).to be_truthy + end - it 'returns true for Packages::BuildInfo' do - expect(subject.available_for_type?(Packages::BuildInfo.all)).to be_truthy - end + it 'returns true for Packages::BuildInfo' do + expect(subject.available_for_type?(Packages::BuildInfo.all)).to be_truthy + end - it 'returns true for User' do - expect(subject.available_for_type?(User.all)).to be_truthy - end + it 'returns true for User' do + expect(subject.available_for_type?(User.all)).to be_truthy + end - it 'return false for other types of relations' do - expect(subject.available_for_type?(Issue.all)).to be_falsey - end + it 'return false for other types of relations' do + expect(subject.available_for_type?(Issue.all)).to be_falsey end end @@ -100,48 +72,20 @@ RSpec.describe Gitlab::Pagination::CursorBasedKeyset do let(:order_by) { :id } let(:sort) { :desc } - context 'with api_keyset_pagination_multi_order FF disabled' do - before do - stub_feature_flags(api_keyset_pagination_multi_order: false) - end - - it 'returns true for Ci::Build' do - expect(subject.available?(cursor_based_request_context, Ci::Build.all)).to be_truthy - end - - it 'returns true for AuditEvent' do - expect(subject.available?(cursor_based_request_context, AuditEvent.all)).to be_truthy - end - - it 'returns true for Packages::BuildInfo' do - expect(subject.available?(cursor_based_request_context, Packages::BuildInfo.all)).to be_truthy - end - - it 'returns false for User' do - expect(subject.available?(cursor_based_request_context, User.all)).to be_falsey - end + it 'returns true for Ci::Build' do + expect(subject.available?(cursor_based_request_context, Ci::Build.all)).to be_truthy end - context 'with api_keyset_pagination_multi_order FF enabled' do - before do - stub_feature_flags(api_keyset_pagination_multi_order: true) - end - - it 'returns true for Ci::Build' do - expect(subject.available?(cursor_based_request_context, Ci::Build.all)).to be_truthy - end - - it 'returns true for AuditEvent' do - expect(subject.available?(cursor_based_request_context, AuditEvent.all)).to be_truthy - end + it 'returns true for AuditEvent' do + expect(subject.available?(cursor_based_request_context, AuditEvent.all)).to be_truthy + end - it 'returns true for Packages::BuildInfo' do - expect(subject.available?(cursor_based_request_context, Packages::BuildInfo.all)).to be_truthy - end + it 'returns true for Packages::BuildInfo' do + expect(subject.available?(cursor_based_request_context, Packages::BuildInfo.all)).to be_truthy + end - it 'returns true for User' do - expect(subject.available?(cursor_based_request_context, User.all)).to be_truthy - end + it 'returns true for User' do + expect(subject.available?(cursor_based_request_context, User.all)).to be_truthy end end diff --git a/spec/lib/gitlab/path_traversal_spec.rb b/spec/lib/gitlab/path_traversal_spec.rb index bba6f8293c2..063919dd985 100644 --- a/spec/lib/gitlab/path_traversal_spec.rb +++ b/spec/lib/gitlab/path_traversal_spec.rb @@ -93,6 +93,13 @@ RSpec.describe Gitlab::PathTraversal, feature_category: :shared do it 'raises for other non-strings' do expect { check_path_traversal!(%w[/tmp /tmp/../etc/passwd]) }.to raise_error(/Invalid path/) end + + context 'when skip_decoding is used' do + it 'does not detect double encoded chars' do + expect(check_path_traversal!('foo%252F..%2Fbar', skip_decoding: true)).to eq('foo%252F..%2Fbar') + expect(check_path_traversal!('foo%252F%2E%2E%2Fbar', skip_decoding: true)).to eq('foo%252F%2E%2E%2Fbar') + end + end end describe '.check_allowed_absolute_path!' do diff --git a/spec/lib/gitlab/prometheus/metric_group_spec.rb b/spec/lib/gitlab/prometheus/metric_group_spec.rb deleted file mode 100644 index a68cdfe5fb2..00000000000 --- a/spec/lib/gitlab/prometheus/metric_group_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Prometheus::MetricGroup do - describe '.common_metrics' do - let!(:project_metric) { create(:prometheus_metric) } - let!(:common_metric_group_a) { create(:prometheus_metric, :common, group: :aws_elb) } - let!(:common_metric_group_b_q1) { create(:prometheus_metric, :common, group: :kubernetes) } - let!(:common_metric_group_b_q2) { create(:prometheus_metric, :common, group: :kubernetes) } - - subject { described_class.common_metrics } - - it 'returns exactly two groups' do - expect(subject.map(&:name)).to contain_exactly( - 'Response metrics (AWS ELB)', 'System metrics (Kubernetes)') - end - - it 'returns exactly three metric queries' do - expect(subject.flat_map(&:metrics).map(&:id)).to contain_exactly( - common_metric_group_a.id, common_metric_group_b_q1.id, - common_metric_group_b_q2.id) - end - - it 'orders by priority' do - priorities = subject.map(&:priority) - names = subject.map(&:name) - expect(priorities).to eq([10, 5]) - expect(names).to eq(['Response metrics (AWS ELB)', 'System metrics (Kubernetes)']) - end - end - - describe '.for_project' do - let!(:other_project) { create(:project) } - let!(:project_metric) { create(:prometheus_metric) } - let!(:common_metric) { create(:prometheus_metric, :common, group: :aws_elb) } - - subject do - described_class.for_project(other_project) - .flat_map(&:metrics) - .map(&:id) - end - - it 'returns exactly one common metric' do - is_expected.to contain_exactly(common_metric.id) - end - end -end diff --git a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb deleted file mode 100644 index 66b93d0dd72..00000000000 --- a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Prometheus::Queries::DeploymentQuery do - let(:environment) { create(:environment, slug: 'environment-slug') } - let(:deployment) { create(:deployment, environment: environment) } - let(:client) { double('prometheus_client') } - - subject { described_class.new(client) } - - around do |example| - time_without_subsecond_values = Time.local(2008, 9, 1, 12, 0, 0) - travel_to(time_without_subsecond_values) { example.run } - end - - it 'sends appropriate queries to prometheus' do - start_time = (deployment.created_at - 30.minutes).to_f - end_time = (deployment.created_at + 30.minutes).to_f - created_at = deployment.created_at.to_f - - expect(client).to receive(:query_range).with('avg(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}) / 2^20', - start_time: start_time, end_time: end_time) - expect(client).to receive(:query).with('avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}[30m]))', - time: created_at) - expect(client).to receive(:query).with('avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}[30m]))', - time: end_time) - - expect(client).to receive(:query_range).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[2m])) * 100', - start_time: start_time, end_time: end_time) - expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100', - time: created_at) - expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100', - time: end_time) - - expect(subject.query(deployment.id)).to eq(memory_values: nil, memory_before: nil, memory_after: nil, - cpu_values: nil, cpu_before: nil, cpu_after: nil) - end -end diff --git a/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb b/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb deleted file mode 100644 index 60449aeef7d..00000000000 --- a/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb +++ /dev/null @@ -1,137 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Prometheus::Queries::MatchedMetricQuery do - include Prometheus::MetricBuilders - - let(:metric_group_class) { Gitlab::Prometheus::MetricGroup } - let(:metric_class) { Gitlab::Prometheus::Metric } - - def series_info_with_environment(*more_metrics) - %w{metric_a metric_b}.concat(more_metrics).map { |metric_name| { '__name__' => metric_name, 'environment' => '' } } - end - - let(:metric_names) { %w{metric_a metric_b} } - let(:series_info_without_environment) do - [{ '__name__' => 'metric_a' }, - { '__name__' => 'metric_b' }] - end - - let(:partially_empty_series_info) { [{ '__name__' => 'metric_a', 'environment' => '' }] } - let(:empty_series_info) { [] } - - let(:client) { double('prometheus_client') } - - subject { described_class.new(client) } - - context 'with one group where two metrics is found' do - before do - allow(metric_group_class).to receive(:common_metrics).and_return([simple_metric_group]) - allow(client).to receive(:label_values).and_return(metric_names) - end - - context 'both metrics in the group pass requirements' do - before do - allow(client).to receive(:series).and_return(series_info_with_environment) - end - - it 'responds with both metrics as actve' do - expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 2, metrics_missing_requirements: 0 }]) - end - end - - context 'none of the metrics pass requirements' do - before do - allow(client).to receive(:series).and_return(series_info_without_environment) - end - - it 'responds with both metrics missing requirements' do - expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 0, metrics_missing_requirements: 2 }]) - end - end - - context 'no series information found about the metrics' do - before do - allow(client).to receive(:series).and_return(empty_series_info) - end - - it 'responds with both metrics missing requirements' do - expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 0, metrics_missing_requirements: 2 }]) - end - end - - context 'one of the series info was not found' do - before do - allow(client).to receive(:series).and_return(partially_empty_series_info) - end - it 'responds with one active and one missing metric' do - expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 1, metrics_missing_requirements: 1 }]) - end - end - end - - context 'with one group where only one metric is found' do - before do - allow(metric_group_class).to receive(:common_metrics).and_return([simple_metric_group]) - allow(client).to receive(:label_values).and_return('metric_a') - end - - context 'both metrics in the group pass requirements' do - before do - allow(client).to receive(:series).and_return(series_info_with_environment) - end - - it 'responds with one metrics as active and no missing requiremens' do - expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 1, metrics_missing_requirements: 0 }]) - end - end - - context 'no metrics in group pass requirements' do - before do - allow(client).to receive(:series).and_return(series_info_without_environment) - end - - it 'responds with one metrics as active and no missing requiremens' do - expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 0, metrics_missing_requirements: 1 }]) - end - end - end - - context 'with two groups where metrics are found in each group' do - let(:second_metric_group) { simple_metric_group(name: 'nameb', metrics: simple_metrics(added_metric_name: 'metric_c')) } - - before do - allow(metric_group_class).to receive(:common_metrics).and_return([simple_metric_group, second_metric_group]) - allow(client).to receive(:label_values).and_return('metric_c') - end - - context 'all metrics in both groups pass requirements' do - before do - allow(client).to receive(:series).and_return(series_info_with_environment('metric_c')) - end - - it 'responds with one metrics as active and no missing requiremens' do - expect(subject.query).to eq([ - { group: 'name', priority: 1, active_metrics: 1, metrics_missing_requirements: 0 }, - { group: 'nameb', priority: 1, active_metrics: 2, metrics_missing_requirements: 0 } - ] - ) - end - end - - context 'no metrics in groups pass requirements' do - before do - allow(client).to receive(:series).and_return(series_info_without_environment) - end - - it 'responds with one metrics as active and no missing requiremens' do - expect(subject.query).to eq([ - { group: 'name', priority: 1, active_metrics: 0, metrics_missing_requirements: 1 }, - { group: 'nameb', priority: 1, active_metrics: 0, metrics_missing_requirements: 2 } - ] - ) - end - end - end -end diff --git a/spec/lib/gitlab/prometheus/queries/validate_query_spec.rb b/spec/lib/gitlab/prometheus/queries/validate_query_spec.rb deleted file mode 100644 index f09fa3548f8..00000000000 --- a/spec/lib/gitlab/prometheus/queries/validate_query_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Prometheus::Queries::ValidateQuery do - include PrometheusHelpers - - let(:api_url) { 'https://prometheus.example.com' } - let(:client) { Gitlab::PrometheusClient.new(api_url) } - let(:query) { 'avg(metric)' } - - subject { described_class.new(client) } - - context 'valid query' do - before do - allow(client).to receive(:query).with(query) - end - - it 'passess query to prometheus' do - expect(subject.query(query)).to eq(valid: true) - - expect(client).to have_received(:query).with(query) - end - end - - context 'invalid query' do - let(:query) { 'invalid query' } - let(:error_message) { "invalid parameter 'query': 1:9: parse error: unexpected identifier \"query\"" } - - it 'returns invalid' do - freeze_time do - stub_prometheus_query_error( - prometheus_query_with_time_url(query, Time.now), - error_message - ) - - expect(subject.query(query)).to eq(valid: false, error: error_message) - end - end - end - - context 'when exceptions occur' do - context 'Gitlab::HTTP::BlockedUrlError' do - let(:api_url) { 'http://192.168.1.1' } - - let(:message) { "URL is blocked: Requests to the local network are not allowed" } - - before do - stub_application_setting(allow_local_requests_from_web_hooks_and_services: false) - end - - it 'catches exception and returns invalid' do - freeze_time do - expect(subject.query(query)).to eq(valid: false, error: message) - end - end - end - end -end diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb deleted file mode 100644 index d0947eef2d9..00000000000 --- a/spec/lib/gitlab/prometheus/query_variables_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Prometheus::QueryVariables do - describe '.call' do - let_it_be_with_refind(:environment) { create(:environment) } - - let(:project) { environment.project } - let(:slug) { environment.slug } - let(:params) { {} } - - subject { described_class.call(environment, **params) } - - it { is_expected.to include(ci_environment_slug: slug) } - it { is_expected.to include(ci_project_name: project.name) } - it { is_expected.to include(ci_project_namespace: project.namespace.name) } - it { is_expected.to include(ci_project_path: project.full_path) } - it { is_expected.to include(ci_environment_name: environment.name) } - - it do - is_expected.to include(environment_filter: - %[container_name!="POD",environment="#{slug}"]) - end - - context 'without deployment platform' do - it { is_expected.to include(kube_namespace: '') } - end - - context 'with deployment platform' do - context 'with project cluster' do - let(:kube_namespace) { environment.deployment_namespace } - - before do - create(:cluster, :project, :provided_by_user, projects: [project]) - end - - it { is_expected.to include(kube_namespace: kube_namespace) } - end - - context 'with group cluster' do - let(:cluster) { create(:cluster, :group, :provided_by_user, groups: [group]) } - let(:group) { create(:group) } - let(:project2) { create(:project) } - let(:kube_namespace) { k8s_ns.namespace } - - let!(:k8s_ns) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project, environment: environment) } - let!(:k8s_ns2) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project2, environment: environment) } - - before do - group.projects << project - group.projects << project2 - end - - it { is_expected.to include(kube_namespace: kube_namespace) } - end - end - - context '__range' do - context 'when start_time and end_time are present' do - let(:params) do - { - start_time: Time.rfc3339('2020-05-29T07:23:05.008Z'), - end_time: Time.rfc3339('2020-05-29T15:23:05.008Z') - } - end - - it { is_expected.to include(__range: "#{8.hours.to_i}s") } - end - - context 'when start_time and end_time are not present' do - it { is_expected.to include(__range: nil) } - end - - context 'when end_time is not present' do - let(:params) do - { - start_time: Time.rfc3339('2020-05-29T07:23:05.008Z') - } - end - - it { is_expected.to include(__range: nil) } - end - - context 'when start_time is not present' do - let(:params) do - { - end_time: Time.rfc3339('2020-05-29T07:23:05.008Z') - } - end - - it { is_expected.to include(__range: nil) } - end - end - end -end diff --git a/spec/lib/gitlab/protocol_access_spec.rb b/spec/lib/gitlab/protocol_access_spec.rb index 4722ea99608..cae14c3d7cf 100644 --- a/spec/lib/gitlab/protocol_access_spec.rb +++ b/spec/lib/gitlab/protocol_access_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe Gitlab::ProtocolAccess do +RSpec.describe Gitlab::ProtocolAccess, feature_category: :source_code_management do using RSpec::Parameterized::TableSyntax let_it_be(:group) { create(:group) } @@ -10,25 +10,34 @@ RSpec.describe Gitlab::ProtocolAccess do describe ".allowed?" do where(:protocol, :project, :admin_setting, :namespace_setting, :expected_result) do - "web" | nil | nil | nil | true - "ssh" | nil | nil | nil | true - "http" | nil | nil | nil | true - "ssh" | nil | "" | nil | true - "http" | nil | "" | nil | true - "ssh" | nil | "ssh" | nil | true - "http" | nil | "http" | nil | true - "ssh" | nil | "http" | nil | false - "http" | nil | "ssh" | nil | false - "ssh" | ref(:p1) | nil | "all" | true - "http" | ref(:p1) | nil | "all" | true - "ssh" | ref(:p1) | nil | "ssh" | true - "http" | ref(:p1) | nil | "http" | true - "ssh" | ref(:p1) | nil | "http" | false - "http" | ref(:p1) | nil | "ssh" | false - "ssh" | ref(:p1) | "" | "all" | true - "http" | ref(:p1) | "" | "all" | true - "ssh" | ref(:p1) | "ssh" | "ssh" | true - "http" | ref(:p1) | "http" | "http" | true + "web" | nil | nil | nil | true + "ssh" | nil | nil | nil | true + "http" | nil | nil | nil | true + "ssh_certificates" | nil | nil | nil | true + "ssh" | nil | "" | nil | true + "http" | nil | "" | nil | true + "ssh_certificates" | nil | "" | nil | true + "ssh" | nil | "ssh" | nil | true + "http" | nil | "http" | nil | true + "ssh_certificates" | nil | "ssh_certificates" | nil | true + "ssh" | nil | "http" | nil | false + "http" | nil | "ssh" | nil | false + "ssh_certificates" | nil | "ssh" | nil | false + "ssh" | ref(:p1) | nil | "all" | true + "http" | ref(:p1) | nil | "all" | true + "ssh_certificates" | ref(:p1) | nil | "all" | true + "ssh" | ref(:p1) | nil | "ssh" | true + "http" | ref(:p1) | nil | "http" | true + "ssh_certificates" | ref(:p1) | nil | "ssh_certificates" | true + "ssh" | ref(:p1) | nil | "http" | false + "http" | ref(:p1) | nil | "ssh" | false + "ssh_certificates" | ref(:p1) | nil | "ssh" | false + "ssh" | ref(:p1) | "" | "all" | true + "http" | ref(:p1) | "" | "all" | true + "ssh_certificates" | ref(:p1) | "" | "all" | true + "ssh" | ref(:p1) | "ssh" | "ssh" | true + "http" | ref(:p1) | "http" | "http" | true + "ssh_certificates" | ref(:p1) | "ssh_certificates" | "ssh_certificates" | true end with_them do diff --git a/spec/lib/gitlab/puma/error_handler_spec.rb b/spec/lib/gitlab/puma/error_handler_spec.rb new file mode 100644 index 00000000000..5b7cdf37af1 --- /dev/null +++ b/spec/lib/gitlab/puma/error_handler_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Puma::ErrorHandler, feature_category: :shared do + subject { described_class.new(is_production) } + + let(:is_production) { true } + let(:ex) { StandardError.new('Sample error message') } + let(:env) { {} } + let(:status_code) { 500 } + + describe '#execute' do + it 'captures the exception and returns a Rack response' do + allow(Raven.configuration).to receive(:capture_allowed?).and_return(true) + expect(Raven).to receive(:capture_exception).with( + ex, + tags: { handler: 'puma_low_level' }, + extra: { puma_env: env, status_code: status_code } + ).and_call_original + + status, headers, message = subject.execute(ex, env, status_code) + + expect(status).to eq(500) + expect(headers).to eq({}) + expect(message).to eq(described_class::PROD_ERROR_MESSAGE) + end + + context 'when capture is not allowed' do + it 'returns a Rack response without capturing the exception' do + allow(Raven.configuration).to receive(:capture_allowed?).and_return(false) + expect(Raven).not_to receive(:capture_exception) + + status, headers, message = subject.execute(ex, env, status_code) + + expect(status).to eq(500) + expect(headers).to eq({}) + expect(message).to eq(described_class::PROD_ERROR_MESSAGE) + end + end + + context 'when not in production' do + let(:is_production) { false } + + it 'returns a Rack response with dev error message' do + allow(Raven.configuration).to receive(:capture_allowed?).and_return(true) + + status, headers, message = subject.execute(ex, env, status_code) + + expect(status).to eq(500) + expect(headers).to eq({}) + expect(message).to eq(described_class::DEV_ERROR_MESSAGE) + end + end + + context 'when status code is nil' do + let(:status_code) { 500 } + + it 'defaults to error 500' do + allow(Raven.configuration).to receive(:capture_allowed?).and_return(false) + expect(Raven).not_to receive(:capture_exception) + + status, headers, message = subject.execute(ex, env, status_code) + + expect(status).to eq(500) + expect(headers).to eq({}) + expect(message).to eq(described_class::PROD_ERROR_MESSAGE) + end + end + + context 'when status code is provided' do + let(:status_code) { 404 } + + it 'uses the provided status code in the response' do + allow(Raven.configuration).to receive(:capture_allowed?).and_return(true) + + status, headers, message = subject.execute(ex, env, status_code) + + expect(status).to eq(404) + expect(headers).to eq({}) + expect(message).to eq(described_class::PROD_ERROR_MESSAGE) + end + end + end +end diff --git a/spec/lib/gitlab/rack_attack/request_spec.rb b/spec/lib/gitlab/rack_attack/request_spec.rb index 9d2144f75db..92c9acb83cf 100644 --- a/spec/lib/gitlab/rack_attack/request_spec.rb +++ b/spec/lib/gitlab/rack_attack/request_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::RackAttack::Request do +RSpec.describe Gitlab::RackAttack::Request, feature_category: :rate_limiting do using RSpec::Parameterized::TableSyntax let(:path) { '/' } @@ -38,8 +38,12 @@ RSpec.describe Gitlab::RackAttack::Request do '/groups' | false '/foo/api' | false - '/api' | true + '/api' | false + '/api/' | true '/api/v4/groups/1' | true + + '/oauth/tokens' | true + '/oauth/userinfo' | true end with_them do @@ -53,6 +57,36 @@ RSpec.describe Gitlab::RackAttack::Request do it { is_expected.to eq(expected) } end end + + context 'when rate_limit_oauth_api feature flag is disabled' do + before do + stub_feature_flags(rate_limit_oauth_api: false) + end + + where(:path, :expected) do + '/' | false + '/groups' | false + '/foo/api' | false + + '/api' | true + '/api/v4/groups/1' | true + + '/oauth/tokens' | false + '/oauth/userinfo' | false + end + + with_them do + it { is_expected.to eq(expected) } + + context 'when the application is mounted at a relative URL' do + before do + stub_config_setting(relative_url_root: '/gitlab/root') + end + + it { is_expected.to eq(expected) } + end + end + end end describe '#api_internal_request?' do @@ -196,7 +230,8 @@ RSpec.describe Gitlab::RackAttack::Request do '/groups' | true '/foo/api' | true - '/api' | false + '/api' | true + '/api/' | false '/api/v4/groups/1' | false end diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb index ce21c2269cc..1745a745ec3 100644 --- a/spec/lib/gitlab/redis/multi_store_spec.rb +++ b/spec/lib/gitlab/redis/multi_store_spec.rb @@ -948,6 +948,55 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do end end + describe '#close' do + subject { multi_store.close } + + context 'when using both stores' do + before do + allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(true) + end + + it 'closes both stores' do + expect(primary_store).to receive(:close) + expect(secondary_store).to receive(:close) + + subject + end + end + + context 'when using only one store' do + before do + allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(false) + end + + context 'when using primary_store as default store' do + before do + allow(multi_store).to receive(:use_primary_store_as_default?).and_return(true) + end + + it 'closes primary store' do + expect(primary_store).to receive(:close) + expect(secondary_store).not_to receive(:close) + + subject + end + end + + context 'when using secondary_store as default store' do + before do + allow(multi_store).to receive(:use_primary_store_as_default?).and_return(false) + end + + it 'closes secondary store' do + expect(primary_store).not_to receive(:close) + expect(secondary_store).to receive(:close) + + subject + end + end + end + end + context 'with unsupported command' do let(:counter) { Gitlab::Metrics::NullMetric.instance } diff --git a/spec/lib/gitlab/redis/queues_metadata_spec.rb b/spec/lib/gitlab/redis/queues_metadata_spec.rb index 693e8074b45..1ac5c3b4e70 100644 --- a/spec/lib/gitlab/redis/queues_metadata_spec.rb +++ b/spec/lib/gitlab/redis/queues_metadata_spec.rb @@ -5,39 +5,4 @@ require 'spec_helper' RSpec.describe Gitlab::Redis::QueuesMetadata, feature_category: :redis do include_examples "redis_new_instance_shared_examples", 'queues_metadata', Gitlab::Redis::Queues include_examples "redis_shared_examples" - - describe '#pool' do - let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" } - let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" } - - subject { described_class.pool } - - around do |example| - clear_pool - example.run - ensure - clear_pool - end - - before do - allow(described_class).to receive(:config_file_name).and_return(config_new_format_host) - - allow(described_class).to receive(:config_file_name).and_return(config_new_format_host) - allow(Gitlab::Redis::Queues).to receive(:config_file_name).and_return(config_new_format_socket) - end - - it 'instantiates an instance of MultiStore' do - subject.with do |redis_instance| - expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore) - - expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99") - expect(redis_instance.secondary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0") - - expect(redis_instance.instance_name).to eq('QueuesMetadata') - end - end - - it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_queues_metadata, - :use_primary_store_as_default_for_queues_metadata - end end diff --git a/spec/lib/gitlab/redis/workhorse_spec.rb b/spec/lib/gitlab/redis/workhorse_spec.rb index 46931a6afcb..db5db18c732 100644 --- a/spec/lib/gitlab/redis/workhorse_spec.rb +++ b/spec/lib/gitlab/redis/workhorse_spec.rb @@ -2,43 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Redis::Workhorse, feature_category: :scalability do +RSpec.describe Gitlab::Redis::Workhorse, feature_category: :redis do include_examples "redis_new_instance_shared_examples", 'workhorse', Gitlab::Redis::SharedState include_examples "redis_shared_examples" - - describe '#pool' do - let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" } - let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" } - - subject { described_class.pool } - - before do - allow(described_class).to receive(:config_file_name).and_return(config_new_format_host) - - # Override rails root to avoid having our fixtures overwritten by `redis.yml` if it exists - allow(Gitlab::Redis::SharedState).to receive(:rails_root).and_return(mktmpdir) - allow(Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_socket) - end - - around do |example| - clear_pool - example.run - ensure - clear_pool - end - - it 'instantiates an instance of MultiStore' do - subject.with do |redis_instance| - expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore) - - expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99") - expect(redis_instance.secondary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0") - - expect(redis_instance.instance_name).to eq('Workhorse') - end - end - - it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_workhorse, - :use_primary_store_as_default_for_workhorse - end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 02ae3f63918..381f3a80799 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -86,33 +86,6 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do it { is_expected.to match('<any-Charact3r$|any-Charact3r$>') } end - describe '.group_path_regex' do - subject { described_class.group_path_regex } - - it { is_expected.not_to match('?gitlab') } - it { is_expected.not_to match("Users's something") } - it { is_expected.not_to match('/source') } - it { is_expected.not_to match('http:') } - it { is_expected.not_to match('https:') } - it { is_expected.not_to match('example.com/?stuff=true') } - it { is_expected.not_to match('example.com:5000/?stuff=true') } - it { is_expected.not_to match('http://gitlab.example/gitlab-org/manage/import/gitlab-migration-test') } - it { is_expected.not_to match('_good_for_me!') } - it { is_expected.not_to match('good_for+you') } - it { is_expected.not_to match('source/') } - it { is_expected.not_to match('.source/full./path') } - - it { is_expected.not_to match('source/full') } - it { is_expected.not_to match('source/full/path') } - it { is_expected.not_to match('.source/.full/.path') } - - it { is_expected.to match('source') } - it { is_expected.to match('.source') } - it { is_expected.to match('_source') } - it { is_expected.to match('domain_namespace') } - it { is_expected.to match('gitlab-migration-test') } - end - describe '.environment_name_regex' do subject { described_class.environment_name_regex } diff --git a/spec/lib/gitlab/saas_spec.rb b/spec/lib/gitlab/saas_spec.rb index a8656c44831..3be0a6c7bf0 100644 --- a/spec/lib/gitlab/saas_spec.rb +++ b/spec/lib/gitlab/saas_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require 'support/helpers/saas_test_helper' -RSpec.describe Gitlab::Saas do +RSpec.describe Gitlab::Saas, feature_category: :shared do include SaasTestHelper describe '.canary_toggle_com_url' do diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index d1f19a5e1ba..00e68f73d2d 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -465,6 +465,6 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do expect(results.objects(scope)).to match_array([milestone_1, milestone_2, milestone_3]) end - include_examples 'search results filtered by archived', 'search_milestones_hide_archived_projects' + include_examples 'search results filtered by archived' end end diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 049b8d4ed86..22220efaa05 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -13,8 +13,6 @@ RSpec.describe Gitlab::Shell do described_class.instance_variable_set(:@secret_token, nil) end - it { is_expected.to respond_to :remove_repository } - describe '.secret_token' do let(:secret_file) { 'tmp/tests/.secret_shell_test' } let(:link_file) { 'tmp/tests/shell-secret-test/.gitlab_shell_secret' } @@ -74,67 +72,11 @@ RSpec.describe Gitlab::Shell do end end - describe 'projects commands' do - let(:gitlab_shell_path) { File.expand_path('tmp/tests/gitlab-shell') } - let(:projects_path) { File.join(gitlab_shell_path, 'bin/gitlab-projects') } - - before do - allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(gitlab_shell_path) - allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) - end - - describe '#remove_repository' do - let!(:project) { create(:project, :repository, :legacy_storage) } - let(:disk_path) { "#{project.disk_path}.git" } - - it 'returns true when the command succeeds' do - expect(project.repository.raw).to exist - - expect(gitlab_shell.remove_repository(project.repository_storage, project.disk_path)).to be(true) - - expect(project.repository.raw).not_to exist - end - end - - describe '#mv_repository' do - let!(:project2) { create(:project, :repository) } - - it 'returns true when the command succeeds' do - old_repo = project2.repository.raw - new_path = "project/new_path" - new_repo = Gitlab::Git::Repository.new(project2.repository_storage, "#{new_path}.git", nil, nil) - - expect(old_repo).to exist - expect(new_repo).not_to exist - - expect(gitlab_shell.mv_repository(project2.repository_storage, project2.disk_path, new_path)).to be_truthy - - expect(old_repo).not_to exist - expect(new_repo).to exist - end - - it 'returns false when the command fails' do - expect(gitlab_shell.mv_repository(project2.repository_storage, project2.disk_path, '')).to be_falsy - expect(project2.repository.raw).to exist - end - end - end - describe 'namespace actions' do subject { described_class.new } let(:storage) { Gitlab.config.repositories.storages.each_key.first } - describe '#add_namespace' do - it 'creates a namespace' do - Gitlab::GitalyClient::NamespaceService.allow do - subject.add_namespace(storage, "mepmep") - - expect(Gitlab::GitalyClient::NamespaceService.new(storage).exists?("mepmep")).to be(true) - end - end - end - describe '#repository_exists?' do context 'when the repository does not exist' do it 'returns false' do @@ -150,28 +92,5 @@ RSpec.describe Gitlab::Shell do end end end - - describe '#remove' do - it 'removes the namespace' do - Gitlab::GitalyClient::NamespaceService.allow do - subject.add_namespace(storage, "mepmep") - subject.rm_namespace(storage, "mepmep") - - expect(Gitlab::GitalyClient::NamespaceService.new(storage).exists?("mepmep")).to be(false) - end - end - end - - describe '#mv_namespace' do - it 'renames the namespace' do - Gitlab::GitalyClient::NamespaceService.allow do - subject.add_namespace(storage, "mepmep") - subject.mv_namespace(storage, "mepmep", "2mep") - - expect(Gitlab::GitalyClient::NamespaceService.new(storage).exists?("mepmep")).to be(false) - expect(Gitlab::GitalyClient::NamespaceService.new(storage).exists?("2mep")).to be(true) - end - end - end end end diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb index 937a1751cc7..7138ad04f69 100644 --- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb @@ -3,8 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, - :clean_gitlab_redis_queues, :clean_gitlab_redis_shared_state, :clean_gitlab_redis_queues_metadata, - feature_category: :shared do + :clean_gitlab_redis_queues_metadata, feature_category: :shared do using RSpec::Parameterized::TableSyntax subject(:duplicate_job) do @@ -79,7 +78,11 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, end end - shared_examples 'with Redis cookies' do + context 'with Redis cookies' do + def with_redis(&block) + Gitlab::Redis::QueuesMetadata.with(&block) + end + let(:cookie_key) { "#{Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE}:#{idempotency_key}:cookie:v2" } let(:cookie) { get_redis_msgpack(cookie_key) } @@ -413,62 +416,6 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, end end - context 'with multi-store feature flags turned on' do - def with_redis(&block) - Gitlab::Redis::QueuesMetadata.with(&block) - end - - shared_examples 'uses QueuesMetadata' do - it 'use Gitlab::Redis::QueuesMetadata.with' do - expect(Gitlab::Redis::QueuesMetadata).to receive(:with).and_call_original - expect(Gitlab::Redis::Queues).not_to receive(:with) - - duplicate_job.check! - end - end - - context 'when migration is ongoing with double-write' do - before do - stub_feature_flags(use_primary_store_as_default_for_queues_metadata: false) - end - - it_behaves_like 'uses QueuesMetadata' - it_behaves_like 'with Redis cookies' - end - - context 'when migration is completed' do - before do - stub_feature_flags(use_primary_and_secondary_stores_for_queues_metadata: false) - end - - it_behaves_like 'uses QueuesMetadata' - it_behaves_like 'with Redis cookies' - end - - it_behaves_like 'uses QueuesMetadata' - it_behaves_like 'with Redis cookies' - end - - context 'when both multi-store feature flags are off' do - def with_redis(&block) - Gitlab::Redis::Queues.with(&block) - end - - before do - stub_feature_flags(use_primary_and_secondary_stores_for_queues_metadata: false) - stub_feature_flags(use_primary_store_as_default_for_queues_metadata: false) - end - - it 'use Gitlab::Redis::Queues' do - expect(Gitlab::Redis::Queues).to receive(:with).and_call_original - expect(Gitlab::Redis::QueuesMetadata).not_to receive(:with) - - duplicate_job.check! - end - - it_behaves_like 'with Redis cookies' - end - describe '#scheduled?' do it 'returns false for non-scheduled jobs' do expect(duplicate_job.scheduled?).to be(false) diff --git a/spec/lib/gitlab/sidekiq_middleware/extra_done_log_metadata_spec.rb b/spec/lib/gitlab/sidekiq_middleware/extra_done_log_metadata_spec.rb index dbab67f5996..5569bc01a6a 100644 --- a/spec/lib/gitlab/sidekiq_middleware/extra_done_log_metadata_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/extra_done_log_metadata_spec.rb @@ -26,10 +26,18 @@ RSpec.describe Gitlab::SidekiqMiddleware::ExtraDoneLogMetadata do expect(job).to eq({ 'jid' => 123, 'extra.admin_email_worker.key1' => 15, 'extra.admin_email_worker.key2' => 16 }) end - it 'does not raise when the worker does not respond to #done_log_extra_metadata' do + it 'does not raise when the worker does not respond to #logging_extras' do expect { |b| subject.call(worker_without_application_worker, job, queue, &b) }.to yield_control expect(job).to eq({ 'jid' => 123 }) end + + it 'still merges logging_extras even when an error is raised during job execution' do + worker.log_extra_metadata_on_done(:key1, 15) + worker.log_extra_metadata_on_done(:key2, 16) + expect { subject.call(worker, job, queue) { raise 'an error' } }.to raise_error(StandardError, 'an error') + + expect(job).to eq({ 'jid' => 123, 'extra.admin_email_worker.key1' => 15, 'extra.admin_email_worker.key2' => 16 }) + end end end diff --git a/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb b/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb index 620de7e7671..2fa0e44d44f 100644 --- a/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb @@ -23,76 +23,76 @@ RSpec.describe Gitlab::SidekiqMiddleware::SkipJobs, feature_category: :scalabili describe '#call' do context 'with worker not opted for database health check' do - describe "with all combinations of drop and defer FFs" do - using RSpec::Parameterized::TableSyntax + let(:metric) { instance_double(Prometheus::Client::Counter, increment: true) } - let(:metric) { instance_double(Prometheus::Client::Counter, increment: true) } - - shared_examples 'runs the job normally' do - it 'yields control' do - expect { |b| subject.call(TestWorker.new, job, queue, &b) }.to yield_control - end + shared_examples 'runs the job normally' do + it 'yields control' do + expect { |b| subject.call(TestWorker.new, job, queue, &b) }.to yield_control + end - it 'does not increment any metric counter' do - expect(metric).not_to receive(:increment) + it 'does not increment any metric counter' do + expect(metric).not_to receive(:increment) - subject.call(TestWorker.new, job, queue) { nil } - end + subject.call(TestWorker.new, job, queue) { nil } + end - it 'does not increment deferred_count' do - subject.call(TestWorker.new, job, queue) { nil } + it 'does not increment deferred_count' do + subject.call(TestWorker.new, job, queue) { nil } - expect(job).not_to include('deferred_count') - end + expect(job).not_to include('deferred_count') end + end - shared_examples 'drops the job' do - it 'does not yield control' do - expect { |b| subject.call(TestWorker.new, job, queue, &b) }.not_to yield_control - end + shared_examples 'drops the job' do + it 'does not yield control' do + expect { |b| subject.call(TestWorker.new, job, queue, &b) }.not_to yield_control + end - it 'increments counter' do - expect(metric).to receive(:increment).with({ worker: "TestWorker", action: "dropped" }) + it 'increments counter' do + expect(metric).to receive(:increment).with({ worker: "TestWorker", action: "dropped" }) - subject.call(TestWorker.new, job, queue) { nil } - end + subject.call(TestWorker.new, job, queue) { nil } + end - it 'does not increment deferred_count' do - subject.call(TestWorker.new, job, queue) { nil } + it 'does not increment deferred_count' do + subject.call(TestWorker.new, job, queue) { nil } - expect(job).not_to include('deferred_count') - end + expect(job).not_to include('deferred_count') + end - it 'has dropped field in job equal to true' do - subject.call(TestWorker.new, job, queue) { nil } + it 'has dropped field in job equal to true' do + subject.call(TestWorker.new, job, queue) { nil } - expect(job).to include({ 'dropped' => true }) - end + expect(job).to include({ 'dropped' => true }) end + end - shared_examples 'defers the job' do - it 'does not yield control' do - expect { |b| subject.call(TestWorker.new, job, queue, &b) }.not_to yield_control - end + shared_examples 'defers the job' do + it 'does not yield control' do + expect { |b| subject.call(TestWorker.new, job, queue, &b) }.not_to yield_control + end - it 'delays the job' do - expect(TestWorker).to receive(:perform_in).with(described_class::DELAY, *job['args']) + it 'delays the job' do + expect(TestWorker).to receive(:perform_in).with(described_class::DELAY, *job['args']) - subject.call(TestWorker.new, job, queue) { nil } - end + subject.call(TestWorker.new, job, queue) { nil } + end - it 'increments counter' do - expect(metric).to receive(:increment).with({ worker: "TestWorker", action: "deferred" }) + it 'increments counter' do + expect(metric).to receive(:increment).with({ worker: "TestWorker", action: "deferred" }) - subject.call(TestWorker.new, job, queue) { nil } - end + subject.call(TestWorker.new, job, queue) { nil } + end - it 'has deferred related fields in job payload' do - subject.call(TestWorker.new, job, queue) { nil } + it 'has deferred related fields in job payload' do + subject.call(TestWorker.new, job, queue) { nil } - expect(job).to include({ 'deferred' => true, 'deferred_by' => :feature_flag, 'deferred_count' => 1 }) - end + expect(job).to include({ 'deferred' => true, 'deferred_by' => :feature_flag, 'deferred_count' => 1 }) end + end + + describe "with all combinations of drop and defer FFs" do + using RSpec::Parameterized::TableSyntax before do stub_feature_flags("drop_sidekiq_jobs_#{TestWorker.name}": drop_ff) @@ -112,6 +112,45 @@ RSpec.describe Gitlab::SidekiqMiddleware::SkipJobs, feature_category: :scalabili it_behaves_like params[:resulting_behavior] end end + + describe 'using current_request actor', :request_store do + before do + allow(Gitlab::Metrics).to receive(:counter).and_call_original + allow(Gitlab::Metrics).to receive(:counter).with(described_class::COUNTER, anything).and_return(metric) + end + + context 'with drop_sidekiq_jobs FF' do + before do + stub_feature_flags("drop_sidekiq_jobs_#{TestWorker.name}": Feature.current_request) + end + + it_behaves_like 'drops the job' + + context 'for different request' do + before do + stub_with_new_feature_current_request + end + + it_behaves_like 'runs the job normally' + end + end + + context 'with run_sidekiq_jobs FF' do + before do + stub_feature_flags("run_sidekiq_jobs_#{TestWorker.name}": Feature.current_request) + end + + it_behaves_like 'runs the job normally' + + context 'for different request' do + before do + stub_with_new_feature_current_request + end + + it_behaves_like 'defers the job' + end + end + end end context 'with worker opted for database health check' do diff --git a/spec/lib/gitlab/slash_commands/run_spec.rb b/spec/lib/gitlab/slash_commands/run_spec.rb index 9d204228d21..5d228a9ba6a 100644 --- a/spec/lib/gitlab/slash_commands/run_spec.rb +++ b/spec/lib/gitlab/slash_commands/run_spec.rb @@ -39,16 +39,6 @@ RSpec.describe Gitlab::SlashCommands::Run do expect(described_class.available?(project)).to eq(false) end - - it 'returns false when chatops is not available' do - allow(Gitlab::Chat) - .to receive(:available?) - .and_return(false) - - project = double(:project, builds_enabled?: true) - - expect(described_class.available?(project)).to eq(false) - end end describe '.allowed?' do diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index cfd40fb93b5..0f827921a66 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -66,7 +66,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only, feature_category: :sh include_context 'when instance configured to deny all requests' it 'blocks the request' do - expect { subject }.to raise_error(described_class::BlockedUrlError) + expect { subject }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end @@ -83,7 +83,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only, feature_category: :sh let(:arg_value) { proc { true } } it 'blocks the request' do - expect { subject }.to raise_error(described_class::BlockedUrlError) + expect { subject }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end @@ -99,7 +99,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only, feature_category: :sh let(:arg_value) { true } it 'blocks the request' do - expect { subject }.to raise_error(described_class::BlockedUrlError) + expect { subject }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end @@ -228,7 +228,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only, feature_category: :sh let(:lfs_enabled) { false } it 'raises an error' do - expect { subject }.to raise_error(described_class::BlockedUrlError) + expect { subject }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end @@ -236,7 +236,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only, feature_category: :sh let(:lfs_enabled) { true } it 'raises an error' do - expect { subject }.to raise_error(described_class::BlockedUrlError) + expect { subject }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end end @@ -251,7 +251,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only, feature_category: :sh end it 'raises an error' do - expect { subject }.to raise_error(described_class::BlockedUrlError) + expect { subject }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end @@ -259,7 +259,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only, feature_category: :sh let(:host) { 'http://127.0.0.1:9000' } it 'raises an error' do - expect { subject }.to raise_error(described_class::BlockedUrlError) + expect { subject }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end end @@ -290,7 +290,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only, feature_category: :sh end it 'raises an error' do - expect { subject }.to raise_error(described_class::BlockedUrlError) + expect { subject }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end context 'with HTTP_PROXY' do @@ -324,7 +324,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only, feature_category: :sh let(:import_url) { "https://example#{'a' * 1024}.com" } it 'raises an error' do - expect { subject }.to raise_error(described_class::BlockedUrlError) + expect { subject }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end end @@ -346,7 +346,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only, feature_category: :sh it 'raises an error' do stub_env('RSPEC_ALLOW_INVALID_URLS', 'false') - expect { subject }.to raise_error(described_class::BlockedUrlError) + expect { subject }.to raise_error(Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError) end end end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 865a8384405..68eb38a1335 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -23,7 +23,8 @@ RSpec.describe Gitlab::UrlBuilder do :commit | ->(commit) { "/#{commit.project.full_path}/-/commit/#{commit.id}" } :issue | ->(issue) { "/#{issue.project.full_path}/-/issues/#{issue.iid}" } [:issue, :task] | ->(issue) { "/#{issue.project.full_path}/-/work_items/#{issue.iid}" } - :work_item | ->(work_item) { "/#{work_item.project.full_path}/-/work_items/#{work_item.iid}" } + [:work_item, :task] | ->(work_item) { "/#{work_item.project.full_path}/-/work_items/#{work_item.iid}" } + [:work_item, :issue] | ->(work_item) { "/#{work_item.project.full_path}/-/issues/#{work_item.iid}" } :merge_request | ->(merge_request) { "/#{merge_request.project.full_path}/-/merge_requests/#{merge_request.iid}" } :project_milestone | ->(milestone) { "/#{milestone.project.full_path}/-/milestones/#{milestone.iid}" } :project_snippet | ->(snippet) { "/#{snippet.project.full_path}/-/snippets/#{snippet.id}" } @@ -59,7 +60,8 @@ RSpec.describe Gitlab::UrlBuilder do :discussion_note_on_project_snippet | ->(note) { "/#{note.project.full_path}/-/snippets/#{note.noteable_id}#note_#{note.id}" } :discussion_note_on_personal_snippet | ->(note) { "/-/snippets/#{note.noteable_id}#note_#{note.id}" } :note_on_personal_snippet | ->(note) { "/-/snippets/#{note.noteable_id}#note_#{note.id}" } - :package | ->(package) { "/#{package.project.full_path}/-/packages/#{package.id}" } + :note_on_abuse_report | ->(note) { "/admin/abuse_reports/#{note.noteable_id}#note_#{note.id}" } + :package | ->(package) { "/#{package.project.full_path}/-/packages/#{package.id}" } end with_them do diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb index 6695736e54c..51d3090c825 100644 --- a/spec/lib/gitlab/usage/metric_definition_spec.rb +++ b/spec/lib/gitlab/usage/metric_definition_spec.rb @@ -40,13 +40,10 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping File.write(path, content) end - after do - # Reset memoized `definitions` result - described_class.instance_variable_set(:@definitions, nil) - end - - it 'has all definitons valid' do - expect { described_class.definitions }.not_to raise_error + it 'has only valid definitions' do + described_class.all.each do |definition| + expect { definition.validate! }.not_to raise_error + end end describe 'not_removed' do @@ -126,11 +123,13 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping context 'with data_source redis metric' do before do attributes[:data_source] = 'redis' - attributes[:options] = { prefix: 'web_ide', event: 'views_count', include_usage_prefix: false } + attributes[:events] = [ + { name: 'web_ide_viewed' } + ] end - it 'returns a ServicePingContext with redis key as event_name' do - expect(subject.to_h[:data][:event_name]).to eq('WEB_IDE_VIEWS_COUNT') + it 'returns a ServicePingContext with first event as event_name' do + expect(subject.to_h[:data][:event_name]).to eq('web_ide_viewed') end end @@ -182,20 +181,6 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping described_class.new(path, attributes).validate! end - - context 'with skip_validation' do - it 'raise exception if skip_validation: false' do - expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError)) - - described_class.new(path, attributes.merge( { skip_validation: false } )).validate! - end - - it 'does not raise exception if has skip_validation: true' do - expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception) - - described_class.new(path, attributes.merge( { skip_validation: true } )).validate! - end - end end context 'conditional validations' do @@ -358,71 +343,4 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping is_expected.to eq([attributes, other_attributes].map(&:deep_stringify_keys).to_yaml) end end - - describe '.metric_definitions_changed?', :freeze_time do - let(:metric1) { Dir.mktmpdir('metric1') } - let(:metric2) { Dir.mktmpdir('metric2') } - - before do - allow(Rails).to receive_message_chain(:env, :development?).and_return(is_dev) - allow(described_class).to receive(:paths).and_return( - [ - File.join(metric1, '**', '*.yml'), - File.join(metric2, '**', '*.yml') - ] - ) - - write_metric(metric1, path, yaml_content) - write_metric(metric2, path, yaml_content) - end - - after do - FileUtils.rm_rf(metric1) - FileUtils.rm_rf(metric2) - end - - context 'in development', :freeze_time do - let(:is_dev) { true } - - it 'has changes on the first invocation' do - expect(described_class.metric_definitions_changed?).to be_truthy - end - - context 'when no files are changed' do - it 'does not have changes on the second invocation' do - described_class.metric_definitions_changed? - - expect(described_class.metric_definitions_changed?).to be_falsy - end - end - - context 'when file is changed' do - it 'has changes on the next invocation when more than 3 seconds have passed' do - described_class.metric_definitions_changed? - - write_metric(metric1, path, yaml_content) - travel_to 10.seconds.from_now - - expect(described_class.metric_definitions_changed?).to be_truthy - end - - it 'does not have changes on the next invocation when less than 3 seconds have passed' do - described_class.metric_definitions_changed? - - write_metric(metric1, path, yaml_content) - travel_to 1.second.from_now - - expect(described_class.metric_definitions_changed?).to be_falsy - end - end - - context 'in production' do - let(:is_dev) { false } - - it 'does not detect changes' do - expect(described_class.metric_definitions_changed?).to be_falsy - end - end - end - end end diff --git a/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb index 59b944ac398..18a97447f1c 100644 --- a/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb +++ b/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb @@ -88,10 +88,12 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::PostgresHll, :clean_ describe '.save_aggregated_metrics' do subject(:save_aggregated_metrics) do - described_class.save_aggregated_metrics(metric_name: metric_1, - time_period: time_period, - recorded_at_timestamp: recorded_at, - data: data) + described_class.save_aggregated_metrics( + metric_name: metric_1, + time_period: time_period, + recorded_at_timestamp: recorded_at, + data: data + ) end context 'with compatible data argument' do diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/container_registry_db_enabled_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/container_registry_db_enabled_metric_spec.rb new file mode 100644 index 00000000000..605764cd7f8 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/container_registry_db_enabled_metric_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::ContainerRegistryDbEnabledMetric, feature_category: :service_ping do + let(:expected_value) { Gitlab::CurrentSettings.container_registry_db_enabled } + + it_behaves_like 'a correct instrumented metric value', { time_frame: 'none' } +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb index 77c49d448d7..2b6e17f615c 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiInternalPipelinesMetric, -feature_category: :service_ping do + feature_category: :service_ping do let_it_be(:ci_pipeline_1) { create(:ci_pipeline, source: :external, created_at: 3.days.ago) } let_it_be(:ci_pipeline_2) { create(:ci_pipeline, source: :push, created_at: 3.days.ago) } let_it_be(:old_pipeline) { create(:ci_pipeline, source: :push, created_at: 2.months.ago) } diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_csv_imports_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_csv_imports_metric_spec.rb new file mode 100644 index 00000000000..2b481563ecd --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_csv_imports_metric_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCsvImportsMetric, feature_category: :service_ping do + let_it_be(:user) { create(:user) } + + let_it_be(:old_import) { create(:issue_csv_import, user: user, created_at: 2.months.ago) } + let_it_be(:new_import) { create(:issue_csv_import, user: user, created_at: 21.days.ago) } + + context 'with all time frame' do + let(:expected_value) { 2 } + let(:expected_query) do + %q{SELECT COUNT("csv_issue_imports"."id") FROM "csv_issue_imports"} + end + + it_behaves_like 'a correct instrumented metric value and query', time_frame: 'all' + end + + context 'for 28d time frame' do + let(:expected_value) { 1 } + let(:start) { 30.days.ago.to_fs(:db) } + let(:finish) { 2.days.ago.to_fs(:db) } + let(:expected_query) do + "SELECT COUNT(\"csv_issue_imports\".\"id\") FROM \"csv_issue_imports\" " \ + "WHERE \"csv_issue_imports\".\"created_at\" " \ + "BETWEEN '#{start}' AND '#{finish}'" + end + + it_behaves_like 'a correct instrumented metric value and query', time_frame: '28d' + end +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb index 65e514bf345..56b847257a5 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountIssuesCreatedManuallyFromAlertsMetric, -feature_category: :service_ping do + feature_category: :service_ping do let_it_be(:issue) { create(:issue) } let_it_be(:issue_with_alert) { create(:issue, :with_alert) } diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_jira_imports_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_jira_imports_metric_spec.rb new file mode 100644 index 00000000000..9a51c3cc408 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_jira_imports_metric_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountJiraImportsMetric, feature_category: :service_ping do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, creator_id: user.id) } + + let_it_be(:old_import) { create(:jira_import_state, :finished, project: project, created_at: 2.months.ago) } + let_it_be(:new_import) { create(:jira_import_state, :finished, project: project, created_at: 21.days.ago) } + + context 'with all time frame' do + let(:expected_value) { 2 } + let(:expected_query) do + %q{SELECT COUNT("jira_imports"."id") FROM "jira_imports"} + end + + it_behaves_like 'a correct instrumented metric value and query', time_frame: 'all' + end + + context 'for 28d time frame' do + let(:expected_value) { 1 } + let(:start) { 30.days.ago.to_fs(:db) } + let(:finish) { 2.days.ago.to_fs(:db) } + let(:expected_query) do + "SELECT COUNT(\"jira_imports\".\"id\") FROM \"jira_imports\" WHERE \"jira_imports\".\"created_at\" " \ + "BETWEEN '#{start}' AND '#{finish}'" + end + + it_behaves_like 'a correct instrumented metric value and query', time_frame: '28d' + end +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_packages_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_packages_metric_spec.rb new file mode 100644 index 00000000000..9a2e5c27c1d --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_packages_metric_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountPackagesMetric, feature_category: :service_ping do + before_all do + create :package, created_at: 2.months.ago + create :package, created_at: 21.days.ago + create :package, created_at: 7.days.ago + end + + context "with all time frame" do + let(:expected_value) { 3 } + let(:expected_query) do + 'SELECT COUNT("packages_packages"."id") FROM "packages_packages"' + end + + it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all' } + end + + context "with 28d time frame" do + let(:expected_value) { 2 } + let(:start) { 30.days.ago.to_fs(:db) } + let(:finish) { 2.days.ago.to_fs(:db) } + let(:expected_query) do + 'SELECT COUNT("packages_packages"."id") FROM "packages_packages" ' \ + 'WHERE "packages_packages"."created_at" ' \ + "BETWEEN '#{start}' AND '#{finish}'" + end + + it_behaves_like 'a correct instrumented metric value and query', { time_frame: '28d' } + end +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_metric_spec.rb new file mode 100644 index 00000000000..28185fb9df4 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_metric_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountProjectsMetric, feature_category: :service_ping do + before_all do + create :project, created_at: 2.months.ago + create :project, created_at: 21.days.ago + create :project, created_at: 7.days.ago + end + + context "with all time frame" do + let(:expected_value) { 3 } + let(:expected_query) do + 'SELECT COUNT("projects"."id") FROM "projects"' + end + + it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all' } + end + + context "with 28d time frame" do + let(:expected_value) { 2 } + let(:start) { 30.days.ago.to_fs(:db) } + let(:finish) { 2.days.ago.to_fs(:db) } + let(:expected_query) do + 'SELECT COUNT("projects"."id") FROM "projects" ' \ + 'WHERE "projects"."created_at" ' \ + "BETWEEN '#{start}' AND '#{finish}'" + end + + it_behaves_like 'a correct instrumented metric value and query', { time_frame: '28d' } + end +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric_spec.rb index cb94da11d58..91ad81c4291 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric_spec.rb @@ -9,10 +9,10 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::InProductMarketingEmail let(:options) { { track: 'verify', series: 0 } } let(:expected_value) { 2 } let(:expected_query) do - 'SELECT COUNT("in_product_marketing_emails"."id") FROM "in_product_marketing_emails"' \ - ' WHERE "in_product_marketing_emails"."cta_clicked_at" IS NOT NULL' \ - ' AND "in_product_marketing_emails"."series" = 0'\ - ' AND "in_product_marketing_emails"."track" = 1' + 'SELECT COUNT("in_product_marketing_emails"."id") FROM "in_product_marketing_emails" ' \ + 'WHERE "in_product_marketing_emails"."cta_clicked_at" IS NOT NULL ' \ + 'AND "in_product_marketing_emails"."series" = 0 ' \ + 'AND "in_product_marketing_emails"."track" = 1' end before do diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric_spec.rb index 0cc82773d56..3c51368f396 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric_spec.rb @@ -8,9 +8,9 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::InProductMarketingEmail let(:email_attributes) { { track: 'verify', series: 0 } } let(:expected_value) { 2 } let(:expected_query) do - 'SELECT COUNT("in_product_marketing_emails"."id") FROM "in_product_marketing_emails"' \ - ' WHERE "in_product_marketing_emails"."series" = 0'\ - ' AND "in_product_marketing_emails"."track" = 1' + 'SELECT COUNT("in_product_marketing_emails"."id") FROM "in_product_marketing_emails" ' \ + 'WHERE "in_product_marketing_emails"."series" = 0 ' \ + 'AND "in_product_marketing_emails"."track" = 1' end before do diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/incoming_email_encrypted_secrets_enabled_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/incoming_email_encrypted_secrets_enabled_metric_spec.rb index b1b193c8d04..ad1f231a12d 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/incoming_email_encrypted_secrets_enabled_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/incoming_email_encrypted_secrets_enabled_metric_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Usage::Metrics::Instrumentations::IncomingEmailEncryptedSecretsEnabledMetric, -feature_category: :service_ping do + feature_category: :service_ping do it_behaves_like 'a correct instrumented metric value', { time_frame: 'none', data_source: 'ruby' } do let(:expected_value) { ::Gitlab::Email::IncomingEmail.encrypted_secrets.active? } end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/service_desk_email_encrypted_secrets_enabled_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/service_desk_email_encrypted_secrets_enabled_metric_spec.rb index ea239e53d01..dae7f17a3b6 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/service_desk_email_encrypted_secrets_enabled_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/service_desk_email_encrypted_secrets_enabled_metric_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Usage::Metrics::Instrumentations::ServiceDeskEmailEncryptedSecretsEnabledMetric, -feature_category: :service_ping do + feature_category: :service_ping do it_behaves_like 'a correct instrumented metric value', { time_frame: 'none', data_source: 'ruby' } do let(:expected_value) { ::Gitlab::Email::ServiceDeskEmail.encrypted_secrets.active? } end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/total_count_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/total_count_metric_spec.rb new file mode 100644 index 00000000000..f3aa1ba4f88 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/total_count_metric_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::TotalCountMetric, :clean_gitlab_redis_shared_state, + feature_category: :product_analytics_data_management do + before do + allow(Gitlab::InternalEvents::EventDefinitions).to receive(:known_event?).and_return(true) + end + + context 'with multiple similar events' do + let(:expected_value) { 10 } + + before do + 10.times do + Gitlab::InternalEvents.track_event('my_event') + end + end + + it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', events: [{ name: 'my_event' }] } + end + + context 'with multiple different events' do + let(:expected_value) { 2 } + + before do + Gitlab::InternalEvents.track_event('my_event1') + Gitlab::InternalEvents.track_event('my_event2') + end + + it_behaves_like 'a correct instrumented metric value', + { time_frame: 'all', events: [{ name: 'my_event1' }, { name: 'my_event2' }] } + end + + describe '.redis_key' do + it 'adds the key prefix to the event name' do + expect(described_class.redis_key('my_event')).to eq('{event_counters}_my_event') + end + end +end diff --git a/spec/lib/gitlab/usage/metrics/query_spec.rb b/spec/lib/gitlab/usage/metrics/query_spec.rb index 750d340551a..418bbf322d0 100644 --- a/spec/lib/gitlab/usage/metrics/query_spec.rb +++ b/spec/lib/gitlab/usage/metrics/query_spec.rb @@ -75,9 +75,9 @@ RSpec.describe Gitlab::Usage::Metrics::Query do describe '.histogram' do it 'returns the histogram sql' do - expect(described_class.for(:histogram, AlertManagement::HttpIntegration.active, - :project_id, buckets: 1..2, bucket_size: 101)) - .to match(/^WITH "count_cte" AS MATERIALIZED/) + expect(described_class.for( + :histogram, AlertManagement::HttpIntegration.active, :project_id, buckets: 1..2, bucket_size: 101 + )).to match(/^WITH "count_cte" AS MATERIALIZED/) end end diff --git a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb index eeef9406841..2c9506dd498 100644 --- a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb @@ -17,24 +17,24 @@ RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter, feature_categ described_class.ci_template_event_name(expanded_template_name, config_source) end - it "has an event defined for template" do + it 'has an event defined for template' do expect do subject end.not_to raise_error end - it "tracks template" do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to(receive(:track_event)).with(template_name, values: project.id) + it 'tracks template' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event).with(template_name, values: project.id).once + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event).with('ci_template_included', values: project.id).once subject end - it_behaves_like 'Snowplow event tracking with RedisHLL context' do - let(:category) { described_class.to_s } - let(:action) { 'ci_templates_unique' } + it_behaves_like 'internal event tracking' do + let(:event) { 'ci_template_included' } let(:namespace) { project.namespace } - let(:label) { 'redis_hll_counters.ci_templates.ci_templates_total_unique_counts_monthly' } - let(:context) { [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: template_name).to_context] } end end diff --git a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb index ab92b59c845..71e9e7a8e7d 100644 --- a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb @@ -35,7 +35,7 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red end context 'for web IDE edit actions' do - let(:action) { described_class::EDIT_BY_WEB_IDE } + let(:event) { described_class::EDIT_BY_WEB_IDE } it_behaves_like 'tracks and counts action' do def track_action(params) @@ -49,7 +49,7 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red end context 'for SFE edit actions' do - let(:action) { described_class::EDIT_BY_SFE } + let(:event) { described_class::EDIT_BY_SFE } it_behaves_like 'tracks and counts action' do def track_action(params) @@ -63,7 +63,7 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red end context 'for snippet editor edit actions' do - let(:action) { described_class::EDIT_BY_SNIPPET_EDITOR } + let(:event) { described_class::EDIT_BY_SNIPPET_EDITOR } it_behaves_like 'tracks and counts action' do def track_action(params) diff --git a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb index 21a820deaa4..2c2bdbeb3e6 100644 --- a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue title edit actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_TITLE_CHANGED } + let(:event) { described_class::ISSUE_TITLE_CHANGED } subject(:track_event) { described_class.track_issue_title_changed_action(author: user, project: project) } end @@ -21,7 +21,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue description edit actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_DESCRIPTION_CHANGED } + let(:event) { described_class::ISSUE_DESCRIPTION_CHANGED } subject(:track_event) { described_class.track_issue_description_changed_action(author: user, project: project) } end @@ -29,7 +29,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue assignee edit actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_ASSIGNEE_CHANGED } + let(:event) { described_class::ISSUE_ASSIGNEE_CHANGED } subject(:track_event) { described_class.track_issue_assignee_changed_action(author: user, project: project) } end @@ -37,7 +37,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue make confidential actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_MADE_CONFIDENTIAL } + let(:event) { described_class::ISSUE_MADE_CONFIDENTIAL } subject(:track_event) { described_class.track_issue_made_confidential_action(author: user, project: project) } end @@ -45,7 +45,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue make visible actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_MADE_VISIBLE } + let(:event) { described_class::ISSUE_MADE_VISIBLE } subject(:track_event) { described_class.track_issue_made_visible_action(author: user, project: project) } end @@ -53,7 +53,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue created actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_CREATED } + let(:event) { described_class::ISSUE_CREATED } let(:project) { nil } subject(:track_event) { described_class.track_issue_created_action(author: user, namespace: namespace) } @@ -62,7 +62,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue closed actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_CLOSED } + let(:event) { described_class::ISSUE_CLOSED } subject(:track_event) { described_class.track_issue_closed_action(author: user, project: project) } end @@ -70,7 +70,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue reopened actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_REOPENED } + let(:event) { described_class::ISSUE_REOPENED } subject(:track_event) { described_class.track_issue_reopened_action(author: user, project: project) } end @@ -78,7 +78,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue label changed actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_LABEL_CHANGED } + let(:event) { described_class::ISSUE_LABEL_CHANGED } subject(:track_event) { described_class.track_issue_label_changed_action(author: user, project: project) } end @@ -86,7 +86,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue label milestone actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_MILESTONE_CHANGED } + let(:event) { described_class::ISSUE_MILESTONE_CHANGED } subject(:track_event) { described_class.track_issue_milestone_changed_action(author: user, project: project) } end @@ -94,7 +94,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue cross-referenced actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_CROSS_REFERENCED } + let(:event) { described_class::ISSUE_CROSS_REFERENCED } subject(:track_event) { described_class.track_issue_cross_referenced_action(author: user, project: project) } end @@ -102,7 +102,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue moved actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_MOVED } + let(:event) { described_class::ISSUE_MOVED } subject(:track_event) { described_class.track_issue_moved_action(author: user, project: project) } end @@ -110,7 +110,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue cloned actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_CLONED } + let(:event) { described_class::ISSUE_CLONED } subject(:track_event) { described_class.track_issue_cloned_action(author: user, project: project) } end @@ -118,7 +118,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue relate actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_RELATED } + let(:event) { described_class::ISSUE_RELATED } subject(:track_event) { described_class.track_issue_related_action(author: user, project: project) } end @@ -126,7 +126,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue unrelate actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_UNRELATED } + let(:event) { described_class::ISSUE_UNRELATED } subject(:track_event) { described_class.track_issue_unrelated_action(author: user, project: project) } end @@ -134,7 +134,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue marked as duplicate actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_MARKED_AS_DUPLICATE } + let(:event) { described_class::ISSUE_MARKED_AS_DUPLICATE } subject(:track_event) { described_class.track_issue_marked_as_duplicate_action(author: user, project: project) } end @@ -142,7 +142,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue locked actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_LOCKED } + let(:event) { described_class::ISSUE_LOCKED } subject(:track_event) { described_class.track_issue_locked_action(author: user, project: project) } end @@ -150,7 +150,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue unlocked actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_UNLOCKED } + let(:event) { described_class::ISSUE_UNLOCKED } subject(:track_event) { described_class.track_issue_unlocked_action(author: user, project: project) } end @@ -158,7 +158,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue designs added actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_DESIGNS_ADDED } + let(:event) { described_class::ISSUE_DESIGNS_ADDED } subject(:track_event) { described_class.track_issue_designs_added_action(author: user, project: project) } end @@ -166,7 +166,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue designs modified actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_DESIGNS_MODIFIED } + let(:event) { described_class::ISSUE_DESIGNS_MODIFIED } subject(:track_event) { described_class.track_issue_designs_modified_action(author: user, project: project) } end @@ -174,7 +174,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue designs removed actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_DESIGNS_REMOVED } + let(:event) { described_class::ISSUE_DESIGNS_REMOVED } subject(:track_event) { described_class.track_issue_designs_removed_action(author: user, project: project) } end @@ -182,7 +182,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue due date changed actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_DUE_DATE_CHANGED } + let(:event) { described_class::ISSUE_DUE_DATE_CHANGED } subject(:track_event) { described_class.track_issue_due_date_changed_action(author: user, project: project) } end @@ -190,7 +190,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue time estimate changed actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_TIME_ESTIMATE_CHANGED } + let(:event) { described_class::ISSUE_TIME_ESTIMATE_CHANGED } subject(:track_event) { described_class.track_issue_time_estimate_changed_action(author: user, project: project) } end @@ -198,7 +198,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue time spent changed actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_TIME_SPENT_CHANGED } + let(:event) { described_class::ISSUE_TIME_SPENT_CHANGED } subject(:track_event) { described_class.track_issue_time_spent_changed_action(author: user, project: project) } end @@ -206,7 +206,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue comment added actions', :snowplow do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_COMMENT_ADDED } + let(:event) { described_class::ISSUE_COMMENT_ADDED } subject(:track_event) { described_class.track_issue_comment_added_action(author: user, project: project) } end @@ -214,7 +214,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue comment edited actions', :snowplow do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_COMMENT_EDITED } + let(:event) { described_class::ISSUE_COMMENT_EDITED } subject(:track_event) { described_class.track_issue_comment_edited_action(author: user, project: project) } end @@ -222,7 +222,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue comment removed actions', :snowplow do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_COMMENT_REMOVED } + let(:event) { described_class::ISSUE_COMMENT_REMOVED } subject(:track_event) { described_class.track_issue_comment_removed_action(author: user, project: project) } end @@ -230,7 +230,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git context 'for Issue design comment removed actions' do it_behaves_like 'internal event tracking' do - let(:action) { described_class::ISSUE_DESIGN_COMMENT_REMOVED } + let(:event) { described_class::ISSUE_DESIGN_COMMENT_REMOVED } subject(:track_event) { described_class.track_issue_design_comment_removed_action(author: user, project: project) } end diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb index 53eee62b386..c3a718e669a 100644 --- a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb @@ -64,7 +64,7 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl end it_behaves_like 'internal event tracking' do - let(:action) { described_class::MR_USER_CREATE_ACTION } + let(:event) { described_class::MR_USER_CREATE_ACTION } let(:project) { target_project } let(:namespace) { project.namespace } end diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb index 3ec7bf33623..6d30947167c 100644 --- a/spec/lib/gitlab/usage_data_queries_spec.rb +++ b/spec/lib/gitlab/usage_data_queries_spec.rb @@ -72,17 +72,18 @@ RSpec.describe Gitlab::UsageDataQueries do describe '.add' do it 'returns the combined raw SQL with an inner query' do - expect(described_class.add('SELECT COUNT("users"."id") FROM "users"', - 'SELECT COUNT("issues"."id") FROM "issues"')) - .to eq('SELECT (SELECT COUNT("users"."id") FROM "users") + (SELECT COUNT("issues"."id") FROM "issues")') + expect(described_class.add( + 'SELECT COUNT("users"."id") FROM "users"', + 'SELECT COUNT("issues"."id") FROM "issues"' + )).to eq('SELECT (SELECT COUNT("users"."id") FROM "users") + (SELECT COUNT("issues"."id") FROM "issues")') end end describe '.histogram' do it 'returns the histogram sql' do - expect(described_class.histogram(AlertManagement::HttpIntegration.active, - :project_id, buckets: 1..2, bucket_size: 101)) - .to match(/^WITH "count_cte" AS MATERIALIZED/) + expect(described_class.histogram( + AlertManagement::HttpIntegration.active, :project_id, buckets: 1..2, bucket_size: 101 + )).to match(/^WITH "count_cte" AS MATERIALIZED/) end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 143d0484392..6f188aa408e 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -17,7 +17,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic it 'includes basic top and second level keys' do is_expected.to include(:counts) - is_expected.to include(:counts_monthly) is_expected.to include(:counts_weekly) is_expected.to include(:license) @@ -152,8 +151,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic it 'includes accurate usage_activity_by_stage data' do for_defined_days_back do user = create(:user) - project = create(:project, :repository_private, - :test_repo, :remote_mirror, creator: user) + project = create(:project, :repository_private, :test_repo, :remote_mirror, creator: user) create(:merge_request, source_project: project) create(:deploy_key, user: user) create(:key, user: user) @@ -293,22 +291,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic bulk_imports: { gitlab_v1: 2 }, - project_imports: { - bitbucket: 2, - bitbucket_server: 2, - git: 2, - gitea: 2, - github: 2, - gitlab_migration: 2, - gitlab_project: 2, - manifest: 2, - total: 16 - }, - issue_imports: { - jira: 2, - fogbugz: 2, - csv: 2 - }, group_imports: { group_import: 2, gitlab_migration: 2 @@ -320,22 +302,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic bulk_imports: { gitlab_v1: 1 }, - project_imports: { - bitbucket: 1, - bitbucket_server: 1, - git: 1, - gitea: 1, - github: 1, - gitlab_migration: 1, - gitlab_project: 1, - manifest: 1, - total: 8 - }, - issue_imports: { - jira: 1, - fogbugz: 1, - csv: 1 - }, group_imports: { group_import: 1, gitlab_migration: 1 @@ -623,28 +589,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic end end - describe '.system_usage_data_monthly' do - let_it_be(:project) { create(:project, created_at: 3.days.ago) } - - before do - create(:package, project: project, created_at: 3.days.ago) - create(:package, created_at: 2.months.ago, project: project) - - for_defined_days_back do - create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote') - end - end - - subject { described_class.system_usage_data_monthly } - - it 'gathers monthly usage counts correctly' do - counts_monthly = subject[:counts_monthly] - - expect(counts_monthly[:projects]).to eq(1) - expect(counts_monthly[:packages]).to eq(1) - end - end - context 'when not relying on database records' do describe '.features_usage_data_ce' do subject { described_class.features_usage_data_ce } @@ -885,8 +829,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic it 'gathers Service Desk data' do create_list(:issue, 2, :confidential, author: Users::Internal.support_bot, project: project) - expect(subject).to eq(service_desk_enabled_projects: 1, - service_desk_issues: 2) + expect(subject).to eq(service_desk_enabled_projects: 1, service_desk_issues: 2) end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 9bc1ebaebcb..cca18cb05c7 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -371,50 +371,13 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do subject(:cleanup_key) { described_class.cleanup_key(key) } - shared_examples 'cleans up key' do |redis = Gitlab::Redis::Workhorse| - before do - described_class.set_key_and_notify(key, value) - end - - it 'deletes the key' do - expect { cleanup_key } - .to change { redis.with { |c| c.exists?(key) } }.from(true).to(false) - end + before do + described_class.set_key_and_notify(key, value) end - it_behaves_like 'cleans up key' - - context 'when workhorse migration feature flags are disabled' do - before do - stub_feature_flags( - use_primary_and_secondary_stores_for_workhorse: false, - use_primary_store_as_default_for_workhorse: false - ) - end - - it_behaves_like 'cleans up key', Gitlab::Redis::SharedState - end - - context 'when either workhorse migration feature flags are enabled' do - context 'when use_primary_and_secondary_stores_for_workhorse is enabled' do - before do - stub_feature_flags( - use_primary_store_as_default_for_workhorse: false - ) - end - - it_behaves_like 'cleans up key' - end - - context 'when use_primary_store_as_default_for_workhorse is enabled' do - before do - stub_feature_flags( - use_primary_and_secondary_stores_for_workhorse: false - ) - end - - it_behaves_like 'cleans up key' - end + it 'deletes the key' do + expect { cleanup_key } + .to change { Gitlab::Redis::Workhorse.with { |c| c.exists?(key) } }.from(true).to(false) end end @@ -424,13 +387,13 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do subject { described_class.set_key_and_notify(key, value, overwrite: overwrite) } - shared_examples 'set and notify' do |redis = Gitlab::Redis::Workhorse| + shared_examples 'set and notify' do it 'set and return the same value' do is_expected.to eq(value) end it 'set and notify' do - expect(redis).to receive(:with).and_call_original + expect(Gitlab::Redis::Workhorse).to receive(:with).and_call_original expect_any_instance_of(::Redis).to receive(:publish) .with(described_class::NOTIFICATION_PREFIX + 'test-key', "test-value") @@ -442,39 +405,6 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do let(:overwrite) { true } it_behaves_like 'set and notify' - - context 'when workhorse migration feature flags are disabled' do - before do - stub_feature_flags( - use_primary_and_secondary_stores_for_workhorse: false, - use_primary_store_as_default_for_workhorse: false - ) - end - - it_behaves_like 'set and notify', Gitlab::Redis::SharedState - end - - context 'when either workhorse migration feature flags are enabled' do - context 'when use_primary_and_secondary_stores_for_workhorse is enabled' do - before do - stub_feature_flags( - use_primary_store_as_default_for_workhorse: false - ) - end - - it_behaves_like 'set and notify' - end - - context 'when use_primary_store_as_default_for_workhorse is enabled' do - before do - stub_feature_flags( - use_primary_and_secondary_stores_for_workhorse: false - ) - end - - it_behaves_like 'set and notify' - end - end end context 'when we set an existing key' do |