diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-20 02:18:09 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-20 02:18:09 +0300 |
commit | 6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde (patch) | |
tree | dc4d20fe6064752c0bd323187252c77e0a89144b /spec/lib | |
parent | 9868dae7fc0655bd7ce4a6887d4e6d487690eeed (diff) |
Add latest changes from gitlab-org/gitlab@15-4-stable-eev15.4.0-rc42
Diffstat (limited to 'spec/lib')
469 files changed, 10147 insertions, 3666 deletions
diff --git a/spec/lib/api/entities/ci/job_request/image_spec.rb b/spec/lib/api/entities/ci/job_request/image_spec.rb index fca3b5d3fa9..14d4a074fce 100644 --- a/spec/lib/api/entities/ci/job_request/image_spec.rb +++ b/spec/lib/api/entities/ci/job_request/image_spec.rb @@ -32,14 +32,4 @@ RSpec.describe API::Entities::Ci::JobRequest::Image do it 'returns the pull policy' do expect(subject[:pull_policy]).to eq(['if-not-present']) end - - context 'when the FF ci_docker_image_pull_policy is disabled' do - before do - stub_feature_flags(ci_docker_image_pull_policy: false) - end - - it 'does not return the pull policy' do - expect(subject).not_to have_key(:pull_policy) - end - end end diff --git a/spec/lib/api/entities/ci/job_request/service_spec.rb b/spec/lib/api/entities/ci/job_request/service_spec.rb index 86f2120c321..11350f7c41b 100644 --- a/spec/lib/api/entities/ci/job_request/service_spec.rb +++ b/spec/lib/api/entities/ci/job_request/service_spec.rb @@ -40,12 +40,4 @@ RSpec.describe API::Entities::Ci::JobRequest::Service do expect(subject[:ports]).to be_nil end end - - context 'when the FF ci_docker_image_pull_policy is disabled' do - before do - stub_feature_flags(ci_docker_image_pull_policy: false) - end - - it { is_expected.not_to have_key(:pull_policy) } - end end diff --git a/spec/lib/api/entities/ml/mlflow/run_info_spec.rb b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb new file mode 100644 index 00000000000..2a6d0825e5c --- /dev/null +++ b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::Ml::Mlflow::RunInfo do + let_it_be(:candidate) { create(:ml_candidates) } + + subject { described_class.new(candidate).as_json } + + context 'when start_time is nil' do + it { expect(subject[:start_time]).to eq(0) } + end + + context 'when start_time is not nil' do + before do + allow(candidate).to receive(:start_time).and_return(1234) + end + + it { expect(subject[:start_time]).to eq(1234) } + end + + describe 'end_time' do + context 'when nil' do + it { is_expected.not_to have_key(:end_time) } + end + + context 'when not nil' do + before do + allow(candidate).to receive(:end_time).and_return(1234) + end + + it { expect(subject[:end_time]).to eq(1234) } + end + end + + describe 'experiment_id' do + it 'is the experiment iid as string' do + expect(subject[:experiment_id]).to eq(candidate.experiment.iid.to_s) + end + end + + describe 'run_id' do + it 'is the iid as string' do + expect(subject[:run_id]).to eq(candidate.iid.to_s) + end + end + + describe 'run_uuid' do + it 'is the iid as string' do + expect(subject[:run_uuid]).to eq(candidate.iid.to_s) + end + end + + describe 'artifact_uri' do + it 'is not implemented' do + expect(subject[:artifact_uri]).to eq('not_implemented') + end + end + + describe 'lifecycle_stage' do + it 'is active' do + expect(subject[:lifecycle_stage]).to eq('active') + end + end +end diff --git a/spec/lib/api/entities/ml/mlflow/run_spec.rb b/spec/lib/api/entities/ml/mlflow/run_spec.rb new file mode 100644 index 00000000000..84234f474f5 --- /dev/null +++ b/spec/lib/api/entities/ml/mlflow/run_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::Ml::Mlflow::Run do + let_it_be(:candidate) { create(:ml_candidates) } + + subject { described_class.new(candidate).as_json } + + it 'has run key' do + expect(subject).to have_key(:run) + end + + it 'has the id' do + expect(subject[:run][:info][:run_id]).to eq(candidate.iid.to_s) + end + + it 'data is empty' do + expect(subject[:run][:data]).to be_empty + end +end diff --git a/spec/lib/api/entities/personal_access_token_with_details_spec.rb b/spec/lib/api/entities/personal_access_token_with_details_spec.rb deleted file mode 100644 index a53d6febba1..00000000000 --- a/spec/lib/api/entities/personal_access_token_with_details_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe API::Entities::PersonalAccessTokenWithDetails do - describe '#as_json' do - let_it_be(:user) { create(:user) } - let_it_be(:token) { create(:personal_access_token, user: user, expires_at: nil) } - - let(:entity) { described_class.new(token) } - - it 'returns token data' do - expect(entity.as_json).to eq({ - id: token.id, - name: token.name, - revoked: false, - created_at: token.created_at, - scopes: ['api'], - user_id: user.id, - last_used_at: nil, - active: true, - expires_at: nil, - expired: false, - expires_soon: false, - revoke_path: Gitlab::Routing.url_helpers.revoke_profile_personal_access_token_path(token) - }) - end - end -end diff --git a/spec/lib/api/helpers/caching_spec.rb b/spec/lib/api/helpers/caching_spec.rb index 38b7b386d5c..828af7b5f91 100644 --- a/spec/lib/api/helpers/caching_spec.rb +++ b/spec/lib/api/helpers/caching_spec.rb @@ -33,10 +33,7 @@ RSpec.describe API::Helpers::Caching, :use_clean_rails_redis_caching do end describe "#present_cached" do - subject do - instance.present_cached(presentable, **kwargs) - end - + let(:method) { :present_cached } let(:kwargs) do { with: presenter, @@ -44,6 +41,10 @@ RSpec.describe API::Helpers::Caching, :use_clean_rails_redis_caching do } end + subject do + instance.public_send(method, presentable, **kwargs) + end + context 'single object' do let_it_be(:presentable) { create(:todo, project: project) } diff --git a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb index ae0c0f53acd..aa4b0a137cd 100644 --- a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb +++ b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb @@ -8,6 +8,8 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do describe '#redirect_registry_request' do using RSpec::Parameterized::TableSyntax + let_it_be(:project) { create(:project) } + let(:options) { {} } subject { helper.redirect_registry_request(forward_to_registry, package_type, options) { helper.fallback } } @@ -18,8 +20,8 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do shared_examples 'executing fallback' do it 'redirects to package registry' do - expect(helper).to receive(:registry_url).never - expect(helper).to receive(:redirect).never + expect(helper).not_to receive(:registry_url) + expect(helper).not_to receive(:redirect) expect(helper).to receive(:fallback).once subject @@ -30,7 +32,7 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do it 'redirects to package registry', :snowplow do expect(helper).to receive(:registry_url).once expect(helper).to receive(:redirect).once - expect(helper).to receive(:fallback).never + expect(helper).not_to receive(:fallback) subject @@ -38,11 +40,12 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do end end - %i[npm pypi].each do |forwardable_package_type| + %i[maven npm pypi].each do |forwardable_package_type| context "with #{forwardable_package_type} packages" do include_context 'dependency proxy helpers context' let(:package_type) { forwardable_package_type } + let(:options) { { project: project } } where(:application_setting, :forward_to_registry, :example_name) do true | true | 'executing redirect' @@ -59,17 +62,41 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do it_behaves_like params[:example_name] end end + + context 'when maven_central_request_forwarding is disabled' do + let(:package_type) { :maven } + let(:options) { { project: project } } + + include_context 'dependency proxy helpers context' + + where(:application_setting, :forward_to_registry) do + true | true + true | false + false | true + false | false + end + + with_them do + before do + stub_feature_flags(maven_central_request_forwarding: false) + allow_fetch_application_setting(attribute: "maven_package_requests_forwarding", return_value: application_setting) + end + + it_behaves_like 'executing fallback' + end + end end context 'with non-forwardable package type' do let(:forward_to_registry) { true } before do + stub_application_setting(maven_package_requests_forwarding: true) stub_application_setting(npm_package_requests_forwarding: true) stub_application_setting(pypi_package_requests_forwarding: true) end - Packages::Package.package_types.keys.without('npm', 'pypi').each do |pkg_type| + Packages::Package.package_types.keys.without('maven', 'npm', 'pypi').each do |pkg_type| context "#{pkg_type}" do let(:package_type) { pkg_type.to_sym } @@ -81,18 +108,21 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do end describe '#registry_url' do - subject { helper.registry_url(package_type, package_name: 'test') } + subject { helper.registry_url(package_type, options) } - where(:package_type, :expected_result) do - :npm | 'https://registry.npmjs.org/test' - :pypi | 'https://pypi.org/simple/test/' + where(:package_type, :expected_result, :params) do + :maven | 'https://repo.maven.apache.org/maven2/test/123' | { path: 'test', file_name: '123', project: project } + :npm | 'https://registry.npmjs.org/test' | { package_name: 'test' } + :pypi | 'https://pypi.org/simple/test/' | { package_name: 'test' } end with_them do + let(:options) { params } + it { is_expected.to eq(expected_result) } end - Packages::Package.package_types.keys.without('npm', 'pypi').each do |pkg_type| + Packages::Package.package_types.keys.without('maven', 'npm', 'pypi').each do |pkg_type| context "with non-forwardable package type #{pkg_type}" do let(:package_type) { pkg_type } diff --git a/spec/lib/api/helpers/packages_helpers_spec.rb b/spec/lib/api/helpers/packages_helpers_spec.rb index 0c51e25bad9..cd6e718ce98 100644 --- a/spec/lib/api/helpers/packages_helpers_spec.rb +++ b/spec/lib/api/helpers/packages_helpers_spec.rb @@ -5,6 +5,8 @@ require 'spec_helper' RSpec.describe API::Helpers::PackagesHelpers do let_it_be(:helper) { Class.new.include(described_class).new } let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group) } + let_it_be(:package) { create(:package) } describe 'authorize_packages_access!' do subject { helper.authorize_packages_access!(project) } @@ -17,7 +19,45 @@ RSpec.describe API::Helpers::PackagesHelpers do end end - %i[read_package create_package destroy_package].each do |action| + describe 'authorize_read_package!' do + using RSpec::Parameterized::TableSyntax + + where(:subject, :expected_class) do + ref(:project) | ::Packages::Policies::Project + ref(:group) | ::Packages::Policies::Group + ref(:package) | ::Packages::Package + end + + with_them do + it 'calls authorize! with correct subject' do + expect(helper).to receive(:authorize!).with(:read_package, have_attributes(id: subject.id, class: expected_class)) + + expect(helper.send('authorize_read_package!', subject)).to eq nil + end + end + + context 'with feature flag disabled' do + before do + stub_feature_flags(read_package_policy_rule: false) + end + + where(:subject, :expected_class) do + ref(:project) | ::Project + ref(:group) | ::Group + ref(:package) | ::Packages::Package + end + + with_them do + it 'calls authorize! with correct subject' do + expect(helper).to receive(:authorize!).with(:read_package, have_attributes(id: subject.id, class: expected_class)) + + expect(helper.send('authorize_read_package!', subject)).to eq nil + end + end + end + end + + %i[create_package destroy_package].each do |action| describe "authorize_#{action}!" do subject { helper.send("authorize_#{action}!", project) } @@ -40,7 +80,7 @@ RSpec.describe API::Helpers::PackagesHelpers do context 'with packages enabled' do it "doesn't call not_found!" do - expect(helper).to receive(:not_found!).never + expect(helper).not_to receive(:not_found!) expect(subject).to eq nil end diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb index a008c1adeac..ae6af5b540e 100644 --- a/spec/lib/api/helpers/pagination_spec.rb +++ b/spec/lib/api/helpers/pagination_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe API::Helpers::Pagination do subject { Class.new.include(described_class).new } diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb index cd41d362d03..f25c75ef93c 100644 --- a/spec/lib/api/helpers_spec.rb +++ b/spec/lib/api/helpers_spec.rb @@ -865,4 +865,93 @@ RSpec.describe API::Helpers do helper.bad_request!('custom reason') end end + + describe '#authenticate_by_gitlab_shell_token!' do + include GitlabShellHelpers + + let(:valid_secret_token) { 'valid' } + let(:invalid_secret_token) { 'invalid' } + let(:headers) { {} } + let(:params) { {} } + + shared_examples 'authorized' do + it 'authorized' do + expect(helper).not_to receive(:unauthorized!) + + helper.authenticate_by_gitlab_shell_token! + end + end + + shared_examples 'unauthorized' do + it 'unauthorized' do + expect(helper).to receive(:unauthorized!) + + helper.authenticate_by_gitlab_shell_token! + end + end + + before do + allow(Gitlab::Shell).to receive(:secret_token).and_return(valid_secret_token) + allow(helper).to receive_messages(params: params, headers: headers, secret_token: valid_secret_token) + end + + context 'when jwt token is not provided' do + it_behaves_like 'unauthorized' + end + + context 'when jwt token is invalid' do + let(:headers) { gitlab_shell_internal_api_request_header(secret_token: invalid_secret_token) } + + it_behaves_like 'unauthorized' + end + + context 'when jwt token issuer is invalid' do + let(:headers) { gitlab_shell_internal_api_request_header(issuer: 'gitlab-workhorse') } + + it_behaves_like 'unauthorized' + end + + context 'when jwt token is valid' do + let(:headers) { gitlab_shell_internal_api_request_header } + + it_behaves_like 'authorized' + end + + context 'when gitlab_shell_jwt_token is disabled' do + let(:valid_secret_token) { +'valid' } # mutable string to use chomp! + let(:invalid_secret_token) { +'invalid' } # mutable string to use chomp! + + before do + stub_feature_flags(gitlab_shell_jwt_token: false) + end + + context 'when shared secret is not provided' do + it_behaves_like 'unauthorized' + end + + context 'when shared secret provided via params' do + let(:params) { { 'secret_token' => valid_secret_token } } + + it_behaves_like 'authorized' + + context 'but it is invalid' do + let(:params) { { 'secret_token' => invalid_secret_token } } + + it_behaves_like 'unauthorized' + end + end + + context 'when shared secret provided via headers' do + let(:headers) { { described_class::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(valid_secret_token) } } + + it_behaves_like 'authorized' + + context 'but it is invalid' do + let(:headers) { { described_class::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(invalid_secret_token) } } + + it_behaves_like 'unauthorized' + end + end + end + end end diff --git a/spec/lib/api/integrations/slack/events/url_verification_spec.rb b/spec/lib/api/integrations/slack/events/url_verification_spec.rb deleted file mode 100644 index 2778f0d708d..00000000000 --- a/spec/lib/api/integrations/slack/events/url_verification_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe API::Integrations::Slack::Events::UrlVerification do - describe '.call' do - it 'returns the challenge' do - expect(described_class.call({ challenge: 'foo' })).to eq({ challenge: 'foo' }) - end - end -end diff --git a/spec/lib/backup/database_backup_error_spec.rb b/spec/lib/backup/database_backup_error_spec.rb index ef627900050..e001f65465c 100644 --- a/spec/lib/backup/database_backup_error_spec.rb +++ b/spec/lib/backup/database_backup_error_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Backup::DatabaseBackupError do let(:config) do diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb index d427e41026e..6b0747735ed 100644 --- a/spec/lib/backup/gitaly_backup_spec.rb +++ b/spec/lib/backup/gitaly_backup_spec.rb @@ -17,7 +17,7 @@ RSpec.describe Backup::GitalyBackup do let(:expected_env) do { 'SSL_CERT_FILE' => Gitlab::X509::Certificate.default_cert_file, - 'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir + 'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir }.merge(ENV) end @@ -121,7 +121,7 @@ RSpec.describe Backup::GitalyBackup do let(:ssl_env) do { 'SSL_CERT_FILE' => '/some/cert/file', - 'SSL_CERT_DIR' => '/some/cert' + 'SSL_CERT_DIR' => '/some/cert' } end diff --git a/spec/lib/backup/task_spec.rb b/spec/lib/backup/task_spec.rb index 80f1fe01b78..1de99729512 100644 --- a/spec/lib/backup/task_spec.rb +++ b/spec/lib/backup/task_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Backup::Task do let(:progress) { StringIO.new } diff --git a/spec/lib/banzai/color_parser_spec.rb b/spec/lib/banzai/color_parser_spec.rb index 95b3955d8fe..3914aee2d4c 100644 --- a/spec/lib/banzai/color_parser_spec.rb +++ b/spec/lib/banzai/color_parser_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Banzai::ColorParser do describe '.parse' do diff --git a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb index 2d326bd77a6..5712ed7da1f 100644 --- a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb +++ b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb @@ -14,6 +14,10 @@ RSpec.describe Banzai::Filter::BlockquoteFenceFilter do expect(output).to eq(expected) end + it 'does not require newlines at start or end of string' do + expect(filter(">>>\ntest\n>>>")).to eq("\n> test\n") + end + it 'allows trailing whitespace on blockquote fence lines' do expect(filter(">>> \ntest\n>>> ")).to eq("\n> test\n") end diff --git a/spec/lib/banzai/filter/kroki_filter_spec.rb b/spec/lib/banzai/filter/kroki_filter_spec.rb index 1fb61ad1991..3f4f3aafdd6 100644 --- a/spec/lib/banzai/filter/kroki_filter_spec.rb +++ b/spec/lib/banzai/filter/kroki_filter_spec.rb @@ -46,4 +46,12 @@ RSpec.describe Banzai::Filter::KrokiFilter do expect(doc.to_s).to start_with '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KyJyVNQiE5KTSxKidXVjS5ILCrKL4lFFrSyi07LL81RyM0vLckAysRGjxo8avCowaMGjxo8avCowaMGU8lgAE7mIdc=" hidden="" class="js-render-kroki" data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,W1BpcmF0ZXxleWVDb3VudDog' end + + it 'allows the lang attribute on the code tag to support RST files processed by gitlab-markup gem' do + stub_application_setting(kroki_enabled: true, kroki_url: "http://localhost:8000") + text = '[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]' * 25 + doc = filter("<pre><code lang='nomnoml'>#{text}</code></pre>") + + expect(doc.to_s).to start_with '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KyJyVNQiE5KTSxKidXVjS5ILCrKL4lFFrSyi07LL81RyM0vLckAysRGjxo8avCowaMGjxo8avCowaMGU8lgAE7mIdc=" hidden="" class="js-render-kroki" data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,W1BpcmF0ZXxleWVDb3VudDog' + end end diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb index 128f8532d39..dd116eb1109 100644 --- a/spec/lib/banzai/filter/math_filter_spec.rb +++ b/spec/lib/banzai/filter/math_filter_spec.rb @@ -3,128 +3,179 @@ require 'spec_helper' RSpec.describe Banzai::Filter::MathFilter do + using RSpec::Parameterized::TableSyntax include FilterSpecHelper - it 'leaves regular inline code unchanged' do - input = "<code>2+2</code>" - doc = filter(input) - - expect(doc.to_s).to eq input - end - - it 'removes surrounding dollar signs and adds class code, math and js-render-math' do - doc = filter("$<code>2+2</code>$") - - expect(doc.to_s).to eq '<code class="code math js-render-math" data-math-style="inline">2+2</code>' - end - - it 'only removes surrounding dollar signs' do - doc = filter("test $<code>2+2</code>$ test") - before = doc.xpath('descendant-or-self::text()[1]').first - after = doc.xpath('descendant-or-self::text()[3]').first - - expect(before.to_s).to eq 'test ' - expect(after.to_s).to eq ' test' - end - - it 'only removes surrounding single dollar sign' do - doc = filter("test $$<code>2+2</code>$$ test") - before = doc.xpath('descendant-or-self::text()[1]').first - after = doc.xpath('descendant-or-self::text()[3]').first - - expect(before.to_s).to eq 'test $' - expect(after.to_s).to eq '$ test' - end - - it 'adds data-math-style inline attribute to inline math' do - doc = filter('$<code>2+2</code>$') - code = doc.xpath('descendant-or-self::code').first - - expect(code['data-math-style']).to eq 'inline' - end - - it 'adds class code and math to inline math' do - doc = filter('$<code>2+2</code>$') - code = doc.xpath('descendant-or-self::code').first - - expect(code[:class]).to include("code") - expect(code[:class]).to include("math") - end - - it 'adds js-render-math class to inline math' do - doc = filter('$<code>2+2</code>$') - code = doc.xpath('descendant-or-self::code').first - - expect(code[:class]).to include("js-render-math") - end - - # Cases with faulty syntax. Should be a no-op - - it 'ignores cases with missing dolar sign at the end' do - input = "test $<code>2+2</code> test" - doc = filter(input) - - expect(doc.to_s).to eq input - end - - it 'ignores cases with missing dolar sign at the beginning' do - input = "test <code>2+2</code>$ test" - doc = filter(input) - - expect(doc.to_s).to eq input - end - - it 'ignores dollar signs if it is not adjacent' do - input = '<p>We check strictly $<code>2+2</code> and <code>2+2</code>$ </p>' - doc = filter(input) - - expect(doc.to_s).to eq input - end - - it 'ignores dollar signs if they are inside another element' do - input = '<p>We check strictly <em>$</em><code>2+2</code><em>$</em></p>' - doc = filter(input) - - expect(doc.to_s).to eq input - end - - # Display math - - it 'adds data-math-style display attribute to display math' do - doc = filter('<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>') - pre = doc.xpath('descendant-or-self::pre').first - - expect(pre['data-math-style']).to eq 'display' - end - - it 'adds js-render-math class to display math' do - doc = filter('<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>') - pre = doc.xpath('descendant-or-self::pre').first - - expect(pre[:class]).to include("js-render-math") - end - - it 'ignores code blocks that are not math' do - input = '<pre class="code highlight js-syntax-highlight language-plaintext" v-pre="true"><code>2+2</code></pre>' - doc = filter(input) - - expect(doc.to_s).to eq input - end - - it 'requires the pre to contain both code and math' do - input = '<pre class="highlight js-syntax-highlight language-plaintext language-math" v-pre="true"><code>2+2</code></pre>' - doc = filter(input) - - expect(doc.to_s).to eq input - end - - it 'dollar signs around to display math' do - doc = filter('$<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>$') - before = doc.xpath('descendant-or-self::text()[1]').first - after = doc.xpath('descendant-or-self::text()[3]').first - - expect(before.to_s).to eq '$' - expect(after.to_s).to eq '$' + shared_examples 'inline math' do + it 'removes surrounding dollar signs and adds class code, math and js-render-math' do + doc = filter(text) + expected = result_template.gsub('<math>', '<code class="code math js-render-math" data-math-style="inline">') + expected.gsub!('</math>', '</code>') + + expect(doc.to_s).to eq expected + end + end + + shared_examples 'display math' do + let_it_be(:template_prefix_with_pre) { '<pre class="code math js-render-math" data-math-style="display"><code>' } + let_it_be(:template_prefix_with_code) { '<code class="code math js-render-math" data-math-style="display">' } + let(:use_pre_tags) { false } + + it 'removes surrounding dollar signs and adds class code, math and js-render-math' do + doc = filter(text) + + template_prefix = use_pre_tags ? template_prefix_with_pre : template_prefix_with_code + template_suffix = "</code>#{'</pre>' if use_pre_tags}" + expected = result_template.gsub('<math>', template_prefix) + expected.gsub!('</math>', template_suffix) + + expect(doc.to_s).to eq expected + end + end + + describe 'inline math using $...$ syntax' do + context 'with valid syntax' do + where(:text, :result_template) do + '$2+2$' | '<math>2+2</math>' + '$22+1$ and $22 + a^2$' | '<math>22+1</math> and <math>22 + a^2</math>' + '$22 and $2+2$' | '$22 and <math>2+2</math>' + '$2+2$ $22 and flightjs/Flight$22 $2+2$' | '<math>2+2</math> $22 and flightjs/Flight$22 <math>2+2</math>' + '$1/2$ <b>test</b>' | '<math>1/2</math> <b>test</b>' + '$a!$' | '<math>a!</math>' + '$x$' | '<math>x</math>' + end + + with_them do + it_behaves_like 'inline math' + end + end + + it 'does not handle dollar literals properly' do + doc = filter('$20+30\$$') + expected = '<code class="code math js-render-math" data-math-style="inline">20+30\\</code>$' + + expect(doc.to_s).to eq expected + end + end + + describe 'inline math using $`...`$ syntax' do + context 'with valid syntax' do + where(:text, :result_template) do + '$<code>2+2</code>$' | '<math>2+2</math>' + '$<code>22+1</code>$ and $<code>22 + a^2</code>$' | '<math>22+1</math> and <math>22 + a^2</math>' + '$22 and $<code>2+2</code>$' | '$22 and <math>2+2</math>' + '$<code>2+2</code>$ $22 and flightjs/Flight$22 $<code>2+2</code>$' | '<math>2+2</math> $22 and flightjs/Flight$22 <math>2+2</math>' + 'test $$<code>2+2</code>$$ test' | 'test $<math>2+2</math>$ test' + end + + with_them do + it_behaves_like 'inline math' + end + end + end + + describe 'inline display math using $$...$$ syntax' do + context 'with valid syntax' do + where(:text, :result_template) do + '$$2+2$$' | '<math>2+2</math>' + '$$ 2+2 $$' | '<math>2+2</math>' + '$$22+1$$ and $$22 + a^2$$' | '<math>22+1</math> and <math>22 + a^2</math>' + '$22 and $$2+2$$' | '$22 and <math>2+2</math>' + '$$2+2$$ $22 and flightjs/Flight$22 $$2+2$$' | '<math>2+2</math> $22 and flightjs/Flight$22 <math>2+2</math>' + 'flightjs/Flight$22 and $$a^2 + b^2 = c^2$$' | 'flightjs/Flight$22 and <math>a^2 + b^2 = c^2</math>' + '$$a!$$' | '<math>a!</math>' + '$$x$$' | '<math>x</math>' + '$$20,000 and $$30,000' | '<math>20,000 and</math>30,000' + end + + with_them do + it_behaves_like 'display math' + end + end + end + + describe 'block display math using $$\n...\n$$ syntax' do + context 'with valid syntax' do + where(:text, :result_template) do + "$$\n2+2\n$$" | "<math>2+2</math>" + end + + with_them do + it_behaves_like 'display math' do + let(:use_pre_tags) { true } + end + end + end + end + + describe 'display math using ```math...``` syntax' do + it 'adds data-math-style display attribute to display math' do + doc = filter('<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>') + pre = doc.xpath('descendant-or-self::pre').first + + expect(pre['data-math-style']).to eq 'display' + end + + it 'adds js-render-math class to display math' do + doc = filter('<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>') + pre = doc.xpath('descendant-or-self::pre').first + + expect(pre[:class]).to include("js-render-math") + end + + it 'ignores code blocks that are not math' do + input = '<pre class="code highlight js-syntax-highlight language-plaintext" v-pre="true"><code>2+2</code></pre>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'requires the pre to contain both code and math' do + input = '<pre class="highlight js-syntax-highlight language-plaintext language-math" v-pre="true"><code>2+2</code></pre>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'dollar signs around to display math' do + doc = filter('$<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>$') + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq '$' + expect(after.to_s).to eq '$' + end + end + + describe 'unrecognized syntax' do + where(:text) do + [ + '<code>2+2</code>', + 'test $<code>2+2</code> test', + 'test <code>2+2</code>$ test', + '<em>$</em><code>2+2</code><em>$</em>', + '$20,000 and $30,000', + '$20,000 in $USD', + '$ a^2 $', + "test $$\n2+2\n$$", + "$\n$", + '$$$' + ] + end + + with_them do + it 'is ignored' do + expect(filter(text).to_s).to eq text + end + end + end + + it 'handles multiple styles in one text block' do + doc = filter('$<code>2+2</code>$ + $3+3$ + $$4+4$$') + + expect(doc.search('.js-render-math').count).to eq(3) + expect(doc.search('[data-math-style="inline"]').count).to eq(2) + expect(doc.search('[data-math-style="display"]').count).to eq(1) end it 'limits how many elements can be marked as math' do @@ -134,4 +185,11 @@ RSpec.describe Banzai::Filter::MathFilter do expect(doc.search('.js-render-math').count).to eq(2) end + + it 'does not recognize new syntax when feature flag is off' do + stub_feature_flags(markdown_dollar_math: false) + doc = filter('$1+2$') + + expect(doc.to_s).to eq '$1+2$' + end end diff --git a/spec/lib/banzai/filter/output_safety_spec.rb b/spec/lib/banzai/filter/output_safety_spec.rb index 5b7b7298411..8186935f4b2 100644 --- a/spec/lib/banzai/filter/output_safety_spec.rb +++ b/spec/lib/banzai/filter/output_safety_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Banzai::Filter::OutputSafety do subject do diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb index dcfeb2ce3ba..4373af90cde 100644 --- a/spec/lib/banzai/filter/plantuml_filter_spec.rb +++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb @@ -15,6 +15,16 @@ RSpec.describe Banzai::Filter::PlantumlFilter do expect(doc.to_s).to eq output end + it 'allows the lang attribute on the code tag to support RST files processed by gitlab-markup gem' do + stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080") + + input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' + output = '<img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq" data-diagram="plantuml" data-diagram-src="data:text/plain;base64,Qm9iIC0+IFNhcmEgOiBIZWxsbw==">' + doc = filter(input) + + expect(doc.to_s).to eq output + end + it 'does not replace plantuml pre tag with img tag if disabled' do stub_application_setting(plantuml_enabled: false) diff --git a/spec/lib/banzai/filter/repository_link_filter_spec.rb b/spec/lib/banzai/filter/repository_link_filter_spec.rb index 815053aac2f..c220263b238 100644 --- a/spec/lib/banzai/filter/repository_link_filter_spec.rb +++ b/spec/lib/banzai/filter/repository_link_filter_spec.rb @@ -8,14 +8,14 @@ RSpec.describe Banzai::Filter::RepositoryLinkFilter do def filter(doc, contexts = {}) contexts.reverse_merge!({ - commit: commit, - project: project, - current_user: user, - group: group, - wiki: wiki, - ref: ref, + commit: commit, + project: project, + current_user: user, + group: group, + wiki: wiki, + ref: ref, requested_path: requested_path, - only_path: only_path + only_path: only_path }) described_class.call(doc, contexts) diff --git a/spec/lib/banzai/filter_array_spec.rb b/spec/lib/banzai/filter_array_spec.rb index 47bc5633300..f341d5d51a0 100644 --- a/spec/lib/banzai/filter_array_spec.rb +++ b/spec/lib/banzai/filter_array_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Banzai::FilterArray do describe '#insert_after' do diff --git a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb index 5021ef3a79a..303d0fcb6c2 100644 --- a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb @@ -32,4 +32,21 @@ RSpec.describe Banzai::Pipeline::PreProcessPipeline do expect(result[:output]).to eq('foo foo f...') end + + context 'when multiline blockquote' do + it 'data-sourcepos references correct line in source markdown' do + markdown = <<~MD + >>> + foo + >>> + MD + + pipeline_output = described_class.call(markdown, {})[:output] + pipeline_output = Banzai::Pipeline::PlainMarkdownPipeline.call(pipeline_output, {})[:output] + sourcepos = pipeline_output.at('blockquote')['data-sourcepos'] + source_line = sourcepos.split(':').first.to_i + + expect(markdown.lines[source_line - 1]).to eq "foo\n" + end + end end diff --git a/spec/lib/banzai/pipeline_spec.rb b/spec/lib/banzai/pipeline_spec.rb index 7d4df2ca5ce..b2c970e4394 100644 --- a/spec/lib/banzai/pipeline_spec.rb +++ b/spec/lib/banzai/pipeline_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Banzai::Pipeline do describe '.[]' do diff --git a/spec/lib/banzai/querying_spec.rb b/spec/lib/banzai/querying_spec.rb index b76f6ec533c..fc7aaa94954 100644 --- a/spec/lib/banzai/querying_spec.rb +++ b/spec/lib/banzai/querying_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Banzai::Querying do describe '.css' do diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb index ae9cf4c5068..705f44baf16 100644 --- a/spec/lib/banzai/renderer_spec.rb +++ b/spec/lib/banzai/renderer_spec.rb @@ -76,7 +76,7 @@ RSpec.describe Banzai::Renderer do let(:object) { fake_object(fresh: true) } it 'uses the cache' do - expect(object).to receive(:refresh_markdown_cache!).never + expect(object).not_to receive(:refresh_markdown_cache!) is_expected.to eq('field_html') end diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb index 349274585c4..715b78c95eb 100644 --- a/spec/lib/bitbucket/collection_spec.rb +++ b/spec/lib/bitbucket/collection_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' # Emulates paginator. It returns 2 pages with results class TestPaginator diff --git a/spec/lib/bitbucket/page_spec.rb b/spec/lib/bitbucket/page_spec.rb index 1d599007d9e..46ab5a45551 100644 --- a/spec/lib/bitbucket/page_spec.rb +++ b/spec/lib/bitbucket/page_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Bitbucket::Page do let(:response) { { 'values' => [{ 'username' => 'Ben' }], 'pagelen' => 2, 'next' => '' } } diff --git a/spec/lib/bitbucket/paginator_spec.rb b/spec/lib/bitbucket/paginator_spec.rb index e74af8a264b..3285fae5b82 100644 --- a/spec/lib/bitbucket/paginator_spec.rb +++ b/spec/lib/bitbucket/paginator_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Bitbucket::Paginator do let(:last_page) { double(:page, next?: false, items: ['item_2']) } diff --git a/spec/lib/bitbucket/representation/comment_spec.rb b/spec/lib/bitbucket/representation/comment_spec.rb index f6766ab685b..d108bcfe767 100644 --- a/spec/lib/bitbucket/representation/comment_spec.rb +++ b/spec/lib/bitbucket/representation/comment_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Bitbucket::Representation::Comment do describe '#author' do diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb index 8c27086546f..a40bbcb7bf8 100644 --- a/spec/lib/bitbucket/representation/issue_spec.rb +++ b/spec/lib/bitbucket/representation/issue_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Bitbucket::Representation::Issue do describe '#iid' do diff --git a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb index cdab683492f..e748cd7b955 100644 --- a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb +++ b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Bitbucket::Representation::PullRequestComment do describe '#iid' do diff --git a/spec/lib/bitbucket/representation/pull_request_spec.rb b/spec/lib/bitbucket/representation/pull_request_spec.rb index 6f05d03aa0a..87a9a0fa76d 100644 --- a/spec/lib/bitbucket/representation/pull_request_spec.rb +++ b/spec/lib/bitbucket/representation/pull_request_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Bitbucket::Representation::PullRequest do describe '#iid' do diff --git a/spec/lib/bitbucket/representation/repo_spec.rb b/spec/lib/bitbucket/representation/repo_spec.rb index a779a153f25..b5b9f45f3d4 100644 --- a/spec/lib/bitbucket/representation/repo_spec.rb +++ b/spec/lib/bitbucket/representation/repo_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Bitbucket::Representation::Repo do describe '#has_wiki?' do diff --git a/spec/lib/bitbucket/representation/user_spec.rb b/spec/lib/bitbucket/representation/user_spec.rb index e1f6c724da8..62431a5ad8b 100644 --- a/spec/lib/bitbucket/representation/user_spec.rb +++ b/spec/lib/bitbucket/representation/user_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Bitbucket::Representation::User do describe '#username' do diff --git a/spec/lib/bitbucket_server/page_spec.rb b/spec/lib/bitbucket_server/page_spec.rb index 2d4e946e590..2837f94ba3e 100644 --- a/spec/lib/bitbucket_server/page_spec.rb +++ b/spec/lib/bitbucket_server/page_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe BitbucketServer::Page do let(:response) { { 'values' => [{ 'description' => 'Test' }], 'isLastPage' => false, 'nextPageStart' => 2 } } diff --git a/spec/lib/bulk_imports/file_downloads/filename_fetch_spec.rb b/spec/lib/bulk_imports/file_downloads/filename_fetch_spec.rb new file mode 100644 index 00000000000..a77eba06027 --- /dev/null +++ b/spec/lib/bulk_imports/file_downloads/filename_fetch_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::FileDownloads::FilenameFetch do + let(:dummy_instance) { dummy_class.new } + let(:dummy_class) do + Class.new do + include BulkImports::FileDownloads::FilenameFetch + end + end + + describe '#raise_error' do + it { expect { dummy_instance.raise_error('text') }.to raise_exception(NotImplementedError) } + end +end diff --git a/spec/lib/bulk_imports/file_downloads/validations_spec.rb b/spec/lib/bulk_imports/file_downloads/validations_spec.rb new file mode 100644 index 00000000000..85f45c2a8f0 --- /dev/null +++ b/spec/lib/bulk_imports/file_downloads/validations_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::FileDownloads::Validations do + let(:dummy_instance) { dummy_class.new } + let(:dummy_class) do + Class.new do + include BulkImports::FileDownloads::Validations + end + end + + describe '#raise_error' do + it { expect { dummy_instance.raise_error('text') }.to raise_exception(NotImplementedError) } + end + + describe '#filepath' do + it { expect { dummy_instance.filepath }.to raise_exception(NotImplementedError) } + end + + describe '#response_headers' do + it { expect { dummy_instance.response_headers }.to raise_exception(NotImplementedError) } + end + + describe '#file_size_limit' do + it { expect { dummy_instance.file_size_limit }.to raise_exception(NotImplementedError) } + end +end diff --git a/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb b/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb index 9c79b3f4c9e..045908de5c4 100644 --- a/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb +++ b/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe BulkImports::Pipeline::ExtractedData do let(:data) { 'data' } diff --git a/spec/lib/bulk_imports/pipeline_spec.rb b/spec/lib/bulk_imports/pipeline_spec.rb index e4ecf99dab0..dc169bb8d88 100644 --- a/spec/lib/bulk_imports/pipeline_spec.rb +++ b/spec/lib/bulk_imports/pipeline_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe BulkImports::Pipeline do let(:context) { instance_double(BulkImports::Pipeline::Context, tracker: nil) } diff --git a/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb index 3f02356b41e..e780cde4ae2 100644 --- a/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb +++ b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do let_it_be(:user) { create(:user) } + let_it_be(:another_user) { create(:user) } let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :repository, group: group) } let_it_be(:bulk_import) { create(:bulk_import, user: user) } @@ -85,6 +86,9 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do describe '#run' do before do group.add_owner(user) + group.add_maintainer(another_user) + + ::BulkImports::UsersMapper.new(context: context).cache_source_user_id(42, another_user.id) allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor| allow(extractor).to receive(:remove_tmp_dir) @@ -293,5 +297,52 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do expect(imported_mr.milestone.title).to eq(attributes.dig('milestone', 'title')) end end + + context 'user assignments' do + let(:attributes) do + { + key => [ + { + 'user_id' => 22, + 'created_at' => '2020-01-07T11:21:21.235Z' + }, + { + 'user_id' => 42, + 'created_at' => '2020-01-08T12:21:21.235Z' + } + ] + } + end + + context 'assignees' do + let(:key) { 'merge_request_assignees' } + + it 'imports mr assignees' do + assignees = imported_mr.merge_request_assignees + + expect(assignees.pluck(:user_id)).to contain_exactly(user.id, another_user.id) + end + end + + context 'approvals' do + let(:key) { 'approvals' } + + it 'imports mr approvals' do + approvals = imported_mr.approvals + + expect(approvals.pluck(:user_id)).to contain_exactly(user.id, another_user.id) + end + end + + context 'reviewers' do + let(:key) { 'merge_request_reviewers' } + + it 'imports mr reviewers' do + reviewers = imported_mr.merge_request_reviewers + + expect(reviewers.pluck(:user_id)).to contain_exactly(user.id, another_user.id) + end + end + end end end diff --git a/spec/lib/bulk_imports/retry_pipeline_error_spec.rb b/spec/lib/bulk_imports/retry_pipeline_error_spec.rb index 9d96407b03a..2ff6a7d2b5c 100644 --- a/spec/lib/bulk_imports/retry_pipeline_error_spec.rb +++ b/spec/lib/bulk_imports/retry_pipeline_error_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe BulkImports::RetryPipelineError do describe '#retry_delay' do diff --git a/spec/lib/constraints/jira_encoded_url_constrainer_spec.rb b/spec/lib/constraints/jira_encoded_url_constrainer_spec.rb index 70e649d35da..f01703033cc 100644 --- a/spec/lib/constraints/jira_encoded_url_constrainer_spec.rb +++ b/spec/lib/constraints/jira_encoded_url_constrainer_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Constraints::JiraEncodedUrlConstrainer do let(:namespace_id) { 'group' } diff --git a/spec/lib/container_registry/gitlab_api_client_spec.rb b/spec/lib/container_registry/gitlab_api_client_spec.rb index 7836d8706f6..f19bedbda0e 100644 --- a/spec/lib/container_registry/gitlab_api_client_spec.rb +++ b/spec/lib/container_registry/gitlab_api_client_spec.rb @@ -421,7 +421,9 @@ RSpec.describe ContainerRegistry::GitlabApiClient do before do expect(Auth::ContainerRegistryAuthenticationService).to receive(:pull_nested_repositories_access_token).with(path.downcase).and_return(token) - stub_repository_details(path, sizing: :self_with_descendants, status_code: 200, respond_with: response) + expect_next_instance_of(described_class) do |client| + expect(client).to receive(:repository_details).with(path.downcase, sizing: :self_with_descendants).and_return(response.with_indifferent_access).once + end end it { is_expected.to eq(555) } diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb index 190ddef0cd5..cb5c6a60e1d 100644 --- a/spec/lib/container_registry/tag_spec.rb +++ b/spec/lib/container_registry/tag_spec.rb @@ -240,6 +240,31 @@ RSpec.describe ContainerRegistry::Tag do it_behaves_like 'setting and caching the created_at value' end end + + describe 'updated_at=' do + subject do + tag.updated_at = input + tag.updated_at + end + + context 'with a valid input' do + let(:input) { 2.days.ago.iso8601 } + + it { is_expected.to eq(DateTime.iso8601(input)) } + end + + context 'with a nil input' do + let(:input) { nil } + + it { is_expected.to eq(nil) } + end + + context 'with an invalid input' do + let(:input) { 'not a timestamp' } + + it { is_expected.to eq(nil) } + end + end end end end diff --git a/spec/lib/declarative_enum_spec.rb b/spec/lib/declarative_enum_spec.rb index 66cda9fc3a8..06e74b639cf 100644 --- a/spec/lib/declarative_enum_spec.rb +++ b/spec/lib/declarative_enum_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe DeclarativeEnum do let(:enum_module) do diff --git a/spec/lib/error_tracking/sentry_client/event_spec.rb b/spec/lib/error_tracking/sentry_client/event_spec.rb index 64e674f1e9b..d65bfa31018 100644 --- a/spec/lib/error_tracking/sentry_client/event_spec.rb +++ b/spec/lib/error_tracking/sentry_client/event_spec.rb @@ -32,6 +32,7 @@ RSpec.describe ErrorTracking::SentryClient do subject { client.issue_latest_event(issue_id: issue_id) } it_behaves_like 'calls sentry api' + it_behaves_like 'Sentry API response size limit' it 'has correct return type' do expect(subject).to be_a(Gitlab::ErrorTracking::ErrorEvent) @@ -50,7 +51,7 @@ RSpec.describe ErrorTracking::SentryClient do end end - context 'error object created from sentry response' do + context 'with error object created from sentry response' do it_behaves_like 'assigns error tracking event correctly' it 'parses the stack trace' do @@ -58,7 +59,7 @@ RSpec.describe ErrorTracking::SentryClient do expect(subject.stack_trace_entries).not_to be_empty end - context 'error without stack trace' do + context 'with error without stack trace' do before do sample_response['entries'] = [] stub_sentry_request(sentry_request_url, body: sample_response) diff --git a/spec/lib/error_tracking/sentry_client/issue_link_spec.rb b/spec/lib/error_tracking/sentry_client/issue_link_spec.rb index f86d328ef89..75e7ac8304e 100644 --- a/spec/lib/error_tracking/sentry_client/issue_link_spec.rb +++ b/spec/lib/error_tracking/sentry_client/issue_link_spec.rb @@ -9,6 +9,7 @@ RSpec.describe ErrorTracking::SentryClient::IssueLink do let_it_be(:error_tracking_setting) { create(:project_error_tracking_setting, api_url: sentry_url) } let_it_be(:issue) { create(:issue, project: error_tracking_setting.project) } + let(:token) { 'test-token' } let(:client) { error_tracking_setting.sentry_client } let(:sentry_issue_id) { 11111111 } @@ -22,11 +23,12 @@ RSpec.describe ErrorTracking::SentryClient::IssueLink do subject { client.create_issue_link(integration_id, sentry_issue_id, issue) } + it_behaves_like 'Sentry API response size limit' it_behaves_like 'calls sentry api' it { is_expected.to be_present } - context 'redirects' do + context 'with redirects' do let(:sentry_api_url) { sentry_issue_link_url } it_behaves_like 'no Sentry redirects', :put @@ -45,11 +47,12 @@ RSpec.describe ErrorTracking::SentryClient::IssueLink do let(:issue_link_sample_response) { Gitlab::Json.parse(fixture_file('sentry/plugin_link_sample_response.json')) } let!(:sentry_api_request) { stub_sentry_request(sentry_issue_link_url, :post, body: sentry_api_response) } + it_behaves_like 'Sentry API response size limit' it_behaves_like 'calls sentry api' it { is_expected.to be_present } - context 'redirects' do + context 'with redirects' do let(:sentry_api_url) { sentry_issue_link_url } it_behaves_like 'no Sentry redirects', :post diff --git a/spec/lib/error_tracking/sentry_client/issue_spec.rb b/spec/lib/error_tracking/sentry_client/issue_spec.rb index d7bb0ca5c9a..1468a1ff7eb 100644 --- a/spec/lib/error_tracking/sentry_client/issue_spec.rb +++ b/spec/lib/error_tracking/sentry_client/issue_spec.rb @@ -58,6 +58,8 @@ RSpec.describe ErrorTracking::SentryClient::Issue do it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error it_behaves_like 'issues have correct length', 3 + it_behaves_like 'maps Sentry exceptions' + it_behaves_like 'Sentry API response size limit', enabled_by_default: true shared_examples 'has correct external_url' do describe '#external_url' do @@ -151,7 +153,7 @@ RSpec.describe ErrorTracking::SentryClient::Issue do context 'with older sentry versions where keys are not present' do let(:sentry_api_response) do - issues_sample_response[0...1].map do |issue| + issues_sample_response.first(1).map do |issue| issue[:project].delete(:id) issue end @@ -167,7 +169,7 @@ RSpec.describe ErrorTracking::SentryClient::Issue do context 'when essential keys are missing in API response' do let(:sentry_api_response) do - issues_sample_response[0...1].map do |issue| + issues_sample_response.first(1).map do |issue| issue.except(:id) end end @@ -178,18 +180,6 @@ RSpec.describe ErrorTracking::SentryClient::Issue do end end - context 'when sentry api response is too large' do - it 'raises exception' do - deep_size = instance_double(Gitlab::Utils::DeepSize, valid?: false) - allow(Gitlab::Utils::DeepSize).to receive(:new).with(sentry_api_response).and_return(deep_size) - - expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError, - 'Sentry API response is too big. Limit is 1 MB.') - end - end - - it_behaves_like 'maps Sentry exceptions' - context 'when search term is present' do let(:search_term) { 'NoMethodError' } let(:sentry_request_url) { "#{sentry_url}/issues/?limit=20&query=is:unresolved NoMethodError" } @@ -219,10 +209,14 @@ RSpec.describe ErrorTracking::SentryClient::Issue do end let(:sentry_request_url) { "#{sentry_url}/issues/#{issue_id}/" } - let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: issue_sample_response) } + let(:sentry_api_response) { issue_sample_response } + let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) } subject { client.issue_details(issue_id: issue_id) } + it_behaves_like 'maps Sentry exceptions' + it_behaves_like 'Sentry API response size limit' + context 'with error object created from sentry response' do using RSpec::Parameterized::TableSyntax @@ -321,6 +315,10 @@ RSpec.describe ErrorTracking::SentryClient::Issue do subject { client.update_issue(issue_id: issue_id, params: params) } + it_behaves_like 'Sentry API response size limit' do + let(:sentry_api_response) { {} } + end + it_behaves_like 'calls sentry api' do let(:sentry_api_request) { stub_sentry_request(sentry_request_url, :put) } end diff --git a/spec/lib/error_tracking/sentry_client/projects_spec.rb b/spec/lib/error_tracking/sentry_client/projects_spec.rb index 247f9c1c085..52f8cdc915e 100644 --- a/spec/lib/error_tracking/sentry_client/projects_spec.rb +++ b/spec/lib/error_tracking/sentry_client/projects_spec.rb @@ -35,10 +35,11 @@ RSpec.describe ErrorTracking::SentryClient::Projects do it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project it_behaves_like 'has correct length', 2 + it_behaves_like 'Sentry API response size limit' context 'essential keys missing in API response' do let(:sentry_api_response) do - projects_sample_response[0...1].map do |project| + projects_sample_response.first(1).map do |project| project.except(:slug) end end @@ -50,7 +51,7 @@ RSpec.describe ErrorTracking::SentryClient::Projects do context 'optional keys missing in sentry response' do let(:sentry_api_response) do - projects_sample_response[0...1].map do |project| + projects_sample_response.first(1).map do |project| project[:organization].delete(:id) project.delete(:id) project.except(:status) diff --git a/spec/lib/error_tracking/sentry_client/repo_spec.rb b/spec/lib/error_tracking/sentry_client/repo_spec.rb index 9a1c7a69c3d..445a8e35f8e 100644 --- a/spec/lib/error_tracking/sentry_client/repo_spec.rb +++ b/spec/lib/error_tracking/sentry_client/repo_spec.rb @@ -19,12 +19,13 @@ RSpec.describe ErrorTracking::SentryClient::Repo do subject { client.repos(organization_slug) } it_behaves_like 'calls sentry api' + it_behaves_like 'Sentry API response size limit' it { is_expected.to all( be_a(Gitlab::ErrorTracking::Repo)) } it { expect(subject.length).to eq(1) } - context 'redirects' do + context 'with redirects' do let(:sentry_api_url) { sentry_repos_url } it_behaves_like 'no Sentry redirects' diff --git a/spec/lib/error_tracking/sentry_client_spec.rb b/spec/lib/error_tracking/sentry_client_spec.rb index 9ffd756f057..633b7ae9a91 100644 --- a/spec/lib/error_tracking/sentry_client_spec.rb +++ b/spec/lib/error_tracking/sentry_client_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe ErrorTracking::SentryClient do let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' } diff --git a/spec/lib/gitlab/alert_management/payload/base_spec.rb b/spec/lib/gitlab/alert_management/payload/base_spec.rb index ad2a3c7b462..3e8d71ac673 100644 --- a/spec/lib/gitlab/alert_management/payload/base_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/base_spec.rb @@ -347,4 +347,26 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do it { is_expected.to be(true) } end + + describe '#source' do + subject { parsed_payload.source } + + it { is_expected.to be_nil } + + context 'with alerting integration provided' do + before do + parsed_payload.integration = instance_double('::AlertManagement::HttpIntegration', name: 'INTEGRATION') + end + + it { is_expected.to eq('INTEGRATION') } + end + + context 'with monitoring tool defined in the raw payload' do + before do + allow(parsed_payload).to receive(:monitoring_tool).and_return('TOOL') + end + + it { is_expected.to eq('TOOL') } + end + end end diff --git a/spec/lib/gitlab/alert_management/payload/generic_spec.rb b/spec/lib/gitlab/alert_management/payload/generic_spec.rb index 59933f7459d..bc3b6edc638 100644 --- a/spec/lib/gitlab/alert_management/payload/generic_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/generic_spec.rb @@ -144,4 +144,40 @@ RSpec.describe Gitlab::AlertManagement::Payload::Generic do it { is_expected.to eq(value) } end end + + describe '#resolved?' do + subject { parsed_payload.resolved? } + + context 'without end time' do + it { is_expected.to eq(false) } + end + + context 'with end time' do + let(:raw_payload) { { 'end_time' => Time.current.to_s } } + + it { is_expected.to eq(true) } + end + end + + describe '#source' do + subject { parsed_payload.source } + + it { is_expected.to eq('Generic Alert Endpoint') } + + context 'with alerting integration provided' do + before do + parsed_payload.integration = instance_double('::AlertManagement::HttpIntegration', name: 'INTEGRATION') + end + + it { is_expected.to eq('INTEGRATION') } + end + + context 'with monitoring tool defined in the raw payload' do + before do + allow(parsed_payload).to receive(:monitoring_tool).and_return('TOOL') + end + + it { is_expected.to eq('TOOL') } + end + end end diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb index 6fc658ecade..1e0034e386e 100644 --- a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb +++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::StageEvent do let(:instance) { described_class.new({}) } diff --git a/spec/lib/gitlab/analytics/date_filler_spec.rb b/spec/lib/gitlab/analytics/date_filler_spec.rb new file mode 100644 index 00000000000..3f547f667f2 --- /dev/null +++ b/spec/lib/gitlab/analytics/date_filler_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true +require 'fast_spec_helper' + +RSpec.describe Gitlab::Analytics::DateFiller do + let(:default_value) { 0 } + let(:formatter) { Gitlab::Analytics::DateFiller::DEFAULT_DATE_FORMATTER } + + subject(:filler_result) do + described_class.new(data, + from: from, + to: to, + period: period, + default_value: default_value, + date_formatter: formatter).fill.to_a + end + + context 'when unknown period is given' do + it 'raises error' do + input = { 3.days.ago.to_date => 10, Date.today => 5 } + + expect do + described_class.new(input, from: 4.days.ago, to: Date.today, period: :unknown).fill + end.to raise_error(/Unknown period given/) + end + end + + context 'when period=:day' do + let(:from) { Date.new(2021, 5, 25) } + let(:to) { Date.new(2021, 6, 5) } + let(:period) { :day } + + let(:expected_result) do + { + Date.new(2021, 5, 25) => 1, + Date.new(2021, 5, 26) => default_value, + Date.new(2021, 5, 27) => default_value, + Date.new(2021, 5, 28) => default_value, + Date.new(2021, 5, 29) => default_value, + Date.new(2021, 5, 30) => default_value, + Date.new(2021, 5, 31) => default_value, + Date.new(2021, 6, 1) => default_value, + Date.new(2021, 6, 2) => default_value, + Date.new(2021, 6, 3) => 10, + Date.new(2021, 6, 4) => default_value, + Date.new(2021, 6, 5) => default_value + } + end + + let(:data) do + { + Date.new(2021, 6, 3) => 10, # deliberatly not sorted + Date.new(2021, 5, 27) => nil, + Date.new(2021, 5, 25) => 1 + } + end + + it { is_expected.to eq(expected_result.to_a) } + + context 'when a custom default value is given' do + let(:default_value) { 'MISSING' } + + it do + is_expected.to eq(expected_result.to_a) + end + end + + context 'when a custom date formatter is given' do + let(:formatter) { -> (date) { date.to_s } } + + it do + expected_result.transform_keys!(&:to_s) + + is_expected.to eq(expected_result.to_a) + end + end + + context 'when the data contains dates outside of the requested period' do + before do + data[Date.new(2022, 6, 1)] = 5 + end + + it 'raises error' do + expect { filler_result }.to raise_error(/Input contains values which doesn't/) + end + end + end + + context 'when period=:week' do + let(:from) { Date.new(2021, 5, 16) } + let(:to) { Date.new(2021, 6, 7) } + let(:period) { :week } + let(:data) do + { + Date.new(2021, 5, 24) => nil, + Date.new(2021, 6, 7) => 10 + } + end + + let(:expected_result) do + { + Date.new(2021, 5, 10) => 0, + Date.new(2021, 5, 17) => 0, + Date.new(2021, 5, 24) => 0, + Date.new(2021, 5, 31) => 0, + Date.new(2021, 6, 7) => 10 + } + end + + it do + is_expected.to eq(expected_result.to_a) + end + end + + context 'when period=:month' do + let(:from) { Date.new(2021, 5, 1) } + let(:to) { Date.new(2021, 7, 1) } + let(:period) { :month } + let(:data) do + { + Date.new(2021, 5, 1) => 100 + } + end + + let(:expected_result) do + { + Date.new(2021, 5, 1) => 100, + Date.new(2021, 6, 1) => 0, + Date.new(2021, 7, 1) => 0 + } + end + + it do + is_expected.to eq(expected_result.to_a) + end + end +end diff --git a/spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb b/spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb index b34ac538b24..12679b51ce9 100644 --- a/spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb +++ b/spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::ApplicationRateLimiter::BaseStrategy do describe '#increment' do diff --git a/spec/lib/gitlab/asciidoc/html5_converter_spec.rb b/spec/lib/gitlab/asciidoc/html5_converter_spec.rb index 84c2cda496e..de1b3e2af71 100644 --- a/spec/lib/gitlab/asciidoc/html5_converter_spec.rb +++ b/spec/lib/gitlab/asciidoc/html5_converter_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Asciidoc::Html5Converter do describe 'convert AsciiDoc to HTML5' do diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index b2bce2076b0..8fec8bce23e 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -638,9 +638,9 @@ module Gitlab context 'with project' do let(:context) do { - commit: commit, - project: project, - ref: ref, + commit: commit, + project: project, + ref: ref, requested_path: requested_path } end diff --git a/spec/lib/gitlab/audit/auditor_spec.rb b/spec/lib/gitlab/audit/auditor_spec.rb index fc5917ca583..f743515e616 100644 --- a/spec/lib/gitlab/audit/auditor_spec.rb +++ b/spec/lib/gitlab/audit/auditor_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Audit::Auditor do let(:name) { 'audit_operation' } - let(:author) { create(:user) } + let(:author) { create(:user, :with_sign_ins) } let(:group) { create(:group) } let(:provider) { 'standard' } let(:context) do @@ -37,6 +37,13 @@ RSpec.describe Gitlab::Audit::Auditor do ).and_call_original audit! + + authentication_event = AuthenticationEvent.last + + expect(authentication_event.user).to eq(author) + expect(authentication_event.user_name).to eq(author.name) + expect(authentication_event.ip_address).to eq(author.current_sign_in_ip) + expect(authentication_event.provider).to eq(provider) end it 'logs audit events to database', :aggregate_failures do diff --git a/spec/lib/gitlab/audit/null_target_spec.rb b/spec/lib/gitlab/audit/null_target_spec.rb index f192e0cd8db..9197b72afd0 100644 --- a/spec/lib/gitlab/audit/null_target_spec.rb +++ b/spec/lib/gitlab/audit/null_target_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Audit::NullTarget do subject { described_class.new } diff --git a/spec/lib/gitlab/audit/type/definition_spec.rb b/spec/lib/gitlab/audit/type/definition_spec.rb new file mode 100644 index 00000000000..9f4282a4ec0 --- /dev/null +++ b/spec/lib/gitlab/audit/type/definition_spec.rb @@ -0,0 +1,219 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Audit::Type::Definition do + let(:attributes) do + { name: 'group_deploy_token_destroyed', + description: 'Group deploy token is deleted', + introduced_by_issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/1', + introduced_by_mr: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1', + group: 'govern::compliance', + milestone: '15.4', + saved_to_database: true, + streamed: true } + end + + let(:path) { File.join('types', 'group_deploy_token_destroyed.yml') } + let(:definition) { described_class.new(path, attributes) } + let(:yaml_content) { attributes.deep_stringify_keys.to_yaml } + + describe '#key' do + subject { definition.key } + + it 'returns a symbol from name' do + is_expected.to eq(:group_deploy_token_destroyed) + end + end + + describe '#validate!', :aggregate_failures do + using RSpec::Parameterized::TableSyntax + + # rubocop:disable Layout/LineLength + where(:param, :value, :result) do + :path | 'audit_event/types/invalid.yml' | /Audit event type 'group_deploy_token_destroyed' has an invalid path/ + :name | nil | %r{property '/name' is not of type: string} + :description | nil | %r{property '/description' is not of type: string} + :introduced_by_issue | nil | %r{property '/introduced_by_issue' is not of type: string} + :introduced_by_mr | nil | %r{property '/introduced_by_mr' is not of type: string} + :group | nil | %r{property '/group' is not of type: string} + :milestone | nil | %r{property '/milestone' is not of type: string} + end + # rubocop:enable Layout/LineLength + + with_them do + let(:params) { attributes.merge(path: path) } + + before do + params[param] = value + end + + it do + expect do + described_class.new( + params[:path], params.except(:path) + ).validate! + end.to raise_error(result) + end + end + + context 'when both saved_to_database and streamed are false' do + let(:params) { attributes.merge({ path: path, saved_to_database: false, streamed: false }) } + + it 'raises an exception' do + expect do + described_class.new( + params[:path], params.except(:path) + ).validate! + end.to raise_error(/root is invalid: error_type=not/) + end + end + end + + describe '.paths' do + it 'returns at least one path' do + expect(described_class.paths).not_to be_empty + end + end + + describe '.get' do + before do + allow(described_class).to receive(:definitions) do + { definition.key => definition } + end + end + + context 'when audit event type is not defined' do + let(:undefined_audit_event_type) { 'undefined_audit_event_type' } + + it 'returns nil' do + expect(described_class.get(undefined_audit_event_type)).to be nil + end + end + + context 'when audit event type is defined' do + let(:audit_event_type) { 'group_deploy_token_destroyed' } + + it 'returns an instance of Gitlab::Audit::Type::Definition' do + expect(described_class.get(audit_event_type)).to be_an_instance_of(described_class) + end + + it 'returns the properties as defined for that audit event type', :aggregate_failures do + audit_event_type_definition = described_class.get(audit_event_type) + + expect(audit_event_type_definition.name).to eq "group_deploy_token_destroyed" + expect(audit_event_type_definition.description).to eq "Group deploy token is deleted" + expect(audit_event_type_definition.group).to eq "govern::compliance" + expect(audit_event_type_definition.milestone).to eq "15.4" + expect(audit_event_type_definition.saved_to_database).to be true + expect(audit_event_type_definition.streamed).to be true + end + end + end + + describe '.load_from_file' do + it 'properly loads a definition from file' do + expect_file_read(path, content: yaml_content) + + expect(described_class.send(:load_from_file, path).attributes) + .to eq(definition.attributes) + end + + context 'for missing file' do + let(:path) { 'missing/audit_events/type/file.yml' } + + it 'raises exception' do + expect do + described_class.send(:load_from_file, path) + end.to raise_error(/Invalid definition for/) + end + end + + context 'for invalid definition' do + it 'raises exception' do + expect_file_read(path, content: '{}') + + expect do + described_class.send(:load_from_file, path) + end.to raise_error(%r{property '/name' is not of type: string}) + end + end + end + + describe '.load_all!' do + let(:store1) { Dir.mktmpdir('path1') } + let(:store2) { Dir.mktmpdir('path2') } + let(:definitions) { {} } + + before do + allow(described_class).to receive(:paths).and_return( + [ + File.join(store1, '**', '*.yml'), + File.join(store2, '**', '*.yml') + ] + ) + end + + subject { described_class.send(:load_all!) } + + after do + FileUtils.rm_rf(store1) + FileUtils.rm_rf(store2) + end + + it "when there are no audit event types a list of definitions is empty" do + is_expected.to be_empty + end + + it "when there's a single audit event type it properly loads them" do + write_audit_event_type(store1, path, yaml_content) + + is_expected.to be_one + end + + it "when the same audit event type is stored multiple times raises exception" do + write_audit_event_type(store1, path, yaml_content) + write_audit_event_type(store2, path, yaml_content) + + expect { subject } + .to raise_error(/Audit event type 'group_deploy_token_destroyed' is already defined/) + end + + it "when one of the YAMLs is invalid it does raise exception" do + write_audit_event_type(store1, path, '{}') + + expect { subject }.to raise_error(/Invalid definition for .* '' must match the filename/) + end + end + + describe '.definitions' do + let(:store1) { Dir.mktmpdir('path1') } + + before do + allow(described_class).to receive(:paths).and_return( + [ + File.join(store1, '**', '*.yml') + ] + ) + end + + subject { described_class.definitions } + + after do + FileUtils.rm_rf(store1) + end + + it "loads the definitions for all the audit event types" do + write_audit_event_type(store1, path, yaml_content) + + is_expected.to be_one + end + end + + def write_audit_event_type(store, path, content) + path = File.join(store, path) + dir = File.dirname(path) + FileUtils.mkdir_p(dir) + File.write(path, content) + end +end diff --git a/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb index 9dff7f7b3dc..e8008aeaf57 100644 --- a/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb +++ b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb @@ -20,17 +20,17 @@ RSpec.describe Gitlab::Auth::Ldap::AuthHash do let(:info) do { - name: 'Smith, J.', - email: 'johnsmith@example.com', + name: 'Smith, J.', + email: 'johnsmith@example.com', nickname: '123456' } end let(:raw_info) do { - uid: ['123456'], - email: ['johnsmith@example.com'], - cn: ['Smith, J.'], + uid: ['123456'], + email: ['johnsmith@example.com'], + cn: ['Smith, J.'], fullName: ['John Smith'] } end @@ -52,8 +52,8 @@ RSpec.describe Gitlab::Auth::Ldap::AuthHash do let(:attributes) do { - 'username' => %w(mail email), - 'name' => 'fullName' + 'username' => %w(mail email), + 'name' => 'fullName' } end diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb index 3039fce6141..3be983857bc 100644 --- a/spec/lib/gitlab/auth/ldap/config_spec.rb +++ b/spec/lib/gitlab/auth/ldap/config_spec.rb @@ -112,8 +112,8 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'constructs basic options' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 386, + 'host' => 'ldap.example.com', + 'port' => 386, 'encryption' => 'plain' } ) @@ -129,16 +129,16 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'includes failover hosts when set' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'hosts' => [ + 'host' => 'ldap.example.com', + 'port' => 686, + 'hosts' => [ ['ldap1.example.com', 636], ['ldap2.example.com', 636] ], - 'encryption' => 'simple_tls', + 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'bind_dn' => 'uid=admin,dc=example,dc=com', - 'password' => 'super_secret' + 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'password' => 'super_secret' } ) @@ -158,12 +158,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'includes authentication options when auth is configured' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls', + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'bind_dn' => 'uid=admin,dc=example,dc=com', - 'password' => 'super_secret' + 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'password' => 'super_secret' } ) @@ -179,9 +179,9 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'sets encryption method to simple_tls when configured as simple_tls' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls' + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls' } ) @@ -191,9 +191,9 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'sets encryption method to start_tls when configured as start_tls' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'start_tls' + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'start_tls' } ) @@ -203,12 +203,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'transforms SSL cert and key to OpenSSL objects' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'start_tls', - 'tls_options' => { - 'cert' => raw_cert, - 'key' => raw_key + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'start_tls', + 'tls_options' => { + 'cert' => raw_cert, + 'key' => raw_key } } ) @@ -221,12 +221,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK allow(Gitlab::AppLogger).to receive(:error) stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'start_tls', - 'tls_options' => { - 'cert' => 'invalid cert', - 'key' => 'invalid_key' + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'start_tls', + 'tls_options' => { + 'cert' => 'invalid cert', + 'key' => 'invalid_key' } } ) @@ -240,9 +240,9 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'sets tls_options to OpenSSL defaults' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls', + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', 'verify_certificates' => true } ) @@ -255,9 +255,9 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'sets verify_mode to OpenSSL VERIFY_NONE' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls', + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', 'verify_certificates' => false } ) @@ -274,11 +274,11 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'passes it through in tls_options' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls', - 'tls_options' => { - 'ca_file' => '/etc/ca.pem' + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'tls_options' => { + 'ca_file' => '/etc/ca.pem' } } ) @@ -291,11 +291,11 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'does not add the ca_file key to tls_options' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls', - 'tls_options' => { - 'ca_file' => ' ' + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'tls_options' => { + 'ca_file' => ' ' } } ) @@ -308,11 +308,11 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'passes it through in tls_options' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls', - 'tls_options' => { - 'ssl_version' => 'TLSv1_2' + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'tls_options' => { + 'ssl_version' => 'TLSv1_2' } } ) @@ -325,11 +325,11 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'does not add the ssl_version key to tls_options' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls', - 'tls_options' => { - 'ssl_version' => ' ' + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'tls_options' => { + 'ssl_version' => ' ' } } ) @@ -343,11 +343,11 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'constructs basic options' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 386, - 'base' => 'ou=users,dc=example,dc=com', + 'host' => 'ldap.example.com', + 'port' => 386, + 'base' => 'ou=users,dc=example,dc=com', 'encryption' => 'plain', - 'uid' => 'uid' + 'uid' => 'uid' } ) @@ -364,10 +364,10 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'includes authentication options when auth is configured' do stub_ldap_config( options: { - 'uid' => 'sAMAccountName', + 'uid' => 'sAMAccountName', 'user_filter' => '(memberOf=cn=group1,ou=groups,dc=example,dc=com)', - 'bind_dn' => 'uid=admin,dc=example,dc=com', - 'password' => 'super_secret' + 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'password' => 'super_secret' } ) @@ -381,12 +381,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'transforms SSL cert and key to OpenSSL objects' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'start_tls', - 'tls_options' => { - 'cert' => raw_cert, - 'key' => raw_key + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'start_tls', + 'tls_options' => { + 'cert' => raw_cert, + 'key' => raw_key } } ) @@ -399,9 +399,9 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'specifies disable_verify_certificates as false' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls', + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', 'verify_certificates' => true } ) @@ -414,9 +414,9 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'specifies disable_verify_certificates as true' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls', + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', 'verify_certificates' => false } ) @@ -429,12 +429,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'passes it through' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls', + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'tls_options' => { - 'ca_file' => '/etc/ca.pem' + 'tls_options' => { + 'ca_file' => '/etc/ca.pem' } } ) @@ -447,12 +447,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'does not include the ca_file option' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls', + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'tls_options' => { - 'ca_file' => ' ' + 'tls_options' => { + 'ca_file' => ' ' } } ) @@ -465,12 +465,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'passes it through' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls', + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'tls_options' => { - 'ssl_version' => 'TLSv1_2' + 'tls_options' => { + 'ssl_version' => 'TLSv1_2' } } ) @@ -483,12 +483,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'does not include the ssl_version option' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'simple_tls', + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'tls_options' => { - 'ssl_version' => ' ' + 'tls_options' => { + 'ssl_version' => ' ' } } ) @@ -503,7 +503,7 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'is true when password is set' do stub_ldap_config( options: { - 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'bind_dn' => 'uid=admin,dc=example,dc=com', 'password' => 'super_secret' } ) @@ -514,7 +514,7 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK it 'is true when bind_dn is set and password is empty' do stub_ldap_config( options: { - 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'bind_dn' => 'uid=admin,dc=example,dc=com', 'password' => '' } ) @@ -539,15 +539,15 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK options: { 'attributes' => { 'username' => %w(sAMAccountName), - 'email' => %w(userPrincipalName) + 'email' => %w(userPrincipalName) } } ) expect(config.attributes).to include({ 'username' => %w(sAMAccountName), - 'email' => %w(userPrincipalName), - 'name' => 'cn' + 'email' => %w(userPrincipalName), + 'name' => 'cn' }) end end diff --git a/spec/lib/gitlab/auth/ldap/person_spec.rb b/spec/lib/gitlab/auth/ldap/person_spec.rb index 6857b561370..f8268bb1666 100644 --- a/spec/lib/gitlab/auth/ldap/person_spec.rb +++ b/spec/lib/gitlab/auth/ldap/person_spec.rb @@ -10,10 +10,10 @@ RSpec.describe Gitlab::Auth::Ldap::Person do before do stub_ldap_config( options: { - 'uid' => 'uid', + 'uid' => 'uid', 'attributes' => { - 'name' => 'cn', - 'email' => %w(mail email userPrincipalName), + 'name' => 'cn', + 'email' => %w(mail email userPrincipalName), 'username' => username_attribute } } @@ -53,10 +53,10 @@ RSpec.describe Gitlab::Auth::Ldap::Person do it 'returns a compact and unique array' do stub_ldap_config( options: { - 'uid' => nil, + 'uid' => nil, 'attributes' => { - 'name' => 'cn', - 'email' => 'mail', + 'name' => 'cn', + 'email' => 'mail', 'username' => %w(uid mail), 'first_name' => '' } 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 a044094179c..c94f962ee93 100644 --- a/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb @@ -40,12 +40,12 @@ RSpec.describe Gitlab::Auth::OAuth::AuthHash do let(:info_hash) do { - email: email_ascii, + email: email_ascii, first_name: first_name_ascii, - last_name: last_name_ascii, - name: name_ascii, - nickname: nickname_ascii, - uid: uid_ascii, + last_name: last_name_ascii, + name: name_ascii, + nickname: nickname_ascii, + uid: uid_ascii, address: { locality: 'some locality', country: 'some country' diff --git a/spec/lib/gitlab/auth/o_auth/provider_spec.rb b/spec/lib/gitlab/auth/o_auth/provider_spec.rb index c1b96819176..96a31c50989 100644 --- a/spec/lib/gitlab/auth/o_auth/provider_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/provider_spec.rb @@ -90,6 +90,24 @@ RSpec.describe Gitlab::Auth::OAuth::Provider do end end end + + context 'for an OpenID Connect provider' do + before do + provider = ActiveSupport::InheritableOptions.new( + name: 'openid_connect', + args: ActiveSupport::InheritableOptions.new(name: 'custom_oidc') + ) + allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider]) + end + + context 'when the provider exists' do + subject { described_class.config_for('custom_oidc') } + + it 'returns the config' do + expect(subject).to be_a(ActiveSupport::InheritableOptions) + end + end + end end describe '.label_for' do diff --git a/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb b/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb index 57ee53a452e..61e17ad2424 100644 --- a/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb +++ b/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb @@ -49,10 +49,10 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do stub_request(:post, otp_verification_url) .with(body: JSON(otp_verification_request_body), - headers: { - 'Content-Type' => 'application/json', - 'Authorization' => "Bearer #{access_token}" - }) + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer #{access_token}" + }) .to_return(status: otp_verification_response_status, body: '', headers: {}) end diff --git a/spec/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities_spec.rb new file mode 100644 index 00000000000..3aab0cdf54b --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillClusterAgentsHasVulnerabilities, :migration do # rubocop:disable Layout/LineLength + let(:migration) do + described_class.new(start_id: 1, end_id: 10, + batch_table: table_name, batch_column: batch_column, + sub_batch_size: sub_batch_size, pause_ms: pause_ms, + connection: ApplicationRecord.connection) + end + + let(:users_table) { table(:users) } + let(:vulnerability_reads_table) { table(:vulnerability_reads) } + let(:vulnerability_scanners_table) { table(:vulnerability_scanners) } + let(:vulnerabilities_table) { table(:vulnerabilities) } + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:cluster_agents_table) { table(:cluster_agents) } + + let(:table_name) { 'cluster_agents' } + let(:batch_column) { :id } + let(:sub_batch_size) { 100 } + let(:pause_ms) { 0 } + + subject(:perform_migration) { migration.perform } + + before do + users_table.create!(id: 1, name: 'John Doe', email: 'test@example.com', projects_limit: 5) + + namespaces_table.create!(id: 1, name: 'Namespace 1', path: 'namespace-1') + namespaces_table.create!(id: 2, name: 'Namespace 2', path: 'namespace-2') + namespaces_table.create!(id: 3, name: 'Namespace 3', path: 'namespace-3') + + projects_table.create!(id: 1, namespace_id: 1, name: 'Project 1', path: 'project-1', project_namespace_id: 1) + projects_table.create!(id: 2, namespace_id: 2, name: 'Project 2', path: 'project-2', project_namespace_id: 2) + projects_table.create!(id: 3, namespace_id: 2, name: 'Project 3', path: 'project-3', project_namespace_id: 3) + + cluster_agents_table.create!(id: 1, name: 'Agent 1', project_id: 1) + cluster_agents_table.create!(id: 2, name: 'Agent 2', project_id: 2) + cluster_agents_table.create!(id: 3, name: 'Agent 3', project_id: 1) + cluster_agents_table.create!(id: 4, name: 'Agent 4', project_id: 1) + cluster_agents_table.create!(id: 5, name: 'Agent 5', project_id: 1) + cluster_agents_table.create!(id: 6, name: 'Agent 6', project_id: 1) + cluster_agents_table.create!(id: 7, name: 'Agent 7', project_id: 3) + cluster_agents_table.create!(id: 8, name: 'Agent 8', project_id: 1) + cluster_agents_table.create!(id: 9, name: 'Agent 9', project_id: 1) + cluster_agents_table.create!(id: 10, name: 'Agent 10', project_id: 3) + cluster_agents_table.create!(id: 11, name: 'Agent 11', project_id: 1) + + vulnerability_scanners_table.create!(id: 1, project_id: 1, external_id: 'starboard', name: 'Starboard') + vulnerability_scanners_table.create!(id: 2, project_id: 2, external_id: 'starboard', name: 'Starboard') + vulnerability_scanners_table.create!(id: 3, project_id: 3, external_id: 'starboard', name: 'Starboard') + + add_vulnerability_read!(1, project_id: 1, cluster_agent_id: 1, report_type: 7) + add_vulnerability_read!(2, project_id: 1, cluster_agent_id: nil, report_type: 7) + add_vulnerability_read!(3, project_id: 1, cluster_agent_id: 3, report_type: 7) + add_vulnerability_read!(4, project_id: 1, cluster_agent_id: nil, report_type: 7) + add_vulnerability_read!(5, project_id: 2, cluster_agent_id: 5, report_type: 5) + add_vulnerability_read!(7, project_id: 2, cluster_agent_id: 7, report_type: 7) + add_vulnerability_read!(9, project_id: 3, cluster_agent_id: 9, report_type: 7) + add_vulnerability_read!(10, project_id: 1, cluster_agent_id: 10, report_type: 7) + add_vulnerability_read!(11, project_id: 2, cluster_agent_id: 11, report_type: 7) + end + + it 'backfills `has_vulnerabilities` for the selected records', :aggregate_failures do + queries = ActiveRecord::QueryRecorder.new do + perform_migration + end + + expect(queries.count).to eq(3) + expect(cluster_agents_table.where(has_vulnerabilities: true).count).to eq 2 + expect(cluster_agents_table.where(has_vulnerabilities: true).pluck(:id)).to match_array([1, 3]) + end + + it 'tracks timings of queries' do + expect(migration.batch_metrics.timings).to be_empty + + expect { perform_migration }.to change { migration.batch_metrics.timings } + end + + private + + def add_vulnerability_read!(id, project_id:, cluster_agent_id:, report_type:) + vulnerabilities_table.create!( + id: id, + project_id: project_id, + author_id: 1, + title: "Vulnerability #{id}", + severity: 5, + confidence: 5, + report_type: report_type + ) + + vulnerability_reads_table.create!( + id: id, + uuid: SecureRandom.uuid, + severity: 5, + state: 1, + vulnerability_id: id, + scanner_id: project_id, + casted_cluster_agent_id: cluster_agent_id, + project_id: project_id, + report_type: report_type + ) + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb b/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb index e363a5a6b20..8947262ae9e 100644 --- a/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb @@ -14,10 +14,10 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillImportedIssueSearchData, table(:projects) .create!( namespace_id: namespace.id, - creator_id: user.id, - name: 'projecty', - path: 'path', - project_namespace_id: namespace.id) + creator_id: user.id, + name: 'projecty', + path: 'path', + project_namespace_id: namespace.id) end let!(:issue) do diff --git a/spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb b/spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb index b3825a7c4ea..3c0b7766871 100644 --- a/spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb @@ -9,25 +9,35 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillIntegrationsEnableSslVerific before do integrations.create!(id: 1, type_new: 'Integrations::Bamboo') # unaffected integration integrations.create!(id: 2, type_new: 'Integrations::DroneCi') # no properties - integrations.create!(id: 3, type_new: 'Integrations::DroneCi', + integrations.create!( + id: 3, type_new: 'Integrations::DroneCi', properties: {}) # no URL - integrations.create!(id: 4, type_new: 'Integrations::DroneCi', + integrations.create!( + id: 4, type_new: 'Integrations::DroneCi', properties: { 'drone_url' => '' }) # blank URL - integrations.create!(id: 5, type_new: 'Integrations::DroneCi', + integrations.create!( + id: 5, type_new: 'Integrations::DroneCi', properties: { 'drone_url' => 'https://example.com:foo' }) # invalid URL - integrations.create!(id: 6, type_new: 'Integrations::DroneCi', + integrations.create!( + id: 6, type_new: 'Integrations::DroneCi', properties: { 'drone_url' => 'https://example.com' }) # unknown URL - integrations.create!(id: 7, type_new: 'Integrations::DroneCi', + integrations.create!( + id: 7, type_new: 'Integrations::DroneCi', properties: { 'drone_url' => 'http://cloud.drone.io' }) # no HTTPS - integrations.create!(id: 8, type_new: 'Integrations::DroneCi', + integrations.create!( + id: 8, type_new: 'Integrations::DroneCi', properties: { 'drone_url' => 'https://cloud.drone.io' }) # known URL - integrations.create!(id: 9, type_new: 'Integrations::Teamcity', + integrations.create!( + id: 9, type_new: 'Integrations::Teamcity', properties: { 'teamcity_url' => 'https://example.com' }) # unknown URL - integrations.create!(id: 10, type_new: 'Integrations::Teamcity', + integrations.create!( + id: 10, type_new: 'Integrations::Teamcity', properties: { 'teamcity_url' => 'https://foo.bar.teamcity.com' }) # unknown URL - integrations.create!(id: 11, type_new: 'Integrations::Teamcity', + integrations.create!( + id: 11, type_new: 'Integrations::Teamcity', properties: { 'teamcity_url' => 'https://teamcity.com' }) # unknown URL - integrations.create!(id: 12, type_new: 'Integrations::Teamcity', + integrations.create!( + id: 12, type_new: 'Integrations::Teamcity', properties: { 'teamcity_url' => 'https://customer.teamcity.com' }) # known URL end diff --git a/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb new file mode 100644 index 00000000000..29833074109 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' +# todo: this will need to specify schema version once we introduce the not null constraint on issues#namespace_id +# https://gitlab.com/gitlab-org/gitlab/-/issues/367835 +RSpec.describe Gitlab::BackgroundMigration::BackfillProjectNamespaceOnIssues do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + + let(:namespace1) { namespaces.create!(name: 'batchtest1', type: 'Group', path: 'space1') } + let(:namespace2) { namespaces.create!(name: 'batchtest2', type: 'Group', parent_id: namespace1.id, path: 'space2') } + + let(:proj_namespace1) { namespaces.create!(name: 'proj1', path: 'proj1', type: 'Project', parent_id: namespace1.id) } + let(:proj_namespace2) { namespaces.create!(name: 'proj2', path: 'proj2', type: 'Project', parent_id: namespace2.id) } + + # rubocop:disable Layout/LineLength + let(:proj1) { projects.create!(name: 'proj1', path: 'proj1', namespace_id: namespace1.id, project_namespace_id: proj_namespace1.id) } + let(:proj2) { projects.create!(name: 'proj2', path: 'proj2', namespace_id: namespace2.id, project_namespace_id: proj_namespace2.id) } + + let!(:proj1_issue_with_namespace) { issues.create!(title: 'issue1', project_id: proj1.id, namespace_id: proj_namespace1.id) } + let!(:proj1_issue_without_namespace1) { issues.create!(title: 'issue2', project_id: proj1.id) } + let!(:proj1_issue_without_namespace2) { issues.create!(title: 'issue3', project_id: proj1.id) } + let!(:proj2_issue_with_namespace) { issues.create!(title: 'issue4', project_id: proj2.id, namespace_id: proj_namespace2.id) } + let!(:proj2_issue_without_namespace1) { issues.create!(title: 'issue5', project_id: proj2.id) } + let!(:proj2_issue_without_namespace2) { issues.create!(title: 'issue6', project_id: proj2.id) } + # rubocop:enable Layout/LineLength + + let(:migration) do + described_class.new( + start_id: proj1_issue_with_namespace.id, + end_id: proj2_issue_without_namespace2.id, + batch_table: :issues, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 2, + connection: ApplicationRecord.connection + ) + end + + subject(:perform_migration) { migration.perform } + + it 'backfills namespace_id for the selected records', :aggregate_failures do + perform_migration + + expected_namespaces = [proj_namespace1.id, proj_namespace2.id] + + expect(issues.where.not(namespace_id: nil).count).to eq(6) + expect(issues.where.not(namespace_id: nil).pluck(:namespace_id).uniq).to match_array(expected_namespaces) + end + + it 'tracks timings of queries' do + expect(migration.batch_metrics.timings).to be_empty + + expect { perform_migration }.to change { migration.batch_metrics.timings } + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb index 6f75d3faef3..1c2e0e991d9 100644 --- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb @@ -14,23 +14,23 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat let!(:user) do users.create!(id: 1, - email: 'user@example.com', - projects_limit: 10, - username: 'test', - name: user_name, - state: user_state, - last_activity_on: 1.minute.ago, - user_type: user_type, - confirmed_at: 1.day.ago) + email: 'user@example.com', + projects_limit: 10, + username: 'test', + name: user_name, + state: user_state, + last_activity_on: 1.minute.ago, + user_type: user_type, + confirmed_at: 1.day.ago) end let!(:migration_bot) do users.create!(id: 100, - email: "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}", - user_type: HasUserType::USER_TYPES[:migration_bot], - name: 'GitLab Migration Bot', - projects_limit: 10, - username: 'bot') + email: "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}", + user_type: HasUserType::USER_TYPES[:migration_bot], + name: 'GitLab Migration Bot', + projects_limit: 10, + username: 'bot') end let!(:snippet_with_repo) { snippets.create!(id: 1, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } @@ -260,14 +260,14 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat let(:user_name) { '.' } let!(:other_user) do users.create!(id: 2, - email: 'user2@example.com', - projects_limit: 10, - username: 'test2', - name: 'Test2', - state: user_state, - last_activity_on: 1.minute.ago, - user_type: user_type, - confirmed_at: 1.day.ago) + email: 'user2@example.com', + projects_limit: 10, + username: 'test2', + name: 'Test2', + state: user_state, + last_activity_on: 1.minute.ago, + user_type: user_type, + confirmed_at: 1.day.ago) end let!(:invalid_snippet) { snippets.create!(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: '.', content: content) } diff --git a/spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb b/spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb index 79699375a8d..f642ec8c20d 100644 --- a/spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb @@ -23,8 +23,6 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillVulnerabilityReadsClusterAge let(:sub_batch_size) { 1_000 } let(:pause_ms) { 0 } - subject(:perform_migration) { migration.perform } - before do users_table.create!(id: 1, name: 'John Doe', email: 'test@example.com', projects_limit: 5) @@ -49,20 +47,30 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillVulnerabilityReadsClusterAge add_vulnerability_read!(11, project_id: 1, cluster_agent_id: 1, report_type: 7) end - it 'backfills `casted_cluster_agent_id` for the selected records', :aggregate_failures do - queries = ActiveRecord::QueryRecorder.new do - perform_migration + describe '#filter_batch' do + it 'pick only vulnerability reads where report_type = 7' do + expect(migration.filter_batch(vulnerability_reads_table).pluck(:id)).to contain_exactly(1, 3, 7, 9, 10, 11) end - - expect(queries.count).to eq(3) - expect(vulnerability_reads_table.where.not(casted_cluster_agent_id: nil).count).to eq 3 - expect(vulnerability_reads_table.where.not(casted_cluster_agent_id: nil).pluck(:id)).to match_array([1, 9, 10]) end - it 'tracks timings of queries' do - expect(migration.batch_metrics.timings).to be_empty + describe '#perform' do + subject(:perform_migration) { migration.perform } - expect { perform_migration }.to change { migration.batch_metrics.timings } + it 'backfills `casted_cluster_agent_id` for the selected records', :aggregate_failures do + queries = ActiveRecord::QueryRecorder.new do + perform_migration + end + + expect(queries.count).to eq(3) + expect(vulnerability_reads_table.where.not(casted_cluster_agent_id: nil).count).to eq 3 + expect(vulnerability_reads_table.where.not(casted_cluster_agent_id: nil).pluck(:id)).to match_array([1, 9, 10]) + end + + it 'tracks timings of queries' do + expect(migration.batch_metrics.timings).to be_empty + + expect { perform_migration }.to change { migration.batch_metrics.timings } + end end private diff --git a/spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb b/spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb index 8d82c533d20..6ef474ad7f9 100644 --- a/spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb @@ -2,12 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::BackfillWorkItemTypeIdForIssues, :migration, schema: 20220326161803 do - subject(:migrate) { migration.perform(start_id, end_id, batch_table, batch_column, sub_batch_size, pause_ms, issue_type_enum[:issue], issue_type.id) } - - let(:migration) { described_class.new } - - let(:batch_table) { 'issues' } +RSpec.describe Gitlab::BackgroundMigration::BackfillWorkItemTypeIdForIssues, :migration, schema: 20220825142324 do let(:batch_column) { 'id' } let(:sub_batch_size) { 2 } let(:pause_ms) { 0 } @@ -15,7 +10,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillWorkItemTypeIdForIssues, :mi # let_it_be can't be used in migration specs because all tables but `work_item_types` are deleted after each spec let(:issue_type_enum) { { issue: 0, incident: 1, test_case: 2, requirement: 3, task: 4 } } let(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') } - let(:project) { table(:projects).create!(namespace_id: namespace.id) } + let(:project) { table(:projects).create!(namespace_id: namespace.id, project_namespace_id: namespace.id) } let(:issues_table) { table(:issues) } let(:issue_type) { table(:work_item_types).find_by!(namespace_id: nil, base_type: issue_type_enum[:issue]) } @@ -32,6 +27,21 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillWorkItemTypeIdForIssues, :mi let(:all_issues) { [issue1, issue2, issue3, incident1, test_case1, requirement1] } + let(:migration) do + described_class.new( + start_id: start_id, + end_id: end_id, + batch_table: :issues, + batch_column: :id, + sub_batch_size: sub_batch_size, + pause_ms: pause_ms, + job_arguments: [issue_type_enum[:issue], issue_type.id], + connection: ApplicationRecord.connection + ) + end + + subject(:migrate) { migration.perform } + it 'sets work_item_type_id only for the given type' do expect(all_issues).to all(have_attributes(work_item_type_id: nil)) diff --git a/spec/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy_spec.rb deleted file mode 100644 index 3cba99bfe51..00000000000 --- a/spec/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy_spec.rb +++ /dev/null @@ -1,135 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::BackfillIssueWorkItemTypeBatchingStrategy, '#next_batch', schema: 20220326161803 do # rubocop:disable Layout/LineLength - # let! can't be used in migration specs because all tables but `work_item_types` are deleted after each spec - let!(:issue_type_enum) { { issue: 0, incident: 1, test_case: 2, requirement: 3, task: 4 } } - let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') } - let!(:project) { table(:projects).create!(namespace_id: namespace.id) } - let!(:issues_table) { table(:issues) } - let!(:task_type) { table(:work_item_types).find_by!(namespace_id: nil, base_type: issue_type_enum[:task]) } - - let!(:issue1) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:issue]) } - let!(:task1) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:task]) } - let!(:issue2) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:issue]) } - let!(:issue3) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:issue]) } - let!(:task2) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:task]) } - let!(:incident1) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:incident]) } - # test_case is EE only, but enum values exist on the FOSS model - let!(:test_case1) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:test_case]) } - - let!(:task3) do - issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:task], work_item_type_id: task_type.id) - end - - let!(:task4) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:task]) } - - let!(:batching_strategy) { described_class.new(connection: ActiveRecord::Base.connection) } - - context 'when issue_type is issue' do - let(:job_arguments) { [issue_type_enum[:issue], 'irrelevant_work_item_id'] } - - context 'when starting on the first batch' do - it 'returns the bounds of the next batch' do - batch_bounds = next_batch(issue1.id, 2) - - expect(batch_bounds).to match_array([issue1.id, issue2.id]) - end - end - - context 'when additional batches remain' do - it 'returns the bounds of the next batch' do - batch_bounds = next_batch(issue2.id, 2) - - expect(batch_bounds).to match_array([issue2.id, issue3.id]) - end - end - - context 'when on the final batch' do - it 'returns the bounds of the next batch' do - batch_bounds = next_batch(issue3.id, 2) - - expect(batch_bounds).to match_array([issue3.id, issue3.id]) - end - end - - context 'when no additional batches remain' do - it 'returns nil' do - batch_bounds = next_batch(issue3.id + 1, 1) - - expect(batch_bounds).to be_nil - end - end - end - - context 'when issue_type is incident' do - let(:job_arguments) { [issue_type_enum[:incident], 'irrelevant_work_item_id'] } - - context 'when starting on the first batch' do - it 'returns the bounds of the next batch with only one element' do - batch_bounds = next_batch(incident1.id, 2) - - expect(batch_bounds).to match_array([incident1.id, incident1.id]) - end - end - end - - context 'when issue_type is requirement and there are no matching records' do - let(:job_arguments) { [issue_type_enum[:requirement], 'irrelevant_work_item_id'] } - - context 'when starting on the first batch' do - it 'returns nil' do - batch_bounds = next_batch(1, 2) - - expect(batch_bounds).to be_nil - end - end - end - - context 'when issue_type is task' do - let(:job_arguments) { [issue_type_enum[:task], 'irrelevant_work_item_id'] } - - context 'when starting on the first batch' do - it 'returns the bounds of the next batch' do - batch_bounds = next_batch(task1.id, 2) - - expect(batch_bounds).to match_array([task1.id, task2.id]) - end - end - - context 'when additional batches remain' do - it 'returns the bounds of the next batch, does not skip records where FK is already set' do - batch_bounds = next_batch(task2.id, 2) - - expect(batch_bounds).to match_array([task2.id, task3.id]) - end - end - - context 'when on the final batch' do - it 'returns the bounds of the next batch' do - batch_bounds = next_batch(task4.id, 2) - - expect(batch_bounds).to match_array([task4.id, task4.id]) - end - end - - context 'when no additional batches remain' do - it 'returns nil' do - batch_bounds = next_batch(task4.id + 1, 1) - - expect(batch_bounds).to be_nil - end - end - end - - def next_batch(min_value, batch_size) - batching_strategy.next_batch( - :issues, - :id, - batch_min_value: min_value, - batch_size: batch_size, - job_arguments: job_arguments - ) - end -end diff --git a/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb index 94e9bcf9207..7076e82ea34 100644 --- a/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb +++ b/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb @@ -2,137 +2,6 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::BackfillProjectStatisticsWithContainerRegistrySizeBatchingStrategy, '#next_batch' do # rubocop:disable Layout/LineLength - let(:batching_strategy) { described_class.new(connection: ActiveRecord::Base.connection) } - let(:namespace) { table(:namespaces) } - let(:project) { table(:projects) } - let(:container_repositories) { table(:container_repositories) } - - let!(:group) do - namespace.create!( - name: 'namespace1', type: 'Group', path: 'space1' - ) - end - - let!(:proj_namespace1) do - namespace.create!( - name: 'proj1', path: 'proj1', type: 'Project', parent_id: group.id - ) - end - - let!(:proj_namespace2) do - namespace.create!( - name: 'proj2', path: 'proj2', type: 'Project', parent_id: group.id - ) - end - - let!(:proj_namespace3) do - namespace.create!( - name: 'proj3', path: 'proj3', type: 'Project', parent_id: group.id - ) - end - - let!(:proj1) do - project.create!( - name: 'proj1', path: 'proj1', namespace_id: group.id, project_namespace_id: proj_namespace1.id - ) - end - - let!(:proj2) do - project.create!( - name: 'proj2', path: 'proj2', namespace_id: group.id, project_namespace_id: proj_namespace2.id - ) - end - - let!(:proj3) do - project.create!( - name: 'proj3', path: 'proj3', namespace_id: group.id, project_namespace_id: proj_namespace3.id - ) - end - - let!(:con1) do - container_repositories.create!( - project_id: proj1.id, - name: "ContReg_#{proj1.id}:1", - migration_state: 'import_done', - created_at: Date.new(2022, 01, 20) - ) - end - - let!(:con2) do - container_repositories.create!( - project_id: proj1.id, - name: "ContReg_#{proj1.id}:2", - migration_state: 'import_done', - created_at: Date.new(2022, 01, 20) - ) - end - - let!(:con3) do - container_repositories.create!( - project_id: proj2.id, - name: "ContReg_#{proj2.id}:1", - migration_state: 'import_done', - created_at: Date.new(2022, 01, 20) - ) - end - - let!(:con4) do - container_repositories.create!( - project_id: proj3.id, - name: "ContReg_#{proj3.id}:1", - migration_state: 'default', - created_at: Date.new(2022, 02, 20) - ) - end - - let!(:con5) do - container_repositories.create!( - project_id: proj3.id, - name: "ContReg_#{proj3.id}:2", - migration_state: 'default', - created_at: Date.new(2022, 02, 20) - ) - end - +RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::BackfillProjectStatisticsWithContainerRegistrySizeBatchingStrategy do # rubocop:disable Layout/LineLength it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy } - - context 'when starting on the first batch' do - it 'returns the bounds of the next batch' do - batch_bounds = batching_strategy.next_batch( - :container_repositories, - :project_id, - batch_min_value: con1.project_id, - batch_size: 3, - job_arguments: [] - ) - expect(batch_bounds).to eq([con1.project_id, con4.project_id]) - end - end - - context 'when additional batches remain' do - it 'returns the bounds of the next batch' do - batch_bounds = batching_strategy.next_batch( - :container_repositories, - :project_id, - batch_min_value: con3.project_id, - batch_size: 3, - job_arguments: [] - ) - - expect(batch_bounds).to eq([con3.project_id, con5.project_id]) - end - end - - context 'when no additional batches remain' do - it 'returns nil' do - batch_bounds = batching_strategy.next_batch(:container_repositories, - :project_id, - batch_min_value: con5.project_id + 1, - batch_size: 1, job_arguments: [] - ) - - expect(batch_bounds).to be_nil - end - end end diff --git a/spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb index f96c7de50f2..e4bef81e0bd 100644 --- a/spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb +++ b/spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb @@ -3,117 +3,5 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::DismissedVulnerabilitiesStrategy, '#next_batch' do - let(:batching_strategy) { described_class.new(connection: ActiveRecord::Base.connection) } - let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } - let(:users) { table(:users) } - let(:user) { create_user! } - let(:project) do - table(:projects).create!( - namespace_id: namespace.id, - project_namespace_id: namespace.id, - packages_enabled: false) - end - - let(:vulnerabilities) { table(:vulnerabilities) } - - let!(:vulnerability1) do - create_vulnerability!( - project_id: project.id, - author_id: user.id, - dismissed_at: Time.current - ) - end - - let!(:vulnerability2) do - create_vulnerability!( - project_id: project.id, - author_id: user.id, - dismissed_at: Time.current - ) - end - - let!(:vulnerability3) do - create_vulnerability!( - project_id: project.id, - author_id: user.id, - dismissed_at: Time.current - ) - end - - let!(:vulnerability4) do - create_vulnerability!( - project_id: project.id, - author_id: user.id, - dismissed_at: nil - ) - end - it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy } - - context 'when starting on the first batch' do - it 'returns the bounds of the next batch' do - batch_bounds = batching_strategy.next_batch( - :vulnerabilities, - :id, - batch_min_value: vulnerability1.id, - batch_size: 2, - job_arguments: [] - ) - expect(batch_bounds).to eq([vulnerability1.id, vulnerability2.id]) - end - end - - context 'when additional batches remain' do - it 'returns the bounds of the next batch and skips the records that do not have `dismissed_at` set' do - batch_bounds = batching_strategy.next_batch( - :vulnerabilities, - :id, - batch_min_value: vulnerability3.id, - batch_size: 2, - job_arguments: [] - ) - - expect(batch_bounds).to eq([vulnerability3.id, vulnerability3.id]) - end - end - - context 'when no additional batches remain' do - it 'returns nil' do - batch_bounds = batching_strategy.next_batch( - :vulnerabilities, - :id, - batch_min_value: vulnerability4.id + 1, - batch_size: 1, - job_arguments: [] - ) - - expect(batch_bounds).to be_nil - end - end - - private - - def create_vulnerability!( - project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0, state: 1, dismissed_at: nil - ) - vulnerabilities.create!( - project_id: project_id, - author_id: author_id, - title: title, - severity: severity, - confidence: confidence, - report_type: report_type, - state: state, - dismissed_at: dismissed_at - ) - end - - def create_user!(name: "Example User", email: "user@example.com", user_type: nil) - users.create!( - name: name, - email: email, - username: name, - projects_limit: 10 - ) - end end diff --git a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb index 9fdd7bf8adc..37fdd209622 100644 --- a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb +++ b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb @@ -60,26 +60,21 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi expect(batch_bounds).to eq([namespace4.id, namespace4.id]) end - end - - context 'additional filters' do - let(:strategy_with_filters) do - Class.new(described_class) do - def apply_additional_filters(relation, job_arguments:, job_class: nil) - min_id = job_arguments.first - relation.where.not(type: 'Project').where('id >= ?', min_id) + context 'when scope has a join which makes the column name ambiguous' do + let(:job_class) do + Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do + scope_to ->(r) { r.joins('LEFT JOIN users ON users.id = namespaces.owner_id') } end end - end - let(:batching_strategy) { strategy_with_filters.new(connection: ActiveRecord::Base.connection) } - let!(:namespace5) { namespaces.create!(name: 'batchtest5', path: 'batch-test5', type: 'Project') } + it 'executes the correct query' do + expect(job_class).to receive(:generic_instance).and_call_original - it 'applies additional filters' do - batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id, batch_size: 3, job_arguments: [1]) + batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id, batch_size: 3, job_arguments: [], job_class: job_class) - expect(batch_bounds).to eq([namespace4.id, namespace4.id]) + expect(batch_bounds).to eq([namespace4.id, namespace4.id]) + end end end end diff --git a/spec/lib/gitlab/background_migration/batching_strategies/remove_backfilled_job_artifacts_expire_at_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/remove_backfilled_job_artifacts_expire_at_batching_strategy_spec.rb new file mode 100644 index 00000000000..e296a46ea2f --- /dev/null +++ b/spec/lib/gitlab/background_migration/batching_strategies/remove_backfilled_job_artifacts_expire_at_batching_strategy_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::RemoveBackfilledJobArtifactsExpireAtBatchingStrategy do # rubocop:disable Layout/LineLength + it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy } +end diff --git a/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb b/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb new file mode 100644 index 00000000000..76a9ea82c76 --- /dev/null +++ b/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidGroupMembers, :migration, schema: 20220809002011 do + # rubocop: disable Layout/LineLength + # rubocop: disable RSpec/ScatteredLet + let!(:migration_attrs) do + { + start_id: 1, + end_id: 1000, + batch_table: :members, + batch_column: :id, + sub_batch_size: 100, + pause_ms: 0, + connection: ApplicationRecord.connection + } + end + + let!(:migration) { described_class.new(**migration_attrs) } + + subject(:perform_migration) { migration.perform } + + let(:users_table) { table(:users) } + let(:namespaces_table) { table(:namespaces) } + let(:members_table) { table(:members) } + let(:projects_table) { table(:projects) } + + let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) } + let(:user2) { users_table.create!(name: 'user2', email: 'user2@example.com', projects_limit: 5) } + let(:user3) { users_table.create!(name: 'user3', email: 'user3@example.com', projects_limit: 5) } + let(:user4) { users_table.create!(name: 'user4', email: 'user4@example.com', projects_limit: 5) } + let(:user5) { users_table.create!(name: 'user5', email: 'user5@example.com', projects_limit: 5) } + let(:user6) { users_table.create!(name: 'user6', email: 'user6@example.com', projects_limit: 5) } + + let!(:group1) { namespaces_table.create!(name: 'marvellous group 1', path: 'group-path-1', type: 'Group') } + + let!(:group2) { namespaces_table.create!(name: 'outstanding group 2', path: 'group-path-2', type: 'Group') } + + # create group member records, a mix of both valid and invalid + # project members will have already been filtered out. + let!(:group_member1) { create_invalid_group_member(id: 1, user_id: user1.id) } + + let!(:group_member4) { create_valid_group_member(id: 4, user_id: user2.id, group_id: group1.id) } + + let!(:group_member5) { create_valid_group_member(id: 5, user_id: user3.id, group_id: group2.id) } + + let!(:group_member6) { create_invalid_group_member(id: 6, user_id: user4.id) } + + let!(:group_member7) { create_valid_group_member(id: 7, user_id: user5.id, group_id: group1.id) } + + let!(:group_member8) { create_invalid_group_member(id: 8, user_id: user6.id) } + + it 'removes invalid memberships but keeps valid ones', :aggregate_failures do + expect(members_table.where(type: 'GroupMember').count).to eq 6 + + queries = ActiveRecord::QueryRecorder.new do + perform_migration + end + + expect(queries.count).to eq(4) + expect(members_table.where(type: 'GroupMember').pluck(:id)).to match_array([group_member4, group_member5, group_member7].map(&:id)) + end + + it 'tracks timings of queries' do + expect(migration.batch_metrics.timings).to be_empty + + expect { perform_migration }.to change { migration.batch_metrics.timings } + end + + it 'logs IDs of deleted records' do + expect(Gitlab::AppLogger).to receive(:info).with({ message: 'Removing invalid group member records', + deleted_count: 3, ids: [group_member1, group_member6, group_member8].map(&:id) }) + + perform_migration + end + + def create_invalid_group_member(id:, user_id:) + members_table.create!(id: id, user_id: user_id, source_id: non_existing_record_id, access_level: Gitlab::Access::MAINTAINER, + type: "GroupMember", source_type: "Namespace", notification_level: 3, member_namespace_id: nil) + end + + def create_valid_group_member(id:, user_id:, group_id:) + members_table.create!(id: id, user_id: user_id, source_id: group_id, access_level: Gitlab::Access::MAINTAINER, + type: "GroupMember", source_type: "Namespace", member_namespace_id: group_id, notification_level: 3) + end + # rubocop: enable Layout/LineLength + # rubocop: enable RSpec/ScatteredLet +end diff --git a/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb b/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb new file mode 100644 index 00000000000..029a6adf831 --- /dev/null +++ b/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidProjectMembers, :migration, schema: 20220901035725 do + # rubocop: disable Layout/LineLength + # rubocop: disable RSpec/ScatteredLet + let!(:migration_attrs) do + { + start_id: 1, + end_id: 1000, + batch_table: :members, + batch_column: :id, + sub_batch_size: 100, + pause_ms: 0, + connection: ApplicationRecord.connection + } + end + + let!(:migration) { described_class.new(**migration_attrs) } + + subject(:perform_migration) { migration.perform } + + let(:users_table) { table(:users) } + let(:namespaces_table) { table(:namespaces) } + let(:members_table) { table(:members) } + let(:projects_table) { table(:projects) } + + let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) } + let(:user2) { users_table.create!(name: 'user2', email: 'user2@example.com', projects_limit: 5) } + let(:user3) { users_table.create!(name: 'user3', email: 'user3@example.com', projects_limit: 5) } + let(:user4) { users_table.create!(name: 'user4', email: 'user4@example.com', projects_limit: 5) } + let(:user5) { users_table.create!(name: 'user5', email: 'user5@example.com', projects_limit: 5) } + let(:user6) { users_table.create!(name: 'user6', email: 'user6@example.com', projects_limit: 5) } + + let!(:group1) { namespaces_table.create!(name: 'marvellous group 1', path: 'group-path-1', type: 'Group') } + + let!(:project_namespace1) do + namespaces_table.create!(name: 'fabulous project', path: 'project-path-1', type: 'ProjectNamespace', + parent_id: group1.id) + end + + let!(:project1) do + projects_table.create!(name: 'fabulous project', path: 'project-path-1', project_namespace_id: project_namespace1.id, + namespace_id: group1.id) + end + + let!(:project_namespace2) do + namespaces_table.create!(name: 'splendiferous project', path: 'project-path-2', type: 'ProjectNamespace', + parent_id: group1.id) + end + + let!(:project2) do + projects_table.create!(name: 'splendiferous project', path: 'project-path-2', project_namespace_id: project_namespace2.id, + namespace_id: group1.id) + end + + # create project member records, a mix of both valid and invalid + # group members will have already been filtered out. + let!(:project_member1) { create_invalid_project_member(id: 1, user_id: user1.id) } + let!(:project_member2) { create_valid_project_member(id: 4, user_id: user2.id, project: project1) } + let!(:project_member3) { create_valid_project_member(id: 5, user_id: user3.id, project: project2) } + let!(:project_member4) { create_invalid_project_member(id: 6, user_id: user4.id) } + let!(:project_member5) { create_valid_project_member(id: 7, user_id: user5.id, project: project2) } + let!(:project_member6) { create_invalid_project_member(id: 8, user_id: user6.id) } + + it 'removes invalid memberships but keeps valid ones', :aggregate_failures do + expect(members_table.where(type: 'ProjectMember').count).to eq 6 + + queries = ActiveRecord::QueryRecorder.new do + perform_migration + end + + expect(queries.count).to eq(4) + expect(members_table.where(type: 'ProjectMember')).to match_array([project_member2, project_member3, project_member5]) + end + + it 'tracks timings of queries' do + expect(migration.batch_metrics.timings).to be_empty + + expect { perform_migration }.to change { migration.batch_metrics.timings } + end + + it 'logs IDs of deleted records' do + expect(Gitlab::AppLogger).to receive(:info).with({ message: 'Removing invalid project member records', + deleted_count: 3, ids: [project_member1, project_member4, project_member6].map(&:id) }) + + perform_migration + end + + def create_invalid_project_member(id:, user_id:) + members_table.create!(id: id, user_id: user_id, source_id: non_existing_record_id, access_level: Gitlab::Access::MAINTAINER, + type: "ProjectMember", source_type: "Project", notification_level: 3, member_namespace_id: nil) + end + + def create_valid_project_member(id:, user_id:, project:) + members_table.create!(id: id, user_id: user_id, source_id: project.id, access_level: Gitlab::Access::MAINTAINER, + type: "ProjectMember", source_type: "Project", member_namespace_id: project.project_namespace_id, notification_level: 3) + end + # rubocop: enable Layout/LineLength + # rubocop: enable RSpec/ScatteredLet +end diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb new file mode 100644 index 00000000000..7edba8cf524 --- /dev/null +++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenceForRecentPublicProjects, :migration do + let(:namespaces_table) { table(:namespaces) } + let(:namespace_1) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-1') } + let(:project_namespace_2) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') } + let(:project_namespace_3) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-3', type: 'Project') } + let(:project_namespace_4) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-4', type: 'Project') } + let(:project_namespace_5) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-5', type: 'Project') } + let(:project_namespace_6) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-6', type: 'Project') } + let(:project_namespace_7) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-7', type: 'Project') } + let(:project_namespace_8) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-8', type: 'Project') } + + let(:project_1) do + projects_table + .create!( + name: 'proj-1', path: 'path-1', namespace_id: namespace_1.id, + project_namespace_id: project_namespace_2.id, visibility_level: 0 + ) + end + + let(:project_2) do + projects_table + .create!( + name: 'proj-2', path: 'path-2', namespace_id: namespace_1.id, + project_namespace_id: project_namespace_3.id, visibility_level: 10, created_at: '2022-02-22' + ) + end + + let(:project_3) do + projects_table + .create!( + name: 'proj-3', path: 'path-3', namespace_id: namespace_1.id, + project_namespace_id: project_namespace_4.id, visibility_level: 20, created_at: '2022-02-17 09:00:01' + ) + end + + let(:project_4) do + projects_table + .create!( + name: 'proj-4', path: 'path-4', namespace_id: namespace_1.id, + project_namespace_id: project_namespace_5.id, visibility_level: 20, created_at: '2022-02-01' + ) + end + + let(:project_5) do + projects_table + .create!( + name: 'proj-5', path: 'path-5', namespace_id: namespace_1.id, + project_namespace_id: project_namespace_6.id, visibility_level: 20, created_at: '2022-01-04' + ) + end + + let(:project_6) do + projects_table + .create!( + name: 'proj-6', path: 'path-6', namespace_id: namespace_1.id, + project_namespace_id: project_namespace_7.id, visibility_level: 20, created_at: '2022-02-17 08:59:59' + ) + end + + let(:project_7) do + projects_table + .create!( + name: 'proj-7', path: 'path-7', namespace_id: namespace_1.id, + project_namespace_id: project_namespace_8.id, visibility_level: 20, created_at: '2022-02-17 09:00:00' + ) + end + + let(:projects_table) { table(:projects) } + let(:project_settings_table) { table(:project_settings) } + + subject(:perform_migration) do + described_class.new(start_id: projects_table.minimum(:id), + end_id: projects_table.maximum(:id), + batch_table: :projects, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 0, + connection: ActiveRecord::Base.connection) + .perform + end + + before do + project_settings_table.create!(project_id: project_1.id, legacy_open_source_license_available: true) + project_settings_table.create!(project_id: project_2.id, legacy_open_source_license_available: true) + project_settings_table.create!(project_id: project_3.id, legacy_open_source_license_available: true) + project_settings_table.create!(project_id: project_4.id, legacy_open_source_license_available: true) + project_settings_table.create!(project_id: project_5.id, legacy_open_source_license_available: false) + project_settings_table.create!(project_id: project_6.id, legacy_open_source_license_available: true) + project_settings_table.create!(project_id: project_7.id, legacy_open_source_license_available: true) + end + + it 'sets `legacy_open_source_license_available` attribute to false for public projects created after threshold time', + :aggregate_failures do + record = ActiveRecord::QueryRecorder.new do + expect { perform_migration } + .to not_change { migrated_attribute(project_1.id) }.from(true) + .and not_change { migrated_attribute(project_2.id) }.from(true) + .and change { migrated_attribute(project_3.id) }.from(true).to(false) + .and not_change { migrated_attribute(project_4.id) }.from(true) + .and not_change { migrated_attribute(project_5.id) }.from(false) + .and not_change { migrated_attribute(project_6.id) }.from(true) + .and change { migrated_attribute(project_7.id) }.from(true).to(false) + end + expect(record.count).to eq(19) + end + + def migrated_attribute(project_id) + project_settings_table.find(project_id).legacy_open_source_license_available + end +end diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb new file mode 100644 index 00000000000..205350f9df4 --- /dev/null +++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForProjectsLessThanOneMb, + :migration, + schema: 20220906074449 do + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:project_settings_table) { table(:project_settings) } + let(:project_statistics_table) { table(:project_statistics) } + + subject(:perform_migration) do + described_class.new(start_id: project_settings_table.minimum(:project_id), + end_id: project_settings_table.maximum(:project_id), + batch_table: :project_settings, + batch_column: :project_id, + sub_batch_size: 2, + pause_ms: 0, + connection: ActiveRecord::Base.connection) + .perform + end + + it 'sets `legacy_open_source_license_available` to false only for projects less than 1 MB', + :aggregate_failures do + project_setting_1_mb = create_legacy_license_project_setting(repo_size: 1) + project_setting_2_mb = create_legacy_license_project_setting(repo_size: 2) + project_setting_quarter_mb = create_legacy_license_project_setting(repo_size: 0.25) + project_setting_half_mb = create_legacy_license_project_setting(repo_size: 0.5) + + queries = ActiveRecord::QueryRecorder.new { perform_migration } + + expect(queries.count).to eq(7) + expect(migrated_attribute(project_setting_1_mb)).to be_truthy + expect(migrated_attribute(project_setting_2_mb)).to be_truthy + expect(migrated_attribute(project_setting_quarter_mb)).to be_falsey + expect(migrated_attribute(project_setting_half_mb)).to be_falsey + end + + private + + # @param repo_size: Repo size in MB + def create_legacy_license_project_setting(repo_size:) + path = "path-for-repo-size-#{repo_size}" + namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}") + project_namespace = + namespaces_table.create!(name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project') + project = projects_table + .create!(name: path, path: path, namespace_id: namespace.id, project_namespace_id: project_namespace.id) + + size_in_bytes = 1.megabyte * repo_size + project_statistics_table.create!(project_id: project.id, namespace_id: namespace.id, repository_size: size_in_bytes) + project_settings_table.create!(project_id: project.id, legacy_open_source_license_available: true) + end + + def migrated_attribute(project_setting) + project_settings_table.find(project_setting.project_id).legacy_open_source_license_available + end +end diff --git a/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb b/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb index 38e8b159e63..c788b701d79 100644 --- a/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb +++ b/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb @@ -40,10 +40,10 @@ RSpec.describe Gitlab::BackgroundMigration::EncryptIntegrationProperties, schema expect(integrations.count).to eq(4) expect(props).to match( - no_properties.id => both(be_nil), + no_properties.id => both(be_nil), with_plaintext_1.id => both(eq some_props(1)), with_plaintext_2.id => both(eq some_props(2)), - with_encrypted.id => match([be_nil, eq(some_props(3))]) + with_encrypted.id => match([be_nil, eq(some_props(3))]) ) end diff --git a/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb new file mode 100644 index 00000000000..41266cb24da --- /dev/null +++ b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::RemoveBackfilledJobArtifactsExpireAt do + it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchedMigrationJob } + + describe '#perform' do + let(:job_artifact) { table(:ci_job_artifacts, database: :ci) } + + let(:test_worker) do + described_class.new( + start_id: 1, + end_id: 100, + batch_table: :ci_job_artifacts, + batch_column: :id, + sub_batch_size: 10, + pause_ms: 0, + connection: Ci::ApplicationRecord.connection + ) + end + + let_it_be(:namespace) { table(:namespaces).create!(id: 1, name: 'user', path: 'user') } + let_it_be(:project) do + table(:projects).create!( + id: 1, + name: 'gitlab1', + path: 'gitlab1', + project_namespace_id: 1, + namespace_id: namespace.id + ) + end + + subject { test_worker.perform } + + context 'with artifacts that has backfilled expire_at' do + let!(:created_on_00_30_45_minutes_on_21_22_23) do + create_job_artifact(id: 1, file_type: 1, expire_at: Time.zone.parse('2022-01-21 00:00:00.000')) + create_job_artifact(id: 2, file_type: 1, expire_at: Time.zone.parse('2022-01-21 01:30:00.000')) + create_job_artifact(id: 3, file_type: 1, expire_at: Time.zone.parse('2022-01-22 12:00:00.000')) + create_job_artifact(id: 4, file_type: 1, expire_at: Time.zone.parse('2022-01-22 12:30:00.000')) + create_job_artifact(id: 5, file_type: 1, expire_at: Time.zone.parse('2022-01-23 23:00:00.000')) + create_job_artifact(id: 6, file_type: 1, expire_at: Time.zone.parse('2022-01-23 23:30:00.000')) + create_job_artifact(id: 7, file_type: 1, expire_at: Time.zone.parse('2022-01-23 06:45:00.000')) + end + + let!(:created_close_to_00_or_30_minutes) do + create_job_artifact(id: 8, file_type: 1, expire_at: Time.zone.parse('2022-01-21 00:00:00.001')) + create_job_artifact(id: 9, file_type: 1, expire_at: Time.zone.parse('2022-01-21 00:30:00.999')) + end + + let!(:created_on_00_or_30_minutes_on_other_dates) do + create_job_artifact(id: 10, file_type: 1, expire_at: Time.zone.parse('2022-01-01 00:00:00.000')) + create_job_artifact(id: 11, file_type: 1, expire_at: Time.zone.parse('2022-01-19 12:00:00.000')) + create_job_artifact(id: 12, file_type: 1, expire_at: Time.zone.parse('2022-01-24 23:30:00.000')) + end + + let!(:created_at_other_times) do + create_job_artifact(id: 13, file_type: 1, expire_at: Time.zone.parse('2022-01-19 00:00:00.000')) + create_job_artifact(id: 14, file_type: 1, expire_at: Time.zone.parse('2022-01-19 00:30:00.000')) + create_job_artifact(id: 15, file_type: 1, expire_at: Time.zone.parse('2022-01-24 00:00:00.000')) + create_job_artifact(id: 16, file_type: 1, expire_at: Time.zone.parse('2022-01-24 00:30:00.000')) + end + + it 'removes expire_at on job artifacts that have expire_at on 00, 30 or 45 minute of 21, 22, 23 of the month' do + expect { subject }.to change { job_artifact.where(expire_at: nil).count }.from(0).to(7) + end + + it 'keeps expire_at on other job artifacts' do + expect { subject }.to change { job_artifact.where.not(expire_at: nil).count }.from(16).to(9) + end + end + + context 'with trace artifacts that has backfilled expire_at' do + let!(:trace_artifacts) do + create_job_artifact(id: 1, file_type: 3, expire_at: Time.zone.parse('2022-01-01 00:00:00.000')) + create_job_artifact(id: 2, file_type: 3, expire_at: Time.zone.parse('2022-01-21 00:00:00.000')) + end + + it 'removes expire_at on trace job artifacts' do + expect { subject }.to change { job_artifact.where(expire_at: nil).count }.from(0).to(2) + end + end + + private + + def create_job_artifact(id:, file_type:, expire_at:) + job = table(:ci_builds, database: :ci).create!(id: id) + job_artifact.create!(id: id, job_id: job.id, expire_at: expire_at, project_id: project.id, file_type: file_type) + end + end +end diff --git a/spec/lib/gitlab/background_migration/remove_self_managed_wiki_notes_spec.rb b/spec/lib/gitlab/background_migration/remove_self_managed_wiki_notes_spec.rb new file mode 100644 index 00000000000..81927100562 --- /dev/null +++ b/spec/lib/gitlab/background_migration/remove_self_managed_wiki_notes_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::RemoveSelfManagedWikiNotes, :migration, schema: 20220601110011 do + let(:notes) { table(:notes) } + + subject(:perform_migration) do + described_class.new(start_id: 1, + end_id: 30, + batch_table: :notes, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 0, + connection: ActiveRecord::Base.connection) + .perform + end + + it 'removes all wiki notes' do + notes.create!(id: 2, note: 'Commit note', noteable_type: 'Commit') + notes.create!(id: 10, note: 'Issue note', noteable_type: 'Issue') + notes.create!(id: 20, note: 'Wiki note', noteable_type: 'Wiki') + notes.create!(id: 30, note: 'MergeRequest note', noteable_type: 'MergeRequest') + + expect(notes.where(noteable_type: 'Wiki').size).to eq(1) + + expect { perform_migration }.to change(notes, :count).by(-1) + + expect(notes.where(noteable_type: 'Wiki').size).to eq(0) + end +end diff --git a/spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb b/spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb new file mode 100644 index 00000000000..45932defaf9 --- /dev/null +++ b/spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::RenameTaskSystemNoteToChecklistItem do + let(:notes) { table(:notes) } + let(:projects) { table(:projects) } + + let(:namespace) { table(:namespaces).create!(name: 'batchtest1', type: 'Group', path: 'space1') } + let(:project) { table(:projects).create!(name: 'proj1', path: 'proj1', namespace_id: namespace.id) } + let(:issue) { table(:issues).create!(title: 'Test issue') } + + let!(:note1) do + notes.create!( + note: 'marked the task **Task 1** as complete', noteable_type: 'Issue', noteable_id: issue.id, system: true + ) + end + + let!(:note2) do + notes.create!( + note: 'NO_MATCH marked the task **Task 2** as complete', + noteable_type: 'Issue', + noteable_id: issue.id, + system: true + ) + end + + let!(:note3) do + notes.create!( + note: 'marked the task **Task 3** as incomplete', + noteable_type: 'Issue', + noteable_id: issue.id, + system: true + ) + end + + let!(:note4) do + notes.create!( + note: 'marked the task **Task 4** as incomplete', + noteable_type: 'Issue', + noteable_id: issue.id, + system: true + ) + end + + let!(:metadata1) { table(:system_note_metadata).create!(note_id: note1.id, action: :task) } + let!(:metadata2) { table(:system_note_metadata).create!(note_id: note2.id, action: :task) } + let!(:metadata3) { table(:system_note_metadata).create!(note_id: note3.id, action: :task) } + let!(:metadata4) { table(:system_note_metadata).create!(note_id: note4.id, action: :not_task) } + + let(:migration) do + described_class.new( + start_id: metadata1.id, + end_id: metadata4.id, + batch_table: :system_note_metadata, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 2, + connection: ApplicationRecord.connection + ) + end + + subject(:perform_migration) { migration.perform } + + it 'renames task to checklist item in task system notes that match', :aggregate_failures do + expect do + perform_migration + + note1.reload + note2.reload + note3.reload + note4.reload + end.to change(note1, :note).to('marked the checklist item **Task 1** as complete').and( + not_change(note2, :note).from('NO_MATCH marked the task **Task 2** as complete') + ).and( + change(note3, :note).to('marked the checklist item **Task 3** as incomplete') + ).and( + not_change(note4, :note).from('marked the task **Task 4** as incomplete') + ) + end + + it 'updates in batches' do + expect { perform_migration }.to make_queries_matching(/UPDATE notes/, 2) + end + + it 'tracks timings of queries' do + expect(migration.batch_metrics.timings).to be_empty + + expect { perform_migration }.to change { migration.batch_metrics.timings } + end +end diff --git a/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb b/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb index d5b98e49a31..2372ce21c4c 100644 --- a/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb +++ b/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb @@ -34,7 +34,7 @@ RSpec.describe Gitlab::BackgroundMigration::SetCorrectVulnerabilityState do let(:detected_state) { 1 } let(:dismissed_state) { 2 } - subject(:perform_migration) do + let(:migration_job) do described_class.new(start_id: vulnerability_with_dismissed_at.id, end_id: vulnerability_without_dismissed_at.id, batch_table: :vulnerabilities, @@ -42,15 +42,24 @@ RSpec.describe Gitlab::BackgroundMigration::SetCorrectVulnerabilityState do sub_batch_size: 1, pause_ms: 0, connection: ActiveRecord::Base.connection) - .perform end - it 'changes vulnerability state to `dismissed` when dismissed_at is not nil' do - expect { perform_migration }.to change { vulnerability_with_dismissed_at.reload.state }.to(dismissed_state) + describe '#filter_batch' do + it 'filters out vulnerabilities where dismissed_at is null' do + expect(migration_job.filter_batch(vulnerabilities)).to contain_exactly(vulnerability_with_dismissed_at) + end end - it 'does not change the state when dismissed_at is nil' do - expect { perform_migration }.not_to change { vulnerability_without_dismissed_at.reload.state } + describe '#perform' do + subject(:perform_migration) { migration_job.perform } + + it 'changes vulnerability state to `dismissed` when dismissed_at is not nil' do + expect { perform_migration }.to change { vulnerability_with_dismissed_at.reload.state }.to(dismissed_state) + end + + it 'does not change the state when dismissed_at is nil' do + expect { perform_migration }.not_to change { vulnerability_without_dismissed_at.reload.state } + end end private diff --git a/spec/lib/gitlab/bullet/exclusions_spec.rb b/spec/lib/gitlab/bullet/exclusions_spec.rb index ba42156b0c4..325b0167f58 100644 --- a/spec/lib/gitlab/bullet/exclusions_spec.rb +++ b/spec/lib/gitlab/bullet/exclusions_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'fast_spec_helper' +require 'tempfile' RSpec.describe Gitlab::Bullet::Exclusions do let(:config_file) do diff --git a/spec/lib/gitlab/cache/helpers_spec.rb b/spec/lib/gitlab/cache/helpers_spec.rb index 08e0d7729bd..39d37e979b4 100644 --- a/spec/lib/gitlab/cache/helpers_spec.rb +++ b/spec/lib/gitlab/cache/helpers_spec.rb @@ -18,10 +18,7 @@ RSpec.describe Gitlab::Cache::Helpers, :use_clean_rails_redis_caching do end describe "#render_cached" do - subject do - instance.render_cached(presentable, **kwargs) - end - + let(:method) { :render_cached } let(:kwargs) do { with: presenter, @@ -29,6 +26,10 @@ RSpec.describe Gitlab::Cache::Helpers, :use_clean_rails_redis_caching do } end + subject do + instance.public_send(method, presentable, **kwargs) + end + context 'single object' do let_it_be(:presentable) { create(:merge_request, source_project: project, source_branch: 'wip') } diff --git a/spec/lib/gitlab/changes_list_spec.rb b/spec/lib/gitlab/changes_list_spec.rb index 8292764f561..762a121340e 100644 --- a/spec/lib/gitlab/changes_list_spec.rb +++ b/spec/lib/gitlab/changes_list_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "spec_helper" +require 'fast_spec_helper' RSpec.describe Gitlab::ChangesList do let(:valid_changes_string) { "\n000000 570e7b2 refs/heads/my_branch\nd14d6c 6fd24d refs/heads/master" } diff --git a/spec/lib/gitlab/chat/responder/base_spec.rb b/spec/lib/gitlab/chat/responder/base_spec.rb index 667228cbab4..2a253449678 100644 --- a/spec/lib/gitlab/chat/responder/base_spec.rb +++ b/spec/lib/gitlab/chat/responder/base_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Chat::Responder::Base do let(:project) { double(:project) } diff --git a/spec/lib/gitlab/ci/ansi2json/parser_spec.rb b/spec/lib/gitlab/ci/ansi2json/parser_spec.rb index cf93ebe0721..b11002e8e93 100644 --- a/spec/lib/gitlab/ci/ansi2json/parser_spec.rb +++ b/spec/lib/gitlab/ci/ansi2json/parser_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' # The rest of the specs for this class are covered in style_spec.rb RSpec.describe Gitlab::Ci::Ansi2json::Parser do diff --git a/spec/lib/gitlab/ci/ansi2json/result_spec.rb b/spec/lib/gitlab/ci/ansi2json/result_spec.rb index b7b4d6de8b9..14e2a9625fe 100644 --- a/spec/lib/gitlab/ci/ansi2json/result_spec.rb +++ b/spec/lib/gitlab/ci/ansi2json/result_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Ansi2json::Result do let(:stream) { StringIO.new('hello') } diff --git a/spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb b/spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb deleted file mode 100644 index 2c236ba3726..00000000000 --- a/spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Ci::Build::Artifacts::Adapters::ZipStream do - let(:file_name) { 'single_file.zip' } - let(:fixture_path) { "lib/gitlab/ci/build/artifacts/adapters/zip_stream/#{file_name}" } - let(:stream) { File.open(expand_fixture_path(fixture_path), 'rb') } - - describe '#initialize' do - it 'initializes when stream is passed' do - expect { described_class.new(stream) }.not_to raise_error - end - - context 'when stream is not passed' do - let(:stream) { nil } - - it 'raises an error' do - expect { described_class.new(stream) }.to raise_error(described_class::InvalidStreamError) - end - end - end - - describe '#each_blob' do - let(:adapter) { described_class.new(stream) } - - context 'when stream is a zip file' do - it 'iterates file content when zip file contains one file' do - expect { |b| adapter.each_blob(&b) } - .to yield_with_args("file 1 content\n") - end - - context 'when zip file contains multiple files' do - let(:file_name) { 'multiple_files.zip' } - - it 'iterates content of all files' do - expect { |b| adapter.each_blob(&b) } - .to yield_successive_args("file 1 content\n", "file 2 content\n") - end - end - - context 'when zip file includes files in a directory' do - let(:file_name) { 'with_directory.zip' } - - it 'iterates contents from files only' do - expect { |b| adapter.each_blob(&b) } - .to yield_successive_args("file 1 content\n", "file 2 content\n") - end - end - - context 'when zip contains a file which decompresses beyond the size limit' do - let(:file_name) { '200_mb_decompressed.zip' } - - it 'does not read the file' do - expect { |b| adapter.each_blob(&b) }.not_to yield_control - end - end - - context 'when the zip contains too many files' do - let(:file_name) { '100_files.zip' } - - it 'stops processing when the limit is reached' do - expect { |b| adapter.each_blob(&b) } - .to yield_control.exactly(described_class::MAX_FILES_PROCESSED).times - end - end - - context 'when stream is a zipbomb' do - let(:file_name) { 'zipbomb.zip' } - - it 'does not read the file' do - expect { |b| adapter.each_blob(&b) }.not_to yield_control - end - end - end - - context 'when stream is not a zip file' do - let(:stream) { File.open(expand_fixture_path('junit/junit.xml.gz'), 'rb') } - - it 'does not yield any data' do - expect { |b| adapter.each_blob(&b) }.not_to yield_control - expect { adapter.each_blob { |b| b } }.not_to raise_error - end - end - end -end diff --git a/spec/lib/gitlab/ci/build/artifacts/path_spec.rb b/spec/lib/gitlab/ci/build/artifacts/path_spec.rb index 27b7dac2ae4..773eaf4b3fc 100644 --- a/spec/lib/gitlab/ci/build/artifacts/path_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/path_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Build::Artifacts::Path do describe '#valid?' do diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb index 436ad59bdf7..e560f1c2b5a 100644 --- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb @@ -108,7 +108,8 @@ RSpec.describe Gitlab::Ci::Build::Policy::Variables do project: project, ref: 'master', stage: 'review', - environment: 'test/$CI_JOB_STAGE/1') + environment: 'test/$CI_JOB_STAGE/1', + ci_stage: build(:ci_stage, name: 'review', project: project, pipeline: pipeline)) end before do diff --git a/spec/lib/gitlab/ci/build/policy_spec.rb b/spec/lib/gitlab/ci/build/policy_spec.rb index b85b093fd03..4baf4a1b4c4 100644 --- a/spec/lib/gitlab/ci/build/policy_spec.rb +++ b/spec/lib/gitlab/ci/build/policy_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Build::Policy do let(:policy) { spy('policy specification') } diff --git a/spec/lib/gitlab/ci/build/port_spec.rb b/spec/lib/gitlab/ci/build/port_spec.rb index 480418e0851..51820c1ab2c 100644 --- a/spec/lib/gitlab/ci/build/port_spec.rb +++ b/spec/lib/gitlab/ci/build/port_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Build::Port do subject { described_class.new(port) } diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb index f192862c1c4..f9ebab149a5 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb @@ -3,44 +3,54 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do - shared_examples 'an exists rule with a context' do - subject { described_class.new(globs).satisfied_by?(pipeline, context) } + describe '#satisfied_by?' do + shared_examples 'an exists rule with a context' do + it_behaves_like 'a glob matching rule' do + let(:project) { create(:project, :custom_repo, files: files) } + end - it_behaves_like 'a glob matching rule' do - let(:project) { create(:project, :custom_repo, files: files) } - end + context 'after pattern comparision limit is reached' do + let(:globs) { ['*definitely_not_a_matching_glob*'] } + let(:project) { create(:project, :repository) } - context 'after pattern comparision limit is reached' do - let(:globs) { ['*definitely_not_a_matching_glob*'] } - let(:project) { create(:project, :repository) } + before do + stub_const('Gitlab::Ci::Build::Rules::Rule::Clause::Exists::MAX_PATTERN_COMPARISONS', 2) + expect(File).to receive(:fnmatch?).twice.and_call_original + end - before do - stub_const('Gitlab::Ci::Build::Rules::Rule::Clause::Exists::MAX_PATTERN_COMPARISONS', 2) - expect(File).to receive(:fnmatch?).twice.and_call_original + it { is_expected.to be_truthy } end - - it { is_expected.to be_truthy } end - end - describe '#satisfied_by?' do - let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) } + subject(:satisfied_by?) { described_class.new(globs).satisfied_by?(nil, context) } context 'when context is Build::Context::Build' do it_behaves_like 'an exists rule with a context' do - let(:context) { Gitlab::Ci::Build::Context::Build.new(pipeline, sha: 'abc1234') } + let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.commit.sha) } + let(:context) { Gitlab::Ci::Build::Context::Build.new(pipeline, sha: project.repository.commit.sha) } end end context 'when context is Build::Context::Global' do it_behaves_like 'an exists rule with a context' do + let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.commit.sha) } let(:context) { Gitlab::Ci::Build::Context::Global.new(pipeline, yaml_variables: {}) } end end context 'when context is Config::External::Context' do + let(:context) { Gitlab::Ci::Config::External::Context.new(project: project, sha: sha) } + it_behaves_like 'an exists rule with a context' do - let(:context) { Gitlab::Ci::Config::External::Context.new(project: project, sha: project.repository.tree.sha) } + let(:sha) { project.repository.commit.sha } + end + + context 'when context has no project' do + let(:globs) { ['Dockerfile'] } + let(:project) {} + let(:sha) { 'abc1234' } + + it { is_expected.to eq(false) } end end end diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index 36c26c8ee4f..3562706ff33 100644 --- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -230,12 +230,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do end end - context 'when auto_stop_in is invalid format' do - let(:auto_stop_in) { 'invalid' } + context 'when variables are used for auto_stop_in' do + let(:auto_stop_in) { '$TTL' } - it 'becomes invalid' do - expect(entry).not_to be_valid - expect(entry.errors).to include 'environment auto stop in should be a duration' + it 'becomes valid' do + expect(entry).to be_valid + expect(entry.auto_stop_in).to eq(auto_stop_in) end end end diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index 6121c28070f..b37498ba10a 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -4,8 +4,6 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Image do before do - stub_feature_flags(ci_docker_image_pull_policy: true) - entry.compose! end @@ -129,18 +127,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do it 'is valid' do expect(entry).to be_valid end - - context 'when the feature flag ci_docker_image_pull_policy is disabled' do - before do - stub_feature_flags(ci_docker_image_pull_policy: false) - entry.compose! - end - - it 'is not valid' do - expect(entry).not_to be_valid - expect(entry.errors).to include('image config contains unknown keys: pull_policy') - end - end end describe '#value' do @@ -150,19 +136,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do pull_policy: ['if-not-present'] ) end - - context 'when the feature flag ci_docker_image_pull_policy is disabled' do - before do - stub_feature_flags(ci_docker_image_pull_policy: false) - entry.compose! - end - - it 'is not valid' do - expect(entry.value).to eq( - name: 'image:1.0' - ) - end - end end end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index ca336c3ecaa..75ac2ca87ab 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -605,8 +605,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do let(:deps) do double('deps', 'default_entry' => default, - 'workflow_entry' => workflow, - 'variables_value' => nil) + 'workflow_entry' => workflow) end context 'when job config overrides default config' do diff --git a/spec/lib/gitlab/ci/config/entry/legacy_variables_spec.rb b/spec/lib/gitlab/ci/config/entry/legacy_variables_spec.rb new file mode 100644 index 00000000000..e9edec9a0a4 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/legacy_variables_spec.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::LegacyVariables do + let(:config) { {} } + let(:metadata) { {} } + + subject(:entry) { described_class.new(config, **metadata) } + + before do + entry.compose! + end + + shared_examples 'valid config' do + describe '#value' do + it 'returns hash with key value strings' do + expect(entry.value).to eq result + end + end + + describe '#errors' do + it 'does not append errors' do + expect(entry.errors).to be_empty + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + shared_examples 'invalid config' do |error_message| + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include(error_message) + end + end + end + + context 'when entry config value has key-value pairs' do + let(:config) do + { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' } + end + + let(:result) do + { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' } + end + + it_behaves_like 'valid config' + + describe '#value_with_data' do + it 'returns variable with data' do + expect(entry.value_with_data).to eq( + 'VARIABLE_1' => { value: 'value 1' }, + 'VARIABLE_2' => { value: 'value 2' } + ) + end + end + end + + context 'with numeric keys and values in the config' do + let(:config) { { 10 => 20 } } + let(:result) do + { '10' => '20' } + end + + it_behaves_like 'valid config' + end + + context 'when key is an array' do + let(:config) { { ['VAR1'] => 'val1' } } + let(:result) do + { 'VAR1' => 'val1' } + end + + it_behaves_like 'invalid config', /should be a hash of key value pairs/ + end + + context 'when value is a symbol' do + let(:config) { { 'VAR1' => :val1 } } + let(:result) do + { 'VAR1' => 'val1' } + end + + it_behaves_like 'valid config' + end + + context 'when value is a boolean' do + let(:config) { { 'VAR1' => true } } + let(:result) do + { 'VAR1' => 'val1' } + end + + it_behaves_like 'invalid config', /should be a hash of key value pairs/ + end + + context 'when entry config value has key-value pair and hash' do + let(:config) do + { 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' }, + 'VARIABLE_2' => 'value 2' } + end + + it_behaves_like 'invalid config', /should be a hash of key value pairs/ + + context 'when metadata has use_value_data: true' do + let(:metadata) { { use_value_data: true } } + + let(:result) do + { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' } + end + + it_behaves_like 'valid config' + + describe '#value_with_data' do + it 'returns variable with data' do + expect(entry.value_with_data).to eq( + 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' }, + 'VARIABLE_2' => { value: 'value 2' } + ) + end + end + end + end + + context 'when entry value is an array' do + let(:config) { [:VAR, 'test'] } + + it_behaves_like 'invalid config', /should be a hash of key value pairs/ + end + + context 'when metadata has use_value_data: true' do + let(:metadata) { { use_value_data: true } } + + context 'when entry value has hash with other key-pairs' do + let(:config) do + { 'VARIABLE_1' => { value: 'value 1', hello: 'variable 1' }, + 'VARIABLE_2' => 'value 2' } + end + + it_behaves_like 'invalid config', /should be a hash of key value pairs, value can be a hash/ + end + + context 'when entry config value has hash with nil description' do + let(:config) do + { 'VARIABLE_1' => { value: 'value 1', description: nil } } + end + + it_behaves_like 'invalid config', /should be a hash of key value pairs, value can be a hash/ + end + + context 'when entry config value has hash without description' do + let(:config) do + { 'VARIABLE_1' => { value: 'value 1' } } + end + + let(:result) do + { 'VARIABLE_1' => 'value 1' } + end + + it_behaves_like 'valid config' + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/port_spec.rb b/spec/lib/gitlab/ci/config/entry/port_spec.rb index e2840c07f6b..77f846f95f0 100644 --- a/spec/lib/gitlab/ci/config/entry/port_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/port_spec.rb @@ -48,7 +48,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Port do let(:config) do { number: 80, protocol: 'http', - name: 'foobar' } + name: 'foobar' } end describe '#valid?' do diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb index 714b0a3b6aa..5f42a8c49a7 100644 --- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb @@ -197,6 +197,34 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do end end end + + context 'when a variable has an invalid data attribute' do + let(:config) do + { + script: 'echo', + variables: { 'VAR1' => 'val 1', 'VAR2' => { value: 'val 2', description: 'hello var 2' } } + } + end + + it 'reports error about variable' do + expect(entry.errors) + .to include 'variables:var2 config must be a string' + end + + context 'when the FF ci_variables_refactoring_to_variable is disabled' do + let(:entry_without_ff) { node_class.new(config, name: :rspec) } + + before do + stub_feature_flags(ci_variables_refactoring_to_variable: false) + entry_without_ff.compose! + end + + it 'reports error about variable' do + expect(entry_without_ff.errors) + .to include /config should be a hash of key value pairs/ + end + end + end end end @@ -212,13 +240,11 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do let(:unspecified) { double('unspecified', 'specified?' => false) } let(:default) { double('default', '[]' => unspecified) } let(:workflow) { double('workflow', 'has_rules?' => false) } - let(:variables) {} let(:deps) do double('deps', default_entry: default, - workflow_entry: workflow, - variables_value: variables) + workflow_entry: workflow) end context 'with workflow rules' do diff --git a/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb b/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb index ff44a235ea5..394d91466bf 100644 --- a/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' require_dependency 'active_model' RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Matrix do diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb index 55ad119ea21..3d19987a0be 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -117,49 +117,49 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do expect(root.jobs_value.keys).to eq([:rspec, :spinach, :release]) expect(root.jobs_value[:rspec]).to eq( { name: :rspec, - script: %w[rspec ls], - before_script: %w(ls pwd), - image: { name: 'image:1.0' }, - services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], - stage: 'test', - cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], - job_variables: {}, - root_variables_inheritance: true, - ignore: false, - after_script: ['make clean'], - only: { refs: %w[branches tags] }, - scheduling_type: :stage } + script: %w[rspec ls], + before_script: %w(ls pwd), + image: { name: 'image:1.0' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], + stage: 'test', + cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], + job_variables: {}, + root_variables_inheritance: true, + ignore: false, + after_script: ['make clean'], + only: { refs: %w[branches tags] }, + scheduling_type: :stage } ) expect(root.jobs_value[:spinach]).to eq( { name: :spinach, - before_script: [], - script: %w[spinach], - image: { name: 'image:1.0' }, - services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], - stage: 'test', - cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], - job_variables: {}, - root_variables_inheritance: true, - ignore: false, - after_script: ['make clean'], - only: { refs: %w[branches tags] }, - scheduling_type: :stage } + before_script: [], + script: %w[spinach], + image: { name: 'image:1.0' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], + stage: 'test', + cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], + job_variables: {}, + root_variables_inheritance: true, + ignore: false, + after_script: ['make clean'], + only: { refs: %w[branches tags] }, + scheduling_type: :stage } ) expect(root.jobs_value[:release]).to eq( { name: :release, - stage: 'release', - before_script: [], - script: ["make changelog | tee release_changelog.txt"], - release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" }, - image: { name: "image:1.0" }, - services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }], - cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }], - only: { refs: %w(branches tags) }, - job_variables: { 'VAR' => 'job' }, - root_variables_inheritance: true, - after_script: [], - ignore: false, - scheduling_type: :stage } + stage: 'release', + before_script: [], + script: ["make changelog | tee release_changelog.txt"], + release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" }, + image: { name: "image:1.0" }, + services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }], + cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }], + only: { refs: %w(branches tags) }, + job_variables: { 'VAR' => { value: 'job' } }, + root_variables_inheritance: true, + after_script: [], + ignore: false, + scheduling_type: :stage } ) end end @@ -196,31 +196,31 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do it 'returns jobs configuration' do expect(root.jobs_value).to eq( rspec: { name: :rspec, - script: %w[rspec ls], - before_script: %w(ls pwd), - image: { name: 'image:1.0' }, - services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], - stage: 'test', - cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], - job_variables: {}, - root_variables_inheritance: true, - ignore: false, - after_script: ['make clean'], - only: { refs: %w[branches tags] }, - scheduling_type: :stage }, + script: %w[rspec ls], + before_script: %w(ls pwd), + image: { name: 'image:1.0' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], + stage: 'test', + cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], + job_variables: {}, + root_variables_inheritance: true, + ignore: false, + after_script: ['make clean'], + only: { refs: %w[branches tags] }, + scheduling_type: :stage }, spinach: { name: :spinach, - before_script: [], - script: %w[spinach], - image: { name: 'image:1.0' }, - services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], - stage: 'test', - cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], - job_variables: { 'VAR' => 'job' }, - root_variables_inheritance: true, - ignore: false, - after_script: ['make clean'], - only: { refs: %w[branches tags] }, - scheduling_type: :stage } + before_script: [], + script: %w[spinach], + image: { name: 'image:1.0' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], + stage: 'test', + cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], + job_variables: { 'VAR' => { value: 'job' } }, + root_variables_inheritance: true, + ignore: false, + after_script: ['make clean'], + only: { refs: %w[branches tags] }, + scheduling_type: :stage } ) end end @@ -350,6 +350,33 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do end end end + + context 'when a variable has an invalid data key' do + let(:hash) do + { variables: { VAR1: { invalid: 'hello' } }, rspec: { script: 'hello' } } + end + + describe '#errors' do + it 'reports errors about the invalid variable' do + expect(root.errors) + .to include /var1 config uses invalid data keys: invalid/ + end + + context 'when the FF ci_variables_refactoring_to_variable is disabled' do + let(:root_without_ff) { described_class.new(hash, user: user, project: project) } + + before do + stub_feature_flags(ci_variables_refactoring_to_variable: false) + root_without_ff.compose! + end + + it 'reports errors about the invalid variable' do + expect(root_without_ff.errors) + .to include /variables config should be a hash of key value pairs, value can be a hash/ + end + end + end + end end context 'when value is not a hash' do diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb index c85fe366da6..303d825c591 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' require 'gitlab_chronic_duration' -require_dependency 'active_model' RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do let(:factory) do @@ -363,7 +362,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do it { is_expected.not_to be_valid } it 'returns an error about invalid variables:' do - expect(subject.errors).to include(/variables config should be a hash of key value pairs/) + expect(subject.errors).to include(/variables config should be a hash/) + end + + context 'when the FF ci_variables_refactoring_to_variable is disabled' do + let(:entry_without_ff) { factory.create! } + + before do + stub_feature_flags(ci_variables_refactoring_to_variable: false) + entry_without_ff.compose! + end + + it 'returns an error about invalid variables:' do + expect(subject.errors).to include(/variables config should be a hash/) + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 821ab442d61..e36484bb0ae 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -4,7 +4,6 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Service do before do - stub_feature_flags(ci_docker_image_pull_policy: true) entry.compose! end @@ -149,18 +148,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do it 'is valid' do expect(entry).to be_valid end - - context 'when the feature flag ci_docker_image_pull_policy is disabled' do - before do - stub_feature_flags(ci_docker_image_pull_policy: false) - entry.compose! - end - - it 'is not valid' do - expect(entry).not_to be_valid - expect(entry.errors).to include('service config contains unknown keys: pull_policy') - end - end end describe '#value' do @@ -170,18 +157,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do pull_policy: ['if-not-present'] ) end - - context 'when the feature flag ci_docker_image_pull_policy is disabled' do - before do - stub_feature_flags(ci_docker_image_pull_policy: false) - end - - it 'is not valid' do - expect(entry.value).to eq( - name: 'postgresql:9.5' - ) - end - end end end end diff --git a/spec/lib/gitlab/ci/config/entry/variable_spec.rb b/spec/lib/gitlab/ci/config/entry/variable_spec.rb new file mode 100644 index 00000000000..744a89d4509 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/variable_spec.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::Variable do + let(:config) { {} } + let(:metadata) { {} } + + subject(:entry) do + described_class.new(config, **metadata).tap do |entry| + entry.key = 'VAR1' # composable_hash requires key to be set + end + end + + before do + entry.compose! + end + + describe 'SimpleVariable' do + context 'when config is a string' do + let(:config) { 'value' } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + subject(:value) { entry.value } + + it { is_expected.to eq('value') } + end + end + + context 'when config is an integer' do + let(:config) { 1 } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + subject(:value) { entry.value } + + it { is_expected.to eq('1') } + end + end + + context 'when config is an array' do + let(:config) { [] } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + subject(:errors) { entry.errors } + + it { is_expected.to include 'variable definition must be either a string or a hash' } + end + end + end + + describe 'ComplexVariable' do + context 'when config is a hash with description' do + let(:config) { { value: 'value', description: 'description' } } + + context 'when metadata allowed_value_data is not provided' do + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + subject(:errors) { entry.errors } + + it { is_expected.to include 'var1 config must be a string' } + end + end + + context 'when metadata allowed_value_data is (value, description)' do + let(:metadata) { { allowed_value_data: %i[value description] } } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + subject(:value) { entry.value } + + it { is_expected.to eq('value') } + end + + describe '#value_with_data' do + subject(:value_with_data) { entry.value_with_data } + + it { is_expected.to eq(value: 'value', description: 'description') } + end + + context 'when config value is a symbol' do + let(:config) { { value: :value, description: 'description' } } + + describe '#value' do + subject(:value) { entry.value } + + it { is_expected.to eq('value') } + end + + describe '#value_with_data' do + subject(:value_with_data) { entry.value_with_data } + + it { is_expected.to eq(value: 'value', description: 'description') } + end + end + + context 'when config value is an integer' do + let(:config) { { value: 123, description: 'description' } } + + describe '#value' do + subject(:value) { entry.value } + + it { is_expected.to eq('123') } + end + + describe '#value_with_data' do + subject(:value_with_data) { entry.value_with_data } + + it { is_expected.to eq(value: '123', description: 'description') } + end + end + + context 'when config value is an array' do + let(:config) { { value: ['value'], description: 'description' } } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + subject(:errors) { entry.errors } + + it { is_expected.to include 'var1 config value must be an alphanumeric string' } + end + end + + context 'when config description is a symbol' do + let(:config) { { value: 'value', description: :description } } + + describe '#value' do + subject(:value) { entry.value } + + it { is_expected.to eq('value') } + end + + describe '#value_with_data' do + subject(:value_with_data) { entry.value_with_data } + + it { is_expected.to eq(value: 'value', description: :description) } + end + end + end + + context 'when metadata allowed_value_data is (value, xyz)' do + let(:metadata) { { allowed_value_data: %i[value xyz] } } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + subject(:errors) { entry.errors } + + it { is_expected.to include 'var1 config uses invalid data keys: description' } + end + end + end + + context 'when config is a hash without description' do + let(:config) { { value: 'value' } } + + context 'when metadata allowed_value_data is not provided' do + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + subject(:errors) { entry.errors } + + it { is_expected.to include 'var1 config must be a string' } + end + end + + context 'when metadata allowed_value_data is (value, description)' do + let(:metadata) { { allowed_value_data: %i[value description] } } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + subject(:value) { entry.value } + + it { is_expected.to eq('value') } + end + + describe '#value_with_data' do + subject(:value_with_data) { entry.value_with_data } + + it { is_expected.to eq(value: 'value') } + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb index 78d37e228df..ad7290d0589 100644 --- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb @@ -3,41 +3,46 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Variables do + let(:config) { {} } let(:metadata) { {} } - subject { described_class.new(config, **metadata) } + subject(:entry) { described_class.new(config, **metadata) } + + before do + entry.compose! + end shared_examples 'valid config' do describe '#value' do it 'returns hash with key value strings' do - expect(subject.value).to eq result + expect(entry.value).to eq result end end describe '#errors' do it 'does not append errors' do - expect(subject.errors).to be_empty + expect(entry.errors).to be_empty end end describe '#valid?' do it 'is valid' do - expect(subject).to be_valid + expect(entry).to be_valid end end end - shared_examples 'invalid config' do + shared_examples 'invalid config' do |error_message| describe '#valid?' do it 'is not valid' do - expect(subject).not_to be_valid + expect(entry).not_to be_valid end end describe '#errors' do it 'saves errors' do - expect(subject.errors) - .to include /should be a hash of key value pairs/ + expect(entry.errors) + .to include(error_message) end end end @@ -52,6 +57,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do end it_behaves_like 'valid config' + + describe '#value_with_data' do + it 'returns variable with data' do + expect(entry.value_with_data).to eq( + 'VARIABLE_1' => { value: 'value 1' }, + 'VARIABLE_2' => { value: 'value 2' } + ) + end + end end context 'with numeric keys and values in the config' do @@ -63,33 +77,63 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do it_behaves_like 'valid config' end + context 'when key is an array' do + let(:config) { { ['VAR1'] => 'val1' } } + + it_behaves_like 'invalid config', /must be an alphanumeric string/ + end + + context 'when value is a symbol' do + let(:config) { { 'VAR1' => :val1 } } + let(:result) do + { 'VAR1' => 'val1' } + end + + it_behaves_like 'valid config' + end + + context 'when value is a boolean' do + let(:config) { { 'VAR1' => true } } + + it_behaves_like 'invalid config', /must be either a string or a hash/ + end + context 'when entry config value has key-value pair and hash' do let(:config) do { 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' }, 'VARIABLE_2' => 'value 2' } end - let(:result) do - { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' } - end + it_behaves_like 'invalid config', /variable_1 config must be a string/ - it_behaves_like 'invalid config' + context 'when metadata has allowed_value_data' do + let(:metadata) { { allowed_value_data: %i[value description] } } - context 'when metadata has use_value_data' do - let(:metadata) { { use_value_data: true } } + let(:result) do + { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' } + end it_behaves_like 'valid config' + + describe '#value_with_data' do + it 'returns variable with data' do + expect(entry.value_with_data).to eq( + 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' }, + 'VARIABLE_2' => { value: 'value 2' } + ) + end + end end end context 'when entry value is an array' do let(:config) { [:VAR, 'test'] } - it_behaves_like 'invalid config' + it_behaves_like 'invalid config', /variables config should be a hash/ end - context 'when metadata has use_value_data' do - let(:metadata) { { use_value_data: true } } + context 'when metadata has allowed_value_data' do + let(:metadata) { { allowed_value_data: %i[value description] } } context 'when entry value has hash with other key-pairs' do let(:config) do @@ -97,7 +141,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do 'VARIABLE_2' => 'value 2' } end - it_behaves_like 'invalid config' + it_behaves_like 'invalid config', /variable_1 config uses invalid data keys: hello/ end context 'when entry config value has hash with nil description' do @@ -105,7 +149,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do { 'VARIABLE_1' => { value: 'value 1', description: nil } } end - it_behaves_like 'invalid config' + it_behaves_like 'invalid config', /variable_1 config description must be an alphanumeric string/ end context 'when entry config value has hash without description' do diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb index 45dfea636f3..c22afb32756 100644 --- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb @@ -219,4 +219,43 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote do ) } end + + describe '#to_hash' do + subject(:to_hash) { remote_file.to_hash } + + before do + stub_full_request(location).to_return(body: remote_file_content) + end + + context 'with a valid remote file' do + it 'returns the content as a hash' do + expect(to_hash).to eql( + before_script: ["apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs", + "ruby -v", + "which ruby", + "bundle install --jobs $(nproc) \"${FLAGS[@]}\""] + ) + end + end + + context 'when it has `include` with rules:exists' do + let(:remote_file_content) do + <<~HEREDOC + include: + - local: another-file.yml + rules: + - exists: [Dockerfile] + HEREDOC + end + + it 'returns the content as a hash' do + expect(to_hash).to eql( + include: [ + { local: 'another-file.yml', + rules: [{ exists: ['Dockerfile'] }] } + ] + ) + end + end + end end diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index e74fdc2071b..9eaba12f388 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -210,7 +210,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do { 'local' => local_file }, { 'local' => local_file } ], - image: 'image:1.0' } + image: 'image:1.0' } end it 'does not raise an exception' do @@ -427,7 +427,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do { 'local' => 'hello/secret-file1.yml' }, { 'local' => 'hello/secret-file2.yml' } ], - image: 'ruby:2.7' } + image: 'ruby:2.7' } end it 'has expanset with two' do diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index 841a46e197d..b1dff6f9723 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -94,6 +94,36 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do end end + context 'when the remote file has `include` with rules:exists' do + let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } + let(:values) { { include: remote_file, image: 'image:1.0' } } + let(:external_file_content) do + <<-HEREDOC + include: + - local: another-file.yml + rules: + - exists: [Dockerfile] + + rspec: + script: + - bundle exec rspec + HEREDOC + end + + before do + stub_full_request(remote_file).to_return(body: external_file_content) + end + + it 'evaluates the rule as false' do + output = processor.perform + expect(output.keys).to match_array([:image, :rspec]) + end + + it "removes the 'include' keyword" do + expect(processor.perform[:include]).to be_nil + end + end + context 'with a valid local external file is defined' do let(:values) { { include: '/lib/gitlab/ci/templates/template.yml', image: 'image:1.0' } } let(:local_file_content) do diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index 7e0b2b5aa8e..3d46d266c13 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -341,9 +341,9 @@ RSpec.describe Gitlab::Ci::Lint do let(:counters) do { 'count' => a_kind_of(Numeric), - 'avg' => a_kind_of(Numeric), - 'max' => a_kind_of(Numeric), - 'min' => a_kind_of(Numeric) + 'avg' => a_kind_of(Numeric), + 'max' => a_kind_of(Numeric), + 'min' => a_kind_of(Numeric) } end diff --git a/spec/lib/gitlab/ci/mask_secret_spec.rb b/spec/lib/gitlab/ci/mask_secret_spec.rb index 7d950c86700..ffe36e69a8f 100644 --- a/spec/lib/gitlab/ci/mask_secret_spec.rb +++ b/spec/lib/gitlab/ci/mask_secret_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::MaskSecret do subject { described_class } diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb index c99cfa94aa6..38b229e0dd8 100644 --- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb +++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties do subject(:parse_source) { described_class.parse_source(properties) } diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb index 431fe9f3591..f3636106b98 100644 --- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb +++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb @@ -102,11 +102,11 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx do it 'adds each component, ignoring unused attributes' do expect(report).to receive(:add_component) - .with({ "name" => "activesupport", "version" => "5.1.4", "type" => "library" }) + .with(an_object_having_attributes(name: "activesupport", version: "5.1.4", component_type: "library")) expect(report).to receive(:add_component) - .with({ "name" => "byebug", "version" => "10.0.0", "type" => "library" }) + .with(an_object_having_attributes(name: "byebug", version: "10.0.0", component_type: "library")) expect(report).to receive(:add_component) - .with({ "name" => "minimal-component", "type" => "library" }) + .with(an_object_having_attributes(name: "minimal-component", version: nil, component_type: "library")) parse! end diff --git a/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb index 30114b17cac..7222ebc3cb8 100644 --- a/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb +++ b/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning do subject { described_class.source(property_data) } @@ -17,11 +17,11 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning do end it 'returns expected source data' do - is_expected.to eq({ - 'type' => :dependency_scanning, - 'data' => property_data, - 'fingerprint' => '4dbcb747e6f0fb3ed4f48d96b777f1d64acdf43e459fdfefad404e55c004a188' - }) + is_expected.to have_attributes( + source_type: :dependency_scanning, + data: property_data, + fingerprint: '4dbcb747e6f0fb3ed4f48d96b777f1d64acdf43e459fdfefad404e55c004a188' + ) end end diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb index 6495d1f654b..297ef1f5bb9 100644 --- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do } end - where(vulnerability_finding_signatures_enabled: [true, false]) + where(signatures_enabled: [true, false]) with_them do let_it_be(:pipeline) { create(:ci_pipeline) } @@ -44,7 +44,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do let(:validator_class) { Gitlab::Ci::Parsers::Security::Validators::SchemaValidator } let(:data) { {}.merge(scanner_data) } let(:json_data) { data.to_json } - let(:parser) { described_class.new(json_data, report, vulnerability_finding_signatures_enabled, validate: validate) } + let(:parser) { described_class.new(json_data, report, signatures_enabled: signatures_enabled, validate: validate) } subject(:parse_report) { parser.parse! } @@ -191,7 +191,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do context 'report parsing' do before do - artifact.each_blob { |blob| described_class.parse!(blob, report, vulnerability_finding_signatures_enabled) } + artifact.each_blob { |blob| described_class.parse!(blob, report, signatures_enabled: signatures_enabled) } end describe 'parsing finding.name' do @@ -262,7 +262,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do describe 'top-level scanner' do it 'is the primary scanner' do expect(report.primary_scanner.external_id).to eq('gemnasium') - expect(report.primary_scanner.name).to eq('Gemnasium') + expect(report.primary_scanner.name).to eq('Gemnasium top-level') expect(report.primary_scanner.vendor).to eq('GitLab') expect(report.primary_scanner.version).to eq('2.18.0') end @@ -278,9 +278,17 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do describe 'parsing scanners' do subject(:scanner) { report.findings.first.scanner } - context 'when vendor is not missing in scanner' do - it 'returns scanner with parsed vendor value' do - expect(scanner.vendor).to eq('GitLab') + context 'when the report contains top-level scanner' do + it 'sets the scanner of finding as top-level scanner' do + expect(scanner.name).to eq('Gemnasium top-level') + end + end + + context 'when the report does not contain top-level scanner' do + let(:artifact) { build(:ci_job_artifact, :common_security_report_without_top_level_scanner) } + + it 'sets the scanner of finding as `vulnerabilities[].scanner`' do + expect(scanner.name).to eq('Gemnasium') end end end @@ -465,7 +473,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do finding = report.findings.first highest_signature = finding.signatures.max_by(&:priority) - identifiers = if vulnerability_finding_signatures_enabled + identifiers = if signatures_enabled "#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{highest_signature.signature_hex}-#{report.project_id}" else "#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{finding.location.fingerprint}-#{report.project_id}" diff --git a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb index 7828aa99f6a..e730afc72b5 100644 --- a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb @@ -19,8 +19,72 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end + let(:report_data) do + { + 'scan' => { + 'analyzer' => { + 'id' => 'my-dast-analyzer', + 'name' => 'My DAST analyzer', + 'version' => '0.1.0', + 'vendor' => { 'name' => 'A DAST analyzer' } + }, + 'end_time' => '2020-01-28T03:26:02', + 'scanned_resources' => [], + 'scanner' => { + 'id' => 'my-dast-scanner', + 'name' => 'My DAST scanner', + 'version' => '0.2.0', + 'vendor' => { 'name' => 'A DAST scanner' } + }, + 'start_time' => '2020-01-28T03:26:01', + 'status' => 'success', + 'type' => 'dast' + }, + 'version' => report_version, + 'vulnerabilities' => [] + } + end + let(:validator) { described_class.new(report_type, report_data, report_version, project: project, scanner: scanner) } + shared_examples 'report is valid' do + context 'and the report is valid' do + it { is_expected.to be_truthy } + end + end + + shared_examples 'logs related information' do + it 'logs related information' do + expect(Gitlab::AppLogger).to receive(:info).with( + message: "security report schema validation problem", + security_report_type: report_type, + security_report_version: report_version, + project_id: project.id, + security_report_failure: security_report_failure, + security_report_scanner_id: 'gemnasium', + security_report_scanner_version: '2.1.0' + ) + + subject + end + end + + shared_examples 'report is invalid' do + context 'and the report is invalid' do + let(:report_data) do + { + 'version' => report_version + } + end + + let(:security_report_failure) { 'schema_validation_fails' } + + it { is_expected.to be_falsey } + + it_behaves_like 'logs related information' + end + end + describe 'SUPPORTED_VERSIONS' do schema_path = Rails.root.join("lib", "gitlab", "ci", "parsers", "security", "validators", "schemas") @@ -75,80 +139,16 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do (latest_vendored_version[0...2] << "34").join(".") end - context 'and the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - - it { is_expected.to be_truthy } - end - - context 'and the report is invalid' do - let(:report_data) do - { - 'version' => report_version - } - end - - it { is_expected.to be_falsey } - - it 'logs related information' do - expect(Gitlab::AppLogger).to receive(:info).with( - message: "security report schema validation problem", - security_report_type: report_type, - security_report_version: report_version, - project_id: project.id, - security_report_failure: 'schema_validation_fails', - security_report_scanner_id: 'gemnasium', - security_report_scanner_version: '2.1.0' - ) - - subject - end - end + it_behaves_like 'report is valid' + it_behaves_like 'report is invalid' end context 'when given a supported schema version' do let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } - context 'and the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - - it { is_expected.to be_truthy } - end - - context 'and the report is invalid' do - let(:report_data) do - { - 'version' => report_version - } - end - - it { is_expected.to be_falsey } - - it 'logs related information' do - expect(Gitlab::AppLogger).to receive(:info).with( - message: "security report schema validation problem", - security_report_type: report_type, - security_report_version: report_version, - project_id: project.id, - security_report_failure: 'schema_validation_fails', - security_report_scanner_id: 'gemnasium', - security_report_scanner_version: '2.1.0' - ) - - subject - end - end + it_behaves_like 'report is valid' + it_behaves_like 'report is invalid' end context 'when given a deprecated schema version' do @@ -173,21 +173,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end + let(:security_report_failure) { 'using_deprecated_schema_version' } + it { is_expected.to be_truthy } - it 'logs related information' do - expect(Gitlab::AppLogger).to receive(:info).with( - message: "security report schema validation problem", - security_report_type: report_type, - security_report_version: report_version, - project_id: project.id, - security_report_failure: 'using_deprecated_schema_version', - security_report_scanner_id: 'gemnasium', - security_report_scanner_version: '2.1.0' - ) - - subject - end + it_behaves_like 'logs related information' end context 'and the report does not pass schema validation' do @@ -213,21 +203,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end + let(:security_report_failure) { 'using_unsupported_schema_version' } + it { is_expected.to be_falsey } - it 'logs related information' do - expect(Gitlab::AppLogger).to receive(:info).with( - message: "security report schema validation problem", - security_report_type: report_type, - security_report_version: report_version, - project_id: project.id, - security_report_failure: 'using_unsupported_schema_version', - security_report_scanner_id: 'gemnasium', - security_report_scanner_version: '2.1.0' - ) - - subject - end + it_behaves_like 'logs related information' end context 'and the report is invalid' do @@ -282,6 +262,16 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do end end + shared_examples 'report is valid with no error' do + context 'and the report is valid' do + it { is_expected.to be_empty } + end + end + + shared_examples 'report with expected errors' do + it { is_expected.to match_array(expected_errors) } + end + describe '#errors' do subject { validator.errors } @@ -289,16 +279,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } - context 'and the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - - it { is_expected.to be_empty } - end + it_behaves_like 'report is valid with no error' context 'and the report is invalid' do let(:report_data) do @@ -309,11 +290,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:expected_errors) do [ - 'root is missing required keys: vulnerabilities' + 'root is missing required keys: scan, vulnerabilities' ] end - it { is_expected.to match_array(expected_errors) } + it_behaves_like 'report with expected errors' end end @@ -331,16 +312,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash) end - context 'and the report passes schema validation' do - let(:report_data) do - { - 'version' => '10.0.0', - 'vulnerabilities' => [] - } - end - - it { is_expected.to be_empty } - end + it_behaves_like 'report is valid with no error' context 'and the report does not pass schema validation' do let(:report_data) do @@ -356,7 +328,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do ] end - it { is_expected.to match_array(expected_errors) } + it_behaves_like 'report with expected errors' end end @@ -383,7 +355,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do ] end - it { is_expected.to match_array(expected_errors) } + it_behaves_like 'report with expected errors' end context 'and the report is invalid' do @@ -400,7 +372,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do ] end - it { is_expected.to match_array(expected_errors) } + it_behaves_like 'report with expected errors' end end @@ -426,10 +398,27 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do ] end - it { is_expected.to match_array(expected_errors) } + it_behaves_like 'report with expected errors' end end + shared_examples 'report is valid with no warning' do + context 'and the report is valid' do + let(:report_data) do + { + 'version' => report_version, + 'vulnerabilities' => [] + } + end + + it { is_expected.to be_empty } + end + end + + shared_examples 'report with expected warnings' do + it { is_expected.to match_array(expected_deprecation_warnings) } + end + describe '#deprecation_warnings' do subject { validator.deprecation_warnings } @@ -491,7 +480,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end - it { is_expected.to match_array(expected_deprecation_warnings) } + it_behaves_like 'report with expected warnings' end context 'and the report does not pass schema validation' do @@ -501,7 +490,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end - it { is_expected.to match_array(expected_deprecation_warnings) } + it_behaves_like 'report with expected warnings' end end @@ -516,7 +505,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end - it { is_expected.to match_array(expected_deprecation_warnings) } + it_behaves_like 'report with expected warnings' end end @@ -561,21 +550,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end + let(:security_report_failure) { 'schema_validation_fails' } + it { is_expected.to match_array([message]) } - it 'logs related information' do - expect(Gitlab::AppLogger).to receive(:info).with( - message: "security report schema validation problem", - security_report_type: report_type, - security_report_version: report_version, - project_id: project.id, - security_report_failure: 'schema_validation_fails', - security_report_scanner_id: 'gemnasium', - security_report_scanner_version: '2.1.0' - ) - - subject - end + it_behaves_like 'logs related information' end end @@ -583,16 +562,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } - context 'and the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - - it { is_expected.to be_empty } - end + it_behaves_like 'report is valid with no warning' context 'and the report is invalid' do let(:report_data) do @@ -644,16 +614,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:report_type) { :dast } let(:report_version) { "12.37.0" } - context 'and the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - - it { is_expected.to be_empty } - end + it_behaves_like 'report is valid with no warning' context 'and the report is invalid' do let(:report_data) do diff --git a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb index 82fa11d5f98..821a5057d2e 100644 --- a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb +++ b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb @@ -4,11 +4,13 @@ require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Parsers::Test::Junit do describe '#parse!' do - subject { described_class.new.parse!(junit, test_suite, job: job) } + subject { described_class.new.parse!(junit, test_report, job: job) } - let(:test_suite) { Gitlab::Ci::Reports::TestSuite.new('rspec') } + let(:job) { double(test_suite_name: 'rspec', max_test_cases_per_report: max_test_cases) } + + let(:test_report) { Gitlab::Ci::Reports::TestReport.new } + let(:test_suite) { test_report.get_suite(job.test_suite_name) } let(:test_cases) { flattened_test_cases(test_suite) } - let(:job) { double(max_test_cases_per_report: max_test_cases) } let(:max_test_cases) { 0 } context 'when data is JUnit style XML' do diff --git a/spec/lib/gitlab/ci/parsers_spec.rb b/spec/lib/gitlab/ci/parsers_spec.rb index c9891c06507..a9adff4fce3 100644 --- a/spec/lib/gitlab/ci/parsers_spec.rb +++ b/spec/lib/gitlab/ci/parsers_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Parsers do describe '.fabricate!' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb new file mode 100644 index 00000000000..15df5b2f68c --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Chain::AssignPartition do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user) + end + + let(:pipeline) { build(:ci_pipeline, project: project) } + let(:step) { described_class.new(pipeline, command) } + let(:current_partition_id) { 123 } + + describe '#perform!' do + before do + allow(Ci::Pipeline).to receive(:current_partition_value) { current_partition_id } + end + + subject { step.perform! } + + it 'assigns partition_id to pipeline' do + expect { subject }.to change(pipeline, :partition_id).to(current_partition_id) + end + + context 'with parent-child pipelines' do + let(:bridge) do + instance_double(Ci::Bridge, + triggers_child_pipeline?: true, + parent_pipeline: instance_double(Ci::Pipeline, partition_id: 125)) + end + + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, + current_user: user, + bridge: bridge) + end + + it 'assigns partition_id to pipeline' do + expect { subject }.to change(pipeline, :partition_id).to(125) + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb index de43e759193..6e8b6e40928 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -302,13 +302,13 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do context 'when bridge is present' do context 'when bridge triggers a child pipeline' do - let(:bridge) { double(:bridge, triggers_child_pipeline?: true) } + let(:bridge) { instance_double(Ci::Bridge, triggers_child_pipeline?: true) } it { is_expected.to be_truthy } end context 'when bridge triggers a multi-project pipeline' do - let(:bridge) { double(:bridge, triggers_child_pipeline?: false) } + let(:bridge) { instance_double(Ci::Bridge, triggers_child_pipeline?: false) } it { is_expected.to be_falsey } end @@ -321,6 +321,38 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do end end + describe '#parent_pipeline_partition_id' do + let(:command) { described_class.new(bridge: bridge) } + + subject { command.parent_pipeline_partition_id } + + context 'when bridge is present' do + context 'when bridge triggers a child pipeline' do + let(:pipeline) { instance_double(Ci::Pipeline, partition_id: 123) } + + let(:bridge) do + instance_double(Ci::Bridge, + triggers_child_pipeline?: true, + parent_pipeline: pipeline) + end + + it { is_expected.to eq(123) } + end + + context 'when bridge triggers a multi-project pipeline' do + let(:bridge) { instance_double(Ci::Bridge, triggers_child_pipeline?: false) } + + it { is_expected.to be_nil } + end + end + + context 'when bridge is not present' do + let(:bridge) { nil } + + it { is_expected.to be_nil } + end + end + describe '#increment_pipeline_failure_reason_counter' do let(:command) { described_class.new } let(:reason) { :size_limit_exceeded } @@ -345,7 +377,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do describe '#observe_step_duration' do context 'when ci_pipeline_creation_step_duration_tracking is enabled' do it 'adds the duration to the step duration histogram' do - histogram = double(:histogram) + histogram = instance_double(Prometheus::Client::Histogram) duration = 1.hour expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_creation_step_duration_histogram) diff --git a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb index e0d656f456e..f451bd6bfef 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb @@ -11,7 +11,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do subject { described_class.new(pipeline, command) } - describe '#perform!' do + # TODO: change this to `describe` and remove rubocop-disable + # when removing the FF ci_project_pipeline_config_refactoring + shared_context '#perform!' do # rubocop:disable RSpec/ContextWording context 'when bridge job is passed in as parameter' do let(:ci_config_path) { nil } let(:bridge) { create(:ci_bridge) } @@ -201,4 +203,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do end end end + + it_behaves_like '#perform!' + + context 'when the FF ci_project_pipeline_config_refactoring is disabled' do + before do + stub_feature_flags(ci_project_pipeline_config_refactoring: false) + end + + it_behaves_like '#perform!' + end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb index e07a3ca9033..7fb5b0b4200 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb @@ -2,11 +2,13 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do +RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments, :aggregate_failures do let(:project) { create(:project) } let(:user) { create(:user) } let(:stage) { build(:ci_stage, project: project, statuses: [job]) } let(:pipeline) { build(:ci_pipeline, project: project, stages: [stage]) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:environment) { project.environments.find_by_name('review/master') } let(:command) do Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user) @@ -24,12 +26,26 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do context 'when a pipeline contains a deployment job' do let!(:job) { build(:ci_build, :start_review_app, project: project) } - it 'ensures environment existence for the job' do - expect { subject }.to change { Environment.count }.by(1) + context 'and the environment does not exist' do + it 'creates the environment specified by the job' do + expect { subject }.to change { Environment.count }.by(1) - expect(project.environments.find_by_name('review/master')).to be_present - expect(job.persisted_environment.name).to eq('review/master') - expect(job.metadata.expanded_environment_name).to eq('review/master') + expect(environment).to be_present + expect(job.persisted_environment.name).to eq('review/master') + expect(job.metadata.expanded_environment_name).to eq('review/master') + end + + context 'and the pipeline is for a merge request' do + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user, merge_request: merge_request) + end + + it 'associates the environment with the merge request' do + expect { subject }.to change { Environment.count }.by(1) + + expect(environment.merge_request).to eq(merge_request) + end + end end context 'when an environment has already been existed' do @@ -40,10 +56,22 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do it 'ensures environment existence for the job' do expect { subject }.not_to change { Environment.count } - expect(project.environments.find_by_name('review/master')).to be_present + expect(environment).to be_present expect(job.persisted_environment.name).to eq('review/master') expect(job.metadata.expanded_environment_name).to eq('review/master') end + + context 'and the pipeline is for a merge request' do + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user, merge_request: merge_request) + end + + it 'does not associate the environment with the merge request' do + expect { subject }.not_to change { Environment.count } + + expect(environment.merge_request).to be_nil + end + end end context 'when an environment name contains an invalid character' do @@ -65,7 +93,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do it 'ensures environment existence for the job' do expect { subject }.to change { Environment.count }.by(1) - expect(project.environments.find_by_name('review/master')).to be_present + expect(environment).to be_present expect(job.persisted_environment.name).to eq('review/master') expect(job.metadata.expanded_environment_name).to eq('review/master') end diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb index f7774e199fb..8c4f7af0ef4 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb @@ -277,7 +277,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do non_handled_sql_queries = 2 # 1. Ci::InstanceVariable Load => `Ci::InstanceVariable#cached_data` => already cached with `fetch_memory_cache` - # 2. Ci::Variable Load => `Project#ci_variables_for` => already cached with `Gitlab::SafeRequestStore` extra_jobs * non_handled_sql_queries end diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb index e8eb3333b88..ee32661f267 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb @@ -83,19 +83,36 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Sequence do .with({ source: 'push' }, 0) end - it 'records active jobs by pipeline plan in a histogram' do - allow(command.metrics) - .to receive(:active_jobs_histogram) - .and_return(histogram) + describe 'active jobs by pipeline plan histogram' do + before do + allow(command.metrics) + .to receive(:active_jobs_histogram) + .and_return(histogram) + + pipeline = create(:ci_pipeline, :running, project: project) + create_list(:ci_build, 3, pipeline: pipeline) + create(:ci_bridge, pipeline: pipeline) + end - pipeline = create(:ci_pipeline, project: project, status: :running) - create(:ci_build, :finished, project: project, pipeline: pipeline) - create(:ci_build, :failed, project: project, pipeline: pipeline) - create(:ci_build, :running, project: project, pipeline: pipeline) - subject.build! + it 'counts all the active jobs' do + subject.build! - expect(histogram).to have_received(:observe) - .with(hash_including(plan: project.actual_plan_name), 3) + expect(histogram).to have_received(:observe) + .with(hash_including(plan: project.actual_plan_name), 4) + end + + context 'when feature flag ci_limit_active_jobs_early is disabled' do + before do + stub_feature_flags(ci_limit_active_jobs_early: false) + end + + it 'counts all the active builds' do + subject.build! + + expect(histogram).to have_received(:observe) + .with(hash_including(plan: project.actual_plan_name), 3) + end + end end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb index fb1a360a4b7..52a00e0d501 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb @@ -179,6 +179,70 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do perform! end end + + describe 'credit_card' do + context 'with no registered credit_card' do + it 'returns the expected credit card counts' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + payload = Gitlab::Json.parse(params[:body]) + + expect(payload['credit_card']['similar_cards_count']).to eq(0) + expect(payload['credit_card']['similar_holder_names_count']).to eq(0) + end + + perform! + end + end + + context 'with a registered credit card' do + let!(:credit_card) { create(:credit_card_validation, last_digits: 10, holder_name: 'Alice', user: user) } + + it 'returns the expected credit card counts' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + payload = Gitlab::Json.parse(params[:body]) + + expect(payload['credit_card']['similar_cards_count']).to eq(1) + expect(payload['credit_card']['similar_holder_names_count']).to eq(1) + end + + perform! + end + + context 'with similar credit cards registered by other users' do + before do + create(:credit_card_validation, last_digits: 10, holder_name: 'Bob') + end + + it 'returns the expected credit card counts' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + payload = Gitlab::Json.parse(params[:body]) + + expect(payload['credit_card']['similar_cards_count']).to eq(2) + expect(payload['credit_card']['similar_holder_names_count']).to eq(1) + end + + perform! + end + end + + context 'with similar holder names registered by other users' do + before do + create(:credit_card_validation, last_digits: 11, holder_name: 'Alice') + end + + it 'returns the expected credit card counts' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + payload = Gitlab::Json.parse(params[:body]) + + expect(payload['credit_card']['similar_cards_count']).to eq(1) + expect(payload['credit_card']['similar_holder_names_count']).to eq(2) + end + + perform! + end + end + end + end end context 'when EXTERNAL_VALIDATION_SERVICE_TOKEN is set' do diff --git a/spec/lib/gitlab/ci/pipeline/duration_spec.rb b/spec/lib/gitlab/ci/pipeline/duration_spec.rb index e0b4928d7f7..46c7072ad8e 100644 --- a/spec/lib/gitlab/ci/pipeline/duration_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/duration_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Duration do let(:calculated_duration) { calculate(data) } diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb index 49686d1a9bd..3ca6fd9143f 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Null do describe '.build' do diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb index c6d0d2534a5..b224fca6011 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::String do describe '.build' do diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb index 3e10ca686ba..b030bd22aa1 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Variable do describe '.build' do diff --git a/spec/lib/gitlab/ci/pipeline/metrics_spec.rb b/spec/lib/gitlab/ci/pipeline/metrics_spec.rb index 83b969ff3c4..8df3c67beaa 100644 --- a/spec/lib/gitlab/ci/pipeline/metrics_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/metrics_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe ::Gitlab::Ci::Pipeline::Metrics do describe '.pipeline_creation_step_duration_histogram' do diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 890ba51157a..75f6a773c2d 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -97,15 +97,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do let(:attributes) do { name: 'rspec', ref: 'master', - job_variables: [{ key: 'VAR1', value: 'var 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }], + job_variables: [{ key: 'VAR1', value: 'var 1' }, + { key: 'VAR2', value: 'var 2' }], rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] } end it do - is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }]) + is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1' }, + { key: 'VAR3', value: 'var 3' }, + { key: 'VAR2', value: 'var 2' }]) end end @@ -114,13 +114,13 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do { name: 'rspec', ref: 'master', - job_variables: [{ key: 'VARIABLE', value: 'value', public: true }], + job_variables: [{ key: 'VARIABLE', value: 'value' }], tag_list: ['static-tag', '$VARIABLE', '$NO_VARIABLE'] } end it { is_expected.to include(tag_list: ['static-tag', 'value', '$NO_VARIABLE']) } - it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value', public: true }]) } + it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value' }]) } end context 'with cache:key' do @@ -257,19 +257,19 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do let(:attributes) do { name: 'rspec', ref: 'master', - yaml_variables: [{ key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }], - job_variables: [{ key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }], + yaml_variables: [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }], + job_variables: [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }], root_variables_inheritance: root_variables_inheritance } end context 'when the pipeline has variables' do let(:root_variables) do - [{ key: 'VAR1', value: 'var overridden pipeline 1', public: true }, - { key: 'VAR2', value: 'var pipeline 2', public: true }, - { key: 'VAR3', value: 'var pipeline 3', public: true }, - { key: 'VAR4', value: 'new var pipeline 4', public: true }] + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var pipeline 2' }, + { key: 'VAR3', value: 'var pipeline 3' }, + { key: 'VAR4', value: 'new var pipeline 4' }] end context 'when root_variables_inheritance is true' do @@ -277,10 +277,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'returns calculated yaml variables' do expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR1', value: 'var overridden pipeline 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }, - { key: 'VAR4', value: 'new var pipeline 4', public: true }] + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }, + { key: 'VAR4', value: 'new var pipeline 4' }] ) end end @@ -290,8 +290,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'returns job variables' do expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }] + [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }] ) end end @@ -301,9 +301,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'returns calculated yaml variables' do expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR1', value: 'var overridden pipeline 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }] + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }] ) end end @@ -314,8 +314,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'returns seed yaml variables' do expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }]) + [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }]) end end end @@ -324,8 +324,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do let(:attributes) do { name: 'rspec', ref: 'master', - yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true }], - job_variables: [{ key: 'VAR1', value: 'var 1', public: true }], + yaml_variables: [{ key: 'VAR1', value: 'var 1' }], + job_variables: [{ key: 'VAR1', value: 'var 1' }], root_variables_inheritance: root_variables_inheritance, rules: rules } end @@ -338,14 +338,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do end it 'recalculates the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1', public: true }, - { key: 'VAR2', value: 'new var 2', public: true }) + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, + { key: 'VAR2', value: 'new var 2' }) end end context 'when the rules use root variables' do let(:root_variables) do - [{ key: 'VAR2', value: 'var pipeline 2', public: true }] + [{ key: 'VAR2', value: 'var pipeline 2' }] end let(:rules) do @@ -353,15 +353,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do end it 'recalculates the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1', public: true }, - { key: 'VAR2', value: 'overridden var 2', public: true }) + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, + { key: 'VAR2', value: 'overridden var 2' }) end context 'when the root_variables_inheritance is false' do let(:root_variables_inheritance) { false } it 'does not recalculate the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1', public: true }) + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1' }) end end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb index 51185be3e74..6569ce937ac 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb @@ -6,8 +6,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Deployment do let_it_be(:project, refind: true) { create(:project, :repository) } let(:pipeline) do - create(:ci_pipeline, project: project, - sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0') + create(:ci_pipeline, project: project, sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0') end let(:job) { build(:ci_build, project: project, pipeline: pipeline) } diff --git a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb index ad89f1f5cda..2b9d8127886 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb @@ -5,7 +5,9 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do let_it_be(:project) { create(:project) } - let(:job) { build(:ci_build, project: project) } + let!(:pipeline) { create(:ci_pipeline, project: project) } + + let(:job) { build(:ci_build, project: project, pipeline: pipeline) } let(:seed) { described_class.new(job) } let(:attributes) { {} } @@ -87,6 +89,28 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do it_behaves_like 'returning a correct environment' end + + context 'and job environment has an auto_stop_in variable attribute' do + let(:environment_auto_stop_in) { '10 minutes' } + let(:expected_auto_stop_in) { '10 minutes' } + + let(:attributes) do + { + environment: environment_name, + options: { + environment: { + name: environment_name, + auto_stop_in: '$TTL' + } + }, + yaml_variables: [ + { key: "TTL", value: environment_auto_stop_in, public: true } + ] + } + end + + it_behaves_like 'returning a correct environment' + end end context 'when job has deployment tier attribute' do @@ -167,5 +191,34 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do it_behaves_like 'returning a correct environment' end + + context 'when merge_request is provided' do + let(:environment_name) { 'development' } + let(:attributes) { { environment: environment_name, options: { environment: { name: environment_name } } } } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:seed) { described_class.new(job, merge_request: merge_request) } + + context 'and environment does not exist' do + let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' } + + it 'creates an environment associated with the merge request' do + expect { subject }.to change { Environment.count }.by(1) + + expect(subject.merge_request).to eq(merge_request) + end + end + + context 'and environment already exists' do + before do + create(:environment, project: project, name: environment_name) + end + + it 'does not change the merge request associated with the environment' do + expect { subject }.not_to change { Environment.count } + + expect(subject.merge_request).to be_nil + end + end + end end end diff --git a/spec/lib/gitlab/ci/processable_object_hierarchy_spec.rb b/spec/lib/gitlab/ci/processable_object_hierarchy_spec.rb new file mode 100644 index 00000000000..a844ce6486b --- /dev/null +++ b/spec/lib/gitlab/ci/processable_object_hierarchy_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::ProcessableObjectHierarchy do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { project.owner } + + let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master') } + + let_it_be(:job1) { create(:ci_build, :created, pipeline: pipeline, name: 'job1') } + let_it_be(:job2) { create(:ci_build, :created, :dependent, pipeline: pipeline, name: 'job2', needed: job1) } + let_it_be(:job3) { create(:ci_build, :created, :dependent, pipeline: pipeline, name: 'job3', needed: job1) } + let_it_be(:job4) { create(:ci_build, :created, :dependent, pipeline: pipeline, name: 'job4', needed: job2) } + let_it_be(:job5) { create(:ci_build, :created, :dependent, pipeline: pipeline, name: 'job5', needed: job3) } + let_it_be(:job6) { create(:ci_build, :created, :dependent, pipeline: pipeline, name: 'job6', needed: job4) } + + describe '#base_and_ancestors' do + it 'includes the base and its ancestors' do + relation = described_class.new(::Ci::Processable.where(id: job2.id)).base_and_ancestors + + expect(relation).to eq([job2, job1]) + end + + it 'can find ancestors upto a certain level' do + relation = described_class.new(::Ci::Processable.where(id: job4.id)).base_and_ancestors(upto: job1.name) + + expect(relation).to eq([job4, job2]) + end + + describe 'hierarchy_order option' do + let(:relation) do + described_class.new(::Ci::Processable.where(id: job4.id)).base_and_ancestors(hierarchy_order: hierarchy_order) + end + + context 'for :asc' do + let(:hierarchy_order) { :asc } + + it 'orders by child to ancestor' do + expect(relation).to eq([job4, job2, job1]) + end + end + + context 'for :desc' do + let(:hierarchy_order) { :desc } + + it 'orders by ancestor to child' do + expect(relation).to eq([job1, job2, job4]) + end + end + end + end + + describe '#base_and_descendants' do + it 'includes the base and its descendants' do + relation = described_class.new(::Ci::Processable.where(id: job2.id)).base_and_descendants + + expect(relation).to contain_exactly(job2, job4, job6) + end + + context 'when with_depth is true' do + let(:relation) do + described_class.new(::Ci::Processable.where(id: job1.id)).base_and_descendants(with_depth: true) + end + + it 'includes depth in the results' do + object_depths = { + job1.id => 1, + job2.id => 2, + job3.id => 2, + job4.id => 3, + job5.id => 3, + job6.id => 4 + } + + relation.each do |object| + expect(object.depth).to eq(object_depths[object.id]) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/project_config/repository_spec.rb b/spec/lib/gitlab/ci/project_config/repository_spec.rb new file mode 100644 index 00000000000..2105b691d9e --- /dev/null +++ b/spec/lib/gitlab/ci/project_config/repository_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::ProjectConfig::Repository do + let(:project) { create(:project, :custom_repo, files: files) } + let(:sha) { project.repository.head_commit.sha } + let(:files) { { 'README.md' => 'hello' } } + + subject(:config) { described_class.new(project, sha, nil, nil, nil) } + + describe '#content' do + subject(:content) { config.content } + + context 'when file is in repository' do + let(:config_content_result) do + <<~CICONFIG + --- + include: + - local: ".gitlab-ci.yml" + CICONFIG + end + + let(:files) { { '.gitlab-ci.yml' => 'content' } } + + it { is_expected.to eq(config_content_result) } + end + + context 'when file is not in repository' do + it { is_expected.to be_nil } + end + + context 'when Gitaly raises error' do + before do + allow(project.repository).to receive(:gitlab_ci_yml_for).and_raise(GRPC::Internal) + end + + it { is_expected.to be_nil } + end + end + + describe '#source' do + subject { config.source } + + it { is_expected.to eq(:repository_source) } + end +end diff --git a/spec/lib/gitlab/ci/project_config/source_spec.rb b/spec/lib/gitlab/ci/project_config/source_spec.rb new file mode 100644 index 00000000000..dda5c7cdce8 --- /dev/null +++ b/spec/lib/gitlab/ci/project_config/source_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::ProjectConfig::Source do + let_it_be(:custom_config_class) { Class.new(described_class) } + let_it_be(:project) { build_stubbed(:project) } + let_it_be(:sha) { '123456' } + + subject(:custom_config) { custom_config_class.new(project, sha, nil, nil, nil) } + + describe '#content' do + subject(:content) { custom_config.content } + + it { expect { content }.to raise_error(NotImplementedError) } + end + + describe '#source' do + subject(:source) { custom_config.source } + + it { expect { source }.to raise_error(NotImplementedError) } + end +end diff --git a/spec/lib/gitlab/ci/project_config_spec.rb b/spec/lib/gitlab/ci/project_config_spec.rb new file mode 100644 index 00000000000..c4b179c9ef5 --- /dev/null +++ b/spec/lib/gitlab/ci/project_config_spec.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::ProjectConfig do + let(:project) { create(:project, :empty_repo, ci_config_path: ci_config_path) } + let(:sha) { '123456' } + let(:content) { nil } + let(:source) { :push } + let(:bridge) { nil } + + subject(:config) do + described_class.new(project: project, sha: sha, + custom_content: content, pipeline_source: source, pipeline_source_bridge: bridge) + end + + context 'when bridge job is passed in as parameter' do + let(:ci_config_path) { nil } + let(:bridge) { create(:ci_bridge) } + + before do + allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml') + end + + it 'returns the content already available in command' do + expect(config.source).to eq(:bridge_source) + expect(config.content).to eq('the-yaml') + end + end + + context 'when config is defined in a custom path in the repository' do + let(:ci_config_path) { 'path/to/config.yml' } + let(:config_content_result) do + <<~CICONFIG + --- + include: + - local: #{ci_config_path} + CICONFIG + end + + before do + allow(project.repository) + .to receive(:gitlab_ci_yml_for) + .with(sha, ci_config_path) + .and_return('the-content') + end + + it 'returns root config including the local custom file' do + expect(config.source).to eq(:repository_source) + expect(config.content).to eq(config_content_result) + end + end + + context 'when config is defined remotely' do + let(:ci_config_path) { 'http://example.com/path/to/ci/config.yml' } + let(:config_content_result) do + <<~CICONFIG + --- + include: + - remote: #{ci_config_path} + CICONFIG + end + + it 'returns root config including the remote config' do + expect(config.source).to eq(:remote_source) + expect(config.content).to eq(config_content_result) + end + end + + context 'when config is defined in a separate repository' do + let(:ci_config_path) { 'path/to/.gitlab-ci.yml@another-group/another-repo' } + let(:config_content_result) do + <<~CICONFIG + --- + include: + - project: another-group/another-repo + file: path/to/.gitlab-ci.yml + CICONFIG + end + + it 'returns root config including the path to another repository' do + expect(config.source).to eq(:external_project_source) + expect(config.content).to eq(config_content_result) + end + + context 'when path specifies a refname' do + let(:ci_config_path) { 'path/to/.gitlab-ci.yml@another-group/another-repo:refname' } + let(:config_content_result) do + <<~CICONFIG + --- + include: + - project: another-group/another-repo + file: path/to/.gitlab-ci.yml + ref: refname + CICONFIG + end + + it 'returns root config including the path and refname to another repository' do + expect(config.source).to eq(:external_project_source) + expect(config.content).to eq(config_content_result) + end + end + end + + context 'when config is defined in the default .gitlab-ci.yml' do + let(:ci_config_path) { nil } + let(:config_content_result) do + <<~CICONFIG + --- + include: + - local: ".gitlab-ci.yml" + CICONFIG + end + + before do + allow(project.repository) + .to receive(:gitlab_ci_yml_for) + .with(sha, '.gitlab-ci.yml') + .and_return('the-content') + end + + it 'returns root config including the canonical CI config file' do + expect(config.source).to eq(:repository_source) + expect(config.content).to eq(config_content_result) + end + end + + context 'when config is the Auto-Devops template' do + let(:ci_config_path) { nil } + let(:config_content_result) do + <<~CICONFIG + --- + include: + - template: Auto-DevOps.gitlab-ci.yml + CICONFIG + end + + before do + allow(project).to receive(:auto_devops_enabled?).and_return(true) + end + + it 'returns root config including the auto-devops template' do + expect(config.source).to eq(:auto_devops_source) + expect(config.content).to eq(config_content_result) + end + end + + context 'when config is passed as a parameter' do + let(:source) { :ondemand_dast_scan } + let(:ci_config_path) { nil } + let(:content) do + <<~CICONFIG + --- + stages: + - dast + CICONFIG + end + + it 'returns the parameter content' do + expect(config.source).to eq(:parameter_source) + expect(config.content).to eq(content) + end + end + + context 'when config is not defined anywhere' do + let(:ci_config_path) { nil } + + before do + allow(project).to receive(:auto_devops_enabled?).and_return(false) + end + + it 'returns nil' do + expect(config.source).to be_nil + expect(config.content).to be_nil + end + end +end diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb index ade0e36cf1e..ad8f1dc11f8 100644 --- a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Reports::AccessibilityReportsComparer do let(:comparer) { described_class.new(base_report, head_report) } diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb index 8c35b2a34cf..af6844491ca 100644 --- a/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Reports::AccessibilityReports do let(:accessibility_report) { described_class.new } diff --git a/spec/lib/gitlab/ci/reports/coverage_report_spec.rb b/spec/lib/gitlab/ci/reports/coverage_report_spec.rb index 53646f7dfc0..23361a0c768 100644 --- a/spec/lib/gitlab/ci/reports/coverage_report_spec.rb +++ b/spec/lib/gitlab/ci/reports/coverage_report_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Reports::CoverageReport do let(:coverage_report) { described_class.new } diff --git a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb index 672117c311f..06ea3433ef0 100644 --- a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb @@ -1,23 +1,23 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Reports::Sbom::Component do let(:attributes) do { - 'type' => 'library', - 'name' => 'component-name', - 'version' => 'v0.0.1' + type: 'library', + name: 'component-name', + version: 'v0.0.1' } end - subject { described_class.new(attributes) } + subject { described_class.new(**attributes) } it 'has correct attributes' do expect(subject).to have_attributes( - component_type: 'library', - name: 'component-name', - version: 'v0.0.1' + component_type: attributes[:type], + name: attributes[:name], + version: attributes[:version] ) end end diff --git a/spec/lib/gitlab/ci/reports/sbom/report_spec.rb b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb index d7a285ab13c..6ffa93e5fc8 100644 --- a/spec/lib/gitlab/ci/reports/sbom/report_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb @@ -15,40 +15,22 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Report do end describe '#set_source' do - let_it_be(:source) do - { - 'type' => :dependency_scanning, - 'data' => { - 'input_file' => { 'path' => 'package-lock.json' }, - 'source_file' => { 'path' => 'package.json' }, - 'package_manager' => { 'name' => 'npm' }, - 'language' => { 'name' => 'JavaScript' } - }, - 'fingerprint' => 'c01df1dc736c1148717e053edbde56cb3a55d3e31f87cea955945b6f67c17d42' - } - end + let_it_be(:source) { create(:ci_reports_sbom_source) } it 'stores the source' do report.set_source(source) - expect(report.source).to be_a(Gitlab::Ci::Reports::Sbom::Source) + expect(report.source).to eq(source) end end describe '#add_component' do - let_it_be(:components) do - [ - { 'type' => 'library', 'name' => 'component1', 'version' => 'v0.0.1' }, - { 'type' => 'library', 'name' => 'component2', 'version' => 'v0.0.2' }, - { 'type' => 'library', 'name' => 'component2' } - ] - end + let_it_be(:components) { create_list(:ci_reports_sbom_component, 3) } it 'appends components to a list' do components.each { |component| report.add_component(component) } - expect(report.components.size).to eq(3) - expect(report.components).to all(be_a(Gitlab::Ci::Reports::Sbom::Component)) + expect(report.components).to match_array(components) end end end diff --git a/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb b/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb index 97d8d7abb33..75ea91251eb 100644 --- a/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Reports::Sbom::Reports do subject(:reports_list) { described_class.new } diff --git a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb index 2d6434534a0..cb30bd721dd 100644 --- a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb @@ -1,29 +1,29 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Reports::Sbom::Source do let(:attributes) do { - 'type' => :dependency_scanning, - 'data' => { + type: :dependency_scanning, + data: { 'category' => 'development', 'input_file' => { 'path' => 'package-lock.json' }, 'source_file' => { 'path' => 'package.json' }, 'package_manager' => { 'name' => 'npm' }, 'language' => { 'name' => 'JavaScript' } }, - 'fingerprint' => '4dbcb747e6f0fb3ed4f48d96b777f1d64acdf43e459fdfefad404e55c004a188' + fingerprint: '4dbcb747e6f0fb3ed4f48d96b777f1d64acdf43e459fdfefad404e55c004a188' } end - subject { described_class.new(attributes) } + subject { described_class.new(**attributes) } it 'has correct attributes' do expect(subject).to have_attributes( - source_type: attributes['type'], - data: attributes['data'], - fingerprint: attributes['fingerprint'] + source_type: attributes[:type], + data: attributes[:data], + fingerprint: attributes[:fingerprint] ) end end diff --git a/spec/lib/gitlab/ci/reports/security/flag_spec.rb b/spec/lib/gitlab/ci/reports/security/flag_spec.rb index d677425a8da..6ee074f7aeb 100644 --- a/spec/lib/gitlab/ci/reports/security/flag_spec.rb +++ b/spec/lib/gitlab/ci/reports/security/flag_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Reports::Security::Flag do subject(:security_flag) { described_class.new(type: 'flagged-as-likely-false-positive', origin: 'post analyzer X', description: 'static string to sink') } diff --git a/spec/lib/gitlab/ci/reports/security/link_spec.rb b/spec/lib/gitlab/ci/reports/security/link_spec.rb index 7b55af27f4d..0e1cdc93f6c 100644 --- a/spec/lib/gitlab/ci/reports/security/link_spec.rb +++ b/spec/lib/gitlab/ci/reports/security/link_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Reports::Security::Link do subject(:security_link) { described_class.new(name: 'CVE-2020-0202', url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202') } diff --git a/spec/lib/gitlab/ci/reports/security/scan_spec.rb b/spec/lib/gitlab/ci/reports/security/scan_spec.rb index b4968ff3a6e..23427e8608c 100644 --- a/spec/lib/gitlab/ci/reports/security/scan_spec.rb +++ b/spec/lib/gitlab/ci/reports/security/scan_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Reports::Security::Scan do describe '#initialize' do diff --git a/spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb b/spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb index e9daa05e8b9..74a5344f79e 100644 --- a/spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb +++ b/spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Reports::Security::ScannedResource do let(:url) { 'http://example.com:3001/1?foo=bar' } diff --git a/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb b/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb index 5e94fe2bb3d..f754786d071 100644 --- a/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Reports::TerraformReports do it 'initializes plans with and empty hash' do diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb index 3e1004754ba..e81c7b0f6be 100644 --- a/spec/lib/gitlab/ci/status/extended_spec.rb +++ b/spec/lib/gitlab/ci/status/extended_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Status::Extended do it 'requires subclass to implement matcher' do diff --git a/spec/lib/gitlab/ci/status/group/factory_spec.rb b/spec/lib/gitlab/ci/status/group/factory_spec.rb index c67c7ff8271..38aa1ba4ebb 100644 --- a/spec/lib/gitlab/ci/status/group/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/group/factory_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Status::Group::Factory do it 'inherits from the core factory' do diff --git a/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..5a62324da74 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Katalon.gitlab-ci.yml' do + subject(:template) do + <<~YAML + include: + - template: 'Katalon.gitlab-ci.yml' + + katalon_tests_placeholder: + extends: .katalon_tests + stage: test + script: + - echo "katalon tests" + + katalon_tests_with_artifacts_placeholder: + extends: .katalon_tests_with_artifacts + stage: test + script: + - echo "katalon tests with artifacts" + YAML + end + + describe 'the created pipeline' do + let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } + let(:user) { project.first_owner } + + let(:service) { Ci::CreatePipelineService.new(project, user, ref: 'master' ) } + let(:pipeline) { service.execute!(:push).payload } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template) + end + + it 'create katalon tests jobs' do + expect(build_names).to match_array(%w[katalon_tests_placeholder katalon_tests_with_artifacts_placeholder]) + + expect(pipeline.builds.find_by(name: 'katalon_tests_placeholder').options).to include( + image: { name: 'katalonstudio/katalon' }, + services: [{ name: 'docker:dind' }] + ) + + expect(pipeline.builds.find_by(name: 'katalon_tests_with_artifacts_placeholder').options).to include( + image: { name: 'katalonstudio/katalon' }, + services: [{ name: 'docker:dind' }], + artifacts: { when: 'always', paths: ['Reports/'], reports: { junit: ['Reports/*/*/*/*.xml'] } } + ) + end + end +end diff --git a/spec/lib/gitlab/ci/trace/archive_spec.rb b/spec/lib/gitlab/ci/trace/archive_spec.rb index 3ae0e5d1f0e..f91cb03883a 100644 --- a/spec/lib/gitlab/ci/trace/archive_spec.rb +++ b/spec/lib/gitlab/ci/trace/archive_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Trace::Archive do context 'with transactional fixtures' do - let_it_be(:job) { create(:ci_build, :success, :trace_live) } + let_it_be_with_reload(:job) { create(:ci_build, :success, :trace_live) } let_it_be_with_reload(:trace_metadata) { create(:ci_build_trace_metadata, build: job) } let_it_be(:src_checksum) do job.trace.read { |stream| Digest::MD5.hexdigest(stream.raw) } diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb index 888ceb7ff9a..3043c8c5467 100644 --- a/spec/lib/gitlab/ci/trace_spec.rb +++ b/spec/lib/gitlab/ci/trace_spec.rb @@ -10,7 +10,6 @@ RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state, factory_defa describe "associations" do it { expect(trace).to respond_to(:job) } - it { expect(trace).to delegate_method(:old_trace).to(:job) } end context 'when trace is migrated to object storage' do diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb index 6ab2089cce8..4833ccf9093 100644 --- a/spec/lib/gitlab/ci/variables/builder_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Variables::Builder do +RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do include Ci::TemplateHelpers let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :repository, namespace: group) } @@ -26,13 +26,13 @@ RSpec.describe Gitlab::Ci::Variables::Builder do { key: 'CI_JOB_NAME', value: job.name }, { key: 'CI_JOB_STAGE', - value: job.stage }, + value: job.stage_name }, { key: 'CI_NODE_TOTAL', value: '1' }, { key: 'CI_BUILD_NAME', value: job.name }, { key: 'CI_BUILD_STAGE', - value: job.stage }, + value: job.stage_name }, { key: 'CI', value: 'true' }, { key: 'GITLAB_CI', @@ -138,11 +138,11 @@ RSpec.describe Gitlab::Ci::Variables::Builder do { key: 'GITLAB_USER_ID', value: user.id.to_s }, { key: 'GITLAB_USER_EMAIL', - value: user.email }, + value: user.email }, { key: 'GITLAB_USER_LOGIN', - value: user.username }, + value: user.username }, { key: 'GITLAB_USER_NAME', - value: user.name } + value: user.name } ].map { |var| var.merge(public: true, masked: false) } end diff --git a/spec/lib/gitlab/ci/variables/collection/sort_spec.rb b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb index 7e4e9602a92..57171e5be69 100644 --- a/spec/lib/gitlab/ci/variables/collection/sort_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb @@ -2,6 +2,7 @@ require 'fast_spec_helper' require 'rspec-parameterized' +require 'tsort' RSpec.describe Gitlab::Ci::Variables::Collection::Sort do describe '#initialize with non-Collection value' do diff --git a/spec/lib/gitlab/ci/variables/helpers_spec.rb b/spec/lib/gitlab/ci/variables/helpers_spec.rb index fc1055751bd..fb1e66bd605 100644 --- a/spec/lib/gitlab/ci/variables/helpers_spec.rb +++ b/spec/lib/gitlab/ci/variables/helpers_spec.rb @@ -15,21 +15,21 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do end let(:result) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value22', public: true }, - { key: 'key3', value: 'value3', public: true }] + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] end subject { described_class.merge_variables(current_variables, new_variables) } - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } context 'when new variables is a hash' do let(:new_variables) do { 'key2' => 'value22', 'key3' => 'value3' } end - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } end context 'when new variables is a hash with symbol keys' do @@ -37,79 +37,68 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do { key2: 'value22', key3: 'value3' } end - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } end context 'when new variables is nil' do let(:new_variables) {} let(:result) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value2', public: true }] + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }] end - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } end end - describe '.transform_to_yaml_variables' do - let(:variables) do - { 'key1' => 'value1', 'key2' => 'value2' } - end - - let(:result) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value2', public: true }] - end - - subject { described_class.transform_to_yaml_variables(variables) } - - it { is_expected.to eq(result) } + describe '.transform_to_array' do + subject { described_class.transform_to_array(variables) } - context 'when variables is nil' do - let(:variables) {} - - it { is_expected.to eq([]) } - end - end + context 'when values are strings' do + let(:variables) do + { 'key1' => 'value1', 'key2' => 'value2' } + end - describe '.transform_from_yaml_variables' do - let(:variables) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value2', public: true }] - end + let(:result) do + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }] + end - let(:result) do - { 'key1' => 'value1', 'key2' => 'value2' } + it { is_expected.to match_array(result) } end - subject { described_class.transform_from_yaml_variables(variables) } - - it { is_expected.to eq(result) } - context 'when variables is nil' do let(:variables) {} - it { is_expected.to eq({}) } + it { is_expected.to match_array([]) } end - context 'when variables is a hash' do + context 'when values are hashes' do let(:variables) do - { key1: 'value1', 'key2' => 'value2' } + { 'key1' => { value: 'value1', description: 'var 1' }, 'key2' => { value: 'value2' } } end - it { is_expected.to eq(result) } - end - - context 'when variables contain integers and symbols' do - let(:variables) do - { key1: 1, key2: :value2 } + let(:result) do + [{ key: 'key1', value: 'value1', description: 'var 1' }, + { key: 'key2', value: 'value2' }] end - let(:result1) do - { 'key1' => '1', 'key2' => 'value2' } - end + it { is_expected.to match_array(result) } + + context 'when a value data has `key` as a key' do + let(:variables) do + { 'key1' => { value: 'value1', key: 'new_key1' }, 'key2' => { value: 'value2' } } + end + + let(:result) do + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }] + end - it { is_expected.to eq(result1) } + it 'ignores the key set with "key"' do + is_expected.to match_array(result) + end + end end end @@ -127,35 +116,35 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do let(:inheritance) { true } let(:result) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value22', public: true }, - { key: 'key3', value: 'value3', public: true }] + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] end subject { described_class.inherit_yaml_variables(from: from, to: to, inheritance: inheritance) } - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } context 'when inheritance is false' do let(:inheritance) { false } let(:result) do - [{ key: 'key2', value: 'value22', public: true }, - { key: 'key3', value: 'value3', public: true }] + [{ key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] end - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } end context 'when inheritance is array' do let(:inheritance) { ['key2'] } let(:result) do - [{ key: 'key2', value: 'value22', public: true }, - { key: 'key3', value: 'value3', public: true }] + [{ key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] end - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } end end end diff --git a/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb b/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb index f815f56543c..082febacbd7 100644 --- a/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'fast_spec_helper' +require 'tsort' RSpec.describe Gitlab::Ci::YamlProcessor::Dag do let(:nodes) { {} } diff --git a/spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb b/spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb index 0bd9563d191..77346f328ca 100644 --- a/spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' RSpec.describe Gitlab::Ci::YamlProcessor::FeatureFlags do let(:feature_flag) { :my_feature_flag } @@ -48,20 +48,32 @@ RSpec.describe Gitlab::Ci::YamlProcessor::FeatureFlags do end context 'when feature flag is checked outside the "with_actor" block' do - it 'raises an error on dev/test environment' do - expect { described_class.enabled?(feature_flag) }.to raise_error(described_class::NoActorError) - end + context 'when yaml_processor_feature_flag_corectness is used', :yaml_processor_feature_flag_corectness do + it 'raises an error on dev/test environment' do + expect { described_class.enabled?(feature_flag) }.to raise_error(described_class::NoActorError) + end + + context 'when on production' do + before do + allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false) + end - context 'when on production' do - before do - allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false) + it 'checks the feature flag without actor' do + expect(Feature).to receive(:enabled?).with(feature_flag, nil) + expect(Gitlab::ErrorTracking) + .to receive(:track_and_raise_for_dev_exception) + .and_call_original + + described_class.enabled?(feature_flag) + end end + end + context 'when yaml_processor_feature_flag_corectness is not used' do it 'checks the feature flag without actor' do expect(Feature).to receive(:enabled?).with(feature_flag, nil) expect(Gitlab::ErrorTracking) - .to receive(:track_and_raise_for_dev_exception) - .and_call_original + .to receive(:track_exception) described_class.enabled?(feature_flag) end diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb index 8416501e949..f7a0905d9da 100644 --- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb @@ -72,8 +72,8 @@ module Gitlab it 'returns calculated variables with root and job variables' do is_expected.to match_array([ - { key: 'VAR1', value: 'value 11', public: true }, - { key: 'VAR2', value: 'value 2', public: true } + { key: 'VAR1', value: 'value 11' }, + { key: 'VAR2', value: 'value 2' } ]) end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 35af9ae6201..cc327f5b5f1 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -298,8 +298,8 @@ module Gitlab context 'when delayed is defined' do let(:config) do YAML.dump(rspec: { - script: 'rollout 10%', - when: 'delayed', + script: 'rollout 10%', + when: 'delayed', start_in: '1 day' }) end @@ -315,7 +315,7 @@ module Gitlab context 'when resource group is defined' do let(:config) do YAML.dump(rspec: { - script: 'test', + script: 'test', resource_group: 'iOS' }) end @@ -448,7 +448,7 @@ module Gitlab it 'parses the root:variables as #root_variables' do expect(subject.root_variables) - .to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true }) + .to contain_exactly({ key: 'SUPPORTED', value: 'parsed' }) end end @@ -490,7 +490,7 @@ module Gitlab it 'parses the root:variables as #root_variables' do expect(subject.root_variables) - .to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true }) + .to contain_exactly({ key: 'SUPPORTED', value: 'parsed' }) end end @@ -997,18 +997,6 @@ module Gitlab scheduling_type: :stage }) end - - context 'when the feature flag ci_docker_image_pull_policy is disabled' do - before do - stub_feature_flags(ci_docker_image_pull_policy: false) - end - - it { is_expected.not_to be_valid } - - it "returns no job" do - expect(processor.jobs).to eq({}) - end - end end context 'when a service has pull_policy' do @@ -1042,39 +1030,29 @@ module Gitlab scheduling_type: :stage }) end - - context 'when the feature flag ci_docker_image_pull_policy is disabled' do - before do - stub_feature_flags(ci_docker_image_pull_policy: false) - end - - it { is_expected.not_to be_valid } - - it "returns no job" do - expect(processor.jobs).to eq({}) - end - end end end - describe 'Variables' do - subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } + # Change this to a `describe` block when removing the FF ci_variables_refactoring_to_variable + shared_examples 'Variables' do + subject(:execute) { described_class.new(config).execute } - let(:build) { subject.builds.first } + let(:build) { execute.builds.first } let(:job_variables) { build[:job_variables] } let(:root_variables_inheritance) { build[:root_variables_inheritance] } context 'when global variables are defined' do - let(:variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - let(:config) do - { - variables: variables, - before_script: ['pwd'], - rspec: { script: 'rspec' } - } + <<~YAML + variables: + VAR1: value1 + VAR2: value2 + + before_script: [pwd] + + rspec: + script: rspec + YAML end it 'returns global variables' do @@ -1084,22 +1062,23 @@ module Gitlab end context 'when job variables are defined' do - let(:config) do - { - before_script: ['pwd'], - rspec: { script: 'rspec', variables: variables } - } - end - context 'when syntax is correct' do - let(:variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } + let(:config) do + <<~YAML + before_script: [pwd] + + rspec: + script: rspec + variables: + VAR1: value1 + VAR2: value2 + YAML end it 'returns job variables' do expect(job_variables).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } + { key: 'VAR1', value: 'value1' }, + { key: 'VAR2', value: 'value2' } ) expect(root_variables_inheritance).to eq(true) end @@ -1107,16 +1086,28 @@ module Gitlab context 'when syntax is incorrect' do context 'when variables defined but invalid' do - let(:variables) do - %w(VAR1 value1 VAR2 value2) + let(:config) do + <<~YAML + before_script: [pwd] + + rspec: + script: rspec + variables: [VAR1 value1 VAR2 value2] + YAML end - it_behaves_like 'returns errors', /jobs:rspec:variables config should be a hash of key value pairs/ + it_behaves_like 'returns errors', /jobs:rspec:variables config should be a hash/ end context 'when variables key defined but value not specified' do - let(:variables) do - nil + let(:config) do + <<~YAML + before_script: [pwd] + + rspec: + script: rspec + variables: null + YAML end it 'returns empty array' do @@ -1133,10 +1124,12 @@ module Gitlab context 'when job variables are not defined' do let(:config) do - { - before_script: ['pwd'], - rspec: { script: 'rspec' } - } + <<~YAML + before_script: ['pwd'] + + rspec: + script: rspec + YAML end it 'returns empty array' do @@ -1144,6 +1137,42 @@ module Gitlab expect(root_variables_inheritance).to eq(true) end end + + context 'when variables have different type of values' do + let(:config) do + <<~YAML + before_script: [pwd] + + rspec: + variables: + VAR1: value1 + VAR2: :value2 + VAR3: 123 + script: rspec + YAML + end + + it 'returns job variables' do + expect(job_variables).to contain_exactly( + { key: 'VAR1', value: 'value1' }, + { key: 'VAR2', value: 'value2' }, + { key: 'VAR3', value: '123' } + ) + expect(root_variables_inheritance).to eq(true) + end + end + end + + context 'when ci_variables_refactoring_to_variable is enabled' do + it_behaves_like 'Variables' + end + + context 'when ci_variables_refactoring_to_variable is disabled' do + before do + stub_feature_flags(ci_variables_refactoring_to_variable: false) + end + + it_behaves_like 'Variables' end context 'when using `extends`' do @@ -1203,21 +1232,21 @@ module Gitlab expect(config_processor.builds[0]).to include( name: 'test1', options: { script: ['test'] }, - job_variables: [{ key: 'VAR1', value: 'test1 var 1', public: true }, - { key: 'VAR2', value: 'test2 var 2', public: true }] + job_variables: [{ key: 'VAR1', value: 'test1 var 1' }, + { key: 'VAR2', value: 'test2 var 2' }] ) expect(config_processor.builds[1]).to include( name: 'test2', options: { script: ['test'] }, - job_variables: [{ key: 'VAR1', value: 'base var 1', public: true }, - { key: 'VAR2', value: 'test2 var 2', public: true }] + job_variables: [{ key: 'VAR1', value: 'base var 1' }, + { key: 'VAR2', value: 'test2 var 2' }] ) expect(config_processor.builds[2]).to include( name: 'test3', options: { script: ['test'] }, - job_variables: [{ key: 'VAR1', value: 'base var 1', public: true }] + job_variables: [{ key: 'VAR1', value: 'base var 1' }] ) expect(config_processor.builds[3]).to include( @@ -1647,10 +1676,10 @@ module Gitlab describe "Artifacts" do it "returns artifacts when defined" do config = YAML.dump({ - image: "image:1.0", - services: ["mysql"], + image: "image:1.0", + services: ["mysql"], before_script: ["pwd"], - rspec: { + rspec: { artifacts: { paths: ["logs/", "binaries/"], expose_as: "Exposed artifacts", @@ -1906,7 +1935,7 @@ module Gitlab let(:config) do { deploy_to_production: { - stage: 'deploy', + stage: 'deploy', script: 'test' } } @@ -2275,15 +2304,15 @@ module Gitlab let(:config) do { - var_default: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null' }] }, - var_when: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null', when: 'always' }] }, + var_default: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null' }] }, + var_when: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null', when: 'always' }] }, var_and_changes: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null', changes: %w[README], when: 'always' }] }, changes_not_var: { stage: 'test', script: 'test', rules: [{ if: '$VAR != null', changes: %w[README] }] }, var_not_changes: { stage: 'test', script: 'test', rules: [{ if: '$VAR == null', changes: %w[other/file.rb], when: 'always' }] }, - nothing: { stage: 'test', script: 'test', rules: [{ when: 'manual' }] }, - var_never: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'never' }] }, - var_delayed: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] }, - two_rules: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'on_success' }, { changes: %w[README], when: 'manual' }] } + nothing: { stage: 'test', script: 'test', rules: [{ when: 'manual' }] }, + var_never: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'never' }] }, + var_delayed: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] }, + two_rules: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'on_success' }, { changes: %w[README], when: 'manual' }] } } end @@ -2729,13 +2758,13 @@ module Gitlab context 'returns errors if variables is not a map' do let(:config) { YAML.dump({ variables: "test", rspec: { script: "test" } }) } - it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs, value can be a hash' + it_behaves_like 'returns errors', 'variables config should be a hash' end context 'returns errors if variables is not a map of key-value strings' do let(:config) { YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) } - it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs, value can be a hash' + it_behaves_like 'returns errors', 'variable definition must be either a string or a hash' end context 'returns errors if job when is not on_success, on_failure or always' do diff --git a/spec/lib/gitlab/ci_access_spec.rb b/spec/lib/gitlab/ci_access_spec.rb index 9b573c6eb7a..e41b666abda 100644 --- a/spec/lib/gitlab/ci_access_spec.rb +++ b/spec/lib/gitlab/ci_access_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::CiAccess do let(:access) { described_class.new } diff --git a/spec/lib/gitlab/cleanup/personal_access_tokens_spec.rb b/spec/lib/gitlab/cleanup/personal_access_tokens_spec.rb new file mode 100644 index 00000000000..36c5d0e9b0c --- /dev/null +++ b/spec/lib/gitlab/cleanup/personal_access_tokens_spec.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Cleanup::PersonalAccessTokens do + let_it_be(:group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:project_bot) { create(:user, :project_bot) } + + let(:group_full_path) { group.full_path } + let(:logger) { instance_double(Gitlab::AppJsonLogger, info: nil, warn: nil) } + let(:last_used_at) { 1.month.ago.beginning_of_hour } + let!(:unused_token) { create(:personal_access_token) } + + let!(:old_unused_token) do + create(:personal_access_token, created_at: last_used_at - 1.minute) + end + + let!(:old_actively_used_token) do + create(:personal_access_token, created_at: last_used_at - 1.minute, last_used_at: 1.day.ago) + end + + let!(:old_unused_token_for_non_group_member) do + create(:personal_access_token, created_at: last_used_at - 1.minute) + end + + let!(:old_unused_token_for_subgroup_member) do + create(:personal_access_token, created_at: last_used_at - 1.minute) + end + + let!(:old_unused_project_access_token) do + create(:personal_access_token, user: project_bot, created_at: last_used_at - 1.minute) + end + + let!(:old_formerly_used_token) do + create(:personal_access_token, + created_at: last_used_at - 1.minute, + last_used_at: last_used_at - 1.minute + ) + end + + before do + group.add_member(old_formerly_used_token.user, Gitlab::Access::DEVELOPER) + group.add_member(old_actively_used_token.user, Gitlab::Access::DEVELOPER) + group.add_member(unused_token.user, Gitlab::Access::DEVELOPER) + group.add_member(old_unused_token.user, Gitlab::Access::DEVELOPER) + group.add_member(project_bot, Gitlab::Access::MAINTAINER) + + subgroup.add_member(old_unused_token_for_subgroup_member.user, Gitlab::Access::DEVELOPER) + end + + subject do + described_class.new( + logger: logger, + cut_off_date: last_used_at, + group_full_path: group_full_path + ) + end + + context 'when initialized with an invalid logger' do + let(:logger) { "not a logger" } + + it 'raises error' do + expect do + subject.run! + end.to raise_error('Invalid logger: not a logger') + end + end + + describe '#run!' do + context 'when invalid group path passed' do + let(:group_full_path) { 'notagroup' } + + it 'raises error' do + expect do + subject.run! + end.to raise_error("Group with full_path notagroup not found") + end + end + + context 'in a real run' do + let(:args) { { dry_run: false } } + + context 'when revoking unused tokens' do + it 'revokes human-owned tokens created and last used over 1 year ago' do + subject.run!(**args) + + expect(PersonalAccessToken.active).to contain_exactly( + unused_token, + old_actively_used_token, + old_unused_project_access_token, + old_unused_token_for_non_group_member, + old_unused_token_for_subgroup_member + ) + expect(PersonalAccessToken.revoked).to contain_exactly( + old_unused_token, + old_formerly_used_token + ) + end + end + + context 'when revoking used and unused tokens' do + let(:args) { { dry_run: false, revoke_active_tokens: true } } + + it 'revokes human-owned tokens created over 1 year ago' do + subject.run!(**args) + + expect(PersonalAccessToken.active).to contain_exactly( + unused_token, + old_unused_project_access_token, + old_unused_token_for_non_group_member, + old_unused_token_for_subgroup_member + ) + expect(PersonalAccessToken.revoked).to contain_exactly( + old_unused_token, + old_actively_used_token, + old_formerly_used_token + ) + end + end + + it 'updates updated_at' do + expect do + subject.run!(**args) + end.to change { + old_unused_token.reload.updated_at + } + end + + it 'logs action as done' do + message = { + dry_run: false, + token_count: 2, + updated_count: 2, + tokens: instance_of(Array), + group_full_path: group_full_path + } + expect(logger).to receive(:info).with(include(message)) + subject.run!(**args) + end + end + + context 'in a dry run' do + # Dry run is the default + let(:args) { {} } + + it 'does not revoke any tokens' do + expect do + subject.run!(**args) + end.to not_change { + PersonalAccessToken.active.count + } + end + + it 'logs what could be revoked' do + message = { + dry_run: true, + token_count: 2, + updated_count: 0, + tokens: instance_of(Array), + group_full_path: group_full_path + } + expect(logger).to receive(:info).with(include(message)) + subject.run!(**args) + end + end + end +end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 279486aa2a1..1422f83c629 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::ClosingIssueExtractor do let_it_be_with_reload(:project) { create(:project) } - let_it_be(:project2) { create(:project) } + let_it_be_with_reload(:project2) { create(:project) } let_it_be(:issue) { create(:issue, project: project) } let_it_be(:issue2) { create(:issue, project: project2) } @@ -335,6 +335,17 @@ RSpec.describe Gitlab::ClosingIssueExtractor do end end + context 'when target project has autoclose issues disabled' do + before do + project2.update!(autoclose_referenced_issues: false) + end + + it 'omits the issue reference' do + message = "Closes #{cross_reference}" + expect(subject.closed_by_message(message)).to be_empty + end + end + context "with an invalid URL" do it do message = "Closes https://google.com#{urls.project_issue_path(issue2.project, issue2)}" @@ -443,14 +454,19 @@ RSpec.describe Gitlab::ClosingIssueExtractor do end context "with autoclose referenced issues disabled" do - before do + before_all do project.update!(autoclose_referenced_issues: false) end - it do + it 'excludes same project references' do message = "Awesome commit (Closes #{reference})" expect(subject.closed_by_message(message)).to eq([]) end + + it 'includes issues from other projects with autoclose enabled' do + message = "Closes #{cross_reference}" + expect(subject.closed_by_message(message)).to eq([issue2]) + end end end diff --git a/spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb b/spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb index 948de161235..cf532cf7be6 100644 --- a/spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb +++ b/spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Cluster::PumaWorkerKillerObserver do let(:counter) { Gitlab::Metrics::NullMetric.instance } diff --git a/spec/lib/gitlab/config/entry/attributable_spec.rb b/spec/lib/gitlab/config/entry/attributable_spec.rb index 1e7880ed898..8a207bddaae 100644 --- a/spec/lib/gitlab/config/entry/attributable_spec.rb +++ b/spec/lib/gitlab/config/entry/attributable_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Config::Entry::Attributable do let(:node) do diff --git a/spec/lib/gitlab/config/entry/composable_hash_spec.rb b/spec/lib/gitlab/config/entry/composable_hash_spec.rb index f64b39231a3..331c9efc741 100644 --- a/spec/lib/gitlab/config/entry/composable_hash_spec.rb +++ b/spec/lib/gitlab/config/entry/composable_hash_spec.rb @@ -6,7 +6,8 @@ RSpec.describe Gitlab::Config::Entry::ComposableHash, :aggregate_failures do let(:valid_config) do { DATABASE_SECRET: 'passw0rd', - API_TOKEN: 'passw0rd2' + API_TOKEN: 'passw0rd2', + ACCEPT_PASSWORD: false } end @@ -55,6 +56,12 @@ RSpec.describe Gitlab::Config::Entry::ComposableHash, :aggregate_failures do expect(entry[:API_TOKEN].metadata).to eq(name: :API_TOKEN) expect(entry[:API_TOKEN].parent.class).to eq(Gitlab::Config::Entry::ComposableHash) expect(entry[:API_TOKEN].value).to eq('passw0rd2') + expect(entry[:ACCEPT_PASSWORD]).to be_a(Gitlab::Config::Entry::Node) + expect(entry[:ACCEPT_PASSWORD].description).to eq('ACCEPT_PASSWORD node definition') + expect(entry[:ACCEPT_PASSWORD].key).to eq(:ACCEPT_PASSWORD) + expect(entry[:ACCEPT_PASSWORD].metadata).to eq(name: :ACCEPT_PASSWORD) + expect(entry[:ACCEPT_PASSWORD].parent.class).to eq(Gitlab::Config::Entry::ComposableHash) + expect(entry[:ACCEPT_PASSWORD].value).to eq(false) end describe '#descendants' do diff --git a/spec/lib/gitlab/config/entry/simplifiable_spec.rb b/spec/lib/gitlab/config/entry/simplifiable_spec.rb index f9088130037..fbbc9571eb0 100644 --- a/spec/lib/gitlab/config/entry/simplifiable_spec.rb +++ b/spec/lib/gitlab/config/entry/simplifiable_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Config::Entry::Simplifiable do describe '.strategy' do diff --git a/spec/lib/gitlab/config/entry/undefined_spec.rb b/spec/lib/gitlab/config/entry/undefined_spec.rb index 31e0f9487aa..faa9b9b8a7c 100644 --- a/spec/lib/gitlab/config/entry/undefined_spec.rb +++ b/spec/lib/gitlab/config/entry/undefined_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Config::Entry::Undefined do let(:entry) { described_class.new } diff --git a/spec/lib/gitlab/config/entry/unspecified_spec.rb b/spec/lib/gitlab/config/entry/unspecified_spec.rb index 35ba992f62a..8fc0889367f 100644 --- a/spec/lib/gitlab/config/entry/unspecified_spec.rb +++ b/spec/lib/gitlab/config/entry/unspecified_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Config::Entry::Unspecified do let(:unspecified) { described_class.new(entry) } diff --git a/spec/lib/gitlab/container_repository/tags/cache_spec.rb b/spec/lib/gitlab/container_repository/tags/cache_spec.rb index f84c1ce173f..fcfc8e7a348 100644 --- a/spec/lib/gitlab/container_repository/tags/cache_spec.rb +++ b/spec/lib/gitlab/container_repository/tags/cache_spec.rb @@ -79,10 +79,14 @@ RSpec.describe ::Gitlab::ContainerRepository::Tags::Cache, :clean_gitlab_redis_c it 'inserts values in redis' do ::Gitlab::Redis::Cache.with do |redis| - expect(redis) - .to receive(:set) - .with(cache_key(tag), rfc3339(tag.created_at), ex: ttl.to_i) - .and_call_original + expect(redis).to receive(:pipelined).and_call_original + + expect_next_instance_of(Redis::PipelinedConnection) do |pipeline| + expect(pipeline) + .to receive(:set) + .with(cache_key(tag), rfc3339(tag.created_at), ex: ttl.to_i) + .and_call_original + end end subject diff --git a/spec/lib/gitlab/cross_project_access/check_collection_spec.rb b/spec/lib/gitlab/cross_project_access/check_collection_spec.rb index 178188f5555..a75c943aaf6 100644 --- a/spec/lib/gitlab/cross_project_access/check_collection_spec.rb +++ b/spec/lib/gitlab/cross_project_access/check_collection_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::CrossProjectAccess::CheckCollection do subject(:collection) { described_class.new } diff --git a/spec/lib/gitlab/cross_project_access/check_info_spec.rb b/spec/lib/gitlab/cross_project_access/check_info_spec.rb index 5327030daf0..7cf2309a1f8 100644 --- a/spec/lib/gitlab/cross_project_access/check_info_spec.rb +++ b/spec/lib/gitlab/cross_project_access/check_info_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::CrossProjectAccess::CheckInfo do let(:dummy_controller) { double } diff --git a/spec/lib/gitlab/cross_project_access/class_methods_spec.rb b/spec/lib/gitlab/cross_project_access/class_methods_spec.rb index afc45c86362..3a6e528c9b0 100644 --- a/spec/lib/gitlab/cross_project_access/class_methods_spec.rb +++ b/spec/lib/gitlab/cross_project_access/class_methods_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::CrossProjectAccess::ClassMethods do let(:dummy_class) do diff --git a/spec/lib/gitlab/cross_project_access_spec.rb b/spec/lib/gitlab/cross_project_access_spec.rb index fb72b85f161..e45c734a003 100644 --- a/spec/lib/gitlab/cross_project_access_spec.rb +++ b/spec/lib/gitlab/cross_project_access_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::CrossProjectAccess do let(:super_class) { Class.new } diff --git a/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb b/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb index c955b288500..41b0604bee0 100644 --- a/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::CycleAnalytics::Summary::Value do describe Gitlab::CycleAnalytics::Summary::Value::None do diff --git a/spec/lib/gitlab/data_builder/issuable_spec.rb b/spec/lib/gitlab/data_builder/issuable_spec.rb index f0802f335f4..455800a3f7d 100644 --- a/spec/lib/gitlab/data_builder/issuable_spec.rb +++ b/spec/lib/gitlab/data_builder/issuable_spec.rb @@ -73,7 +73,7 @@ RSpec.describe Gitlab::DataBuilder::Issuable do }, assignees: { previous: [], - current: [{ + current: [{ name: "Foo Bar", username: "foobar", avatar_url: "http://www.example.com/my-avatar.jpg" diff --git a/spec/lib/gitlab/data_builder/note_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb index 3fa535dd800..8e8b8ce6681 100644 --- a/spec/lib/gitlab/data_builder/note_spec.rb +++ b/spec/lib/gitlab/data_builder/note_spec.rb @@ -49,8 +49,7 @@ RSpec.describe Gitlab::DataBuilder::Note do let(:label) { create(:label, project: project) } let(:issue) do - create(:labeled_issue, created_at: fixed_time, updated_at: fixed_time, - project: project, labels: [label]) + create(:labeled_issue, created_at: fixed_time, updated_at: fixed_time, project: project, labels: [label]) end let(:note) do @@ -84,15 +83,15 @@ RSpec.describe Gitlab::DataBuilder::Note do describe 'When asking for a note on merge request' do let(:label) { create(:label, project: project) } let(:merge_request) do - create(:labeled_merge_request, created_at: fixed_time, - updated_at: fixed_time, - source_project: project, - labels: [label]) + create(:labeled_merge_request, + created_at: fixed_time, + updated_at: fixed_time, + source_project: project, + labels: [label]) end let(:note) do - create(:note_on_merge_request, noteable: merge_request, - project: project) + create(:note_on_merge_request, noteable: merge_request, project: project) end it_behaves_like 'includes general data' @@ -112,14 +111,15 @@ RSpec.describe Gitlab::DataBuilder::Note do describe 'When asking for a note on merge request diff' do let(:label) { create(:label, project: project) } let(:merge_request) do - create(:labeled_merge_request, created_at: fixed_time, updated_at: fixed_time, - source_project: project, - labels: [label]) + create(:labeled_merge_request, + created_at: fixed_time, + updated_at: fixed_time, + source_project: + project, labels: [label]) end let(:note) do - create(:diff_note_on_merge_request, noteable: merge_request, - project: project) + create(:diff_note_on_merge_request, noteable: merge_request, project: project) end it_behaves_like 'includes general data' @@ -138,13 +138,11 @@ RSpec.describe Gitlab::DataBuilder::Note do describe 'When asking for a note on project snippet' do let!(:snippet) do - create(:project_snippet, created_at: fixed_time, updated_at: fixed_time, - project: project) + create(:project_snippet, created_at: fixed_time, updated_at: fixed_time, project: project) end let!(:note) do - create(:note_on_project_snippet, noteable: snippet, - project: project) + create(:note_on_project_snippet, noteable: snippet, project: project) end it_behaves_like 'includes general data' diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb index 06c2bc32db3..3daed2508a2 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb @@ -59,6 +59,50 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m end end + describe '#pause!' do + context 'when an invalid transition is applied' do + %i[finished failed finalizing].each do |state| + it 'raises an exception' do + batched_migration = create(:batched_background_migration, state) + + expect { batched_migration.pause! }.to raise_error(StateMachines::InvalidTransition, /Cannot transition status/) + end + end + end + + context 'when a valid transition is applied' do + %i[active paused].each do |state| + it 'moves to pause' do + batched_migration = create(:batched_background_migration, state) + + expect(batched_migration.pause!).to be_truthy + end + end + end + end + + describe '#execute!' do + context 'when an invalid transition is applied' do + %i[finished finalizing].each do |state| + it 'raises an exception' do + batched_migration = create(:batched_background_migration, state) + + expect { batched_migration.execute! }.to raise_error(StateMachines::InvalidTransition, /Cannot transition status/) + end + end + end + + context 'when a valid transition is applied' do + %i[active paused failed].each do |state| + it 'moves to active' do + batched_migration = create(:batched_background_migration, state) + + expect(batched_migration.execute!).to be_truthy + end + end + end + end + describe '.valid_status' do valid_status = [:paused, :active, :finished, :failed, :finalizing] @@ -77,6 +121,16 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m end end + describe '.ordered_by_created_at_desc' do + let!(:migration_1) { create(:batched_background_migration, created_at: Time.zone.now - 2) } + let!(:migration_2) { create(:batched_background_migration, created_at: Time.zone.now - 1) } + let!(:migration_3) { create(:batched_background_migration, created_at: Time.zone.now - 3) } + + it 'returns batched migrations ordered by created_at (DESC)' do + expect(described_class.ordered_by_created_at_desc).to eq([migration_2, migration_1, migration_3]) + end + end + describe '.active_migration' do let(:connection) { Gitlab::Database.database_base_models[:main].connection } let!(:migration1) { create(:batched_background_migration, :finished) } @@ -620,7 +674,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m describe '#progress' do subject { migration.progress } - context 'when the migration is finished' do + context 'when the migration is completed' do let(:migration) do create(:batched_background_migration, :finished, total_tuple_count: 1).tap do |record| create(:batched_background_migration_job, :succeeded, batched_migration: record, batch_size: 1) @@ -632,6 +686,18 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m end end + context 'when the status is finished' do + let(:migration) do + create(:batched_background_migration, :finished, total_tuple_count: 100).tap do |record| + create(:batched_background_migration_job, :succeeded, batched_migration: record, batch_size: 5) + end + end + + it 'returns 100' do + expect(subject).to be 100 + end + end + context 'when the migration does not have jobs' do let(:migration) { create(:batched_background_migration, :active) } diff --git a/spec/lib/gitlab/database/batch_average_counter_spec.rb b/spec/lib/gitlab/database/batch_average_counter_spec.rb new file mode 100644 index 00000000000..43c7a1554f7 --- /dev/null +++ b/spec/lib/gitlab/database/batch_average_counter_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::BatchAverageCounter do + let(:model) { Issue } + let(:column) { :weight } + + let(:in_transaction) { false } + + before do + allow(model.connection).to receive(:transaction_open?).and_return(in_transaction) + end + + describe '#count' do + before do + create_list(:issue, 2, weight: 4) + create_list(:issue, 2, weight: 2) + create_list(:issue, 2, weight: 3) + end + + subject(:batch_average_counter) { described_class.new(model, column) } + + it 'returns correct average of weights' do + expect(subject.count).to eq(3.0) + end + + it 'does no raise an exception if transaction is not open' do + expect { subject.count }.not_to raise_error + end + + context 'when transaction is open' do + let(:in_transaction) { true } + + it 'raises an error' do + expect { subject.count }.to raise_error('BatchAverageCounter can not be run inside a transaction') + end + end + + context 'when batch size is small' do + let(:batch_size) { 2 } + + it 'returns correct average of weights' do + expect(subject.count(batch_size: batch_size)).to eq(3.0) + end + end + + context 'when column passed is an Arel attribute' do + let(:column) { model.arel_table[:weight] } + + it 'returns correct average of weights' do + expect(subject.count).to eq(3.0) + end + end + + context 'when column has total count of zero' do + before do + Issue.update_all(weight: nil) + end + + it 'returns the fallback value' do + expect(subject.count).to eq(-1) + end + end + + context 'when one batch has nil weights (no average)' do + before do + issues = Issue.where(weight: 4) + issues.update_all(weight: nil) + end + + let(:batch_size) { 2 } + + it 'calculates average of weights with no errors' do + expect(subject.count(batch_size: batch_size)).to eq(2.5) + end + end + + context 'when batch fetch query is cancelled' do + let(:batch_size) { 22_000 } + let(:relation) { instance_double(ActiveRecord::Relation, to_sql: batch_average_query) } + + context 'when all retries fail' do + let(:batch_average_query) { 'SELECT AVG(weight) FROM issues WHERE weight BETWEEN 2 and 5' } + let(:query_timed_out_exception) { ActiveRecord::QueryCanceled.new('query timed out') } + + before do + allow(model).to receive(:where).and_return(relation) + allow(relation).to receive(:pick).and_raise(query_timed_out_exception) + end + + it 'logs failing query' do + expect(Gitlab::AppJsonLogger).to receive(:error).with( + event: 'batch_count', + relation: model.table_name, + operation: 'average', + start: 2, + query: batch_average_query, + message: 'Query has been canceled with message: query timed out' + ) + + expect(subject.count(batch_size: batch_size)).to eq(-1) + end + end + end + end +end diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb index 811d4fad95c..a87b0c1a3a8 100644 --- a/spec/lib/gitlab/database/batch_count_spec.rb +++ b/spec/lib/gitlab/database/batch_count_spec.rb @@ -86,48 +86,48 @@ RSpec.describe Gitlab::Database::BatchCount do query: batch_count_query, message: 'Query has been canceled with message: query timed out' ) - expect(subject.call(model, column, batch_size: batch_size, start: 0)).to eq(-1) + expect(subject.call(model, column, batch_size: batch_size, start: 0)).to eq(fallback) end end end describe '#batch_count' do it 'counts table' do - expect(described_class.batch_count(model)).to eq(5) + expect(described_class.batch_count(model)).to eq(model.count) end it 'counts with :id field' do - expect(described_class.batch_count(model, :id)).to eq(5) + expect(described_class.batch_count(model, :id)).to eq(model.count) end it 'counts with "id" field' do - expect(described_class.batch_count(model, 'id')).to eq(5) + expect(described_class.batch_count(model, 'id')).to eq(model.count) end it 'counts with table.id field' do - expect(described_class.batch_count(model, "#{model.table_name}.id")).to eq(5) + expect(described_class.batch_count(model, "#{model.table_name}.id")).to eq(model.count) end it 'counts with Arel column' do - expect(described_class.batch_count(model, model.arel_table[:id])).to eq(5) + expect(described_class.batch_count(model, model.arel_table[:id])).to eq(model.count) end it 'counts table with batch_size 50K' do - expect(described_class.batch_count(model, batch_size: 50_000)).to eq(5) + expect(described_class.batch_count(model, batch_size: 50_000)).to eq(model.count) end it 'will not count table with a batch size less than allowed' do expect(described_class.batch_count(model, batch_size: small_batch_size)).to eq(fallback) end - it 'counts with a small edge case batch_sizes than result' do + it 'produces the same result with different batch sizes' do stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0) - [1, 2, 4, 5, 6].each { |i| expect(described_class.batch_count(model, batch_size: i)).to eq(5) } + [1, 2, 4, 5, 6].each { |i| expect(described_class.batch_count(model, batch_size: i)).to eq(model.count) } end it 'counts with a start and finish' do - expect(described_class.batch_count(model, start: model.minimum(:id), finish: model.maximum(:id))).to eq(5) + expect(described_class.batch_count(model, start: model.minimum(:id), finish: model.maximum(:id))).to eq(model.count) end it 'stops counting when finish value is reached' do @@ -217,6 +217,113 @@ RSpec.describe Gitlab::Database::BatchCount do end end + describe '#batch_count_with_timeout' do + it 'counts table' do + expect(described_class.batch_count_with_timeout(model)).to eq({ status: :completed, count: model.count }) + end + + it 'counts with :id field' do + expect(described_class.batch_count_with_timeout(model, :id)).to eq({ status: :completed, count: model.count }) + end + + it 'counts with "id" field' do + expect(described_class.batch_count_with_timeout(model, 'id')).to eq({ status: :completed, count: model.count }) + end + + it 'counts with table.id field' do + expect(described_class.batch_count_with_timeout(model, "#{model.table_name}.id")).to eq({ status: :completed, count: model.count }) + end + + it 'counts with Arel column' do + expect(described_class.batch_count_with_timeout(model, model.arel_table[:id])).to eq({ status: :completed, count: model.count }) + end + + it 'counts table with batch_size 50K' do + expect(described_class.batch_count_with_timeout(model, batch_size: 50_000)).to eq({ status: :completed, count: model.count }) + end + + it 'will not count table with a batch size less than allowed' do + expect(described_class.batch_count_with_timeout(model, batch_size: small_batch_size)).to eq({ status: :bad_config }) + end + + it 'produces the same result with different batch sizes' do + stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0) + + [1, 2, 4, 5, 6].each { |i| expect(described_class.batch_count_with_timeout(model, batch_size: i)).to eq({ status: :completed, count: model.count }) } + end + + it 'counts with a start and finish' do + expect(described_class.batch_count_with_timeout(model, start: model.minimum(:id), finish: model.maximum(:id))).to eq({ status: :completed, count: model.count }) + end + + it 'stops counting when finish value is reached' do + stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0) + + expect(described_class.batch_count_with_timeout(model, + start: model.minimum(:id), + finish: model.maximum(:id) - 1, # Do not count the last record + batch_size: model.count - 2 # Ensure there are multiple batches + )).to eq({ status: :completed, count: model.count - 1 }) + end + + it 'returns a partial count when timeout elapses' do + stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0) + + expect(::Gitlab::Metrics::System).to receive(:monotonic_time).and_return(1, 10, 300) + + expect( + described_class.batch_count_with_timeout(model, batch_size: 1, timeout: 250.seconds) + ).to eq({ status: :timeout, partial_results: 1, continue_from: model.minimum(:id) + 1 }) + end + + it 'starts counting from a given partial result' do + expect(described_class.batch_count_with_timeout(model, partial_results: 3)).to eq({ status: :completed, count: 3 + model.count }) + end + + it_behaves_like 'when a transaction is open' do + subject { described_class.batch_count_with_timeout(model) } + end + + it_behaves_like 'when batch fetch query is canceled' do + let(:mode) { :itself } + let(:operation) { :count } + let(:operation_args) { nil } + let(:column) { nil } + let(:fallback) { { status: :cancelled } } + + subject { described_class.method(:batch_count_with_timeout) } + end + + context 'disallowed_configurations' do + include_examples 'disallowed configurations', :batch_count do + let(:args) { [Issue] } + let(:default_batch_size) { Gitlab::Database::BatchCounter::DEFAULT_BATCH_SIZE } + end + + it 'raises an error if distinct count is requested' do + expect { described_class.batch_count_with_timeout(model.distinct(column)) }.to raise_error 'Use distinct count for optimized distinct counting' + end + end + + context 'when a relation is grouped' do + let!(:one_more_issue) { create(:issue, author: user, project: model.first.project) } + + before do + stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 1) + end + + context 'count by default column' do + let(:count) do + described_class.batch_count_with_timeout(model.group(column), batch_size: 2) + end + + it 'counts grouped records' do + expect(count).to eq({ status: :completed, count: { user.id => 4, another_user.id => 2 } }) + end + end + end + end + describe '#batch_distinct_count' do it 'counts with column field' do expect(described_class.batch_distinct_count(model, column)).to eq(2) @@ -242,7 +349,7 @@ RSpec.describe Gitlab::Database::BatchCount do expect(described_class.batch_distinct_count(model, column, batch_size: small_batch_size)).to eq(fallback) end - it 'counts with a small edge case batch_sizes than result' do + it 'produces the same result with different batch sizes' do stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0) [1, 2, 4, 5, 6].each { |i| expect(described_class.batch_distinct_count(model, column, batch_size: i)).to eq(2) } @@ -386,56 +493,18 @@ RSpec.describe Gitlab::Database::BatchCount do end describe '#batch_average' do - let(:model) { Issue } let(:column) { :weight } before do - Issue.update_all(weight: 2) - end - - it 'returns the average of values in the given column' do - expect(described_class.batch_average(model, column)).to eq(2) - end - - it 'works when given an Arel column' do - expect(described_class.batch_average(model, model.arel_table[column])).to eq(2) - end - - it 'works with a batch size of 50K' do - expect(described_class.batch_average(model, column, batch_size: 50_000)).to eq(2) - end - - it 'works with start and finish provided' do - expect(described_class.batch_average(model, column, start: model.minimum(:id), finish: model.maximum(:id))).to eq(2) + allow_next_instance_of(Gitlab::Database::BatchAverageCounter) do |instance| + allow(instance).to receive(:count).and_return + end end - it "defaults the batch size to #{Gitlab::Database::BatchCounter::DEFAULT_AVERAGE_BATCH_SIZE}" do - min_id = model.minimum(:id) - relation = instance_double(ActiveRecord::Relation) - allow(model).to receive_message_chain(:select, public_send: relation) - batch_end_id = min_id + calculate_batch_size(Gitlab::Database::BatchCounter::DEFAULT_AVERAGE_BATCH_SIZE) - - expect(relation).to receive(:where).with("id" => min_id..batch_end_id).and_return(double(send: 1)) + it 'calls BatchAverageCounter' do + expect(Gitlab::Database::BatchAverageCounter).to receive(:new).with(model, column).and_call_original described_class.batch_average(model, column) end - - it_behaves_like 'when a transaction is open' do - subject { described_class.batch_average(model, column) } - end - - it_behaves_like 'disallowed configurations', :batch_average do - let(:args) { [model, column] } - let(:default_batch_size) { Gitlab::Database::BatchCounter::DEFAULT_AVERAGE_BATCH_SIZE } - let(:small_batch_size) { Gitlab::Database::BatchCounter::DEFAULT_AVERAGE_BATCH_SIZE - 1 } - end - - it_behaves_like 'when batch fetch query is canceled' do - let(:mode) { :itself } - let(:operation) { :average } - let(:operation_args) { [column] } - - subject { described_class.method(:batch_average) } - end end end diff --git a/spec/lib/gitlab/database/lock_writes_manager_spec.rb b/spec/lib/gitlab/database/lock_writes_manager_spec.rb index eb527d492cf..b1cc8add55a 100644 --- a/spec/lib/gitlab/database/lock_writes_manager_spec.rb +++ b/spec/lib/gitlab/database/lock_writes_manager_spec.rb @@ -6,13 +6,15 @@ RSpec.describe Gitlab::Database::LockWritesManager do let(:connection) { ApplicationRecord.connection } let(:test_table) { '_test_table' } let(:logger) { instance_double(Logger) } + let(:dry_run) { false } subject(:lock_writes_manager) do described_class.new( table_name: test_table, connection: connection, database_name: 'main', - logger: logger + logger: logger, + dry_run: dry_run ) end @@ -27,6 +29,16 @@ RSpec.describe Gitlab::Database::LockWritesManager do SQL end + describe "#table_locked_for_writes?" do + it 'returns false for a table that is not locked for writes' do + expect(subject.table_locked_for_writes?(test_table)).to eq(false) + end + + it 'returns true for a table that is locked for writes' do + expect { subject.lock_writes }.to change { subject.table_locked_for_writes?(test_table) }.from(false).to(true) + end + end + describe '#lock_writes' do it 'prevents any writes on the table' do subject.lock_writes @@ -84,11 +96,57 @@ RSpec.describe Gitlab::Database::LockWritesManager do subject.lock_writes end.to raise_error(ActiveRecord::QueryCanceled) end + + it 'skips the operation if the table is already locked for writes' do + subject.lock_writes + + expect(logger).to receive(:info).with("Skipping lock_writes, because #{test_table} is already locked for writes") + expect(connection).not_to receive(:execute).with(/CREATE TRIGGER/) + + expect do + subject.lock_writes + end.not_to change { + number_of_triggers_on(connection, test_table) + } + end + + context 'when running in dry_run mode' do + let(:dry_run) { true } + + it 'prints the sql statement to the logger' do + expect(logger).to receive(:info).with("Database: 'main', Table: '#{test_table}': Lock Writes") + expected_sql_statement = <<~SQL + CREATE TRIGGER gitlab_schema_write_trigger_for_#{test_table} + BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE + ON #{test_table} + FOR EACH STATEMENT EXECUTE FUNCTION gitlab_schema_prevent_write(); + SQL + expect(logger).to receive(:info).with(expected_sql_statement) + + subject.lock_writes + end + + it 'does not lock the tables for writes' do + subject.lock_writes + + expect do + connection.execute("delete from #{test_table}") + connection.execute("truncate #{test_table}") + end.not_to raise_error + end + end end describe '#unlock_writes' do before do - subject.lock_writes + # Locking the table without the considering the value of dry_run + described_class.new( + table_name: test_table, + connection: connection, + database_name: 'main', + logger: logger, + dry_run: false + ).lock_writes end it 'allows writing on the table again' do @@ -114,6 +172,28 @@ RSpec.describe Gitlab::Database::LockWritesManager do subject.unlock_writes end + + context 'when running in dry_run mode' do + let(:dry_run) { true } + + it 'prints the sql statement to the logger' do + expect(logger).to receive(:info).with("Database: 'main', Table: '#{test_table}': Allow Writes") + expected_sql_statement = <<~SQL + DROP TRIGGER IF EXISTS gitlab_schema_write_trigger_for_#{test_table} ON #{test_table}; + SQL + expect(logger).to receive(:info).with(expected_sql_statement) + + subject.unlock_writes + end + + it 'does not unlock the tables for writes' do + subject.unlock_writes + + expect do + connection.execute("delete from #{test_table}") + end.to raise_error(ActiveRecord::StatementInvalid, /Table: "#{test_table}" is write protected/) + end + end end def number_of_triggers_on(connection, table_name) diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index dd5ad40d8ef..d73b478ee7c 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -667,7 +667,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do column: :user_id, on_delete: :cascade, name: name, - primary_key: :id).and_return(true) + primary_key: :id).and_return(true) expect(model).not_to receive(:execute).with(/ADD CONSTRAINT/) expect(model).to receive(:execute).with(/VALIDATE CONSTRAINT/) diff --git a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb index 9451a6bd34a..3ac483c8ab7 100644 --- a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb +++ b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb @@ -24,7 +24,7 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez connection.execute(<<~SQL) CREATE TABLE #{table_name} ( id bigint primary key not null, - data bigint + data bigint default 0 ); insert into #{table_name} (id) select i from generate_series(1, 1000) g(i); @@ -40,10 +40,12 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez :id, :data, batch_size: 100, job_interval: 5.minutes) # job_interval is skipped when testing - described_class.new(result_dir: result_dir, connection: connection).run_jobs(for_duration: 1.minute) - unmigrated_row_count = define_batchable_model(table_name).where('id != data').count - expect(unmigrated_row_count).to eq(0) + # Expect that running sampling for this migration processes some of the rows. Sampling doesn't run + # over every row in the table, so this does not completely migrate the table. + expect { described_class.new(result_dir: result_dir, connection: connection).run_jobs(for_duration: 1.minute) } + .to change { define_batchable_model(table_name).where('id IS DISTINCT FROM data').count } + .by_at_most(-1) end end @@ -62,7 +64,7 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez described_class.new(result_dir: result_dir, connection: connection).run_jobs(for_duration: 3.minutes) - expect(calls.count).to eq(10) # 1000 rows / batch size 100 = 10 + expect(calls).not_to be_empty end context 'with multiple jobs to run' do @@ -92,4 +94,19 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez end end end + + context 'choosing uniform batches to run' do + subject { described_class.new(result_dir: result_dir, connection: connection) } + + describe '#uniform_fractions' do + it 'generates evenly distributed sequences of fractions' do + received = subject.uniform_fractions.take(9) + expected = [0, 1, 1.0 / 2, 1.0 / 4, 3.0 / 4, 1.0 / 8, 3.0 / 8, 5.0 / 8, 7.0 / 8] + + # All the fraction numerators are small integers, and all denominators are powers of 2, so these + # fit perfectly into floating point numbers with zero loss of precision + expect(received).to eq(expected) + end + end + end end diff --git a/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb b/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb new file mode 100644 index 00000000000..af7d751a404 --- /dev/null +++ b/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb @@ -0,0 +1,246 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition do + include Gitlab::Database::DynamicModelHelpers + include Database::TableSchemaHelpers + + let(:migration_context) { Gitlab::Database::Migration[2.0].new } + + let(:connection) { migration_context.connection } + let(:table_name) { '_test_table_to_partition' } + let(:table_identifier) { "#{connection.current_schema}.#{table_name}" } + let(:partitioning_column) { :partition_number } + let(:partitioning_default) { 1 } + let(:referenced_table_name) { '_test_referenced_table' } + let(:other_referenced_table_name) { '_test_other_referenced_table' } + let(:parent_table_name) { "#{table_name}_parent" } + + let(:model) { define_batchable_model(table_name, connection: connection) } + + let(:parent_model) { define_batchable_model(parent_table_name, connection: connection) } + + let(:converter) do + described_class.new( + migration_context: migration_context, + table_name: table_name, + partitioning_column: partitioning_column, + parent_table_name: parent_table_name, + zero_partition_value: partitioning_default + ) + end + + before do + # Suppress printing migration progress + allow(migration_context).to receive(:puts) + allow(migration_context.connection).to receive(:transaction_open?).and_return(false) + + connection.execute(<<~SQL) + create table #{referenced_table_name} ( + id bigserial primary key not null + ) + SQL + + connection.execute(<<~SQL) + create table #{other_referenced_table_name} ( + id bigserial primary key not null + ) + SQL + + connection.execute(<<~SQL) + insert into #{referenced_table_name} default values; + insert into #{other_referenced_table_name} default values; + SQL + + connection.execute(<<~SQL) + create table #{table_name} ( + id bigserial not null, + #{partitioning_column} bigint not null default #{partitioning_default}, + referenced_id bigint not null references #{referenced_table_name} (id) on delete cascade, + other_referenced_id bigint not null references #{other_referenced_table_name} (id) on delete set null, + primary key (id, #{partitioning_column}) + ) + SQL + + connection.execute(<<~SQL) + insert into #{table_name} (referenced_id, other_referenced_id) + select #{referenced_table_name}.id, #{other_referenced_table_name}.id + from #{referenced_table_name}, #{other_referenced_table_name}; + SQL + end + + describe "#prepare_for_partitioning" do + subject(:prepare) { converter.prepare_for_partitioning } + + it 'adds a check constraint' do + expect { prepare }.to change { + Gitlab::Database::PostgresConstraint + .check_constraints + .by_table_identifier(table_identifier) + .count + }.from(0).to(1) + end + end + + describe '#revert_prepare_for_partitioning' do + before do + converter.prepare_for_partitioning + end + + subject(:revert_prepare) { converter.revert_preparation_for_partitioning } + + it 'removes a check constraint' do + expect { revert_prepare }.to change { + Gitlab::Database::PostgresConstraint + .check_constraints + .by_table_identifier("#{connection.current_schema}.#{table_name}") + .count + }.from(1).to(0) + end + end + + describe "#convert_to_zero_partition" do + subject(:partition) { converter.partition } + + before do + converter.prepare_for_partitioning + end + + context 'when the primary key is incorrect' do + before do + connection.execute(<<~SQL) + alter table #{table_name} drop constraint #{table_name}_pkey; + alter table #{table_name} add constraint #{table_name}_pkey PRIMARY KEY (id); + SQL + end + + it 'throws a reasonable error message' do + expect { partition }.to raise_error(described_class::UnableToPartition, /#{partitioning_column}/) + end + end + + context 'when there is not a supporting check constraint' do + before do + connection.execute(<<~SQL) + alter table #{table_name} drop constraint partitioning_constraint; + SQL + end + + it 'throws a reasonable error message' do + expect { partition }.to raise_error(described_class::UnableToPartition, /constraint /) + end + end + + it 'migrates the table to a partitioned table' do + fks_before = migration_context.foreign_keys(table_name) + + partition + + expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(1) + expect(migration_context.foreign_keys(parent_table_name).map(&:options)).to match_array(fks_before.map(&:options)) + + connection.execute(<<~SQL) + insert into #{table_name} (referenced_id, other_referenced_id) select #{referenced_table_name}.id, #{other_referenced_table_name}.id from #{referenced_table_name}, #{other_referenced_table_name}; + SQL + + # Create a second partition + connection.execute(<<~SQL) + create table #{table_name}2 partition of #{parent_table_name} FOR VALUES IN (2) + SQL + + parent_model.create!(partitioning_column => 2, :referenced_id => 1, :other_referenced_id => 1) + expect(parent_model.pluck(:id)).to match_array([1, 2, 3]) + end + + context 'when an error occurs during the conversion' do + def fail_first_time + # We can't directly use a boolean here, as we need something that will be passed by-reference to the proc + fault_status = { faulted: false } + proc do |m, *args, **kwargs| + next m.call(*args, **kwargs) if fault_status[:faulted] + + fault_status[:faulted] = true + raise 'fault!' + end + end + + def fail_sql_matching(regex) + proc do + allow(migration_context.connection).to receive(:execute).and_call_original + allow(migration_context.connection).to receive(:execute).with(regex).and_wrap_original(&fail_first_time) + end + end + + def fail_adding_fk(from_table, to_table) + proc do + allow(migration_context.connection).to receive(:add_foreign_key).and_call_original + expect(migration_context.connection).to receive(:add_foreign_key).with(from_table, to_table, any_args) + .and_wrap_original(&fail_first_time) + end + end + + where(:case_name, :fault) do + [ + ["creating parent table", lazy { fail_sql_matching(/CREATE/i) }], + ["adding the first foreign key", lazy { fail_adding_fk(parent_table_name, referenced_table_name) }], + ["adding the second foreign key", lazy { fail_adding_fk(parent_table_name, other_referenced_table_name) }], + ["attaching table", lazy { fail_sql_matching(/ATTACH/i) }] + ] + end + + before do + # Set up the fault that we'd like to inject + fault.call + end + + with_them do + it 'recovers from a fault', :aggregate_failures do + expect { converter.partition }.to raise_error(/fault/) + expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(0) + + expect { converter.partition }.not_to raise_error + expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(1) + end + end + end + end + + describe '#revert_conversion_to_zero_partition' do + before do + converter.prepare_for_partitioning + converter.partition + end + + subject(:revert_conversion) { converter.revert_partitioning } + + it 'detaches the partition' do + expect { revert_conversion }.to change { + Gitlab::Database::PostgresPartition + .for_parent_table(parent_table_name).count + }.from(1).to(0) + end + + it 'does not drop the child partition' do + expect { revert_conversion }.not_to change { table_oid(table_name) } + end + + it 'removes the parent table' do + expect { revert_conversion }.to change { table_oid(parent_table_name).present? }.from(true).to(false) + end + + it 're-adds the check constraint' do + expect { revert_conversion }.to change { + Gitlab::Database::PostgresConstraint + .check_constraints + .by_table_identifier(table_identifier) + .count + }.by(1) + end + + it 'moves sequences back to the original table' do + expect { revert_conversion }.to change { converter.send(:sequences_owned_by, table_name).count }.from(0) + .and change { converter.send(:sequences_owned_by, parent_table_name).count }.to(0) + end + end +end diff --git a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb index dca4548a0a3..8027990a546 100644 --- a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb +++ b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb @@ -21,20 +21,11 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table, connection: connection) } let(:connection) { ActiveRecord::Base.connection } - let(:table) { "issues" } + let(:table) { "my_model_example_table" } let(:partitioning_strategy) do double(missing_partitions: partitions, extra_partitions: [], after_adding_partitions: nil) end - before do - allow(connection).to receive(:table_exists?).and_call_original - allow(connection).to receive(:table_exists?).with(table).and_return(true) - allow(connection).to receive(:execute).and_call_original - expect(partitioning_strategy).to receive(:validate_and_fix) - - stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT) - end - let(:partitions) do [ instance_double(Gitlab::Database::Partitioning::TimePartition, table: 'bar', partition_name: 'foo', to_sql: "SELECT 1"), @@ -42,19 +33,63 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do ] end - it 'creates the partition' do - expect(connection).to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE") - expect(connection).to receive(:execute).with(partitions.first.to_sql) - expect(connection).to receive(:execute).with(partitions.second.to_sql) + context 'when the given table is partitioned' do + before do + create_partitioned_table(connection, table) - sync_partitions + allow(connection).to receive(:table_exists?).and_call_original + allow(connection).to receive(:table_exists?).with(table).and_return(true) + allow(connection).to receive(:execute).and_call_original + expect(partitioning_strategy).to receive(:validate_and_fix) + + stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT) + end + + it 'creates the partition' do + expect(connection).to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE") + expect(connection).to receive(:execute).with(partitions.first.to_sql) + expect(connection).to receive(:execute).with(partitions.second.to_sql) + + sync_partitions + end + + context 'with eplicitly provided connection' do + let(:connection) { Ci::ApplicationRecord.connection } + + it 'uses the explicitly provided connection when any' do + skip_if_multiple_databases_not_setup + + expect(connection).to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE") + expect(connection).to receive(:execute).with(partitions.first.to_sql) + expect(connection).to receive(:execute).with(partitions.second.to_sql) + + described_class.new(model, connection: connection).sync_partitions + end + end + + context 'when an error occurs during partition management' do + it 'does not raise an error' do + expect(partitioning_strategy).to receive(:missing_partitions).and_raise('this should never happen (tm)') + + expect { sync_partitions }.not_to raise_error + end + end end - context 'when an error occurs during partition management' do - it 'does not raise an error' do - expect(partitioning_strategy).to receive(:missing_partitions).and_raise('this should never happen (tm)') + context 'when the table is not partitioned' do + let(:table) { 'this_does_not_need_to_be_real_table' } + + it 'does not try creating the partitions' do + expect(connection).not_to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE") + expect(Gitlab::AppLogger).to receive(:warn).with( + { + message: 'Skipping synching partitions', + table_name: table, + connection_name: 'main' + } + ) - expect { sync_partitions }.not_to raise_error + sync_partitions end end end @@ -74,11 +109,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do end before do - connection.execute(<<~SQL) - CREATE TABLE my_model_example_table - (id serial not null, created_at timestamptz not null, primary key (id, created_at)) - PARTITION BY RANGE (created_at); - SQL + create_partitioned_table(connection, 'my_model_example_table') end it 'creates partitions' do @@ -98,6 +129,8 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do end before do + create_partitioned_table(connection, table) + allow(connection).to receive(:table_exists?).and_call_original allow(connection).to receive(:table_exists?).with(table).and_return(true) expect(partitioning_strategy).to receive(:validate_and_fix) @@ -260,4 +293,12 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do expect { described_class.new(my_model).sync_partitions }.to change { has_partition(my_model, 2.months.ago.beginning_of_month) }.from(true).to(false).and(change { num_partitions(my_model) }.by(0)) end end + + def create_partitioned_table(connection, table) + connection.execute(<<~SQL) + CREATE TABLE #{table} + (id serial not null, created_at timestamptz not null, primary key (id, created_at)) + PARTITION BY RANGE (created_at); + SQL + end end diff --git a/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb index 04b9fba5b2f..07c2c6606d8 100644 --- a/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb +++ b/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb @@ -136,7 +136,7 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy do end context 'when some partitions are true for detach_partition_if' do - let(:detach_partition_if) { ->(p) { p != 5 } } + let(:detach_partition_if) { ->(p) { p.value != 5 } } it 'is the leading set of partitions before that value' do # should not contain partition 2 since it's the default value for the partition column @@ -181,9 +181,10 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy do Class.new(ApplicationRecord) do include PartitionedTable - partitioned_by :partition, strategy: :sliding_list, - next_partition_if: proc { false }, - detach_partition_if: proc { false } + partitioned_by :partition, + strategy: :sliding_list, + next_partition_if: proc { false }, + detach_partition_if: proc { false } end end.to raise_error(/ignored_columns/) end @@ -195,7 +196,8 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy do self.ignored_columns = [:partition] - partitioned_by :partition, strategy: :sliding_list, + partitioned_by :partition, + strategy: :sliding_list, next_partition_if: proc { false }, detach_partition_if: proc { false } end @@ -221,7 +223,8 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy do def self.detach_partition_if_wrapper(...) detach_partition?(...) end - partitioned_by :partition, strategy: :sliding_list, + partitioned_by :partition, + strategy: :sliding_list, next_partition_if: method(:next_partition_if_wrapper), detach_partition_if: method(:detach_partition_if_wrapper) diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb index 3072c413246..1885e84ac4c 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb @@ -97,7 +97,8 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition end it 'marks each job record as succeeded after processing' do - create(:background_migration_job, class_name: "::#{described_class.name}", + create(:background_migration_job, + class_name: "::#{described_class.name}", arguments: [source1.id, source3.id, source_table, destination_table, unique_key]) expect(::Gitlab::Database::BackgroundMigrationJob).to receive(:mark_all_as_succeeded).and_call_original @@ -108,7 +109,8 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition end it 'returns the number of job records marked as succeeded' do - create(:background_migration_job, class_name: "::#{described_class.name}", + create(:background_migration_job, + class_name: "::#{described_class.name}", arguments: [source1.id, source3.id, source_table, destination_table, unique_key]) jobs_updated = backfill_job.perform(source1.id, source3.id, source_table, destination_table, unique_key) diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb index edb8ae36c45..7465f69b87c 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb @@ -26,6 +26,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do CREATE TABLE #{table_name} ( id serial NOT NULL, created_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL, PRIMARY KEY (id, created_at) ) PARTITION BY RANGE (created_at); @@ -204,4 +205,30 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do end end end + + describe '#find_duplicate_indexes' do + context 'when duplicate and non-duplicate indexes exist' do + let(:nonduplicate_column_name) { 'updated_at' } + let(:nonduplicate_index_name) { 'updated_at_idx' } + let(:duplicate_column_name) { 'created_at' } + let(:duplicate_index_name1) { 'created_at_idx' } + let(:duplicate_index_name2) { 'index_on_created_at' } + + before do + connection.execute(<<~SQL) + CREATE INDEX #{nonduplicate_index_name} ON #{table_name} (#{nonduplicate_column_name}); + CREATE INDEX #{duplicate_index_name1} ON #{table_name} (#{duplicate_column_name}); + CREATE INDEX #{duplicate_index_name2} ON #{table_name} (#{duplicate_column_name}); + SQL + end + + subject do + migration.find_duplicate_indexes(table_name) + end + + it 'finds the duplicate index' do + expect(subject).to match_array([match_array([duplicate_index_name1, duplicate_index_name2])]) + end + end + end end diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb index 1026b4370a5..8bb9ad2737a 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb @@ -41,6 +41,76 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe allow(migration).to receive(:assert_table_is_allowed) end + context 'list partitioning conversion helpers' do + shared_examples_for 'delegates to ConvertTableToFirstListPartition' do + it 'throws an error if in a transaction' do + allow(migration).to receive(:transaction_open?).and_return(true) + expect { migrate }.to raise_error(/cannot be run inside a transaction/) + end + + it 'delegates to a method on ConvertTableToFirstListPartition' do + expect_next_instance_of(Gitlab::Database::Partitioning::ConvertTableToFirstListPartition, + migration_context: migration, + table_name: source_table, + parent_table_name: partitioned_table, + partitioning_column: partition_column, + zero_partition_value: min_date) do |converter| + expect(converter).to receive(expected_method) + end + + migrate + end + end + + describe '#convert_table_to_first_list_partition' do + it_behaves_like 'delegates to ConvertTableToFirstListPartition' do + let(:expected_method) { :partition } + let(:migrate) do + migration.convert_table_to_first_list_partition(table_name: source_table, + partitioning_column: partition_column, + parent_table_name: partitioned_table, + initial_partitioning_value: min_date) + end + end + end + + describe '#revert_converting_table_to_first_list_partition' do + it_behaves_like 'delegates to ConvertTableToFirstListPartition' do + let(:expected_method) { :revert_partitioning } + let(:migrate) do + migration.revert_converting_table_to_first_list_partition(table_name: source_table, + partitioning_column: partition_column, + parent_table_name: partitioned_table, + initial_partitioning_value: min_date) + end + end + end + + describe '#prepare_constraint_for_list_partitioning' do + it_behaves_like 'delegates to ConvertTableToFirstListPartition' do + let(:expected_method) { :prepare_for_partitioning } + let(:migrate) do + migration.prepare_constraint_for_list_partitioning(table_name: source_table, + partitioning_column: partition_column, + parent_table_name: partitioned_table, + initial_partitioning_value: min_date) + end + end + end + + describe '#revert_preparing_constraint_for_list_partitioning' do + it_behaves_like 'delegates to ConvertTableToFirstListPartition' do + let(:expected_method) { :revert_preparation_for_partitioning } + let(:migrate) do + migration.revert_preparing_constraint_for_list_partitioning(table_name: source_table, + partitioning_column: partition_column, + parent_table_name: partitioned_table, + initial_partitioning_value: min_date) + end + end + end + end + describe '#partition_table_by_date' do let(:partition_column) { 'created_at' } let(:old_primary_key) { 'id' } diff --git a/spec/lib/gitlab/database/partitioning_spec.rb b/spec/lib/gitlab/database/partitioning_spec.rb index 36c8b0811fe..94cdbfb2328 100644 --- a/spec/lib/gitlab/database/partitioning_spec.rb +++ b/spec/lib/gitlab/database/partitioning_spec.rb @@ -56,7 +56,7 @@ RSpec.describe Gitlab::Database::Partitioning do end it 'does not call sync_partitions' do - expect(described_class).to receive(:sync_partitions).never + expect(described_class).not_to receive(:sync_partitions) described_class.sync_partitions_ignore_db_error end @@ -64,6 +64,7 @@ RSpec.describe Gitlab::Database::Partitioning do end describe '.sync_partitions' do + let(:ci_connection) { Ci::ApplicationRecord.connection } let(:table_names) { %w[partitioning_test1 partitioning_test2] } let(:models) do table_names.map do |table_name| @@ -94,6 +95,38 @@ RSpec.describe Gitlab::Database::Partitioning do .and change { find_partitions(table_names.last).size }.from(0) end + context 'with multiple databases' do + before do + table_names.each do |table_name| + ci_connection.execute("DROP TABLE IF EXISTS #{table_name}") + + ci_connection.execute(<<~SQL) + CREATE TABLE #{table_name} ( + id serial not null, + created_at timestamptz not null, + PRIMARY KEY (id, created_at)) + PARTITION BY RANGE (created_at); + SQL + end + end + + after do + table_names.each do |table_name| + ci_connection.execute("DROP TABLE IF EXISTS #{table_name}") + end + end + + it 'creates partitions in each database' do + skip_if_multiple_databases_not_setup + + expect { described_class.sync_partitions(models) } + .to change { find_partitions(table_names.first, conn: connection).size }.from(0) + .and change { find_partitions(table_names.last, conn: connection).size }.from(0) + .and change { find_partitions(table_names.first, conn: ci_connection).size }.from(0) + .and change { find_partitions(table_names.last, conn: ci_connection).size }.from(0) + end + end + context 'when no partitioned models are given' do it 'manages partitions for each registered model' do described_class.register_models([models.first]) @@ -111,16 +144,44 @@ RSpec.describe Gitlab::Database::Partitioning do end context 'when only a specific database is requested' do + let(:ci_model) do + Class.new(Ci::ApplicationRecord) do + include PartitionedTable + + self.table_name = 'partitioning_test3' + partitioned_by :created_at, strategy: :monthly + end + end + before do - allow(models.first).to receive_message_chain('connection_db_config.name').and_return('main') - allow(models.last).to receive_message_chain('connection_db_config.name').and_return('ci') + (table_names + ['partitioning_test3']).each do |table_name| + ci_connection.execute("DROP TABLE IF EXISTS #{table_name}") + + ci_connection.execute(<<~SQL) + CREATE TABLE #{table_name} ( + id serial not null, + created_at timestamptz not null, + PRIMARY KEY (id, created_at)) + PARTITION BY RANGE (created_at); + SQL + end + end + + after do + (table_names + ['partitioning_test3']).each do |table_name| + ci_connection.execute("DROP TABLE IF EXISTS #{table_name}") + end end it 'manages partitions for models for the given database', :aggregate_failures do - expect { described_class.sync_partitions(models, only_on: 'ci') } - .to change { find_partitions(table_names.last).size }.from(0) + skip_if_multiple_databases_not_setup + + expect { described_class.sync_partitions([models.first, ci_model], only_on: 'ci') } + .to change { find_partitions(ci_model.table_name, conn: ci_connection).size }.from(0) - expect(find_partitions(table_names.first).size).to eq(0) + expect(find_partitions(models.first.table_name).size).to eq(0) + expect(find_partitions(models.first.table_name, conn: ci_connection).size).to eq(0) + expect(find_partitions(ci_model.table_name).size).to eq(0) end end end diff --git a/spec/lib/gitlab/database/postgres_constraint_spec.rb b/spec/lib/gitlab/database/postgres_constraint_spec.rb new file mode 100644 index 00000000000..75084a69115 --- /dev/null +++ b/spec/lib/gitlab/database/postgres_constraint_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::PostgresConstraint, type: :model do + # PostgresConstraint does not `behaves_like 'a postgres model'` because it does not correspond 1-1 with a single entry + # in pg_class + let(:schema) { ActiveRecord::Base.connection.current_schema } + let(:table_name) { '_test_table' } + let(:table_identifier) { "#{schema}.#{table_name}" } + let(:referenced_name) { '_test_referenced' } + let(:check_constraint_a_positive) { 'check_constraint_a_positive' } + let(:check_constraint_a_gt_b) { 'check_constraint_a_gt_b' } + let(:invalid_constraint_a) { 'check_constraint_b_positive_invalid' } + let(:unique_constraint_a) { "#{table_name}_a_key" } + + before do + ActiveRecord::Base.connection.execute(<<~SQL) + create table #{referenced_name} ( + id bigserial primary key not null + ); + + create table #{table_name} ( + id bigserial not null, + referenced_id bigint not null references #{referenced_name}(id), + a integer unique, + b integer, + primary key (id, referenced_id), + constraint #{check_constraint_a_positive} check (a > 0), + constraint #{check_constraint_a_gt_b} check (a > b) + ); + + alter table #{table_name} add constraint #{invalid_constraint_a} CHECK (a > 1) NOT VALID; + SQL + end + + describe '#by_table_identifier' do + subject(:constraints_for_table) { described_class.by_table_identifier(table_identifier) } + + it 'includes all constraints on the table' do + all_constraints_for_table = described_class.all.to_a.select { |c| c.table_identifier == table_identifier } + expect(all_constraints_for_table.map(&:oid)).to match_array(constraints_for_table.pluck(:oid)) + end + + it 'throws an error if the format is incorrect' do + expect { described_class.by_table_identifier('not-an-identifier') }.to raise_error(ArgumentError) + end + end + + describe '#check_constraints' do + subject(:check_constraints) { described_class.check_constraints.by_table_identifier(table_identifier) } + + it 'finds check constraints for the table' do + expect(check_constraints.map(&:name)).to contain_exactly(check_constraint_a_positive, + check_constraint_a_gt_b, + invalid_constraint_a) + end + + it 'includes columns for the check constraints', :aggregate_failures do + expect(check_constraints.find_by(name: check_constraint_a_positive).column_names).to contain_exactly('a') + expect(check_constraints.find_by(name: check_constraint_a_gt_b).column_names).to contain_exactly('a', 'b') + end + end + + describe "#valid" do + subject(:valid_constraint_names) { described_class.valid.by_table_identifier(table_identifier).pluck(:name) } + + let(:all_constraint_names) { described_class.by_table_identifier(table_identifier).pluck(:name) } + + it 'excludes invalid constraints' do + expect(valid_constraint_names).not_to include(invalid_constraint_a) + expect(valid_constraint_names).to match_array(all_constraint_names - [invalid_constraint_a]) + end + end + + describe '#primary_key_constraints' do + subject(:pk_constraints) { described_class.primary_key_constraints.by_table_identifier(table_identifier) } + + it 'finds the primary key constraint for the table' do + expect(pk_constraints.count).to eq(1) + expect(pk_constraints.first.constraint_type).to eq('p') + end + + it 'finds the columns in the primary key constraint' do + constraint = pk_constraints.first + expect(constraint.column_names).to contain_exactly('id', 'referenced_id') + end + end + + describe '#unique_constraints' do + subject(:unique_constraints) { described_class.unique_constraints.by_table_identifier(table_identifier) } + + it 'finds the unique constraints for the table' do + expect(unique_constraints.pluck(:name)).to contain_exactly(unique_constraint_a) + end + end + + describe '#primary_or_unique_constraints' do + subject(:pk_or_unique_constraints) do + described_class.primary_or_unique_constraints.by_table_identifier(table_identifier) + end + + it 'finds primary and unique constraints' do + expect(pk_or_unique_constraints.pluck(:name)).to contain_exactly("#{table_name}_pkey", unique_constraint_a) + end + end + + describe '#including_column' do + it 'only matches constraints on the given column' do + constraints_on_a = described_class.by_table_identifier(table_identifier).including_column('a').map(&:name) + expect(constraints_on_a).to contain_exactly(check_constraint_a_positive, check_constraint_a_gt_b, + unique_constraint_a, invalid_constraint_a) + end + end + + describe '#not_including_column' do + it 'only matches constraints not including the given column' do + constraints_not_on_a = described_class.by_table_identifier(table_identifier).not_including_column('a').map(&:name) + + expect(constraints_not_on_a).to contain_exactly("#{table_name}_pkey", "#{table_name}_referenced_id_fkey") + end + end +end diff --git a/spec/lib/gitlab/database/query_analyzers/ci/partitioning_analyzer_spec.rb b/spec/lib/gitlab/database/query_analyzers/ci/partitioning_analyzer_spec.rb new file mode 100644 index 00000000000..ef7c7965c09 --- /dev/null +++ b/spec/lib/gitlab/database/query_analyzers/ci/partitioning_analyzer_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::QueryAnalyzers::Ci::PartitioningAnalyzer, query_analyzers: false do + let(:analyzer) { described_class } + + before do + allow(Gitlab::Database::QueryAnalyzer.instance).to receive(:all_analyzers).and_return([analyzer]) + end + + context 'when ci_partitioning_analyze_queries is disabled' do + before do + stub_feature_flags(ci_partitioning_analyze_queries: false) + end + + it 'does not analyze the query' do + expect(analyzer).not_to receive(:analyze) + + process_sql(Ci::BuildMetadata, "SELECT 1 FROM ci_builds_metadata") + end + end + + context 'when ci_partitioning_analyze_queries is enabled' do + context 'when analyzing targeted tables' do + described_class::ENABLED_TABLES.each do |enabled_table| + context 'when querying a non routing table' do + it 'tracks exception' do + expect(::Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) + process_sql(Ci::ApplicationRecord, "SELECT 1 FROM #{enabled_table}") + end + + it 'raises RoutingTableNotUsedError' do + expect { process_sql(Ci::ApplicationRecord, "SELECT 1 FROM #{enabled_table}") } + .to raise_error(described_class::RoutingTableNotUsedError) + end + end + end + + context 'when updating a record' do + it 'raises RoutingTableNotUsedError' do + expect { process_sql(Ci::BuildMetadata, "UPDATE ci_builds_metadata SET id = 1") } + .to raise_error(described_class::RoutingTableNotUsedError) + end + end + + context 'when inserting a record' do + it 'raises RoutingTableNotUsedError' do + expect { process_sql(Ci::BuildMetadata, "INSERT INTO ci_builds_metadata (id) VALUES(1)") } + .to raise_error(described_class::RoutingTableNotUsedError) + end + end + end + + context 'when analyzing non targeted table' do + it 'does not raise error' do + expect { process_sql(Ci::BuildMetadata, "SELECT 1 FROM projects") } + .not_to raise_error + end + end + + context 'when querying a routing table' do + it 'does not raise error' do + expect { process_sql(Ci::BuildMetadata, "SELECT 1 FROM p_ci_builds_metadata") } + .not_to raise_error + end + end + end + + private + + def process_sql(model, sql) + Gitlab::Database::QueryAnalyzer.instance.within do + # Skip load balancer and retrieve connection assigned to model + Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection) + end + end +end diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb index 495e953f993..4c98185e780 100644 --- a/spec/lib/gitlab/database/reindexing_spec.rb +++ b/spec/lib/gitlab/database/reindexing_spec.rb @@ -46,25 +46,11 @@ RSpec.describe Gitlab::Database::Reindexing do end end - context 'when async index destruction is enabled' do - it 'executes async index destruction prior to any reindexing actions' do - stub_feature_flags(database_async_index_destruction: true) + it 'executes async index destruction prior to any reindexing actions' do + expect(Gitlab::Database::AsyncIndexes).to receive(:drop_pending_indexes!).ordered.exactly(databases_count).times + expect(described_class).to receive(:automatic_reindexing).ordered.exactly(databases_count).times - expect(Gitlab::Database::AsyncIndexes).to receive(:drop_pending_indexes!).ordered.exactly(databases_count).times - expect(described_class).to receive(:automatic_reindexing).ordered.exactly(databases_count).times - - described_class.invoke - end - end - - context 'when async index destruction is disabled' do - it 'does not execute async index destruction' do - stub_feature_flags(database_async_index_destruction: false) - - expect(Gitlab::Database::AsyncIndexes).not_to receive(:drop_pending_indexes!) - - described_class.invoke - end + described_class.invoke end context 'calls automatic reindexing' do diff --git a/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb b/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb new file mode 100644 index 00000000000..97abd6d23bd --- /dev/null +++ b/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::TablesSortedByForeignKeys do + let(:connection) { ApplicationRecord.connection } + let(:tables) { %w[_test_gitlab_main_items _test_gitlab_main_references] } + + subject do + described_class.new(connection, tables).execute + end + + before do + statement = <<~SQL + CREATE TABLE _test_gitlab_main_items (id serial NOT NULL PRIMARY KEY); + + CREATE TABLE _test_gitlab_main_references ( + id serial NOT NULL PRIMARY KEY, + item_id BIGINT NOT NULL, + CONSTRAINT fk_constrained_1 FOREIGN KEY(item_id) REFERENCES _test_gitlab_main_items(id) + ); + SQL + connection.execute(statement) + end + + describe '#execute' do + it 'returns the tables sorted by the foreign keys dependency' do + expect(subject).to eq([['_test_gitlab_main_references'], ['_test_gitlab_main_items']]) + end + + it 'returns both tables together if they are strongly connected' do + statement = <<~SQL + ALTER TABLE _test_gitlab_main_items ADD COLUMN reference_id BIGINT + REFERENCES _test_gitlab_main_references(id) + SQL + connection.execute(statement) + + expect(subject).to eq([tables]) + end + end +end diff --git a/spec/lib/gitlab/database/tables_truncate_spec.rb b/spec/lib/gitlab/database/tables_truncate_spec.rb new file mode 100644 index 00000000000..01af9efd782 --- /dev/null +++ b/spec/lib/gitlab/database/tables_truncate_spec.rb @@ -0,0 +1,257 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_base, + :suppress_gitlab_schemas_validate_connection do + include MigrationsHelpers + + let(:logger) { instance_double(Logger) } + let(:dry_run) { false } + let(:until_table) { nil } + let(:min_batch_size) { 1 } + let(:main_connection) { ApplicationRecord.connection } + let(:ci_connection) { Ci::ApplicationRecord.connection } + let(:test_gitlab_main_table) { '_test_gitlab_main_table' } + let(:test_gitlab_ci_table) { '_test_gitlab_ci_table' } + + # Main Database + let(:main_db_main_item_model) { table("_test_gitlab_main_items", database: "main") } + let(:main_db_main_reference_model) { table("_test_gitlab_main_references", database: "main") } + let(:main_db_ci_item_model) { table("_test_gitlab_ci_items", database: "main") } + let(:main_db_ci_reference_model) { table("_test_gitlab_ci_references", database: "main") } + let(:main_db_shared_item_model) { table("_test_gitlab_shared_items", database: "main") } + # CI Database + let(:ci_db_main_item_model) { table("_test_gitlab_main_items", database: "ci") } + let(:ci_db_main_reference_model) { table("_test_gitlab_main_references", database: "ci") } + let(:ci_db_ci_item_model) { table("_test_gitlab_ci_items", database: "ci") } + let(:ci_db_ci_reference_model) { table("_test_gitlab_ci_references", database: "ci") } + let(:ci_db_shared_item_model) { table("_test_gitlab_shared_items", database: "ci") } + + subject(:truncate_legacy_tables) do + described_class.new( + database_name: database_name, + min_batch_size: min_batch_size, + logger: logger, + dry_run: dry_run, + until_table: until_table + ).execute + end + + shared_examples 'truncating legacy tables on a database' do + before do + skip_if_multiple_databases_not_setup + + # Creating some test tables on the main database + main_tables_sql = <<~SQL + CREATE TABLE _test_gitlab_main_items (id serial NOT NULL PRIMARY KEY); + + CREATE TABLE _test_gitlab_main_references ( + id serial NOT NULL PRIMARY KEY, + item_id BIGINT NOT NULL, + CONSTRAINT fk_constrained_1 FOREIGN KEY(item_id) REFERENCES _test_gitlab_main_items(id) + ); + SQL + + main_connection.execute(main_tables_sql) + ci_connection.execute(main_tables_sql) + + ci_tables_sql = <<~SQL + CREATE TABLE _test_gitlab_ci_items (id serial NOT NULL PRIMARY KEY); + + CREATE TABLE _test_gitlab_ci_references ( + id serial NOT NULL PRIMARY KEY, + item_id BIGINT NOT NULL, + CONSTRAINT fk_constrained_1 FOREIGN KEY(item_id) REFERENCES _test_gitlab_ci_items(id) + ); + SQL + + main_connection.execute(ci_tables_sql) + ci_connection.execute(ci_tables_sql) + + internal_tables_sql = <<~SQL + CREATE TABLE _test_gitlab_shared_items (id serial NOT NULL PRIMARY KEY); + SQL + + main_connection.execute(internal_tables_sql) + ci_connection.execute(internal_tables_sql) + + # Filling the tables + 5.times do |i| + # Main Database + main_db_main_item_model.create!(id: i) + main_db_main_reference_model.create!(item_id: i) + main_db_ci_item_model.create!(id: i) + main_db_ci_reference_model.create!(item_id: i) + main_db_shared_item_model.create!(id: i) + # CI Database + ci_db_main_item_model.create!(id: i) + ci_db_main_reference_model.create!(item_id: i) + ci_db_ci_item_model.create!(id: i) + ci_db_ci_reference_model.create!(item_id: i) + ci_db_shared_item_model.create!(id: i) + end + + allow(Gitlab::Database::GitlabSchema).to receive(:tables_to_schema).and_return( + { + "_test_gitlab_main_items" => :gitlab_main, + "_test_gitlab_main_references" => :gitlab_main, + "_test_gitlab_ci_items" => :gitlab_ci, + "_test_gitlab_ci_references" => :gitlab_ci, + "_test_gitlab_shared_items" => :gitlab_shared, + "_test_gitlab_geo_items" => :gitlab_geo + } + ) + + allow(logger).to receive(:info).with(any_args) + end + + context 'when the truncated tables are not locked for writes' do + it 'raises an error that the tables are not locked for writes' do + error_message = /is not locked for writes. Run the rake task gitlab:db:lock_writes first/ + expect { truncate_legacy_tables }.to raise_error(error_message) + end + end + + context 'when the truncated tables are locked for writes' do + before do + legacy_tables_models.map(&:table_name).each do |table| + Gitlab::Database::LockWritesManager.new( + table_name: table, + connection: connection, + database_name: database_name + ).lock_writes + end + end + + it 'truncates the legacy tables' do + old_counts = legacy_tables_models.map(&:count) + expect do + truncate_legacy_tables + end.to change { legacy_tables_models.map(&:count) }.from(old_counts).to([0] * legacy_tables_models.length) + end + + it 'does not affect the other tables' do + expect do + truncate_legacy_tables + end.not_to change { other_tables_models.map(&:count) } + end + + it 'logs the sql statements to the logger' do + expect(logger).to receive(:info).with("SET LOCAL lock_timeout = 0") + expect(logger).to receive(:info).with("SET LOCAL statement_timeout = 0") + expect(logger).to receive(:info) + .with(/TRUNCATE TABLE #{legacy_tables_models.map(&:table_name).sort.join(', ')} RESTRICT/) + truncate_legacy_tables + end + + context 'when running in dry_run mode' do + let(:dry_run) { true } + + it 'does not truncate the legacy tables if running in dry run mode' do + legacy_tables_models = [main_db_ci_reference_model, main_db_ci_reference_model] + expect do + truncate_legacy_tables + end.not_to change { legacy_tables_models.map(&:count) } + end + end + + context 'when passing until_table parameter' do + context 'with a table that exists' do + let(:until_table) { referencing_table_model.table_name } + + it 'only truncates until the table specified' do + expect do + truncate_legacy_tables + end.to change(referencing_table_model, :count).by(-5) + .and change(referenced_table_model, :count).by(0) + end + end + + context 'with a table that does not exist' do + let(:until_table) { 'foobar' } + + it 'raises an error if the specified table does not exist' do + expect do + truncate_legacy_tables + end.to raise_error(/The table 'foobar' is not within the truncated tables/) + end + end + end + + context 'with geo configured' do + let(:geo_connection) { Gitlab::Database.database_base_models[:geo].connection } + + before do + skip unless geo_configured? + geo_connection.execute('CREATE TABLE _test_gitlab_geo_items (id serial NOT NULL PRIMARY KEY)') + geo_connection.execute('INSERT INTO _test_gitlab_geo_items VALUES(generate_series(1, 50))') + end + + it 'does not truncate gitlab_geo tables' do + expect do + truncate_legacy_tables + end.not_to change { geo_connection.select_value("select count(*) from _test_gitlab_geo_items") } + end + end + end + end + + context 'when truncating gitlab_ci tables on the main database' do + let(:connection) { ApplicationRecord.connection } + let(:database_name) { "main" } + let(:legacy_tables_models) { [main_db_ci_item_model, main_db_ci_reference_model] } + let(:referencing_table_model) { main_db_ci_reference_model } + let(:referenced_table_model) { main_db_ci_item_model } + let(:other_tables_models) do + [ + main_db_main_item_model, main_db_main_reference_model, + ci_db_ci_item_model, ci_db_ci_reference_model, + ci_db_main_item_model, ci_db_main_reference_model, + main_db_shared_item_model, ci_db_shared_item_model + ] + end + + it_behaves_like 'truncating legacy tables on a database' + end + + context 'when truncating gitlab_main tables on the ci database' do + let(:connection) { Ci::ApplicationRecord.connection } + let(:database_name) { "ci" } + let(:legacy_tables_models) { [ci_db_main_item_model, ci_db_main_reference_model] } + let(:referencing_table_model) { ci_db_main_reference_model } + let(:referenced_table_model) { ci_db_main_item_model } + let(:other_tables_models) do + [ + main_db_main_item_model, main_db_main_reference_model, + ci_db_ci_item_model, ci_db_ci_reference_model, + main_db_ci_item_model, main_db_ci_reference_model, + main_db_shared_item_model, ci_db_shared_item_model + ] + end + + it_behaves_like 'truncating legacy tables on a database' + end + + context 'when running in a single database mode' do + before do + skip_if_multiple_databases_are_setup + end + + it 'raises an error when truncating the main database that it is a single database setup' do + expect do + described_class.new(database_name: 'main', min_batch_size: min_batch_size).execute + end.to raise_error(/Cannot truncate legacy tables in single-db setup/) + end + + it 'raises an error when truncating the ci database that it is a single database setup' do + expect do + described_class.new(database_name: 'ci', min_batch_size: min_batch_size).execute + end.to raise_error(/Cannot truncate legacy tables in single-db setup/) + end + end + + def geo_configured? + !!ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'geo') + end +end diff --git a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb index 6601b6658d5..ad91320c6eb 100644 --- a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb +++ b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb @@ -181,8 +181,8 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService let(:existing_project) { create(:project, namespace: existing_group) } before do - application_setting.update!(instance_administrators_group_id: existing_group.id, - self_monitoring_project_id: existing_project.id) + application_setting.update!( + instance_administrators_group_id: existing_group.id, self_monitoring_project_id: existing_project.id) end it 'returns success' do diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 452a662bdcb..c893bca9e62 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -237,6 +237,26 @@ RSpec.describe Gitlab::Database do end end + it 'does return a valid schema for a replica connection' do + with_replica_pool_for(ActiveRecord::Base) do |main_replica_pool| + expect(described_class.gitlab_schemas_for_connection(main_replica_pool.connection)).to include(:gitlab_main, :gitlab_shared) + end + + with_replica_pool_for(Ci::ApplicationRecord) do |ci_replica_pool| + expect(described_class.gitlab_schemas_for_connection(ci_replica_pool.connection)).to include(:gitlab_ci, :gitlab_shared) + end + end + + def with_replica_pool_for(base_model) + config = Gitlab::Database::LoadBalancing::Configuration.new(base_model, [base_model.connection_pool.db_config.host]) + lb = Gitlab::Database::LoadBalancing::LoadBalancer.new(config) + pool = lb.create_replica_connection_pool(1) + + yield pool + ensure + pool&.disconnect! + end + context "when there's CI connection", :request_store do before do skip_if_multiple_databases_not_setup diff --git a/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb index 15f580a3a60..47d09e7a165 100644 --- a/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb +++ b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::DependencyLinker::Parser::Gemfile do describe '#parse' do diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb index 2daa8df815d..8feab0f8017 100644 --- a/spec/lib/gitlab/dependency_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::DependencyLinker do describe '.link' do diff --git a/spec/lib/gitlab/diff/file_collection_sorter_spec.rb b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb index 9ba9271cefc..ca9c156c1ad 100644 --- a/spec/lib/gitlab/diff/file_collection_sorter_spec.rb +++ b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Diff::FileCollectionSorter do let(:diffs) do diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb index 1d1ffc8c275..53e74748234 100644 --- a/spec/lib/gitlab/diff/highlight_cache_spec.rb +++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb @@ -9,33 +9,33 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do { ".gitignore-false-false-false" => [{ line_code: nil, rich_text: nil, text: "@@ -17,3 +17,4 @@ rerun.txt", type: "match", index: 0, old_pos: 17, new_pos: 17 }, { line_code: "a5cc2925ca8258af241be7e5b0381edf30266302_17_17", - rich_text: " <span id=\"LC17\" class=\"line\" lang=\"plaintext\">pickle-email-*.html</span>\n", - text: " pickle-email-*.html", - type: nil, - index: 1, - old_pos: 17, - new_pos: 17 }, + rich_text: " <span id=\"LC17\" class=\"line\" lang=\"plaintext\">pickle-email-*.html</span>\n", + text: " pickle-email-*.html", + type: nil, + index: 1, + old_pos: 17, + new_pos: 17 }, { line_code: "a5cc2925ca8258af241be7e5b0381edf30266302_18_18", - rich_text: " <span id=\"LC18\" class=\"line\" lang=\"plaintext\">.project</span>\n", - text: " .project", - type: nil, - index: 2, - old_pos: 18, - new_pos: 18 }, + rich_text: " <span id=\"LC18\" class=\"line\" lang=\"plaintext\">.project</span>\n", + text: " .project", + type: nil, + index: 2, + old_pos: 18, + new_pos: 18 }, { line_code: "a5cc2925ca8258af241be7e5b0381edf30266302_19_19", - rich_text: " <span id=\"LC19\" class=\"line\" lang=\"plaintext\">config/initializers/secret_token.rb</span>\n", - text: " config/initializers/secret_token.rb", - type: nil, - index: 3, - old_pos: 19, - new_pos: 19 }, + rich_text: " <span id=\"LC19\" class=\"line\" lang=\"plaintext\">config/initializers/secret_token.rb</span>\n", + text: " config/initializers/secret_token.rb", + type: nil, + index: 3, + old_pos: 19, + new_pos: 19 }, { line_code: "a5cc2925ca8258af241be7e5b0381edf30266302_20_20", - rich_text: "+<span id=\"LC20\" class=\"line\" lang=\"plaintext\">.DS_Store</span>", - text: "+.DS_Store", - type: "new", - index: 4, - old_pos: 20, - new_pos: 20 }] } + rich_text: "+<span id=\"LC20\" class=\"line\" lang=\"plaintext\">.DS_Store</span>", + text: "+.DS_Store", + type: "new", + index: 4, + old_pos: 20, + new_pos: 20 }] } end let(:cache_key) { cache.key } @@ -109,23 +109,59 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do end shared_examples 'caches missing entries' do - it 'filters the key/value list of entries to be caches for each invocation' do - expect(cache).to receive(:write_to_redis_hash) - .with(hash_including(*paths)) - .once - .and_call_original - - Gitlab::Redis::Cache.with do |redis| - expect(redis).to receive(:expire).with(cache.key, described_class::EXPIRATION) + where(:expiration_period, :renewable_expiration_ff, :short_renewable_expiration_ff) do + [ + [1.day, false, true], + [1.day, false, false], + [1.hour, true, true], + [8.hours, true, false] + ] + end + + with_them do + before do + stub_feature_flags( + highlight_diffs_renewable_expiration: renewable_expiration_ff, + highlight_diffs_short_renewable_expiration: short_renewable_expiration_ff + ) end - 2.times { cache.write_if_empty } - end + it 'filters the key/value list of entries to be caches for each invocation' do + expect(cache).to receive(:write_to_redis_hash) + .with(hash_including(*paths)) + .once + .and_call_original - it 'reads from cache once' do - expect(cache).to receive(:read_cache).once.and_call_original + 2.times { cache.write_if_empty } + end - cache.write_if_empty + it 'reads from cache once' do + expect(cache).to receive(:read_cache).once.and_call_original + + cache.write_if_empty + end + + it 'refreshes TTL of the key on read' do + cache.write_if_empty + + time_until_expire = 30.minutes + + Gitlab::Redis::Cache.with do |redis| + # Emulate that a key is going to expire soon + redis.expire(cache.key, time_until_expire) + + expect(redis.ttl(cache.key)).to be <= time_until_expire + + cache.send(:read_cache) + + if renewable_expiration_ff + expect(redis.ttl(cache.key)).to be > time_until_expire + expect(redis.ttl(cache.key)).to be_within(1.minute).of(expiration_period) + else + expect(redis.ttl(cache.key)).to be <= time_until_expire + end + end + end end end diff --git a/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb index 3670074cc21..87d47e36f6a 100644 --- a/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Diff::InlineDiffMarkdownMarker do describe '#mark' do diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb index 6820a7df95e..8ab2a7b64dd 100644 --- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Diff::InlineDiffMarker do describe '#mark' do diff --git a/spec/lib/gitlab/diff/lines_unfolder_spec.rb b/spec/lib/gitlab/diff/lines_unfolder_spec.rb index f0e710be2e4..98f0c4df204 100644 --- a/spec/lib/gitlab/diff/lines_unfolder_spec.rb +++ b/spec/lib/gitlab/diff/lines_unfolder_spec.rb @@ -189,14 +189,14 @@ RSpec.describe Gitlab::Diff::LinesUnfolder do let(:diff) do Gitlab::Git::Diff.new({ diff: raw_diff, - new_path: "build-aux/flatpak/org.gnome.Nautilus.json", - old_path: "build-aux/flatpak/org.gnome.Nautilus.json", - a_mode: "100644", - b_mode: "100644", - new_file: false, - renamed_file: false, - deleted_file: false, - too_large: false }) + new_path: "build-aux/flatpak/org.gnome.Nautilus.json", + old_path: "build-aux/flatpak/org.gnome.Nautilus.json", + a_mode: "100644", + b_mode: "100644", + new_file: false, + renamed_file: false, + deleted_file: false, + too_large: false }) end let(:diff_file) do diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index bb3522eb579..00a468bfef6 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -684,7 +684,7 @@ RSpec.describe Gitlab::Diff::Position do "old_line" => 18, "new_line" => 18 }, - "end" => { + "end" => { "line_code" => end_line_code, "type" => nil, "old_line" => end_old_line, diff --git a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb index b5137f9db6b..e1135f4d546 100644 --- a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb +++ b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb @@ -5,7 +5,8 @@ require 'spec_helper' RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFile do include RepoHelpers - let(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } + let(:commit) { project.commit("5d6ed1503801ca9dc28e95eeb85a7cf863527aee") } let(:diffs) { commit.raw_diffs.to_a } let(:diff) { diffs.first } diff --git a/spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb b/spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb new file mode 100644 index 00000000000..df17d92bb0c --- /dev/null +++ b/spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::DoorkeeperSecretStoring::Secret::Pbkdf2Sha512 do + describe '.transform_secret' do + let(:plaintext_secret) { 'CzOBzBfU9F-HvsqfTaTXF4ivuuxYZuv3BoAK4pnvmyw' } + + it 'generates a PBKDF2+SHA512 hashed value in the correct format' do + expect(described_class.transform_secret(plaintext_secret)) + .to eq("$pbkdf2-sha512$20000$$.c0G5XJVEew1TyeJk5TrkvB0VyOaTmDzPrsdNRED9vVeZlSyuG3G90F0ow23zUCiWKAVwmNnR/ceh.nJG3MdpQ") # rubocop:disable Layout/LineLength + end + + context 'when hash_oauth_secrets is disabled' do + before do + stub_feature_flags(hash_oauth_secrets: false) + end + + it 'returns a plaintext secret' do + expect(described_class.transform_secret(plaintext_secret)).to eq(plaintext_secret) + end + end + end + + describe 'STRETCHES' do + it 'is 20_000' do + expect(described_class::STRETCHES).to eq(20_000) + end + end + + describe 'SALT' do + it 'is empty' do + expect(described_class::SALT).to be_empty + end + end + + describe '.secret_matches?' do + it "match by hashing the input if the stored value is hashed" do + stub_feature_flags(hash_oauth_secrets: false) + plain_secret = 'plain_secret' + stored_value = '$pbkdf2-sha512$20000$$/BwQRdwSpL16xkQhstavh7nvA5avCP7.4n9LLKe9AupgJDeA7M5xOAvG3N3E5XbRyGWWBbbr.BsojPVWzd1Sqg' # rubocop:disable Layout/LineLength + expect(described_class.secret_matches?(plain_secret, stored_value)).to be true + end + end +end diff --git a/spec/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512_spec.rb b/spec/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512_spec.rb index e953733c997..c73744cd481 100644 --- a/spec/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512_spec.rb +++ b/spec/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::DoorkeeperSecretStoring::Pbkdf2Sha512 do +RSpec.describe Gitlab::DoorkeeperSecretStoring::Token::Pbkdf2Sha512 do describe '.transform_secret' do let(:plaintext_token) { 'CzOBzBfU9F-HvsqfTaTXF4ivuuxYZuv3BoAK4pnvmyw' } diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index b0c67cdafe1..690396d4dbc 100644 --- a/spec/lib/gitlab/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -98,6 +98,36 @@ RSpec.describe Gitlab::EncodingHelper do end end + describe '#encode_utf8_with_escaping!' do + where(:input, :expected) do + "abcd" | "abcd" + "DzDzDz" | "DzDzDz" + "\xC7\xB2\xC7DzDzDz" | "Dz%C7DzDzDz" + "🐤🐤🐤🐤\xF0\x9F\x90" | "🐤🐤🐤🐤%F0%9F%90" + "\xD0\x9F\xD1\x80 \x90" | "Пр %90" + "\x41" | "A" + end + + with_them do + it 'escapes invalid UTF-8' do + expect(ext_class.encode_utf8_with_escaping!(input.dup.force_encoding(Encoding::ASCII_8BIT))).to eq(expected) + expect(ext_class.encode_utf8_with_escaping!(input)).to eq(expected) + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(escape_gitaly_refs: false) + end + + it 'uses #encode! method' do + expect(ext_class).to receive(:encode!).with('String') + + ext_class.encode_utf8_with_escaping!('String') + end + end + end + describe '#encode_utf8' do [ ["nil", nil, nil], diff --git a/spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb index 210829056c8..c9b632b50e1 100644 --- a/spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb +++ b/spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb @@ -38,10 +38,10 @@ RSpec.describe Gitlab::ErrorTracking::Processor::ContextPayloadProcessor do expect(result_hash[:tags]) .to include(priority: 'high', - locale: 'en', - program: 'test', - feature_category: 'feature_a', - correlation_id: 'cid') + locale: 'en', + program: 'test', + feature_category: 'feature_a', + correlation_id: 'cid') expect(result_hash[:extra]) .to include(some_info: 'info', diff --git a/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb b/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb index 577d59798da..3d23249d00d 100644 --- a/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb +++ b/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb @@ -52,7 +52,7 @@ RSpec.describe Gitlab::ErrorTracking::StackTraceHighlightDecorator do 'function' => 'print', 'lineNo' => 3, 'filename' => 'hello_world.php', - 'context' => [ + 'context' => [ [1, '<span id="LC1" class="line" lang="hack"><span class="c1">// PHP/Hack example</span></span>'], [2, '<span id="LC1" class="line" lang="hack"><span class="cp"><?php</span></span>'], [3, '<span id="LC1" class="line" lang="hack"><span class="k">echo</span> <span class="s1">\'Hello, World!\'</span><span class="p">;</span></span>'] diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb index 8228f95dd5e..da5eaf2e4ab 100644 --- a/spec/lib/gitlab/etag_caching/middleware_spec.rb +++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb @@ -119,11 +119,11 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state let(:expected_items) do { etag_route: endpoint, - params: {}, - format: :html, - method: 'GET', - path: enabled_path, - status: status_code + params: {}, + format: :html, + method: 'GET', + path: enabled_path, + status: status_code } end diff --git a/spec/lib/gitlab/etag_caching/router/graphql_spec.rb b/spec/lib/gitlab/etag_caching/router/graphql_spec.rb index 9a6787e3640..792f02f8cda 100644 --- a/spec/lib/gitlab/etag_caching/router/graphql_spec.rb +++ b/spec/lib/gitlab/etag_caching/router/graphql_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Gitlab::EtagCaching::Router::Graphql do def match_route(path, header) described_class.match( double(path_info: path, - headers: { 'X-GITLAB-GRAPHQL-RESOURCE-ETAG' => header })) + headers: { 'X-GITLAB-GRAPHQL-RESOURCE-ETAG' => header })) end describe '.cache_key' do diff --git a/spec/lib/gitlab/experimentation/group_types_spec.rb b/spec/lib/gitlab/experimentation/group_types_spec.rb index 599ad08f706..2b118d76fa4 100644 --- a/spec/lib/gitlab/experimentation/group_types_spec.rb +++ b/spec/lib/gitlab/experimentation/group_types_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Experimentation::GroupTypes do it 'defines a GROUP_CONTROL constant' do diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb index 8c0c56ea2c3..208acf28cc4 100644 --- a/spec/lib/gitlab/file_detector_spec.rb +++ b/spec/lib/gitlab/file_detector_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::FileDetector do describe '.types_in_paths' do diff --git a/spec/lib/gitlab/file_markdown_link_builder_spec.rb b/spec/lib/gitlab/file_markdown_link_builder_spec.rb index d684beaaaca..54dfde9fc45 100644 --- a/spec/lib/gitlab/file_markdown_link_builder_spec.rb +++ b/spec/lib/gitlab/file_markdown_link_builder_spec.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::FileMarkdownLinkBuilder do let(:custom_class) do diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb index 763e6f1b5f4..a16f96a7d11 100644 --- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb @@ -19,7 +19,7 @@ RSpec.describe Gitlab::Gfm::UploadsRewriter do end let(:text) do - "Text and #{image_uploader.markdown_link} and #{zip_uploader.markdown_link}" + "Text and #{image_uploader.markdown_link} and #{zip_uploader.markdown_link}".freeze # rubocop:disable Style/RedundantFreeze end def referenced_files(text, project) diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 0da7aa7dad0..d35d288050a 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -2,11 +2,9 @@ require "spec_helper" -RSpec.describe Gitlab::Git::Blob, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } - let(:rugged) do - Rugged::Repository.new(File.join(TestEnv.repos_path, TEST_REPO_PATH)) - end +RSpec.describe Gitlab::Git::Blob do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:repository) { project.repository.raw } describe 'initialize' do let(:blob) { Gitlab::Git::Blob.new(name: 'test') } @@ -44,7 +42,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do shared_examples '.find' do context 'nil path' do - let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, nil) } + let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], nil) } it { expect(blob).to eq(nil) } end @@ -56,30 +54,30 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do end context 'blank path' do - let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, '') } + let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], '') } it { expect(blob).to eq(nil) } end context 'file in subdir' do - let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") } + let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "files/ruby/popen.rb") } it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) } it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) } it { expect(blob.path).to eq("files/ruby/popen.rb") } - it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(blob.commit_id).to eq(TestEnv::BRANCH_SHA['master']) } it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) } it { expect(blob.size).to eq(669) } it { expect(blob.mode).to eq("100644") } end context 'file in root' do - let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, ".gitignore") } + let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], ".gitignore") } it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") } it { expect(blob.name).to eq(".gitignore") } it { expect(blob.path).to eq(".gitignore") } - it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(blob.commit_id).to eq(TestEnv::BRANCH_SHA['master']) } it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") } it { expect(blob.size).to eq(241) } it { expect(blob.mode).to eq("100644") } @@ -87,25 +85,25 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do end context 'file in root with leading slash' do - let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "/.gitignore") } + let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "/.gitignore") } it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") } it { expect(blob.name).to eq(".gitignore") } it { expect(blob.path).to eq(".gitignore") } - it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(blob.commit_id).to eq(TestEnv::BRANCH_SHA['master']) } it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") } it { expect(blob.size).to eq(241) } it { expect(blob.mode).to eq("100644") } end context 'non-exist file' do - let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "missing.rb") } + let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "missing.rb") } it { expect(blob).to be_nil } end context 'six submodule' do - let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'six') } + let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], 'six') } it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') } it { expect(blob.data).to eq('') } @@ -121,7 +119,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do end context 'large file' do - let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'files/images/6049019_460s.jpg') } + let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], 'files/images/6049019_460s.jpg') } let(:blob_size) { 111803 } let(:stub_limit) { 1000 } @@ -159,10 +157,10 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do describe '.find with Rugged enabled', :enable_rugged do it 'calls out to the Rugged implementation' do allow_next_instance_of(Rugged) do |instance| - allow(instance).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original + allow(instance).to receive(:rev_parse).with(TestEnv::BRANCH_SHA['master']).and_call_original end - described_class.find(repository, SeedRepo::Commit::ID, 'files/images/6049019_460s.jpg') + described_class.find(repository, TestEnv::BRANCH_SHA['master'], 'files/images/6049019_460s.jpg') end it_behaves_like '.find' @@ -177,40 +175,13 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do it { expect(raw_blob.size).to eq(669) } it { expect(raw_blob.truncated?).to be_falsey } it { expect(bad_blob).to be_nil } - - context 'large file' do - it 'limits the size of a large file' do - blob_size = Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE + 1 - buffer = Array.new(blob_size, 0) - rugged_blob = Rugged::Blob.from_buffer(rugged, buffer.join('')) - blob = Gitlab::Git::Blob.raw(repository, rugged_blob) - - expect(blob.size).to eq(blob_size) - expect(blob.loaded_size).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) - expect(blob.data.length).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) - expect(blob.truncated?).to be_truthy - - blob.load_all_data!(repository) - expect(blob.loaded_size).to eq(blob_size) - end - end - - context 'when sha references a tree' do - it 'returns nil' do - tree = rugged.rev_parse('master^{tree}') - - blob = Gitlab::Git::Blob.raw(repository, tree.oid) - - expect(blob).to be_nil - end - end end describe '.batch' do let(:blob_references) do [ - [SeedRepo::Commit::ID, "files/ruby/popen.rb"], - [SeedRepo::Commit::ID, 'six'] + [TestEnv::BRANCH_SHA['master'], "files/ruby/popen.rb"], + [TestEnv::BRANCH_SHA['master'], 'six'] ] end @@ -224,7 +195,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) } it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) } it { expect(blob.path).to eq("files/ruby/popen.rb") } - it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(blob.commit_id).to eq(TestEnv::BRANCH_SHA['master']) } it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) } it { expect(blob.size).to eq(669) } it { expect(blob.mode).to eq("100644") } @@ -273,21 +244,21 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do context 'when large number of blobs requested' do let(:first_batch) do [ - [SeedRepo::Commit::ID, 'files/ruby/popen.rb'], - [SeedRepo::Commit::ID, 'six'] + [TestEnv::BRANCH_SHA['master'], 'files/ruby/popen.rb'], + [TestEnv::BRANCH_SHA['master'], 'six'] ] end let(:second_batch) do [ - [SeedRepo::Commit::ID, 'some'], - [SeedRepo::Commit::ID, 'other'] + [TestEnv::BRANCH_SHA['master'], 'some'], + [TestEnv::BRANCH_SHA['master'], 'other'] ] end let(:third_batch) do [ - [SeedRepo::Commit::ID, 'files'] + [TestEnv::BRANCH_SHA['master'], 'files'] ] end @@ -315,8 +286,8 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do describe '.batch_metadata' do let(:blob_references) do [ - [SeedRepo::Commit::ID, "files/ruby/popen.rb"], - [SeedRepo::Commit::ID, 'six'] + [TestEnv::BRANCH_SHA['master'], "files/ruby/popen.rb"], + [TestEnv::BRANCH_SHA['master'], 'six'] ] end @@ -333,8 +304,6 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do end describe '.batch_lfs_pointers' do - let(:tree_object) { rugged.rev_parse('master^{tree}') } - let(:non_lfs_blob) do Gitlab::Git::Blob.find( repository, @@ -346,8 +315,8 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do let(:lfs_blob) do Gitlab::Git::Blob.find( repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'files/lfs/image.jpg' + TestEnv::BRANCH_SHA['master'], + 'files/lfs/lfs_object.iso' ) end @@ -374,12 +343,6 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do expect(blobs_2).to eq([]) end - it 'silently ignores tree objects' do - blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid]) - - expect(blobs).to eq([]) - end - it 'silently ignores non lfs objects' do blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id]) @@ -398,7 +361,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do describe 'encoding', :aggregate_failures do context 'file with russian text' do - let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") } + let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "encoding/russian.rb") } it 'has the correct blob attributes' do expect(blob.name).to eq("russian.rb") @@ -412,7 +375,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do end context 'file with Japanese text' do - let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/テスト.txt") } + let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "encoding/テスト.txt") } it 'has the correct blob attributes' do expect(blob.name).to eq("テスト.txt") @@ -424,12 +387,12 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do end context 'file with ISO-8859 text' do - let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::LastCommit::ID, "encoding/iso8859.txt") } + let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "encoding/iso8859.txt") } it 'has the correct blob attributes' do expect(blob.name).to eq("iso8859.txt") - expect(blob.loaded_size).to eq(4) - expect(blob.size).to eq(4) + expect(blob.loaded_size).to eq(3) + expect(blob.size).to eq(3) expect(blob.mode).to eq("100644") expect(blob.truncated?).to be_falsey end @@ -441,7 +404,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do let(:blob) do Gitlab::Git::Blob.find( repository, - 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', + TestEnv::BRANCH_SHA['master'], 'files/ruby/regex.rb' ) end @@ -456,14 +419,14 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do let(:blob) do Gitlab::Git::Blob.find( repository, - 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', + TestEnv::BRANCH_SHA['with-executables'], 'files/executables/ls' ) end it { expect(blob.name).to eq('ls') } it { expect(blob.path).to eq('files/executables/ls') } - it { expect(blob.size).to eq(110080) } + it { expect(blob.size).to eq(23) } it { expect(blob.mode).to eq("100755") } end @@ -471,29 +434,14 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do let(:blob) do Gitlab::Git::Blob.find( repository, - 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', - 'files/links/ruby-style-guide.md' + '88ce9520c07b7067f589b7f83a30b6250883115c', + 'symlink' ) end - it { expect(blob.name).to eq('ruby-style-guide.md') } - it { expect(blob.path).to eq('files/links/ruby-style-guide.md') } - it { expect(blob.size).to eq(31) } - it { expect(blob.mode).to eq("120000") } - end - - context 'file symlink to binary' do - let(:blob) do - Gitlab::Git::Blob.find( - repository, - 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', - 'files/links/touch' - ) - end - - it { expect(blob.name).to eq('touch') } - it { expect(blob.path).to eq('files/links/touch') } - it { expect(blob.size).to eq(20) } + it { expect(blob.name).to eq('symlink') } + it { expect(blob.path).to eq('symlink') } + it { expect(blob.size).to eq(6) } it { expect(blob.mode).to eq("120000") } end end @@ -503,79 +451,20 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do let(:blob) do Gitlab::Git::Blob.find( repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'files/lfs/image.jpg' + TestEnv::BRANCH_SHA['png-lfs'], + 'files/images/emoji.png' ) end it { expect(blob.lfs_pointer?).to eq(true) } - it { expect(blob.lfs_oid).to eq("4206f951d2691c78aac4c0ce9f2b23580b2c92cdcc4336e1028742c0274938e0") } - it { expect(blob.lfs_size).to eq(19548) } - it { expect(blob.id).to eq("f4d76af13003d1106be7ac8c5a2a3d37ddf32c2a") } - it { expect(blob.name).to eq("image.jpg") } - it { expect(blob.path).to eq("files/lfs/image.jpg") } - it { expect(blob.size).to eq(130) } + it { expect(blob.lfs_oid).to eq("96f74c6fe7a2979eefb9ec74a5dfc6888fb25543cf99b77586b79afea1da6f97") } + it { expect(blob.lfs_size).to eq(1219696) } + it { expect(blob.id).to eq("ff0ab3afd1616ff78d0331865d922df103b64cf0") } + it { expect(blob.name).to eq("emoji.png") } + it { expect(blob.path).to eq("files/images/emoji.png") } + it { expect(blob.size).to eq(132) } it { expect(blob.mode).to eq("100644") } end - - describe 'file an invalid lfs pointer' do - context 'with correct version header but incorrect size and oid' do - let(:blob) do - Gitlab::Git::Blob.find( - repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'files/lfs/archive-invalid.tar' - ) - end - - it { expect(blob.lfs_pointer?).to eq(false) } - it { expect(blob.lfs_oid).to eq(nil) } - it { expect(blob.lfs_size).to eq(nil) } - it { expect(blob.id).to eq("f8a898db217a5a85ed8b3d25b34c1df1d1094c46") } - it { expect(blob.name).to eq("archive-invalid.tar") } - it { expect(blob.path).to eq("files/lfs/archive-invalid.tar") } - it { expect(blob.size).to eq(43) } - it { expect(blob.mode).to eq("100644") } - end - - context 'with correct version header and size but incorrect size and oid' do - let(:blob) do - Gitlab::Git::Blob.find( - repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'files/lfs/picture-invalid.png' - ) - end - - it { expect(blob.lfs_pointer?).to eq(false) } - it { expect(blob.lfs_oid).to eq(nil) } - it { expect(blob.lfs_size).to eq(1575078) } - it { expect(blob.id).to eq("5ae35296e1f95c1ef9feda1241477ed29a448572") } - it { expect(blob.name).to eq("picture-invalid.png") } - it { expect(blob.path).to eq("files/lfs/picture-invalid.png") } - it { expect(blob.size).to eq(57) } - it { expect(blob.mode).to eq("100644") } - end - - context 'with correct version header and size but invalid size and oid' do - let(:blob) do - Gitlab::Git::Blob.find( - repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'files/lfs/file-invalid.zip' - ) - end - - it { expect(blob.lfs_pointer?).to eq(false) } - it { expect(blob.lfs_oid).to eq(nil) } - it { expect(blob.lfs_size).to eq(nil) } - it { expect(blob.id).to eq("d831981bd876732b85a1bcc6cc01210c9f36248f") } - it { expect(blob.name).to eq("file-invalid.zip") } - it { expect(blob.path).to eq("files/lfs/file-invalid.zip") } - it { expect(blob.size).to eq(60) } - it { expect(blob.mode).to eq("100644") } - end - end end describe '#load_all_data!' do diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index feaa1f6595c..95cc833390f 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -111,7 +111,7 @@ RSpec.describe Gitlab::Git::Branch do end def create_commit - repository.multi_action( + repository.commit_files( user, branch_name: 'HEAD', message: 'commit message', diff --git a/spec/lib/gitlab/git/changed_path_spec.rb b/spec/lib/gitlab/git/changed_path_spec.rb index 93db107ad5c..ef51021ba4c 100644 --- a/spec/lib/gitlab/git/changed_path_spec.rb +++ b/spec/lib/gitlab/git/changed_path_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "spec_helper" +require 'fast_spec_helper' RSpec.describe Gitlab::Git::ChangedPath do subject(:changed_path) { described_class.new(path: path, status: status) } diff --git a/spec/lib/gitlab/git/changes_spec.rb b/spec/lib/gitlab/git/changes_spec.rb index 310be7a3731..7cded9740ee 100644 --- a/spec/lib/gitlab/git/changes_spec.rb +++ b/spec/lib/gitlab/git/changes_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Git::Changes do let(:changes) { described_class.new } diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 95b49186d0f..d873151421d 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -2,8 +2,8 @@ require "spec_helper" -RSpec.describe Gitlab::Git::Commit, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } +RSpec.describe Gitlab::Git::Commit do + let(:repository) { create(:project, :repository).repository.raw } let(:commit) { described_class.find(repository, SeedRepo::Commit::ID) } describe "Commit info from gitaly commit" do @@ -121,14 +121,6 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do it "returns nil for id containing NULL" do expect(described_class.find(repository, "HE\x00AD")).to be_nil end - - context 'with broken repo' do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH, '', 'group/project') } - - it 'returns nil' do - expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_nil - end - end end describe '.find with Gitaly enabled' do @@ -154,7 +146,7 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do describe '#id' do subject { super().id } - it { is_expected.to eq(SeedRepo::LastCommit::ID) } + it { is_expected.to eq(TestEnv::BRANCH_SHA['master']) } end end @@ -223,7 +215,7 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do expect(subject.size).to eq(10) end - it { is_expected.to include(SeedRepo::EmptyCommit::ID) } + it { is_expected.to include(TestEnv::BRANCH_SHA['master']) } end context 'path is nil' do @@ -242,28 +234,7 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do expect(subject.size).to eq(10) end - it { is_expected.to include(SeedRepo::EmptyCommit::ID) } - end - - context 'ref is branch name' do - subject do - commits = described_class.where( - repo: repository, - ref: 'master', - path: 'files', - limit: 3, - offset: 1 - ) - - commits.map { |c| c.id } - end - - it 'has 3 elements' do - expect(subject.size).to eq(3) - end - - it { is_expected.to include("d14d6c0abdd253381df51a723d58691b2ee1ab08") } - it { is_expected.not_to include("eb49186cfa5c4338011f5f590fac11bd66c5c631") } + it { is_expected.to include(TestEnv::BRANCH_SHA['master']) } end context 'ref is commit id' do @@ -323,13 +294,12 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do context 'requesting a commit range' do let(:from) { 'v1.0.0' } - let(:to) { 'v1.2.0' } + let(:to) { 'v1.1.0' } let(:commits_in_range) do %w[ 570e7b2abdd848b95f2f578043fc23bd6f6fd24d 5937ac0a7beb003549fc5fd26fc247adbce4a52e - eb49186cfa5c4338011f5f590fac11bd66c5c631 ] end @@ -338,9 +308,9 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do end context 'limited' do - let(:limit) { 2 } + let(:limit) { 1 } - it { expect(commit_ids).to eq(commits_in_range.last(2)) } + it { expect(commit_ids).to eq(commits_in_range.last(1)) } end end end @@ -383,16 +353,8 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do commits.map(&:id) end - it 'has 34 elements' do - expect(subject.size).to eq(34) - end - - it 'includes the expected commits' do - expect(subject).to include( - SeedRepo::Commit::ID, - SeedRepo::Commit::PARENT_ID, - SeedRepo::FirstCommit::ID - ) + it 'has maximum elements' do + expect(subject.size).to eq(50) end end @@ -408,13 +370,13 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do commits.map(&:id) end - it 'has 24 elements' do - expect(subject.size).to eq(24) + it 'has 36 elements' do + expect(subject.size).to eq(36) end it 'includes the expected commits' do expect(subject).to include(SeedRepo::Commit::ID, SeedRepo::FirstCommit::ID) - expect(subject).not_to include(SeedRepo::LastCommit::ID) + expect(subject).not_to include(TestEnv::BRANCH_SHA['master']) end end end @@ -650,8 +612,8 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do subject { commit.ref_names(repository) } - it 'has 2 element' do - expect(subject.size).to eq(2) + it 'has 3 elements' do + expect(subject.size).to eq(3) end it { is_expected.to include("master") } @@ -681,6 +643,8 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do end it 'gets messages in one batch', :request_store do + repository # preload repository so that the project factory does not pollute request counts + expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1) end end diff --git a/spec/lib/gitlab/git/commit_stats_spec.rb b/spec/lib/gitlab/git/commit_stats_spec.rb index 29d3909efec..81d9dda4b8f 100644 --- a/spec/lib/gitlab/git/commit_stats_spec.rb +++ b/spec/lib/gitlab/git/commit_stats_spec.rb @@ -2,17 +2,19 @@ require "spec_helper" -RSpec.describe Gitlab::Git::CommitStats, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } - let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) } +RSpec.describe Gitlab::Git::CommitStats do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:repository) { project.repository.raw } + + let(:commit) { Gitlab::Git::Commit.find(repository, TestEnv::BRANCH_SHA['feature']) } def verify_stats! stats = described_class.new(repository, commit) expect(stats).to have_attributes( - additions: eq(11), - deletions: eq(6), - total: eq(17) + additions: eq(5), + deletions: eq(0), + total: eq(5) ) end @@ -21,7 +23,7 @@ RSpec.describe Gitlab::Git::CommitStats, :seed_helper do verify_stats! - expect(Rails.cache.fetch("commit_stats:group/project:#{commit.id}")).to eq([11, 6]) + expect(Rails.cache.fetch("commit_stats:#{repository.gl_project_path}:#{commit.id}")).to eq([5, 0]) expect(repository.gitaly_commit_client).not_to receive(:commit_stats) diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb index 51043355ede..e8c683cf8aa 100644 --- a/spec/lib/gitlab/git/compare_spec.rb +++ b/spec/lib/gitlab/git/compare_spec.rb @@ -2,8 +2,9 @@ require "spec_helper" -RSpec.describe Gitlab::Git::Compare, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } +RSpec.describe Gitlab::Git::Compare do + let_it_be(:repository) { create(:project, :repository).repository.raw } + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: false) } let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: true) } diff --git a/spec/lib/gitlab/git/conflict/file_spec.rb b/spec/lib/gitlab/git/conflict/file_spec.rb index 6eb7a7e394e..fb1bec0a554 100644 --- a/spec/lib/gitlab/git/conflict/file_spec.rb +++ b/spec/lib/gitlab/git/conflict/file_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Git::Conflict::File do let(:conflict) { { ancestor: { path: 'ancestor' }, theirs: { path: 'foo', mode: 33188 }, ours: { path: 'foo', mode: 33188 } } } diff --git a/spec/lib/gitlab/git/conflict/parser_spec.rb b/spec/lib/gitlab/git/conflict/parser_spec.rb index 7d81af92412..67f288e0299 100644 --- a/spec/lib/gitlab/git/conflict/parser_spec.rb +++ b/spec/lib/gitlab/git/conflict/parser_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Git::Conflict::Parser do describe '.parse' do diff --git a/spec/lib/gitlab/git/cross_repo_comparer_spec.rb b/spec/lib/gitlab/git/cross_repo_comparer_spec.rb index 1c49486b7b1..7888e224d59 100644 --- a/spec/lib/gitlab/git/cross_repo_comparer_spec.rb +++ b/spec/lib/gitlab/git/cross_repo_comparer_spec.rb @@ -110,7 +110,7 @@ RSpec.describe Gitlab::Git::CrossRepoComparer do def create_commit(user, repo, branch) action = { action: :create, file_path: '/FILE', content: 'content' } - result = repo.multi_action(user, branch_name: branch, message: 'Commit', actions: [action]) + result = repo.commit_files(user, branch_name: branch, message: 'Commit', actions: [action]) result.newrev end diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 0e3e92e03cf..7fa5bd8a92b 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do +RSpec.describe Gitlab::Git::DiffCollection do before do stub_const('MutatingConstantIterator', Class.new) diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 2c931a999f1..6745c700b92 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -2,8 +2,10 @@ require "spec_helper" -RSpec.describe Gitlab::Git::Diff, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } +RSpec.describe Gitlab::Git::Diff do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:repository) { project.repository } + let(:gitaly_diff) do Gitlab::GitalyClient::Diff.new( from_path: '.gitmodules', @@ -190,16 +192,6 @@ EOT expect(binary_diff(project).diff).not_to be_empty end end - - context 'when convert_diff_to_utf8_with_replacement_symbol feature flag is disabled' do - before do - stub_feature_flags(convert_diff_to_utf8_with_replacement_symbol: false) - end - - it 'will not try to convert invalid characters' do - expect(Gitlab::EncodingHelper).not_to receive(:encode_utf8_with_replacement_character?) - end - end end context 'when replace_invalid_utf8_chars is false' do @@ -218,7 +210,7 @@ EOT let(:diffs) { described_class.between(repository, 'feature', 'master', options) } it 'has the correct size' do - expect(diffs.size).to eq(24) + expect(diffs.size).to eq(21) end context 'diff' do diff --git a/spec/lib/gitlab/git/gitmodules_parser_spec.rb b/spec/lib/gitlab/git/gitmodules_parser_spec.rb index 0e386c7f3d1..33268b4edcb 100644 --- a/spec/lib/gitlab/git/gitmodules_parser_spec.rb +++ b/spec/lib/gitlab/git/gitmodules_parser_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Git::GitmodulesParser do it 'parses a .gitmodules file correctly' do diff --git a/spec/lib/gitlab/git/lfs_pointer_file_spec.rb b/spec/lib/gitlab/git/lfs_pointer_file_spec.rb index f45c7cccca0..b210c86c3d1 100644 --- a/spec/lib/gitlab/git/lfs_pointer_file_spec.rb +++ b/spec/lib/gitlab/git/lfs_pointer_file_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Git::LfsPointerFile do let(:data) { "1234\n" } diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index a1fb8b70bd7..9a87911b6e8 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe Gitlab::Git::Repository, :seed_helper do +RSpec.describe Gitlab::Git::Repository do include Gitlab::EncodingHelper include RepoHelpers using RSpec::Parameterized::TableSyntax @@ -21,13 +21,11 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end end - let(:mutable_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '', 'group/project') } - let(:mutable_repository_path) { File.join(TestEnv.repos_path, mutable_repository.relative_path) } - let(:mutable_repository_rugged) { Rugged::Repository.new(mutable_repository_path) } - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } - let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) } - let(:repository_rugged) { Rugged::Repository.new(repository_path) } - let(:storage_path) { TestEnv.repos_path } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:repository) { project.repository.raw } + + let(:mutable_project) { create(:project, :repository) } + let(:mutable_repository) { mutable_project.repository.raw } let(:user) { build(:user) } describe "Respond to" do @@ -61,8 +59,8 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#branch_names' do subject { repository.branch_names } - it 'has SeedRepo::Repo::BRANCHES.size elements' do - expect(subject.size).to eq(SeedRepo::Repo::BRANCHES.size) + it 'has TestRepo::BRANCH_SHA.size elements' do + expect(subject.size).to eq(TestEnv::BRANCH_SHA.size) end it 'returns UTF-8' do @@ -85,8 +83,8 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it { is_expected.to be_kind_of Array } - it 'has SeedRepo::Repo::TAGS.size elements' do - expect(subject.size).to eq(SeedRepo::Repo::TAGS.size) + it 'has some elements' do + expect(subject.size).to be >= 1 end it 'returns UTF-8' do @@ -96,63 +94,24 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#last' do subject { super().last } - it { is_expected.to eq("v1.2.1") } + it { is_expected.to eq("v1.1.1") } end + it { is_expected.to include("v1.0.0") } it { is_expected.not_to include("v5.0.0") } - it 'gets the tag names from GitalyClient' do - expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:tag_names) - subject - end - it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tag_names end describe '#tags' do subject { repository.tags } - it 'gets tags from GitalyClient' do - expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service| - expect(service).to receive(:tags) - end - - subject - end - - context 'with sorting option' do - subject { repository.tags(sort_by: 'name_asc') } - - it 'gets tags from GitalyClient' do - expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service| - expect(service).to receive(:tags).with(sort_by: 'name_asc', pagination_params: nil) - end - - subject - end - end - - context 'with pagination option' do - subject { repository.tags(pagination_params: { limit: 5, page_token: 'refs/tags/v1.0.0' }) } - - it 'gets tags from GitalyClient' do - expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service| - expect(service).to receive(:tags).with( - sort_by: nil, - pagination_params: { limit: 5, page_token: 'refs/tags/v1.0.0' } - ) - end - - subject - end - end - it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tags end describe '#archive_metadata' do let(:storage_path) { '/tmp' } - let(:cache_key) { File.join(repository.gl_repository, SeedRepo::LastCommit::ID) } + let(:cache_key) { File.join(repository.gl_repository, TestEnv::BRANCH_SHA['master']) } let(:append_sha) { true } let(:ref) { 'master' } @@ -162,12 +121,12 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do let(:expected_extension) { 'tar.gz' } let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" } let(:expected_path) { File.join(storage_path, cache_key, "@v2", expected_filename) } - let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" } + let(:expected_prefix) { "gitlab-git-test-#{ref}-#{TestEnv::BRANCH_SHA['master']}" } subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha, path: path) } it 'sets CommitId to the commit SHA' do - expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID) + expect(metadata['CommitId']).to start_with(TestEnv::BRANCH_SHA['master']) end it 'sets ArchivePrefix to the expected prefix' do @@ -175,7 +134,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end it 'sets ArchivePath to the expected globally-unique path' do - expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID)) + expect(expected_path).to include(File.join(repository.gl_repository, TestEnv::BRANCH_SHA['master'])) expect(metadata['ArchivePath']).to eq(expected_path) end @@ -190,7 +149,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do context 'append_sha varies archive path and filename' do where(:append_sha, :ref, :expected_prefix) do - sha = SeedRepo::LastCommit::ID + sha = TestEnv::BRANCH_SHA['master'] true | 'master' | "gitlab-git-test-master-#{sha}" true | sha | "gitlab-git-test-#{sha}-#{sha}" @@ -224,13 +183,13 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#size' do subject { repository.size } - it { is_expected.to be < 2 } + it { is_expected.to be > 0 } end describe '#to_s' do subject { repository.to_s } - it { is_expected.to eq("<Gitlab::Git::Repository: group/project>") } + it { is_expected.to eq("<Gitlab::Git::Repository: #{project.full_path}>") } end describe '#object_directory_size' do @@ -259,26 +218,25 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#first' do subject { super().first } - it { is_expected.to eq('feature') } + it { is_expected.to eq(TestEnv::BRANCH_SHA.keys.min) } end describe '#last' do subject { super().last } - it { is_expected.to eq('v1.2.1') } + it { is_expected.to eq('v1.1.1') } end end describe '#submodule_url_for' do - let(:ref) { 'master' } + let(:ref) { 'submodule_inside_folder' } def submodule_url(path) repository.submodule_url_for(ref, path) end it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') } - it { expect(submodule_url('nested/six')).to eq('git://github.com/randx/six.git') } - it { expect(submodule_url('deeper/nested/six')).to eq('git://github.com/randx/six.git') } + it { expect(submodule_url('test_inside_folder/another_folder/six')).to eq('git://github.com/randx/six.git') } it { expect(submodule_url('invalid/path')).to eq(nil) } context 'uncommitted submodule dir' do @@ -288,7 +246,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end context 'tags' do - let(:ref) { 'v1.2.1' } + let(:ref) { 'v1.1.1' } it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') } end @@ -313,17 +271,15 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do urls = repository.submodule_urls_for(ref) expect(urls).to eq({ - "deeper/nested/six" => "git://github.com/randx/six.git", - "gitlab-grack" => "https://gitlab.com/gitlab-org/gitlab-grack.git", - "gitlab-shell" => "https://github.com/gitlabhq/gitlab-shell.git", - "nested/six" => "git://github.com/randx/six.git", + "gitlab-grack" => "https://gitlab.com/gitlab-org/gitlab-grack.git", + "gitlab-shell" => "https://github.com/gitlabhq/gitlab-shell.git", "six" => "git://github.com/randx/six.git" }) end end describe '#commit_count' do - it { expect(repository.commit_count("master")).to eq(25) } + it { expect(repository.commit_count("master")).to eq(37) } it { expect(repository.commit_count("feature")).to eq(9) } it { expect(repository.commit_count("does-not-exist")).to eq(0) } @@ -353,7 +309,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do repository.create_branch('right-branch') left.times do |i| - repository.multi_action( + repository.commit_files( user, branch_name: 'left-branch', message: 'some more content for a', @@ -366,7 +322,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end right.times do |i| - repository.multi_action( + repository.commit_files( user, branch_name: 'right-branch', message: 'some more content for b', @@ -411,7 +367,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do repository.create_branch('right-branch') left.times do |i| - repository.multi_action( + repository.commit_files( user, branch_name: 'left-branch', message: 'some more content for a', @@ -424,7 +380,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end right.times do |i| - repository.multi_action( + repository.commit_files( user, branch_name: 'right-branch', message: 'some more content for b', @@ -461,47 +417,32 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#has_local_branches?' do context 'check for local branches' do it { expect(repository.has_local_branches?).to eq(true) } + end + end - context 'mutable' do - let(:repository) { mutable_repository } + describe '#delete_branch' do + let(:repository) { mutable_repository } - after do - ensure_seeds - end + it 'deletes a branch' do + expect(repository.find_branch('feature')).not_to be_nil - it 'returns false when there are no branches' do - # Sanity check - expect(repository.has_local_branches?).to eq(true) + repository.delete_branch('feature') - FileUtils.rm_rf(File.join(repository_path, 'packed-refs')) - heads_dir = File.join(repository_path, 'refs/heads') - FileUtils.rm_rf(heads_dir) - FileUtils.mkdir_p(heads_dir) + expect(repository.find_branch('feature')).to be_nil + end - repository.expire_has_local_branches_cache - expect(repository.has_local_branches?).to eq(false) - end - end + it 'deletes a fully qualified branch' do + expect(repository.find_branch('feature')).not_to be_nil - context 'memoizes the value' do - it 'returns true' do - expect(repository).to receive(:uncached_has_local_branches?).once.and_call_original + repository.delete_branch('refs/heads/feature') - 2.times do - expect(repository.has_local_branches?).to eq(true) - end - end - end + expect(repository.find_branch('feature')).to be_nil end end describe '#delete_refs' do let(:repository) { mutable_repository } - after do - ensure_seeds - end - it 'deletes the ref' do repository.delete_refs('refs/heads/feature') @@ -548,9 +489,8 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do subject { repository.refs_hash } it "has as many entries as branches and tags" do - expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS # We flatten in case a commit is pointed at by more than one branch and/or tag - expect(subject.values.flatten.size).to eq(expected_refs.size) + expect(subject.values.flatten.size).to be > 0 end it 'has valid commit ids as keys' do @@ -598,7 +538,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do before do repository.create_branch(ref) - repository.multi_action( + repository.commit_files( user, branch_name: ref, message: 'committing something', @@ -608,7 +548,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do content: content }] ) - repository.multi_action( + repository.commit_files( user, branch_name: ref, message: 'committing something', @@ -620,10 +560,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do ) end - after do - ensure_seeds - end - subject do repository.search_files_by_content(content, ref) end @@ -647,8 +583,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do let(:filter) { 'files\/.*\/.*\.rb' } it 'returns matched files' do - expect(result).to contain_exactly('files/links/regex.rb', - 'files/ruby/popen.rb', + expect(result).to contain_exactly('files/ruby/popen.rb', 'files/ruby/regex.rb', 'files/ruby/version_info.rb') end @@ -673,6 +608,61 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end end + describe '#search_files_by_name' do + let(:ref) { 'master' } + + subject(:result) { mutable_repository.search_files_by_name(query, ref) } + + context 'when sending a valid name' do + let(:query) { 'files/ruby/popen.rb' } + + it 'returns matched files' do + expect(result).to contain_exactly('files/ruby/popen.rb') + end + end + + context 'when sending a name with space' do + let(:query) { 'file with space.md' } + + before do + mutable_repository.commit_files( + user, + actions: [{ action: :create, file_path: "file with space.md", content: "Test content" }], + branch_name: ref, message: "Test" + ) + end + + it 'returns matched files' do + expect(result).to contain_exactly('file with space.md') + end + end + + context 'when sending a name with special ASCII characters' do + let(:file_name) { 'Hello !@#$%^&*()' } + let(:query) { file_name } + + before do + mutable_repository.commit_files( + user, + actions: [{ action: :create, file_path: file_name, content: "Test content" }], + branch_name: ref, message: "Test" + ) + end + + it 'returns matched files' do + expect(result).to contain_exactly(file_name) + end + end + + context 'when sending a non-existing name' do + let(:query) { 'please do not exist.md' } + + it 'raises error' do + expect(result).to eql([]) + end + end + end + describe '#find_remote_root_ref' do it 'gets the remote root ref from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::RemoteService) @@ -720,7 +710,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do before do # Add new commits so that there's a renamed file in the commit history - @commit_with_old_name_id = repository.multi_action( + @commit_with_old_name_id = repository.commit_files( user, branch_name: repository.root_ref, message: 'Update CHANGELOG', @@ -730,7 +720,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do content: 'CHANGELOG' }] ).newrev - @rename_commit_id = repository.multi_action( + @rename_commit_id = repository.commit_files( user, branch_name: repository.root_ref, message: 'Move CHANGELOG to encoding/', @@ -741,7 +731,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do content: 'CHANGELOG' }] ).newrev - @commit_with_new_name_id = repository.multi_action( + @commit_with_new_name_id = repository.commit_files( user, branch_name: repository.root_ref, message: 'Edit encoding/CHANGELOG', @@ -755,7 +745,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do after do # Erase our commits so other tests get the original repo - repository.write_ref(repository.root_ref, SeedRepo::LastCommit::ID) + repository.write_ref(repository.root_ref, TestEnv::BRANCH_SHA['master']) end context "where 'follow' == true" do @@ -908,16 +898,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do expect(log_commits).not_to include(commit_with_new_name) end end - - context "and 'path' includes a directory that used to be a file" do - let(:log_commits) do - repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt")) - end - - it "returns a list of commits" do - expect(log_commits.size).to eq(1) - end - end end context "where provides 'after' timestamp" do @@ -981,7 +961,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it 'returns a list of commits' do commits = repository.log({ all: true, limit: 50 }) - expect(commits.size).to eq(37) + expect(commits.size).to eq(50) end end end @@ -992,7 +972,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end describe '#blobs' do - let_it_be(:commit_oid) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } + let_it_be(:commit_oid) { TestEnv::BRANCH_SHA['master'] } shared_examples 'a blob enumeration' do it 'enumerates blobs' do @@ -1008,7 +988,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do context 'single revision' do let(:revisions) { [commit_oid] } - let(:expected_blobs) { 53 } + let(:expected_blobs) { 52 } it_behaves_like 'a blob enumeration' end @@ -1038,48 +1018,31 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it_behaves_like 'a blob enumeration' end - - context 'partially blank revisions' do - let(:revisions) { [::Gitlab::Git::BLANK_SHA, commit_oid] } - let(:expected_blobs) { 53 } - - before do - expect_next_instance_of(Gitlab::GitalyClient::BlobService) do |service| - expect(service) - .to receive(:list_blobs) - .with([commit_oid], kind_of(Hash)) - .and_call_original - end - end - - it_behaves_like 'a blob enumeration' - end end describe '#new_blobs' do let(:repository) { mutable_repository } - let(:repository_rugged) { mutable_repository_rugged } - let(:blob) { create_blob('This is a new blob') } - let(:commit) { create_commit('nested/new-blob.txt' => blob) } - - def create_blob(content) - repository_rugged.write(content, :blob) - end + let(:commit) { create_commit('nested/new-blob.txt' => 'This is a new blob') } def create_commit(blobs) - author = { name: 'Test User', email: 'mail@example.com', time: Time.now } + commit_result = repository.commit_files( + user, + branch_name: 'a-new-branch', + message: 'Add a file', + actions: blobs.map do |path, content| + { + action: :create, + file_path: path, + content: content + } + end + ) - index = repository_rugged.index - blobs.each do |path, oid| - index.add(path: path, oid: oid, mode: 0100644) - end + # new_blobs only returns unreferenced blobs because it is used for hooks. + # Gitaly does not allow us to create loose objects via the RPC. + repository.delete_branch('a-new-branch') - Rugged::Commit.create(repository_rugged, - author: author, - committer: author, - message: "Message", - parents: [], - tree: index.write_tree(repository_rugged)) + commit_result.newrev end subject { repository.new_blobs(newrevs).to_a } @@ -1112,7 +1075,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do let(:newrevs) { commit } let(:expected_newrevs) { ['--not', '--all', '--not', newrevs] } let(:expected_blobs) do - [have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18)] + [have_attributes(class: Gitlab::Git::Blob, id: an_instance_of(String), path: 'nested/new-blob.txt', size: 18)] end it_behaves_like '#new_blobs with revisions' @@ -1122,20 +1085,19 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do let(:newrevs) { [commit] } let(:expected_newrevs) { ['--not', '--all', '--not'] + newrevs } let(:expected_blobs) do - [have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18)] + [have_attributes(class: Gitlab::Git::Blob, id: an_instance_of(String), path: 'nested/new-blob.txt', size: 18)] end it_behaves_like '#new_blobs with revisions' end context 'with multiple revisions' do - let(:another_blob) { create_blob('Another blob') } - let(:newrevs) { [commit, create_commit('another_path.txt' => another_blob)] } + let(:newrevs) { [commit, create_commit('another_path.txt' => 'Another blob')] } let(:expected_newrevs) { ['--not', '--all', '--not'] + newrevs.sort } let(:expected_blobs) do [ - have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18), - have_attributes(class: Gitlab::Git::Blob, id: another_blob, path: 'another_path.txt', size: 12) + have_attributes(class: Gitlab::Git::Blob, id: an_instance_of(String), path: 'nested/new-blob.txt', size: 18), + have_attributes(class: Gitlab::Git::Blob, id: an_instance_of(String), path: 'another_path.txt', size: 12) ] end @@ -1147,7 +1109,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do let(:expected_newrevs) { ['--not', '--all', '--not', commit] } let(:expected_blobs) do [ - have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18) + have_attributes(class: Gitlab::Git::Blob, id: an_instance_of(String), path: 'nested/new-blob.txt', size: 18) ] end @@ -1159,7 +1121,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do let(:expected_newrevs) { ['--not', '--all', '--not', commit] } let(:expected_blobs) do [ - have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18) + have_attributes(class: Gitlab::Git::Blob, id: an_instance_of(String), path: 'nested/new-blob.txt', size: 18) ] end @@ -1212,14 +1174,22 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#new_commits' do let(:repository) { mutable_repository } let(:new_commit) do - author = { name: 'Test User', email: 'mail@example.com', time: Time.now } + commit_result = repository.commit_files( + user, + branch_name: 'a-new-branch', + message: 'Message', + actions: [{ + action: :create, + file_path: 'some_file.txt', + content: 'This is a file' + }] + ) + + # new_commits only returns unreferenced commits because it is used for + # hooks. Gitaly does not allow us to create loose objects via the RPC. + repository.delete_branch('a-new-branch') - Rugged::Commit.create(repository_rugged, - author: author, - committer: author, - message: "Message", - parents: [], - tree: "4b825dc642cb6eb9a060e54bf8d69288fbee4904") + commit_result.newrev end let(:expected_commits) { 1 } @@ -1248,7 +1218,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#count_commits_between' do subject { repository.count_commits_between('feature', 'master') } - it { is_expected.to eq(17) } + it { is_expected.to eq(29) } end describe '#raw_changes_between' do @@ -1275,26 +1245,26 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end end - context 'with valid revs' do - let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' } - let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } + context 'with valid revs', :aggregate_failures do + let(:old_rev) { TestEnv::BRANCH_SHA['feature'] } + let(:new_rev) { TestEnv::BRANCH_SHA['master'] } it 'returns the changes' do - expect(changes.size).to eq(9) - expect(changes.first.operation).to eq(:modified) - expect(changes.first.new_path).to eq('.gitmodules') + expect(changes.size).to eq(21) + expect(changes.first.operation).to eq(:deleted) + expect(changes.first.old_path).to eq('.DS_Store') expect(changes.last.operation).to eq(:added) - expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png') + expect(changes.last.new_path).to eq('with space/README.md') end end end describe '#merge_base' do where(:from, :to, :result) do - '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' - '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' - '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | 'foobar' | nil - 'foobar' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | nil + 'master' | 'feature' | 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f' + 'feature' | 'master' | 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f' + 'master' | 'foobar' | nil + 'foobar' | 'master' | nil end with_them do @@ -1308,7 +1278,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it 'returns the number of commits after timestamp' do options = { ref: 'master', after: Time.iso8601('2013-03-03T20:15:01+00:00') } - expect(repository.count_commits(options)).to eq(25) + expect(repository.count_commits(options)).to eq(37) end end @@ -1337,28 +1307,28 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end context 'with option :from and option :to' do - it 'returns the number of commits ahead for fix-mode..fix-blob-path' do - options = { from: 'fix-mode', to: 'fix-blob-path' } + it 'returns the number of commits ahead for master..feature' do + options = { from: 'master', to: 'feature' } - expect(repository.count_commits(options)).to eq(2) + expect(repository.count_commits(options)).to eq(1) end - it 'returns the number of commits ahead for fix-blob-path..fix-mode' do - options = { from: 'fix-blob-path', to: 'fix-mode' } + it 'returns the number of commits ahead for feature..master' do + options = { from: 'feature', to: 'master' } - expect(repository.count_commits(options)).to eq(1) + expect(repository.count_commits(options)).to eq(29) end context 'with option :left_right' do - it 'returns the number of commits for fix-mode...fix-blob-path' do - options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true } + it 'returns the number of commits for master..feature' do + options = { from: 'master', to: 'feature', left_right: true } - expect(repository.count_commits(options)).to eq([1, 2]) + expect(repository.count_commits(options)).to eq([29, 1]) end context 'with max_count' do - it 'returns the number of commits with path' do - options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true, max_count: 1 } + it 'returns the number of commits' do + options = { from: 'feature', to: 'master', left_right: true, max_count: 1 } expect(repository.count_commits(options)).to eq([1, 1]) end @@ -1378,7 +1348,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it "returns the number of commits in the whole repository" do options = { all: true } - expect(repository.count_commits(options)).to eq(34) + expect(repository.count_commits(options)).to eq(314) end end @@ -1416,10 +1386,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do repository.create_branch('local_branch') end - after do - ensure_seeds - end - it 'returns the local and remote branches' do expect(subject.any? { |b| b.name == 'joe/remote_branch' }).to eq(true) expect(subject.any? { |b| b.name == 'local_branch' }).to eq(true) @@ -1431,7 +1397,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#branch_count' do it 'returns the number of branches' do - expect(repository.branch_count).to eq(11) + expect(repository.branch_count).to eq(TestEnv::BRANCH_SHA.size) end context 'with local and remote branches' do @@ -1442,10 +1408,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do repository.create_branch('local_branch') end - after do - ensure_seeds - end - it 'returns the count of local branches' do expect(repository.branch_count).to eq(repository.local_branches.count) end @@ -1488,21 +1450,16 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end context 'when no branch names are specified' do + let(:repository) { mutable_repository } + before do repository.create_branch('identical') end - after do - ensure_seeds - end - it 'returns all merged branch names except for identical one' do names = repository.merged_branch_names - expect(names).to include('merge-test') - expect(names).to include('fix-mode') - expect(names).not_to include('feature') - expect(names).not_to include('identical') + expect(names).to match_array(["'test'", "branch-merged", "flatten-dir", "improve/awesome", "merge-test"]) end end end @@ -1556,24 +1513,15 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end describe '#find_changed_paths' do - let(:commit_1) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' } - let(:commit_2) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } + let(:commit_1) { TestEnv::BRANCH_SHA['with-executables'] } + let(:commit_2) { TestEnv::BRANCH_SHA['master'] } let(:commit_3) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' } let(:commit_1_files) do - [ - Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/executables/ls"), - Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/executables/touch"), - Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/regex.rb"), - Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/ruby-style-guide.md"), - Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/touch"), - Gitlab::Git::ChangedPath.new(status: :MODIFIED, path: ".gitmodules"), - Gitlab::Git::ChangedPath.new(status: :ADDED, path: "deeper/nested/six"), - Gitlab::Git::ChangedPath.new(status: :ADDED, path: "nested/six") - ] + [Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/executables/ls")] end let(:commit_2_files) do - [Gitlab::Git::ChangedPath.new(status: :ADDED, path: "bin/executable")] + [Gitlab::Git::ChangedPath.new(status: :ADDED, path: "bar/branch-test.txt")] end let(:commit_3_files) do @@ -1621,7 +1569,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do let(:not_existed_branch) { repository.ls_files("not_existed_branch") } it "read every file paths of master branch" do - expect(master_file_paths.length).to equal(40) + expect(master_file_paths.length).to equal(38) end it "reads full file paths of master branch" do @@ -1646,11 +1594,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end describe "#copy_gitattributes" do - let(:attributes_path) { File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info/attributes') } - - after do - FileUtils.rm_rf(attributes_path) if Dir.exist?(attributes_path) - end + let(:repository) { mutable_repository } it "raises an error with invalid ref" do expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef) @@ -1673,63 +1617,10 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do repository end end - - context "with no .gitattrbutes" do - before do - repository.copy_gitattributes("master") - end - - it "does not have an info/attributes" do - expect(File.exist?(attributes_path)).to be_falsey - end - end - - context "with .gitattrbutes" do - before do - repository.copy_gitattributes("gitattributes") - end - - it "has an info/attributes" do - expect(File.exist?(attributes_path)).to be_truthy - end - - it "has the same content in info/attributes as .gitattributes" do - contents = File.open(attributes_path, "rb") { |f| f.read } - expect(contents).to eq("*.md binary\n") - end - end - - context "with updated .gitattrbutes" do - before do - repository.copy_gitattributes("gitattributes") - repository.copy_gitattributes("gitattributes-updated") - end - - it "has an info/attributes" do - expect(File.exist?(attributes_path)).to be_truthy - end - - it "has the updated content in info/attributes" do - contents = File.read(attributes_path) - expect(contents).to eq("*.txt binary\n") - end - end - - context "with no .gitattrbutes in HEAD but with previous info/attributes" do - before do - repository.copy_gitattributes("gitattributes") - repository.copy_gitattributes("master") - end - - it "does not have an info/attributes" do - expect(File.exist?(attributes_path)).to be_falsey - end - end end describe '#gitattribute' do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository } + let(:repository) { mutable_repository } context 'with gitattributes' do before do @@ -1808,10 +1699,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do repository.create_branch('local_branch') end - after do - ensure_seeds - end - it 'returns the local branches' do expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false) expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) @@ -1880,7 +1767,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#languages' do it 'returns exactly the expected results' do - languages = repository.languages('4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6') + languages = repository.languages(TestEnv::BRANCH_SHA['master']) expect(languages).to match_array([ { value: a_value_within(0.1).of(66.7), label: "Ruby", color: "#701516", highlight: "#701516" }, @@ -1918,18 +1805,15 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#fetch_source_branch!' do let(:local_ref) { 'refs/merge-requests/1/head' } + let(:repository) { create(:project, :repository).repository.raw } let(:source_repository) { mutable_repository } - after do - ensure_seeds - end - context 'when the branch exists' do context 'when the commit does not exist locally' do let(:source_branch) { 'new-branch-for-fetch-source-branch' } let!(:new_oid) do - source_repository.multi_action( + source_repository.commit_files( user, branch_name: source_branch, message: 'Add a file', @@ -1949,14 +1833,14 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do context 'when the commit exists locally' do let(:source_branch) { 'master' } - let(:expected_oid) { SeedRepo::LastCommit::ID } + let(:expected_oid) { TestEnv::BRANCH_SHA['master'] } it 'writes the ref' do # Sanity check: the commit should already exist expect(repository.commit(expected_oid)).not_to be_nil expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true) - expect(repository.commit(local_ref).sha).to eq(expected_oid) + expect(repository.commit(local_ref).sha).to start_with(expected_oid) end end end @@ -2012,9 +1896,9 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end it 'writes other refs' do - repository.write_ref('refs/heads/feature', SeedRepo::Commit::ID) + repository.write_ref('refs/heads/feature', TestEnv::BRANCH_SHA['master']) - expect(repository.commit('feature').sha).to eq(SeedRepo::Commit::ID) + expect(repository.commit('feature').sha).to start_with(TestEnv::BRANCH_SHA['master']) end end @@ -2052,28 +1936,28 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it 'returns nil for an empty repo' do project = create(:project) - expect(project.repository.refs_by_oid(oid: SeedRepo::Commit::ID, limit: 0)).to be_nil + expect(project.repository.refs_by_oid(oid: TestEnv::BRANCH_SHA['master'], limit: 0)).to be_nil end end describe '#set_full_path' do + let(:full_path) { 'some/path' } + before do - repository.set_full_path(full_path: repository_path) + repository.set_full_path(full_path: full_path) end - context 'is given a path' do - it 'writes it to disk' do - repository.set_full_path(full_path: "not-the/real-path.git") + it 'writes full_path to gitaly' do + repository.set_full_path(full_path: "not-the/real-path.git") - expect(repository.full_path).to eq('not-the/real-path.git') - end + expect(repository.full_path).to eq('not-the/real-path.git') end context 'it is given an empty path' do it 'does not write it to disk' do repository.set_full_path(full_path: "") - expect(repository.full_path).to eq(repository_path) + expect(repository.full_path).to eq(full_path) end end @@ -2145,10 +2029,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do repository.create_branch(target_branch, '6d394385cf567f80a8fd85055db1ab4c5295806f') end - after do - ensure_seeds - end - it 'can perform a merge' do merge_commit_id = nil result = repository.merge(user, source_sha, target_branch, 'Test merge') do |commit_id| @@ -2185,10 +2065,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do repository.create_branch(target_branch, branch_head) end - after do - ensure_seeds - end - subject { repository.ff_merge(user, source_sha, target_branch) } shared_examples '#ff_merge' do @@ -2242,14 +2118,10 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do let(:repository) { mutable_repository } before do - repository.write_ref("refs/delete/a", "0b4bc9a49b562e85de7cc9e834518ea6828729b9") - repository.write_ref("refs/also-delete/b", "12d65c8dd2b2676fa3ac47d955accc085a37a9c1") - repository.write_ref("refs/keep/c", "6473c90867124755509e100d0d35ebdc85a0b6ae") - repository.write_ref("refs/also-keep/d", "0b4bc9a49b562e85de7cc9e834518ea6828729b9") - end - - after do - ensure_seeds + repository.write_ref("refs/delete/a", TestEnv::BRANCH_SHA['master']) + repository.write_ref("refs/also-delete/b", TestEnv::BRANCH_SHA['master']) + repository.write_ref("refs/keep/c", TestEnv::BRANCH_SHA['master']) + repository.write_ref("refs/also-keep/d", TestEnv::BRANCH_SHA['master']) end it 'deletes all refs except those with the specified prefixes' do @@ -2272,11 +2144,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it 'saves a bundle to disk' do repository.bundle_to_disk(save_path) - success = system( - *%W(#{Gitlab.config.git.bin_path} -C #{repository_path} bundle verify #{save_path}), - [:out, :err] => '/dev/null' - ) - expect(success).to be true + expect(File).to exist(save_path) end end @@ -2326,41 +2194,22 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#checksum' do it 'calculates the checksum for non-empty repo' do - expect(repository.checksum).to eq '51d0a9662681f93e1fee547a6b7ba2bcaf716059' - end - - it 'returns 0000000000000000000000000000000000000000 for an empty repo' do - FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git')) - - system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git), - chdir: storage_path, - out: '/dev/null', - err: '/dev/null') - - empty_repo = described_class.new('default', 'empty-repo.git', '', 'group/empty-repo') - - expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000' + expect(repository.checksum.length).to be(40) + expect(Gitlab::Git.blank_ref?(repository.checksum)).to be false end - it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do - FileUtils.rm_rf(File.join(storage_path, 'non-valid.git')) - - system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git), - chdir: SEED_STORAGE_PATH, - out: '/dev/null', - err: '/dev/null') - - File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0) + it 'returns a blank sha for an empty repo' do + repository = create(:project, :empty_repo).repository - non_valid = described_class.new('default', 'non-valid.git', '', 'a/non-valid') - - expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository) + expect(Gitlab::Git.blank_ref?(repository.checksum)).to be true end - it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do - broken_repo = described_class.new('default', 'a/path.git', '', 'a/path') + it 'raises NoRepository for a non-existent repo' do + repository = create(:project).repository - expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository) + expect do + repository.checksum + end.to raise_error(described_class::NoRepository) end end @@ -2375,7 +2224,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#squash' do let(:branch_name) { 'fix' } - let(:start_sha) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } + let(:start_sha) { TestEnv::BRANCH_SHA['master'] } let(:end_sha) { '12d65c8dd2b2676fa3ac47d955accc085a37a9c1' } subject do @@ -2412,7 +2261,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do context 'when the diff contains a rename' do let(:end_sha) do - repository.multi_action( + repository.commit_files( user, branch_name: repository.root_ref, message: 'Move CHANGELOG to encoding/', @@ -2427,7 +2276,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do after do # Erase our commits so other tests get the original repo - repository.write_ref(repository.root_ref, SeedRepo::LastCommit::ID) + repository.write_ref(repository.root_ref, TestEnv::BRANCH_SHA['master']) end it 'does not include the renamed file in the sparse checkout' do @@ -2480,9 +2329,9 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end describe '#disconnect_alternates' do - let(:project) { create(:project, :repository) } + let(:project) { mutable_project } + let(:repository) { mutable_repository } let(:pool_repository) { create(:pool_repository) } - let(:repository) { project.repository } let(:object_pool) { pool_repository.object_pool } before do @@ -2495,7 +2344,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it 'can still access objects in the object pool' do object_pool.link(repository) - new_commit_id = object_pool.repository.multi_action( + new_commit_id = object_pool.repository.commit_files( project.owner, branch_name: object_pool.repository.root_ref, message: 'Add a file', @@ -2515,8 +2364,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end describe '#rename' do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository } + let(:repository) { mutable_repository } it 'moves the repository' do checksum = repository.checksum @@ -2531,15 +2379,14 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end describe '#remove' do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository } + let(:repository) { mutable_repository } it 'removes the repository' do expect(repository.exists?).to be true repository.remove - expect(repository.raw_repository.exists?).to be false + expect(repository.exists?).to be false end context 'when the repository does not exist' do @@ -2550,15 +2397,14 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do repository.remove - expect(repository.raw_repository.exists?).to be false + expect(repository.exists?).to be false end end end describe '#import_repository' do - let_it_be(:project) { create(:project) } + let_it_be(:repository) { create(:project).repository } - let(:repository) { project.repository } let(:url) { 'http://invalid.invalid' } it 'raises an error if a relative path is provided' do @@ -2584,11 +2430,9 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#replicate' do let(:new_repository) do - Gitlab::Git::Repository.new('test_second_storage', TEST_REPO_PATH, '', 'group/project') + Gitlab::Git::Repository.new('test_second_storage', repository.relative_path, '', 'group/project') end - let(:new_repository_path) { File.join(TestEnv::SECOND_STORAGE_PATH, new_repository.relative_path) } - subject { new_repository.replicate(repository) } before do @@ -2622,7 +2466,8 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end context 'with keep-around refs' do - let(:sha) { SeedRepo::Commit::ID } + let(:repository) { mutable_repository } + let(:sha) { TestEnv::BRANCH_SHA['master'] } let(:keep_around_ref) { "refs/keep-around/#{sha}" } let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" } 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 03d1c125e36..747611a59e6 100644 --- a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb +++ b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' require 'json' require 'tempfile' -RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do +RSpec.describe Gitlab::Git::RuggedImpl::UseRugged do let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:feature_flag_name) { wrapper.rugged_feature_keys.first } diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 2e4520cd3a0..7c84c737c00 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -95,7 +95,7 @@ RSpec.describe Gitlab::Git::Tree do let(:subdir_file) { entries.first } # rubocop: enable Rails/FindBy let!(:sha) do - repository.multi_action( + repository.commit_files( user, branch_name: 'HEAD', message: "Create #{filename}", diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb index a0237c821b5..dd925a902ab 100644 --- a/spec/lib/gitlab/git/util_spec.rb +++ b/spec/lib/gitlab/git/util_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Git::Util do describe '#count_lines' do diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb index dddcf8c40fc..05c7ac149e4 100644 --- a/spec/lib/gitlab/git/wiki_spec.rb +++ b/spec/lib/gitlab/git/wiki_spec.rb @@ -8,9 +8,15 @@ RSpec.describe Gitlab::Git::Wiki do let(:project) { create(:project) } let(:user) { project.first_owner } let(:project_wiki) { ProjectWiki.new(project, user) } + let(:repository) { project_wiki.repository } + let(:default_branch) { described_class.default_ref(project) } subject(:wiki) { project_wiki.wiki } + before do + repository.create_if_not_exists(project_wiki.default_branch) + end + describe '#pages' do before do create_page('page1', 'content') @@ -44,7 +50,7 @@ RSpec.describe Gitlab::Git::Wiki do after do destroy_page('page1') - destroy_page('page1', 'foo') + destroy_page('foo/page1') end it 'returns the right page' do @@ -71,20 +77,20 @@ RSpec.describe Gitlab::Git::Wiki do end describe '#preview_slug' do - where(:title, :format, :expected_slug) do - 'The Best Thing' | :markdown | 'The-Best-Thing' - 'The Best Thing' | :md | 'The-Best-Thing' - 'The Best Thing' | :txt | 'The-Best-Thing' - 'A Subject/Title Here' | :txt | 'A-Subject/Title-Here' - 'A subject' | :txt | 'A-subject' - 'A 1/B 2/C 3' | :txt | 'A-1/B-2/C-3' - 'subject/title' | :txt | 'subject/title' - 'subject/title.md' | :txt | 'subject/title.md' - 'foo<bar>+baz' | :txt | 'foo-bar--baz' - 'foo%2Fbar' | :txt | 'foo%2Fbar' - '' | :markdown | '.md' - '' | :md | '.md' - '' | :txt | '.txt' + where(:title, :file_extension, :format, :expected_slug) do + 'The Best Thing' | :md | :markdown | 'The-Best-Thing' + 'The Best Thing' | :md | :md | 'The-Best-Thing' + 'The Best Thing' | :txt | :txt | 'The-Best-Thing' + 'A Subject/Title Here' | :txt | :txt | 'A-Subject/Title-Here' + 'A subject' | :txt | :txt | 'A-subject' + 'A 1/B 2/C 3' | :txt | :txt | 'A-1/B-2/C-3' + 'subject/title' | :txt | :txt | 'subject/title' + 'subject/title.md' | :txt | :txt | 'subject/title.md' + 'foo<bar>+baz' | :txt | :txt | 'foo-bar--baz' + 'foo%2Fbar' | :txt | :txt | 'foo%2Fbar' + '' | :md | :markdown | '.md' + '' | :md | :md | '.md' + '' | :txt | :txt | '.txt' end with_them do @@ -97,7 +103,7 @@ RSpec.describe Gitlab::Git::Wiki do it 'matches the slug generated by gitaly' do skip('Gitaly cannot generate a slug for an empty title') unless title.present? - create_page(title, 'content', format: format) + create_page(title, 'content', file_extension) gitaly_slug = wiki.list_pages.first.url_path @@ -106,16 +112,23 @@ RSpec.describe Gitlab::Git::Wiki do end end - def create_page(name, content, format: :markdown) - wiki.write_page(name, format, content, commit_details(name)) - end - - def commit_details(name) - Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "created page #{name}") + def create_page(name, content, extension = :md) + repository.create_file( + user, ::Wiki.sluggified_full_path(name, extension.to_s), content, + branch_name: default_branch, + message: "created page #{name}", + author_email: user.email, + author_name: user.name + ) end - def destroy_page(title, dir = '') - page = wiki.page(title: title, dir: dir) - project_wiki.delete_page(page, "test commit") + def destroy_page(name, extension = :md) + repository.delete_file( + user, ::Wiki.sluggified_full_path(name, extension.to_s), + branch_name: described_class.default_ref(project), + message: "delete page #{name}", + author_email: user.email, + author_name: user.name + ) end end diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb index f359679a930..0f6ef55b4b1 100644 --- a/spec/lib/gitlab/git_spec.rb +++ b/spec/lib/gitlab/git_spec.rb @@ -7,10 +7,18 @@ RSpec.describe Gitlab::Git do let(:committer_name) { 'John Doe' } describe '.ref_name' do + let(:ref) { Gitlab::Git::BRANCH_REF_PREFIX + "an_invalid_ref_\xE5" } + it 'ensure ref is a valid UTF-8 string' do - utf8_invalid_ref = Gitlab::Git::BRANCH_REF_PREFIX + "an_invalid_ref_\xE5" + expect(described_class.ref_name(ref)).to eq("an_invalid_ref_%E5") + end - expect(described_class.ref_name(utf8_invalid_ref)).to eq("an_invalid_ref_å") + context 'when ref contains characters \x80 - \xFF' do + let(:ref) { Gitlab::Git::BRANCH_REF_PREFIX + "\x90" } + + it 'correctly converts it' do + expect(described_class.ref_name(ref)).to eq("%90") + end end end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index ed6a87cda6f..ff3cade07c0 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -297,6 +297,11 @@ RSpec.describe Gitlab::GitalyClient::CommitService do describe '#list_commits' do let(:revisions) { 'master' } let(:reverse) { false } + let(:author) { nil } + let(:ignore_case) { nil } + let(:commit_message_patterns) { nil } + let(:before) { nil } + let(:after) { nil } let(:pagination_params) { nil } shared_examples 'a ListCommits request' do @@ -309,13 +314,18 @@ RSpec.describe Gitlab::GitalyClient::CommitService do expected_request = gitaly_request_with_params( Array.wrap(revisions), reverse: reverse, + author: author, + ignore_case: ignore_case, + commit_message_patterns: commit_message_patterns, + before: before, + after: after, pagination_params: pagination_params ) expect(service).to receive(:list_commits).with(expected_request, kind_of(Hash)).and_return([]) end - client.list_commits(revisions, reverse: reverse, pagination_params: pagination_params) + client.list_commits(revisions, { reverse: reverse, author: author, ignore_case: ignore_case, commit_message_patterns: commit_message_patterns, before: before, after: after, pagination_params: pagination_params }) end end @@ -333,7 +343,12 @@ RSpec.describe Gitlab::GitalyClient::CommitService do it_behaves_like 'a ListCommits request' end - context 'with pagination params' do + context 'with commit message, author, before and after' do + let(:author) { "Dmitriy" } + let(:before) { 1474828200 } + let(:after) { 1474828200 } + let(:commit_message_patterns) { "Initial commit" } + let(:ignore_case) { true } let(:pagination_params) { { limit: 1, page_token: 'foo' } } it_behaves_like 'a ListCommits request' diff --git a/spec/lib/gitlab/gitaly_client/diff_spec.rb b/spec/lib/gitlab/gitaly_client/diff_spec.rb index 230322faecd..2c1f684c0c5 100644 --- a/spec/lib/gitlab/gitaly_client/diff_spec.rb +++ b/spec/lib/gitlab/gitaly_client/diff_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::GitalyClient::Diff do let(:diff_fields) do diff --git a/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb b/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb index 54c84ddc56f..39fd752ef7f 100644 --- a/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb +++ b/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::GitalyClient::DiffStitcher do describe 'enumeration' do diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 5d854f0c9d1..7e8aaa3cdf4 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -56,6 +56,85 @@ RSpec.describe Gitlab::GitalyClient::OperationService do Gitlab::Git::PreReceiveError, "something failed") end end + + context 'with structured errors' do + context 'with CustomHookError' do + let(:stdout) { nil } + let(:stderr) { nil } + let(:error_message) { "error_message" } + + let(:custom_hook_error) do + new_detailed_error( + GRPC::Core::StatusCodes::PERMISSION_DENIED, + error_message, + Gitaly::UserCreateBranchError.new( + custom_hook: Gitaly::CustomHookError.new( + stdout: stdout, + stderr: stderr, + hook_type: Gitaly::CustomHookError::HookType::HOOK_TYPE_PRERECEIVE + ))) + end + + shared_examples 'failed branch creation' do + it 'raised a PreRecieveError' do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_create_branch) + .and_raise(custom_hook_error) + + expect { subject }.to raise_error do |error| + expect(error).to be_a(Gitlab::Git::PreReceiveError) + expect(error.message).to eq(expected_message) + expect(error.raw_message).to eq(expected_raw_message) + end + end + end + + context 'when details contain stderr without prefix' do + let(:stderr) { "something" } + let(:stdout) { "GL-HOOK-ERR: stdout is overridden by stderr" } + let(:expected_message) { error_message } + let(:expected_raw_message) { stderr } + + it_behaves_like 'failed branch creation' + end + + context 'when details contain stderr with prefix' do + let(:stderr) { "GL-HOOK-ERR: something" } + let(:stdout) { "GL-HOOK-ERR: stdout is overridden by stderr" } + let(:expected_message) { "something" } + let(:expected_raw_message) { stderr } + + it_behaves_like 'failed branch creation' + end + + context 'when details contain stdout without prefix' do + let(:stderr) { " \n" } + let(:stdout) { "something" } + let(:expected_message) { error_message } + let(:expected_raw_message) { stdout } + + it_behaves_like 'failed branch creation' + end + + context 'when details contain stdout with prefix' do + let(:stderr) { " \n" } + let(:stdout) { "GL-HOOK-ERR: something" } + let(:expected_message) { "something" } + let(:expected_raw_message) { stdout } + + it_behaves_like 'failed branch creation' + end + + context 'when details contain no stderr or stdout' do + let(:stderr) { " \n" } + let(:stdout) { "\n \n" } + let(:expected_message) { error_message } + let(:expected_raw_message) { "\n \n" } + + it_behaves_like 'failed branch creation' + end + end + end end describe '#user_update_branch' do diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 277276bb1d3..b7c21516c77 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -156,35 +156,84 @@ RSpec.describe Gitlab::GitalyClient::RefService do end describe '#local_branches' do - it 'sends a find_local_branches message' do - expect_any_instance_of(Gitaly::RefService::Stub) - .to receive(:find_local_branches) - .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) - .and_return([]) + let(:remote_name) { 'my_remote' } - client.local_branches - end + shared_examples 'common examples' do + it 'sends a find_local_branches message' do + target_commits = create_list(:gitaly_commit, 4) + branches = target_commits.each_with_index.map do |gitaly_commit, i| + Gitaly::FindLocalBranchResponse.new( + name: "#{remote_name}/#{i}", + commit: gitaly_commit, + commit_author: Gitaly::FindLocalBranchCommitAuthor.new( + name: gitaly_commit.author.name, + email: gitaly_commit.author.email, + date: gitaly_commit.author.date, + timezone: gitaly_commit.author.timezone + ), + commit_committer: Gitaly::FindLocalBranchCommitAuthor.new( + name: gitaly_commit.committer.name, + email: gitaly_commit.committer.email, + date: gitaly_commit.committer.date, + timezone: gitaly_commit.committer.timezone + ) + ) + end + local_branches = target_commits.each_with_index.map do |gitaly_commit, i| + Gitaly::Branch.new(name: "#{remote_name}/#{i}", target_commit: gitaly_commit) + end + response = [ + Gitaly::FindLocalBranchesResponse.new(branches: branches[0, 2], local_branches: local_branches[0, 2]), + Gitaly::FindLocalBranchesResponse.new(branches: branches[2, 2], local_branches: local_branches[2, 2]) + ] - it 'parses and sends the sort parameter' do - expect_any_instance_of(Gitaly::RefService::Stub) - .to receive(:find_local_branches) - .with(gitaly_request_with_params(sort_by: :UPDATED_DESC), kind_of(Hash)) - .and_return([]) + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:find_local_branches) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(response) + + subject = client.local_branches + + expect(subject.length).to be(target_commits.length) + end + + it 'parses and sends the sort parameter' do + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:find_local_branches) + .with(gitaly_request_with_params(sort_by: :UPDATED_DESC), kind_of(Hash)) + .and_return([]) + + client.local_branches(sort_by: 'updated_desc') + end + + it 'translates known mismatches on sort param values' do + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:find_local_branches) + .with(gitaly_request_with_params(sort_by: :NAME), kind_of(Hash)) + .and_return([]) + + client.local_branches(sort_by: 'name_asc') + end - client.local_branches(sort_by: 'updated_desc') + it 'raises an argument error if an invalid sort_by parameter is passed' do + expect { client.local_branches(sort_by: 'invalid_sort') }.to raise_error(ArgumentError) + end end - it 'translates known mismatches on sort param values' do - expect_any_instance_of(Gitaly::RefService::Stub) - .to receive(:find_local_branches) - .with(gitaly_request_with_params(sort_by: :NAME), kind_of(Hash)) - .and_return([]) + context 'when feature flag :gitaly_simplify_find_local_branches_response is enabled' do + before do + stub_feature_flags(gitaly_simplify_find_local_branches_response: true) + end - client.local_branches(sort_by: 'name_asc') + it_behaves_like 'common examples' end - it 'raises an argument error if an invalid sort_by parameter is passed' do - expect { client.local_branches(sort_by: 'invalid_sort') }.to raise_error(ArgumentError) + context 'when feature flag :gitaly_simplify_find_local_branches_response is disabled' do + before do + stub_feature_flags(gitaly_simplify_find_local_branches_response: false) + end + + it_behaves_like 'common examples' end end @@ -211,6 +260,22 @@ RSpec.describe Gitlab::GitalyClient::RefService do client.tags(sort_by: 'name_asc') end + + context 'with semantic version sorting' do + it 'sends a correct find_all_tags message' do + expected_sort_by = Gitaly::FindAllTagsRequest::SortBy.new( + key: :VERSION_REFNAME, + direction: :ASCENDING + ) + + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:find_all_tags) + .with(gitaly_request_with_params(sort_by: expected_sort_by), kind_of(Hash)) + .and_return([]) + + client.tags(sort_by: 'version_asc') + end + end end context 'with pagination option' do diff --git a/spec/lib/gitlab/gitaly_client/server_service_spec.rb b/spec/lib/gitlab/gitaly_client/server_service_spec.rb new file mode 100644 index 00000000000..615f2ce0c21 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/server_service_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GitalyClient::ServerService do + let(:storage) { 'default' } + + describe '#readiness_check' do + before do + ::Gitlab::GitalyClient.clear_stubs! + end + + let(:request) do + Gitaly::ReadinessCheckRequest.new(timeout: 30) + end + + subject(:readiness_check) { described_class.new(storage).readiness_check } + + it 'returns a positive success if no failures happened' do + expect_next_instance_of(Gitaly::ServerService::Stub) do |service| + response = Gitaly::ReadinessCheckResponse.new(ok_response: Gitaly::ReadinessCheckResponse::Ok.new) + expect(service).to receive(:readiness_check).with(request, kind_of(Hash)).and_return(response) + end + + expect(readiness_check[:success]).to eq(true) + end + + it 'returns a negative success and a compiled message if at least one failure happened' do + failure1 = Gitaly::ReadinessCheckResponse::Failure::Response.new(name: '1', error_message: 'msg 1') + failure2 = Gitaly::ReadinessCheckResponse::Failure::Response.new(name: '2', error_message: 'msg 2') + failures = Gitaly::ReadinessCheckResponse::Failure.new(failed_checks: [failure1, failure2]) + response = Gitaly::ReadinessCheckResponse.new(failure_response: failures) + + expect_next_instance_of(Gitaly::ServerService::Stub) do |service| + expect(service).to receive(:readiness_check).with(request, kind_of(Hash)).and_return(response) + end + + expect(readiness_check[:success]).to eq(false) + expect(readiness_check[:message]).to eq("1: msg 1\n2: msg 2") + end + end +end diff --git a/spec/lib/gitlab/gitaly_client/util_spec.rb b/spec/lib/gitlab/gitaly_client/util_spec.rb index b6589a08f7d..ae7c3789051 100644 --- a/spec/lib/gitlab/gitaly_client/util_spec.rb +++ b/spec/lib/gitlab/gitaly_client/util_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::GitalyClient::Util do describe '.repository' do diff --git a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb new file mode 100644 index 00000000000..57391e06192 --- /dev/null +++ b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::AttachmentsDownloader do + subject(:downloader) { described_class.new(file_url) } + + let_it_be(:file_url) { 'https://example.com/avatar.png' } + let_it_be(:content_type) { 'application/octet-stream' } + + let(:content_length) { 1000 } + let(:chunk_double) { instance_double(HTTParty::FragmentWithResponse, code: 200) } + let(:headers_double) do + instance_double( + HTTParty::Response, + code: 200, + success?: true, + parsed_response: {}, + headers: { + 'content-length' => content_length, + 'content-type' => content_type + } + ) + end + + describe '#perform' do + before do + allow(Gitlab::HTTP).to receive(:perform_request) + .with(Net::HTTP::Get, file_url, stream_body: true).and_yield(chunk_double) + allow(Gitlab::HTTP).to receive(:perform_request) + .with(Net::HTTP::Head, file_url, {}).and_return(headers_double) + end + + context 'when file valid' do + it 'downloads file' do + file = downloader.perform + + expect(File.exist?(file.path)).to eq(true) + end + end + + context 'when filename is malicious' do + let_it_be(:file_url) { 'https://example.com/ava%2F..%2Ftar.png' } + + it 'raises expected exception' do + expect { downloader.perform }.to raise_exception( + Gitlab::Utils::PathTraversalAttackError, + 'Invalid path' + ) + end + end + + context 'when file size exceeds limit' do + let(:content_length) { 26.megabytes } + + it 'raises expected exception' do + expect { downloader.perform }.to raise_exception( + Gitlab::GithubImport::AttachmentsDownloader::DownloadError, + 'File size 26 MB exceeds limit of 25 MB' + ) + end + end + + context 'when file name length exceeds limit' do + before do + stub_const('BulkImports::FileDownloads::FilenameFetch::FILENAME_SIZE_LIMIT', 2) + end + + it 'chops filename' do + file = downloader.perform + + expect(File.exist?(file.path)).to eq(true) + expect(File.basename(file)).to eq('av.png') + end + end + end + + describe '#delete' do + let(:tmp_dir_path) { File.join(Dir.tmpdir, 'github_attachments_test') } + let(:file) do + downloader.mkdir_p(tmp_dir_path) + file = File.open("#{tmp_dir_path}/test.txt", 'wb') + file.write('foo') + file.close + file + end + + before do + allow(downloader).to receive(:filepath).and_return(file.path) + end + + it 'removes file with parent folder' do + downloader.delete + expect(Dir.exist?(tmp_dir_path)).to eq false + end + end +end diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index 2bd3910ad87..c88bb6de859 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -40,6 +40,22 @@ RSpec.describe Gitlab::GithubImport::Client do end end + describe '#repos' do + it 'returns the user\'s repositories as a hash' do + client = described_class.new('foo') + + stub_request(:get, 'https://api.github.com/rate_limit') + .to_return(status: 200, headers: { 'X-RateLimit-Limit' => 5000, 'X-RateLimit-Remaining' => 5000 }) + + stub_request(:get, 'https://api.github.com/user/repos?page=1&page_length=10&per_page=100') + .to_return(status: 200, body: [{ id: 1 }, { id: 2 }].to_json, headers: { 'Content-Type' => 'application/json' }) + + repos = client.repos({ page: 1, page_length: 10 }) + + expect(repos).to match_array([{ id: 1 }, { id: 2 }]) + end + end + describe '#repository' do it 'returns the details of a repository' do client = described_class.new('foo') @@ -49,6 +65,20 @@ RSpec.describe Gitlab::GithubImport::Client do client.repository('foo/bar') end + + it 'returns repository data as a hash' do + client = described_class.new('foo') + + stub_request(:get, 'https://api.github.com/rate_limit') + .to_return(status: 200, headers: { 'X-RateLimit-Limit' => 5000, 'X-RateLimit-Remaining' => 5000 }) + + stub_request(:get, 'https://api.github.com/repos/foo/bar') + .to_return(status: 200, body: { id: 1 }.to_json, headers: { 'Content-Type' => 'application/json' }) + + repository = client.repository('foo/bar') + + expect(repository).to eq({ id: 1 }) + end end describe '#pull_request' do @@ -98,6 +128,30 @@ RSpec.describe Gitlab::GithubImport::Client do end end + describe '#branches' do + it 'returns the branches' do + client = described_class.new('foo') + + expect(client) + .to receive(:each_object) + .with(:branches, 'foo/bar') + + client.branches('foo/bar') + end + end + + describe '#branch_protection' do + it 'returns the protection details for the given branch' do + client = described_class.new('foo') + + expect(client.octokit) + .to receive(:branch_protection).with('org/repo', 'bar') + expect(client).to receive(:with_rate_limit).and_yield + + client.branch_protection('org/repo', 'bar') + end + end + describe '#each_page' do let(:client) { described_class.new('foo') } let(:object1) { double(:object1) } @@ -234,7 +288,7 @@ RSpec.describe Gitlab::GithubImport::Client do expect(client).to receive(:requests_remaining?).twice.and_return(true) expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once - expect(client.with_rate_limit(&block_to_rate_limit)).to be(true) + expect(client.with_rate_limit(&block_to_rate_limit)).to eq({}) end it 'retries and does not succeed' do @@ -255,7 +309,7 @@ RSpec.describe Gitlab::GithubImport::Client do expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once - expect(client.with_rate_limit(&block_to_rate_limit)).to be(true) + expect(client.with_rate_limit(&block_to_rate_limit)).to eq({}) end it 'retries and does not succeed' do @@ -559,7 +613,7 @@ RSpec.describe Gitlab::GithubImport::Client do expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once - expect(client.search_repos_by_name('test')).to be(true) + expect(client.search_repos_by_name('test')).to eq({}) end it 'retries and does not succeed' do @@ -599,7 +653,7 @@ RSpec.describe Gitlab::GithubImport::Client do call_count = 0 allow(client.octokit).to receive(method) do call_count += 1 - call_count > 1 ? true : raise(described_class::CLIENT_CONNECTION_ERROR, 'execution expired') + call_count > 1 ? {} : raise(described_class::CLIENT_CONNECTION_ERROR, 'execution expired') end end end diff --git a/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb index 2f6f727dc38..dbc72574ec2 100644 --- a/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb @@ -6,31 +6,30 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } + let_it_be(:author) { create(:user) } let_it_be(:assignee) { create(:user) } - let_it_be(:assigner) { create(:user) } let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:issue) { create(:issue, project: project) } + let(:issuable) { create(:issue, project: project) } let(:issue_event) do Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( 'id' => 6501124486, - 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'actor' => { 'id' => author.id, 'login' => author.username }, 'event' => event_type, 'commit_id' => nil, 'created_at' => '2022-04-26 18:30:53 UTC', - 'assigner' => { 'id' => assigner.id, 'login' => assigner.username }, 'assignee' => { 'id' => assignee.id, 'login' => assignee.username }, - 'issue' => { 'number' => issue.iid } + 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) } ) end let(:note_attrs) do { - noteable_id: issue.id, - noteable_type: Issue.name, + noteable_id: issuable.id, + noteable_type: issuable.class.name, project_id: project.id, - author_id: assigner.id, + author_id: author.id, system: true, created_at: issue_event.created_at, updated_at: issue_event.created_at @@ -45,12 +44,12 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do }.stringify_keys end - shared_examples 'new note' do + shared_examples 'create expected notes' do it 'creates expected note' do - expect { importer.execute(issue_event) }.to change { issue.notes.count } + expect { importer.execute(issue_event) }.to change { issuable.notes.count } .from(0).to(1) - expect(issue.notes.last) + expect(issuable.notes.last) .to have_attributes(expected_note_attrs) end @@ -67,29 +66,41 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do end end + shared_examples 'process assigned & unassigned events' do + context 'when importing an assigned event' do + let(:event_type) { 'assigned' } + let(:expected_note_attrs) { note_attrs.merge(note: "assigned to @#{assignee.username}") } + + it_behaves_like 'create expected notes' + end + + context 'when importing an unassigned event' do + let(:event_type) { 'unassigned' } + let(:expected_note_attrs) { note_attrs.merge(note: "unassigned @#{assignee.username}") } + + it_behaves_like 'create expected notes' + end + end + describe '#execute' do before do allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| - allow(finder).to receive(:database_id).and_return(issue.id) + allow(finder).to receive(:database_id).and_return(issuable.id) end allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(author.id, author.username).and_return(author.id) allow(finder).to receive(:find).with(assignee.id, assignee.username).and_return(assignee.id) - allow(finder).to receive(:find).with(assigner.id, assigner.username).and_return(assigner.id) end end - context 'when importing an assigned event' do - let(:event_type) { 'assigned' } - let(:expected_note_attrs) { note_attrs.merge(note: "assigned to @#{assignee.username}") } - - it_behaves_like 'new note' + context 'with Issue' do + it_behaves_like 'process assigned & unassigned events' end - context 'when importing an unassigned event' do - let(:event_type) { 'unassigned' } - let(:expected_note_attrs) { note_attrs.merge(note: "unassigned @#{assigner.username}") } + context 'with MergeRequest' do + let(:issuable) { create(:merge_request, source_project: project, target_project: project) } - it_behaves_like 'new note' + it_behaves_like 'process assigned & unassigned events' end end end diff --git a/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb index e21672aa430..4476b4123ee 100644 --- a/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do let_it_be(:user) { create(:user) } let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:issue) { create(:issue, project: project) } + let(:issuable) { create(:issue, project: project) } let!(:label) { create(:label, project: project) } let(:issue_event) do @@ -19,16 +19,14 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do 'event' => event_type, 'commit_id' => nil, 'label_title' => label.title, - 'issue_db_id' => issue.id, 'created_at' => '2022-04-26 18:30:53 UTC', - 'issue' => { 'number' => issue.iid } + 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) } ) end let(:event_attrs) do { user_id: user.id, - issue_id: issue.id, label_id: label.id, created_at: issue_event.created_at }.stringify_keys @@ -36,9 +34,9 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do shared_examples 'new event' do it 'creates a new label event' do - expect { importer.execute(issue_event) }.to change { issue.resource_label_events.count } + expect { importer.execute(issue_event) }.to change { issuable.resource_label_events.count } .from(0).to(1) - expect(issue.resource_label_events.last) + expect(issuable.resource_label_events.last) .to have_attributes(expected_event_attrs) end end @@ -46,24 +44,44 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do before do allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(label.id) allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| - allow(finder).to receive(:database_id).and_return(issue.id) + allow(finder).to receive(:database_id).and_return(issuable.id) end allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) end end - context 'when importing a labeled event' do - let(:event_type) { 'labeled' } - let(:expected_event_attrs) { event_attrs.merge(action: 'add') } + context 'with Issue' do + context 'when importing a labeled event' do + let(:event_type) { 'labeled' } + let(:expected_event_attrs) { event_attrs.merge(issue_id: issuable.id, action: 'add') } - it_behaves_like 'new event' + it_behaves_like 'new event' + end + + context 'when importing an unlabeled event' do + let(:event_type) { 'unlabeled' } + let(:expected_event_attrs) { event_attrs.merge(issue_id: issuable.id, action: 'remove') } + + it_behaves_like 'new event' + end end - context 'when importing an unlabeled event' do - let(:event_type) { 'unlabeled' } - let(:expected_event_attrs) { event_attrs.merge(action: 'remove') } + context 'with MergeRequest' do + let(:issuable) { create(:merge_request, source_project: project, target_project: project) } + + context 'when importing a labeled event' do + let(:event_type) { 'labeled' } + let(:expected_event_attrs) { event_attrs.merge(merge_request_id: issuable.id, action: 'add') } - it_behaves_like 'new event' + it_behaves_like 'new event' + end + + context 'when importing an unlabeled event' do + let(:event_type) { 'unlabeled' } + let(:expected_event_attrs) { event_attrs.merge(merge_request_id: issuable.id, action: 'remove') } + + it_behaves_like 'new event' + end end end diff --git a/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb index 2687627fc23..bc14b81bd91 100644 --- a/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do let_it_be(:user) { create(:user) } let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:issue) { create(:issue, project: project) } + let(:issuable) { create(:issue, project: project) } let!(:milestone) { create(:milestone, project: project) } let(:issue_event) do @@ -19,16 +19,15 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do 'event' => event_type, 'commit_id' => nil, 'milestone_title' => milestone.title, - 'issue_db_id' => issue.id, + 'issue_db_id' => issuable.id, 'created_at' => '2022-04-26 18:30:53 UTC', - 'issue' => { 'number' => issue.iid } + 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) } ) end let(:event_attrs) do { user_id: user.id, - issue_id: issue.id, milestone_id: milestone.id, state: 'opened', created_at: issue_event.created_at @@ -37,9 +36,9 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do shared_examples 'new event' do it 'creates a new milestone event' do - expect { importer.execute(issue_event) }.to change { issue.resource_milestone_events.count } + expect { importer.execute(issue_event) }.to change { issuable.resource_milestone_events.count } .from(0).to(1) - expect(issue.resource_milestone_events.last) + expect(issuable.resource_milestone_events.last) .to have_attributes(expected_event_attrs) end end @@ -48,25 +47,45 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do before do allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(milestone.id) allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| - allow(finder).to receive(:database_id).and_return(issue.id) + allow(finder).to receive(:database_id).and_return(issuable.id) end allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) end end - context 'when importing a milestoned event' do - let(:event_type) { 'milestoned' } - let(:expected_event_attrs) { event_attrs.merge(action: 'add') } + context 'with Issue' do + context 'when importing a milestoned event' do + let(:event_type) { 'milestoned' } + let(:expected_event_attrs) { event_attrs.merge(issue_id: issuable.id, action: 'add') } - it_behaves_like 'new event' + it_behaves_like 'new event' + end + + context 'when importing demilestoned event' do + let(:event_type) { 'demilestoned' } + let(:expected_event_attrs) { event_attrs.merge(issue_id: issuable.id, action: 'remove') } + + it_behaves_like 'new event' + end end - context 'when importing demilestoned event' do - let(:event_type) { 'demilestoned' } - let(:expected_event_attrs) { event_attrs.merge(action: 'remove') } + context 'with MergeRequest' do + let(:issuable) { create(:merge_request, source_project: project, target_project: project) } + + context 'when importing a milestoned event' do + let(:event_type) { 'milestoned' } + let(:expected_event_attrs) { event_attrs.merge(merge_request_id: issuable.id, action: 'add') } - it_behaves_like 'new event' + it_behaves_like 'new event' + end + + context 'when importing demilestoned event' do + let(:event_type) { 'demilestoned' } + let(:expected_event_attrs) { event_attrs.merge(merge_request_id: issuable.id, action: 'remove') } + + it_behaves_like 'new event' + end end end end diff --git a/spec/lib/gitlab/github_import/importer/events/changed_reviewer_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_reviewer_spec.rb new file mode 100644 index 00000000000..ff813dd41d9 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/events/changed_reviewer_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedReviewer do + subject(:importer) { described_class.new(project, client) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:requested_reviewer) { create(:user) } + let_it_be(:review_requester) { create(:user) } + + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:issuable) { create(:merge_request, source_project: project, target_project: project) } + + let(:issue_event) do + Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( + 'id' => 6501124486, + 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'event' => event_type, + 'commit_id' => nil, + 'created_at' => '2022-04-26 18:30:53 UTC', + 'review_requester' => { 'id' => review_requester.id, 'login' => review_requester.username }, + 'requested_reviewer' => { 'id' => requested_reviewer.id, 'login' => requested_reviewer.username }, + 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) } + ) + end + + let(:note_attrs) do + { + noteable_id: issuable.id, + noteable_type: issuable.class.name, + project_id: project.id, + author_id: review_requester.id, + system: true, + created_at: issue_event.created_at, + updated_at: issue_event.created_at + }.stringify_keys + end + + let(:expected_system_note_metadata_attrs) do + { + action: 'reviewer', + created_at: issue_event.created_at, + updated_at: issue_event.created_at + }.stringify_keys + end + + shared_examples 'create expected notes' do + it 'creates expected note' do + expect { importer.execute(issue_event) }.to change { issuable.notes.count } + .from(0).to(1) + + expect(issuable.notes.last) + .to have_attributes(expected_note_attrs) + end + + it 'creates expected system note metadata' do + expect { importer.execute(issue_event) }.to change(SystemNoteMetadata, :count) + .from(0).to(1) + + expect(SystemNoteMetadata.last) + .to have_attributes( + expected_system_note_metadata_attrs.merge( + note_id: Note.last.id + ) + ) + end + end + + shared_examples 'process review_requested & review_request_removed MR events' do + context 'when importing a review_requested event' do + let(:event_type) { 'review_requested' } + let(:expected_note_attrs) { note_attrs.merge(note: "requested review from @#{requested_reviewer.username}") } + + it_behaves_like 'create expected notes' + end + + context 'when importing a review_request_removed event' do + let(:event_type) { 'review_request_removed' } + let(:expected_note_attrs) { note_attrs.merge(note: "removed review request for @#{requested_reviewer.username}") } + + it_behaves_like 'create expected notes' + end + end + + describe '#execute' do + before do + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issuable.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(requested_reviewer.id, requested_reviewer.username) + .and_return(requested_reviewer.id) + allow(finder).to receive(:find).with(review_requester.id, review_requester.username) + .and_return(review_requester.id) + end + end + + it_behaves_like 'process review_requested & review_request_removed MR events' + end +end diff --git a/spec/lib/gitlab/github_import/importer/events/closed_spec.rb b/spec/lib/gitlab/github_import/importer/events/closed_spec.rb index 9a49d80a8bb..f7e38f373c0 100644 --- a/spec/lib/gitlab/github_import/importer/events/closed_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/closed_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do let_it_be(:user) { create(:user) } let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:issue) { create(:issue, project: project) } + let(:issuable) { create(:issue, project: project) } let(:commit_id) { nil } let(:issue_event) do @@ -21,7 +21,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do 'event' => 'closed', 'created_at' => '2022-04-26 18:30:53 UTC', 'commit_id' => commit_id, - 'issue' => { 'number' => issue.iid } + 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) } ) end @@ -29,54 +29,74 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do { project_id: project.id, author_id: user.id, - target_id: issue.id, - target_type: Issue.name, + target_id: issuable.id, + target_type: issuable.class.name, action: 'closed', created_at: issue_event.created_at, updated_at: issue_event.created_at }.stringify_keys end - let(:expected_state_event_attrs) do - { - user_id: user.id, - issue_id: issue.id, - state: 'closed', - created_at: issue_event.created_at - }.stringify_keys - end - before do allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| - allow(finder).to receive(:database_id).and_return(issue.id) + allow(finder).to receive(:database_id).and_return(issuable.id) end allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) end end - it 'creates expected event and state event' do - importer.execute(issue_event) + shared_examples 'new event' do + it 'creates expected event and state event' do + importer.execute(issue_event) + + expect(issuable.events.count).to eq 1 + expect(issuable.events[0].attributes) + .to include expected_event_attrs + + expect(issuable.resource_state_events.count).to eq 1 + expect(issuable.resource_state_events[0].attributes) + .to include expected_state_event_attrs + end + + context 'when closed by commit' do + let!(:closing_commit) { create(:commit, project: project) } + let(:commit_id) { closing_commit.id } - expect(issue.events.count).to eq 1 - expect(issue.events[0].attributes) - .to include expected_event_attrs + it 'creates expected event and state event' do + importer.execute(issue_event) - expect(issue.resource_state_events.count).to eq 1 - expect(issue.resource_state_events[0].attributes) - .to include expected_state_event_attrs + expect(issuable.events.count).to eq 1 + state_event = issuable.resource_state_events.last + expect(state_event.source_commit).to eq commit_id[0..40] + end + end end - context 'when closed by commit' do - let!(:closing_commit) { create(:commit, project: project) } - let(:commit_id) { closing_commit.id } + context 'with Issue' do + let(:expected_state_event_attrs) do + { + user_id: user.id, + issue_id: issuable.id, + state: 'closed', + created_at: issue_event.created_at + }.stringify_keys + end - it 'creates expected event and state event' do - importer.execute(issue_event) + it_behaves_like 'new event' + end - expect(issue.events.count).to eq 1 - state_event = issue.resource_state_events.last - expect(state_event.source_commit).to eq commit_id[0..40] + context 'with MergeRequest' do + let(:issuable) { create(:merge_request, source_project: project, target_project: project) } + let(:expected_state_event_attrs) do + { + user_id: user.id, + merge_request_id: issuable.id, + state: 'closed', + created_at: issue_event.created_at + }.stringify_keys end + + it_behaves_like 'new event' end end diff --git a/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb b/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb index 68e001c7364..bf19147d4c8 100644 --- a/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb @@ -9,9 +9,8 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g let_it_be(:user) { create(:user) } let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:issue_iid) { 999 } - let(:issue) { create(:issue, project: project, iid: issue_iid) } + let(:issuable) { create(:issue, project: project, iid: issue_iid) } let(:referenced_in) { build_stubbed(:issue, project: project, iid: issue_iid + 1) } let(:commit_id) { nil } @@ -30,7 +29,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g } }, 'created_at' => '2022-04-26 18:30:53 UTC', - 'issue' => { 'number' => issue.iid } + 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) } ) end @@ -38,8 +37,8 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g let(:expected_note_attrs) do { system: true, - noteable_type: Issue.name, - noteable_id: issue.id, + noteable_type: issuable.class.name, + noteable_id: issuable.id, project_id: project.id, author_id: user.id, note: expected_note_body, @@ -47,58 +46,70 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g }.stringify_keys end - context 'when referenced in other issue' do - let(:expected_note_body) { "mentioned in issue ##{referenced_in.iid}" } - - before do - allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| - allow(finder).to receive(:database_id).and_return(referenced_in.iid) - allow(finder).to receive(:database_id).and_return(issue.id) - end - allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| - allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + shared_examples 'import cross-referenced event' do + context 'when referenced in other issue' do + let(:expected_note_body) { "mentioned in issue ##{referenced_in.iid}" } + + before do + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(referenced_in.iid) + allow(finder).to receive(:database_id).and_return(issuable.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end - end - it 'creates expected note' do - importer.execute(issue_event) + it 'creates expected note' do + importer.execute(issue_event) - expect(issue.notes.count).to eq 1 - expect(issue.notes[0]).to have_attributes expected_note_attrs - expect(issue.notes[0].system_note_metadata.action).to eq 'cross_reference' + expect(issuable.notes.count).to eq 1 + expect(issuable.notes[0]).to have_attributes expected_note_attrs + expect(issuable.notes[0].system_note_metadata.action).to eq 'cross_reference' + end end - end - context 'when referenced in pull request' do - let(:referenced_in) { build_stubbed(:merge_request, project: project) } - let(:pull_request_resource) { { 'id' => referenced_in.iid } } + context 'when referenced in pull request' do + let(:referenced_in) { build_stubbed(:merge_request, project: project) } + let(:pull_request_resource) { { 'id' => referenced_in.iid } } - let(:expected_note_body) { "mentioned in merge request !#{referenced_in.iid}" } + let(:expected_note_body) { "mentioned in merge request !#{referenced_in.iid}" } - before do - allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| - allow(finder).to receive(:database_id).and_return(referenced_in.iid) - allow(finder).to receive(:database_id).and_return(issue.id) + before do + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(referenced_in.iid) + allow(finder).to receive(:database_id).and_return(issuable.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end - allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| - allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + + it 'creates expected note' do + importer.execute(issue_event) + + expect(issuable.notes.count).to eq 1 + expect(issuable.notes[0]).to have_attributes expected_note_attrs + expect(issuable.notes[0].system_note_metadata.action).to eq 'cross_reference' end end - it 'creates expected note' do - importer.execute(issue_event) + context 'when referenced in out of project issue/pull_request' do + it 'does not create expected note' do + importer.execute(issue_event) - expect(issue.notes.count).to eq 1 - expect(issue.notes[0]).to have_attributes expected_note_attrs - expect(issue.notes[0].system_note_metadata.action).to eq 'cross_reference' + expect(issuable.notes.count).to eq 0 + end end end - context 'when referenced in out of project issue/pull_request' do - it 'does not create expected note' do - importer.execute(issue_event) + context 'with Issue' do + it_behaves_like 'import cross-referenced event' + end - expect(issue.notes.count).to eq 0 - end + context 'with MergeRequest' do + let(:issuable) { create(:merge_request, source_project: project, target_project: project) } + + it_behaves_like 'import cross-referenced event' end end diff --git a/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb index 316ea798965..29598cb4354 100644 --- a/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } - let(:issue) { create(:issue, project: project) } + let(:issuable) { create(:issue, project: project) } let(:client) { instance_double('Gitlab::GithubImport::Client') } let(:issue_event) do @@ -20,14 +20,14 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do 'created_at' => '2022-04-26 18:30:53 UTC', 'old_title' => 'old title', 'new_title' => 'new title', - 'issue' => { 'number' => issue.iid } + 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) } ) end let(:expected_note_attrs) do { - noteable_id: issue.id, - noteable_type: Issue.name, + noteable_id: issuable.id, + noteable_type: issuable.class.name, project_id: project.id, author_id: user.id, note: "changed title from **{-old-} title** to **{+new+} title**", @@ -48,31 +48,43 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do describe '#execute' do before do allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| - allow(finder).to receive(:database_id).and_return(issue.id) + allow(finder).to receive(:database_id).and_return(issuable.id) end allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) end end - it 'creates expected note' do - expect { importer.execute(issue_event) }.to change { issue.notes.count } - .from(0).to(1) + shared_examples 'import renamed event' do + it 'creates expected note' do + expect { importer.execute(issue_event) }.to change { issuable.notes.count } + .from(0).to(1) - expect(issue.notes.last) - .to have_attributes(expected_note_attrs) - end + expect(issuable.notes.last) + .to have_attributes(expected_note_attrs) + end - it 'creates expected system note metadata' do - expect { importer.execute(issue_event) }.to change { SystemNoteMetadata.count } - .from(0).to(1) + it 'creates expected system note metadata' do + expect { importer.execute(issue_event) }.to change { SystemNoteMetadata.count } + .from(0).to(1) - expect(SystemNoteMetadata.last) - .to have_attributes( - expected_system_note_metadata_attrs.merge( - note_id: Note.last.id + expect(SystemNoteMetadata.last) + .to have_attributes( + expected_system_note_metadata_attrs.merge( + note_id: Note.last.id + ) ) - ) + end + end + + context 'with Issue' do + it_behaves_like 'import renamed event' + end + + context 'with MergeRequest' do + let(:issuable) { create(:merge_request, source_project: project, target_project: project) } + + it_behaves_like 'import renamed event' end end end diff --git a/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb b/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb index 2461dbb9701..354003fc997 100644 --- a/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail let_it_be(:user) { create(:user) } let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:issue) { create(:issue, project: project) } + let(:issuable) { create(:issue, project: project) } let(:issue_event) do Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( @@ -19,7 +19,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => 'reopened', 'created_at' => '2022-04-26 18:30:53 UTC', - 'issue' => { 'number' => issue.iid } + 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) } ) end @@ -27,40 +27,61 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail { project_id: project.id, author_id: user.id, - target_id: issue.id, - target_type: Issue.name, + target_id: issuable.id, + target_type: issuable.class.name, action: 'reopened', created_at: issue_event.created_at, updated_at: issue_event.created_at }.stringify_keys end - let(:expected_state_event_attrs) do - { - user_id: user.id, - state: 'reopened', - created_at: issue_event.created_at - }.stringify_keys - end - before do allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| - allow(finder).to receive(:database_id).and_return(issue.id) + allow(finder).to receive(:database_id).and_return(issuable.id) end allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) end end - it 'creates expected event and state event' do - importer.execute(issue_event) + shared_examples 'new event' do + it 'creates expected event and state event' do + importer.execute(issue_event) - expect(issue.events.count).to eq 1 - expect(issue.events[0].attributes) - .to include expected_event_attrs + expect(issuable.events.count).to eq 1 + expect(issuable.events[0].attributes) + .to include expected_event_attrs + + expect(issuable.resource_state_events.count).to eq 1 + expect(issuable.resource_state_events[0].attributes) + .to include expected_state_event_attrs + end + end + + context 'with Issue' do + let(:expected_state_event_attrs) do + { + user_id: user.id, + issue_id: issuable.id, + state: 'reopened', + created_at: issue_event.created_at + }.stringify_keys + end + + it_behaves_like 'new event' + end + + context 'with MergeRequest' do + let(:issuable) { create(:merge_request, source_project: project, target_project: project) } + let(:expected_state_event_attrs) do + { + user_id: user.id, + merge_request_id: issuable.id, + state: 'reopened', + created_at: issue_event.created_at + }.stringify_keys + end - expect(issue.resource_state_events.count).to eq 1 - expect(issue.resource_state_events[0].attributes) - .to include expected_state_event_attrs + it_behaves_like 'new event' end end diff --git a/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb index 49a76fb5e6b..d28640a4f07 100644 --- a/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::GithubImport::Importer::IssueAndLabelLinksImporter do describe '#execute' do diff --git a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb index 33d5fbf13a0..91121f3c3fc 100644 --- a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb @@ -42,10 +42,6 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab end describe '#execute' do - before do - issue_event.attributes[:issue_db_id] = issue.id - end - context "when it's closed issue event" do let(:event_name) { 'closed' } @@ -116,6 +112,20 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab Gitlab::GithubImport::Importer::Events::ChangedAssignee end + context "when it's review_requested issue event" do + let(:event_name) { 'review_requested' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::ChangedReviewer + end + + context "when it's review_request_removed issue event" do + let(:event_name) { 'review_request_removed' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::ChangedReviewer + end + context "when it's unknown issue event" do let(:event_name) { 'fake' } diff --git a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb index 8d4c1b01e50..2c1af4f8948 100644 --- a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb @@ -11,8 +11,8 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventsImporter do let(:parallel) { true } let(:issue_event) do struct = Struct.new( - :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone, - :source, :assignee, :assigner, :issue, :created_at, :performed_via_github_app, + :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone, :source, + :assignee, :assigner, :review_requester, :requested_reviewer, :issue, :created_at, :performed_via_github_app, keyword_init: true ) struct.new(id: rand(10), event: 'closed', created_at: '2022-04-26 18:30:53 UTC') diff --git a/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb new file mode 100644 index 00000000000..6dc6db739f4 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do + subject(:importer) { described_class.new(github_protected_branch, project, client) } + + let(:allow_force_pushes_on_github) { true } + let(:github_protected_branch) do + Gitlab::GithubImport::Representation::ProtectedBranch.new( + id: 'protection', + allow_force_pushes: allow_force_pushes_on_github + ) + end + + let(:project) { create(:project, :repository) } + let(:client) { instance_double('Gitlab::GithubImport::Client') } + + describe '#execute' do + let(:create_service) { instance_double('ProtectedBranches::CreateService') } + + shared_examples 'create branch protection by the strictest ruleset' do + let(:expected_ruleset) do + { + name: 'protection', + push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], + merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], + allow_force_push: expected_allow_force_push + } + end + + it 'calls service with the correct arguments' do + expect(ProtectedBranches::CreateService).to receive(:new).with( + project, + project.creator, + expected_ruleset + ).and_return(create_service) + + expect(create_service).to receive(:execute).with(skip_authorization: true) + importer.execute + end + + it 'creates protected branch and access levels for given github rule' do + expect { importer.execute }.to change(ProtectedBranch, :count).by(1) + .and change(ProtectedBranch::PushAccessLevel, :count).by(1) + .and change(ProtectedBranch::MergeAccessLevel, :count).by(1) + end + end + + context 'when branch is protected on GitLab' do + before do + create( + :protected_branch, + project: project, + name: 'protect*', + allow_force_push: allow_force_pushes_on_gitlab + ) + end + + context 'when branch protection rule on Gitlab is stricter than on Github' do + let(:allow_force_pushes_on_github) { true } + let(:allow_force_pushes_on_gitlab) { false } + let(:expected_allow_force_push) { false } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + + context 'when branch protection rule on Github is stricter than on Gitlab' do + let(:allow_force_pushes_on_github) { false } + let(:allow_force_pushes_on_gitlab) { true } + let(:expected_allow_force_push) { false } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + + context 'when branch protection rules on Github and Gitlab are the same' do + let(:allow_force_pushes_on_github) { true } + let(:allow_force_pushes_on_gitlab) { true } + let(:expected_allow_force_push) { true } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + end + + context 'when branch is not protected on GitLab' do + let(:expected_allow_force_push) { true } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb new file mode 100644 index 00000000000..4e9208be985 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb @@ -0,0 +1,225 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter do + subject(:importer) { described_class.new(project, client, parallel: parallel) } + + let(:project) { instance_double('Project', id: 4, import_source: 'foo/bar') } + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:parallel) { true } + + let(:branches) do + branch = Struct.new(:name, :protection, keyword_init: true) + protection = Struct.new(:enabled, keyword_init: true) + + [ + branch.new(name: 'main', protection: protection.new(enabled: false)), + branch.new(name: 'staging', protection: protection.new(enabled: true)), + branch.new(name: 'development', protection: nil) # when user has no admin right for this repo + ] + end + + let(:github_protection_rule) do + response = Struct.new(:name, :url, :required_signatures, :enforce_admins, :required_linear_history, + :allow_force_pushes, :allow_deletion, :block_creations, :required_conversation_resolution, + keyword_init: true + ) + required_signatures = Struct.new(:url, :enabled, keyword_init: true) + enforce_admins = Struct.new(:url, :enabled, keyword_init: true) + allow_option = Struct.new(:enabled, keyword_init: true) + response.new( + name: 'main', + url: 'https://example.com/branches/main/protection', + required_signatures: required_signatures.new( + url: 'https://example.com/branches/main/protection/required_signatures', + enabled: false + ), + enforce_admins: enforce_admins.new( + url: 'https://example.com/branches/main/protection/enforce_admins', + enabled: false + ), + required_linear_history: allow_option.new( + enabled: false + ), + allow_force_pushes: allow_option.new( + enabled: false + ), + allow_deletion: allow_option.new( + enabled: false + ), + block_creations: allow_option.new( + enabled: true + ), + required_conversation_resolution: allow_option.new( + enabled: false + ) + ) + end + + describe '#parallel?' do + context 'when running in parallel mode' do + it { expect(importer).to be_parallel } + end + + context 'when running in sequential mode' do + let(:parallel) { false } + + it { expect(importer).not_to be_parallel } + end + end + + describe '#execute' do + context 'when running in parallel mode' do + it 'imports protected branches in parallel' do + expect(importer).to receive(:parallel_import) + + importer.execute + end + end + + context 'when running in sequential mode' do + let(:parallel) { false } + + it 'imports protected branches in sequence' do + expect(importer).to receive(:sequential_import) + + importer.execute + end + end + end + + describe '#sequential_import', :clean_gitlab_redis_cache do + let(:parallel) { false } + + before do + allow(client).to receive(:branches).and_return(branches) + allow(client) + .to receive(:branch_protection) + .with(project.import_source, 'staging') + .and_return(github_protection_rule) + .once + end + + it 'imports each protected branch in sequence' do + protected_branch_importer = instance_double('Gitlab::GithubImport::Importer::ProtectedBranchImporter') + + expect(Gitlab::GithubImport::Importer::ProtectedBranchImporter) + .to receive(:new) + .with( + an_instance_of(Gitlab::GithubImport::Representation::ProtectedBranch), + project, + client + ) + .and_return(protected_branch_importer) + + expect(protected_branch_importer).to receive(:execute) + expect(Gitlab::GithubImport::ObjectCounter) + .to receive(:increment).with(project, :protected_branch, :fetched) + + importer.sequential_import + end + end + + describe '#parallel_import', :clean_gitlab_redis_cache do + before do + allow(client).to receive(:branches).and_return(branches) + allow(client) + .to receive(:branch_protection) + .with(project.import_source, 'staging') + .and_return(github_protection_rule) + .once + end + + it 'imports each protected branch in parallel' do + expect(Gitlab::GithubImport::ImportProtectedBranchWorker) + .to receive(:bulk_perform_in) + .with( + 1.second, + [[project.id, an_instance_of(Hash), an_instance_of(String)]], + batch_delay: 1.minute, + batch_size: 1000 + ) + expect(Gitlab::GithubImport::ObjectCounter) + .to receive(:increment).with(project, :protected_branch, :fetched) + + waiter = importer.parallel_import + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(1) + end + end + + describe '#each_object_to_import', :clean_gitlab_redis_cache do + let(:branch_struct) { Struct.new(:protection, :name, :url, keyword_init: true) } + let(:protection_struct) { Struct.new(:enabled, keyword_init: true) } + let(:protected_branch) { branch_struct.new(name: 'main', protection: protection_struct.new(enabled: true)) } + let(:unprotected_branch) { branch_struct.new(name: 'staging', protection: protection_struct.new(enabled: false)) } + # when user has no admin rights on repo + let(:unknown_protection_branch) { branch_struct.new(name: 'development', protection: nil) } + + let(:page_counter) { instance_double(Gitlab::GithubImport::PageCounter) } + + before do + allow(client).to receive(:branches).with(project.import_source) + .and_return([protected_branch, unprotected_branch, unknown_protection_branch]) + allow(client).to receive(:branch_protection) + .with(project.import_source, protected_branch.name).once + .and_return(github_protection_rule) + allow(Gitlab::GithubImport::ObjectCounter).to receive(:increment) + .with(project, :protected_branch, :fetched) + end + + it 'imports each protected branch page by page' do + subject.each_object_to_import do |object| + expect(object).to eq github_protection_rule + end + expect(Gitlab::GithubImport::ObjectCounter).to have_received(:increment).once + end + + context 'when protected branch is already processed' do + it "doesn't process this branch" do + subject.mark_as_imported(protected_branch) + + subject.each_object_to_import {} + expect(Gitlab::GithubImport::ObjectCounter).not_to have_received(:increment) + end + end + end + + describe '#importer_class' do + it { expect(importer.importer_class).to eq Gitlab::GithubImport::Importer::ProtectedBranchImporter } + end + + describe '#representation_class' do + it { expect(importer.representation_class).to eq Gitlab::GithubImport::Representation::ProtectedBranch } + end + + describe '#sidekiq_worker_class' do + it { expect(importer.sidekiq_worker_class).to eq Gitlab::GithubImport::ImportProtectedBranchWorker } + end + + describe '#object_type' do + it { expect(importer.object_type).to eq :protected_branch } + end + + describe '#collection_method' do + it { expect(importer.collection_method).to eq :protected_branches } + end + + describe '#id_for_already_imported_cache' do + it 'returns the ID of the given protected branch' do + expect(importer.id_for_already_imported_cache(github_protection_rule)).to eq('main') + end + end + + describe '#collection_options' do + it 'returns an empty Hash' do + # For large projects (e.g. kubernetes/kubernetes) GitHub's API may produce + # HTTP 500 errors when using explicit sorting options, regardless of what + # order you sort in. Not using any sorting options at all allows us to + # work around this. + expect(importer.collection_options).to eq({}) + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/release_attachments_importer_spec.rb b/spec/lib/gitlab/github_import/importer/release_attachments_importer_spec.rb new file mode 100644 index 00000000000..4779f9c8982 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/release_attachments_importer_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::ReleaseAttachmentsImporter do + subject(:importer) { described_class.new(release_attachments, project, client) } + + let_it_be(:project) { create(:project) } + + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:release) { create(:release, project: project, description: description) } + let(:release_attachments) do + Gitlab::GithubImport::Representation::ReleaseAttachments + .from_json_hash(release_db_id: release.id, description: release.description) + end + + let(:doc_url) { 'https://github.com/nickname/public-test-repo/files/9020437/git-cheat-sheet.txt' } + let(:image_url) { 'https://user-images.githubusercontent.com/6833842/0cf366b61ef2.jpeg' } + let(:description) do + <<-TEXT.strip + Some text... + + [special-doc](#{doc_url}) + ![image.jpeg](#{image_url}) + TEXT + end + + describe '#execute' do + let(:downloader_stub) { instance_double(Gitlab::GithubImport::AttachmentsDownloader) } + let(:tmp_stub_doc) { Tempfile.create('attachment_download_test.txt') } + let(:tmp_stub_image) { Tempfile.create('image.jpeg') } + + context 'when importing doc attachment' do + before do + allow(Gitlab::GithubImport::AttachmentsDownloader).to receive(:new).with(doc_url) + .and_return(downloader_stub) + allow(Gitlab::GithubImport::AttachmentsDownloader).to receive(:new).with(image_url) + .and_return(downloader_stub) + allow(downloader_stub).to receive(:perform).and_return(tmp_stub_doc, tmp_stub_image) + allow(downloader_stub).to receive(:delete).twice + + allow(UploadService).to receive(:new) + .with(project, tmp_stub_doc, FileUploader).and_call_original + allow(UploadService).to receive(:new) + .with(project, tmp_stub_image, FileUploader).and_call_original + end + + it 'updates release description with new attachment url' do + importer.execute + + release.reload + expect(release.description).to start_with("Some text...\n\n [special-doc](/uploads/") + expect(release.description).to include('![image.jpeg](/uploads/') + end + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/releases_attachments_importer_spec.rb b/spec/lib/gitlab/github_import/importer/releases_attachments_importer_spec.rb new file mode 100644 index 00000000000..1aeb3462cd5 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/releases_attachments_importer_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::ReleasesAttachmentsImporter do + subject { described_class.new(project, client) } + + let_it_be(:project) { create(:project) } + + let(:client) { instance_double(Gitlab::GithubImport::Client) } + + describe '#each_object_to_import', :clean_gitlab_redis_cache do + let!(:release_1) { create(:release, project: project) } + let!(:release_2) { create(:release, project: project) } + + it 'iterates each project release' do + list = [] + subject.each_object_to_import do |object| + list << object + end + expect(list).to contain_exactly(release_1, release_2) + end + + context 'when release is already processed' do + it "doesn't process this release" do + subject.mark_as_imported(release_1) + + list = [] + subject.each_object_to_import do |object| + list << object + end + expect(list).to contain_exactly(release_2) + end + end + end + + describe '#representation_class' do + it { expect(subject.representation_class).to eq(Gitlab::GithubImport::Representation::ReleaseAttachments) } + end + + describe '#importer_class' do + it { expect(subject.importer_class).to eq(Gitlab::GithubImport::Importer::ReleaseAttachmentsImporter) } + end + + describe '#sidekiq_worker_class' do + it { expect(subject.sidekiq_worker_class).to eq(Gitlab::GithubImport::ImportReleaseAttachmentsWorker) } + end + + describe '#collection_method' do + it { expect(subject.collection_method).to eq(:release_attachments) } + end + + describe '#object_type' do + it { expect(subject.object_type).to eq(:release_attachment) } + end + + describe '#id_for_already_imported_cache' do + let(:release) { build_stubbed(:release) } + + it { expect(subject.id_for_already_imported_cache(release)).to eq(release.id) } + end + + describe '#object_representation' do + let(:release) { build_stubbed(:release) } + + it 'returns release attachments representation' do + representation = subject.object_representation(release) + + expect(representation.class).to eq subject.representation_class + expect(representation.release_db_id).to eq release.id + expect(representation.description).to eq release.description + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb index f2730ba74ec..0b8b1922d94 100644 --- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb @@ -48,7 +48,7 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do describe '#import_wiki?' do it 'returns true if the wiki should be imported' do - repo = double(:repo, has_wiki: true) + repo = { has_wiki: true } expect(client) .to receive(:repository) @@ -67,7 +67,7 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do end it 'returns false if the GitHub wiki is disabled' do - repo = double(:repo, has_wiki: false) + repo = { has_wiki: false } expect(client) .to receive(:repository) @@ -78,7 +78,7 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do end it 'returns false if the wiki has already been imported' do - repo = double(:repo, has_wiki: true) + repo = { has_wiki: true } expect(client) .to receive(:repository) @@ -186,7 +186,7 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do describe '#import_repository' do it 'imports the repository' do - repo = double(:repo, default_branch: 'develop') + repo = { default_branch: 'develop' } expect(client) .to receive(:repository) diff --git a/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb index bb1ee79ad93..4ed01fd7e0b 100644 --- a/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb @@ -6,7 +6,8 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter let(:client) { double } let_it_be(:project) { create(:project, :import_started, import_source: 'http://somegithub.com') } - let_it_be(:issue) { create(:issue, project: project) } + + let!(:issuable) { create(:issue, project: project) } subject { described_class.new(project, client, parallel: parallel) } @@ -35,7 +36,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter end describe '#page_counter_id' do - it { expect(subject.page_counter_id(issue)).to eq("issues/#{issue.iid}/issue_timeline") } + it { expect(subject.page_counter_id(issuable)).to eq("issues/#{issuable.iid}/issue_timeline") } end describe '#id_for_already_imported_cache' do @@ -51,6 +52,39 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter end end + describe '#compose_associated_id!' do + let(:issuable) { build_stubbed(:issue, iid: 99) } + let(:event_resource) { Struct.new(:id, :event, :source, keyword_init: true) } + + context 'when event type is cross-referenced' do + let(:event) do + source_resource = Struct.new(:issue, keyword_init: true) + issue_resource = Struct.new(:id, keyword_init: true) + event_resource.new( + id: nil, + event: 'cross-referenced', + source: source_resource.new(issue: issue_resource.new(id: '100500')) + ) + end + + it 'assigns event id' do + subject.compose_associated_id!(issuable, event) + + expect(event.id).to eq 'cross-reference#99-in-100500' + end + end + + context "when event type isn't cross-referenced" do + let(:event) { event_resource.new(id: nil, event: 'labeled') } + + it "doesn't assign event id" do + subject.compose_associated_id!(issuable, event) + + expect(event.id).to eq nil + end + end + end + describe '#each_object_to_import', :clean_gitlab_redis_cache do let(:issue_event) do struct = Struct.new(:id, :event, :created_at, :issue, keyword_init: true) @@ -72,19 +106,37 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter .with( :issue_timeline, project.import_source, - issue.iid, + issuable.iid, { state: 'all', sort: 'created', direction: 'asc', page: 1 } ).and_yield(page) end - it 'imports each issue event page by page' do - counter = 0 - subject.each_object_to_import do |object| - expect(object).to eq issue_event - expect(issue_event.issue['number']).to eq issue.iid - counter += 1 + context 'with issues' do + it 'imports each issue event page by page' do + counter = 0 + subject.each_object_to_import do |object| + expect(object).to eq issue_event + expect(issue_event.issue['number']).to eq issuable.iid + expect(issue_event.issue['pull_request']).to eq false + counter += 1 + end + expect(counter).to eq 1 + end + end + + context 'with merge requests' do + let!(:issuable) { create(:merge_request, source_project: project, target_project: project) } + + it 'imports each merge request event page by page' do + counter = 0 + subject.each_object_to_import do |object| + expect(object).to eq issue_event + expect(issue_event.issue['number']).to eq issuable.iid + expect(issue_event.issue['pull_request']).to eq true + counter += 1 + end + expect(counter).to eq 1 end - expect(counter).to eq 1 end it 'triggers page number increment' do @@ -103,7 +155,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter context 'when page is already processed' do before do page_counter = Gitlab::GithubImport::PageCounter.new( - project, subject.page_counter_id(issue) + project, subject.page_counter_id(issuable) ) page_counter.set(page.number) end diff --git a/spec/lib/gitlab/github_import/markdown_text_spec.rb b/spec/lib/gitlab/github_import/markdown_text_spec.rb index ad45469a4c3..1da6bb06403 100644 --- a/spec/lib/gitlab/github_import/markdown_text_spec.rb +++ b/spec/lib/gitlab/github_import/markdown_text_spec.rb @@ -60,6 +60,34 @@ RSpec.describe Gitlab::GithubImport::MarkdownText do end end + describe '.fetch_attachment_urls' do + let(:image_extension) { described_class::MEDIA_TYPES.sample } + let(:image_attachment) do + "![special-image](https://user-images.githubusercontent.com/6833862/"\ + "176685788-e7a93168-7ded-406a-82b5-eb1c56685a93.#{image_extension})" + end + + let(:doc_extension) { described_class::DOC_TYPES.sample } + let(:doc_attachment) do + "[some-doc](https://github.com/nickname/public-test-repo/"\ + "files/9020437/git-cheat-sheet.#{doc_extension})" + end + + let(:text) do + <<-TEXT + Comment with an attachment + #{image_attachment} + #{FFaker::Lorem.sentence} + #{doc_attachment} + TEXT + end + + it 'fetches attachment urls' do + expect(described_class.fetch_attachment_urls(text)) + .to contain_exactly(image_attachment, doc_attachment) + end + end + describe '#to_s' do it 'returns the text when the author was found' do author = double(:author, login: 'Alice') diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb index 738e7c88d7d..860bb60f3ed 100644 --- a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb +++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb @@ -15,6 +15,10 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do Class end + def sidekiq_worker_class + Class + end + def object_type :dummy end diff --git a/spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb b/spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb index bcb8575bdbf..5a24f929388 100644 --- a/spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::GithubImport::Representation::DiffNotes::SuggestionFormatter do it 'does nothing when there is any text before the suggestion tag' do diff --git a/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb b/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb index d40be0e841c..43f0198704f 100644 --- a/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb +++ b/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb @@ -1,21 +1,41 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::GithubImport::Representation::ExposeAttribute do - it 'defines a getter method that returns an attribute value' do - klass = Class.new do + let(:klass) do + Class.new do include Gitlab::GithubImport::Representation::ExposeAttribute expose_attribute :number attr_reader :attributes - def initialize - @attributes = { number: 42 } + def initialize(attributes) + @attributes = attributes + end + end + end + + it 'defines a getter method that returns an attribute value' do + expect(klass.new({ number: 42 }).number).to eq(42) + end + + describe '#[]' do + it 'returns exposed attributes value using array notation' do + expect(klass.new({ number: 42 })[:number]).to eq(42) + end + + context 'when attribute does not exist' do + it 'returns nil' do + expect(klass.new({})[:number]).to eq(nil) end end - expect(klass.new.number).to eq(42) + context 'when attribute is not exposed' do + it 'returns nil' do + expect(klass.new({ not_exposed_attribute: 42 })[:not_exposed_attribute]).to eq(nil) + end + end end end diff --git a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb index d3a98035e73..0256858ecf1 100644 --- a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb +++ b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb @@ -43,7 +43,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_actor) { false } it 'does not return such info' do - expect(issue_event.actor).to eq nil + expect(issue_event.actor).to be_nil end end @@ -57,7 +57,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_label) { false } it 'does not return such info' do - expect(issue_event.label_title).to eq nil + expect(issue_event.label_title).to be_nil end end @@ -72,8 +72,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_rename) { false } it 'does not return such info' do - expect(issue_event.old_title).to eq nil - expect(issue_event.new_title).to eq nil + expect(issue_event.old_title).to be_nil + expect(issue_event.new_title).to be_nil end end @@ -87,30 +87,47 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_milestone) { false } it 'does not return such info' do - expect(issue_event.milestone_title).to eq nil + expect(issue_event.milestone_title).to be_nil end end - context 'when assignee and assigner data is present' do - it 'includes assignee and assigner details' do + context 'when assignee data is present' do + it 'includes assignee details' do expect(issue_event.assignee) .to be_an_instance_of(Gitlab::GithubImport::Representation::User) expect(issue_event.assignee.id).to eq(5) expect(issue_event.assignee.login).to eq('tom') + end + end + + context 'when assignee data is empty' do + let(:with_assignee) { false } - expect(issue_event.assigner) + it 'does not return such info' do + expect(issue_event.assignee).to be_nil + end + end + + context 'when requested_reviewer and review_requester data is present' do + it 'includes requested_reviewer and review_requester details' do + expect(issue_event.requested_reviewer) .to be_an_instance_of(Gitlab::GithubImport::Representation::User) - expect(issue_event.assigner.id).to eq(6) - expect(issue_event.assigner.login).to eq('jerry') + expect(issue_event.requested_reviewer.id).to eq(6) + expect(issue_event.requested_reviewer.login).to eq('mickey') + + expect(issue_event.review_requester) + .to be_an_instance_of(Gitlab::GithubImport::Representation::User) + expect(issue_event.review_requester.id).to eq(7) + expect(issue_event.review_requester.login).to eq('minnie') end end - context 'when assignee and assigner data is empty' do - let(:with_assignee) { false } + context 'when requested_reviewer and review_requester data is empty' do + let(:with_reviewer) { false } it 'does not return such info' do - expect(issue_event.assignee).to eq nil - expect(issue_event.assigner).to eq nil + expect(issue_event.requested_reviewer).to be_nil + expect(issue_event.review_requester).to be_nil end end @@ -148,7 +165,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:response) do event_resource = Struct.new( :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone, - :source, :assignee, :assigner, :issue, :created_at, :performed_via_github_app, + :source, :assignee, :requested_reviewer, :review_requester, :issue, :created_at, + :performed_via_github_app, keyword_init: true ) user_resource = Struct.new(:id, :login, keyword_init: true) @@ -166,7 +184,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do milestone: with_milestone ? { title: 'milestone title' } : nil, source: { type: 'issue', id: 123456 }, assignee: with_assignee ? user_resource.new(id: 5, login: 'tom') : nil, - assigner: with_assignee ? user_resource.new(id: 6, login: 'jerry') : nil, + requested_reviewer: with_reviewer ? user_resource.new(id: 6, login: 'mickey') : nil, + review_requester: with_reviewer ? user_resource.new(id: 7, login: 'minnie') : nil, issue: { 'number' => 2, 'pull_request' => pull_request }, created_at: '2022-04-26 18:30:53 UTC', performed_via_github_app: nil @@ -178,6 +197,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_rename) { true } let(:with_milestone) { true } let(:with_assignee) { true } + let(:with_reviewer) { true } let(:pull_request) { nil } it_behaves_like 'an IssueEvent' do @@ -203,7 +223,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do 'milestone_title' => (with_milestone ? 'milestone title' : nil), 'source' => { 'type' => 'issue', 'id' => 123456 }, 'assignee' => (with_assignee ? { 'id' => 5, 'login' => 'tom' } : nil), - 'assigner' => (with_assignee ? { 'id' => 6, 'login' => 'jerry' } : nil), + 'requested_reviewer' => (with_reviewer ? { 'id' => 6, 'login' => 'mickey' } : nil), + 'review_requester' => (with_reviewer ? { 'id' => 7, 'login' => 'minnie' } : nil), 'issue' => { 'number' => 2, 'pull_request' => pull_request }, 'created_at' => '2022-04-26 18:30:53 UTC', 'performed_via_github_app' => nil @@ -215,6 +236,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_rename) { true } let(:with_milestone) { true } let(:with_assignee) { true } + let(:with_reviewer) { true } let(:pull_request) { nil } let(:issue_event) { described_class.from_json_hash(hash) } diff --git a/spec/lib/gitlab/github_import/representation/lfs_object_spec.rb b/spec/lib/gitlab/github_import/representation/lfs_object_spec.rb index b59ea513436..6663a7366a5 100644 --- a/spec/lib/gitlab/github_import/representation/lfs_object_spec.rb +++ b/spec/lib/gitlab/github_import/representation/lfs_object_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::GithubImport::Representation::LfsObject do describe '#github_identifiers' do diff --git a/spec/lib/gitlab/github_import/representation/note_spec.rb b/spec/lib/gitlab/github_import/representation/note_spec.rb index 97addcc1c98..9f416eb3c02 100644 --- a/spec/lib/gitlab/github_import/representation/note_spec.rb +++ b/spec/lib/gitlab/github_import/representation/note_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::GithubImport::Representation::Note do let(:created_at) { Time.new(2017, 1, 1, 12, 00) } diff --git a/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb b/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb new file mode 100644 index 00000000000..e762dc469c1 --- /dev/null +++ b/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Representation::ProtectedBranch do + shared_examples 'a ProtectedBranch rule' do + it 'returns an instance of ProtectedBranch' do + expect(protected_branch).to be_an_instance_of(described_class) + end + + context 'with ProtectedBranch' do + it 'includes the protected branch ID (name)' do + expect(protected_branch.id).to eq 'main' + end + + it 'includes the protected branch allow_force_pushes' do + expect(protected_branch.allow_force_pushes).to eq true + end + end + end + + describe '.from_api_response' do + let(:response) do + response = Struct.new(:url, :allow_force_pushes, keyword_init: true) + allow_force_pushes = Struct.new(:enabled, keyword_init: true) + response.new( + url: 'https://example.com/branches/main/protection', + allow_force_pushes: allow_force_pushes.new( + enabled: true + ) + ) + end + + it_behaves_like 'a ProtectedBranch rule' do + let(:protected_branch) { described_class.from_api_response(response) } + end + end + + describe '.from_json_hash' do + it_behaves_like 'a ProtectedBranch rule' do + let(:hash) do + { + 'id' => 'main', + 'allow_force_pushes' => true + } + end + + let(:protected_branch) { described_class.from_json_hash(hash) } + end + end +end diff --git a/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb index f812fd85fbc..d6e7a8172f7 100644 --- a/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb +++ b/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do let(:submitted_at) { Time.new(2017, 1, 1, 12, 00).utc } diff --git a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb index 925dba5b5a7..deb9535a845 100644 --- a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb +++ b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::GithubImport::Representation::PullRequest do let(:created_at) { Time.new(2017, 1, 1, 12, 00) } diff --git a/spec/lib/gitlab/github_import/representation/release_attachments_spec.rb b/spec/lib/gitlab/github_import/representation/release_attachments_spec.rb new file mode 100644 index 00000000000..0ef9dad6a13 --- /dev/null +++ b/spec/lib/gitlab/github_import/representation/release_attachments_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Representation::ReleaseAttachments do + shared_examples 'a Release attachments data' do + it 'returns an instance of ReleaseAttachments' do + expect(representation).to be_an_instance_of(described_class) + end + + it 'includes release DB id' do + expect(representation.release_db_id).to eq 42 + end + + it 'includes release description' do + expect(representation.description).to eq 'Some text here..' + end + end + + describe '.from_db_record' do + let(:release) { build_stubbed(:release, id: 42, description: 'Some text here..') } + + it_behaves_like 'a Release attachments data' do + let(:representation) { described_class.from_db_record(release) } + end + end + + describe '.from_json_hash' do + it_behaves_like 'a Release attachments data' do + let(:hash) do + { + 'release_db_id' => 42, + 'description' => 'Some text here..' + } + end + + let(:representation) { described_class.from_json_hash(hash) } + end + end + + describe '#github_identifiers' do + it 'returns a hash with needed identifiers' do + release_id = rand(100) + representation = described_class.new(release_db_id: release_id, description: 'text') + + expect(representation.github_identifiers).to eq({ db_id: release_id }) + end + end +end diff --git a/spec/lib/gitlab/github_import/representation/to_hash_spec.rb b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb index 2770e5c5397..739c832025c 100644 --- a/spec/lib/gitlab/github_import/representation/to_hash_spec.rb +++ b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::GithubImport::Representation::ToHash do describe '#to_hash' do diff --git a/spec/lib/gitlab/github_import/representation/user_spec.rb b/spec/lib/gitlab/github_import/representation/user_spec.rb index 14204886e9b..d7219556ada 100644 --- a/spec/lib/gitlab/github_import/representation/user_spec.rb +++ b/spec/lib/gitlab/github_import/representation/user_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::GithubImport::Representation::User do shared_examples 'a User' do diff --git a/spec/lib/gitlab/github_import/representation_spec.rb b/spec/lib/gitlab/github_import/representation_spec.rb index 58c10c4a775..9a0ef45fc1d 100644 --- a/spec/lib/gitlab/github_import/representation_spec.rb +++ b/spec/lib/gitlab/github_import/representation_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::GithubImport::Representation do describe '.symbolize_hash' do diff --git a/spec/lib/gitlab/github_import/user_finder_spec.rb b/spec/lib/gitlab/github_import/user_finder_spec.rb index d85e298785c..8ebbff31f64 100644 --- a/spec/lib/gitlab/github_import/user_finder_spec.rb +++ b/spec/lib/gitlab/github_import/user_finder_spec.rb @@ -68,10 +68,16 @@ RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache do it_behaves_like 'user ID finder', :assignee end - context 'when the author_key parameter is :assigner' do - let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', assigner: user) } + context 'when the author_key parameter is :requested_reviewer' do + let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', requested_reviewer: user) } - it_behaves_like 'user ID finder', :assigner + it_behaves_like 'user ID finder', :requested_reviewer + end + + context 'when the author_key parameter is :review_requester' do + let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', review_requester: user) } + + it_behaves_like 'user ID finder', :review_requester end end end diff --git a/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb b/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb index 487b19a98e0..5006d27c356 100644 --- a/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb +++ b/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp do let(:log_entry) do { status: 200, - time: { + time: { total: 758.58, db: 77.06, view: 681.52 diff --git a/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb b/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb new file mode 100644 index 00000000000..5858986dfc8 --- /dev/null +++ b/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Gitlab::Graphql::Limit::FieldCallCount do + include GraphqlHelpers + + let(:field_args) { {} } + let(:owner) { fresh_object_type } + let(:field) do + ::Types::BaseField.new(name: 'value', type: GraphQL::Types::String, null: true, owner: owner) do + extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1) + end + end + + let(:query) do + GraphQL::Query.new(GitlabSchema) + end + + def resolve_value + resolve_field(field, { value: 'foo' }, object_type: owner, query: query) + end + + it 'allows the call' do + expect { resolve_value }.not_to raise_error + end + + it 'executes the extension' do + expect(described_class).to receive(:new).and_call_original + + resolve_value + end + + it 'returns an error when the field is called multiple times' do + resolve_value + + expect(resolve_value).to be_an_instance_of(Gitlab::Graphql::Errors::LimitError) + end + + context 'when limit is not specified' do + let(:field) do + ::Types::BaseField.new(name: 'value', type: GraphQL::Types::String, null: true, owner: owner) do + extension(::Gitlab::Graphql::Limit::FieldCallCount) + end + end + + it 'returns an error' do + expect(resolve_value).to be_an_instance_of(Gitlab::Graphql::Errors::ArgumentError) + end + end + + context 'when the field is not extended' do + let(:field) do + ::Types::BaseField.new(name: 'value', type: GraphQL::Types::String, null: true, owner: owner) + end + + it 'allows the call' do + expect { resolve_value }.not_to raise_error + end + + it 'does not execute the extension' do + expect(described_class).not_to receive(:new) + + resolve_value + end + end +end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb index b54c618d8e0..bf09e98331f 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb @@ -49,6 +49,31 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do Gitlab::Json.parse(Base64Bp.urlsafe_decode64(cursor)) end + before do + stub_feature_flags(graphql_keyset_pagination_without_next_page_query: false) + end + + it 'invokes an extra query for the next page check' do + arguments[:first] = 1 + + subject.nodes + + count = ActiveRecord::QueryRecorder.new { subject.has_next_page }.count + expect(count).to eq(1) + end + + context 'when the relation is loaded' do + it 'invokes no extra query' do + allow(subject).to receive(:sliced_nodes).and_return(Project.all.to_a) + arguments[:first] = 1 + + subject.nodes + + count = ActiveRecord::QueryRecorder.new { subject.has_next_page }.count + expect(count).to eq(0) + end + end + describe "with generic keyset order support" do let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } @@ -412,4 +437,382 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do end end end + + # duplicated tests, remove with the removal of the graphql_keyset_pagination_without_next_page_query FF + context 'when the graphql_keyset_pagination_without_next_page_query is on' do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } + + before do + stub_feature_flags(graphql_keyset_pagination_without_next_page_query: true) + end + + it 'does not invoke an extra query for the next page check' do + arguments[:first] = 1 + + subject.nodes + + count = ActiveRecord::QueryRecorder.new { subject.has_next_page }.count + expect(count).to eq(0) + end + + it_behaves_like 'a connection with collection methods' + + it_behaves_like 'a redactable connection' do + let_it_be(:projects) { create_list(:project, 2) } + let(:unwanted) { projects.second } + end + + describe '#cursor_for' do + let(:project) { create(:project) } + let(:cursor) { connection.cursor_for(project) } + + it 'returns an encoded ID' do + expect(decoded_cursor(cursor)).to eq('id' => project.id.to_s) + end + + context 'when an order is specified' do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } + + it 'returns the encoded value of the order' do + expect(decoded_cursor(cursor)).to include('id' => project.id.to_s) + end + end + + context 'when multiple orders are specified' do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_updated_at, column_order_created_at, column_order_id])) } + + it 'returns the encoded value of the order' do + expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) + end + end + end + + describe '#sliced_nodes' do + let(:projects) { create_list(:project, 4) } + + context 'when before is passed' do + let(:arguments) { { before: encoded_cursor(projects[1]) } } + + it 'only returns the project before the selected one' do + expect(subject.sliced_nodes).to contain_exactly(projects.first) + end + + context 'when the sort order is descending' do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) } + + it 'returns the correct nodes' do + expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) + end + end + end + + context 'when after is passed' do + let(:arguments) { { after: encoded_cursor(projects[1]) } } + + it 'only returns the project before the selected one' do + expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) + end + + context 'when the sort order is descending' do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) } + + it 'returns the correct nodes' do + expect(subject.sliced_nodes).to contain_exactly(projects.first) + end + end + end + + context 'when both before and after are passed' do + let(:arguments) do + { + after: encoded_cursor(projects[1]), + before: encoded_cursor(projects[3]) + } + end + + it 'returns the expected set' do + expect(subject.sliced_nodes).to contain_exactly(projects[2]) + end + end + + shared_examples 'nodes are in ascending order' do + context 'when no cursor is passed' do + let(:arguments) { {} } + + it 'returns projects in ascending order' do + expect(subject.sliced_nodes).to eq(ascending_nodes) + end + end + + context 'when before cursor value is not NULL' do + let(:arguments) { { before: encoded_cursor(ascending_nodes[2]) } } + + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq(ascending_nodes.first(2)) + end + end + + context 'when after cursor value is not NULL' do + let(:arguments) { { after: encoded_cursor(ascending_nodes[1]) } } + + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(ascending_nodes.last(3)) + end + end + + context 'when before and after cursor' do + let(:arguments) { { before: encoded_cursor(ascending_nodes.last), after: encoded_cursor(ascending_nodes.first) } } + + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(ascending_nodes[1..3]) + end + end + end + + shared_examples 'nodes are in descending order' do + context 'when no cursor is passed' do + let(:arguments) { {} } + + it 'only returns projects in descending order' do + expect(subject.sliced_nodes).to eq(descending_nodes) + end + end + + context 'when before cursor value is not NULL' do + let(:arguments) { { before: encoded_cursor(descending_nodes[2]) } } + + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq(descending_nodes.first(2)) + end + end + + context 'when after cursor value is not NULL' do + let(:arguments) { { after: encoded_cursor(descending_nodes[1]) } } + + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(descending_nodes.last(3)) + end + end + + context 'when before and after cursor' do + let(:arguments) { { before: encoded_cursor(descending_nodes.last), after: encoded_cursor(descending_nodes.first) } } + + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(descending_nodes[1..3]) + end + end + end + + context 'when multiple orders with nil values are defined' do + let_it_be(:project1) { create(:project, last_repository_check_at: 10.days.ago) } # Asc: project5 Desc: project3 + let_it_be(:project2) { create(:project, last_repository_check_at: nil) } # Asc: project1 Desc: project1 + let_it_be(:project3) { create(:project, last_repository_check_at: 5.days.ago) } # Asc: project3 Desc: project5 + let_it_be(:project4) { create(:project, last_repository_check_at: nil) } # Asc: project2 Desc: project2 + let_it_be(:project5) { create(:project, last_repository_check_at: 20.days.ago) } # Asc: project4 Desc: project4 + + context 'when ascending' do + let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo, column_order_id]) } + let_it_be(:nodes) { Project.order(order) } + let_it_be(:ascending_nodes) { [project5, project1, project3, project2, project4] } + + it_behaves_like 'nodes are in ascending order' + + context 'when before cursor value is NULL' do + let(:arguments) { { before: encoded_cursor(project4) } } + + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq([project5, project1, project3, project2]) + end + end + + context 'when after cursor value is NULL' do + let(:arguments) { { after: encoded_cursor(project2) } } + + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq([project4]) + end + end + end + + context 'when descending' do + let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo_desc, column_order_id]) } + let_it_be(:nodes) { Project.order(order) } + let_it_be(:descending_nodes) { [project3, project1, project5, project2, project4] } + + it_behaves_like 'nodes are in descending order' + + context 'when before cursor value is NULL' do + let(:arguments) { { before: encoded_cursor(project4) } } + + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq([project3, project1, project5, project2]) + end + end + + context 'when after cursor value is NULL' do + let(:arguments) { { after: encoded_cursor(project2) } } + + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq([project4]) + end + end + end + end + + context 'when ordering by similarity' do + let_it_be(:project1) { create(:project, name: 'test') } + let_it_be(:project2) { create(:project, name: 'testing') } + let_it_be(:project3) { create(:project, name: 'tests') } + let_it_be(:project4) { create(:project, name: 'testing stuff') } + let_it_be(:project5) { create(:project, name: 'test') } + + let_it_be(:nodes) do + # Note: sorted_by_similarity_desc scope internally supports the generic keyset order. + Project.sorted_by_similarity_desc('test', include_in_select: true) + end + + let_it_be(:descending_nodes) { nodes.to_a } + + it_behaves_like 'nodes are in descending order' + end + + context 'when an invalid cursor is provided' do + let(:arguments) { { before: Base64Bp.urlsafe_encode64('invalidcursor', padding: false) } } + + it 'raises an error' do + expect { subject.sliced_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end + end + end + + describe '#nodes' do + let_it_be(:all_nodes) { create_list(:project, 5) } + + let(:paged_nodes) { subject.nodes } + + it_behaves_like 'connection with paged nodes' do + let(:paged_nodes_size) { 3 } + end + + context 'when both are passed' do + let(:arguments) { { first: 2, last: 2 } } + + it 'raises an error' do + expect { paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end + end + + context 'when primary key is not in original order' do + let(:nodes) { Project.order(last_repository_check_at: :desc) } + + it 'is added to end' do + sliced = subject.sliced_nodes + + order_sql = sliced.order_values.last.to_sql + + expect(order_sql).to end_with(Project.arel_table[:id].desc.to_sql) + end + end + + context 'when there is no primary key' do + before do + stub_const('NoPrimaryKey', Class.new(ActiveRecord::Base)) + NoPrimaryKey.class_eval do + self.table_name = 'no_primary_key' + self.primary_key = nil + end + end + + let(:nodes) { NoPrimaryKey.all } + + it 'raises an error' do + expect(NoPrimaryKey.primary_key).to be_nil + expect { subject.sliced_nodes }.to raise_error(ArgumentError, 'Relation must have a primary key') + end + end + end + + describe '#has_previous_page and #has_next_page' do + # using a list of 5 items with a max_page of 3 + let_it_be(:project_list) { create_list(:project, 5) } + let_it_be(:nodes) { Project.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } + + context 'when default query' do + let(:arguments) { {} } + + it 'has no previous, but a next' do + expect(subject.has_previous_page).to be_falsey + expect(subject.has_next_page).to be_truthy + end + end + + context 'when before is first item' do + let(:arguments) { { before: encoded_cursor(project_list.first) } } + + it 'has no previous, but a next' do + expect(subject.has_previous_page).to be_falsey + expect(subject.has_next_page).to be_truthy + end + end + + describe 'using `before`' do + context 'when before is the last item' do + let(:arguments) { { before: encoded_cursor(project_list.last) } } + + it 'has no previous, but a next' do + expect(subject.has_previous_page).to be_falsey + expect(subject.has_next_page).to be_truthy + end + end + + context 'when before and last specified' do + let(:arguments) { { before: encoded_cursor(project_list.last), last: 2 } } + + it 'has a previous and a next' do + expect(subject.has_previous_page).to be_truthy + expect(subject.has_next_page).to be_truthy + end + end + + context 'when before and last does request all remaining nodes' do + let(:arguments) { { before: encoded_cursor(project_list[1]), last: 3 } } + + it 'has a previous and a next' do + expect(subject.has_previous_page).to be_falsey + expect(subject.has_next_page).to be_truthy + expect(subject.nodes).to eq [project_list[0]] + end + end + end + + describe 'using `after`' do + context 'when after is the first item' do + let(:arguments) { { after: encoded_cursor(project_list.first) } } + + it 'has a previous, and a next' do + expect(subject.has_previous_page).to be_truthy + expect(subject.has_next_page).to be_truthy + end + end + + context 'when after and first specified' do + let(:arguments) { { after: encoded_cursor(project_list.first), first: 2 } } + + it 'has a previous and a next' do + expect(subject.has_previous_page).to be_truthy + expect(subject.has_next_page).to be_truthy + end + end + + context 'when before and last does request all remaining nodes' do + let(:arguments) { { after: encoded_cursor(project_list[2]), last: 3 } } + + it 'has a previous but no next' do + expect(subject.has_previous_page).to be_truthy + expect(subject.has_next_page).to be_falsey + end + end + end + end + end end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb deleted file mode 100644 index 792cb03e8c7..00000000000 --- a/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Graphql::Pagination::Keyset::LastItems do - let_it_be(:merge_request) { create(:merge_request) } - - let(:scope) { MergeRequest.order_merged_at_asc } - - subject { described_class.take_items(*args) } - - context 'when the `count` parameter is nil' do - let(:args) { [scope, nil] } - - it 'returns a single record' do - expect(subject).to eq(merge_request) - end - end - - context 'when the `count` parameter is given' do - let(:args) { [scope, 1] } - - it 'returns an array' do - expect(subject).to eq([merge_request]) - end - end -end diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb index 7c346e3eb69..000b8eff661 100644 --- a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb +++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::HealthChecks::GitalyCheck do let(:result_class) { Gitlab::HealthChecks::Result } @@ -14,20 +14,36 @@ RSpec.describe Gitlab::HealthChecks::GitalyCheck do subject { described_class.readiness } before do - expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check) + expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(healthy_check) end context 'Gitaly server is up' do - let(:gitaly_check) { double(check: { success: true }) } + before do + expect(Gitlab::GitalyClient::ServerService).to receive(:new).and_return(ready_check) + end + + let(:healthy_check) { double(check: { success: true }) } + let(:ready_check) { double(readiness_check: { success: true }) } it { is_expected.to eq([result_class.new('gitaly_check', true, nil, shard: 'default')]) } end context 'Gitaly server is down' do - let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) } + let(:healthy_check) { double(check: { success: false, message: 'Connection refused' }) } it { is_expected.to eq([result_class.new('gitaly_check', false, 'Connection refused', shard: 'default')]) } end + + context 'Gitaly server is not ready' do + before do + expect(Gitlab::GitalyClient::ServerService).to receive(:new).and_return(ready_check) + end + + let(:healthy_check) { double(check: { success: true }) } + let(:ready_check) { double(readiness_check: { success: false, message: 'Clock is out of sync' }) } + + it { is_expected.to match_array([result_class.new('gitaly_check', false, 'Clock is out of sync', shard: 'default')]) } + end end describe '#metrics' do diff --git a/spec/lib/gitlab/health_checks/master_check_spec.rb b/spec/lib/gitlab/health_checks/master_check_spec.rb index 287ebcec207..8a87b01c560 100644 --- a/spec/lib/gitlab/health_checks/master_check_spec.rb +++ b/spec/lib/gitlab/health_checks/master_check_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' require_relative './simple_check_shared' RSpec.describe Gitlab::HealthChecks::MasterCheck do diff --git a/spec/lib/gitlab/health_checks/probes/collection_spec.rb b/spec/lib/gitlab/health_checks/probes/collection_spec.rb index 741c45d953c..f1791375cea 100644 --- a/spec/lib/gitlab/health_checks/probes/collection_spec.rb +++ b/spec/lib/gitlab/health_checks/probes/collection_spec.rb @@ -12,18 +12,16 @@ RSpec.describe Gitlab::HealthChecks::Probes::Collection do let(:checks) do [ Gitlab::HealthChecks::DbCheck, - Gitlab::HealthChecks::Redis::RedisCheck, - Gitlab::HealthChecks::Redis::CacheCheck, - Gitlab::HealthChecks::Redis::QueuesCheck, - Gitlab::HealthChecks::Redis::SharedStateCheck, - Gitlab::HealthChecks::Redis::TraceChunksCheck, - Gitlab::HealthChecks::Redis::RateLimitingCheck, - Gitlab::HealthChecks::Redis::SessionsCheck, + *Gitlab::HealthChecks::Redis::ALL_INSTANCE_CHECKS, Gitlab::HealthChecks::GitalyCheck ] end it 'responds with readiness checks data' do + expect_next_instance_of(Gitlab::GitalyClient::ServerService) do |service| + expect(service).to receive(:readiness_check).and_return({ success: true }) + end + expect(subject.http_status).to eq(200) expect(subject.json[:status]).to eq('ok') @@ -37,8 +35,8 @@ RSpec.describe Gitlab::HealthChecks::Probes::Collection do context 'when Redis fails' do before do - allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return( - Gitlab::HealthChecks::Result.new('redis_check', false, "check error")) + allow(Gitlab::HealthChecks::Redis::SharedStateCheck).to receive(:readiness).and_return( + Gitlab::HealthChecks::Result.new('shared_state_check', false, "check error")) end it 'responds with failure' do @@ -46,14 +44,14 @@ RSpec.describe Gitlab::HealthChecks::Probes::Collection do expect(subject.json[:status]).to eq('failed') expect(subject.json['cache_check']).to contain_exactly(status: 'ok') - expect(subject.json['redis_check']).to contain_exactly( + expect(subject.json['shared_state_check']).to contain_exactly( status: 'failed', message: 'check error') end end context 'when check raises exception not handled inside the check' do before do - expect(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_raise( + expect(Gitlab::HealthChecks::Redis::CacheCheck).to receive(:readiness).and_raise( ::Redis::CannotConnectError, 'Redis down') end diff --git a/spec/lib/gitlab/health_checks/redis/cache_check_spec.rb b/spec/lib/gitlab/health_checks/redis/cache_check_spec.rb deleted file mode 100644 index c44bd2ed585..00000000000 --- a/spec/lib/gitlab/health_checks/redis/cache_check_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require_relative '../simple_check_shared' - -RSpec.describe Gitlab::HealthChecks::Redis::CacheCheck do - include_examples 'simple_check', 'redis_cache_ping', 'RedisCache', 'PONG' -end diff --git a/spec/lib/gitlab/health_checks/redis/queues_check_spec.rb b/spec/lib/gitlab/health_checks/redis/queues_check_spec.rb deleted file mode 100644 index 3882e7db9d9..00000000000 --- a/spec/lib/gitlab/health_checks/redis/queues_check_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require_relative '../simple_check_shared' - -RSpec.describe Gitlab::HealthChecks::Redis::QueuesCheck do - include_examples 'simple_check', 'redis_queues_ping', 'RedisQueues', 'PONG' -end diff --git a/spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb b/spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb deleted file mode 100644 index 1521fc99cde..00000000000 --- a/spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require_relative '../simple_check_shared' - -RSpec.describe Gitlab::HealthChecks::Redis::RateLimitingCheck do - include_examples 'simple_check', 'redis_rate_limiting_ping', 'RedisRateLimiting', 'PONG' -end diff --git a/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb b/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb deleted file mode 100644 index 145d573b6de..00000000000 --- a/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require_relative '../simple_check_shared' - -RSpec.describe Gitlab::HealthChecks::Redis::RedisCheck do - include_examples 'simple_check', 'redis_ping', 'Redis', true -end diff --git a/spec/lib/gitlab/health_checks/redis/sessions_check_spec.rb b/spec/lib/gitlab/health_checks/redis/sessions_check_spec.rb deleted file mode 100644 index 82b3b33ec0a..00000000000 --- a/spec/lib/gitlab/health_checks/redis/sessions_check_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require_relative '../simple_check_shared' - -RSpec.describe Gitlab::HealthChecks::Redis::SessionsCheck do - include_examples 'simple_check', 'redis_sessions_ping', 'RedisSessions', 'PONG' -end diff --git a/spec/lib/gitlab/health_checks/redis/shared_state_check_spec.rb b/spec/lib/gitlab/health_checks/redis/shared_state_check_spec.rb deleted file mode 100644 index 25917741a1c..00000000000 --- a/spec/lib/gitlab/health_checks/redis/shared_state_check_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require_relative '../simple_check_shared' - -RSpec.describe Gitlab::HealthChecks::Redis::SharedStateCheck do - include_examples 'simple_check', 'redis_shared_state_ping', 'RedisSharedState', 'PONG' -end diff --git a/spec/lib/gitlab/health_checks/redis/trace_chunks_check_spec.rb b/spec/lib/gitlab/health_checks/redis/trace_chunks_check_spec.rb deleted file mode 100644 index 5fb5232a4dd..00000000000 --- a/spec/lib/gitlab/health_checks/redis/trace_chunks_check_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require_relative '../simple_check_shared' - -RSpec.describe Gitlab::HealthChecks::Redis::TraceChunksCheck do - include_examples 'simple_check', 'redis_trace_chunks_ping', 'RedisTraceChunks', 'PONG' -end diff --git a/spec/lib/gitlab/health_checks/redis_spec.rb b/spec/lib/gitlab/health_checks/redis_spec.rb new file mode 100644 index 00000000000..2460f57a9ec --- /dev/null +++ b/spec/lib/gitlab/health_checks/redis_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +require 'spec_helper' +require_relative './simple_check_shared' + +RSpec.describe Gitlab::HealthChecks::Redis do + describe "ALL_INSTANCE_CHECKS" do + subject { described_class::ALL_INSTANCE_CHECKS } + + it { is_expected.to include(described_class::CacheCheck, described_class::QueuesCheck) } + + it "contains a check for each redis instance" do + expect(subject.map(&:redis_instance_class_name)).to contain_exactly(*Gitlab::Redis::ALL_CLASSES) + end + end + + describe 'all checks' do + described_class::ALL_INSTANCE_CHECKS.each do |check| + describe check do + include_examples 'simple_check', + "redis_#{check.redis_instance_class_name.store_name.underscore}_ping", + check.redis_instance_class_name.store_name, + 'PONG' + end + end + end +end diff --git a/spec/lib/gitlab/i18n/metadata_entry_spec.rb b/spec/lib/gitlab/i18n/metadata_entry_spec.rb index 2f8816e62cc..fcdf3358570 100644 --- a/spec/lib/gitlab/i18n/metadata_entry_spec.rb +++ b/spec/lib/gitlab/i18n/metadata_entry_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::I18n::MetadataEntry do describe '#expected_forms' do diff --git a/spec/lib/gitlab/i18n/translation_entry_spec.rb b/spec/lib/gitlab/i18n/translation_entry_spec.rb index f05346d07d3..df503e68cf1 100644 --- a/spec/lib/gitlab/i18n/translation_entry_spec.rb +++ b/spec/lib/gitlab/i18n/translation_entry_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::I18n::TranslationEntry do describe '#singular_translation' do diff --git a/spec/lib/gitlab/import/merge_request_creator_spec.rb b/spec/lib/gitlab/import/merge_request_creator_spec.rb index 9aedca40f1b..8f502216294 100644 --- a/spec/lib/gitlab/import/merge_request_creator_spec.rb +++ b/spec/lib/gitlab/import/merge_request_creator_spec.rb @@ -8,10 +8,13 @@ RSpec.describe Gitlab::Import::MergeRequestCreator do subject { described_class.new(project) } describe '#execute' do + let(:attributes) do + HashWithIndifferentAccess.new(merge_request.attributes.except('merge_params', 'suggested_reviewers')) + end + context 'merge request already exists' do let(:merge_request) { create(:merge_request, target_project: project, source_project: project) } let(:commits) { merge_request.merge_request_diffs.first.commits } - let(:attributes) { HashWithIndifferentAccess.new(merge_request.attributes.except("merge_params")) } it 'updates the data' do commits_count = commits.count @@ -31,7 +34,6 @@ RSpec.describe Gitlab::Import::MergeRequestCreator do context 'new merge request' do let(:merge_request) { build(:merge_request, target_project: project, source_project: project) } - let(:attributes) { HashWithIndifferentAccess.new(merge_request.attributes.except("merge_params")) } it 'creates a new merge request' do attributes.delete(:id) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 9aec3271913..e270ca9ec6a 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -211,6 +211,8 @@ merge_requests: - user_note_authors - cleanup_schedule - compliance_violations +- created_environments +- predictions external_pull_requests: - project merge_request_diff: @@ -315,6 +317,7 @@ statuses: - user - auto_canceled_by - needs +- ci_stage variables: - project triggers: @@ -654,11 +657,9 @@ search_data: merge_request_assignees: - merge_request - assignee -- updated_state_by merge_request_reviewers: - merge_request - reviewer -- updated_state_by lfs_file_locks: - user project_badges: @@ -821,3 +822,28 @@ service_desk_setting: approvals: - user - merge_request +resource_milestone_events: + - user + - issue + - merge_request + - milestone +resource_state_events: + - user + - issue + - merge_request + - source_merge_request + - epic +iteration: + - group + - iterations_cadence + - issues + - labels + - merge_requests +resource_iteration_events: + - user + - issue + - merge_request + - iteration +iterations_cadence: + - group + - iterations diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb index 733be7fc226..272c2629b08 100644 --- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb +++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::ImportExport::AttributeCleaner do let(:relation_class) { double('relation_class').as_null_object } diff --git a/spec/lib/gitlab/import_export/attributes_finder_spec.rb b/spec/lib/gitlab/import_export/attributes_finder_spec.rb index 428d8d605ee..6536b895b2f 100644 --- a/spec/lib/gitlab/import_export/attributes_finder_spec.rb +++ b/spec/lib/gitlab/import_export/attributes_finder_spec.rb @@ -123,7 +123,7 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder do is_expected.to match( include: [{ merge_requests: { include: [{ notes: { include: [{ author: { include: [] } }], - preload: { author: nil } } }], + preload: { author: nil } } }], preload: { notes: { author: nil } } } }], preload: { merge_requests: { notes: { author: nil } } } @@ -132,7 +132,7 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder do it 'generates the correct hash for a relation with included attributes' do setup_yaml(tree: { project: [:issues] }, - included_attributes: { issues: [:name, :description] }) + included_attributes: { issues: [:name, :description] }) is_expected.to match( include: [{ issues: { include: [], @@ -143,7 +143,7 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder do it 'generates the correct hash for a relation with excluded attributes' do setup_yaml(tree: { project: [:issues] }, - excluded_attributes: { issues: [:name] }) + excluded_attributes: { issues: [:name] }) is_expected.to match( include: [{ issues: { except: [:name], @@ -154,8 +154,8 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder do 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] }) + excluded_attributes: { issues: [:name] }, + included_attributes: { issues: [:description] }) is_expected.to match( include: [{ issues: { except: [:name], @@ -167,7 +167,7 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder do it 'generates the correct hash for a relation with custom methods' do setup_yaml(tree: { project: [:issues] }, - methods: { issues: [:name] }) + methods: { issues: [:name] }) is_expected.to match( include: [{ issues: { include: [], diff --git a/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb b/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb index 9f1b15aa049..4ee825c71b6 100644 --- a/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb +++ b/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb @@ -79,14 +79,14 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver do let(:relation_definition) { { 'notes' => {} } } it 'saves valid subrelations and logs invalid subrelation' do - expect(relation_object.notes).to receive(:<<).and_call_original + expect(relation_object.notes).to receive(:<<).twice.and_call_original expect(Gitlab::Import::Logger) .to receive(:info) .with( message: '[Project/Group Import] Invalid subrelation', project_id: project.id, relation_key: 'issues', - error_messages: "Noteable can't be blank and Project does not match noteable project" + error_messages: "Project does not match noteable project" ) saver.execute @@ -94,9 +94,28 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver do issue = project.issues.last import_failure = project.import_failures.last + expect(invalid_note.persisted?).to eq(false) expect(issue.notes.count).to eq(5) expect(import_failure.source).to eq('RelationObjectSaver#save!') - expect(import_failure.exception_message).to eq("Noteable can't be blank and Project does not match noteable project") + expect(import_failure.exception_message).to eq('Project does not match noteable project') + end + + context 'when invalid subrelation can still be persisted' do + let(:relation_key) { 'merge_requests' } + let(:relation_definition) { { 'approvals' => {} } } + let(:approval_1) { build(:approval, merge_request_id: nil, user: create(:user)) } + let(:approval_2) { build(:approval, merge_request_id: nil, user: create(:user)) } + let(:relation_object) { build(:merge_request, source_project: project, target_project: project, approvals: [approval_1, approval_2]) } + + it 'saves the subrelation' do + expect(approval_1.valid?).to eq(false) + expect(Gitlab::Import::Logger).not_to receive(:info) + + saver.execute + + expect(project.merge_requests.first.approvals.count).to eq(2) + expect(project.merge_requests.first.approvals.first.persisted?).to eq(true) + end end context 'when importable is group' do diff --git a/spec/lib/gitlab/import_export/config_spec.rb b/spec/lib/gitlab/import_export/config_spec.rb index fcb48678b88..8f848af8bd3 100644 --- a/spec/lib/gitlab/import_export/config_spec.rb +++ b/spec/lib/gitlab/import_export/config_spec.rb @@ -21,10 +21,12 @@ RSpec.describe Gitlab::ImportExport::Config do end it 'parses default config' do + expected_keys = [:tree, :excluded_attributes, :included_attributes, :methods, :preloads, :export_reorders] + expected_keys << :include_if_exportable if ee + expect { subject }.not_to raise_error expect(subject).to be_a(Hash) - expect(subject.keys).to contain_exactly( - :tree, :excluded_attributes, :included_attributes, :methods, :preloads, :export_reorders) + expect(subject.keys).to match_array(expected_keys) end end end diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb index 7b27f7183b0..5a75631ec4d 100644 --- a/spec/lib/gitlab/import_export/file_importer_spec.rb +++ b/spec/lib/gitlab/import_export/file_importer_spec.rb @@ -169,7 +169,7 @@ RSpec.describe Gitlab::ImportExport::FileImporter do end it 'skips validation' do - expect(subject).to receive(:validate_decompressed_archive_size).never + expect(subject).not_to receive(:validate_decompressed_archive_size) subject.import end diff --git a/spec/lib/gitlab/import_export/group/object_builder_spec.rb b/spec/lib/gitlab/import_export/group/object_builder_spec.rb index 09f40199b31..25d9858dd4c 100644 --- a/spec/lib/gitlab/import_export/group/object_builder_spec.rb +++ b/spec/lib/gitlab/import_export/group/object_builder_spec.rb @@ -6,9 +6,9 @@ RSpec.describe Gitlab::ImportExport::Group::ObjectBuilder do let(:group) { create(:group) } let(:base_attributes) do { - 'title' => 'title', + 'title' => 'title', 'description' => 'description', - 'group' => group + 'group' => group } end diff --git a/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb index 2f1e2dd2db4..5e84284a060 100644 --- a/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb @@ -33,15 +33,15 @@ RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer do let(:relation_tree_restorer) do described_class.new( - user: user, - shared: shared, - relation_reader: relation_reader, - object_builder: Gitlab::ImportExport::Group::ObjectBuilder, - members_mapper: members_mapper, - relation_factory: Gitlab::ImportExport::Group::RelationFactory, - reader: reader, - importable: importable, - importable_path: nil, + user: user, + shared: shared, + relation_reader: relation_reader, + object_builder: Gitlab::ImportExport::Group::ObjectBuilder, + members_mapper: members_mapper, + relation_factory: Gitlab::ImportExport::Group::RelationFactory, + reader: reader, + importable: importable, + importable_path: nil, importable_attributes: attributes ) end diff --git a/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb b/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb index ab2c4cc2059..ed4368ba802 100644 --- a/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb +++ b/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::ImportExport::Json::LegacyWriter do let(:path) { "#{Dir.tmpdir}/legacy_writer_spec/test.json" } diff --git a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb index 3088129a732..02ac8065c9f 100644 --- a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb +++ b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb @@ -32,18 +32,20 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do let(:hash) { { name: exportable.name, description: exportable.description }.stringify_keys } let(:include) { [] } let(:custom_orderer) { nil } + let(:include_if_exportable) { {} } let(:relations_schema) do { only: [:name, :description], include: include, preload: { issues: nil }, - export_reorder: custom_orderer + export_reorder: custom_orderer, + include_if_exportable: include_if_exportable } end subject do - described_class.new(exportable, relations_schema, json_writer, exportable_path: exportable_path, logger: logger) + described_class.new(exportable, relations_schema, json_writer, exportable_path: exportable_path, logger: logger, current_user: user) end describe '#execute' do @@ -210,11 +212,62 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do subject.execute end end - end - describe '.batch_size' do - it 'returns default batch size' do - expect(described_class.batch_size(exportable)).to eq(described_class::BATCH_SIZE) + describe 'conditional export of included associations' do + let(:include) do + [{ issues: { include: [{ label_links: { include: [:label] } }] } }] + end + + let(:include_if_exportable) do + { issues: [:label_links] } + end + + let_it_be(:label) { create(:label, project: exportable) } + let_it_be(:link) { create(:label_link, label: label, target: issue) } + + context 'when association is exportable' do + before do + allow_next_found_instance_of(Issue) do |issue| + allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(true) + end + end + + it 'includes exportable association' do + expected_issue = issue.to_json(include: [{ label_links: { include: [:label] } }]) + + expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(expected_issue)) + + subject.execute + end + end + + context 'when association is not exportable' do + before do + allow_next_found_instance_of(Issue) do |issue| + allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(false) + end + end + + it 'filters out not exportable association' do + expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(issue.to_json)) + + subject.execute + end + end + + context 'when association does not respond to exportable_association?' do + before do + allow_next_found_instance_of(Issue) do |issue| + allow(issue).to receive(:respond_to?).with(:exportable_association?).and_return(false) + end + end + + it 'filters out not exportable association' do + expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(issue.to_json)) + + subject.execute + end + end end end diff --git a/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb b/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb index 0d372def8b0..c2c50751c3f 100644 --- a/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Gitlab::ImportExport::LegacyRelationTreeSaver do it 'uses FastHashSerializer' do expect(Gitlab::ImportExport::FastHashSerializer) .to receive(:new) - .with(exportable, tree, batch_size: Gitlab::ImportExport::Json::StreamingSerializer::BATCH_SIZE) + .with(exportable, tree) .and_return(serializer) expect(serializer).to receive(:execute) diff --git a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb index b7b652005e9..ac646087a95 100644 --- a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb @@ -21,15 +21,15 @@ RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer do let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) } let(:relation_tree_restorer) do described_class.new( - user: user, - shared: shared, - relation_reader: relation_reader, - object_builder: Gitlab::ImportExport::Project::ObjectBuilder, - members_mapper: members_mapper, - relation_factory: Gitlab::ImportExport::Project::RelationFactory, - reader: reader, - importable: importable, - importable_path: 'project', + user: user, + shared: shared, + relation_reader: relation_reader, + object_builder: Gitlab::ImportExport::Project::ObjectBuilder, + members_mapper: members_mapper, + relation_factory: Gitlab::ImportExport::Project::RelationFactory, + reader: reader, + importable: importable, + importable_path: 'project', importable_attributes: attributes ) end diff --git a/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb index 3dab84af744..d1fe9b80062 100644 --- a/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb @@ -21,15 +21,15 @@ RSpec.describe Gitlab::ImportExport::Project::Sample::RelationTreeRestorer do let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) } let(:sample_data_relation_tree_restorer) do described_class.new( - user: user, - shared: shared, - relation_reader: relation_reader, - object_builder: Gitlab::ImportExport::Project::ObjectBuilder, - members_mapper: members_mapper, - relation_factory: Gitlab::ImportExport::Project::Sample::RelationFactory, - reader: reader, - importable: importable, - importable_path: 'project', + user: user, + shared: shared, + relation_reader: relation_reader, + object_builder: Gitlab::ImportExport::Project::ObjectBuilder, + members_mapper: members_mapper, + relation_factory: Gitlab::ImportExport::Project::Sample::RelationFactory, + reader: reader, + importable: importable, + importable_path: 'project', importable_attributes: attributes ) end 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 47d7555c8f4..299e107c881 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -192,10 +192,26 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do expect(Issue.find_by(title: 'Voluptatem').resource_label_events).not_to be_empty end + it 'restores issue resource milestone events' do + expect(Issue.find_by(title: 'Voluptatem').resource_milestone_events).not_to be_empty + end + + it 'restores issue resource state events' do + expect(Issue.find_by(title: 'Voluptatem').resource_state_events).not_to be_empty + end + it 'restores merge requests resource label events' do expect(MergeRequest.find_by(title: 'MR1').resource_label_events).not_to be_empty end + it 'restores merge request resource milestone events' do + expect(MergeRequest.find_by(title: 'MR1').resource_milestone_events).not_to be_empty + end + + it 'restores merge request resource state events' do + expect(MergeRequest.find_by(title: 'MR1').resource_state_events).not_to be_empty + end + it 'restores suggestion' do note = Note.find_by("note LIKE 'Saepe asperiores exercitationem non dignissimos laborum reiciendis et ipsum%'") diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 6cfc24a8996..e591cbd05a0 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -586,6 +586,7 @@ ProjectFeature: - environments_access_level - feature_flags_access_level - releases_access_level +- monitor_access_level - created_at - updated_at ProtectedBranch::MergeAccessLevel: @@ -706,7 +707,7 @@ ProtectedEnvironment: - name - created_at - updated_at -ProtectedEnvironment::DeployAccessLevel: +ProtectedEnvironments::DeployAccessLevel: - id - protected_environment_id - access_level @@ -917,3 +918,29 @@ Approval: - user_id - created_at - updated_at +ResourceMilestoneEvent: + - user_id + - action + - state + - created_at +ResourceStateEvent: + - user_id + - created_at + - state + - source_commit + - close_after_error_tracking_resolve + - close_auto_resolve_prometheus_alert +Iteration: + - created_at + - updated_at + - start_date + - due_date + - group_id + - iid + - description +ResourceIterationEvent: + - user_id + - created_at + - action +Iterations::Cadence: + - title diff --git a/spec/lib/gitlab/import_formatter_spec.rb b/spec/lib/gitlab/import_formatter_spec.rb index fbf00ab92d3..0feff61725b 100644 --- a/spec/lib/gitlab/import_formatter_spec.rb +++ b/spec/lib/gitlab/import_formatter_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::ImportFormatter do let(:formatter) { Gitlab::ImportFormatter.new } diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb index f42a109aa3a..41ffcece221 100644 --- a/spec/lib/gitlab/import_sources_spec.rb +++ b/spec/lib/gitlab/import_sources_spec.rb @@ -7,17 +7,17 @@ RSpec.describe Gitlab::ImportSources do it 'returns a hash' do expected = { - 'GitHub' => 'github', - 'Bitbucket Cloud' => 'bitbucket', - 'Bitbucket Server' => 'bitbucket_server', - 'GitLab.com' => 'gitlab', - 'Google Code' => 'google_code', - 'FogBugz' => 'fogbugz', + 'GitHub' => 'github', + 'Bitbucket Cloud' => 'bitbucket', + 'Bitbucket Server' => 'bitbucket_server', + 'GitLab.com' => 'gitlab', + 'Google Code' => 'google_code', + 'FogBugz' => 'fogbugz', 'Repository by URL' => 'git', - 'GitLab export' => 'gitlab_project', - 'Gitea' => 'gitea', - 'Manifest file' => 'manifest', - 'Phabricator' => 'phabricator' + 'GitLab export' => 'gitlab_project', + 'Gitea' => 'gitea', + 'Manifest file' => 'manifest', + 'Phabricator' => 'phabricator' } expect(described_class.options).to eq(expected) diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index 72d201eed77..1545de6d8fd 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "spec_helper" +require 'fast_spec_helper' RSpec.describe Gitlab::IncomingEmail do describe "self.enabled?" do diff --git a/spec/lib/gitlab/insecure_key_fingerprint_spec.rb b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb index 3a281574563..f2bf06236b9 100644 --- a/spec/lib/gitlab/insecure_key_fingerprint_spec.rb +++ b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::InsecureKeyFingerprint do let(:key) do diff --git a/spec/lib/gitlab/instrumentation/redis_base_spec.rb b/spec/lib/gitlab/instrumentation/redis_base_spec.rb index a7e08b5a9bd..f9dd0c94c97 100644 --- a/spec/lib/gitlab/instrumentation/redis_base_spec.rb +++ b/spec/lib/gitlab/instrumentation/redis_base_spec.rb @@ -65,6 +65,13 @@ RSpec.describe Gitlab::Instrumentation::RedisBase, :request_store do expect(instrumentation_class_b.get_request_count).to eq(2) end end + + it 'increments by the given amount' do + instrumentation_class_a.increment_request_count(2) + instrumentation_class_a.increment_request_count(3) + + expect(instrumentation_class_a.get_request_count).to eq(5) + end end describe '.increment_write_bytes' do @@ -103,21 +110,21 @@ RSpec.describe Gitlab::Instrumentation::RedisBase, :request_store do context 'storage key overlapping' do it 'keys do not overlap across storages' do 2.times do - instrumentation_class_a.add_call_details(0.3, [:set]) - instrumentation_class_b.add_call_details(0.4, [:set]) + instrumentation_class_a.add_call_details(0.3, [[:set]]) + instrumentation_class_b.add_call_details(0.4, [[:set]]) end expect(instrumentation_class_a.detail_store).to match( [ - a_hash_including(cmd: :set, duration: 0.3, backtrace: an_instance_of(Array)), - a_hash_including(cmd: :set, duration: 0.3, backtrace: an_instance_of(Array)) + a_hash_including(commands: [[:set]], duration: 0.3, backtrace: an_instance_of(Array)), + a_hash_including(commands: [[:set]], duration: 0.3, backtrace: an_instance_of(Array)) ] ) expect(instrumentation_class_b.detail_store).to match( [ - a_hash_including(cmd: :set, duration: 0.4, backtrace: an_instance_of(Array)), - a_hash_including(cmd: :set, duration: 0.4, backtrace: an_instance_of(Array)) + a_hash_including(commands: [[:set]], duration: 0.4, backtrace: an_instance_of(Array)), + a_hash_including(commands: [[:set]], duration: 0.4, backtrace: an_instance_of(Array)) ] ) end diff --git a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb index 09280402e2b..5b5516f100b 100644 --- a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb +++ b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb @@ -47,11 +47,22 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh let(:instrumentation_class) { Gitlab::Redis::SharedState.instrumentation_class } it 'counts successful requests' do - expect(instrumentation_class).to receive(:instance_count_request).and_call_original + expect(instrumentation_class).to receive(:instance_count_request).with(1).and_call_original Gitlab::Redis::SharedState.with { |redis| redis.call(:get, 'foobar') } end + it 'counts successful pipelined requests' do + expect(instrumentation_class).to receive(:instance_count_request).with(2).and_call_original + + Gitlab::Redis::SharedState.with do |redis| + redis.pipelined do |pipeline| + pipeline.call(:get, 'foobar') + pipeline.call(:get, 'foobarbaz') + end + end + end + it 'counts exceptions' do expect(instrumentation_class).to receive(:instance_count_exception) .with(instance_of(Redis::CommandError)).and_call_original @@ -84,6 +95,20 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh Gitlab::Redis::SharedState.with { |redis| redis.call(*command) } end end + + context 'with pipelined commands' do + it 'measures requests that do not have blocking commands' do + expect(instrumentation_class).to receive(:instance_observe_duration).twice.with(a_value > 0) + .and_call_original + + Gitlab::Redis::SharedState.with do |redis| + redis.pipelined do |pipeline| + pipeline.call(:get, 'foobar') + pipeline.call(:get, 'foobarbaz') + end + end + end + end end describe 'commands not in the apdex' do @@ -109,6 +134,19 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh end end end + + context 'with pipelined commands' do + it 'skips requests that have blocking commands', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/373026' do + expect(instrumentation_class).not_to receive(:instance_observe_duration) + + Gitlab::Redis::SharedState.with do |redis| + redis.pipelined do |pipeline| + pipeline.call(:get, 'foo') + pipeline.call(:brpop, 'foobar', '0.01') + end + end + end + end end end end diff --git a/spec/lib/gitlab/instrumentation/redis_spec.rb b/spec/lib/gitlab/instrumentation/redis_spec.rb index 900a079cdd2..c01d06c97b0 100644 --- a/spec/lib/gitlab/instrumentation/redis_spec.rb +++ b/spec/lib/gitlab/instrumentation/redis_spec.rb @@ -71,14 +71,10 @@ RSpec.describe Gitlab::Instrumentation::Redis do stub_storages(:detail_store, [details_row]) - expect(described_class.detail_store) - .to contain_exactly(details_row.merge(storage: 'ActionCable'), - details_row.merge(storage: 'Cache'), - details_row.merge(storage: 'Queues'), - details_row.merge(storage: 'SharedState'), - details_row.merge(storage: 'TraceChunks'), - details_row.merge(storage: 'RateLimiting'), - details_row.merge(storage: 'Sessions')) + expected_detail_stores = Gitlab::Redis::ALL_CLASSES.map(&:store_name) + .map { |store_name| details_row.merge(storage: store_name) } + expected_detail_stores << details_row.merge(storage: 'ActionCable') + expect(described_class.detail_store).to contain_exactly(*expected_detail_stores) end end end diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index 4fa9079144d..d5ff39767c4 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -140,13 +140,13 @@ RSpec.describe Gitlab::InstrumentationHelper do subject expect(payload).to include(db_replica_count: 0, - db_replica_cached_count: 0, - db_primary_count: 0, - db_primary_cached_count: 0, - db_primary_wal_count: 0, - db_replica_wal_count: 0, - db_primary_wal_cached_count: 0, - db_replica_wal_cached_count: 0) + db_replica_cached_count: 0, + db_primary_count: 0, + db_primary_cached_count: 0, + db_primary_wal_count: 0, + db_replica_wal_count: 0, + db_primary_wal_cached_count: 0, + db_replica_wal_cached_count: 0) end context 'when replica caught up search was made' do diff --git a/spec/lib/gitlab/internal_post_receive/response_spec.rb b/spec/lib/gitlab/internal_post_receive/response_spec.rb index 135596c2de3..23ea5191486 100644 --- a/spec/lib/gitlab/internal_post_receive/response_spec.rb +++ b/spec/lib/gitlab/internal_post_receive/response_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::InternalPostReceive::Response do subject { described_class.new } diff --git a/spec/lib/gitlab/jira/middleware_spec.rb b/spec/lib/gitlab/jira/middleware_spec.rb index e7a79e40ac5..09cf67d0657 100644 --- a/spec/lib/gitlab/jira/middleware_spec.rb +++ b/spec/lib/gitlab/jira/middleware_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Jira::Middleware do let(:app) { double(:app) } @@ -24,7 +24,7 @@ RSpec.describe Gitlab::Jira::Middleware do describe '#call' do it 'adjusts HTTP_AUTHORIZATION env when request from Jira DVCS user agent' do expect(app).to receive(:call).with({ 'HTTP_USER_AGENT' => jira_user_agent, - 'HTTP_AUTHORIZATION' => 'Bearer hash-123' }) + 'HTTP_AUTHORIZATION' => 'Bearer hash-123' }) middleware.call('HTTP_USER_AGENT' => jira_user_agent, 'HTTP_AUTHORIZATION' => 'token hash-123') end diff --git a/spec/lib/gitlab/jira_import/metadata_collector_spec.rb b/spec/lib/gitlab/jira_import/metadata_collector_spec.rb index 51751c7b75f..d8e31d0ae22 100644 --- a/spec/lib/gitlab/jira_import/metadata_collector_spec.rb +++ b/spec/lib/gitlab/jira_import/metadata_collector_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::JiraImport::MetadataCollector do describe '#execute' do diff --git a/spec/lib/gitlab/jira_import_spec.rb b/spec/lib/gitlab/jira_import_spec.rb index 972b0ab6ed1..c0c1a28b9ff 100644 --- a/spec/lib/gitlab/jira_import_spec.rb +++ b/spec/lib/gitlab/jira_import_spec.rb @@ -106,12 +106,6 @@ RSpec.describe Gitlab::JiraImport do end end - describe '.jira_issue_cache_key' do - it 'returns cache key for Jira issue imported to given project' do - expect(described_class.jira_item_cache_key(project_id, 'DEMO-123', :issues)).to eq("jira-import/items-mapper/#{project_id}/issues/DEMO-123") - end - end - describe '.already_imported_cache_key' do it 'returns cache key for already imported items' do expect(described_class.already_imported_cache_key(:issues, project_id)).to eq("jira-importer/already-imported/#{project_id}/issues") diff --git a/spec/lib/gitlab/kubernetes/helm/v2/certificate_spec.rb b/spec/lib/gitlab/kubernetes/helm/v2/certificate_spec.rb index a3f0fd9eb9b..698b88c9fa1 100644 --- a/spec/lib/gitlab/kubernetes/helm/v2/certificate_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/v2/certificate_spec.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Kubernetes::Helm::V2::Certificate do describe '.generate_root' do diff --git a/spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb b/spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb index 508808be1be..549fd862d2d 100644 --- a/spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb +++ b/spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Kubernetes::Kubeconfig::Entry::Cluster do describe '#to_h' do diff --git a/spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb b/spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb index 43d4c46fda1..4734111a8ec 100644 --- a/spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb +++ b/spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Kubernetes::Kubeconfig::Entry::Context do describe '#to_h' do diff --git a/spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb b/spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb index 3d6acc80823..9eb6ddcf30c 100644 --- a/spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb +++ b/spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Kubernetes::Kubeconfig::Entry::User do describe '#to_h' do diff --git a/spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb b/spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb index 7d1f1aea291..869bba22a01 100644 --- a/spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb +++ b/spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Kubernetes::Kubeconfig::Template do let(:template) { described_class.new } diff --git a/spec/lib/gitlab/lazy_spec.rb b/spec/lib/gitlab/lazy_spec.rb index 3e929cf200a..92907081867 100644 --- a/spec/lib/gitlab/lazy_spec.rb +++ b/spec/lib/gitlab/lazy_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Lazy do let(:dummy) { double(:dummy) } diff --git a/spec/lib/gitlab/legacy_github_import/client_spec.rb b/spec/lib/gitlab/legacy_github_import/client_spec.rb index 83ba5858d81..08679b7e9f1 100644 --- a/spec/lib/gitlab/legacy_github_import/client_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/client_spec.rb @@ -98,6 +98,30 @@ RSpec.describe Gitlab::LegacyGithubImport::Client do end end + describe '#repository' do + it 'returns repository data as a hash' do + stub_request(:get, 'https://api.github.com/rate_limit') + .to_return(status: 200, headers: { 'X-RateLimit-Limit' => 5000, 'X-RateLimit-Remaining' => 5000 }) + + stub_request(:get, 'https://api.github.com/repositories/1') + .to_return(status: 200, body: { id: 1 }.to_json, headers: { 'Content-Type' => 'application/json' }) + + expect(client.repository(1)).to eq({ id: 1 }) + end + end + + describe '#repos' do + it 'returns the user\'s repositories as a hash' do + stub_request(:get, 'https://api.github.com/rate_limit') + .to_return(status: 200, headers: { 'X-RateLimit-Limit' => 5000, 'X-RateLimit-Remaining' => 5000 }) + + stub_request(:get, 'https://api.github.com/user/repos') + .to_return(status: 200, body: [{ id: 1 }, { id: 2 }].to_json, headers: { 'Content-Type' => 'application/json' }) + + expect(client.repos).to match_array([{ id: 1 }, { id: 2 }]) + end + end + context 'github rate limit' do it 'does not raise error when rate limit is disabled' do stub_request(:get, /api.github.com/) diff --git a/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb index a5d2e00890b..a285a5820a2 100644 --- a/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::LegacyGithubImport::IssuableFormatter do let(:raw_data) do diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb index 68f1c214cef..17ecd183ac9 100644 --- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb @@ -7,15 +7,15 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do let(:namespace) { create(:group) } let(:repo) do - ActiveSupport::InheritableOptions.new( + { login: 'vim', name: 'vim', full_name: 'asd/vim', clone_url: 'https://gitlab.com/asd/vim.git' - ) + } end - subject(:service) { described_class.new(repo, repo.name, namespace, user, github_access_token: 'asdffg') } + subject(:service) { described_class.new(repo, repo[:name], namespace, user, github_access_token: 'asdffg') } before do namespace.add_owner(user) @@ -40,7 +40,7 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do context 'when GitHub project is private' do it 'sets project visibility to private' do - repo.private = true + repo[:private] = true project = service.execute @@ -50,17 +50,19 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do context 'when GitHub project is public' do it 'sets project visibility to namespace visibility level' do - repo.private = false + repo[:private] = false + project = service.execute expect(project.visibility_level).to eq(namespace.visibility_level) end context 'when importing into a user namespace' do - subject(:service) { described_class.new(repo, repo.name, user.namespace, user, github_access_token: 'asdffg') } + subject(:service) { described_class.new(repo, repo[:name], user.namespace, user, github_access_token: 'asdffg') } it 'sets project visibility to user namespace visibility level' do - repo.private = false + repo[:private] = false + project = service.execute expect(project.visibility_level).to eq(user.namespace.visibility_level) @@ -76,7 +78,7 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do end it 'sets project visibility to the default project visibility' do - repo.private = true + repo[:private] = true project = service.execute @@ -91,7 +93,7 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do end it 'sets project visibility to the default project visibility' do - repo.private = false + repo[:private] = false project = service.execute @@ -102,7 +104,7 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do context 'when GitHub project has wiki' do it 'does not create the wiki repository' do - allow(repo).to receive(:has_wiki?).and_return(true) + repo[:has_wiki] = true project = service.execute @@ -112,7 +114,7 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do context 'when GitHub project does not have wiki' do it 'creates the wiki repository' do - allow(repo).to receive(:has_wiki?).and_return(false) + repo[:has_wiki] = false project = service.execute diff --git a/spec/lib/gitlab/loop_helpers_spec.rb b/spec/lib/gitlab/loop_helpers_spec.rb index 0535cb6068c..bb328e3dcce 100644 --- a/spec/lib/gitlab/loop_helpers_spec.rb +++ b/spec/lib/gitlab/loop_helpers_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::LoopHelpers do let(:class_instance) { (Class.new { include ::Gitlab::LoopHelpers }).new } diff --git a/spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb b/spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb index a2286415e96..4b9ea1c15a9 100644 --- a/spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb +++ b/spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb @@ -20,18 +20,43 @@ RSpec.describe Gitlab::Mailgun::WebhookProcessors::FailureLogger do context 'on permanent failure' do let(:processor) { described_class.new(base_payload.merge({ 'severity' => 'permanent' })) } - it 'logs the failure immediately' do - expect(Gitlab::ErrorTracking::Logger).to receive(:error).with( - event: 'email_delivery_failure', - mailgun_event_id: base_payload['id'], - recipient: base_payload['recipient'], - failure_type: 'permanent', - failure_reason: base_payload['reason'], - failure_code: base_payload['delivery-status']['code'], - failure_message: base_payload['delivery-status']['message'] - ) + before do + allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits) + .and_return(permanent_email_failure: { threshold: 1, interval: 1.minute }) + end - processor.execute + context 'when threshold is not exceeded' do + it 'increments counter but does not log the failure' do + expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with( + :permanent_email_failure, scope: 'recipient@gitlab.com' + ).and_call_original + expect(Gitlab::ErrorTracking::Logger).not_to receive(:error) + + processor.execute + end + end + + context 'when threshold is exceeded' do + before do + processor.execute + end + + it 'increments counter and logs the failure' do + expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with( + :permanent_email_failure, scope: 'recipient@gitlab.com' + ).and_call_original + expect(Gitlab::ErrorTracking::Logger).to receive(:error).with( + event: 'email_delivery_failure', + mailgun_event_id: base_payload['id'], + recipient: base_payload['recipient'], + failure_type: 'permanent', + failure_reason: base_payload['reason'], + failure_code: base_payload['delivery-status']['code'], + failure_message: base_payload['delivery-status']['message'] + ) + + processor.execute + end end end diff --git a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb index 81910773dfa..57f2b1cfd96 100644 --- a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb +++ b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb @@ -174,8 +174,8 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do expect(thing).to receive(:update_columns) .with({ "title_html" => updated_html, - "description_html" => "", - "cached_markdown_version" => cache_version }) + "description_html" => "", + "cached_markdown_version" => cache_version }) thing.refresh_markdown_cache! end diff --git a/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb index b5d458f15fc..8e75009099d 100644 --- a/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb +++ b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb @@ -62,7 +62,13 @@ RSpec.describe Gitlab::MarkdownCache::Redis::Extension, :clean_gitlab_redis_cach it 'does not preload the markdown twice' do expect(Gitlab::MarkdownCache::Redis::Store).to receive(:bulk_read).and_call_original - expect(Gitlab::Redis::Cache).to receive(:with).twice.and_call_original + Gitlab::Redis::Cache.with do |redis| + expect(redis).to receive(:pipelined).and_call_original + + expect_next_instance_of(Redis::PipelinedConnection) do |pipeline| + expect(pipeline).to receive(:mapped_hmget).once.and_call_original + end + end klass.preload_markdown_cache!([thing]) diff --git a/spec/lib/gitlab/markup_helper_spec.rb b/spec/lib/gitlab/markup_helper_spec.rb index bf5415ba1d7..2bffd029568 100644 --- a/spec/lib/gitlab/markup_helper_spec.rb +++ b/spec/lib/gitlab/markup_helper_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::MarkupHelper do describe '#markup?' do diff --git a/spec/lib/gitlab/memory/jemalloc_spec.rb b/spec/lib/gitlab/memory/jemalloc_spec.rb index 482ac6e5802..414d6017534 100644 --- a/spec/lib/gitlab/memory/jemalloc_spec.rb +++ b/spec/lib/gitlab/memory/jemalloc_spec.rb @@ -1,12 +1,15 @@ # frozen_string_literal: true require 'fast_spec_helper' +require 'tmpdir' RSpec.describe Gitlab::Memory::Jemalloc do let(:outdir) { Dir.mktmpdir } + let(:tmp_outdir) { Dir.mktmpdir } after do FileUtils.rm_f(outdir) + FileUtils.rm_f(tmp_outdir) end context 'when jemalloc is loaded' do @@ -28,7 +31,7 @@ RSpec.describe Gitlab::Memory::Jemalloc do describe '.dump_stats' do it 'writes stats JSON file' do - file_path = described_class.dump_stats(path: outdir, format: format) + file_path = described_class.dump_stats(path: outdir, tmp_dir: tmp_outdir, format: format) file = Dir.entries(outdir).find { |e| e.match(/jemalloc_stats\.#{$$}\.\d+\.json$/) } expect(file).not_to be_nil @@ -55,7 +58,8 @@ RSpec.describe Gitlab::Memory::Jemalloc do describe '.dump_stats' do shared_examples 'writes stats text file' do |filename_label, filename_pattern| it do - described_class.dump_stats(path: outdir, format: format, filename_label: filename_label) + described_class.dump_stats( + path: outdir, tmp_dir: tmp_outdir, format: format, filename_label: filename_label) file = Dir.entries(outdir).find { |e| e.match(filename_pattern) } expect(file).not_to be_nil @@ -87,7 +91,7 @@ RSpec.describe Gitlab::Memory::Jemalloc do describe '.dump_stats' do it 'raises an error' do expect do - described_class.dump_stats(path: outdir, format: format) + described_class.dump_stats(path: outdir, tmp_dir: tmp_outdir, format: format) end.to raise_error(/format must be one of/) end end @@ -109,7 +113,7 @@ RSpec.describe Gitlab::Memory::Jemalloc do it 'does nothing' do stub_env('LD_PRELOAD', nil) - described_class.dump_stats(path: outdir) + described_class.dump_stats(path: outdir, tmp_dir: tmp_outdir) expect(Dir.empty?(outdir)).to be(true) end diff --git a/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb b/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb index 53fae48776b..b327a40bc2c 100644 --- a/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb +++ b/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb @@ -3,14 +3,19 @@ require 'spec_helper' RSpec.describe Gitlab::Memory::Reports::JemallocStats do - let(:reports_dir) { '/empty-dir' } - let(:jemalloc_stats) { described_class.new(reports_path: reports_dir) } + let_it_be(:outdir) { Dir.mktmpdir } + + let(:jemalloc_stats) { described_class.new(reports_path: outdir) } + + after do + FileUtils.rm_f(outdir) + end describe '.run' do context 'when :report_jemalloc_stats ops FF is enabled' do let(:worker_id) { 'puma_1' } let(:report_name) { 'report.json' } - let(:report_path) { File.join(reports_dir, report_name) } + let(:report_path) { File.join(outdir, report_name) } before do allow(Prometheus::PidProvider).to receive(:worker_id).and_return(worker_id) @@ -18,14 +23,16 @@ RSpec.describe Gitlab::Memory::Reports::JemallocStats do it 'invokes Jemalloc.dump_stats and returns file path' do expect(Gitlab::Memory::Jemalloc) - .to receive(:dump_stats).with(path: reports_dir, filename_label: worker_id).and_return(report_path) + .to receive(:dump_stats) + .with(path: outdir, + tmp_dir: File.join(outdir, '/tmp'), + filename_label: worker_id) + .and_return(report_path) expect(jemalloc_stats.run).to eq(report_path) end describe 'reports cleanup' do - let_it_be(:outdir) { Dir.mktmpdir } - let(:jemalloc_stats) { described_class.new(reports_path: outdir) } before do @@ -33,10 +40,6 @@ RSpec.describe Gitlab::Memory::Reports::JemallocStats do allow(Gitlab::Memory::Jemalloc).to receive(:dump_stats) end - after do - FileUtils.rm_f(outdir) - end - context 'when number of reports exceeds `max_reports_stored`' do let_it_be(:reports) do now = Time.current diff --git a/spec/lib/gitlab/memory/watchdog_spec.rb b/spec/lib/gitlab/memory/watchdog_spec.rb index 010f6884df3..beb49660022 100644 --- a/spec/lib/gitlab/memory/watchdog_spec.rb +++ b/spec/lib/gitlab/memory/watchdog_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'spec_helper' +require_relative '../../../../lib/gitlab/cluster/lifecycle_events' RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do context 'watchdog' do @@ -8,23 +9,31 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do let(:handler) { instance_double(described_class::NullHandler) } let(:heap_frag_limit_gauge) { instance_double(::Prometheus::Client::Gauge) } - let(:heap_frag_violations_counter) { instance_double(::Prometheus::Client::Counter) } - let(:heap_frag_violations_handled_counter) { instance_double(::Prometheus::Client::Counter) } + let(:violations_counter) { instance_double(::Prometheus::Client::Counter) } + let(:violations_handled_counter) { instance_double(::Prometheus::Client::Counter) } let(:sleep_time) { 0.1 } let(:max_heap_fragmentation) { 0.2 } + let(:max_mem_growth) { 2 } + + # Defaults that will not trigger any events. + let(:fragmentation) { 0 } + let(:worker_memory) { 0 } + let(:primary_memory) { 0 } + let(:max_strikes) { 0 } # Tests should set this to control the number of loop iterations in `call`. let(:watchdog_iterations) { 1 } subject(:watchdog) do described_class.new(handler: handler, logger: logger, sleep_time_seconds: sleep_time, - max_strikes: max_strikes, max_heap_fragmentation: max_heap_fragmentation).tap do |instance| + max_strikes: max_strikes, max_mem_growth: max_mem_growth, + max_heap_fragmentation: max_heap_fragmentation).tap do |instance| # We need to defuse `sleep` and stop the internal loop after N iterations. iterations = 0 - expect(instance).to receive(:sleep) do - instance.stop if (iterations += 1) >= watchdog_iterations - end.at_most(watchdog_iterations) + allow(instance).to receive(:sleep) do + instance.stop if (iterations += 1) > watchdog_iterations + end end end @@ -33,34 +42,35 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do .with(:gitlab_memwd_heap_frag_limit, anything) .and_return(heap_frag_limit_gauge) allow(Gitlab::Metrics).to receive(:counter) - .with(:gitlab_memwd_heap_frag_violations_total, anything, anything) - .and_return(heap_frag_violations_counter) + .with(:gitlab_memwd_violations_total, anything, anything) + .and_return(violations_counter) allow(Gitlab::Metrics).to receive(:counter) - .with(:gitlab_memwd_heap_frag_violations_handled_total, anything, anything) - .and_return(heap_frag_violations_handled_counter) + .with(:gitlab_memwd_violations_handled_total, anything, anything) + .and_return(violations_handled_counter) allow(heap_frag_limit_gauge).to receive(:set) - allow(heap_frag_violations_counter).to receive(:increment) - allow(heap_frag_violations_handled_counter).to receive(:increment) + allow(violations_counter).to receive(:increment) + allow(violations_handled_counter).to receive(:increment) end before do stub_prometheus_metrics - allow(handler).to receive(:on_high_heap_fragmentation).and_return(true) + allow(handler).to receive(:call).and_return(true) allow(logger).to receive(:warn) allow(logger).to receive(:info) allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(fragmentation) + allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return({ uss: worker_memory }) + allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).with( + pid: Gitlab::Cluster::PRIMARY_PID + ).and_return({ uss: primary_memory }) allow(::Prometheus::PidProvider).to receive(:worker_id).and_return('worker_1') end context 'when created' do - let(:fragmentation) { 0 } - let(:max_strikes) { 0 } - it 'sets the heap fragmentation limit gauge' do expect(heap_frag_limit_gauge).to receive(:set).with({}, max_heap_fragmentation) @@ -71,7 +81,8 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do it 'initializes with defaults' do watchdog = described_class.new(handler: handler, logger: logger) - expect(watchdog.max_heap_fragmentation).to eq(described_class::DEFAULT_HEAP_FRAG_THRESHOLD) + expect(watchdog.max_heap_fragmentation).to eq(described_class::DEFAULT_MAX_HEAP_FRAG) + expect(watchdog.max_mem_growth).to eq(described_class::DEFAULT_MAX_MEM_GROWTH) expect(watchdog.max_strikes).to eq(described_class::DEFAULT_MAX_STRIKES) expect(watchdog.sleep_time_seconds).to eq(described_class::DEFAULT_SLEEP_TIME_SECONDS) end @@ -82,6 +93,7 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do stub_env('GITLAB_MEMWD_MAX_HEAP_FRAG', 1) stub_env('GITLAB_MEMWD_MAX_STRIKES', 2) stub_env('GITLAB_MEMWD_SLEEP_TIME_SEC', 3) + stub_env('GITLAB_MEMWD_MAX_MEM_GROWTH', 4) end it 'initializes with these settings' do @@ -90,30 +102,17 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do expect(watchdog.max_heap_fragmentation).to eq(1) expect(watchdog.max_strikes).to eq(2) expect(watchdog.sleep_time_seconds).to eq(3) + expect(watchdog.max_mem_growth).to eq(4) end end end - context 'when process does not exceed heap fragmentation threshold' do - let(:fragmentation) { max_heap_fragmentation - 0.1 } - let(:max_strikes) { 0 } # To rule out that we were granting too many strikes. - - it 'does not signal the handler' do - expect(handler).not_to receive(:on_high_heap_fragmentation) - - watchdog.call - end - end - - context 'when process exceeds heap fragmentation threshold permanently' do - let(:fragmentation) { max_heap_fragmentation + 0.1 } - let(:max_strikes) { 3 } - + shared_examples 'has strikes left' do |stat| context 'when process has not exceeded allowed number of strikes' do let(:watchdog_iterations) { max_strikes } it 'does not signal the handler' do - expect(handler).not_to receive(:on_high_heap_fragmentation) + expect(handler).not_to receive(:call) watchdog.call end @@ -125,119 +124,228 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do end it 'increments the violations counter' do - expect(heap_frag_violations_counter).to receive(:increment).exactly(watchdog_iterations) + expect(violations_counter).to receive(:increment).with(reason: stat).exactly(watchdog_iterations) watchdog.call end it 'does not increment violations handled counter' do - expect(heap_frag_violations_handled_counter).not_to receive(:increment) + expect(violations_handled_counter).not_to receive(:increment) watchdog.call end end + end + + shared_examples 'no strikes left' do |stat| + it 'signals the handler and resets strike counter' do + expect(handler).to receive(:call).and_return(true) + + watchdog.call + + expect(watchdog.strikes(stat.to_sym)).to eq(0) + end + + it 'increments both the violations and violations handled counters' do + expect(violations_counter).to receive(:increment).with(reason: stat).exactly(watchdog_iterations) + expect(violations_handled_counter).to receive(:increment).with(reason: stat) + + watchdog.call + end - context 'when process exceeds the allowed number of strikes' do - let(:watchdog_iterations) { max_strikes + 1 } + context 'when enforce_memory_watchdog ops toggle is off' do + before do + stub_feature_flags(enforce_memory_watchdog: false) + end - it 'signals the handler and resets strike counter' do - expect(handler).to receive(:on_high_heap_fragmentation).and_return(true) + it 'always uses the NullHandler' do + expect(handler).not_to receive(:call) + expect(described_class::NullHandler.instance).to receive(:call).and_return(true) watchdog.call + end + end - expect(watchdog.strikes).to eq(0) + context 'when handler result is true' do + it 'considers the event handled and stops itself' do + expect(handler).to receive(:call).once.and_return(true) + expect(logger).to receive(:info).with(hash_including(message: 'stopped')) + + watchdog.call end + end - it 'logs the event' do - expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024) - expect(logger).to receive(:warn).with({ - message: 'heap fragmentation limit exceeded', - pid: Process.pid, - worker_id: 'worker_1', - memwd_handler_class: 'RSpec::Mocks::InstanceVerifyingDouble', - memwd_sleep_time_s: sleep_time, - memwd_max_heap_frag: max_heap_fragmentation, - memwd_cur_heap_frag: fragmentation, - memwd_max_strikes: max_strikes, - memwd_cur_strikes: max_strikes + 1, - memwd_rss_bytes: 1024 - }) + context 'when handler result is false' do + let(:max_strikes) { 0 } # to make sure the handler fires each iteration + let(:watchdog_iterations) { 3 } + + it 'keeps running' do + expect(violations_counter).to receive(:increment).exactly(watchdog_iterations) + expect(violations_handled_counter).to receive(:increment).exactly(watchdog_iterations) + # Return true the third time to terminate the daemon. + expect(handler).to receive(:call).and_return(false, false, true) watchdog.call end + end + end + + context 'when monitoring memory growth' do + let(:primary_memory) { 2048 } - it 'increments both the violations and violations handled counters' do - expect(heap_frag_violations_counter).to receive(:increment).exactly(watchdog_iterations) - expect(heap_frag_violations_handled_counter).to receive(:increment) + context 'when process does not exceed threshold' do + let(:worker_memory) { max_mem_growth * primary_memory - 1 } + + it 'does not signal the handler' do + expect(handler).not_to receive(:call) watchdog.call end + end - context 'when enforce_memory_watchdog ops toggle is off' do - before do - stub_feature_flags(enforce_memory_watchdog: false) - end + context 'when process exceeds threshold permanently' do + let(:worker_memory) { max_mem_growth * primary_memory + 1 } + let(:max_strikes) { 3 } + + it_behaves_like 'has strikes left', 'mem_growth' + + context 'when process exceeds the allowed number of strikes' do + let(:watchdog_iterations) { max_strikes + 1 } - it 'always uses the NullHandler' do - expect(handler).not_to receive(:on_high_heap_fragmentation) - expect(described_class::NullHandler.instance).to( - receive(:on_high_heap_fragmentation).with(fragmentation).and_return(true) - ) + it_behaves_like 'no strikes left', 'mem_growth' + + it 'only reads reference memory once' do + expect(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss) + .with(pid: Gitlab::Cluster::PRIMARY_PID) + .once watchdog.call end - end - context 'when handler result is true' do - it 'considers the event handled and stops itself' do - expect(handler).to receive(:on_high_heap_fragmentation).once.and_return(true) - expect(logger).to receive(:info).with(hash_including(message: 'stopped')) + it 'logs the event' do + expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024) + expect(logger).to receive(:warn).with({ + message: 'memory limit exceeded', + pid: Process.pid, + worker_id: 'worker_1', + memwd_handler_class: 'RSpec::Mocks::InstanceVerifyingDouble', + memwd_sleep_time_s: sleep_time, + memwd_max_uss_bytes: max_mem_growth * primary_memory, + memwd_ref_uss_bytes: primary_memory, + memwd_uss_bytes: worker_memory, + memwd_rss_bytes: 1024, + memwd_max_strikes: max_strikes, + memwd_cur_strikes: max_strikes + 1 + }) watchdog.call end end + end + + context 'when process exceeds threshold temporarily' do + let(:worker_memory) { max_mem_growth * primary_memory } + let(:max_strikes) { 1 } + let(:watchdog_iterations) { 4 } + + before do + allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return( + { uss: worker_memory - 0.1 }, + { uss: worker_memory + 0.2 }, + { uss: worker_memory - 0.1 }, + { uss: worker_memory + 0.1 } + ) + allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).with( + pid: Gitlab::Cluster::PRIMARY_PID + ).and_return({ uss: primary_memory }) + end + + it 'does not signal the handler' do + expect(handler).not_to receive(:call) + + watchdog.call + end + end + end + + context 'when monitoring heap fragmentation' do + context 'when process does not exceed threshold' do + let(:fragmentation) { max_heap_fragmentation - 0.1 } + + it 'does not signal the handler' do + expect(handler).not_to receive(:call) + + watchdog.call + end + end + + context 'when process exceeds threshold permanently' do + let(:fragmentation) { max_heap_fragmentation + 0.1 } + let(:max_strikes) { 3 } - context 'when handler result is false' do - let(:max_strikes) { 0 } # to make sure the handler fires each iteration - let(:watchdog_iterations) { 3 } + it_behaves_like 'has strikes left', 'heap_frag' - it 'keeps running' do - expect(heap_frag_violations_counter).to receive(:increment).exactly(watchdog_iterations) - expect(heap_frag_violations_handled_counter).to receive(:increment).exactly(watchdog_iterations) - # Return true the third time to terminate the daemon. - expect(handler).to receive(:on_high_heap_fragmentation).and_return(false, false, true) + context 'when process exceeds the allowed number of strikes' do + let(:watchdog_iterations) { max_strikes + 1 } + + it_behaves_like 'no strikes left', 'heap_frag' + + it 'logs the event' do + expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024) + expect(logger).to receive(:warn).with({ + message: 'heap fragmentation limit exceeded', + pid: Process.pid, + worker_id: 'worker_1', + memwd_handler_class: 'RSpec::Mocks::InstanceVerifyingDouble', + memwd_sleep_time_s: sleep_time, + memwd_max_heap_frag: max_heap_fragmentation, + memwd_cur_heap_frag: fragmentation, + memwd_max_strikes: max_strikes, + memwd_cur_strikes: max_strikes + 1, + memwd_rss_bytes: 1024 + }) watchdog.call end end end - end - context 'when process exceeds heap fragmentation threshold temporarily' do - let(:fragmentation) { max_heap_fragmentation } - let(:max_strikes) { 1 } - let(:watchdog_iterations) { 4 } + context 'when process exceeds threshold temporarily' do + let(:fragmentation) { max_heap_fragmentation } + let(:max_strikes) { 1 } + let(:watchdog_iterations) { 4 } - before do - allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return( - fragmentation - 0.1, - fragmentation + 0.2, - fragmentation - 0.1, - fragmentation + 0.1 - ) + before do + allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return( + fragmentation - 0.1, + fragmentation + 0.2, + fragmentation - 0.1, + fragmentation + 0.1 + ) + end + + it 'does not signal the handler' do + expect(handler).not_to receive(:call) + + watchdog.call + end end + end - it 'does not signal the handler' do - expect(handler).not_to receive(:on_high_heap_fragmentation) + context 'when both memory fragmentation and growth exceed thresholds' do + let(:fragmentation) { max_heap_fragmentation + 0.1 } + let(:primary_memory) { 2048 } + let(:worker_memory) { max_mem_growth * primary_memory + 1 } + let(:watchdog_iterations) { max_strikes + 1 } + + it 'only calls the handler once' do + expect(handler).to receive(:call).once.and_return(true) watchdog.call end end context 'when gitlab_memory_watchdog ops toggle is off' do - let(:fragmentation) { 0 } - let(:max_strikes) { 0 } - before do stub_feature_flags(gitlab_memory_watchdog: false) end @@ -247,6 +355,12 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do watchdog.call end + + it 'does not monitor memory growth' do + expect(Gitlab::Metrics::System).not_to receive(:memory_usage_uss_pss) + + watchdog.call + end end end @@ -254,9 +368,9 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do context 'NullHandler' do subject(:handler) { described_class::NullHandler.instance } - describe '#on_high_heap_fragmentation' do + describe '#call' do it 'does nothing' do - expect(handler.on_high_heap_fragmentation(1.0)).to be(false) + expect(handler.call).to be(false) end end end @@ -264,11 +378,11 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do context 'TermProcessHandler' do subject(:handler) { described_class::TermProcessHandler.new(42) } - describe '#on_high_heap_fragmentation' do + describe '#call' do it 'sends SIGTERM to the current process' do expect(Process).to receive(:kill).with(:TERM, 42) - expect(handler.on_high_heap_fragmentation(1.0)).to be(true) + expect(handler.call).to be(true) end end end @@ -286,12 +400,12 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do stub_const('::Puma::Cluster::WorkerHandle', puma_worker_handle_class) end - describe '#on_high_heap_fragmentation' do + describe '#call' do it 'invokes orderly termination via Puma API' do expect(puma_worker_handle_class).to receive(:new).and_return(puma_worker_handle) expect(puma_worker_handle).to receive(:term) - expect(handler.on_high_heap_fragmentation(1.0)).to be(true) + expect(handler.call).to be(true) end end end diff --git a/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb index ed11f8ea6bb..0e8b598730c 100644 --- a/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb +++ b/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::MergeRequests::Mergeability::ResultsStore do subject(:results_store) { described_class.new(merge_request: merge_request, interface: interface) } diff --git a/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb b/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb index 1f306753c39..b8556829a59 100644 --- a/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Metrics::Dashboard::Defaults do it { is_expected.to be_const_defined(:DEFAULT_PANEL_TYPE) } diff --git a/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb b/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb index c15e717b126..bc6cd383758 100644 --- a/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb @@ -24,13 +24,13 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do context 'with existing metrics' do let(:existing_metric_attributes) do { - project: project, - identifier: 'metric_b', - title: 'overwrite', - y_label: 'overwrite', - query: 'overwrite', - unit: 'overwrite', - legend: 'overwrite', + project: project, + identifier: 'metric_b', + title: 'overwrite', + y_label: 'overwrite', + query: 'overwrite', + unit: 'overwrite', + legend: 'overwrite', dashboard_path: dashboard_path } end @@ -43,11 +43,11 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do subject.execute expect(existing_metric.reload.attributes.with_indifferent_access).to include({ - title: 'Super Chart B', + title: 'Super Chart B', y_label: 'y_label', - query: 'query', - unit: 'unit', - legend: 'Legend Label' + query: 'query', + unit: 'unit', + legend: 'Legend Label' }) end @@ -69,11 +69,11 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do subject.execute expect(existing_metric.reload.attributes.with_indifferent_access).to include({ - title: 'Super Chart B', + title: 'Super Chart B', y_label: 'y_label', - query: 'query', - unit: 'unit', - legend: 'Legend Label' + query: 'query', + unit: 'unit', + legend: 'Legend Label' }) end diff --git a/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb index fdbba6c31b5..a50c2a506cb 100644 --- a/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb @@ -17,11 +17,11 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do let(:error_hash) do { - 'data' => 'property_name', + 'data' => 'property_name', 'data_pointer' => pointer, - 'type' => type, - 'schema' => 'schema', - 'details' => details + 'type' => type, + 'schema' => 'schema', + 'details' => details } end @@ -72,10 +72,10 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do let(:type) { 'pattern' } let(:error_hash) do { - 'data' => 'property_name', + 'data' => 'property_name', 'data_pointer' => pointer, - 'type' => type, - 'schema' => { 'pattern' => 'aa.*' } + 'type' => type, + 'schema' => { 'pattern' => 'aa.*' } } end @@ -86,10 +86,10 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do let(:type) { 'format' } let(:error_hash) do { - 'data' => 'property_name', + 'data' => 'property_name', 'data_pointer' => pointer, - 'type' => type, - 'schema' => { 'format' => 'date-time' } + 'type' => type, + 'schema' => { 'format' => 'date-time' } } end @@ -100,10 +100,10 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do let(:type) { 'const' } let(:error_hash) do { - 'data' => 'property_name', + 'data' => 'property_name', 'data_pointer' => pointer, - 'type' => type, - 'schema' => { 'const' => 'one' } + 'type' => type, + 'schema' => { 'const' => 'one' } } end @@ -114,10 +114,10 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do let(:type) { 'enum' } let(:error_hash) do { - 'data' => 'property_name', + 'data' => 'property_name', 'data_pointer' => pointer, - 'type' => type, - 'schema' => { 'enum' => %w(one two) } + 'type' => type, + 'schema' => { 'enum' => %w(one two) } } end @@ -128,10 +128,10 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do let(:type) { 'unknown' } let(:error_hash) do { - 'data' => 'property_name', + 'data' => 'property_name', 'data_pointer' => pointer, - 'type' => type, - 'schema' => 'schema' + 'type' => type, + 'schema' => 'schema' } end diff --git a/spec/lib/gitlab/metrics/dashboard/validator_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator_spec.rb index eb67ea2b7da..aaa9daf8fee 100644 --- a/spec/lib/gitlab/metrics/dashboard/validator_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/validator_spec.rb @@ -33,9 +33,9 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do context 'with metric identifier present in current dashboard' do before do create(:prometheus_metric, - identifier: 'metric_a1', + identifier: 'metric_a1', dashboard_path: 'test/path.yml', - project: project + project: project ) end @@ -45,9 +45,9 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do context 'with metric identifier present in another dashboard' do before do create(:prometheus_metric, - identifier: 'metric_a1', + identifier: 'metric_a1', dashboard_path: 'some/other/dashboard/path.yml', - project: project + project: project ) end @@ -94,9 +94,9 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do context 'with metric identifier present in current dashboard' do before do create(:prometheus_metric, - identifier: 'metric_a1', + identifier: 'metric_a1', dashboard_path: 'test/path.yml', - project: project + project: project ) end @@ -106,9 +106,9 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do context 'with metric identifier present in another dashboard' do before do create(:prometheus_metric, - identifier: 'metric_a1', + identifier: 'metric_a1', dashboard_path: 'some/other/dashboard/path.yml', - project: project + project: project ) end @@ -166,9 +166,9 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do context 'with metric identifier present in current dashboard' do before do create(:prometheus_metric, - identifier: 'metric_a1', + identifier: 'metric_a1', dashboard_path: 'test/path.yml', - project: project + project: project ) end @@ -178,9 +178,9 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do context 'with metric identifier present in another dashboard' do before do create(:prometheus_metric, - identifier: 'metric_a1', + identifier: 'metric_a1', dashboard_path: 'some/other/dashboard/path.yml', - project: project + project: project ) end diff --git a/spec/lib/gitlab/metrics/delta_spec.rb b/spec/lib/gitlab/metrics/delta_spec.rb index e768da875c2..fdbb5e4ce4d 100644 --- a/spec/lib/gitlab/metrics/delta_spec.rb +++ b/spec/lib/gitlab/metrics/delta_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Metrics::Delta do let(:delta) { described_class.new } diff --git a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb index dc5c7eb2e55..fa50adb4e4f 100644 --- a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb +++ b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb @@ -10,11 +10,12 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do describe 'when exporter is enabled' do before do allow(::WEBrick::HTTPServer).to receive(:new).with( - Port: anything, - BindAddress: anything, - Logger: anything, - AccessLog: anything - ).and_call_original + { + Port: anything, + BindAddress: anything, + Logger: anything, + AccessLog: anything + }).and_call_original allow(settings).to receive(:enabled).and_return(true) allow(settings).to receive(:port).and_return(0) @@ -45,11 +46,12 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do it 'starts server with port and address from settings' do expect(::WEBrick::HTTPServer).to receive(:new).with( - Port: port, - BindAddress: address, - Logger: anything, - AccessLog: anything - ).and_wrap_original do |m, *args| + { + Port: port, + BindAddress: address, + Logger: anything, + AccessLog: anything + }).and_wrap_original do |m, *args| m.call(DoNotListen: true, Logger: args.first[:Logger]) end diff --git a/spec/lib/gitlab/metrics/global_search_slis_spec.rb b/spec/lib/gitlab/metrics/global_search_slis_spec.rb new file mode 100644 index 00000000000..28496eff2fc --- /dev/null +++ b/spec/lib/gitlab/metrics/global_search_slis_spec.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::GlobalSearchSlis do + using RSpec::Parameterized::TableSyntax + + let(:apdex_feature_flag_enabled) { true } + let(:error_rate_feature_flag_enabled) { true } + + before do + stub_feature_flags(global_search_custom_slis: apdex_feature_flag_enabled) + stub_feature_flags(global_search_error_rate_sli: error_rate_feature_flag_enabled) + end + + describe '#initialize_slis!' do + context 'when global_search_custom_slis feature flag is enabled' do + let(:apdex_feature_flag_enabled) { true } + + it 'initializes Apdex SLIs for global_search' do + expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with( + :global_search, + a_kind_of(Array) + ) + + described_class.initialize_slis! + end + end + + context 'when global_search_error_rate_sli feature flag is enabled' do + let(:error_rate_feature_flag_enabled) { true } + + it 'initializes ErrorRate SLIs for global_search' do + expect(Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli).with( + :global_search, + a_kind_of(Array) + ) + + described_class.initialize_slis! + end + end + + context 'when global_search_custom_slis feature flag is disabled' do + let(:apdex_feature_flag_enabled) { false } + + it 'does not initialize the Apdex SLIs for global_search' do + expect(Gitlab::Metrics::Sli::Apdex).not_to receive(:initialize_sli) + + described_class.initialize_slis! + end + end + + context 'when global_search_error_rate_sli feature flag is disabled' do + let(:error_rate_feature_flag_enabled) { false } + + it 'does not initialize the ErrorRate SLIs for global_search' do + expect(Gitlab::Metrics::Sli::ErrorRate).not_to receive(:initialize_sli) + + described_class.initialize_slis! + end + end + end + + describe '#record_apdex' do + context 'when global_search_custom_slis feature flag is enabled' do + let(:apdex_feature_flag_enabled) { true } + + where(:search_type, :code_search, :duration_target) do + 'basic' | false | 7.031 + 'basic' | true | 21.903 + 'advanced' | false | 4.865 + 'advanced' | true | 13.546 + end + + with_them do + before do + allow(::Gitlab::ApplicationContext).to receive(:current_context_attribute).with(:caller_id).and_return('end') + end + + let(:search_scope) { code_search ? 'blobs' : 'issues' } + + it 'increments the global_search SLI as a success if the elapsed time is within the target' do + duration = duration_target - 0.1 + + expect(Gitlab::Metrics::Sli::Apdex[:global_search]).to receive(:increment).with( + labels: { + search_type: search_type, + search_level: 'global', + search_scope: search_scope, + endpoint_id: 'end' + }, + success: true + ) + + described_class.record_apdex( + elapsed: duration, + search_type: search_type, + search_level: 'global', + search_scope: search_scope + ) + end + + it 'increments the global_search SLI as a failure if the elapsed time is not within the target' do + duration = duration_target + 0.1 + + expect(Gitlab::Metrics::Sli::Apdex[:global_search]).to receive(:increment).with( + labels: { + search_type: search_type, + search_level: 'global', + search_scope: search_scope, + endpoint_id: 'end' + }, + success: false + ) + + described_class.record_apdex( + elapsed: duration, + search_type: search_type, + search_level: 'global', + search_scope: search_scope + ) + end + end + end + + context 'when global_search_custom_slis feature flag is disabled' do + let(:apdex_feature_flag_enabled) { false } + + it 'does not call increment on the apdex SLI' do + expect(Gitlab::Metrics::Sli::Apdex[:global_search]).not_to receive(:increment) + + described_class.record_apdex( + elapsed: 1, + search_type: 'basic', + search_level: 'global', + search_scope: 'issues' + ) + end + end + end + + describe '#record_error_rate' do + context 'when global_search_error_rate_sli feature flag is enabled' do + let(:error_rate_feature_flag_enabled) { true } + + it 'calls increment on the error rate SLI' do + expect(Gitlab::Metrics::Sli::ErrorRate[:global_search]).to receive(:increment) + + described_class.record_error_rate( + error: true, + search_type: 'basic', + search_level: 'global', + search_scope: 'issues' + ) + end + end + + context 'when global_search_error_rate_sli feature flag is disabled' do + let(:error_rate_feature_flag_enabled) { false } + + it 'does not call increment on the error rate SLI' do + expect(Gitlab::Metrics::Sli::ErrorRate[:global_search]).not_to receive(:increment) + + described_class.record_error_rate( + error: true, + search_type: 'basic', + search_level: 'global', + search_scope: 'issues' + ) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index ab56f38f0c1..21028d18648 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Metrics::RackMiddleware do let(:app) { double(:app) } diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb index 3396de9b12c..ed78548ef62 100644 --- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb @@ -194,9 +194,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do let(:endpoint) do route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)') - double(:endpoint, route: route, - options: { for: api_handler, path: [":id/archive"] }, - namespace: "/projects") + double(:endpoint, + route: route, options: { for: api_handler, path: [":id/archive"] }, namespace: "/projects") end let(:env) { { 'api.endpoint' => endpoint, 'REQUEST_METHOD' => 'GET' } } @@ -256,9 +255,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do context 'Grape API without expected duration' do let(:endpoint) do route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)') - double(:endpoint, route: route, - options: { for: api_handler, path: [":id/archive"] }, - namespace: "/projects") + double(:endpoint, + route: route, options: { for: api_handler, path: [":id/archive"] }, namespace: "/projects") end let(:env) { { 'api.endpoint' => endpoint, 'REQUEST_METHOD' => 'GET' } } diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb index adbc474343f..67cd8630758 100644 --- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActionView do root = Rails.root.to_s double(:event, duration: 2.1, - payload: { identifier: "#{root}/app/views/x.html.haml" }) + payload: { identifier: "#{root}/app/views/x.html.haml" }) end before do diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 28c3ef229ab..005c1ae2d0a 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -137,7 +137,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do :event, name: 'transaction.active_record', duration: 230, - payload: { connection: connection } + payload: { connection: connection } ) end @@ -213,7 +213,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do :event, name: 'sql.active_record', duration: 2, - payload: payload + payload: payload ) end @@ -278,7 +278,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do :event, name: 'sql.active_record', duration: 2, - payload: payload + payload: payload ) end diff --git a/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb b/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb index bc6effd0438..7f7efaffd9e 100644 --- a/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::LoadBalancing, :request_store do double( :event, name: 'load_balancing.caught_up_replica_pick', - payload: payload + payload: payload ) end @@ -37,7 +37,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::LoadBalancing, :request_store do double( :event, name: 'load_balancing.web_transaction_completed', - payload: {} + payload: {} ) end diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb index ce3caf8cdfe..7739501dd95 100644 --- a/spec/lib/gitlab/metrics/system_spec.rb +++ b/spec/lib/gitlab/metrics/system_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Metrics::System do context 'when /proc files exist' do @@ -72,10 +72,20 @@ RSpec.describe Gitlab::Metrics::System do end describe '.memory_usage_rss' do - it "returns the process' resident set size (RSS) in bytes" do - mock_existing_proc_file('/proc/self/status', proc_status) + context 'without PID' do + it "returns the current process' resident set size (RSS) in bytes" do + mock_existing_proc_file('/proc/self/status', proc_status) + + expect(described_class.memory_usage_rss).to eq(2527232) + end + end + + context 'with PID' do + it "returns the given process' resident set size (RSS) in bytes" do + mock_existing_proc_file('/proc/7/status', proc_status) - expect(described_class.memory_usage_rss).to eq(2527232) + expect(described_class.memory_usage_rss(pid: 7)).to eq(2527232) + end end end @@ -96,11 +106,22 @@ RSpec.describe Gitlab::Metrics::System do end describe '.memory_usage_uss_pss' do - it "returns the process' unique and porportional set size (USS/PSS) in bytes" do - mock_existing_proc_file('/proc/self/smaps_rollup', proc_smaps_rollup) + context 'without PID' do + it "returns the current process' unique and porportional set size (USS/PSS) in bytes" do + mock_existing_proc_file('/proc/self/smaps_rollup', proc_smaps_rollup) + + # (Private_Clean (152 kB) + Private_Dirty (312 kB) + Private_Hugetlb (0 kB)) * 1024 + expect(described_class.memory_usage_uss_pss).to eq(uss: 475136, pss: 515072) + end + end + + context 'with PID' do + it "returns the given process' unique and porportional set size (USS/PSS) in bytes" do + mock_existing_proc_file('/proc/7/smaps_rollup', proc_smaps_rollup) - # (Private_Clean (152 kB) + Private_Dirty (312 kB) + Private_Hugetlb (0 kB)) * 1024 - expect(described_class.memory_usage_uss_pss).to eq(uss: 475136, pss: 515072) + # (Private_Clean (152 kB) + Private_Dirty (312 kB) + Private_Hugetlb (0 kB)) * 1024 + expect(described_class.memory_usage_uss_pss(pid: 7)).to eq(uss: 475136, pss: 515072) + end end end diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index b1c15db5193..1a8538b5d6a 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Metrics::Transaction do describe '#run' do diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb index d6590efcf4f..dc59fa804c4 100644 --- a/spec/lib/gitlab/metrics/web_transaction_spec.rb +++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb @@ -66,8 +66,8 @@ RSpec.describe Gitlab::Metrics::WebTransaction do before do route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)') endpoint = double(:endpoint, route: route, - options: { for: API::Projects, path: [":id/archive"] }, - namespace: "/projects") + options: { for: API::Projects, path: [":id/archive"] }, + namespace: "/projects") env['api.endpoint'] = endpoint diff --git a/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb b/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb index b868207e67c..02c4ea4df27 100644 --- a/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb +++ b/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb @@ -2,6 +2,7 @@ require 'fast_spec_helper' require 'rack' +require 'tempfile' RSpec.describe Gitlab::Middleware::RackMultipartTempfileFactory do let(:app) do diff --git a/spec/lib/gitlab/middleware/release_env_spec.rb b/spec/lib/gitlab/middleware/release_env_spec.rb index ca0ec0b9d83..a5bda23b38b 100644 --- a/spec/lib/gitlab/middleware/release_env_spec.rb +++ b/spec/lib/gitlab/middleware/release_env_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Middleware::ReleaseEnv do let(:inner_app) { double(:app, call: 'yay') } diff --git a/spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb b/spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb index 91c030a0f45..9fb56e45103 100644 --- a/spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb +++ b/spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Middleware::SidekiqWebStatic do let(:app) { double(:app) } diff --git a/spec/lib/gitlab/namespaced_session_store_spec.rb b/spec/lib/gitlab/namespaced_session_store_spec.rb index a569c86960c..2c258ce3da6 100644 --- a/spec/lib/gitlab/namespaced_session_store_spec.rb +++ b/spec/lib/gitlab/namespaced_session_store_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::NamespacedSessionStore do let(:key) { :some_key } diff --git a/spec/lib/gitlab/nav/top_nav_menu_header_spec.rb b/spec/lib/gitlab/nav/top_nav_menu_header_spec.rb new file mode 100644 index 00000000000..d9da3ba1e46 --- /dev/null +++ b/spec/lib/gitlab/nav/top_nav_menu_header_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe ::Gitlab::Nav::TopNavMenuHeader do + describe '.build' do + it 'builds a hash from with the given header' do + title = 'Test Header' + expected = { + title: title, + type: :header + } + expect(described_class.build(title: title)).to eq(expected) + end + end +end diff --git a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb index 966b23bf51a..d1d6ac80c40 100644 --- a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb +++ b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe ::Gitlab::Nav::TopNavMenuItem do describe '.build' do @@ -17,7 +17,7 @@ RSpec.describe ::Gitlab::Nav::TopNavMenuItem do emoji: 'smile' } - expect(described_class.build(**item)).to eq(item) + expect(described_class.build(**item)).to eq(item.merge(type: :item)) end end end diff --git a/spec/lib/gitlab/net_http_adapter_spec.rb b/spec/lib/gitlab/net_http_adapter_spec.rb index 21c1a1ebe25..fdaf35be31e 100644 --- a/spec/lib/gitlab/net_http_adapter_spec.rb +++ b/spec/lib/gitlab/net_http_adapter_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::NetHttpAdapter do describe '#connect' do diff --git a/spec/lib/gitlab/null_request_store_spec.rb b/spec/lib/gitlab/null_request_store_spec.rb index 66700313c9a..f68f478c73e 100644 --- a/spec/lib/gitlab/null_request_store_spec.rb +++ b/spec/lib/gitlab/null_request_store_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::NullRequestStore do let(:null_store) { described_class.new } diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb index c91b14a33ba..563c97fa2cb 100644 --- a/spec/lib/gitlab/omniauth_initializer_spec.rb +++ b/spec/lib/gitlab/omniauth_initializer_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Gitlab::OmniauthInitializer do context 'when there is an app_id and an app_secret, and an array of args' do let(:provider) do { - 'name' => 'unknown', + 'name' => 'unknown', 'app_id' => 1, 'app_secret' => 2, 'args' => %w[one two three] @@ -46,7 +46,7 @@ RSpec.describe Gitlab::OmniauthInitializer do context 'when there is an app_id and an app_secret, and an array of args, and default values' do let(:provider) do { - 'name' => 'unknown', + 'name' => 'unknown', 'app_id' => 1, 'app_secret' => 2, 'args' => %w[one two three] @@ -68,7 +68,7 @@ RSpec.describe Gitlab::OmniauthInitializer do context 'when there is an app_id and an app_secret, and a hash of args' do let(:provider) do { - 'name' => 'unknown', + 'name' => 'unknown', 'app_id' => 1, 'app_secret' => 2, 'args' => { 'foo' => 100, 'bar' => 200, 'nested' => { 'value' => 300 } } @@ -84,7 +84,7 @@ RSpec.describe Gitlab::OmniauthInitializer do context 'when there is an app_id and an app_secret, and a hash of args, and default arguments' do let(:provider) do { - 'name' => 'unknown', + 'name' => 'unknown', 'app_id' => 1, 'app_secret' => 2, 'args' => { 'foo' => 100, 'bar' => 200, 'nested' => { 'value' => 300 } } @@ -106,7 +106,7 @@ RSpec.describe Gitlab::OmniauthInitializer do context 'when there is an app_id and an app_secret, no args, and default values' do let(:provider) do { - 'name' => 'unknown', + 'name' => 'unknown', 'app_id' => 1, 'app_secret' => 2 } @@ -127,7 +127,7 @@ RSpec.describe Gitlab::OmniauthInitializer do context 'when there are args, of an unsupported type' do let(:provider) do { - 'name' => 'unknown', + 'name' => 'unknown', 'args' => 1 } end diff --git a/spec/lib/gitlab/pagination/keyset/column_order_definition_spec.rb b/spec/lib/gitlab/pagination/keyset/column_order_definition_spec.rb index 778244677ef..100574cc75f 100644 --- a/spec/lib/gitlab/pagination/keyset/column_order_definition_spec.rb +++ b/spec/lib/gitlab/pagination/keyset/column_order_definition_spec.rb @@ -50,6 +50,20 @@ RSpec.describe Gitlab::Pagination::Keyset::ColumnOrderDefinition do it { expect(project_calculated_column).to be_ascending_order } it { expect(project_calculated_column).not_to be_descending_order } + context 'when order expression is an Arel node with nulls_last' do + it 'can automatically determine the reversed expression' do + column_order_definition = described_class.new( + attribute_name: :name, + column_expression: Project.arel_table[:name], + order_expression: Project.arel_table[:name].asc.nulls_last, + nullable: :nulls_last, + distinct: false + ) + + expect(column_order_definition).to be_ascending_order + end + end + it 'raises error when order direction cannot be infered' do expect do described_class.new( @@ -132,6 +146,21 @@ RSpec.describe Gitlab::Pagination::Keyset::ColumnOrderDefinition do expect(column_order_definition.reverse.order_expression).to eq('name desc') end end + + context 'when order expression is an Arel node with nulls_last' do + it 'can automatically determine the reversed expression' do + column_order_definition = described_class.new( + attribute_name: :name, + column_expression: Project.arel_table[:name], + order_expression: Project.arel_table[:name].asc.nulls_last, + order_direction: :asc, + nullable: :nulls_last, + distinct: false + ) + + expect(column_order_definition.reverse.order_expression).to eq(Project.arel_table[:name].desc.nulls_first) + end + end end describe '#nullable' do diff --git a/spec/lib/gitlab/phabricator_import/representation/task_spec.rb b/spec/lib/gitlab/phabricator_import/representation/task_spec.rb index 25a52af3a7a..2b8570e4aff 100644 --- a/spec/lib/gitlab/phabricator_import/representation/task_spec.rb +++ b/spec/lib/gitlab/phabricator_import/representation/task_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::PhabricatorImport::Representation::Task do subject(:task) do diff --git a/spec/lib/gitlab/phabricator_import/representation/user_spec.rb b/spec/lib/gitlab/phabricator_import/representation/user_spec.rb index f51be0f7d8d..6df26b905cc 100644 --- a/spec/lib/gitlab/phabricator_import/representation/user_spec.rb +++ b/spec/lib/gitlab/phabricator_import/representation/user_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::PhabricatorImport::Representation::User do subject(:user) do diff --git a/spec/lib/gitlab/popen/runner_spec.rb b/spec/lib/gitlab/popen/runner_spec.rb index c7b64e8108b..eacb63c8f8a 100644 --- a/spec/lib/gitlab/popen/runner_spec.rb +++ b/spec/lib/gitlab/popen/runner_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Popen::Runner do subject { described_class.new } diff --git a/spec/lib/gitlab/push_options_spec.rb b/spec/lib/gitlab/push_options_spec.rb index 8f43943e2d1..3ff1c8e9012 100644 --- a/spec/lib/gitlab/push_options_spec.rb +++ b/spec/lib/gitlab/push_options_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::PushOptions do describe 'namespace and key validation' do diff --git a/spec/lib/gitlab/quick_actions/substitution_definition_spec.rb b/spec/lib/gitlab/quick_actions/substitution_definition_spec.rb index 8a4e9ab8bb7..08bb06150d4 100644 --- a/spec/lib/gitlab/quick_actions/substitution_definition_spec.rb +++ b/spec/lib/gitlab/quick_actions/substitution_definition_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::QuickActions::SubstitutionDefinition do let(:content) do diff --git a/spec/lib/gitlab/quick_actions/timeline_text_and_date_time_separator_spec.rb b/spec/lib/gitlab/quick_actions/timeline_text_and_date_time_separator_spec.rb new file mode 100644 index 00000000000..89fe19b8f60 --- /dev/null +++ b/spec/lib/gitlab/quick_actions/timeline_text_and_date_time_separator_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::QuickActions::TimelineTextAndDateTimeSeparator do + subject(:timeline_text_and_datetime_separator) { described_class } + + shared_examples 'arg line with invalid parameters' do + it 'returns nil' do + expect(timeline_text_and_datetime_separator.new(invalid_arg).execute).to eq(nil) + end + end + + shared_examples 'arg line with valid parameters' do + it 'returns text and date time array' do + freeze_time do + expect(timeline_text_and_datetime_separator.new(valid_arg).execute).to eq(expected_response) + end + end + end + + describe 'execute' do + context 'with invalid parameters in arg line' do + context 'with empty arg line' do + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { '' } + end + end + + context 'with invalid date' do + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { 'timeline comment | 2022-13-13 09:30' } + end + + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { 'timeline comment | 2022-09/09 09:30' } + end + + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { 'timeline comment | 2022-09.09 09:30' } + end + end + + context 'with invalid time' do + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { 'timeline comment | 2022-11-13 29:30' } + end + end + + context 'when date is invalid in arg line' do + let(:invalid_arg) { 'timeline comment | wrong data type' } + + it 'return current date' do + timeline_args = timeline_text_and_datetime_separator.new(invalid_arg).execute + + expect(timeline_args).to be_an_instance_of(Array) + expect(timeline_args.first).to eq('timeline comment') + expect(timeline_args.second).to match(Gitlab::QuickActions::TimelineTextAndDateTimeSeparator::DATETIME_REGEX) + end + end + end + + context 'with valid parameters' do + context 'when only timeline text present in arg line' do + it_behaves_like 'arg line with valid parameters' do + let(:timeline_text) { 'timeline comment' } + let(:valid_arg) { timeline_text } + let(:date) { DateTime.current.strftime("%Y-%m-%d %H:%M:00 UTC") } + let(:expected_response) { [timeline_text, date] } + end + end + + context 'when only timeline text and time present in arg line' do + it_behaves_like 'arg line with valid parameters' do + let(:timeline_text) { 'timeline comment' } + let(:date) { '09:30' } + let(:valid_arg) { "#{timeline_text} | #{date}" } + let(:parsed_date) { DateTime.parse(date) } + let(:expected_response) { [timeline_text, parsed_date] } + end + end + + context 'when timeline text and date is present in arg line' do + it_behaves_like 'arg line with valid parameters' do + let(:timeline_text) { 'timeline comment' } + let(:date) { '2022-06-05 09:30' } + let(:valid_arg) { "#{timeline_text} | #{date}" } + let(:parsed_date) { DateTime.parse(date) } + let(:expected_response) { [timeline_text, parsed_date] } + end + end + end + end +end diff --git a/spec/lib/gitlab/redis/boolean_spec.rb b/spec/lib/gitlab/redis/boolean_spec.rb index 9c233ba089f..661261c79da 100644 --- a/spec/lib/gitlab/redis/boolean_spec.rb +++ b/spec/lib/gitlab/redis/boolean_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "spec_helper" +require 'fast_spec_helper' RSpec.describe Gitlab::Redis::Boolean do subject(:redis_boolean) { described_class.new(bool) } diff --git a/spec/lib/gitlab/redis/cache_spec.rb b/spec/lib/gitlab/redis/cache_spec.rb index 1f0ebbe107f..82ff8a26199 100644 --- a/spec/lib/gitlab/redis/cache_spec.rb +++ b/spec/lib/gitlab/redis/cache_spec.rb @@ -17,8 +17,8 @@ RSpec.describe Gitlab::Redis::Cache do end describe '.active_support_config' do - it 'has a default ttl of 2 weeks' do - expect(described_class.active_support_config[:expires_in]).to eq(2.weeks) + it 'has a default ttl of 8 hours' do + expect(described_class.active_support_config[:expires_in]).to eq(8.hours) end it 'allows configuring the TTL through an env variable' do diff --git a/spec/lib/gitlab/redis/duplicate_jobs_spec.rb b/spec/lib/gitlab/redis/duplicate_jobs_spec.rb index 53e3d73d17e..be20e6dcdaf 100644 --- a/spec/lib/gitlab/redis/duplicate_jobs_spec.rb +++ b/spec/lib/gitlab/redis/duplicate_jobs_spec.rb @@ -46,7 +46,7 @@ RSpec.describe Gitlab::Redis::DuplicateJobs do expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99") expect(redis_instance.primary_store.connection[:namespace]).to be_nil - expect(redis_instance.secondary_store.connection[:id]).to eq("redis:///path/to/redis.sock/0") + expect(redis_instance.secondary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0") expect(redis_instance.secondary_store.connection[:namespace]).to eq("resque:gitlab") expect(redis_instance.instance_name).to eq('DuplicateJobs') diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb index ef8549548d7..8b73b5e03c0 100644 --- a/spec/lib/gitlab/redis/multi_store_spec.rb +++ b/spec/lib/gitlab/redis/multi_store_spec.rb @@ -264,13 +264,20 @@ RSpec.describe Gitlab::Redis::MultiStore do context 'when the command is executed within pipelined block' do subject do - multi_store.pipelined do - multi_store.send(name, *args) + multi_store.pipelined do |pipeline| + pipeline.send(name, *args) end end - it 'is executed only 1 time on primary instance' do - expect(primary_store).to receive(name).with(*args).once + it 'is executed only 1 time on primary and secondary instance' do + expect(primary_store).to receive(:pipelined).and_call_original + expect(secondary_store).to receive(:pipelined).and_call_original + + 2.times do + expect_next_instance_of(Redis::PipelinedConnection) do |pipeline| + expect(pipeline).to receive(name).with(*args).once.and_call_original + end + end subject end @@ -438,14 +445,21 @@ RSpec.describe Gitlab::Redis::MultiStore do context 'when the command is executed within pipelined block' do subject do - multi_store.pipelined do - multi_store.send(name, *args) + multi_store.pipelined do |pipeline| + pipeline.send(name, *args) end end it 'is executed only 1 time on each instance', :aggregate_errors do - expect(primary_store).to receive(name).with(*expected_args).once - expect(secondary_store).to receive(name).with(*expected_args).once + expect(primary_store).to receive(:pipelined).and_call_original + expect_next_instance_of(Redis::PipelinedConnection) do |pipeline| + expect(pipeline).to receive(name).with(*expected_args).once.and_call_original + end + + expect(secondary_store).to receive(:pipelined).and_call_original + expect_next_instance_of(Redis::PipelinedConnection) do |pipeline| + expect(pipeline).to receive(name).with(*expected_args).once.and_call_original + end subject end @@ -781,14 +795,20 @@ RSpec.describe Gitlab::Redis::MultiStore do context 'when the command is executed within pipelined block' do subject do - multi_store.pipelined do - multi_store.incr(key) + multi_store.pipelined do |pipeline| + pipeline.incr(key) end end it 'is executed only 1 time on each instance', :aggregate_errors do - expect(primary_store).to receive(:incr).with(key).once - expect(secondary_store).to receive(:incr).with(key).once + expect(primary_store).to receive(:pipelined).once.and_call_original + expect(secondary_store).to receive(:pipelined).once.and_call_original + + 2.times do + expect_next_instance_of(Redis::PipelinedConnection) do |pipeline| + expect(pipeline).to receive(:incr).with(key).once + end + end subject end diff --git a/spec/lib/gitlab/redis/sidekiq_status_spec.rb b/spec/lib/gitlab/redis/sidekiq_status_spec.rb index f641ea40efd..76d130d67f7 100644 --- a/spec/lib/gitlab/redis/sidekiq_status_spec.rb +++ b/spec/lib/gitlab/redis/sidekiq_status_spec.rb @@ -42,7 +42,7 @@ RSpec.describe Gitlab::Redis::SidekiqStatus do 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("redis:///path/to/redis.sock/0") + expect(redis_instance.secondary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0") expect(redis_instance.instance_name).to eq('SidekiqStatus') end diff --git a/spec/lib/gitlab/render_timeout_spec.rb b/spec/lib/gitlab/render_timeout_spec.rb index f322d71867b..b1386855fd5 100644 --- a/spec/lib/gitlab/render_timeout_spec.rb +++ b/spec/lib/gitlab/render_timeout_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::RenderTimeout do def expect_timeout(period) diff --git a/spec/lib/gitlab/seeder_spec.rb b/spec/lib/gitlab/seeder_spec.rb index 0ad80323085..a94ae2bca7a 100644 --- a/spec/lib/gitlab/seeder_spec.rb +++ b/spec/lib/gitlab/seeder_spec.rb @@ -77,44 +77,4 @@ RSpec.describe Gitlab::Seeder do end end end - - describe ::Gitlab::Seeder::Ci::DailyBuildGroupReportResult do - let_it_be(:group) { create(:group) } - let_it_be(:project) { create(:project, :repository, group: group) } - let_it_be(:pipeline) { create(:ci_pipeline, project: project) } - let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) } - - subject(:build_report) do - described_class.new(project) - end - - describe '#seed' do - it 'creates daily build results for the project' do - expect { build_report.seed }.to change { - Ci::DailyBuildGroupReportResult.count - }.by(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS) - end - - it 'matches project data with last report' do - build_report.seed - - report = project.daily_build_group_report_results.last - reports_count = project.daily_build_group_report_results.count - - expect(build.group_name).to eq(report.group_name) - expect(pipeline.source_ref_path).to eq(report.ref_path) - expect(pipeline.default_branch?).to eq(report.default_branch) - expect(reports_count).to eq(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS) - end - - it 'does not raise error on RecordNotUnique' do - build_report.seed - build_report.seed - - reports_count = project.daily_build_group_report_results.count - - expect(reports_count).to eq(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS) - end - end - end end diff --git a/spec/lib/gitlab/seeders/ci/daily_build_group_report_result_spec.rb b/spec/lib/gitlab/seeders/ci/daily_build_group_report_result_spec.rb new file mode 100644 index 00000000000..4b41122d23c --- /dev/null +++ b/spec/lib/gitlab/seeders/ci/daily_build_group_report_result_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Gitlab::Seeders::Ci::DailyBuildGroupReportResult do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :repository, group: group) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) } + + subject(:build_report) do + described_class.new(project) + end + + describe '#seed' do + it 'creates daily build results for the project' do + expect { build_report.seed }.to change { + Ci::DailyBuildGroupReportResult.count + }.by(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS) + end + + it 'matches project data with last report' do + build_report.seed + + report = project.daily_build_group_report_results.last + reports_count = project.daily_build_group_report_results.count + + expect(build.group_name).to eq(report.group_name) + expect(pipeline.source_ref_path).to eq(report.ref_path) + expect(pipeline.default_branch?).to eq(report.default_branch) + expect(reports_count).to eq(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS) + end + + it 'does not raise error on RecordNotUnique' do + build_report.seed + build_report.seed + + reports_count = project.daily_build_group_report_results.count + + expect(reports_count).to eq(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS) + end + end +end diff --git a/spec/lib/gitlab/service_desk_email_spec.rb b/spec/lib/gitlab/service_desk_email_spec.rb index 9847496e361..6667b61c02b 100644 --- a/spec/lib/gitlab/service_desk_email_spec.rb +++ b/spec/lib/gitlab/service_desk_email_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::ServiceDeskEmail do describe '.enabled?' do diff --git a/spec/lib/gitlab/session_spec.rb b/spec/lib/gitlab/session_spec.rb index 67ad59f956d..171288da1d5 100644 --- a/spec/lib/gitlab/session_spec.rb +++ b/spec/lib/gitlab/session_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Session do it 'uses the current thread as a data store' do diff --git a/spec/lib/gitlab/setup_helper/workhorse_spec.rb b/spec/lib/gitlab/setup_helper/workhorse_spec.rb index 18cb266bf4e..726b73a9dfe 100644 --- a/spec/lib/gitlab/setup_helper/workhorse_spec.rb +++ b/spec/lib/gitlab/setup_helper/workhorse_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::SetupHelper::Workhorse do describe '.make' do diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 891b3639709..785429aa3b0 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -9,9 +9,13 @@ RSpec.describe Gitlab::Shell do let(:repository) { project.repository } let(:gitlab_shell) { described_class.new } + before do + described_class.instance_variable_set(:@secret_token, nil) + end + it { is_expected.to respond_to :remove_repository } - describe 'memoized secret_token' do + describe '.secret_token' do let(:secret_file) { 'tmp/tests/.secret_shell_test' } let(:link_file) { 'tmp/tests/shell-secret-test/.gitlab_shell_secret' } @@ -19,7 +23,6 @@ RSpec.describe Gitlab::Shell do allow(Gitlab.config.gitlab_shell).to receive(:secret_file).and_return(secret_file) allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-secret-test') FileUtils.mkdir('tmp/tests/shell-secret-test') - described_class.ensure_secret_token! end after do @@ -27,13 +30,47 @@ RSpec.describe Gitlab::Shell do FileUtils.rm_rf(secret_file) end - it 'creates and links the secret token file' do - secret_token = described_class.secret_token + shared_examples 'creates and links the secret token file' do + it 'creates and links the secret token file' do + secret_token = described_class.secret_token + + expect(File.exist?(secret_file)).to be(true) + expect(File.read(secret_file).chomp).to eq(secret_token) + expect(File.symlink?(link_file)).to be(true) + expect(File.readlink(link_file)).to eq(secret_file) + end + end + + describe 'memoized secret_token' do + before do + described_class.ensure_secret_token! + end + + it_behaves_like 'creates and links the secret token file' + end + + context 'when link_file is a broken symbolic link' do + before do + File.symlink('tmp/tests/non_existing_file', link_file) + described_class.ensure_secret_token! + end + + it_behaves_like 'creates and links the secret token file' + end + + context 'when secret_file exists' do + let(:secret_token) { 'secret-token' } - expect(File.exist?(secret_file)).to be(true) - expect(File.read(secret_file).chomp).to eq(secret_token) - expect(File.symlink?(link_file)).to be(true) - expect(File.readlink(link_file)).to eq(secret_file) + before do + File.write(secret_file, 'secret-token') + described_class.ensure_secret_token! + end + + it_behaves_like 'creates and links the secret token file' + + it 'reads the token from the existing file' do + expect(described_class.secret_token).to eq(secret_token) + end end end diff --git a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb index 635f572daef..dff04a2e509 100644 --- a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb +++ b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb @@ -326,7 +326,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do class: described_class.to_s, signal: signal, pid: pid, - message: "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})") + message: "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})") expect(Process).to receive(:kill).with(signal, pid).ordered subject @@ -340,7 +340,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do class: described_class.to_s, signal: signal, pid: pid, - message: "sending Sidekiq worker PGRP-#{pid} #{signal} (#{explanation})") + message: "sending Sidekiq worker PGRP-#{pid} #{signal} (#{explanation})") expect(Process).to receive(:kill).with(signal, 0).ordered subject diff --git a/spec/lib/gitlab/sidekiq_death_handler_spec.rb b/spec/lib/gitlab/sidekiq_death_handler_spec.rb index e3f9f8277a0..434642bf3ef 100644 --- a/spec/lib/gitlab/sidekiq_death_handler_spec.rb +++ b/spec/lib/gitlab/sidekiq_death_handler_spec.rb @@ -24,8 +24,8 @@ RSpec.describe Gitlab::SidekiqDeathHandler, :clean_gitlab_redis_queues do expect(described_class.counter) .to receive(:increment) .with({ queue: 'test_queue', worker: 'TestWorker', - urgency: 'low', external_dependencies: 'yes', - feature_category: 'users', boundary: 'cpu' }) + urgency: 'low', external_dependencies: 'yes', + feature_category: 'users', boundary: 'cpu' }) described_class.handler({ 'class' => 'TestWorker', 'queue' => 'test_queue' }, nil) end @@ -40,8 +40,8 @@ RSpec.describe Gitlab::SidekiqDeathHandler, :clean_gitlab_redis_queues do expect(described_class.counter) .to receive(:increment) .with({ queue: 'test_queue', worker: 'TestWorker', - urgency: '', external_dependencies: 'no', - feature_category: '', boundary: '' }) + urgency: '', external_dependencies: 'no', + feature_category: '', boundary: '' }) described_class.handler({ 'class' => 'TestWorker', 'queue' => 'test_queue' }, nil) end diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb index 09548d21106..cc730e203f6 100644 --- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb @@ -41,10 +41,10 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Server, :clean_gitlab_r describe '#call' do it 'removes the stored job from redis before execution' do bare_job = { 'class' => 'TestDeduplicationWorker', 'args' => ['hello'] } - job_definition = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(bare_job.dup, 'test_deduplication') + job_definition = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(bare_job.dup, 'default') expect(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob) - .to receive(:new).with(a_hash_including(bare_job), 'test_deduplication') + .to receive(:new).with(a_hash_including(bare_job), 'default') .and_return(job_definition).twice # once in client middleware expect(job_definition).to receive(:delete!).ordered.and_call_original @@ -60,10 +60,10 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Server, :clean_gitlab_r it 'removes the stored job from redis after execution' do bare_job = { 'class' => 'TestDeduplicationWorker', 'args' => ['hello'] } - job_definition = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(bare_job.dup, 'test_deduplication') + job_definition = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(bare_job.dup, 'default') expect(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob) - .to receive(:new).with(a_hash_including(bare_job), 'test_deduplication') + .to receive(:new).with(a_hash_including(bare_job), 'default') .and_return(job_definition).twice # once in client middleware expect(TestDeduplicationWorker).to receive(:work).ordered.and_call_original diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb index d6d24ea3a24..52b50a143fc 100644 --- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb @@ -22,39 +22,39 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do expect(completion_seconds_metric) .to receive(:get).with({ queue: 'merge', - worker: 'MergeWorker', - urgency: 'high', - external_dependencies: 'no', - feature_category: 'source_code_management', - boundary: '', - job_status: 'done' }) + worker: 'MergeWorker', + urgency: 'high', + external_dependencies: 'no', + feature_category: 'source_code_management', + boundary: '', + job_status: 'done' }) expect(completion_seconds_metric) .to receive(:get).with({ queue: 'merge', - worker: 'MergeWorker', - urgency: 'high', - external_dependencies: 'no', - feature_category: 'source_code_management', - boundary: '', - job_status: 'fail' }) + worker: 'MergeWorker', + urgency: 'high', + external_dependencies: 'no', + feature_category: 'source_code_management', + boundary: '', + job_status: 'fail' }) expect(completion_seconds_metric) .to receive(:get).with({ queue: 'default', - worker: 'Ci::BuildFinishedWorker', - urgency: 'high', - external_dependencies: 'no', - feature_category: 'continuous_integration', - boundary: 'cpu', - job_status: 'done' }) + worker: 'Ci::BuildFinishedWorker', + urgency: 'high', + external_dependencies: 'no', + feature_category: 'continuous_integration', + boundary: 'cpu', + job_status: 'done' }) expect(completion_seconds_metric) .to receive(:get).with({ queue: 'default', - worker: 'Ci::BuildFinishedWorker', - urgency: 'high', - external_dependencies: 'no', - feature_category: 'continuous_integration', - boundary: 'cpu', - job_status: 'fail' }) + worker: 'Ci::BuildFinishedWorker', + urgency: 'high', + external_dependencies: 'no', + feature_category: 'continuous_integration', + boundary: 'cpu', + job_status: 'fail' }) described_class.initialize_process_metrics end diff --git a/spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb index 91b8ef97ab4..12430313141 100644 --- a/spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' # rubocop: disable RSpec/MultipleMemoizedHelpers RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Server, :clean_gitlab_redis_queues do diff --git a/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb b/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb index d4391d3023a..a576cf3e2ab 100644 --- a/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb +++ b/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb @@ -46,7 +46,7 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do expect(migrator.execute('PostReceive' => 'new_queue')).to eq(scanned: 3, migrated: 0) expect(set_after.length).to eq(3) - expect(set_after.map(&:first)).to all(include('queue' => 'authorized_projects', + expect(set_after.map(&:first)).to all(include('queue' => 'default', 'class' => 'AuthorizedProjectsWorker')) end end @@ -62,7 +62,7 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do if item['class'] == 'AuthorizedProjectsWorker' expect(item).to include('queue' => 'new_queue', 'args' => [i]) else - expect(item).to include('queue' => 'post_receive', 'args' => [i]) + expect(item).to include('queue' => 'default', 'args' => [i]) end expect(score).to be_within(schedule_jitter).of(i.succ.hours.from_now.to_i) @@ -116,7 +116,7 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do expect(migrator.execute('PostReceive' => 'new_queue')).to eq(scanned: 4, migrated: 0) expect(set_after.length).to eq(3) - expect(set_after.map(&:first)).to all(include('queue' => 'authorized_projects')) + expect(set_after.map(&:first)).to all(include('queue' => 'default')) end end @@ -138,7 +138,7 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do expect(migrator.execute('PostReceive' => 'new_queue')).to eq(scanned: 4, migrated: 1) expect(set_after.group_by { |job| job.first['queue'] }.transform_values(&:count)) - .to eq('authorized_projects' => 6, 'new_queue' => 1) + .to eq('default' => 6, 'new_queue' => 1) end it 'iterates through the entire set of jobs' do diff --git a/spec/lib/gitlab/sidekiq_queue_spec.rb b/spec/lib/gitlab/sidekiq_queue_spec.rb index 5e91282612e..93632848788 100644 --- a/spec/lib/gitlab/sidekiq_queue_spec.rb +++ b/spec/lib/gitlab/sidekiq_queue_spec.rb @@ -4,15 +4,15 @@ require 'spec_helper' RSpec.describe Gitlab::SidekiqQueue, :clean_gitlab_redis_queues do around do |example| - Sidekiq::Queue.new('default').clear + Sidekiq::Queue.new('foobar').clear Sidekiq::Testing.disable!(&example) - Sidekiq::Queue.new('default').clear + Sidekiq::Queue.new('foobar').clear end def add_job(args, user:, klass: 'AuthorizedProjectsWorker') Sidekiq::Client.push( 'class' => klass, - 'queue' => 'default', + 'queue' => 'foobar', 'args' => args, 'meta.user' => user.username ) @@ -20,7 +20,7 @@ RSpec.describe Gitlab::SidekiqQueue, :clean_gitlab_redis_queues do describe '#drop_jobs!' do shared_examples 'queue processing' do - let(:sidekiq_queue) { described_class.new('default') } + let(:sidekiq_queue) { described_class.new('foobar') } let_it_be(:sidekiq_queue_user) { create(:user) } before do @@ -80,7 +80,7 @@ RSpec.describe Gitlab::SidekiqQueue, :clean_gitlab_redis_queues do it 'raises NoMetadataError' do add_job([1], user: create(:user)) - expect { described_class.new('default').drop_jobs!({ username: 'sidekiq_queue_user' }, timeout: 1) } + expect { described_class.new('foobar').drop_jobs!({ username: 'sidekiq_queue_user' }, timeout: 1) } .to raise_error(described_class::NoMetadataError) end end diff --git a/spec/lib/gitlab/sidekiq_signals_spec.rb b/spec/lib/gitlab/sidekiq_signals_spec.rb index 2f751839f6a..734b9e79088 100644 --- a/spec/lib/gitlab/sidekiq_signals_spec.rb +++ b/spec/lib/gitlab/sidekiq_signals_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::SidekiqSignals do describe '.install' do diff --git a/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb index 5a0c4cbd1b5..c0fd88eab1b 100644 --- a/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb +++ b/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::SidekiqStatus::ServerMiddleware do describe '#call' do diff --git a/spec/lib/gitlab/sidekiq_versioning_spec.rb b/spec/lib/gitlab/sidekiq_versioning_spec.rb index afafd04d87d..bdbba04e0c0 100644 --- a/spec/lib/gitlab/sidekiq_versioning_spec.rb +++ b/spec/lib/gitlab/sidekiq_versioning_spec.rb @@ -2,30 +2,9 @@ require 'spec_helper' -RSpec.describe Gitlab::SidekiqVersioning, :redis do - let(:foo_worker) do - Class.new do - def self.name - 'FooWorker' - end - - include ApplicationWorker - end - end - - let(:bar_worker) do - Class.new do - def self.name - 'BarWorker' - end - - include ApplicationWorker - end - end - +RSpec.describe Gitlab::SidekiqVersioning, :clean_gitlab_redis_queues do before do - allow(Gitlab::SidekiqConfig).to receive(:workers).and_return([foo_worker, bar_worker]) - allow(Gitlab::SidekiqConfig).to receive(:worker_queues).and_return([foo_worker.queue, bar_worker.queue]) + allow(Gitlab::SidekiqConfig).to receive(:worker_queues).and_return(%w[foo bar]) end describe '.install!' do diff --git a/spec/lib/gitlab/slug/environment_spec.rb b/spec/lib/gitlab/slug/environment_spec.rb index f516322b937..e8f0fba27b2 100644 --- a/spec/lib/gitlab/slug/environment_spec.rb +++ b/spec/lib/gitlab/slug/environment_spec.rb @@ -1,27 +1,27 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Slug::Environment do describe '#generate' do { "staging-12345678901234567" => "staging-123456789-q517sa", "9-staging-123456789012345" => "env-9-staging-123-q517sa", - "staging-1234567890123456" => "staging-1234567890123456", + "staging-1234567890123456" => "staging-1234567890123456", "staging-1234567890123456-" => "staging-123456789-q517sa", - "production" => "production", - "PRODUCTION" => "production-q517sa", - "review/1-foo" => "review-1-foo-q517sa", - "1-foo" => "env-1-foo-q517sa", - "1/foo" => "env-1-foo-q517sa", - "foo-" => "foo", - "foo--bar" => "foo-bar-q517sa", - "foo**bar" => "foo-bar-q517sa", - "*-foo" => "env-foo-q517sa", - "staging-12345678-" => "staging-12345678", + "production" => "production", + "PRODUCTION" => "production-q517sa", + "review/1-foo" => "review-1-foo-q517sa", + "1-foo" => "env-1-foo-q517sa", + "1/foo" => "env-1-foo-q517sa", + "foo-" => "foo", + "foo--bar" => "foo-bar-q517sa", + "foo**bar" => "foo-bar-q517sa", + "*-foo" => "env-foo-q517sa", + "staging-12345678-" => "staging-12345678", "staging-12345678-01234567" => "staging-12345678-q517sa", - "" => "env-q517sa", - nil => "env-q517sa" + "" => "env-q517sa", + nil => "env-q517sa" }.each do |name, matcher| before do # ('a' * 64).to_i(16).to_s(36).last(6) gives 'q517sa' diff --git a/spec/lib/gitlab/spamcheck/client_spec.rb b/spec/lib/gitlab/spamcheck/client_spec.rb index 956ed2a976f..2fe978125c4 100644 --- a/spec/lib/gitlab/spamcheck/client_spec.rb +++ b/spec/lib/gitlab/spamcheck/client_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Gitlab::Spamcheck::Client do end let_it_be(:issue) { create(:issue, description: 'Test issue description') } + let_it_be(:snippet) { create(:personal_snippet, :public, description: 'Test issue description') } let(:response) do verdict = ::Spamcheck::SpamVerdict.new @@ -26,7 +27,7 @@ RSpec.describe Gitlab::Spamcheck::Client do verdict end - subject { described_class.new.issue_spam?(spam_issue: issue, user: user) } + subject { described_class.new.spam?(spammable: issue, user: user) } before do stub_application_setting(spam_check_endpoint_url: endpoint) @@ -56,10 +57,11 @@ RSpec.describe Gitlab::Spamcheck::Client do end end - describe '#issue_spam?' do + shared_examples 'check for spam' do before do allow_next_instance_of(::Spamcheck::SpamcheckService::Stub) do |instance| allow(instance).to receive(:check_for_spam_issue).and_return(response) + allow(instance).to receive(:check_for_spam_snippet).and_return(response) end end @@ -89,12 +91,26 @@ RSpec.describe Gitlab::Spamcheck::Client do end end - describe "#build_issue_protobuf", :aggregate_failures do - it 'builds the expected protobuf object' do + describe "#spam?", :aggregate_failures do + describe 'issue' do + subject { described_class.new.spam?(spammable: issue, user: user) } + + it_behaves_like "check for spam" + end + + describe 'snippet' do + subject { described_class.new.spam?(spammable: snippet, user: user, extra_features: { files: [{ path: "file.rb" }] }) } + + it_behaves_like "check for spam" + end + end + + describe "#build_protobuf", :aggregate_failures do + it 'builds the expected issue protobuf object' do cxt = { action: :create } - issue_pb = described_class.new.send(:build_issue_protobuf, - issue: issue, user: user, - context: cxt) + issue_pb, _ = described_class.new.send(:build_protobuf, + spammable: issue, user: user, + context: cxt, extra_features: {}) expect(issue_pb.title).to eq issue.title expect(issue_pb.description).to eq issue.description expect(issue_pb.user_in_project).to be false @@ -104,6 +120,22 @@ RSpec.describe Gitlab::Spamcheck::Client do expect(issue_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE) expect(issue_pb.user.username).to eq user.username end + + it 'builds the expected snippet protobuf object' do + cxt = { action: :create } + snippet_pb, _ = described_class.new.send(:build_protobuf, + spammable: snippet, user: user, + context: cxt, extra_features: { files: [{ path: 'first.rb' }, { path: 'second.rb' }] }) + expect(snippet_pb.title).to eq snippet.title + expect(snippet_pb.description).to eq snippet.description + expect(snippet_pb.created_at).to eq timestamp_to_protobuf_timestamp(snippet.created_at) + expect(snippet_pb.updated_at).to eq timestamp_to_protobuf_timestamp(snippet.updated_at) + expect(snippet_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE) + expect(snippet_pb.user.username).to eq user.username + expect(snippet_pb.user.username).to eq user.username + expect(snippet_pb.files.first.path).to eq 'first.rb' + expect(snippet_pb.files.last.path).to eq 'second.rb' + end end describe '#build_user_protobuf', :aggregate_failures do @@ -143,6 +175,19 @@ RSpec.describe Gitlab::Spamcheck::Client do end end + describe "#get_spammable_mappings", :aggregate_failures do + it 'is an expected spammable' do + protobuf_class, _ = described_class.new.send(:get_spammable_mappings, issue) + expect(protobuf_class).to eq ::Spamcheck::Issue + end + + it 'is an unexpected spammable' do + expect { described_class.new.send(:get_spammable_mappings, 'spam') }.to raise_error( + ArgumentError, 'Not a spammable type: String' + ) + end + end + private def timestamp_to_protobuf_timestamp(timestamp) diff --git a/spec/lib/gitlab/string_placeholder_replacer_spec.rb b/spec/lib/gitlab/string_placeholder_replacer_spec.rb index 8f17bf64005..9f477998be2 100644 --- a/spec/lib/gitlab/string_placeholder_replacer_spec.rb +++ b/spec/lib/gitlab/string_placeholder_replacer_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::StringPlaceholderReplacer do describe '.render_url' do diff --git a/spec/lib/gitlab/string_range_marker_spec.rb b/spec/lib/gitlab/string_range_marker_spec.rb index 6f63c8e2df4..2ababd6a938 100644 --- a/spec/lib/gitlab/string_range_marker_spec.rb +++ b/spec/lib/gitlab/string_range_marker_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::StringRangeMarker do describe '#mark' do diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb index 0cbe44eacf4..393bfea7c6b 100644 --- a/spec/lib/gitlab/string_regex_marker_spec.rb +++ b/spec/lib/gitlab/string_regex_marker_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::StringRegexMarker do describe '#mark' do diff --git a/spec/lib/gitlab/subscription_portal_spec.rb b/spec/lib/gitlab/subscription_portal_spec.rb index 098a58bff83..f93eb6f96cc 100644 --- a/spec/lib/gitlab/subscription_portal_spec.rb +++ b/spec/lib/gitlab/subscription_portal_spec.rb @@ -53,12 +53,13 @@ RSpec.describe ::Gitlab::SubscriptionPortal do it { is_expected.to match(link_match) } end - context 'url methods' do + describe 'class methods' do where(:method_name, :result) do :default_subscriptions_url | staging_customers_url :payment_form_url | "#{staging_customers_url}/payment_forms/cc_validation" :payment_validation_form_id | 'payment_method_validation' :registration_validation_form_url | "#{staging_customers_url}/payment_forms/cc_registration_validation" + :registration_validation_form_id | 'cc_registration_validation' :subscriptions_graphql_url | "#{staging_customers_url}/graphql" :subscriptions_more_minutes_url | "#{staging_customers_url}/buy_pipeline_minutes" :subscriptions_more_storage_url | "#{staging_customers_url}/buy_storage" @@ -108,4 +109,16 @@ RSpec.describe ::Gitlab::SubscriptionPortal do is_expected.to eq(url) end end + + describe 'constants' do + where(:constant_name, :result) do + 'REGISTRATION_VALIDATION_FORM_ID' | 'cc_registration_validation' + end + + with_them do + subject { "#{described_class}::#{constant_name}".constantize } + + it { is_expected.to eq(result) } + end + end end diff --git a/spec/lib/gitlab/tcp_checker_spec.rb b/spec/lib/gitlab/tcp_checker_spec.rb index 12149576de0..5f9960265ec 100644 --- a/spec/lib/gitlab/tcp_checker_spec.rb +++ b/spec/lib/gitlab/tcp_checker_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::TcpChecker, :permit_dns do before do diff --git a/spec/lib/gitlab/tracking/incident_management_spec.rb b/spec/lib/gitlab/tracking/incident_management_spec.rb index ef7816aa0db..c27e2548526 100644 --- a/spec/lib/gitlab/tracking/incident_management_spec.rb +++ b/spec/lib/gitlab/tracking/incident_management_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Tracking::IncidentManagement do describe '.track_from_params' do diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb index 028c985f3b3..e11175c776d 100644 --- a/spec/lib/gitlab/tracking_spec.rb +++ b/spec/lib/gitlab/tracking_spec.rb @@ -132,9 +132,36 @@ RSpec.describe Gitlab::Tracking do expect(args[:context].last).to eq(other_context) end - described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5, - context: [other_context], project: project, user: user, namespace: namespace, - extra_key_1: 'extra value 1', extra_key_2: 'extra value 2') + described_class.event('category', 'action', + label: 'label', + property: 'property', + value: 1.5, + context: [other_context], + project: project, + user: user, + namespace: namespace, + extra_key_1: 'extra value 1', + extra_key_2: 'extra value 2') + end + end + + context 'when the action is not passed in as a string' do + it 'allows symbols' do + expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception) + + described_class.event('category', :some_action) + end + + it 'allows nil' do + expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception) + + described_class.event('category', nil) + end + + it 'allows integers' do + expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception) + + described_class.event('category', 1) end end @@ -197,8 +224,15 @@ RSpec.describe Gitlab::Tracking do expect(args[:extra_key_1]).to eq('extra value 1') end - described_class.definition('filename', category: nil, action: nil, label: 'label', property: '...', - project: project, user: user, namespace: namespace, extra_key_1: 'extra value 1') + described_class.definition('filename', + category: nil, + action: nil, + label: 'label', + property: '...', + project: project, + user: user, + namespace: namespace, + extra_key_1: 'extra value 1') end end diff --git a/spec/lib/gitlab/tree_summary_spec.rb b/spec/lib/gitlab/tree_summary_spec.rb index f45005fcc9b..42cc15a9033 100644 --- a/spec/lib/gitlab/tree_summary_spec.rb +++ b/spec/lib/gitlab/tree_summary_spec.rb @@ -25,6 +25,14 @@ RSpec.describe Gitlab::TreeSummary do it 'defaults limit to 25' do expect(summary.limit).to eq(25) end + + context 'when offset is larger than the maximum' do + let(:offset) { described_class::MAX_OFFSET + 1 } + + it 'sets offset to the maximum' do + expect(subject.offset).to eq(described_class::MAX_OFFSET) + end + end end describe '#summarize' do @@ -45,6 +53,14 @@ RSpec.describe Gitlab::TreeSummary do end end + context 'when offset is negative' do + let(:offset) { -1 } + + it 'returns an empty array' do + expect(entries).to eq([]) + end + end + context 'with caching', :use_clean_rails_memory_store_caching do subject { Rails.cache.fetch(key) } diff --git a/spec/lib/gitlab/url_blockers/domain_allowlist_entry_spec.rb b/spec/lib/gitlab/url_blockers/domain_allowlist_entry_spec.rb index ece0a018d53..2405b6769b7 100644 --- a/spec/lib/gitlab/url_blockers/domain_allowlist_entry_spec.rb +++ b/spec/lib/gitlab/url_blockers/domain_allowlist_entry_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::UrlBlockers::DomainAllowlistEntry do let(:domain) { 'www.example.com' } diff --git a/spec/lib/gitlab/url_blockers/ip_allowlist_entry_spec.rb b/spec/lib/gitlab/url_blockers/ip_allowlist_entry_spec.rb index 110a6c17adb..8dcb402dfb2 100644 --- a/spec/lib/gitlab/url_blockers/ip_allowlist_entry_spec.rb +++ b/spec/lib/gitlab/url_blockers/ip_allowlist_entry_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::UrlBlockers::IpAllowlistEntry do let(:ipv4) { IPAddr.new('192.168.1.1') } diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb index b85d5a3ebf9..ce15d44b1e1 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb @@ -5,15 +5,17 @@ require 'spec_helper' RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitiesMetric do let_it_be(:user) { create(:user) } let_it_be(:bulk_import_projects) do - create_list(:bulk_import_entity, 3, source_type: 'project_entity', created_at: 3.weeks.ago) + create_list(:bulk_import_entity, 2, source_type: 'project_entity', created_at: 3.weeks.ago, status: 2) + create(:bulk_import_entity, source_type: 'project_entity', created_at: 3.weeks.ago, status: 0) end let_it_be(:bulk_import_groups) do - create_list(:bulk_import_entity, 3, source_type: 'group_entity', created_at: 3.weeks.ago) + create_list(:bulk_import_entity, 2, source_type: 'group_entity', created_at: 3.weeks.ago, status: 2) + create(:bulk_import_entity, source_type: 'group_entity', created_at: 3.weeks.ago, status: 0) end let_it_be(:old_bulk_import_project) do - create(:bulk_import_entity, source_type: 'project_entity', created_at: 2.months.ago) + create(:bulk_import_entity, source_type: 'project_entity', created_at: 2.months.ago, status: 2) end context 'with no source_type' do @@ -103,4 +105,62 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitie options: { source_type: 'group_entity' } end end + + context 'with entity status' do + context 'with all time frame' do + let(:expected_value) { 5 } + let(:expected_query) do + "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\""\ + " WHERE \"bulk_import_entities\".\"status\" = 2" + end + + it_behaves_like 'a correct instrumented metric value and query', + time_frame: 'all', + options: { status: 2 } + end + + context 'for 28d time frame' do + let(:expected_value) { 4 } + let(:start) { 30.days.ago.to_s(:db) } + let(:finish) { 2.days.ago.to_s(:db) } + let(:expected_query) do + "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\""\ + " WHERE \"bulk_import_entities\".\"created_at\" BETWEEN '#{start}' AND '#{finish}'"\ + " AND \"bulk_import_entities\".\"status\" = 2" + end + + it_behaves_like 'a correct instrumented metric value and query', + time_frame: '28d', + options: { status: 2 } + end + end + + context 'with entity status and source_type' do + context 'with all time frame' do + let(:expected_value) { 3 } + let(:expected_query) do + "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\""\ + " WHERE \"bulk_import_entities\".\"source_type\" = 1 AND \"bulk_import_entities\".\"status\" = 2" + end + + it_behaves_like 'a correct instrumented metric value and query', + time_frame: 'all', + options: { status: 2, source_type: 'project_entity' } + end + + context 'for 28d time frame' do + let(:expected_value) { 2 } + let(:start) { 30.days.ago.to_s(:db) } + let(:finish) { 2.days.ago.to_s(:db) } + let(:expected_query) do + "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\""\ + " WHERE \"bulk_import_entities\".\"created_at\" BETWEEN '#{start}' AND '#{finish}'"\ + " AND \"bulk_import_entities\".\"source_type\" = 1 AND \"bulk_import_entities\".\"status\" = 2" + end + + it_behaves_like 'a correct instrumented metric value and query', + time_frame: '28d', + options: { status: 2, source_type: 'project_entity' } + end + end end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric_spec.rb new file mode 100644 index 00000000000..2f49c427bd0 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountUserAuthMetric do + context 'with all time frame' do + let(:expected_value) { 2 } + + before do + user = create(:user) + user2 = create(:user) + create(:authentication_event, user: user, provider: :ldapmain, result: :success) + create(:authentication_event, user: user2, provider: :ldapsecondary, result: :success) + create(:authentication_event, user: user2, provider: :group_saml, result: :success) + create(:authentication_event, user: user2, provider: :group_saml, result: :success) + create(:authentication_event, user: user, provider: :group_saml, result: :failed) + end + + it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' } + end + + context 'with 28d time frame' do + let(:expected_value) { 1 } + + before do + user = create(:user) + user2 = create(:user) + + create(:authentication_event, created_at: 1.year.ago, user: user, provider: :ldapmain, result: :success) + create(:authentication_event, created_at: 1.week.ago, user: user2, provider: :ldapsecondary, result: :success) + end + + it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', data_source: 'database' } + end +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb index 831f775ec9a..80ae5c6fd21 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb @@ -11,18 +11,18 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisMetric, :clean_git let(:expected_value) { 4 } - it_behaves_like 'a correct instrumented metric value', { options: { event: 'pushes', counter_class: 'SourceCodeCounter' } } + it_behaves_like 'a correct instrumented metric value', { options: { event: 'pushes', prefix: 'source_code' } } it 'raises an exception if event option is not present' do - expect { described_class.new(counter_class: 'SourceCodeCounter') }.to raise_error(ArgumentError) + expect { described_class.new(prefix: 'source_code') }.to raise_error(ArgumentError) end - it 'raises an exception if counter_class option is not present' do + it 'raises an exception if prefix option is not present' do expect { described_class.new(event: 'pushes') }.to raise_error(ArgumentError) end describe 'children classes' do - let(:options) { { event: 'pushes', counter_class: 'SourceCodeCounter' } } + let(:options) { { event: 'pushes', prefix: 'source_code' } } context 'availability not defined' do subject { Class.new(described_class).new(time_frame: nil, options: options) } @@ -44,4 +44,18 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisMetric, :clean_git end end end + + context "with usage prefix disabled" do + let(:expected_value) { 3 } + + before do + 3.times do + Gitlab::UsageDataCounters::WebIdeCounter.increment_merge_requests_count + end + end + + it_behaves_like 'a correct instrumented metric value', { + options: { event: 'merge_requests_count', prefix: 'web_ide', include_usage_prefix: false } + } + end end diff --git a/spec/lib/gitlab/usage_data/topology_spec.rb b/spec/lib/gitlab/usage_data/topology_spec.rb index 737580e3493..dfdf8eaabe8 100644 --- a/spec/lib/gitlab/usage_data/topology_spec.rb +++ b/spec/lib/gitlab/usage_data/topology_spec.rb @@ -187,7 +187,7 @@ RSpec.describe Gitlab::UsageData::Topology do [ { 'metric' => { 'instance' => 'localhost:9100' }, - 'value' => [1000, '512'] + 'value' => [1000, '512'] } ] end @@ -196,7 +196,7 @@ RSpec.describe Gitlab::UsageData::Topology do [ { 'metric' => { 'instance' => 'localhost:9100' }, - 'value' => [1000, '0.35'] + 'value' => [1000, '0.35'] } ] end @@ -224,23 +224,23 @@ RSpec.describe Gitlab::UsageData::Topology do [ { 'metric' => { 'instance' => 'localhost:8080', 'job' => 'gitlab-rails' }, - 'value' => [1000, '10'] + 'value' => [1000, '10'] }, { 'metric' => { 'instance' => '127.0.0.1:8090', 'job' => 'gitlab-sidekiq' }, - 'value' => [1000, '11'] + 'value' => [1000, '11'] }, { 'metric' => { 'instance' => '0.0.0.0:9090', 'job' => 'prometheus' }, - 'value' => [1000, '12'] + 'value' => [1000, '12'] }, { 'metric' => { 'instance' => '[::1]:1234', 'job' => 'redis' }, - 'value' => [1000, '13'] + 'value' => [1000, '13'] }, { 'metric' => { 'instance' => '[::]:1234', 'job' => 'postgres' }, - 'value' => [1000, '14'] + 'value' => [1000, '14'] } ] end @@ -640,7 +640,7 @@ RSpec.describe Gitlab::UsageData::Topology do .and_return(result || [ { 'metric' => { 'instance' => 'instance1:8080', 'job' => 'gitlab-rails' }, - 'value' => [1000, '300'] + 'value' => [1000, '300'] }, { 'metric' => { 'instance' => 'instance1:8090', 'job' => 'gitlab-sidekiq' }, diff --git a/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb index 4a31191d75f..9cecaa01885 100644 --- a/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::UsageDataCounters::BaseCounter do describe '.fetch_supported_event' do diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index e0b334cb5af..3fb2532521a 100644 --- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb @@ -107,10 +107,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'quickactions', 'pipeline_authoring', 'epics_usage', - 'epic_boards_usage', 'secure', 'importer', - 'network_policies', 'geo', 'growth', 'work_items', 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 84a6f338282..032a5e78385 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 @@ -15,7 +15,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git let(:time) { Time.zone.now } context 'for Issue title edit actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_TITLE_CHANGED } def track_action(params) @@ -25,7 +25,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue description edit actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_DESCRIPTION_CHANGED } def track_action(params) @@ -35,7 +35,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue assignee edit actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_ASSIGNEE_CHANGED } def track_action(params) @@ -45,7 +45,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue make confidential actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_MADE_CONFIDENTIAL } def track_action(params) @@ -55,7 +55,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue make visible actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_MADE_VISIBLE } def track_action(params) @@ -65,7 +65,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue created actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_CREATED } def track_action(params) @@ -75,7 +75,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue closed actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_CLOSED } def track_action(params) @@ -85,7 +85,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue reopened actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_REOPENED } def track_action(params) @@ -95,7 +95,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue label changed actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_LABEL_CHANGED } def track_action(params) @@ -104,8 +104,18 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end end + context 'for Issue label milestone actions' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do + let(:action) { described_class::ISSUE_MILESTONE_CHANGED } + + def track_action(params) + described_class.track_issue_milestone_changed_action(**params) + end + end + end + context 'for Issue cross-referenced actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_CROSS_REFERENCED } def track_action(params) @@ -115,7 +125,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue moved actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_MOVED } def track_action(params) @@ -135,7 +145,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue relate actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_RELATED } def track_action(params) @@ -145,7 +155,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue unrelate actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_UNRELATED } def track_action(params) @@ -155,7 +165,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue marked as duplicate actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_MARKED_AS_DUPLICATE } def track_action(params) @@ -165,7 +175,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue locked actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_LOCKED } def track_action(params) @@ -175,7 +185,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue unlocked actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_UNLOCKED } def track_action(params) @@ -185,7 +195,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue designs added actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_DESIGNS_ADDED } def track_action(params) @@ -195,7 +205,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue designs modified actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_DESIGNS_MODIFIED } def track_action(params) @@ -205,7 +215,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue designs removed actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_DESIGNS_REMOVED } def track_action(params) @@ -215,7 +225,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue due date changed actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_DUE_DATE_CHANGED } def track_action(params) @@ -225,7 +235,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue time estimate changed actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_TIME_ESTIMATE_CHANGED } def track_action(params) @@ -235,7 +245,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue time spent changed actions' do - it_behaves_like 'a daily tracked issuable event' do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_TIME_SPENT_CHANGED } def track_action(params) @@ -275,15 +285,15 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end it 'can return the count of actions per user deduplicated', :aggregate_failures do - described_class.track_issue_title_changed_action(author: user1) - described_class.track_issue_description_changed_action(author: user1) - described_class.track_issue_assignee_changed_action(author: user1) + described_class.track_issue_title_changed_action(author: user1, project: project) + described_class.track_issue_description_changed_action(author: user1, project: project) + described_class.track_issue_assignee_changed_action(author: user1, project: project) travel_to(2.days.ago) do - described_class.track_issue_title_changed_action(author: user2) - described_class.track_issue_title_changed_action(author: user3) - described_class.track_issue_description_changed_action(author: user3) - described_class.track_issue_assignee_changed_action(author: user3) + described_class.track_issue_title_changed_action(author: user2, project: project) + described_class.track_issue_title_changed_action(author: user3, project: project) + described_class.track_issue_description_changed_action(author: user3, project: project) + described_class.track_issue_assignee_changed_action(author: user3, project: project) end events = Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(described_class::ISSUE_CATEGORY) 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 3f44cfdcf27..74e63d219bd 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 @@ -100,9 +100,9 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl subject expect_snowplow_event( - category: 'merge_requests', + category: 'merge_requests', action: 'i_code_review_user_approve_mr', - namespace: target_project.namespace, + namespace: target_project.namespace, user: user, project: target_project ) diff --git a/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb index 7e8f0172e06..687f8c2cd41 100644 --- a/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb @@ -41,7 +41,7 @@ RSpec.describe Gitlab::UsageDataCounters::NoteCounter, :clean_gitlab_redis_share let(:expected_totals) do { snippet_comment: 3, merge_request_comment: 4, - commit_comment: 5 } + commit_comment: 5 } end before do diff --git a/spec/lib/gitlab/usage_data_counters_spec.rb b/spec/lib/gitlab/usage_data_counters_spec.rb index 0696b375eb5..040b5deca54 100644 --- a/spec/lib/gitlab/usage_data_counters_spec.rb +++ b/spec/lib/gitlab/usage_data_counters_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::UsageDataCounters do describe '.usage_data_counters' do diff --git a/spec/lib/gitlab/usage_data_metrics_spec.rb b/spec/lib/gitlab/usage_data_metrics_spec.rb index 485f2131d87..ed0eabf1b4d 100644 --- a/spec/lib/gitlab/usage_data_metrics_spec.rb +++ b/spec/lib/gitlab/usage_data_metrics_spec.rb @@ -16,6 +16,10 @@ RSpec.describe Gitlab::UsageDataMetrics do allow_next_instance_of(Gitlab::Database::BatchCounter) do |batch_counter| allow(batch_counter).to receive(:transaction_open?).and_return(false) end + + allow_next_instance_of(Gitlab::Database::BatchAverageCounter) do |instance| + allow(instance).to receive(:transaction_open?).and_return(false) + end end context 'with instrumentation_class' do @@ -33,6 +37,10 @@ RSpec.describe Gitlab::UsageDataMetrics do expect(subject[:usage_activity_by_stage][:plan]).to include(:issues) end + it 'includes usage_activity_by_stage metrics' do + expect(subject[:usage_activity_by_stage][:manage]).to include(:count_user_auth) + end + it 'includes usage_activity_by_stage_monthly keys' do expect(subject[:usage_activity_by_stage_monthly][:plan]).to include(:issues) end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 692b6483149..46ed4b57d3a 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -215,14 +215,28 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do groups: 2, users_created: 10, omniauth_providers: ['google_oauth2'], - user_auth_by_provider: { 'group_saml' => 2, 'ldap' => 4, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 } + user_auth_by_provider: { + 'group_saml' => 2, + 'ldap' => 4, + 'standard' => 0, + 'two-factor' => 0, + 'two-factor-via-u2f-device' => 0, + "two-factor-via-webauthn-device" => 0 + } ) expect(described_class.usage_activity_by_stage_manage(described_class.monthly_time_range_db_params)).to include( events: be_within(error_rate).percent_of(2), groups: 1, users_created: 6, omniauth_providers: ['google_oauth2'], - user_auth_by_provider: { 'group_saml' => 1, 'ldap' => 2, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 } + user_auth_by_provider: { + 'group_saml' => 1, + 'ldap' => 2, + 'standard' => 0, + 'two-factor' => 0, + 'two-factor-via-u2f-device' => 0, + "two-factor-via-webauthn-device" => 0 + } ) end @@ -583,10 +597,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do it 'gathers object store usage correctly' do expect(subject[:object_store]).to eq( { artifacts: { enabled: true, object_store: { enabled: true, direct_upload: true, background_upload: false, provider: "AWS" } }, - external_diffs: { enabled: false }, - lfs: { enabled: true, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } }, - uploads: { enabled: nil, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } }, - packages: { enabled: true, object_store: { enabled: false, direct_upload: false, background_upload: true, provider: "AWS" } } } + external_diffs: { enabled: false }, + lfs: { enabled: true, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } }, + uploads: { enabled: nil, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } }, + packages: { enabled: true, object_store: { enabled: false, direct_upload: false, background_upload: true, provider: "AWS" } } } ) end @@ -749,9 +763,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do it { is_expected.to include(:kubernetes_agent_gitops_sync) } it { is_expected.to include(:kubernetes_agent_k8s_api_proxy_request) } - it { is_expected.to include(:package_events_i_package_pull_package) } - it { is_expected.to include(:package_events_i_package_delete_package_by_user) } - it { is_expected.to include(:package_events_i_package_conan_push_package) } end describe '.usage_data_counters' do diff --git a/spec/lib/gitlab/utils/deep_size_spec.rb b/spec/lib/gitlab/utils/deep_size_spec.rb index 7595fb2c1b0..6b0be4590f1 100644 --- a/spec/lib/gitlab/utils/deep_size_spec.rb +++ b/spec/lib/gitlab/utils/deep_size_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Utils::DeepSize do let(:data) do @@ -58,10 +58,4 @@ RSpec.describe Gitlab::Utils::DeepSize do it { is_expected.not_to be_valid } end end - - describe '.human_default_max_size' do - it 'returns 1 MB' do - expect(described_class.human_default_max_size).to eq('1 MB') - end - end end diff --git a/spec/lib/gitlab/utils/delegator_override_spec.rb b/spec/lib/gitlab/utils/delegator_override_spec.rb index af4c7fa5d8e..2dafa75e344 100644 --- a/spec/lib/gitlab/utils/delegator_override_spec.rb +++ b/spec/lib/gitlab/utils/delegator_override_spec.rb @@ -31,6 +31,7 @@ RSpec.describe Gitlab::Utils::DelegatorOverride do before do stub_env('STATIC_VERIFICATION', 'true') + described_class.validators.clear end describe '.delegator_target' do diff --git a/spec/lib/gitlab/utils/execution_tracker_spec.rb b/spec/lib/gitlab/utils/execution_tracker_spec.rb new file mode 100644 index 00000000000..6c42863658c --- /dev/null +++ b/spec/lib/gitlab/utils/execution_tracker_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Utils::ExecutionTracker do + subject(:tracker) { described_class.new } + + describe '#over_limit?' do + it 'is true when max runtime is exceeded' do + monotonic_time_before = 1 # this will be the start time + monotonic_time_after = described_class::MAX_RUNTIME.to_i + 1 # this will be returned when over_limit? is called + + allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after) + + tracker + + expect(tracker).to be_over_limit + end + + it 'is false when max runtime is not exceeded' do + expect(tracker).not_to be_over_limit + end + end +end diff --git a/spec/lib/gitlab/utils/json_size_estimator_spec.rb b/spec/lib/gitlab/utils/json_size_estimator_spec.rb index 5fd66caa5e9..ba49cc3a847 100644 --- a/spec/lib/gitlab/utils/json_size_estimator_spec.rb +++ b/spec/lib/gitlab/utils/json_size_estimator_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Utils::JsonSizeEstimator do RSpec::Matchers.define :match_json_bytesize_of do |expected| diff --git a/spec/lib/gitlab/utils/markdown_spec.rb b/spec/lib/gitlab/utils/markdown_spec.rb index acc5bd47c8c..0a7d1160bbc 100644 --- a/spec/lib/gitlab/utils/markdown_spec.rb +++ b/spec/lib/gitlab/utils/markdown_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Utils::Markdown do let(:klass) do diff --git a/spec/lib/gitlab/utils/merge_hash_spec.rb b/spec/lib/gitlab/utils/merge_hash_spec.rb index 11daa05c9ee..4eec6e83be2 100644 --- a/spec/lib/gitlab/utils/merge_hash_spec.rb +++ b/spec/lib/gitlab/utils/merge_hash_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Utils::MergeHash do describe '.crush' do it 'can flatten a hash to each element' do diff --git a/spec/lib/gitlab/utils/nokogiri_spec.rb b/spec/lib/gitlab/utils/nokogiri_spec.rb index 90f137f53c8..7b4c63f9168 100644 --- a/spec/lib/gitlab/utils/nokogiri_spec.rb +++ b/spec/lib/gitlab/utils/nokogiri_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require 'rspec-parameterized' RSpec.describe Gitlab::Utils::Nokogiri do describe '#css_to_xpath' do diff --git a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb index 3ab592dfc62..1fc10bc3aa8 100644 --- a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb +++ b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require 'html/pipeline' +require 'addressable' RSpec.describe Gitlab::Utils::SanitizeNodeLink do let(:klass) do diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index ad1a65ffae8..61323f0646b 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -174,7 +174,7 @@ RSpec.describe Gitlab::Utils do { 'TEST' => 'test', 'project_with_underscores' => 'project-with-underscores', - 'namespace/project' => 'namespace-project', + 'namespace/project' => 'namespace-project', 'a' * 70 => 'a' * 63, 'test_trailing_' => 'test-trailing' }.each do |original, expected| diff --git a/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb b/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb index 7d96adf95e8..8d4629bf48b 100644 --- a/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb +++ b/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb @@ -150,6 +150,29 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Terminal do } ) end + + context 'when the FF ci_variables_refactoring_to_variable is disabled' do + let(:entry_without_ff) { described_class.new(config, with_image_ports: true) } + + before do + stub_feature_flags(ci_variables_refactoring_to_variable: false) + entry_without_ff.compose! + end + + it 'returns correct value' do + expect(entry_without_ff.value) + .to eq( + tag_list: ['webide'], + job_variables: [{ key: 'KEY', value: 'value', public: true }], + options: { + image: { name: "image:1.0" }, + services: [{ name: "mysql" }], + before_script: %w[ls pwd], + script: ['sleep 100'] + } + ) + end + end end end end diff --git a/spec/lib/gitlab/word_diff/chunk_collection_spec.rb b/spec/lib/gitlab/word_diff/chunk_collection_spec.rb index 73e9ff3974a..f76c4213c19 100644 --- a/spec/lib/gitlab/word_diff/chunk_collection_spec.rb +++ b/spec/lib/gitlab/word_diff/chunk_collection_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::WordDiff::ChunkCollection do subject(:collection) { described_class.new } diff --git a/spec/lib/gitlab/word_diff/line_processor_spec.rb b/spec/lib/gitlab/word_diff/line_processor_spec.rb index f448f5b5eb6..7246ed772f8 100644 --- a/spec/lib/gitlab/word_diff/line_processor_spec.rb +++ b/spec/lib/gitlab/word_diff/line_processor_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::WordDiff::LineProcessor do subject(:line_processor) { described_class.new(line) } diff --git a/spec/lib/gitlab/word_diff/parser_spec.rb b/spec/lib/gitlab/word_diff/parser_spec.rb index e793e44fd45..18109a8160b 100644 --- a/spec/lib/gitlab/word_diff/parser_spec.rb +++ b/spec/lib/gitlab/word_diff/parser_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::WordDiff::Parser do subject(:parser) { described_class.new } @@ -42,18 +42,18 @@ RSpec.describe Gitlab::WordDiff::Parser do { index: 1, old_pos: 2, new_pos: 2, text: 'Unchanged line', type: nil, marker_ranges: [] }, { index: 2, old_pos: 3, new_pos: 3, text: '', type: nil, marker_ranges: [] }, { index: 3, old_pos: 4, new_pos: 4, text: 'Old changeNew addition unchanged content', type: nil, - marker_ranges: [ - Gitlab::MarkerRange.new(0, 9, mode: :deletion), - Gitlab::MarkerRange.new(10, 21, mode: :addition) - ] }, + marker_ranges: [ + Gitlab::MarkerRange.new(0, 9, mode: :deletion), + Gitlab::MarkerRange.new(10, 21, mode: :addition) + ] }, { index: 4, old_pos: 50, new_pos: 50, text: '@@ -50,14 +50,13 @@', type: 'match', marker_ranges: [] }, { index: 5, old_pos: 50, new_pos: 50, text: 'First change same same same_removed_added_end of the line', type: nil, - marker_ranges: [ - Gitlab::MarkerRange.new(0, 11, mode: :addition), - Gitlab::MarkerRange.new(28, 35, mode: :deletion), - Gitlab::MarkerRange.new(36, 41, mode: :addition) - ] }, + marker_ranges: [ + Gitlab::MarkerRange.new(0, 11, mode: :addition), + Gitlab::MarkerRange.new(28, 35, mode: :deletion), + Gitlab::MarkerRange.new(36, 41, mode: :addition) + ] }, { index: 6, old_pos: 51, new_pos: 51, text: '', type: nil, marker_ranges: [] } ] diff --git a/spec/lib/gitlab/word_diff/positions_counter_spec.rb b/spec/lib/gitlab/word_diff/positions_counter_spec.rb index e2c246f6801..32ce7c50591 100644 --- a/spec/lib/gitlab/word_diff/positions_counter_spec.rb +++ b/spec/lib/gitlab/word_diff/positions_counter_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::WordDiff::PositionsCounter do subject(:counter) { described_class.new } diff --git a/spec/lib/gitlab/word_diff/segments/chunk_spec.rb b/spec/lib/gitlab/word_diff/segments/chunk_spec.rb index 797cc42a03c..75c0e5b4a77 100644 --- a/spec/lib/gitlab/word_diff/segments/chunk_spec.rb +++ b/spec/lib/gitlab/word_diff/segments/chunk_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::WordDiff::Segments::Chunk do subject(:chunk) { described_class.new(line) } diff --git a/spec/lib/gitlab/word_diff/segments/diff_hunk_spec.rb b/spec/lib/gitlab/word_diff/segments/diff_hunk_spec.rb index 5250e6d73c2..a65f55c716f 100644 --- a/spec/lib/gitlab/word_diff/segments/diff_hunk_spec.rb +++ b/spec/lib/gitlab/word_diff/segments/diff_hunk_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::WordDiff::Segments::DiffHunk do subject(:diff_hunk) { described_class.new(line) } diff --git a/spec/lib/gitlab/word_diff/segments/newline_spec.rb b/spec/lib/gitlab/word_diff/segments/newline_spec.rb index ed5054844f1..4c0cf0c5ee4 100644 --- a/spec/lib/gitlab/word_diff/segments/newline_spec.rb +++ b/spec/lib/gitlab/word_diff/segments/newline_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::WordDiff::Segments::Newline do subject(:newline) { described_class.new } diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 703a4b5399e..5c9a3cc0a24 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -365,7 +365,7 @@ RSpec.describe Gitlab::Workhorse do it 'set and notify' do expect(Gitlab::Redis::SharedState).to receive(:with).and_call_original expect_any_instance_of(::Redis).to receive(:publish) - .with(described_class::NOTIFICATION_CHANNEL, "test-key=test-value") + .with(described_class::NOTIFICATION_PREFIX + 'test-key', "test-value") subject end diff --git a/spec/lib/gitlab_edition_spec.rb b/spec/lib/gitlab_edition_spec.rb index 6fc4312252d..46be1471896 100644 --- a/spec/lib/gitlab_edition_spec.rb +++ b/spec/lib/gitlab_edition_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require 'rspec-parameterized' RSpec.describe GitlabEdition do def remove_instance_variable(ivar) @@ -27,7 +28,57 @@ RSpec.describe GitlabEdition do end end - describe 'extensions' do + describe '.path_glob' do + using RSpec::Parameterized::TableSyntax + + let(:root) { described_class.root.to_s } + + subject { described_class.path_glob(path) } + + before do + allow(described_class).to receive(:jh?).and_return(jh) + allow(described_class).to receive(:ee?).and_return(ee) + end + + where(:ee, :jh, :path, :expected) do + false | false | nil | '' + true | false | nil | '{,ee/}' + true | true | nil | '{,ee/,jh/}' + false | true | nil | '{,ee/,jh/}' + false | false | 'app/models' | 'app/models' + true | false | 'app/models' | '{,ee/}app/models' + true | true | 'app/models' | '{,ee/,jh/}app/models' + false | true | 'app/models' | '{,ee/,jh/}app/models' + end + + with_them do + it { is_expected.to eq("#{root}/#{expected}") } + end + end + + describe '.extension_path_prefixes' do + using RSpec::Parameterized::TableSyntax + + subject { described_class.extension_path_prefixes } + + before do + allow(described_class).to receive(:jh?).and_return(jh) + allow(described_class).to receive(:ee?).and_return(ee) + end + + where(:ee, :jh, :expected) do + false | false | '' + true | false | '{,ee/}' + true | true | '{,ee/,jh/}' + false | true | '{,ee/,jh/}' + end + + with_them do + it { is_expected.to eq(expected) } + end + end + + describe '.extensions' do context 'when .jh? is true' do before do allow(described_class).to receive(:jh?).and_return(true) diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb index aeca7b09a88..0f117f495d1 100644 --- a/spec/lib/google_api/cloud_platform/client_spec.rb +++ b/spec/lib/google_api/cloud_platform/client_spec.rb @@ -306,7 +306,7 @@ RSpec.describe GoogleApi::CloudPlatform::Client do .with({ 'role': 'roles/storage.admin', 'members': ["serviceAccount:#{mock_email}"] }) expect(Google::Apis::CloudresourcemanagerV1::Binding).to receive(:new) - .with({ 'role': 'roles/cloudsql.admin', 'members': ["serviceAccount:#{mock_email}"] }) + .with({ 'role': 'roles/cloudsql.client', 'members': ["serviceAccount:#{mock_email}"] }) expect(Google::Apis::CloudresourcemanagerV1::Binding).to receive(:new) .with({ 'role': 'roles/browser', 'members': ["serviceAccount:#{mock_email}"] }) diff --git a/spec/lib/learn_gitlab/onboarding_spec.rb b/spec/lib/learn_gitlab/onboarding_spec.rb deleted file mode 100644 index 3e22ce59091..00000000000 --- a/spec/lib/learn_gitlab/onboarding_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe LearnGitlab::Onboarding do - describe '#completed_percentage' do - let(:completed_actions) { {} } - let(:onboarding_progress) { build(:onboarding_progress, namespace: namespace, **completed_actions) } - let(:namespace) { create(:namespace) } - - let_it_be(:tracked_action_columns) do - [ - *described_class::ACTION_ISSUE_IDS.keys, - *described_class::ACTION_PATHS, - :security_scan_enabled - ].map { |key| OnboardingProgress.column_name(key) } - end - - before do - expect(OnboardingProgress).to receive(:find_by).with(namespace: namespace).and_return(onboarding_progress) - end - - subject { described_class.new(namespace).completed_percentage } - - context 'when no onboarding_progress exists' do - let(:onboarding_progress) { nil } - - it { is_expected.to eq(0) } - end - - context 'when no action has been completed' do - it { is_expected.to eq(0) } - end - - context 'when all tracked actions have been completed' do - let(:completed_actions) do - tracked_action_columns.to_h { |action| [action, Time.current] } - end - - it { is_expected.to eq(100) } - end - - describe 'security_actions_continuous_onboarding experiment' do - let(:completed_actions) { Hash[tracked_action_columns.first, Time.current] } - - context 'when control' do - before do - stub_experiments(security_actions_continuous_onboarding: :control) - end - - it { is_expected.to eq(11) } - end - - context 'when candidate' do - before do - stub_experiments(security_actions_continuous_onboarding: :candidate) - end - - it { is_expected.to eq(9) } - end - end - end -end diff --git a/spec/lib/learn_gitlab/project_spec.rb b/spec/lib/learn_gitlab/project_spec.rb deleted file mode 100644 index 23784709817..00000000000 --- a/spec/lib/learn_gitlab/project_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe LearnGitlab::Project do - let_it_be(:current_user) { create(:user) } - let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME) } - let_it_be(:learn_gitlab_board) { create(:board, project: learn_gitlab_project, name: LearnGitlab::Project::BOARD_NAME) } - let_it_be(:learn_gitlab_label) { create(:label, project: learn_gitlab_project, name: LearnGitlab::Project::LABEL_NAME) } - - before do - learn_gitlab_project.add_developer(current_user) - end - - describe '.available?' do - using RSpec::Parameterized::TableSyntax - - where(:project, :board, :label, :expected_result) do - nil | nil | nil | nil - nil | nil | true | nil - nil | true | nil | nil - nil | true | true | nil - true | nil | nil | nil - true | nil | true | nil - true | true | nil | nil - true | true | true | true - end - - with_them do - before do - allow_next_instance_of(described_class) do |learn_gitlab| - allow(learn_gitlab).to receive(:project).and_return(project) - allow(learn_gitlab).to receive(:board).and_return(board) - allow(learn_gitlab).to receive(:label).and_return(label) - end - end - - subject { described_class.new(current_user).available? } - - it { is_expected.to be expected_result } - end - end - - describe '.project' do - subject { described_class.new(current_user).project } - - it { is_expected.to eq learn_gitlab_project } - - context 'when it is created during trial signup' do - let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME_ULTIMATE_TRIAL, path: 'learn-gitlab-ultimate-trial') } - - it { is_expected.to eq learn_gitlab_project } - end - end - - describe '.board' do - subject { described_class.new(current_user).board } - - it { is_expected.to eq learn_gitlab_board } - end - - describe '.label' do - subject { described_class.new(current_user).label } - - it { is_expected.to eq learn_gitlab_label } - end -end diff --git a/spec/lib/marginalia_spec.rb b/spec/lib/marginalia_spec.rb index 59add4e8347..5f405e71d79 100644 --- a/spec/lib/marginalia_spec.rb +++ b/spec/lib/marginalia_spec.rb @@ -45,8 +45,8 @@ RSpec.describe 'Marginalia spec' do let(:component_map) do { - "application" => "test", - "endpoint_id" => "MarginaliaTestController#first_user", + "application" => "test", + "endpoint_id" => "MarginaliaTestController#first_user", "correlation_id" => correlation_id, "db_config_name" => "main" } @@ -62,8 +62,8 @@ RSpec.describe 'Marginalia spec' do let(:recorded) { ActiveRecord::QueryRecorder.new { make_request(correlation_id, :first_ci_pipeline) } } let(:component_map) do { - "application" => "test", - "endpoint_id" => "MarginaliaTestController#first_ci_pipeline", + "application" => "test", + "endpoint_id" => "MarginaliaTestController#first_ci_pipeline", "correlation_id" => correlation_id, "db_config_name" => 'ci' } @@ -104,10 +104,10 @@ RSpec.describe 'Marginalia spec' do let(:component_map) do { - "application" => "sidekiq", - "endpoint_id" => "MarginaliaTestJob", + "application" => "sidekiq", + "endpoint_id" => "MarginaliaTestJob", "correlation_id" => sidekiq_job['correlation_id'], - "jid" => sidekiq_job['jid'], + "jid" => sidekiq_job['jid'], "db_config_name" => "main" } end @@ -129,9 +129,9 @@ RSpec.describe 'Marginalia spec' do let(:component_map) do { - "application" => "sidekiq", - "endpoint_id" => "ActionMailer::MailDeliveryJob", - "jid" => delivery_job.job_id, + "application" => "sidekiq", + "endpoint_id" => "ActionMailer::MailDeliveryJob", + "jid" => delivery_job.job_id, "db_config_name" => "main" } end diff --git a/spec/lib/microsoft_teams/activity_spec.rb b/spec/lib/microsoft_teams/activity_spec.rb index d1eac7204a6..08f71985a2a 100644 --- a/spec/lib/microsoft_teams/activity_spec.rb +++ b/spec/lib/microsoft_teams/activity_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe MicrosoftTeams::Activity do subject { described_class.new(title: 'title', subtitle: 'subtitle', text: 'text', image: 'image') } diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb index 18a58522d12..1629aec89f5 100644 --- a/spec/lib/object_storage/direct_upload_spec.rb +++ b/spec/lib/object_storage/direct_upload_spec.rb @@ -342,84 +342,68 @@ RSpec.describe ObjectStorage::DirectUpload do context 'when length is unknown' do let(:has_length) { false } - context 'when s3_omit_multipart_urls feature flag is enabled' do - let(:consolidated_settings) { true } - - it 'omits multipart URLs' do - expect(subject).not_to have_key(:MultipartUpload) - end - - it_behaves_like 'a valid upload' - end - - context 'when s3_omit_multipart_urls feature flag is disabled' do + it_behaves_like 'a valid S3 upload with multipart data' do before do - stub_feature_flags(s3_omit_multipart_urls: false) + stub_object_storage_multipart_init(storage_url, "myUpload") end - it_behaves_like 'a valid S3 upload with multipart data' do - before do - stub_object_storage_multipart_init(storage_url, "myUpload") - end - - context 'when maximum upload size is 0' do - let(:maximum_size) { 0 } + context 'when maximum upload size is 0' do + let(:maximum_size) { 0 } - it 'returns maximum number of parts' do - expect(subject[:MultipartUpload][:PartURLs].length).to eq(100) - end + it 'returns maximum number of parts' do + expect(subject[:MultipartUpload][:PartURLs].length).to eq(100) + end - it 'part size is minimum, 5MB' do - expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) - end + it 'part size is minimum, 5MB' do + expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) end + end - context 'when maximum upload size is < 5 MB' do - let(:maximum_size) { 1024 } + context 'when maximum upload size is < 5 MB' do + let(:maximum_size) { 1024 } - it 'returns only 1 part' do - expect(subject[:MultipartUpload][:PartURLs].length).to eq(1) - end + it 'returns only 1 part' do + expect(subject[:MultipartUpload][:PartURLs].length).to eq(1) + end - it 'part size is minimum, 5MB' do - expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) - end + it 'part size is minimum, 5MB' do + expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) end + end - context 'when maximum upload size is 10MB' do - let(:maximum_size) { 10.megabyte } + context 'when maximum upload size is 10MB' do + let(:maximum_size) { 10.megabyte } - it 'returns only 2 parts' do - expect(subject[:MultipartUpload][:PartURLs].length).to eq(2) - end + it 'returns only 2 parts' do + expect(subject[:MultipartUpload][:PartURLs].length).to eq(2) + end - it 'part size is minimum, 5MB' do - expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) - end + it 'part size is minimum, 5MB' do + expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) end + end - context 'when maximum upload size is 12MB' do - let(:maximum_size) { 12.megabyte } + context 'when maximum upload size is 12MB' do + let(:maximum_size) { 12.megabyte } - it 'returns only 3 parts' do - expect(subject[:MultipartUpload][:PartURLs].length).to eq(3) - end + it 'returns only 3 parts' do + expect(subject[:MultipartUpload][:PartURLs].length).to eq(3) + end - it 'part size is rounded-up to 5MB' do - expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) - end + it 'part size is rounded-up to 5MB' do + expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) end + end - context 'when maximum upload size is 49GB' do - let(:maximum_size) { 49.gigabyte } + context 'when maximum upload size is 49GB' do + let(:maximum_size) { 49.gigabyte } - it 'returns maximum, 100 parts' do - expect(subject[:MultipartUpload][:PartURLs].length).to eq(100) - end + it 'returns maximum, 100 parts' do + expect(subject[:MultipartUpload][:PartURLs].length).to eq(100) + end - it 'part size is rounded-up to 5MB' do - expect(subject[:MultipartUpload][:PartSize]).to eq(505.megabyte) - end + it 'part size is rounded-up to 5MB' do + expect(subject[:MultipartUpload][:PartSize]).to eq(505.megabyte) end end end diff --git a/spec/lib/omni_auth/strategies/bitbucket_spec.rb b/spec/lib/omni_auth/strategies/bitbucket_spec.rb new file mode 100644 index 00000000000..d85ce71d60a --- /dev/null +++ b/spec/lib/omni_auth/strategies/bitbucket_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe OmniAuth::Strategies::Bitbucket do + subject { described_class.new({}) } + + describe '#callback_url' do + let(:base_url) { 'https://example.com' } + + context 'when script name is not present' do + it 'has the correct default callback path' do + allow(subject).to receive(:full_host) { base_url } + allow(subject).to receive(:script_name).and_return('') + allow(subject).to receive(:query_string).and_return('') + expect(subject.callback_url).to eq("#{base_url}/users/auth/bitbucket/callback") + end + end + + context 'when script name is present' do + it 'sets the callback path with script_name' do + allow(subject).to receive(:full_host) { base_url } + allow(subject).to receive(:script_name).and_return('/v1') + allow(subject).to receive(:query_string).and_return('') + expect(subject.callback_url).to eq("#{base_url}/v1/users/auth/bitbucket/callback") + end + end + end +end diff --git a/spec/lib/peek/views/redis_detailed_spec.rb b/spec/lib/peek/views/redis_detailed_spec.rb index a757af50dcb..5d75a6522e4 100644 --- a/spec/lib/peek/views/redis_detailed_spec.rb +++ b/spec/lib/peek/views/redis_detailed_spec.rb @@ -7,17 +7,19 @@ RSpec.describe Peek::Views::RedisDetailed, :request_store do using RSpec::Parameterized::TableSyntax - where(:cmd, :expected) do - [:auth, 'test'] | 'auth <redacted>' - [:set, 'key', 'value'] | 'set key <redacted>' - [:set, 'bad'] | 'set bad' - [:hmset, 'key1', 'value1', 'key2', 'value2'] | 'hmset key1 <redacted>' - [:get, 'key'] | 'get key' + where(:commands, :expected) do + [[:auth, 'test']] | 'auth <redacted>' + [[:set, 'key', 'value']] | 'set key <redacted>' + [[:set, 'bad']] | 'set bad' + [[:hmset, 'key1', 'value1', 'key2', 'value2']] | 'hmset key1 <redacted>' + [[:get, 'key']] | 'get key' + [[:get, 'key1'], [:get, 'key2']] | 'get key1, get key2' + [[:set, 'key1', 'value'], [:set, 'key2', 'value']] | 'set key1 <redacted>, set key2 <redacted>' end with_them do it 'scrubs Redis commands' do - Gitlab::Instrumentation::Redis::SharedState.detail_store << { cmd: cmd, duration: 1.second } + Gitlab::Instrumentation::Redis::SharedState.detail_store << { commands: commands, duration: 1.second } expect(subject.results[:details].count).to eq(1) expect(subject.results[:details].first) @@ -29,9 +31,9 @@ RSpec.describe Peek::Views::RedisDetailed, :request_store do end it 'returns aggregated results' do - Gitlab::Instrumentation::Redis::Cache.detail_store << { cmd: [:get, 'test'], duration: 0.001 } - Gitlab::Instrumentation::Redis::Cache.detail_store << { cmd: [:get, 'test'], duration: 1.second } - Gitlab::Instrumentation::Redis::SharedState.detail_store << { cmd: [:get, 'test'], duration: 1.second } + Gitlab::Instrumentation::Redis::Cache.detail_store << { commands: [[:get, 'test']], duration: 0.001 } + Gitlab::Instrumentation::Redis::Cache.detail_store << { commands: [[:get, 'test']], duration: 1.second } + Gitlab::Instrumentation::Redis::SharedState.detail_store << { commands: [[:get, 'test']], duration: 1.second } expect(subject.results[:calls]).to eq(3) expect(subject.results[:duration]).to eq('2001.00ms') diff --git a/spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb b/spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb index bdf9673a53f..f93066e82be 100644 --- a/spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb +++ b/spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'fast_spec_helper' +require 'tmpdir' RSpec.describe Prometheus::CleanupMultiprocDirService do describe '#execute' do diff --git a/spec/lib/security/ci_configuration/sast_build_action_spec.rb b/spec/lib/security/ci_configuration/sast_build_action_spec.rb index 611a886d252..381ea60e7f5 100644 --- a/spec/lib/security/ci_configuration/sast_build_action_spec.rb +++ b/spec/lib/security/ci_configuration/sast_build_action_spec.rb @@ -33,12 +33,12 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do params.merge( { analyzers: [ { - name: "bandit", - enabled: false + name: "bandit", + enabled: false }, { - name: "brakeman", - enabled: true, + name: "brakeman", + enabled: true, variables: [ { field: "SAST_BRAKEMAN_LEVEL", default_value: "1", @@ -46,8 +46,8 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do ] }, { - name: "flawfinder", - enabled: true, + name: "flawfinder", + enabled: true, variables: [ { field: "SAST_FLAWFINDER_LEVEL", default_value: "1", @@ -62,12 +62,12 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do params.merge( { analyzers: [ { - name: "flawfinder", - enabled: true + name: "flawfinder", + enabled: true }, { - name: "brakeman", - enabled: true + name: "brakeman", + enabled: true } ] } ) @@ -219,49 +219,49 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do def existing_gitlab_ci_and_template_array_without_sast { "stages" => %w(test security), - "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, - "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, - "include" => [{ "template" => "existing.yml" }] } + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, + "include" => [{ "template" => "existing.yml" }] } end def existing_gitlab_ci_and_single_template_with_sast_and_default_stage { "stages" => %w(test), - "variables" => { "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, - "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "test" }, - "include" => { "template" => "Security/SAST.gitlab-ci.yml" } } + "variables" => { "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "test" }, + "include" => { "template" => "Security/SAST.gitlab-ci.yml" } } end def existing_gitlab_ci_and_single_template_without_sast { "stages" => %w(test security), - "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, - "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, - "include" => { "template" => "existing.yml" } } + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, + "include" => { "template" => "existing.yml" } } end def existing_gitlab_ci_with_no_variables { "stages" => %w(test security), - "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, - "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } + "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, + "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } end def existing_gitlab_ci_with_no_sast_section { "stages" => %w(test security), - "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, - "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } end def existing_gitlab_ci_with_no_sast_variables { "stages" => %w(test security), - "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, - "sast" => { "stage" => "security" }, - "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "sast" => { "stage" => "security" }, + "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } end def existing_gitlab_ci { "stages" => %w(test security), - "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "bad_prefix" }, - "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, - "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "bad_prefix" }, + "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, + "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } end end diff --git a/spec/lib/security/report_schema_version_matcher_spec.rb b/spec/lib/security/report_schema_version_matcher_spec.rb index 9c40f0bc6fa..eaf49aa4744 100644 --- a/spec/lib/security/report_schema_version_matcher_spec.rb +++ b/spec/lib/security/report_schema_version_matcher_spec.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Security::ReportSchemaVersionMatcher do let(:vendored_versions) { %w[14.0.0 14.0.1 14.0.2 14.1.0] } diff --git a/spec/lib/security/weak_passwords_spec.rb b/spec/lib/security/weak_passwords_spec.rb new file mode 100644 index 00000000000..9d12c352abf --- /dev/null +++ b/spec/lib/security/weak_passwords_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Security::WeakPasswords do + describe "#weak_for_user?" do + using RSpec::Parameterized::TableSyntax + + let(:user) do + build_stubbed(:user, username: "56d4ab689a_win", + name: "Weakést McWeaky-Pass Jr", + email: "predictāble.ZZZ+seventeen@examplecorp.com", + public_email: "fortunate@acme.com" + ) + end + + where(:password, :too_weak) do + # A random password is not too weak + "d2262d56" | false + + # The case-insensitive weak password list + "password" | true + "pAssWord" | true + "princeofdarkness" | true + + # Forbidden substrings + "A1B2gitlabC3" | true + "gitlab123" | true + "theonedevopsplatform" | true + "A1gitlib" | false + + # Predicatable name substrings + "Aweakést" | true + "!@mCwEaKy" | true + "A1B2pass" | true + "A1B2C3jr" | false # jr is too short + + # Predictable username substrings + "56d4ab689a" | true + "56d4ab689a_win" | true + "56d4ab68" | false # it's part of the username, but not a full part + "A1B2Cwin" | false # win is too short + + # Predictable user.email substrings + "predictāble.ZZZ+seventeen@examplecorp.com" | true + "predictable.ZZZ+seventeen@examplecorp.com" | true + "predictāble.ZZZ+seventeen" | true + "examplecorp.com" | true + "!@exAmplecorp" | true + "predictāble123" | true + "seventeen" | true + "predictable" | false # the accent is different + "A1B2CZzZ" | false # ZZZ is too short + # Other emails are not considered + "fortunate@acme.com" | false + "A1B2acme" | false + "fortunate" | false + + # A short password is not automatically too weak + # We rely on User's password length validation, not WeakPasswords. + "1" | false + "1234567" | false + # But a short password with forbidden words or user attributes + # is still weak + "gitlab" | true + "pass" | true + end + + with_them do + it { expect(subject.weak_for_user?(password, user)).to eq(too_weak) } + end + + context 'with a user who has short email parts' do + before do + user.email = 'sid@1.io' + end + + where(:password, :too_weak) do + "11111111" | true # This is on the weak password list + "1.ioABCD" | true # 1.io is long enough to match + "sid@1.io" | true # matches the email in full + "sid@1.ioAB" | true + # sid, 1, and io on their own are too short + "sid1ioAB" | false + "sidsidsi" | false + "ioioioio" | false + end + + with_them do + it { expect(subject.weak_for_user?(password, user)).to eq(too_weak) } + end + end + + context 'with a user who is missing attributes' do + before do + user.name = nil + user.email = nil + user.username = nil + end + + where(:password, :too_weak) do + "d2262d56" | false + "password" | true + "gitlab123" | true + end + + with_them do + it { expect(subject.weak_for_user?(password, user)).to eq(too_weak) } + end + end + end +end diff --git a/spec/lib/sidebars/concerns/container_with_html_options_spec.rb b/spec/lib/sidebars/concerns/container_with_html_options_spec.rb index 7f834419866..d95cdb9e0fe 100644 --- a/spec/lib/sidebars/concerns/container_with_html_options_spec.rb +++ b/spec/lib/sidebars/concerns/container_with_html_options_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Sidebars::Concerns::ContainerWithHtmlOptions do subject do diff --git a/spec/lib/sidebars/concerns/link_with_html_options_spec.rb b/spec/lib/sidebars/concerns/link_with_html_options_spec.rb index 1e890bffad1..f7e6701c37d 100644 --- a/spec/lib/sidebars/concerns/link_with_html_options_spec.rb +++ b/spec/lib/sidebars/concerns/link_with_html_options_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Sidebars::Concerns::LinkWithHtmlOptions do let(:options) { {} } diff --git a/spec/lib/sidebars/groups/menus/observability_menu_spec.rb b/spec/lib/sidebars/groups/menus/observability_menu_spec.rb new file mode 100644 index 00000000000..3a91b1aea2f --- /dev/null +++ b/spec/lib/sidebars/groups/menus/observability_menu_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Groups::Menus::ObservabilityMenu do + let_it_be(:owner) { create(:user) } + let_it_be(:root_group) do + build(:group, :private).tap do |g| + g.add_owner(owner) + end + end + + let(:group) { root_group } + let(:user) { owner } + let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) } + let(:menu) { described_class.new(context) } + + describe '#render?' do + before do + allow(menu).to receive(:can?).and_call_original + end + + context 'when user can :read_observability' do + before do + allow(menu).to receive(:can?).with(user, :read_observability, group).and_return(true) + end + + it 'returns true' do + expect(menu.render?).to eq true + end + end + + context 'when user cannot :read_observability' do + before do + allow(menu).to receive(:can?).with(user, :read_observability, group).and_return(false) + end + + it 'returns false' do + expect(menu.render?).to eq false + end + end + end +end diff --git a/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb index d3cb18222b5..c5666724acf 100644 --- a/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb +++ b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb @@ -10,6 +10,8 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do end end + let_it_be(:harbor_integration) { create(:harbor_integration, group: group, project: nil) } + let(:user) { owner } let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) } let(:menu) { described_class.new(context) } diff --git a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb index 252da8ea699..4e3c639672b 100644 --- a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb +++ b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb @@ -80,7 +80,7 @@ RSpec.describe Sidebars::Groups::Menus::SettingsMenu do it_behaves_like 'access rights checks' end - describe 'Packages & Registries' do + describe 'Packages and registries' do let(:item_id) { :packages_and_registries } before do diff --git a/spec/lib/sidebars/menu_item_spec.rb b/spec/lib/sidebars/menu_item_spec.rb index 3adde64f550..15804f51934 100644 --- a/spec/lib/sidebars/menu_item_spec.rb +++ b/spec/lib/sidebars/menu_item_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Sidebars::MenuItem do let(:title) { 'foo' } diff --git a/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb index 56eb082e101..90ff04a2064 100644 --- a/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Sidebars::Projects::Menus::DeploymentsMenu do - let_it_be(:project) { create(:project, :repository) } + let_it_be(:project, reload: true) { create(:project, :repository) } let(:user) { project.first_owner } let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } @@ -37,6 +37,40 @@ RSpec.describe Sidebars::Projects::Menus::DeploymentsMenu do specify { is_expected.to be_nil } end + + describe 'when the feature is disabled' do + before do + project.update_attribute("#{item_id}_access_level", 'disabled') + end + + it { is_expected.to be_nil } + end + + describe 'when split_operations_visibility_permissions FF is disabled' do + before do + stub_feature_flags(split_operations_visibility_permissions: false) + end + + it { is_expected.not_to be_nil } + + context 'and the feature is disabled' do + before do + project.update_attribute("#{item_id}_access_level", 'disabled') + end + + it { is_expected.not_to be_nil } + end + + context 'and operations is disabled' do + before do + project.update_attribute(:operations_access_level, 'disabled') + end + + it do + is_expected.to be_nil if [:environments, :feature_flags].include?(item_id) + end + end + end end describe 'Feature Flags' do diff --git a/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb b/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb index 36a76e70a48..4ae29f28f3a 100644 --- a/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb @@ -68,13 +68,11 @@ RSpec.describe Sidebars::Projects::Menus::LearnGitlabMenu do end describe '#pill_count' do - before do - expect_next_instance_of(LearnGitlab::Onboarding) do |onboarding| - expect(onboarding).to receive(:completed_percentage).and_return(20) + it 'returns pill count' do + expect_next_instance_of(Onboarding::Completion) do |onboarding| + expect(onboarding).to receive(:percentage).and_return(20) end - end - it 'returns pill count' do expect(subject.pill_count).to eq '20%' end end diff --git a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb index ba5137e2b92..bd0904b9db2 100644 --- a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb @@ -12,11 +12,28 @@ RSpec.describe Sidebars::Projects::Menus::MonitorMenu do subject { described_class.new(context) } describe '#render?' do - context 'when operations feature is disabled' do - it 'returns false' do - project.project_feature.update!(operations_access_level: Featurable::DISABLED) + using RSpec::Parameterized::TableSyntax + let(:enabled) { Featurable::PRIVATE } + let(:disabled) { Featurable::DISABLED } + + where(:flag_enabled, :operations_access_level, :monitor_level, :render) do + true | ref(:disabled) | ref(:enabled) | true + true | ref(:disabled) | ref(:disabled) | false + true | ref(:enabled) | ref(:enabled) | true + true | ref(:enabled) | ref(:disabled) | false + false | ref(:disabled) | ref(:enabled) | false + false | ref(:disabled) | ref(:disabled) | false + false | ref(:enabled) | ref(:enabled) | true + false | ref(:enabled) | ref(:disabled) | true + end + + with_them do + it 'renders when expected to' do + stub_feature_flags(split_operations_visibility_permissions: flag_enabled) + project.project_feature.update!(operations_access_level: operations_access_level) + project.project_feature.update!(monitor_access_level: monitor_level) - expect(subject.render?).to be false + expect(subject.render?).to be render end end diff --git a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb index 9b78fc807bf..6491ef823e9 100644 --- a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb @@ -5,6 +5,8 @@ require 'spec_helper' RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do let_it_be(:project) { create(:project) } + let_it_be(:harbor_integration) { create(:harbor_integration, project: project) } + let(:user) { project.first_owner } let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } @@ -65,7 +67,7 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do describe 'Packages Registry' do let(:item_id) { :packages_registry } - context 'when user can read packages' do + shared_examples 'when user can read packages' do context 'when config package setting is disabled' do it 'the menu item is not added to list of menu items' do stub_config(packages: { enabled: false }) @@ -83,13 +85,25 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do end end - context 'when user cannot read packages' do + shared_examples 'when user cannot read packages' do let(:user) { nil } it 'the menu item is not added to list of menu items' do is_expected.to be_nil end end + + it_behaves_like 'when user can read packages' + it_behaves_like 'when user cannot read packages' + + context 'with feature flag disabled' do + before do + stub_feature_flags(read_package_policy_rule: false) + end + + it_behaves_like 'when user can read packages' + it_behaves_like 'when user cannot read packages' + end end describe 'Container Registry' do diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb index f41f7a01d88..0733e0c6521 100644 --- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb @@ -133,7 +133,13 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do end end - describe 'Packages & Registries' do + describe 'Merge requests' do + let(:item_id) { :merge_requests } + + it_behaves_like 'access rights checks' + end + + describe 'Packages and registries' do let(:item_id) { :packages_and_registries } let(:packages_enabled) { false } diff --git a/spec/lib/system_check/base_check_spec.rb b/spec/lib/system_check/base_check_spec.rb index 59b2fc519ae..241c3b33777 100644 --- a/spec/lib/system_check/base_check_spec.rb +++ b/spec/lib/system_check/base_check_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe SystemCheck::BaseCheck do context 'helpers on instance level' do |