diff options
Diffstat (limited to 'spec/lib')
293 files changed, 6490 insertions, 3193 deletions
diff --git a/spec/lib/api/entities/clusters/agent_spec.rb b/spec/lib/api/entities/clusters/agent_spec.rb new file mode 100644 index 00000000000..04f7ec28407 --- /dev/null +++ b/spec/lib/api/entities/clusters/agent_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::Clusters::Agent do + let_it_be(:cluster_agent) { create(:cluster_agent) } + + subject { described_class.new(cluster_agent).as_json } + + it 'includes basic fields' do + expect(subject).to include( + id: cluster_agent.id, + config_project: a_hash_including(id: cluster_agent.project_id) + ) + end +end diff --git a/spec/lib/api/entities/design_management/design_spec.rb b/spec/lib/api/entities/design_management/design_spec.rb index fe449e3e9bc..fe2b1dadfa7 100644 --- a/spec/lib/api/entities/design_management/design_spec.rb +++ b/spec/lib/api/entities/design_management/design_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe API::Entities::DesignManagement::Design do let_it_be(:design) { create(:design) } + let(:entity) { described_class.new(design, request: double) } subject { entity.as_json } diff --git a/spec/lib/api/entities/merge_request_changes_spec.rb b/spec/lib/api/entities/merge_request_changes_spec.rb index f46d8981328..29bfd1da6cc 100644 --- a/spec/lib/api/entities/merge_request_changes_spec.rb +++ b/spec/lib/api/entities/merge_request_changes_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe ::API::Entities::MergeRequestChanges do let_it_be(:user) { create(:user) } let_it_be(:merge_request) { create(:merge_request) } + let(:entity) { described_class.new(merge_request, current_user: user) } subject(:basic_entity) { entity.as_json } diff --git a/spec/lib/api/entities/project_import_failed_relation_spec.rb b/spec/lib/api/entities/project_import_failed_relation_spec.rb index 51a684c4564..d3c24f6fce3 100644 --- a/spec/lib/api/entities/project_import_failed_relation_spec.rb +++ b/spec/lib/api/entities/project_import_failed_relation_spec.rb @@ -14,7 +14,7 @@ RSpec.describe API::Entities::ProjectImportFailedRelation do id: import_failure.id, created_at: import_failure.created_at, exception_class: import_failure.exception_class, - exception_message: import_failure.exception_message, + exception_message: nil, relation_name: import_failure.relation_key, source: import_failure.source ) diff --git a/spec/lib/api/entities/release_spec.rb b/spec/lib/api/entities/release_spec.rb index d57c283c1f4..06062634015 100644 --- a/spec/lib/api/entities/release_spec.rb +++ b/spec/lib/api/entities/release_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe API::Entities::Release do let_it_be(:project) { create(:project) } + let(:release) { create(:release, project: project) } let(:evidence) { release.evidences.first } let(:user) { create(:user) } diff --git a/spec/lib/api/helpers/authentication_spec.rb b/spec/lib/api/helpers/authentication_spec.rb index 461b0d2f6f9..eea5c10d4f8 100644 --- a/spec/lib/api/helpers/authentication_spec.rb +++ b/spec/lib/api/helpers/authentication_spec.rb @@ -7,6 +7,7 @@ RSpec.describe API::Helpers::Authentication do let_it_be(:project, reload: true) { create(:project, :public) } let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } + let_it_be(:ci_build) { create(:ci_build, :running, user: user) } describe 'class methods' do subject { Class.new.include(described_class::ClassMethods).new } @@ -176,6 +177,20 @@ RSpec.describe API::Helpers::Authentication do end end + describe '#ci_build_from_namespace_inheritable' do + subject { object.ci_build_from_namespace_inheritable } + + it 'returns #token_from_namespace_inheritable if it is a ci build' do + expect(object).to receive(:token_from_namespace_inheritable).and_return(ci_build) + expect(subject).to be(ci_build) + end + + it 'returns nil if #token_from_namespace_inheritable is not a ci build' do + expect(object).to receive(:token_from_namespace_inheritable).and_return(personal_access_token) + expect(subject).to eq(nil) + end + end + describe '#user_from_namespace_inheritable' do subject { object.user_from_namespace_inheritable } diff --git a/spec/lib/api/helpers/caching_spec.rb b/spec/lib/api/helpers/caching_spec.rb new file mode 100644 index 00000000000..a8cd061e123 --- /dev/null +++ b/spec/lib/api/helpers/caching_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe API::Helpers::Caching do + subject(:instance) { Class.new.include(described_class).new } + + describe "#present_cached" do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:presenter) { API::Entities::Todo } + + let(:kwargs) do + { + with: presenter, + project: project + } + end + + subject do + instance.present_cached(presentable, **kwargs) + end + + before do + # We have to stub #body as it's a Grape method + # unavailable in the module by itself + expect(instance).to receive(:body) do |data| + data + end + + allow(instance).to receive(:current_user) { user } + end + + context "single object" do + let_it_be(:presentable) { create(:todo, project: project) } + + it { is_expected.to be_a(Gitlab::Json::PrecompiledJson) } + + it "uses the presenter" do + expect(presenter).to receive(:represent).with(presentable, project: project) + + subject + end + + it "is valid JSON" do + parsed = Gitlab::Json.parse(subject.to_s) + + expect(parsed).to be_a(Hash) + expect(parsed["id"]).to eq(presentable.id) + end + + it "fetches from the cache" do + expect(instance.cache).to receive(:fetch).with("#{presentable.cache_key}:#{user.cache_key}", expires_in: described_class::DEFAULT_EXPIRY).once + + subject + end + + context "when a cache context is supplied" do + before do + kwargs[:cache_context] = -> (todo) { todo.project.cache_key } + end + + it "uses the context to augment the cache key" do + expect(instance.cache).to receive(:fetch).with("#{presentable.cache_key}:#{project.cache_key}", expires_in: described_class::DEFAULT_EXPIRY).once + + subject + end + end + + context "when expires_in is supplied" do + it "sets the expiry when accessing the cache" do + kwargs[:expires_in] = 7.days + + expect(instance.cache).to receive(:fetch).with("#{presentable.cache_key}:#{user.cache_key}", expires_in: 7.days).once + + subject + end + end + end + + context "for a collection of objects" do + let_it_be(:presentable) { Array.new(5).map { create(:todo, project: project) } } + + it { is_expected.to be_an(Gitlab::Json::PrecompiledJson) } + + it "uses the presenter" do + presentable.each do |todo| + expect(presenter).to receive(:represent).with(todo, project: project) + end + + subject + end + + it "is valid JSON" do + parsed = Gitlab::Json.parse(subject.to_s) + + expect(parsed).to be_an(Array) + + presentable.each_with_index do |todo, i| + expect(parsed[i]["id"]).to eq(todo.id) + end + end + + it "fetches from the cache" do + keys = presentable.map { |todo| "#{todo.cache_key}:#{user.cache_key}" } + + expect(instance.cache).to receive(:fetch_multi).with(*keys, expires_in: described_class::DEFAULT_EXPIRY).once.and_call_original + + subject + end + + context "when a cache context is supplied" do + before do + kwargs[:cache_context] = -> (todo) { todo.project.cache_key } + end + + it "uses the context to augment the cache key" do + keys = presentable.map { |todo| "#{todo.cache_key}:#{project.cache_key}" } + + expect(instance.cache).to receive(:fetch_multi).with(*keys, expires_in: described_class::DEFAULT_EXPIRY).once.and_call_original + + subject + end + end + + context "expires_in is supplied" do + it "sets the expiry when accessing the cache" do + keys = presentable.map { |todo| "#{todo.cache_key}:#{user.cache_key}" } + kwargs[:expires_in] = 7.days + + expect(instance.cache).to receive(:fetch_multi).with(*keys, expires_in: 7.days).once.and_call_original + + subject + end + end + end + end +end 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 6d06fc3618d..99b52236771 100644 --- a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb +++ b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb @@ -12,6 +12,10 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do subject { helper.redirect_registry_request(forward_to_registry, package_type, options) { helper.fallback } } + before do + allow(helper).to receive(:options).and_return(for: API::NpmInstancePackages) + end + shared_examples 'executing fallback' do it 'redirects to package registry' do expect(helper).to receive(:registry_url).never @@ -23,13 +27,14 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do end shared_examples 'executing redirect' do - it 'redirects to package registry' do - expect(helper).to receive(:track_event).with('npm_request_forward').once + 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 subject + + expect_snowplow_event(category: 'API::NpmInstancePackages', action: 'npm_request_forward') end end @@ -64,7 +69,6 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do let(:package_type) { pkg_type } it 'raises an error' do - allow(helper).to receive(:track_event) expect { subject }.to raise_error(ArgumentError, "Can't build registry_url for package_type #{package_type}") end end diff --git a/spec/lib/api/helpers/packages_manager_clients_helpers_spec.rb b/spec/lib/api/helpers/packages_manager_clients_helpers_spec.rb index 3c40859da21..e4c5002aa68 100644 --- a/spec/lib/api/helpers/packages_manager_clients_helpers_spec.rb +++ b/spec/lib/api/helpers/packages_manager_clients_helpers_spec.rb @@ -8,6 +8,7 @@ RSpec.describe API::Helpers::PackagesManagerClientsHelpers do let_it_be(:personal_access_token) { create(:personal_access_token) } let_it_be(:username) { personal_access_token.user.username } let_it_be(:helper) { Class.new.include(described_class).new } + let(:password) { personal_access_token.token } let(:env) do @@ -50,6 +51,7 @@ RSpec.describe API::Helpers::PackagesManagerClientsHelpers do describe '#find_job_from_http_basic_auth' do let_it_be(:user) { personal_access_token.user } + let(:job) { create(:ci_build, user: user, status: :running) } let(:password) { job.token } @@ -74,6 +76,7 @@ RSpec.describe API::Helpers::PackagesManagerClientsHelpers do describe '#find_deploy_token_from_http_basic_auth' do let_it_be(:deploy_token) { create(:deploy_token) } + let(:token) { deploy_token.token } let(:username) { deploy_token.username } let(:password) { token } diff --git a/spec/lib/api/helpers/variables_helpers_spec.rb b/spec/lib/api/helpers/variables_helpers_spec.rb new file mode 100644 index 00000000000..de6bebaa827 --- /dev/null +++ b/spec/lib/api/helpers/variables_helpers_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Helpers::VariablesHelpers do + let(:helper) { Class.new.include(described_class).new } + + describe '#filter_variable_parameters' do + let(:project) { double } + let(:params) { double } + + subject { helper.filter_variable_parameters(project, params) } + + it 'returns unmodified params (overridden in EE)' do + expect(subject).to eq(params) + end + end + + describe '#find_variable' do + let(:owner) { double } + let(:params) { double } + let(:variables) { [double] } + + subject { helper.find_variable(owner, params) } + + before do + expect(Ci::VariablesFinder).to receive(:new).with(owner, params) + .and_return(double(execute: variables)) + end + + it { is_expected.to eq(variables.first) } + + context 'there are multiple variables with the supplied key' do + let(:variables) { [double, double] } + + it 'raises a conflict!' do + expect(helper).to receive(:conflict!).with(/There are multiple variables with provided parameters/) + + subject + end + end + end +end diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb index bdf04fafaae..15b22fcf25e 100644 --- a/spec/lib/api/helpers_spec.rb +++ b/spec/lib/api/helpers_spec.rb @@ -47,6 +47,58 @@ RSpec.describe API::Helpers do end end + describe '#find_project!' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + + shared_examples 'private project without access' do + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value('private')) + allow(subject).to receive(:authenticate_non_public?).and_return(false) + end + + it 'returns not found' do + expect(subject).to receive(:not_found!) + + subject.find_project!(project.id) + end + end + + context 'when user is authenticated' do + before do + subject.instance_variable_set(:@current_user, user) + subject.instance_variable_set(:@initial_current_user, user) + end + + context 'public project' do + it 'returns requested project' do + expect(subject.find_project!(project.id)).to eq(project) + end + end + + context 'private project' do + it_behaves_like 'private project without access' + end + end + + context 'when user is not authenticated' do + before do + subject.instance_variable_set(:@current_user, nil) + subject.instance_variable_set(:@initial_current_user, nil) + end + + context 'public project' do + it 'returns requested project' do + expect(subject.find_project!(project.id)).to eq(project) + end + end + + context 'private project' do + it_behaves_like 'private project without access' + end + end + end + describe '#find_namespace' do let(:namespace) { create(:namespace) } @@ -175,64 +227,27 @@ RSpec.describe API::Helpers do end end - describe '#track_event' do - it "creates a gitlab tracking event", :snowplow do - subject.track_event('my_event', category: 'foo') - - expect_snowplow_event(category: 'foo', action: 'my_event') - end - - it "logs an exception" do - expect(Gitlab::AppLogger).to receive(:warn).with(/Tracking event failed/) - - subject.track_event('my_event', category: nil) - end - end - describe '#increment_unique_values' do let(:value) { '9f302fea-f828-4ca9-aef4-e10bd723c0b3' } let(:event_name) { 'g_compliance_dashboard' } let(:unknown_event) { 'unknown' } - let(:feature) { "usage_data_#{event_name}" } - - before do - skip_feature_flags_yaml_validation - end - context 'with feature enabled' do - before do - stub_feature_flags(feature => true) - end + it 'tracks redis hll event' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(event_name, values: value) - it 'tracks redis hll event' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(event_name, values: value) - - subject.increment_unique_values(event_name, value) - end - - it 'logs an exception for unknown event' do - expect(Gitlab::AppLogger).to receive(:warn).with("Redis tracking event failed for event: #{unknown_event}, message: Unknown event #{unknown_event}") - - subject.increment_unique_values(unknown_event, value) - end + subject.increment_unique_values(event_name, value) + end - it 'does not track event for nil values' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + it 'logs an exception for unknown event' do + expect(Gitlab::AppLogger).to receive(:warn).with("Redis tracking event failed for event: #{unknown_event}, message: Unknown event #{unknown_event}") - subject.increment_unique_values(unknown_event, nil) - end + subject.increment_unique_values(unknown_event, value) end - context 'with feature disabled' do - before do - stub_feature_flags(feature => false) - end - - it 'does not track event' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + it 'does not track event for nil values' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) - subject.increment_unique_values(event_name, value) - end + subject.increment_unique_values(unknown_event, nil) end end diff --git a/spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb index 872ba1ab43d..6399fc9053b 100644 --- a/spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb +++ b/spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::PullRequestEntity do end context 'with user_notes_count option' do - let(:user_notes_count) { merge_requests.map { |merge_request| [merge_request.id, 1] }.to_h } + let(:user_notes_count) { merge_requests.to_h { |merge_request| [merge_request.id, 1] } } subject { described_class.represent(merge_requests, user_notes_count: user_notes_count).as_json } diff --git a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb index 03a6cc34962..f7cb6b92b48 100644 --- a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb @@ -139,6 +139,12 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter do end context "structure" do + it 'starts with two newlines to separate with actual commit message' do + doc = filter(commit_message_html) + + expect(doc.xpath('pre').text).to start_with("\n\n") + end + it 'preserves the commit trailer structure' do doc = filter(commit_message_html) diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb index ec17bb26346..23626576c0c 100644 --- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb +++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb @@ -16,18 +16,14 @@ RSpec.describe Banzai::Filter::GollumTagsFilter do context 'linking internal images' do it 'creates img tag if image exists' do - gollum_file_double = double('Gollum::File', - mime_type: 'image/jpeg', - name: 'images/image.jpg', - path: 'images/image.jpg', - raw_data: '') - wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double) + blob = double(mime_type: 'image/jpeg', name: 'images/image.jpg', path: 'images/image.jpg', data: '') + wiki_file = Gitlab::Git::WikiFile.new(blob) expect(wiki).to receive(:find_file).with('images/image.jpg', load_content: false).and_return(wiki_file) tag = '[[images/image.jpg]]' doc = filter("See #{tag}", wiki: wiki) - expect(doc.at_css('img')['data-src']).to eq "#{wiki.wiki_base_path}/images/image.jpg" + expect(doc.at_css('img')['src']).to eq 'images/image.jpg' end it 'does not creates img tag if image does not exist' do @@ -45,7 +41,7 @@ RSpec.describe Banzai::Filter::GollumTagsFilter do tag = '[[http://example.com/image.jpg]]' doc = filter("See #{tag}", wiki: wiki) - expect(doc.at_css('img')['data-src']).to eq "http://example.com/image.jpg" + expect(doc.at_css('img')['src']).to eq "http://example.com/image.jpg" end it 'does not creates img tag for invalid URL' do diff --git a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb index 3c736b46131..9ccea1cc3e9 100644 --- a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Banzai::Filter::InlineMetricsRedactorFilter do include FilterSpecHelper let_it_be(:project) { create(:project) } + let(:url) { urls.metrics_dashboard_project_environment_url(project, 1, embedded: true) } let(:input) { %(<a href="#{url}">example</a>) } let(:doc) { filter(input) } @@ -38,6 +39,7 @@ RSpec.describe Banzai::Filter::InlineMetricsRedactorFilter do context 'for a cluster metric embed' do let_it_be(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [project]) } + let(:params) { [project.namespace.path, project.path, cluster.id] } let(:query_params) { { group: 'Cluster Health', title: 'CPU Usage', y_label: 'CPU (cores)' } } let(:url) { urls.metrics_dashboard_namespace_project_cluster_url(*params, **query_params, format: :json) } @@ -84,6 +86,7 @@ RSpec.describe Banzai::Filter::InlineMetricsRedactorFilter do context 'for an alert embed' do let_it_be(:alert) { create(:prometheus_alert, project: project) } + let(:url) do urls.metrics_dashboard_project_prometheus_alert_url( project, diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb index 9f6688f4f7d..6d22fa3a001 100644 --- a/spec/lib/banzai/filter/math_filter_spec.rb +++ b/spec/lib/banzai/filter/math_filter_spec.rb @@ -91,35 +91,35 @@ RSpec.describe Banzai::Filter::MathFilter do # Display math it 'adds data-math-style display attribute to display math' do - doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>') + 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 math" v-pre="true"><code>2+2</code></pre>') + 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 plaintext" v-pre="true"><code>2+2</code></pre>' + 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 plaintext math" v-pre="true"><code>2+2</code></pre>' + 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 math" v-pre="true"><code>2+2</code></pre>$') + 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 diff --git a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb index 797f1c8d52f..076c112ac87 100644 --- a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::AbstractReferenceFilter do +RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do let_it_be(:project) { create(:project) } let(:doc) { Nokogiri::HTML.fragment('') } diff --git a/spec/lib/banzai/filter/alert_reference_filter_spec.rb b/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb index c57a8a7321c..7c6b0cac24b 100644 --- a/spec/lib/banzai/filter/alert_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::AlertReferenceFilter do +RSpec.describe Banzai::Filter::References::AlertReferenceFilter do include FilterSpecHelper let_it_be(:project) { create(:project, :public) } diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/references/commit_range_reference_filter_spec.rb index f04d3212437..b235de06b30 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/commit_range_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::CommitRangeReferenceFilter do +RSpec.describe Banzai::Filter::References::CommitRangeReferenceFilter do include FilterSpecHelper let(:project) { create(:project, :public, :repository) } diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb index 925fd031d95..bee8e42d12e 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::CommitReferenceFilter do +RSpec.describe Banzai::Filter::References::CommitReferenceFilter do include FilterSpecHelper let(:project) { create(:project, :public, :repository) } diff --git a/spec/lib/banzai/filter/design_reference_filter_spec.rb b/spec/lib/banzai/filter/references/design_reference_filter_spec.rb index 847c398964a..52514ad17fc 100644 --- a/spec/lib/banzai/filter/design_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/design_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::DesignReferenceFilter do +RSpec.describe Banzai::Filter::References::DesignReferenceFilter do include FilterSpecHelper include DesignManagementTestHelpers diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb index 35ef2abfa63..3b274f98020 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do +RSpec.describe Banzai::Filter::References::ExternalIssueReferenceFilter do include FilterSpecHelper let_it_be_with_refind(:project) { create(:project) } @@ -184,6 +184,7 @@ RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do context "jira project" do let_it_be(:service) { create(:jira_service, project: project) } + let(:reference) { issue.to_reference } context "with right markdown" do diff --git a/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb b/spec/lib/banzai/filter/references/feature_flag_reference_filter_spec.rb index 2d7089853cf..c64b66f746e 100644 --- a/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/feature_flag_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::FeatureFlagReferenceFilter do +RSpec.describe Banzai::Filter::References::FeatureFlagReferenceFilter do include FilterSpecHelper let_it_be(:project) { create(:project, :public) } diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb index 4b8b575c1f0..b849355f6db 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::IssueReferenceFilter do +RSpec.describe Banzai::Filter::References::IssueReferenceFilter do include FilterSpecHelper include DesignManagementTestHelpers diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/references/label_reference_filter_spec.rb index 726ef8c57ab..db7dda96cad 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/label_reference_filter_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require 'html/pipeline' -RSpec.describe Banzai::Filter::LabelReferenceFilter do +RSpec.describe Banzai::Filter::References::LabelReferenceFilter do include FilterSpecHelper let(:project) { create(:project, :public, name: 'sample-project') } diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb index 811c2aca342..7a634b0b513 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::MergeRequestReferenceFilter do +RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter do include FilterSpecHelper let(:project) { create(:project, :public) } diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb index 276fa7952be..dafdc71ce64 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::MilestoneReferenceFilter do +RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter do include FilterSpecHelper let_it_be(:parent_group) { create(:group, :public) } diff --git a/spec/lib/banzai/filter/project_reference_filter_spec.rb b/spec/lib/banzai/filter/references/project_reference_filter_spec.rb index ac7a90a5893..7a77d57cd42 100644 --- a/spec/lib/banzai/filter/project_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/project_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::ProjectReferenceFilter do +RSpec.describe Banzai::Filter::References::ProjectReferenceFilter do include FilterSpecHelper def invalidate_reference(reference) diff --git a/spec/lib/banzai/filter/reference_filter_spec.rb b/spec/lib/banzai/filter/references/reference_filter_spec.rb index 2888965dbc4..4bcb41ef2a9 100644 --- a/spec/lib/banzai/filter/reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::ReferenceFilter do +RSpec.describe Banzai::Filter::References::ReferenceFilter do let(:project) { build_stubbed(:project) } describe '#each_node' do diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb index f23fbc5be88..32a706925ba 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::SnippetReferenceFilter do +RSpec.describe Banzai::Filter::References::SnippetReferenceFilter do include FilterSpecHelper let(:project) { create(:project, :public) } diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/references/user_reference_filter_spec.rb index b8baccf6658..e4703606b47 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/user_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::UserReferenceFilter do +RSpec.describe Banzai::Filter::References::UserReferenceFilter do include FilterSpecHelper def get_reference(user) diff --git a/spec/lib/banzai/filter/suggestion_filter_spec.rb b/spec/lib/banzai/filter/suggestion_filter_spec.rb index 7d6092e21e9..d74bac4898e 100644 --- a/spec/lib/banzai/filter/suggestion_filter_spec.rb +++ b/spec/lib/banzai/filter/suggestion_filter_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Banzai::Filter::SuggestionFilter do include FilterSpecHelper - let(:input) { %(<pre class="code highlight js-syntax-highlight suggestion"><code>foo\n</code></pre>) } + let(:input) { %(<pre class="code highlight js-syntax-highlight language-suggestion"><code>foo\n</code></pre>) } let(:default_context) do { suggestions_filter_enabled: true } end @@ -26,7 +26,7 @@ RSpec.describe Banzai::Filter::SuggestionFilter do context 'multi-line suggestions' do let(:data_attr) { Banzai::Filter::SyntaxHighlightFilter::LANG_PARAMS_ATTR } - let(:input) { %(<pre class="code highlight js-syntax-highlight suggestion" #{data_attr}="-3+2"><code>foo\n</code></pre>) } + let(:input) { %(<pre class="code highlight js-syntax-highlight language-suggestion" #{data_attr}="-3+2"><code>foo\n</code></pre>) } it 'element has correct data-lang-params' do doc = filter(input, default_context) diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb index 78f84ee44f7..16e30604c99 100644 --- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb +++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do it "highlights as plaintext" do result = filter('<pre><code>def fun end</code></pre>') - expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre>') end include_examples "XSS prevention", "" @@ -38,7 +38,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do it "highlights as that language" do result = filter('<pre><code lang="ruby">def fun end</code></pre>') - expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>') end include_examples "XSS prevention", "ruby" @@ -48,7 +48,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do it "highlights as plaintext" do result = filter('<pre><code lang="gnuplot">This is a test</code></pre>') - expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>') end include_examples "XSS prevention", "gnuplot" @@ -63,7 +63,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do it "highlights as plaintext but with the correct language attribute and class" do result = filter(%{<pre><code lang="#{lang}">This is a test</code></pre>}) - expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}) + expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}) end include_examples "XSS prevention", lang @@ -75,7 +75,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do it "includes data-lang-params tag with extra information" do result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}">This is a test</code></pre>}) - expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{lang}" lang="#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}) + expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}) end include_examples "XSS prevention", lang @@ -93,7 +93,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do it "delimits on the first appearance" do result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}#{delimiter}more-things">This is a test</code></pre>}) - expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{lang}" lang="#{lang}" #{data_attr}="#{lang_params}#{delimiter}more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}) + expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}#{delimiter}more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}) end end end diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb index 0f8c773c68d..9ca499be665 100644 --- a/spec/lib/banzai/filter/upload_link_filter_spec.rb +++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb @@ -35,6 +35,7 @@ RSpec.describe Banzai::Filter::UploadLinkFilter do let_it_be(:project) { create(:project, :public) } let_it_be(:user) { create(:user) } + let(:group) { nil } let(:project_path) { project.full_path } let(:only_path) { true } @@ -114,6 +115,7 @@ RSpec.describe Banzai::Filter::UploadLinkFilter do context 'to a group upload' do let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') } let_it_be(:group) { create(:group) } + let(:project) { nil } let(:relative_path) { "/groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" } diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb index d1f6ee49260..b5b5349946b 100644 --- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb +++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb @@ -22,6 +22,15 @@ RSpec.describe Banzai::Filter::WikiLinkFilter do expect(filtered_link.attribute('href').value).to eq('/uploads/a.test') end + describe 'when links point to the relative wiki path' do + it 'does not rewrite links' do + path = "#{wiki.wiki_base_path}/#{repository_upload_folder}/a.jpg" + filtered_link = filter("<a href='#{path}'>Link</a>", wiki: wiki, page_slug: 'home').children[0] + + expect(filtered_link.attribute('href').value).to eq(path) + end + end + describe "when links point to the #{Wikis::CreateAttachmentService::ATTACHMENT_PATH} folder" do context 'with an "a" html tag' do it 'rewrites links' do diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb index 31047b9494a..e24177a7043 100644 --- a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Banzai::Pipeline::GfmPipeline do issue = create(:issue, project: project) markdown = "text #{issue.to_reference(project, full: true)}" - expect_any_instance_of(Banzai::Filter::ReferenceFilter).to receive(:each_node).once + expect_any_instance_of(Banzai::Filter::References::ReferenceFilter).to receive(:each_node).once described_class.call(markdown, project: project) end @@ -145,6 +145,7 @@ RSpec.describe Banzai::Pipeline::GfmPipeline do describe 'emoji in references' do let_it_be(:project) { create(:project, :public) } + let(:emoji) { '💯' } it 'renders a label reference with emoji inside' do diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index b102de24041..007d310247b 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -289,4 +289,29 @@ RSpec.describe Banzai::Pipeline::WikiPipeline do expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/audio%20file%20name.wav"') end end + + describe 'gollum tag filters' do + context 'when local image file exists' do + it 'sets the proper attributes for the image' do + gollum_file_double = double('Gollum::File', + mime_type: 'image/jpeg', + name: 'images/image.jpg', + path: 'images/image.jpg', + data: '') + + wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double) + markdown = "[[#{wiki_file.path}]]" + + expect(wiki).to receive(:find_file).with(wiki_file.path, load_content: false).and_return(wiki_file) + + output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) + doc = Nokogiri::HTML::DocumentFragment.parse(output) + + full_path = "/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/#{wiki_file.path}" + expect(doc.css('a')[0].attr('href')).to eq(full_path) + expect(doc.css('img')[0].attr('class')).to eq('gfm lazy') + expect(doc.css('img')[0].attr('data-src')).to eq(full_path) + end + end + end end diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb index 5f92eb42e74..0c1b98e5ec3 100644 --- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Banzai::ReferenceParser::ExternalIssueParser do levels.each do |level| it "creates reference when the feature is #{level}" do - project.project_feature.update(issues_access_level: level) + project.project_feature.update!(issues_access_level: level) visible_nodes = subject.nodes_visible_to_user(user, [link]) diff --git a/spec/lib/banzai/reference_redactor_spec.rb b/spec/lib/banzai/reference_redactor_spec.rb index 668e427cfa2..78cceedd0e5 100644 --- a/spec/lib/banzai/reference_redactor_spec.rb +++ b/spec/lib/banzai/reference_redactor_spec.rb @@ -64,7 +64,7 @@ RSpec.describe Banzai::ReferenceRedactor do let(:redactor) { described_class.new(Banzai::RenderContext.new(project, user)) } before do - project.update(pending_delete: true) + project.update!(pending_delete: true) end it 'redacts an issue attached' do diff --git a/spec/lib/bulk_imports/common/extractors/rest_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/rest_extractor_spec.rb new file mode 100644 index 00000000000..721dacbe3f4 --- /dev/null +++ b/spec/lib/bulk_imports/common/extractors/rest_extractor_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Common::Extractors::RestExtractor do + let(:http_client) { instance_double(BulkImports::Clients::Http) } + let(:options) { { query: double(to_h: { resource: nil, query: nil }) } } + let(:response) { double(parsed_response: { 'data' => { 'foo' => 'bar' } }, headers: { 'x-next-page' => '2' }) } + + subject { described_class.new(options) } + + describe '#extract' do + before do + allow(subject).to receive(:http_client).and_return(http_client) + allow(http_client).to receive(:get).and_return(response) + end + + it 'returns instance of ExtractedData' do + entity = create(:bulk_import_entity) + tracker = create(:bulk_import_tracker, entity: entity) + context = BulkImports::Pipeline::Context.new(tracker) + + extracted_data = subject.extract(context) + + expect(extracted_data).to be_instance_of(BulkImports::Pipeline::ExtractedData) + expect(extracted_data.data).to contain_exactly(response.parsed_response) + expect(extracted_data.next_page).to eq(response.headers['x-next-page']) + expect(extracted_data.has_next_page?).to eq(true) + end + end +end diff --git a/spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb index ff11a10bfe9..ba74c173794 100644 --- a/spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb +++ b/spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb @@ -8,7 +8,8 @@ RSpec.describe BulkImports::Common::Transformers::UserReferenceTransformer do let_it_be(:group) { create(:group) } let_it_be(:bulk_import) { create(:bulk_import) } let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) } - let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } let(:hash) do { @@ -51,19 +52,26 @@ RSpec.describe BulkImports::Common::Transformers::UserReferenceTransformer do end context 'when custom reference is provided' do - it 'updates provided reference' do - hash = { - 'author' => { - 'public_email' => user.email + shared_examples 'updates provided reference' do |reference| + let(:hash) do + { + 'author' => { + 'public_email' => user.email + } } - } + end - transformer = described_class.new(reference: 'author') - result = transformer.transform(context, hash) + it 'updates provided reference' do + transformer = described_class.new(reference: reference) + result = transformer.transform(context, hash) - expect(result['author']).to be_nil - expect(result['author_id']).to eq(user.id) + expect(result['author']).to be_nil + expect(result['author_id']).to eq(user.id) + end end + + include_examples 'updates provided reference', 'author' + include_examples 'updates provided reference', :author end end end diff --git a/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb b/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb index 627247c04ab..ac8786440e9 100644 --- a/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb +++ b/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb @@ -8,8 +8,9 @@ RSpec.describe BulkImports::Groups::Extractors::SubgroupsExtractor do bulk_import = create(:bulk_import) create(:bulk_import_configuration, bulk_import: bulk_import) entity = create(:bulk_import_entity, bulk_import: bulk_import) + tracker = create(:bulk_import_tracker, entity: entity) response = [{ 'test' => 'group' }] - context = BulkImports::Pipeline::Context.new(entity) + context = BulkImports::Pipeline::Context.new(tracker) allow_next_instance_of(BulkImports::Clients::Http) do |client| allow(client).to receive(:each_page).and_return(response) diff --git a/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb index ef46da7062b..b0f8f74783b 100644 --- a/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb +++ b/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb @@ -4,10 +4,10 @@ require 'spec_helper' RSpec.describe BulkImports::Groups::Graphql::GetGroupQuery do describe '#variables' do - let(:entity) { double(source_full_path: 'test', bulk_import: nil) } - let(:context) { BulkImports::Pipeline::Context.new(entity) } - it 'returns query variables based on entity information' do + entity = double(source_full_path: 'test', bulk_import: nil) + tracker = double(entity: entity) + context = BulkImports::Pipeline::Context.new(tracker) expected = { full_path: entity.source_full_path } expect(described_class.variables(context)).to eq(expected) diff --git a/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb index 85f82be7d18..61db644a372 100644 --- a/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb +++ b/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb @@ -4,8 +4,8 @@ require 'spec_helper' RSpec.describe BulkImports::Groups::Graphql::GetLabelsQuery do it 'has a valid query' do - entity = create(:bulk_import_entity) - context = BulkImports::Pipeline::Context.new(entity) + tracker = create(:bulk_import_tracker) + context = BulkImports::Pipeline::Context.new(tracker) query = GraphQL::Query.new( GitlabSchema, diff --git a/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb index 5d05f5a2d30..d0c4bb817b2 100644 --- a/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb +++ b/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb @@ -4,8 +4,8 @@ require 'spec_helper' RSpec.describe BulkImports::Groups::Graphql::GetMembersQuery do it 'has a valid query' do - entity = create(:bulk_import_entity) - context = BulkImports::Pipeline::Context.new(entity) + tracker = create(:bulk_import_tracker) + context = BulkImports::Pipeline::Context.new(tracker) query = GraphQL::Query.new( GitlabSchema, diff --git a/spec/lib/bulk_imports/groups/graphql/get_milestones_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_milestones_query_spec.rb index a38505fbf85..7a0f964c5f3 100644 --- a/spec/lib/bulk_imports/groups/graphql/get_milestones_query_spec.rb +++ b/spec/lib/bulk_imports/groups/graphql/get_milestones_query_spec.rb @@ -4,8 +4,8 @@ require 'spec_helper' RSpec.describe BulkImports::Groups::Graphql::GetMilestonesQuery do it 'has a valid query' do - entity = create(:bulk_import_entity) - context = BulkImports::Pipeline::Context.new(entity) + tracker = create(:bulk_import_tracker) + context = BulkImports::Pipeline::Context.new(tracker) query = GraphQL::Query.new( GitlabSchema, diff --git a/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb index 183292722d2..533955b057c 100644 --- a/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb +++ b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb @@ -4,12 +4,13 @@ require 'spec_helper' RSpec.describe BulkImports::Groups::Loaders::GroupLoader do describe '#load' do - let(:user) { create(:user) } - let(:data) { { foo: :bar } } + let_it_be(:user) { create(:user) } + let_it_be(:bulk_import) { create(:bulk_import, user: user) } + let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } let(:service_double) { instance_double(::Groups::CreateService) } - let(:bulk_import) { create(:bulk_import, user: user) } - let(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) } - let(:context) { BulkImports::Pipeline::Context.new(entity) } + let(:data) { { foo: :bar } } subject { described_class.new } diff --git a/spec/lib/bulk_imports/groups/pipelines/badges_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/badges_pipeline_spec.rb new file mode 100644 index 00000000000..9fa35c4707d --- /dev/null +++ b/spec/lib/bulk_imports/groups/pipelines/badges_pipeline_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Pipelines::BadgesPipeline do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + + let_it_be(:entity) do + create( + :bulk_import_entity, + source_full_path: 'source/full/path', + destination_name: 'My Destination Group', + destination_namespace: group.full_path, + group: group + ) + end + + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } + + subject { described_class.new(context) } + + describe '#run' do + it 'imports a group badge' do + first_page = extracted_data(has_next_page: true) + last_page = extracted_data(name: 'badge2') + + allow_next_instance_of(BulkImports::Common::Extractors::RestExtractor) do |extractor| + allow(extractor) + .to receive(:extract) + .and_return(first_page, last_page) + end + + expect { subject.run }.to change(Badge, :count).by(2) + + badge = group.badges.last + + expect(badge.name).to eq('badge2') + expect(badge.link_url).to eq(badge_data['link_url']) + expect(badge.image_url).to eq(badge_data['image_url']) + end + + describe '#load' do + it 'creates a badge' do + expect { subject.load(context, badge_data) }.to change(Badge, :count).by(1) + + badge = group.badges.first + + badge_data.each do |key, value| + expect(badge[key]).to eq(value) + end + end + + it 'does nothing when the data is blank' do + expect { subject.load(context, nil) }.not_to change(Badge, :count) + end + end + + describe '#transform' do + it 'return transformed badge hash' do + badge = subject.transform(context, badge_data) + + expect(badge[:name]).to eq('badge') + expect(badge[:link_url]).to eq(badge_data['link_url']) + expect(badge[:image_url]).to eq(badge_data['image_url']) + expect(badge.keys).to contain_exactly(:name, :link_url, :image_url) + end + + context 'when data is blank' do + it 'does nothing when the data is blank' do + expect(subject.transform(context, nil)).to be_nil + end + end + end + + describe 'pipeline parts' do + it { expect(described_class).to include_module(BulkImports::Pipeline) } + it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) } + + it 'has extractors' do + expect(described_class.get_extractor) + .to eq( + klass: BulkImports::Common::Extractors::RestExtractor, + options: { + query: BulkImports::Groups::Rest::GetBadgesQuery + } + ) + end + + it 'has transformers' do + expect(described_class.transformers) + .to contain_exactly( + { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil } + ) + end + end + + def badge_data(name = 'badge') + { + 'name' => name, + 'link_url' => 'https://gitlab.example.com', + 'image_url' => 'https://gitlab.example.com/image.png' + } + end + + def extracted_data(name: 'badge', has_next_page: false) + page_info = { + 'has_next_page' => has_next_page, + 'next_page' => has_next_page ? '2' : nil + } + + BulkImports::Pipeline::ExtractedData.new(data: [badge_data(name)], page_info: page_info) + end + end +end diff --git a/spec/lib/bulk_imports/groups/pipelines/entity_finisher_spec.rb b/spec/lib/bulk_imports/groups/pipelines/entity_finisher_spec.rb new file mode 100644 index 00000000000..8276349c5f4 --- /dev/null +++ b/spec/lib/bulk_imports/groups/pipelines/entity_finisher_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Pipelines::EntityFinisher do + it 'updates the entity status to finished' do + entity = create(:bulk_import_entity, :started) + pipeline_tracker = create(:bulk_import_tracker, entity: entity) + context = BulkImports::Pipeline::Context.new(pipeline_tracker) + subject = described_class.new(context) + + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger) + .to receive(:info) + .with( + bulk_import_id: entity.bulk_import.id, + bulk_import_entity_id: entity.id, + bulk_import_entity_type: entity.source_type, + pipeline_class: described_class.name, + message: 'Entity finished' + ) + end + + expect { subject.run } + .to change(entity, :status_name).to(:finished) + end + + it 'does nothing when the entity is already finished' do + entity = create(:bulk_import_entity, :finished) + pipeline_tracker = create(:bulk_import_tracker, entity: entity) + context = BulkImports::Pipeline::Context.new(pipeline_tracker) + subject = described_class.new(context) + + expect { subject.run } + .not_to change(entity, :status_name) + end +end diff --git a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb index 61950cdd9b0..39e782dc093 100644 --- a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb @@ -4,10 +4,11 @@ require 'spec_helper' RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do describe '#run' do - let(:user) { create(:user) } - let(:parent) { create(:group) } - let(:bulk_import) { create(:bulk_import, user: user) } - let(:entity) do + let_it_be(:user) { create(:user) } + let_it_be(:parent) { create(:group) } + let_it_be(:bulk_import) { create(:bulk_import, user: user) } + + let_it_be(:entity) do create( :bulk_import_entity, bulk_import: bulk_import, @@ -17,7 +18,8 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do ) end - let(:context) { BulkImports::Pipeline::Context.new(entity) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } let(:group_data) do { @@ -37,7 +39,7 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do before do allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| - allow(extractor).to receive(:extract).and_return([group_data]) + allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: group_data)) end parent.add_owner(user) diff --git a/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb index 3327a30f1d5..8af646d1101 100644 --- a/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb @@ -3,11 +3,11 @@ require 'spec_helper' RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:cursor) { 'cursor' } - let(:timestamp) { Time.new(2020, 01, 01).utc } - let(:entity) do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:timestamp) { Time.new(2020, 01, 01).utc } + + let_it_be(:entity) do create( :bulk_import_entity, source_full_path: 'source/full/path', @@ -17,33 +17,15 @@ RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do ) end - let(:context) { BulkImports::Pipeline::Context.new(entity) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } subject { described_class.new(context) } - def label_data(title) - { - 'title' => title, - 'description' => 'desc', - 'color' => '#428BCA', - 'created_at' => timestamp.to_s, - 'updated_at' => timestamp.to_s - } - end - - def extractor_data(title:, has_next_page:, cursor: nil) - page_info = { - 'end_cursor' => cursor, - 'has_next_page' => has_next_page - } - - BulkImports::Pipeline::ExtractedData.new(data: [label_data(title)], page_info: page_info) - end - describe '#run' do it 'imports a group labels' do - first_page = extractor_data(title: 'label1', has_next_page: true, cursor: cursor) - last_page = extractor_data(title: 'label2', has_next_page: false) + first_page = extracted_data(title: 'label1', has_next_page: true) + last_page = extracted_data(title: 'label2') allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| allow(extractor) @@ -63,38 +45,6 @@ RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do end end - describe '#after_run' do - context 'when extracted data has next page' do - it 'updates tracker information and runs pipeline again' do - data = extractor_data(title: 'label', has_next_page: true, cursor: cursor) - - expect(subject).to receive(:run) - - subject.after_run(data) - - tracker = entity.trackers.find_by(relation: :labels) - - expect(tracker.has_next_page).to eq(true) - expect(tracker.next_page).to eq(cursor) - end - end - - context 'when extracted data has no next page' do - it 'updates tracker information and does not run pipeline' do - data = extractor_data(title: 'label', has_next_page: false) - - expect(subject).not_to receive(:run) - - subject.after_run(data) - - tracker = entity.trackers.find_by(relation: :labels) - - expect(tracker.has_next_page).to eq(false) - expect(tracker.next_page).to be_nil - end - end - end - describe '#load' do it 'creates the label' do data = label_data('label') @@ -130,4 +80,23 @@ RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do ) end end + + def label_data(title) + { + 'title' => title, + 'description' => 'desc', + 'color' => '#428BCA', + 'created_at' => timestamp.to_s, + 'updated_at' => timestamp.to_s + } + end + + def extracted_data(title:, has_next_page: false) + page_info = { + 'has_next_page' => has_next_page, + 'next_page' => has_next_page ? 'cursor' : nil + } + + BulkImports::Pipeline::ExtractedData.new(data: [label_data(title)], page_info: page_info) + end end diff --git a/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb index 74d3e09d263..d8a667ec92a 100644 --- a/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb @@ -8,17 +8,17 @@ RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } - let_it_be(:cursor) { 'cursor' } let_it_be(:bulk_import) { create(:bulk_import, user: user) } let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) } - let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } subject { described_class.new(context) } describe '#run' do it 'maps existing users to the imported group' do - first_page = member_data(email: member_user1.email, has_next_page: true, cursor: cursor) - last_page = member_data(email: member_user2.email, has_next_page: false) + first_page = extracted_data(email: member_user1.email, has_next_page: true) + last_page = extracted_data(email: member_user2.email) allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| allow(extractor) @@ -88,7 +88,7 @@ RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do end end - def member_data(email:, has_next_page:, cursor: nil) + def extracted_data(email:, has_next_page: false) data = { 'created_at' => '2020-01-01T00:00:00Z', 'updated_at' => '2020-01-01T00:00:00Z', @@ -102,8 +102,8 @@ RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do } page_info = { - 'end_cursor' => cursor, - 'has_next_page' => has_next_page + 'has_next_page' => has_next_page, + 'next_page' => has_next_page ? 'cursor' : nil } BulkImports::Pipeline::ExtractedData.new(data: data, page_info: page_info) diff --git a/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb index f0c34c65257..e5cf75c566b 100644 --- a/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb @@ -5,11 +5,10 @@ require 'spec_helper' RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } - let_it_be(:cursor) { 'cursor' } let_it_be(:timestamp) { Time.new(2020, 01, 01).utc } let_it_be(:bulk_import) { create(:bulk_import, user: user) } - let(:entity) do + let_it_be(:entity) do create( :bulk_import_entity, bulk_import: bulk_import, @@ -20,39 +19,19 @@ RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do ) end - let(:context) { BulkImports::Pipeline::Context.new(entity) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } subject { described_class.new(context) } - def milestone_data(title) - { - 'title' => title, - 'description' => 'desc', - 'state' => 'closed', - 'start_date' => '2020-10-21', - 'due_date' => '2020-10-22', - 'created_at' => timestamp.to_s, - 'updated_at' => timestamp.to_s - } - end - - def extracted_data(title:, has_next_page:, cursor: nil) - page_info = { - 'end_cursor' => cursor, - 'has_next_page' => has_next_page - } - - BulkImports::Pipeline::ExtractedData.new(data: [milestone_data(title)], page_info: page_info) - end - before do group.add_owner(user) end describe '#run' do it 'imports group milestones' do - first_page = extracted_data(title: 'milestone1', has_next_page: true, cursor: cursor) - last_page = extracted_data(title: 'milestone2', has_next_page: false) + first_page = extracted_data(title: 'milestone1', iid: 1, has_next_page: true) + last_page = extracted_data(title: 'milestone2', iid: 2) allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| allow(extractor) @@ -75,38 +54,6 @@ RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do end end - describe '#after_run' do - context 'when extracted data has next page' do - it 'updates tracker information and runs pipeline again' do - data = extracted_data(title: 'milestone', has_next_page: true, cursor: cursor) - - expect(subject).to receive(:run) - - subject.after_run(data) - - tracker = entity.trackers.find_by(relation: :milestones) - - expect(tracker.has_next_page).to eq(true) - expect(tracker.next_page).to eq(cursor) - end - end - - context 'when extracted data has no next page' do - it 'updates tracker information and does not run pipeline' do - data = extracted_data(title: 'milestone', has_next_page: false) - - expect(subject).not_to receive(:run) - - subject.after_run(data) - - tracker = entity.trackers.find_by(relation: :milestones) - - expect(tracker.has_next_page).to eq(false) - expect(tracker.next_page).to be_nil - end - end - end - describe '#load' do it 'creates the milestone' do data = milestone_data('milestone') @@ -120,7 +67,7 @@ RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do end it 'raises NotAllowedError' do - data = extracted_data(title: 'milestone', has_next_page: false) + data = extracted_data(title: 'milestone') expect { subject.load(context, data) }.to raise_error(::BulkImports::Pipeline::NotAllowedError) end @@ -148,4 +95,29 @@ RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do ) end end + + def milestone_data(title, iid: 1) + { + 'title' => title, + 'description' => 'desc', + 'iid' => iid, + 'state' => 'closed', + 'start_date' => '2020-10-21', + 'due_date' => '2020-10-22', + 'created_at' => timestamp.to_s, + 'updated_at' => timestamp.to_s + } + end + + def extracted_data(title:, iid: 1, has_next_page: false) + page_info = { + 'has_next_page' => has_next_page, + 'next_page' => has_next_page ? 'cursor' : nil + } + + BulkImports::Pipeline::ExtractedData.new( + data: milestone_data(title, iid: iid), + page_info: page_info + ) + end end diff --git a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb index 2a99646bb4a..e4a41428dd2 100644 --- a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb @@ -6,31 +6,23 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group, path: 'group') } let_it_be(:parent) { create(:group, name: 'imported-group', path: 'imported-group') } - let(:context) { BulkImports::Pipeline::Context.new(parent_entity) } + let_it_be(:parent_entity) { create(:bulk_import_entity, destination_namespace: parent.full_path, group: parent) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: parent_entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } subject { described_class.new(context) } - describe '#run' do - let!(:parent_entity) do - create( - :bulk_import_entity, - destination_namespace: parent.full_path, - group: parent - ) - end - - let(:subgroup_data) do - [ - { - "name" => "subgroup", - "full_path" => "parent/subgroup" - } - ] - end + let(:extracted_data) do + BulkImports::Pipeline::ExtractedData.new(data: { + 'name' => 'subgroup', + 'full_path' => 'parent/subgroup' + }) + end + describe '#run' do before do allow_next_instance_of(BulkImports::Groups::Extractors::SubgroupsExtractor) do |extractor| - allow(extractor).to receive(:extract).and_return(subgroup_data) + allow(extractor).to receive(:extract).and_return(extracted_data) end parent.add_owner(user) diff --git a/spec/lib/bulk_imports/groups/rest/get_badges_query_spec.rb b/spec/lib/bulk_imports/groups/rest/get_badges_query_spec.rb new file mode 100644 index 00000000000..eef6848e118 --- /dev/null +++ b/spec/lib/bulk_imports/groups/rest/get_badges_query_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Rest::GetBadgesQuery do + describe '.to_h' do + it 'returns query resource and page info' do + entity = create(:bulk_import_entity) + tracker = create(:bulk_import_tracker, entity: entity) + context = BulkImports::Pipeline::Context.new(tracker) + encoded_full_path = ERB::Util.url_encode(entity.source_full_path) + expected = { + resource: ['groups', encoded_full_path, 'badges'].join('/'), + query: { + page: context.tracker.next_page + } + } + + expect(described_class.to_h(context)).to eq(expected) + end + end +end diff --git a/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb index b3fe8a2ba25..75d8c15088a 100644 --- a/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb +++ b/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb @@ -4,11 +4,12 @@ require 'spec_helper' RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do describe '#transform' do - let(:user) { create(:user) } - let(:parent) { create(:group) } - let(:group) { create(:group, name: 'My Source Group', parent: parent) } - let(:bulk_import) { create(:bulk_import, user: user) } - let(:entity) do + let_it_be(:user) { create(:user) } + let_it_be(:parent) { create(:group) } + let_it_be(:group) { create(:group, name: 'My Source Group', parent: parent) } + let_it_be(:bulk_import) { create(:bulk_import, user: user) } + + let_it_be(:entity) do create( :bulk_import_entity, bulk_import: bulk_import, @@ -18,7 +19,8 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do ) end - let(:context) { BulkImports::Pipeline::Context.new(entity) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } let(:data) do { @@ -82,14 +84,7 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do context 'when destination namespace is empty' do it 'does not set parent id' do - entity = create( - :bulk_import_entity, - bulk_import: bulk_import, - source_full_path: 'source/full/path', - destination_name: group.name, - destination_namespace: '' - ) - context = BulkImports::Pipeline::Context.new(entity) + entity.update!(destination_namespace: '') transformed_data = subject.transform(context, data) diff --git a/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb index f66c67fc6a2..f3905a4b6e4 100644 --- a/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb +++ b/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb @@ -8,7 +8,8 @@ RSpec.describe BulkImports::Groups::Transformers::MemberAttributesTransformer do let_it_be(:group) { create(:group) } let_it_be(:bulk_import) { create(:bulk_import, user: user) } let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) } - let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } it 'returns nil when receives no data' do expect(subject.transform(context, nil)).to eq(nil) diff --git a/spec/lib/bulk_imports/importers/group_importer_spec.rb b/spec/lib/bulk_imports/importers/group_importer_spec.rb deleted file mode 100644 index 5d501b49e41..00000000000 --- a/spec/lib/bulk_imports/importers/group_importer_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe BulkImports::Importers::GroupImporter do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:bulk_import) { create(:bulk_import) } - let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import, group: group) } - let(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) } - let(:context) { BulkImports::Pipeline::Context.new(bulk_import_entity) } - - before do - allow(BulkImports::Pipeline::Context).to receive(:new).and_return(context) - end - - subject { described_class.new(bulk_import_entity) } - - describe '#execute' do - it 'starts the entity and run its pipelines' do - expect_to_run_pipeline BulkImports::Groups::Pipelines::GroupPipeline, context: context - expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context - expect_to_run_pipeline BulkImports::Groups::Pipelines::MembersPipeline, context: context - expect_to_run_pipeline BulkImports::Groups::Pipelines::LabelsPipeline, context: context - expect_to_run_pipeline BulkImports::Groups::Pipelines::MilestonesPipeline, context: context - - if Gitlab.ee? - expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) - expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline'.constantize, context: context) - expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicEventsPipeline'.constantize, context: context) - expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::IterationsPipeline'.constantize, context: context) - end - - subject.execute - - expect(bulk_import_entity.reload).to be_finished - end - - context 'when failed' do - let(:bulk_import_entity) { create(:bulk_import_entity, :failed, bulk_import: bulk_import, group: group) } - - it 'does not transition entity to finished state' do - allow(bulk_import_entity).to receive(:start!) - - subject.execute - - expect(bulk_import_entity.reload).to be_failed - end - end - end - - def expect_to_run_pipeline(klass, context:) - expect_next_instance_of(klass, context) do |pipeline| - expect(pipeline).to receive(:run) - end - end -end diff --git a/spec/lib/bulk_imports/pipeline/context_spec.rb b/spec/lib/bulk_imports/pipeline/context_spec.rb index c8c3fe3a861..5b7711ad5d7 100644 --- a/spec/lib/bulk_imports/pipeline/context_spec.rb +++ b/spec/lib/bulk_imports/pipeline/context_spec.rb @@ -3,29 +3,52 @@ require 'spec_helper' RSpec.describe BulkImports::Pipeline::Context do - let(:group) { instance_double(Group) } - let(:user) { instance_double(User) } - let(:bulk_import) { instance_double(BulkImport, user: user, configuration: :config) } - - let(:entity) do - instance_double( - BulkImports::Entity, - bulk_import: bulk_import, - group: group + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:bulk_import) { create(:bulk_import, user: user) } + + let_it_be(:entity) do + create( + :bulk_import_entity, + source_full_path: 'source/full/path', + destination_name: 'My Destination Group', + destination_namespace: group.full_path, + group: group, + bulk_import: bulk_import + ) + end + + let_it_be(:tracker) do + create( + :bulk_import_tracker, + entity: entity, + pipeline_name: described_class.name ) end - subject { described_class.new(entity) } + subject { described_class.new(tracker, extra: :data) } + + describe '#entity' do + it { expect(subject.entity).to eq(entity) } + end describe '#group' do it { expect(subject.group).to eq(group) } end + describe '#bulk_import' do + it { expect(subject.bulk_import).to eq(bulk_import) } + end + describe '#current_user' do it { expect(subject.current_user).to eq(user) } end - describe '#current_user' do + describe '#configuration' do it { expect(subject.configuration).to eq(bulk_import.configuration) } end + + describe '#extra' do + it { expect(subject.extra).to eq(extra: :data) } + 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 25c5178227a..9c79b3f4c9e 100644 --- a/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb +++ b/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb @@ -9,7 +9,7 @@ RSpec.describe BulkImports::Pipeline::ExtractedData do let(:page_info) do { 'has_next_page' => has_next_page, - 'end_cursor' => cursor + 'next_page' => cursor } end diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb index 59f01c9caaa..7235b7c95cd 100644 --- a/spec/lib/bulk_imports/pipeline/runner_spec.rb +++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb @@ -38,23 +38,20 @@ RSpec.describe BulkImports::Pipeline::Runner do extractor BulkImports::Extractor transformer BulkImports::Transformer loader BulkImports::Loader - - def after_run(_); end end stub_const('BulkImports::MyPipeline', pipeline) end - let_it_be_with_refind(:entity) { create(:bulk_import_entity) } - let(:context) { BulkImports::Pipeline::Context.new(entity, extra: :data) } + let_it_be_with_reload(:entity) { create(:bulk_import_entity) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker, extra: :data) } subject { BulkImports::MyPipeline.new(context) } describe 'pipeline runner' do context 'when entity is not marked as failed' do it 'runs pipeline extractor, transformer, loader' do - extracted_data = BulkImports::Pipeline::ExtractedData.new(data: { foo: :bar }) - expect_next_instance_of(BulkImports::Extractor) do |extractor| expect(extractor) .to receive(:extract) @@ -132,6 +129,22 @@ RSpec.describe BulkImports::Pipeline::Runner do subject.run end + context 'when extracted data has multiple pages' do + it 'updates tracker information and runs pipeline again' do + first_page = extracted_data(has_next_page: true) + last_page = extracted_data + + expect_next_instance_of(BulkImports::Extractor) do |extractor| + expect(extractor) + .to receive(:extract) + .with(context) + .and_return(first_page, last_page) + end + + subject.run + end + end + context 'when exception is raised' do before do allow_next_instance_of(BulkImports::Extractor) do |extractor| @@ -170,12 +183,7 @@ RSpec.describe BulkImports::Pipeline::Runner do BulkImports::MyPipeline.abort_on_failure! end - it 'marks entity as failed' do - expect { subject.run } - .to change(entity, :status_name).to(:failed) - end - - it 'logs warn message' do + it 'logs a warn message and marks entity as failed' do expect_next_instance_of(Gitlab::Import::Logger) do |logger| expect(logger).to receive(:warn) .with( @@ -188,6 +196,9 @@ RSpec.describe BulkImports::Pipeline::Runner do end subject.run + + expect(entity.status_name).to eq(:failed) + expect(tracker.status_name).to eq(:failed) end end @@ -206,11 +217,11 @@ RSpec.describe BulkImports::Pipeline::Runner do entity.fail_op! expect_next_instance_of(Gitlab::Import::Logger) do |logger| - expect(logger).to receive(:info) + expect(logger).to receive(:warn) .with( log_params( context, - message: 'Skipping due to failed pipeline status', + message: 'Skipping pipeline due to failed entity', pipeline_class: 'BulkImports::MyPipeline' ) ) @@ -219,14 +230,24 @@ RSpec.describe BulkImports::Pipeline::Runner do subject.run end end - end - def log_params(context, extra = {}) - { - bulk_import_id: context.bulk_import.id, - bulk_import_entity_id: context.entity.id, - bulk_import_entity_type: context.entity.source_type, - context_extra: context.extra - }.merge(extra) + def log_params(context, extra = {}) + { + bulk_import_id: context.bulk_import.id, + bulk_import_entity_id: context.entity.id, + bulk_import_entity_type: context.entity.source_type, + context_extra: context.extra + }.merge(extra) + end + + def extracted_data(has_next_page: false) + BulkImports::Pipeline::ExtractedData.new( + data: { foo: :bar }, + page_info: { + 'has_next_page' => has_next_page, + 'next_page' => has_next_page ? 'cursor' : nil + } + ) + end end end diff --git a/spec/lib/bulk_imports/pipeline_spec.rb b/spec/lib/bulk_imports/pipeline_spec.rb index c882e3d26ea..dda2e41f06c 100644 --- a/spec/lib/bulk_imports/pipeline_spec.rb +++ b/spec/lib/bulk_imports/pipeline_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe BulkImports::Pipeline do + let(:context) { instance_double(BulkImports::Pipeline::Context, tracker: nil) } + before do stub_const('BulkImports::Extractor', Class.new) stub_const('BulkImports::Transformer', Class.new) @@ -44,7 +46,7 @@ RSpec.describe BulkImports::Pipeline do end it 'returns itself when retrieving extractor & loader' do - pipeline = BulkImports::AnotherPipeline.new(nil) + pipeline = BulkImports::AnotherPipeline.new(context) expect(pipeline.send(:extractor)).to eq(pipeline) expect(pipeline.send(:loader)).to eq(pipeline) @@ -83,7 +85,7 @@ RSpec.describe BulkImports::Pipeline do expect(BulkImports::Transformer).to receive(:new).with(foo: :bar) expect(BulkImports::Loader).to receive(:new).with(foo: :bar) - pipeline = BulkImports::MyPipeline.new(nil) + pipeline = BulkImports::MyPipeline.new(context) pipeline.send(:extractor) pipeline.send(:transformers) @@ -109,7 +111,7 @@ RSpec.describe BulkImports::Pipeline do expect(BulkImports::Transformer).to receive(:new).with(no_args) expect(BulkImports::Loader).to receive(:new).with(no_args) - pipeline = BulkImports::NoOptionsPipeline.new(nil) + pipeline = BulkImports::NoOptionsPipeline.new(context) pipeline.send(:extractor) pipeline.send(:transformers) @@ -135,7 +137,7 @@ RSpec.describe BulkImports::Pipeline do transformer = double allow(BulkImports::Transformer).to receive(:new).and_return(transformer) - pipeline = BulkImports::TransformersPipeline.new(nil) + pipeline = BulkImports::TransformersPipeline.new(context) expect(pipeline.send(:transformers)).to eq([pipeline, transformer]) end diff --git a/spec/lib/constraints/admin_constrainer_spec.rb b/spec/lib/constraints/admin_constrainer_spec.rb index ac6ad31120e..6e8909ca129 100644 --- a/spec/lib/constraints/admin_constrainer_spec.rb +++ b/spec/lib/constraints/admin_constrainer_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Constraints::AdminConstrainer do end describe '#matches' do - context 'feature flag :user_mode_in_session is enabled' do + context 'application setting :admin_mode is enabled' do context 'when user is a regular user' do it 'forbids access' do expect(subject.matches?(request)).to be(false) @@ -46,9 +46,9 @@ RSpec.describe Constraints::AdminConstrainer do end end - context 'feature flag :user_mode_in_session is disabled' do + context 'application setting :admin_mode is disabled' do before do - stub_feature_flags(user_mode_in_session: false) + stub_application_setting(admin_mode: false) end context 'when user is a regular user' do diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 3e158391d7f..dc8fd0de313 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -487,6 +487,98 @@ RSpec.describe Feature, stub_feature_flags: false do end end + context 'caching with stale reads from the database', :use_clean_rails_redis_caching, :request_store, :aggregate_failures do + let(:actor) { stub_feature_flag_gate('CustomActor:5') } + let(:another_actor) { stub_feature_flag_gate('CustomActor:10') } + + # This is a bit unpleasant. For these tests we want to simulate stale reads + # from the database (due to database load balancing). A simple way to do + # that is to stub the response on the adapter Flipper uses for reading from + # the database. However, there isn't a convenient API for this. We know that + # the ActiveRecord adapter is always at the 'bottom' of the chain, so we can + # find it that way. + let(:active_record_adapter) do + adapter = described_class.flipper + + loop do + break adapter unless adapter.instance_variable_get(:@adapter) + + adapter = adapter.instance_variable_get(:@adapter) + end + end + + it 'gives the correct value when enabling for an additional actor' do + described_class.enable(:enabled_feature_flag, actor) + initial_gate_values = active_record_adapter.get(described_class.get(:enabled_feature_flag)) + + # This should only be enabled for `actor` + expect(described_class.enabled?(:enabled_feature_flag, actor)).to be(true) + expect(described_class.enabled?(:enabled_feature_flag, another_actor)).to be(false) + expect(described_class.enabled?(:enabled_feature_flag)).to be(false) + + # Enable for `another_actor` and simulate a stale read + described_class.enable(:enabled_feature_flag, another_actor) + allow(active_record_adapter).to receive(:get).once.and_return(initial_gate_values) + + # Should read from the cache and be enabled for both of these actors + expect(described_class.enabled?(:enabled_feature_flag, actor)).to be(true) + expect(described_class.enabled?(:enabled_feature_flag, another_actor)).to be(true) + expect(described_class.enabled?(:enabled_feature_flag)).to be(false) + end + + it 'gives the correct value when enabling for percentage of time' do + described_class.enable_percentage_of_time(:enabled_feature_flag, 10) + initial_gate_values = active_record_adapter.get(described_class.get(:enabled_feature_flag)) + + # Test against `gate_values` directly as otherwise it would be non-determistic + expect(described_class.get(:enabled_feature_flag).gate_values.percentage_of_time).to eq(10) + + # Enable 50% of time and simulate a stale read + described_class.enable_percentage_of_time(:enabled_feature_flag, 50) + allow(active_record_adapter).to receive(:get).once.and_return(initial_gate_values) + + # Should read from the cache and be enabled 50% of the time + expect(described_class.get(:enabled_feature_flag).gate_values.percentage_of_time).to eq(50) + end + + it 'gives the correct value when disabling the flag' do + described_class.enable(:enabled_feature_flag, actor) + described_class.enable(:enabled_feature_flag, another_actor) + initial_gate_values = active_record_adapter.get(described_class.get(:enabled_feature_flag)) + + # This be enabled for `actor` and `another_actor` + expect(described_class.enabled?(:enabled_feature_flag, actor)).to be(true) + expect(described_class.enabled?(:enabled_feature_flag, another_actor)).to be(true) + expect(described_class.enabled?(:enabled_feature_flag)).to be(false) + + # Disable for `another_actor` and simulate a stale read + described_class.disable(:enabled_feature_flag, another_actor) + allow(active_record_adapter).to receive(:get).once.and_return(initial_gate_values) + + # Should read from the cache and be enabled only for `actor` + expect(described_class.enabled?(:enabled_feature_flag, actor)).to be(true) + expect(described_class.enabled?(:enabled_feature_flag, another_actor)).to be(false) + expect(described_class.enabled?(:enabled_feature_flag)).to be(false) + end + + it 'gives the correct value when deleting the flag' do + described_class.enable(:enabled_feature_flag, actor) + initial_gate_values = active_record_adapter.get(described_class.get(:enabled_feature_flag)) + + # This should only be enabled for `actor` + expect(described_class.enabled?(:enabled_feature_flag, actor)).to be(true) + expect(described_class.enabled?(:enabled_feature_flag)).to be(false) + + # Remove and simulate a stale read + described_class.remove(:enabled_feature_flag) + allow(active_record_adapter).to receive(:get).once.and_return(initial_gate_values) + + # Should read from the cache and be disabled everywhere + expect(described_class.enabled?(:enabled_feature_flag, actor)).to be(false) + expect(described_class.enabled?(:enabled_feature_flag)).to be(false) + end + end + describe Feature::Target do describe '#targets' do let(:project) { create(:project) } diff --git a/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb new file mode 100644 index 00000000000..021fb8f5f58 --- /dev/null +++ b/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'generator_helper' + +RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator do + include UsageDataHelpers + + let(:category) { 'test_category' } + let(:event) { 'i_test_event' } + let(:args) { [category, event] } + let(:temp_dir) { Dir.mktmpdir } + + # Interpolating to preload the class + # See https://github.com/rspec/rspec-mocks/issues/1079 + before do + stub_const("#{Gitlab::UsageMetricDefinitionGenerator}::TOP_LEVEL_DIR", temp_dir) + # Stub Prometheus requests from Gitlab::Utils::UsageData + stub_prometheus_queries + end + + it 'creates metric definition files' do + described_class.new(args).invoke_all + + weekly_metric_definition_path = Dir.glob(File.join(temp_dir, 'metrics/counts_7d/*i_test_event_weekly.yml')).first + monthly_metric_definition_path = Dir.glob(File.join(temp_dir, 'metrics/counts_28d/*i_test_event_monthly.yml')).first + + expect(YAML.safe_load(File.read(weekly_metric_definition_path))).to include("key_path" => "redis_hll_counters.test_category.i_test_event_weekly") + expect(YAML.safe_load(File.read(monthly_metric_definition_path))).to include("key_path" => "redis_hll_counters.test_category.i_test_event_monthly") + end +end diff --git a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb index b62eac14e3e..f8c055ae111 100644 --- a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb +++ b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb @@ -3,10 +3,42 @@ require 'generator_helper' RSpec.describe Gitlab::UsageMetricDefinitionGenerator do + include UsageDataHelpers + + let(:key_path) { 'counts_weekly.test_metric' } + let(:dir) { '7d' } + let(:temp_dir) { Dir.mktmpdir } + + before do + stub_const("#{described_class}::TOP_LEVEL_DIR", temp_dir) + # Stub Prometheus requests from Gitlab::Utils::UsageData + stub_prometheus_queries + end + + after do + FileUtils.rm_rf(temp_dir) + end + + describe 'Creating metric definition file' do + # Stub version so that `milestone` key remains constant between releases to prevent flakiness. + before do + stub_const('Gitlab::VERSION', '13.9.0') + allow(::Gitlab::Usage::Metrics::NamesSuggestions::Generator).to receive(:generate).and_return('test metric name') + end + + let(:sample_metric) { load_sample_metric_definition(filename: 'sample_metric_with_name_suggestions.yml') } + + it 'creates a metric definition file using the template' do + described_class.new([key_path], { 'dir' => dir }).invoke_all + + metric_definition_path = Dir.glob(File.join(temp_dir, 'metrics/counts_7d/*_test_metric.yml')).first + + expect(YAML.safe_load(File.read(metric_definition_path))).to eq(sample_metric) + end + end + describe 'Validation' do - let(:key_path) { 'counter.category.event' } - let(:dir) { '7d' } - let(:options) { [key_path, '--dir', dir, '--pretend'] } + let(:options) { [key_path, '--dir', dir] } subject { described_class.start(options) } @@ -42,34 +74,12 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator do end describe 'Name suggestions' do - let(:temp_dir) { Dir.mktmpdir } - - before do - stub_const("#{described_class}::TOP_LEVEL_DIR", temp_dir) - end - - context 'with product_intelligence_metrics_names_suggestions feature ON' do - it 'adds name key to metric definition' do - stub_feature_flags(product_intelligence_metrics_names_suggestions: true) - - expect(::Gitlab::Usage::Metrics::NamesSuggestions::Generator).to receive(:generate).and_return('some name') - described_class.new(['counts_weekly.test_metric'], { 'dir' => '7d' }).invoke_all - metric_definition_path = Dir.glob(File.join(temp_dir, 'metrics/counts_7d/*_test_metric.yml')).first + it 'adds name key to metric definition' do + expect(::Gitlab::Usage::Metrics::NamesSuggestions::Generator).to receive(:generate).and_return('some name') + described_class.new([key_path], { 'dir' => dir }).invoke_all + metric_definition_path = Dir.glob(File.join(temp_dir, 'metrics/counts_7d/*_test_metric.yml')).first - expect(YAML.safe_load(File.read(metric_definition_path))).to include("name" => "some name") - end - end - - context 'with product_intelligence_metrics_names_suggestions feature OFF' do - it 'adds name key to metric definition' do - stub_feature_flags(product_intelligence_metrics_names_suggestions: false) - - expect(::Gitlab::Usage::Metrics::NamesSuggestions::Generator).not_to receive(:generate) - described_class.new(['counts_weekly.test_metric'], { 'dir' => '7d' }).invoke_all - metric_definition_path = Dir.glob(File.join(temp_dir, 'metrics/counts_7d/*_test_metric.yml')).first - - expect(YAML.safe_load(File.read(metric_definition_path)).keys).not_to include(:name) - end + expect(YAML.safe_load(File.read(metric_definition_path))).to include("name" => "some name") end end end diff --git a/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb b/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb index fceda763717..1ed43145aa6 100644 --- a/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb +++ b/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Gitlab::AlertManagement::AlertStatusCounts do let_it_be(:alert_resolved) { create(:alert_management_alert, :resolved, project: project) } let_it_be(:alert_ignored) { create(:alert_management_alert, :ignored, project: project) } let_it_be(:alert_triggered) { create(:alert_management_alert) } + let(:params) { {} } describe '#execute' do diff --git a/spec/lib/gitlab/alert_management/payload/base_spec.rb b/spec/lib/gitlab/alert_management/payload/base_spec.rb index 0c26e94e596..e093b3587c2 100644 --- a/spec/lib/gitlab/alert_management/payload/base_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/base_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::AlertManagement::Payload::Base do let_it_be(:project) { create(:project) } + let(:raw_payload) { {} } let(:payload_class) { described_class } diff --git a/spec/lib/gitlab/alert_management/payload/generic_spec.rb b/spec/lib/gitlab/alert_management/payload/generic_spec.rb index b0c238c62c8..59933f7459d 100644 --- a/spec/lib/gitlab/alert_management/payload/generic_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/generic_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::AlertManagement::Payload::Generic do let_it_be(:project) { build_stubbed(:project) } + let(:raw_payload) { {} } let(:parsed_payload) { described_class.new(project: project, payload: raw_payload) } diff --git a/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb b/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb index 862b5b2bdc3..fa8afd47c53 100644 --- a/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::AlertManagement::Payload::ManagedPrometheus do let_it_be(:project) { create(:project) } + let(:raw_payload) { {} } let(:parsed_payload) { described_class.new(project: project, payload: raw_payload) } @@ -136,6 +137,7 @@ RSpec.describe Gitlab::AlertManagement::Payload::ManagedPrometheus do context 'with sufficient fallback info' do let_it_be(:environment) { create(:environment, project: project, name: 'production') } + let(:raw_payload) do { 'labels' => { diff --git a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb index f574f5ba6a3..6a4f35c01e3 100644 --- a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do let_it_be(:project) { create(:project) } + let(:raw_payload) { {} } let(:parsed_payload) { described_class.new(project: project, payload: raw_payload) } diff --git a/spec/lib/gitlab/alert_management/payload_spec.rb b/spec/lib/gitlab/alert_management/payload_spec.rb index 7c129a8a48e..efde7ed3772 100644 --- a/spec/lib/gitlab/alert_management/payload_spec.rb +++ b/spec/lib/gitlab/alert_management/payload_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::AlertManagement::Payload do describe '#parse' do let_it_be(:project) { build_stubbed(:project) } + let(:payload) { {} } context 'without a monitoring_tool specified by caller' do diff --git a/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb index 80d3f82b404..0a333965f68 100644 --- a/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb +++ b/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::BaseQueryBuilder do let_it_be(:mr1) { create(:merge_request, target_project: project, source_project: project, allow_broken: true, created_at: 3.months.ago) } let_it_be(:mr2) { create(:merge_request, target_project: project, source_project: project, allow_broken: true, created_at: 1.month.ago) } let_it_be(:user) { create(:user) } + let(:params) { { current_user: user } } let(:records) do stage = build(:cycle_analytics_project_stage, { diff --git a/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb index c1ea000eb7b..14768025932 100644 --- a/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb +++ b/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Analytics::CycleAnalytics::Median do let_it_be(:project) { create(:project, :repository) } + let(:query) { Project.joins(merge_requests: :metrics) } let(:stage) do diff --git a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb index b8f9dde4291..ebc5ae2a632 100644 --- a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb +++ b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb @@ -7,16 +7,15 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do Timecop.freeze { example.run } end + let(:params) { { from: 1.year.ago, current_user: user } } + let_it_be(:project) { create(:project, :empty_repo) } let_it_be(:user) { create(:user) } subject do Gitlab::Analytics::CycleAnalytics::DataCollector.new( stage: stage, - params: { - from: 1.year.ago, - current_user: user - } + params: params ).records_fetcher.serialized_records end @@ -34,6 +33,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do describe 'for issue based stage' do let_it_be(:issue1) { create(:issue, project: project) } let_it_be(:issue2) { create(:issue, project: project, confidential: true) } + let(:stage) do build(:cycle_analytics_project_stage, { start_event_identifier: :plan_stage_start, @@ -130,4 +130,40 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do end end end + + describe 'pagination' do + let_it_be(:issue1) { create(:issue, project: project) } + let_it_be(:issue2) { create(:issue, project: project) } + let_it_be(:issue3) { create(:issue, project: project) } + + let(:stage) do + build(:cycle_analytics_project_stage, { + start_event_identifier: :plan_stage_start, + end_event_identifier: :issue_first_mentioned_in_commit, + project: project + }) + end + + before(:all) do + issue1.metrics.update(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago) + issue2.metrics.update(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago) + issue3.metrics.update(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago) + end + + before do + project.add_user(user, Gitlab::Access::DEVELOPER) + + stub_const('Gitlab::Analytics::CycleAnalytics::RecordsFetcher::MAX_RECORDS', 2) + end + + it 'limits the results' do + expect(subject.size).to eq(2) + end + + it 'loads the record for the next page' do + params[:page] = 2 + + expect(subject.size).to eq(1) + end + end end diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb index 52e9f2d9846..b6f9c8106c9 100644 --- a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb +++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::CodeStageStart do other_merge_request = create(:merge_request, source_project: project, source_branch: 'a', target_branch: 'master') - records = subject.apply_query_customization(MergeRequest.all).where('merge_requests_closing_issues.issue_id IS NOT NULL') + records = subject.apply_query_customization(MergeRequest.all).where.not('merge_requests_closing_issues.issue_id' => nil) expect(records).to eq([merge_request]) expect(records).not_to include(other_merge_request) end diff --git a/spec/lib/gitlab/analytics/unique_visits_spec.rb b/spec/lib/gitlab/analytics/unique_visits_spec.rb index 6ac58e13f4c..f4d5c0b1eca 100644 --- a/spec/lib/gitlab/analytics/unique_visits_spec.rb +++ b/spec/lib/gitlab/analytics/unique_visits_spec.rb @@ -24,18 +24,18 @@ RSpec.describe Gitlab::Analytics::UniqueVisits, :clean_gitlab_redis_shared_state describe '#track_visit' do it 'tracks the unique weekly visits for targets' do - unique_visits.track_visit(visitor1_id, target1_id, 7.days.ago) - unique_visits.track_visit(visitor1_id, target1_id, 7.days.ago) - unique_visits.track_visit(visitor2_id, target1_id, 7.days.ago) + unique_visits.track_visit(target1_id, values: visitor1_id, time: 7.days.ago) + unique_visits.track_visit(target1_id, values: visitor1_id, time: 7.days.ago) + unique_visits.track_visit(target1_id, values: visitor2_id, time: 7.days.ago) - unique_visits.track_visit(visitor2_id, target2_id, 7.days.ago) - unique_visits.track_visit(visitor1_id, target2_id, 8.days.ago) - unique_visits.track_visit(visitor1_id, target2_id, 15.days.ago) + unique_visits.track_visit(target2_id, values: visitor2_id, time: 7.days.ago) + unique_visits.track_visit(target2_id, values: visitor1_id, time: 8.days.ago) + unique_visits.track_visit(target2_id, values: visitor1_id, time: 15.days.ago) - unique_visits.track_visit(visitor3_id, target4_id, 7.days.ago) + unique_visits.track_visit(target4_id, values: visitor3_id, time: 7.days.ago) - unique_visits.track_visit(visitor3_id, target5_id, 15.days.ago) - unique_visits.track_visit(visitor2_id, target5_id, 15.days.ago) + unique_visits.track_visit(target5_id, values: visitor3_id, time: 15.days.ago) + unique_visits.track_visit(target5_id, values: visitor2_id, time: 15.days.ago) expect(unique_visits.unique_visits_for(targets: target1_id)).to eq(2) expect(unique_visits.unique_visits_for(targets: target2_id)).to eq(1) @@ -61,7 +61,7 @@ RSpec.describe Gitlab::Analytics::UniqueVisits, :clean_gitlab_redis_shared_state end it 'sets the keys in Redis to expire automatically after 12 weeks' do - unique_visits.track_visit(visitor1_id, target1_id) + unique_visits.track_visit(target1_id, values: visitor1_id) Gitlab::Redis::SharedState.with do |redis| redis.scan_each(match: "{#{target1_id}}-*").each do |key| @@ -74,7 +74,7 @@ RSpec.describe Gitlab::Analytics::UniqueVisits, :clean_gitlab_redis_shared_state invalid_target_id = "x_invalid" expect do - unique_visits.track_visit(visitor1_id, invalid_target_id) + unique_visits.track_visit(invalid_target_id, values: visitor1_id) end.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent) end end diff --git a/spec/lib/gitlab/application_context_spec.rb b/spec/lib/gitlab/application_context_spec.rb index 0fbbc67ef6a..c4fe2ebaba9 100644 --- a/spec/lib/gitlab/application_context_spec.rb +++ b/spec/lib/gitlab/application_context_spec.rb @@ -27,6 +27,20 @@ RSpec.describe Gitlab::ApplicationContext do end end + describe '.with_raw_context' do + it 'yields the block' do + expect { |b| described_class.with_raw_context({}, &b) }.to yield_control + end + + it 'passes the attributes unaltered on to labkit' do + attrs = { foo: :bar } + + expect(Labkit::Context).to receive(:with_context).with(attrs) + + described_class.with_raw_context(attrs) {} + end + end + describe '.push' do it 'passes the expected context on to labkit' do fake_proc = duck_type(:call) @@ -138,7 +152,7 @@ RSpec.describe Gitlab::ApplicationContext do it 'does not cause queries' do context = described_class.new(project: create(:project), namespace: create(:group, :nested), user: create(:user)) - expect { context.use { Labkit::Context.current.to_h } }.not_to exceed_query_limit(0) + expect { context.use { Gitlab::ApplicationContext.current } }.not_to exceed_query_limit(0) end end end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 3eb015a5a22..f3799c58fed 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -83,7 +83,7 @@ module Gitlab }, 'fenced code with inline script' => { input: '```mypre"><script>alert(3)</script>', - output: "<div>\n<div>\n<pre class=\"code highlight js-syntax-highlight plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">\"></span></code></pre>\n</div>\n</div>" + output: "<div>\n<div>\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">\"></span></code></pre>\n</div>\n</div>" } } @@ -353,7 +353,7 @@ module Gitlab output = <<~HTML <div> <div> - <pre class="code highlight js-syntax-highlight javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre> + <pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre> </div> </div> HTML @@ -380,7 +380,7 @@ module Gitlab <div> <div>class.cpp</div> <div> - <pre class="code highlight js-syntax-highlight cpp" lang="cpp" v-pre="true"><code><span id="LC1" class="line" lang="cpp"><span class="cp">#include <stdio.h></span></span> + <pre class="code highlight js-syntax-highlight language-cpp" lang="cpp" v-pre="true"><code><span id="LC1" class="line" lang="cpp"><span class="cp">#include <stdio.h></span></span> <span id="LC2" class="line" lang="cpp"></span> <span id="LC3" class="line" lang="cpp"><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o"><</span> <span class="mi">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span></span> <span id="LC4" class="line" lang="cpp"> <span class="n">std</span><span class="o">::</span><span class="n">cout</span><span class="o"><<</span><span class="s">"*"</span><span class="o"><<</span><span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span></span> 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 67ffdee0c4a..69068883096 100644 --- a/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Auth::OAuth::AuthHash do - let(:provider) { 'ldap'.freeze } + let(:provider) { 'ldap' } let(:auth_hash) do described_class.new( OmniAuth::AuthHash.new( diff --git a/spec/lib/gitlab/auth/otp/strategies/devise_spec.rb b/spec/lib/gitlab/auth/otp/strategies/devise_spec.rb index 0c88421d456..e51705bdb9c 100644 --- a/spec/lib/gitlab/auth/otp/strategies/devise_spec.rb +++ b/spec/lib/gitlab/auth/otp/strategies/devise_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Auth::Otp::Strategies::Devise do let_it_be(:user) { create(:user) } + let(:otp_code) { 42 } subject(:validate) { described_class.new(user).validate(otp_code) } diff --git a/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb index 88a245b6b10..dc20df98185 100644 --- a/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb +++ b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do let_it_be(:user) { create(:user) } + let(:otp_code) { 42 } let(:host) { 'forti_authenticator.example.com' } 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 368cf98dfec..57ee53a452e 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 @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do let_it_be(:user) { create(:user) } + let(:otp_code) { 42 } let(:url) { 'https://ftc.example.com:9696/api/v1' } diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 4e4bbd1bb60..7a578ad3c90 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do let_it_be(:project) { create(:project) } + let(:gl_auth) { described_class } describe 'constants' do @@ -543,6 +544,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do context 'and belong to different projects' do let_it_be(:other_project) { create(:project) } + let!(:read_registry) { create(:deploy_token, username: 'deployer', read_repository: false, projects: [project]) } let!(:read_repository) { create(:deploy_token, username: read_registry.username, read_registry: false, projects: [other_project]) } let(:auth_success) { Gitlab::Auth::Result.new(read_repository, other_project, :deploy_token, [:download_code]) } 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 50e799908c6..dbf74bd9333 100644 --- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, schema: 2020_04_20_094444 do +RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, schema: 2021_03_13_045845 do let(:gitlab_shell) { Gitlab::Shell.new } let(:users) { table(:users) } let(:snippets) { table(:snippets) } diff --git a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb index 7ad93c3124a..c4c0247ad3e 100644 --- a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb +++ b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb @@ -64,5 +64,13 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo expect(test_table.where('name is NULL and name_convert_to_text is NULL').pluck(:id)).to contain_exactly(15) expect(test_table.where("name_convert_to_text = 'no name'").count).to eq(0) end + + it 'tracks timings of queries' do + expect(subject.batch_metrics.timings).to be_empty + + subject.perform(10, 20, table_name, 'id', sub_batch_size, 'name', 'name_convert_to_text') + + expect(subject.batch_metrics.timings[:update_all]).not_to be_empty + end end end diff --git a/spec/lib/gitlab/background_migration/migrate_pages_to_zip_storage_spec.rb b/spec/lib/gitlab/background_migration/migrate_pages_to_zip_storage_spec.rb new file mode 100644 index 00000000000..557dd8ddee6 --- /dev/null +++ b/spec/lib/gitlab/background_migration/migrate_pages_to_zip_storage_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::MigratePagesToZipStorage do + let(:namespace) { create(:group) } # rubocop: disable RSpec/FactoriesInMigrationSpecs + let(:migration) { described_class.new } + + describe '#perform' do + context 'when there is project to migrate' do + let!(:project) { create_project('project') } + + after do + FileUtils.rm_rf(project.pages_path) + end + + it 'migrates project to zip storage' do + expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, + anything, + ignore_invalid_entries: false, + mark_projects_as_not_deployed: false) do |service| + expect(service).to receive(:execute_for_batch).with(project.id..project.id).and_call_original + end + + migration.perform(project.id, project.id) + + expect(project.reload.pages_metadatum.pages_deployment.file.filename).to eq("_migrated.zip") + end + end + end + + def create_project(path) + project = create(:project) # rubocop: disable RSpec/FactoriesInMigrationSpecs + project.mark_pages_as_deployed + + FileUtils.mkdir_p File.join(project.pages_path, "public") + File.open(File.join(project.pages_path, "public/index.html"), "w") do |f| + f.write("Hello!") + end + + project + end +end diff --git a/spec/lib/gitlab/bullet/exclusions_spec.rb b/spec/lib/gitlab/bullet/exclusions_spec.rb new file mode 100644 index 00000000000..ba42156b0c4 --- /dev/null +++ b/spec/lib/gitlab/bullet/exclusions_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Bullet::Exclusions do + let(:config_file) do + file = Tempfile.new('bullet.yml') + File.basename(file) + end + + let(:exclude) { [] } + let(:config) do + { + exclusions: { + abc: { + merge_request: '_mr_', + path_with_method: true, + exclude: exclude + } + } + } + end + + before do + File.write(config_file, config.deep_stringify_keys.to_yaml) + end + + after do + FileUtils.rm_f(config_file) + end + + describe '#execute' do + subject(:executor) { described_class.new(config_file).execute } + + shared_examples_for 'loads exclusion results' do + let(:config) { { exclusions: { abc: { exclude: exclude } } } } + let(:results) { [exclude] } + + specify do + expect(executor).to match(results) + end + end + + context 'with preferred method of path and method name' do + it_behaves_like 'loads exclusion results' do + let(:exclude) { %w[_path_ _method_] } + end + end + + context 'with file pattern' do + it_behaves_like 'loads exclusion results' do + let(:exclude) { ['_file_pattern_'] } + end + end + + context 'with file name and line range' do + it_behaves_like 'loads exclusion results' do + let(:exclude) { ['file_name.rb', 5..10] } + end + end + + context 'without exclusions' do + it_behaves_like 'loads exclusion results' do + let(:exclude) { [] } + end + end + + context 'without exclusions key in config' do + it_behaves_like 'loads exclusion results' do + let(:config) { {} } + let(:results) { [] } + end + end + + context 'when config file does not exist' do + it 'provides an empty array for exclusions' do + expect(described_class.new('_some_bogus_file_').execute).to match([]) + end + end + end + + describe '#validate_paths!' do + context 'when validating scenarios' do + let(:source_file) do + file = Tempfile.new('bullet_test_source_file.rb') + File.basename(file) + end + + subject { described_class.new(config_file).validate_paths! } + + before do + FileUtils.touch(source_file) + end + + after do + FileUtils.rm_f(source_file) + end + + context 'when using paths with method name' do + let(:exclude) { [source_file, '_method_'] } + + context 'when source file for exclusion exists' do + specify do + expect { subject }.not_to raise_error + end + end + + context 'when source file for exclusion does not exist' do + let(:exclude) { %w[_bogus_file_ _method_] } + + specify do + expect { subject }.to raise_error(RuntimeError) + end + end + end + + context 'when using path only' do + let(:exclude) { [source_file] } + + context 'when source file for exclusion exists' do + specify do + expect { subject }.not_to raise_error + end + end + + context 'when source file for exclusion does not exist' do + let(:exclude) { '_bogus_file_' } + + specify do + expect { subject }.to raise_error(RuntimeError) + end + end + end + + context 'when path_with_method is false for a file pattern' do + let(:exclude) { ['_file_pattern_'] } + let(:config) do + { + exclusions: { + abc: { + merge_request: '_mr_', + path_with_method: false, + exclude: exclude + } + } + } + end + + specify do + expect { subject }.not_to raise_error + end + end + end + end +end diff --git a/spec/lib/gitlab/bullet_spec.rb b/spec/lib/gitlab/bullet_spec.rb new file mode 100644 index 00000000000..1262a0b8bde --- /dev/null +++ b/spec/lib/gitlab/bullet_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Bullet do + describe '#enabled?' do + it 'is enabled' do + stub_env('ENABLE_BULLET', true) + + expect(described_class.enabled?).to be(true) + end + + it 'is not enabled' do + stub_env('ENABLE_BULLET', nil) + + expect(described_class.enabled?).to be(false) + end + + it 'is correctly aliased for #extra_logging_enabled?' do + expect(described_class.method(:extra_logging_enabled?).original_name).to eq(:enabled?) + end + end + + describe '#configure_bullet?' do + context 'with ENABLE_BULLET true' do + before do + stub_env('ENABLE_BULLET', true) + end + + it 'is configurable' do + expect(described_class.configure_bullet?).to be(true) + end + end + + context 'with ENABLE_BULLET falsey' do + before do + stub_env('ENABLE_BULLET', nil) + end + + it 'is not configurable' do + expect(described_class.configure_bullet?).to be(false) + end + + it 'is configurable in development' do + allow(Rails).to receive_message_chain(:env, :development?).and_return(true) + + expect(described_class.configure_bullet?).to be(true) + end + end + end +end diff --git a/spec/lib/gitlab/changelog/config_spec.rb b/spec/lib/gitlab/changelog/config_spec.rb index 51988acf3d1..2809843b832 100644 --- a/spec/lib/gitlab/changelog/config_spec.rb +++ b/spec/lib/gitlab/changelog/config_spec.rb @@ -37,7 +37,8 @@ RSpec.describe Gitlab::Changelog::Config do project, 'date_format' => 'foo', 'template' => 'bar', - 'categories' => { 'foo' => 'bar' } + 'categories' => { 'foo' => 'bar' }, + 'tag_regex' => 'foo' ) expect(config.date_format).to eq('foo') @@ -45,6 +46,7 @@ RSpec.describe Gitlab::Changelog::Config do .to be_instance_of(Gitlab::Changelog::AST::Expressions) expect(config.categories).to eq({ 'foo' => 'bar' }) + expect(config.tag_regex).to eq('foo') end it 'raises Error when the categories are not a Hash' do diff --git a/spec/lib/gitlab/checks/project_created_spec.rb b/spec/lib/gitlab/checks/project_created_spec.rb index f099f19b061..74e43b04b6b 100644 --- a/spec/lib/gitlab/checks/project_created_spec.rb +++ b/spec/lib/gitlab/checks/project_created_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Checks::ProjectCreated, :clean_gitlab_redis_shared_state do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :repository, namespace: user.namespace) } + let(:protocol) { 'http' } let(:git_user) { user } let(:repository) { project.repository } diff --git a/spec/lib/gitlab/checks/project_moved_spec.rb b/spec/lib/gitlab/checks/project_moved_spec.rb index c7dad0a91d4..469aea8d093 100644 --- a/spec/lib/gitlab/checks/project_moved_spec.rb +++ b/spec/lib/gitlab/checks/project_moved_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) } + let(:repository) { project.repository } let(:protocol) { 'http' } let(:git_user) { user } @@ -101,6 +102,7 @@ RSpec.describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do context 'with project snippet' do let_it_be(:snippet) { create(:project_snippet, :repository, project: project, author: user) } + let(:repository) { snippet.repository } it_behaves_like 'errors per protocol' do @@ -111,6 +113,7 @@ RSpec.describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do context 'with personal snippet' do let_it_be(:snippet) { create(:personal_snippet, :repository, author: user) } + let(:repository) { snippet.repository } it 'returns nil' do diff --git a/spec/lib/gitlab/ci/ansi2json/style_spec.rb b/spec/lib/gitlab/ci/ansi2json/style_spec.rb index ff70ff69aaa..87085950a9f 100644 --- a/spec/lib/gitlab/ci/ansi2json/style_spec.rb +++ b/spec/lib/gitlab/ci/ansi2json/style_spec.rb @@ -160,9 +160,9 @@ RSpec.describe Gitlab::Ci::Ansi2json::Style do with_them do it 'change the style' do style = described_class.new - style.update(initial_state) + style.update(initial_state) # rubocop:disable Rails/SaveBang - style.update(ansi_commands) + style.update(ansi_commands) # rubocop:disable Rails/SaveBang expect(style.to_s).to eq(result) end diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb index 179578fe0a8..d294eca7f15 100644 --- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb @@ -107,6 +107,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do stage: 'test', only: { refs: %w[branches tags] }, variables: {}, + job_variables: {}, + root_variables_inheritance: true, scheduling_type: :stage) end end @@ -130,6 +132,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do stage: 'test', only: { refs: %w[branches tags] }, variables: {}, + job_variables: {}, + root_variables_inheritance: true, scheduling_type: :stage) end end @@ -284,6 +288,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do parallel: { matrix: [{ 'PROVIDER' => ['aws'], 'STACK' => %w(monitoring app1) }, { 'PROVIDER' => ['gcp'], 'STACK' => %w(data) }] }, variables: {}, + job_variables: {}, + root_variables_inheritance: true, scheduling_type: :stage ) end diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb index 064990667d5..cec1c97085b 100644 --- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb @@ -13,6 +13,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end describe '#valid?' do + context 'with an empty hash as cache' do + let(:config) { {} } + + it 'is valid' do + expect(entry).to be_valid + end + end + context 'when configuration is valid with a single cache' do let(:config) { { key: 'key', paths: ["logs/"], untracked: true } } diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index a4167003987..ffcd029172a 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -663,6 +663,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do after_script: %w[cleanup], only: { refs: %w[branches tags] }, variables: {}, + job_variables: {}, + root_variables_inheritance: true, scheduling_type: :stage) end end diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb index ac6b589ec6b..cb73044b62b 100644 --- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb @@ -100,6 +100,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Jobs do stage: 'test', trigger: { project: 'my/project' }, variables: {}, + job_variables: {}, + root_variables_inheritance: true, scheduling_type: :stage }, regular_job: { @@ -109,6 +111,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Jobs do script: ['something'], stage: 'test', variables: {}, + job_variables: {}, + root_variables_inheritance: true, scheduling_type: :stage }) end diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb index 04e80450263..016d59e98b9 100644 --- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb @@ -382,7 +382,9 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do context 'with only job variables' do it 'does return defined variables' do expect(entry.value).to include( - variables: { 'A' => 'job', 'B' => 'job' } + variables: { 'A' => 'job', 'B' => 'job' }, + job_variables: { 'A' => 'job', 'B' => 'job' }, + root_variables_inheritance: true ) end end @@ -394,9 +396,11 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do ).value end - it 'does return all variables and overwrite them' do + it 'does return job and root variables' do expect(entry.value).to include( - variables: { 'A' => 'job', 'B' => 'job', 'C' => 'root', 'D' => 'root' } + variables: { 'A' => 'job', 'B' => 'job', 'C' => 'root', 'D' => 'root' }, + job_variables: { 'A' => 'job', 'B' => 'job' }, + root_variables_inheritance: true ) end @@ -408,9 +412,11 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do } end - it 'does return only job variables' do + it 'does return job and root variables' do expect(entry.value).to include( - variables: { 'A' => 'job', 'B' => 'job' } + variables: { 'A' => 'job', 'B' => 'job' }, + job_variables: { 'A' => 'job', 'B' => 'job' }, + root_variables_inheritance: false ) end end @@ -423,9 +429,11 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do } end - it 'does return only job variables' do + it 'does return job and root variables' do expect(entry.value).to include( - variables: { 'A' => 'job', 'B' => 'job', 'D' => 'root' } + variables: { 'A' => 'job', 'B' => 'job', 'D' => 'root' }, + job_variables: { 'A' => 'job', 'B' => 'job' }, + root_variables_inheritance: ['D'] ) end end @@ -493,7 +501,9 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do name: :rspec, stage: 'test', only: { refs: %w[branches tags] }, - variables: {} + variables: {}, + job_variables: {}, + root_variables_inheritance: true ) end end diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb index 7b38c21788f..041eb748fc9 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -133,6 +133,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do stage: 'test', cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, + job_variables: {}, + root_variables_inheritance: true, ignore: false, after_script: ['make clean'], only: { refs: %w[branches tags] }, @@ -147,6 +149,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do stage: 'test', cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, + job_variables: {}, + root_variables_inheritance: true, ignore: false, after_script: ['make clean'], only: { refs: %w[branches tags] }, @@ -163,6 +167,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }], only: { refs: %w(branches tags) }, variables: { 'VAR' => 'job', 'VAR2' => 'val 2' }, + job_variables: { 'VAR' => 'job' }, + root_variables_inheritance: true, after_script: [], ignore: false, scheduling_type: :stage } @@ -188,6 +194,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, + job_variables: {}, + root_variables_inheritance: true, ignore: false, after_script: ['make clean'], only: { refs: %w[branches tags] }, @@ -202,6 +210,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, + job_variables: {}, + root_variables_inheritance: true, ignore: false, after_script: ['make clean'], only: { refs: %w[branches tags] }, @@ -218,6 +228,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }, only: { refs: %w(branches tags) }, variables: { 'VAR' => 'job', 'VAR2' => 'val 2' }, + job_variables: { 'VAR' => 'job' }, + root_variables_inheritance: true, after_script: [], ignore: false, scheduling_type: :stage } @@ -267,6 +279,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, variables: { 'VAR' => 'root' }, + job_variables: {}, + root_variables_inheritance: true, ignore: false, after_script: ['make clean'], only: { refs: %w[branches tags] }, @@ -279,6 +293,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, variables: { 'VAR' => 'job' }, + job_variables: { 'VAR' => 'job' }, + root_variables_inheritance: true, ignore: false, after_script: ['make clean'], only: { refs: %w[branches tags] }, @@ -311,6 +327,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do stage: 'test', cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], variables: { 'VAR' => 'root' }, + job_variables: {}, + root_variables_inheritance: true, ignore: false, after_script: ['make clean'], only: { refs: %w[branches tags] }, @@ -323,6 +341,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do stage: 'test', cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], variables: { 'VAR' => 'job' }, + job_variables: { 'VAR' => 'job' }, + root_variables_inheritance: true, ignore: false, after_script: ['make clean'], only: { refs: %w[branches tags] }, diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index 99f546ceb37..e5b008a482e 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -324,5 +324,39 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do end end end + + context 'when local file path has wildcard' do + let(:project) { create(:project, :repository) } + + let(:values) do + { include: 'myfolder/*.yml' } + end + + before do + allow_next_instance_of(Repository) do |repository| + allow(repository).to receive(:search_files_by_wildcard_path).with('myfolder/*.yml', '123456') do + ['myfolder/file1.yml', 'myfolder/file2.yml'] + end + end + end + + it 'includes the matched local files' do + expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Local), + an_instance_of(Gitlab::Ci::Config::External::File::Local)) + + expect(subject.map(&:location)).to contain_exactly('myfolder/file1.yml', 'myfolder/file2.yml') + end + + context 'when the FF ci_wildcard_file_paths is disabled' do + before do + stub_feature_flags(ci_wildcard_file_paths: false) + end + + it 'cannot find any file returns an error message' do + expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Local)) + expect(subject[0].errors).to eq(['Local file `myfolder/*.yml` does not exist!']) + end + end + end end end diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index d2d7116bb12..d657c3e943f 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -366,5 +366,40 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do expect(output.keys).to match_array([:image, :my_build, :my_test]) end end + + context 'when local file path has wildcard' do + let_it_be(:project) { create(:project, :repository) } + + let(:values) do + { include: 'myfolder/*.yml', image: 'ruby:2.7' } + end + + before do + allow_next_instance_of(Repository) do |repository| + allow(repository).to receive(:search_files_by_wildcard_path).with('myfolder/*.yml', sha) do + ['myfolder/file1.yml', 'myfolder/file2.yml'] + end + + allow(repository).to receive(:blob_data_at).with(sha, 'myfolder/file1.yml') do + <<~HEREDOC + my_build: + script: echo Hello World + HEREDOC + end + + allow(repository).to receive(:blob_data_at).with(sha, 'myfolder/file2.yml') do + <<~HEREDOC + my_test: + script: echo Hello World + HEREDOC + end + end + end + + it 'fetches the matched files' do + output = processor.perform + expect(output.keys).to match_array([:image, :my_build, :my_test]) + end + end end end diff --git a/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb b/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb index fbf86927bd9..e5f0341c5fe 100644 --- a/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb +++ b/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true require 'fast_spec_helper' +require 'support/helpers/stubbed_feature' +require 'support/helpers/stub_feature_flags' RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do + include StubFeatureFlags + describe '.applies_to?' do subject { described_class.applies_to?(config) } @@ -49,6 +53,10 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do variables: { 'PROVIDER' => 'aws', 'STACK' => 'app1' + }, + job_variables: { + 'PROVIDER' => 'aws', + 'STACK' => 'app1' } }, { @@ -58,6 +66,10 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do variables: { 'PROVIDER' => 'aws', 'STACK' => 'app2' + }, + job_variables: { + 'PROVIDER' => 'aws', + 'STACK' => 'app2' } }, { @@ -67,6 +79,10 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do variables: { 'PROVIDER' => 'ovh', 'STACK' => 'app' + }, + job_variables: { + 'PROVIDER' => 'ovh', + 'STACK' => 'app' } }, { @@ -76,6 +92,10 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do variables: { 'PROVIDER' => 'gcp', 'STACK' => 'app' + }, + job_variables: { + 'PROVIDER' => 'gcp', + 'STACK' => 'app' } } ] diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index 67324c09d86..aaa3a7a8b9d 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -92,7 +92,7 @@ RSpec.describe Gitlab::Ci::Lint do it 'sets merged_config' do root_config = YAML.safe_load(content, [Symbol]) included_config = YAML.safe_load(included_content, [Symbol]) - expected_config = included_config.merge(root_config).except(:include) + expected_config = included_config.merge(root_config).except(:include).deep_stringify_keys expect(subject.merged_yaml).to eq(expected_config.to_yaml) end diff --git a/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb b/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb index c6b8cf2a985..6a08e8f0b7f 100644 --- a/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb +++ b/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb @@ -131,7 +131,6 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do expect { parse }.not_to raise_error expect(codequality_report.degradations_count).to eq(0) - expect(codequality_report.error_message).to eq("Invalid degradation format: The property '#/' did not contain a required property of 'location'") 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 9ca5aeeea58..900dfec38e2 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -321,4 +321,25 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do it { is_expected.to be_falsey } end end + + describe '#increment_pipeline_failure_reason_counter' do + let(:command) { described_class.new } + let(:reason) { :size_limit_exceeded } + + subject { command.increment_pipeline_failure_reason_counter(reason) } + + it 'increments the error metric' do + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { subject }.to change { counter.get(reason: reason.to_s) }.by(1) + end + + context 'when the reason is nil' do + let(:reason) { nil } + + it 'increments the error metric with unknown_failure' do + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { subject }.to change { counter.get(reason: 'unknown_failure') }.by(1) + end + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb index 4ae51ac8bf9..e30a78546af 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb @@ -16,8 +16,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules do describe '#perform!' do context 'when pipeline has been skipped by workflow configuration' do before do - allow(step).to receive(:workflow_passed?) - .and_return(false) + allow(step).to receive(:workflow_rules_result) + .and_return( + double(pass?: false, variables: {}) + ) step.perform! end @@ -33,12 +35,18 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules do it 'attaches an error to the pipeline' do expect(pipeline.errors[:base]).to include('Pipeline filtered out by workflow rules.') end + + it 'saves workflow_rules_result' do + expect(command.workflow_rules_result.variables).to eq({}) + end end context 'when pipeline has not been skipped by workflow configuration' do before do - allow(step).to receive(:workflow_passed?) - .and_return(true) + allow(step).to receive(:workflow_rules_result) + .and_return( + double(pass?: true, variables: { 'VAR1' => 'val2' }) + ) step.perform! end @@ -55,6 +63,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules do it 'attaches no errors' do expect(pipeline.errors).to be_empty end + + it 'saves workflow_rules_result' do + expect(command.workflow_rules_result.variables).to eq({ 'VAR1' => 'val2' }) + end end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb new file mode 100644 index 00000000000..bcea6462790 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Chain::Helpers do + let(:helper_class) do + Class.new do + include Gitlab::Ci::Pipeline::Chain::Helpers + + attr_accessor :pipeline, :command + + def initialize(pipeline, command) + self.pipeline = pipeline + self.command = command + end + end + end + + subject(:helper) { helper_class.new(pipeline, command) } + + let(:pipeline) { build(:ci_empty_pipeline) } + let(:command) { double(save_incompleted: true) } + let(:message) { 'message' } + + describe '.error' do + shared_examples 'error function' do + specify do + expect(pipeline).to receive(:drop!).with(drop_reason).and_call_original + expect(pipeline).to receive(:add_error_message).with(message).and_call_original + expect(pipeline).to receive(:ensure_project_iid!).twice.and_call_original + + subject.error(message, config_error: config_error, drop_reason: drop_reason) + + expect(pipeline.yaml_errors).to eq(yaml_error) + expect(pipeline.errors[:base]).to include(message) + end + end + + context 'when given a drop reason' do + context 'when config error is true' do + context 'sets the yaml error and overrides the drop reason' do + let(:drop_reason) { :config_error } + let(:config_error) { true } + let(:yaml_error) { message } + + it_behaves_like "error function" + end + end + + context 'when config error is false' do + context 'does not set the yaml error or override the drop reason' do + let(:drop_reason) { :size_limit_exceeded } + let(:config_error) { false } + let(:yaml_error) { nil } + + it_behaves_like "error function" + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb index 78363be7f36..23cdec61bb3 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb @@ -11,7 +11,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do let(:save_incompleted) { false } let(:command) do - double(:command, + Gitlab::Ci::Pipeline::Chain::Command.new( project: project, pipeline_seed: pipeline_seed, save_incompleted: save_incompleted @@ -49,6 +49,11 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do expect(pipeline.deployments_limit_exceeded?).to be true end + + it 'calls increment_pipeline_failure_reason_counter' do + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { perform }.to change { counter.get(reason: 'deployments_limit_exceeded') }.by(1) + end end context 'when not saving incomplete pipelines' do @@ -71,6 +76,12 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do expect(pipeline.errors.messages).to include(base: ['Pipeline has too many deployments! Requested 2, but the limit is 1.']) end + + it 'increments the error metric' do + expect(command).to receive(:increment_pipeline_failure_reason_counter).with(:deployments_limit_exceeded) + + perform + end end it 'logs the error' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/pipeline/process_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/pipeline/process_spec.rb new file mode 100644 index 00000000000..3885cea2d1b --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/pipeline/process_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Chain::Pipeline::Process do + let_it_be(:project) { build(:project) } + let_it_be(:user) { build(:user) } + let_it_be(:pipeline) { build(:ci_pipeline, project: project, id: 42) } + + let_it_be(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user) + end + + let(:step) { described_class.new(pipeline, command) } + + describe '#perform!' do + subject(:perform) { step.perform! } + + it 'schedules a job to process the pipeline' do + expect(Ci::InitialPipelineProcessWorker) + .to receive(:perform_async) + .with(42) + + perform + end + end + + describe '#break?' do + it { expect(step.break?).to be_falsey } + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb index 5506b079d0f..62de4d2e96d 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb @@ -22,6 +22,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Populate do [ Gitlab::Ci::Pipeline::Chain::Config::Content.new(pipeline, command), Gitlab::Ci::Pipeline::Chain::Config::Process.new(pipeline, command), + Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules.new(pipeline, command), Gitlab::Ci::Pipeline::Chain::SeedBlock.new(pipeline, command), Gitlab::Ci::Pipeline::Chain::Seed.new(pipeline, command) ] @@ -95,6 +96,11 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Populate do it 'wastes pipeline iid' do expect(InternalId.ci_pipelines.where(project_id: project.id).last.last_value).to be > 0 end + + it 'increments the error metric' do + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { run_chain }.to change { counter.get(reason: 'unknown_failure') }.by(1) + end end describe 'pipeline protect' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb index 80013cab6ee..264076859cb 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb @@ -3,24 +3,16 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do - let(:project) { create(:project, :repository) } - let(:user) { create(:user, developer_projects: [project]) } - let(:seeds_block) { } - - let(:command) do - Gitlab::Ci::Pipeline::Chain::Command.new( - project: project, - current_user: user, - origin_ref: 'master', - seeds_block: seeds_block) - end + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user, developer_projects: [project]) } + let(:seeds_block) { } + let(:command) { initialize_command } let(:pipeline) { build(:ci_pipeline, project: project) } describe '#perform!' do before do stub_ci_pipeline_yaml_file(YAML.dump(config)) - run_chain end let(:config) do @@ -28,23 +20,25 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do end subject(:run_chain) do - [ - Gitlab::Ci::Pipeline::Chain::Config::Content.new(pipeline, command), - Gitlab::Ci::Pipeline::Chain::Config::Process.new(pipeline, command) - ].map(&:perform!) - - described_class.new(pipeline, command).perform! + run_previous_chain(pipeline, command) + perform_seed(pipeline, command) end it 'allocates next IID' do + run_chain + expect(pipeline.iid).to be_present end it 'ensures ci_ref' do + run_chain + expect(pipeline.ci_ref).to be_present end it 'sets the seeds in the command object' do + run_chain + expect(command.pipeline_seed).to be_a(Gitlab::Ci::Pipeline::Seed::Pipeline) expect(command.pipeline_seed.size).to eq 1 end @@ -59,6 +53,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do end it 'correctly fabricates stages and builds' do + run_chain + seed = command.pipeline_seed expect(seed.stages.size).to eq 2 @@ -84,6 +80,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do end it 'returns pipeline seed with jobs only assigned to master' do + run_chain + seed = command.pipeline_seed expect(seed.size).to eq 1 @@ -103,6 +101,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do end it 'returns pipeline seed with jobs only assigned to schedules' do + run_chain + seed = command.pipeline_seed expect(seed.size).to eq 1 @@ -130,6 +130,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do let(:pipeline) { build(:ci_pipeline, project: project) } it 'returns seeds for kubernetes dependent job' do + run_chain + seed = command.pipeline_seed expect(seed.size).to eq 2 @@ -141,6 +143,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do context 'when kubernetes is not active' do it 'does not return seeds for kubernetes dependent job' do + run_chain + seed = command.pipeline_seed expect(seed.size).to eq 1 @@ -158,6 +162,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do end it 'returns stage seeds only when variables expression is truthy' do + run_chain + seed = command.pipeline_seed expect(seed.size).to eq 1 @@ -171,8 +177,125 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do end it 'does not execute the block' do + run_chain + expect(pipeline.variables.size).to eq(0) end end + + describe '#root_variables' do + let(:config) do + { + variables: { VAR1: 'var 1' }, + workflow: { + rules: [{ if: '$CI_PIPELINE_SOURCE', + variables: { VAR1: 'overridden var 1' } }, + { when: 'always' }] + }, + rspec: { script: 'rake' } + } + end + + let(:rspec_variables) { command.pipeline_seed.stages[0].statuses[0].variables.to_hash } + + it 'sends root variable with overridden by rules' do + run_chain + + expect(rspec_variables['VAR1']).to eq('overridden var 1') + end + + context 'when the FF ci_workflow_rules_variables is disabled' do + before do + stub_feature_flags(ci_workflow_rules_variables: false) + end + + it 'sends root variable' do + run_chain + + expect(rspec_variables['VAR1']).to eq('var 1') + end + end + end + + context 'N+1 queries' do + it 'avoids N+1 queries when calculating variables of jobs' do + pipeline1, command1 = prepare_pipeline1 + pipeline2, command2 = prepare_pipeline2 + + control = ActiveRecord::QueryRecorder.new do + perform_seed(pipeline1, command1) + end + + expect { perform_seed(pipeline2, command2) }.not_to exceed_query_limit( + control.count + expected_extra_queries + ) + end + + private + + def prepare_pipeline1 + config1 = { build: { stage: 'build', script: 'build' } } + stub_ci_pipeline_yaml_file(YAML.dump(config1)) + pipeline1 = build(:ci_pipeline, project: project) + command1 = initialize_command + + run_previous_chain(pipeline1, command1) + + [pipeline1, command1] + end + + def prepare_pipeline2 + config2 = { build1: { stage: 'build', script: 'build1' }, + build2: { stage: 'build', script: 'build2' }, + test: { stage: 'build', script: 'test' } } + stub_ci_pipeline_yaml_file(YAML.dump(config2)) + pipeline2 = build(:ci_pipeline, project: project) + command2 = initialize_command + + run_previous_chain(pipeline2, command2) + + [pipeline2, command2] + end + + def expected_extra_queries + extra_jobs = 2 + non_handled_sql_queries = 3 + + # 1. Ci::Build Load () SELECT "ci_builds".* FROM "ci_builds" + # WHERE "ci_builds"."type" = 'Ci::Build' + # AND "ci_builds"."commit_id" IS NULL + # AND ("ci_builds"."retried" = FALSE OR "ci_builds"."retried" IS NULL) + # AND (stage_idx < 1) + # 2. Ci::InstanceVariable Load => `Ci::InstanceVariable#cached_data` => already cached with `fetch_memory_cache` + # 3. Ci::Variable Load => `Project#ci_variables_for` => already cached with `Gitlab::SafeRequestStore` + + extra_jobs * non_handled_sql_queries + end + end + + private + + def run_previous_chain(pipeline, command) + [ + Gitlab::Ci::Pipeline::Chain::Config::Content.new(pipeline, command), + Gitlab::Ci::Pipeline::Chain::Config::Process.new(pipeline, command), + Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules.new(pipeline, command) + ].map(&:perform!) + end + + def perform_seed(pipeline, command) + described_class.new(pipeline, command).perform! + end + end + + private + + def initialize_command + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, + current_user: user, + origin_ref: 'master', + seeds_block: seeds_block + ) 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 e55281f9705..caf3a053c4e 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do - let(:project) { create(:project) } - let(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } let(:pipeline) { build(:ci_empty_pipeline, user: user, project: project) } let!(:step) { described_class.new(pipeline, command) } @@ -42,6 +42,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do end let(:save_incompleted) { true } + let(:dot_com) { true } let(:command) do Gitlab::Ci::Pipeline::Chain::Command.new( project: project, current_user: user, yaml_processor_result: yaml_processor_result, save_incompleted: save_incompleted @@ -51,11 +52,79 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do describe '#perform!' do subject(:perform!) { step.perform! } - context 'when validation returns true' do + let(:validation_service_url) { 'https://validation-service.external/' } + + before do + stub_env('EXTERNAL_VALIDATION_SERVICE_URL', validation_service_url) + allow(Gitlab).to receive(:com?).and_return(dot_com) + allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('correlation-id') + end + + context 'with configuration values in ApplicationSetting' do + let(:alternate_validation_service_url) { 'https://alternate-validation-service.external/' } + let(:validation_service_token) { 'SECURE_TOKEN' } + let(:shorter_timeout) { described_class::DEFAULT_VALIDATION_REQUEST_TIMEOUT - 1 } + before do - allow(step).to receive(:validate_external).and_return(true) + stub_env('EXTERNAL_VALIDATION_SERVICE_TOKEN', 'TOKEN_IN_ENV') + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:external_pipeline_validation_service_timeout).and_return(shorter_timeout) + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:external_pipeline_validation_service_token).and_return(validation_service_token) + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:external_pipeline_validation_service_url).and_return(alternate_validation_service_url) + end + + it 'uses those values rather than env vars or defaults' do + expect(::Gitlab::HTTP).to receive(:post) do |url, params| + expect(url).to eq(alternate_validation_service_url) + expect(params[:timeout]).to eq(shorter_timeout) + expect(params[:headers]).to include('X-Gitlab-Token' => validation_service_token) + expect(params[:timeout]).to eq(shorter_timeout) + end + + perform! + end + end + + it 'respects the defined payload schema' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + expect(params[:body]).to match_schema('/external_validation') + expect(params[:timeout]).to eq(described_class::DEFAULT_VALIDATION_REQUEST_TIMEOUT) + expect(params[:headers]).to eq({ 'X-Gitlab-Correlation-id' => 'correlation-id' }) + end + + perform! + end + + context 'with EXTERNAL_VALIDATION_SERVICE_TIMEOUT defined' do + before do + stub_env('EXTERNAL_VALIDATION_SERVICE_TIMEOUT', validation_service_timeout) + end + + context 'with valid value' do + let(:validation_service_timeout) { '1' } + + it 'uses defined timeout' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + expect(params[:timeout]).to eq(1) + end + + perform! + end + end + + context 'with invalid value' do + let(:validation_service_timeout) { '??' } + + it 'uses default timeout' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + expect(params[:timeout]).to eq(described_class::DEFAULT_VALIDATION_REQUEST_TIMEOUT) + end + + perform! + end end + end + shared_examples 'successful external authorization' do it 'does not drop the pipeline' do perform! @@ -76,9 +145,117 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do end end - context 'when validation return false' do + context 'when EXTERNAL_VALIDATION_SERVICE_TOKEN is set' do + before do + stub_env('EXTERNAL_VALIDATION_SERVICE_TOKEN', '123') + end + + it 'passes token in X-Gitlab-Token header' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + expect(params[:headers]).to include({ 'X-Gitlab-Token' => '123' }) + end + + perform! + end + end + + context 'when validation returns 200 OK' do + before do + stub_request(:post, validation_service_url).to_return(status: 200, body: "{}") + end + + it_behaves_like 'successful external authorization' + end + + context 'when validation returns 404 Not Found' do before do - allow(step).to receive(:validate_external).and_return(false) + stub_request(:post, validation_service_url).to_return(status: 404, body: "{}") + end + + it_behaves_like 'successful external authorization' + end + + context 'when validation returns 500 Internal Server Error' do + before do + stub_request(:post, validation_service_url).to_return(status: 500, body: "{}") + end + + it_behaves_like 'successful external authorization' + end + + context 'when validation raises exceptions' do + before do + stub_request(:post, validation_service_url).to_raise(Net::OpenTimeout) + end + + it_behaves_like 'successful external authorization' + + it 'logs exceptions' do + expect(Gitlab::ErrorTracking).to receive(:track_exception) + .with(instance_of(Net::OpenTimeout), { project_id: project.id }) + + perform! + end + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(ci_external_validation_service: false) + stub_request(:post, validation_service_url) + end + + it 'does not drop the pipeline' do + perform! + + expect(pipeline.status).not_to eq('failed') + expect(pipeline.errors).to be_empty + end + + it 'does not break the chain' do + perform! + + expect(step.break?).to be false + end + + it 'does not make requests' do + perform! + + expect(WebMock).not_to have_requested(:post, validation_service_url) + end + end + + context 'when not on .com' do + let(:dot_com) { false } + + before do + stub_feature_flags(ci_external_validation_service: false) + stub_request(:post, validation_service_url).to_return(status: 404, body: "{}") + end + + it 'drops the pipeline' do + perform! + + expect(pipeline.status).to eq('failed') + expect(pipeline).to be_persisted + expect(pipeline.errors.to_a).to include('External validation failed') + end + + it 'breaks the chain' do + perform! + + expect(step.break?).to be true + end + + it 'logs the authorization' do + expect(Gitlab::AppLogger).to receive(:info).with(message: 'Pipeline not authorized', project_id: project.id, user_id: user.id) + + perform! + end + end + + context 'when validation returns 406 Not Acceptable' do + before do + stub_request(:post, validation_service_url).to_return(status: 406, body: "{}") end it 'drops the pipeline' do @@ -126,16 +303,4 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do end end end - - describe '#validation_service_payload' do - subject(:validation_service_payload) { step.send(:validation_service_payload, pipeline, command.yaml_processor_result.stages_attributes) } - - it 'respects the defined schema' do - expect(validation_service_payload).to match_schema('/external_validation') - end - - it 'does not fire sql queries' do - expect { validation_service_payload }.not_to exceed_query_limit(1) - end - end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 7ec6949f852..f97935feb86 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -6,10 +6,12 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do let_it_be(:project) { create(:project, :repository) } let_it_be(:head_sha) { project.repository.head_commit.id } let(:pipeline) { build(:ci_empty_pipeline, project: project, sha: head_sha) } + let(:root_variables) { [] } + let(:seed_context) { double(pipeline: pipeline, root_variables: root_variables) } let(:attributes) { { name: 'rspec', ref: 'master', scheduling_type: :stage } } let(:previous_stages) { [] } - let(:seed_build) { described_class.new(pipeline, attributes, previous_stages) } + let(:seed_build) { described_class.new(seed_context, attributes, previous_stages) } describe '#attributes' do subject { seed_build.attributes } @@ -75,8 +77,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 }, - { key: 'VAR2', value: 'var 2', public: true }], + job_variables: [{ key: 'VAR1', value: 'var 1', public: true }, + { key: 'VAR2', value: 'var 2', public: true }], rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] } end @@ -301,6 +303,133 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) } end end + + context 'with workflow:rules:[variables:]' 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 }], + 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 }] + end + + context 'when root_variables_inheritance is true' do + let(:root_variables_inheritance) { true } + + 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 }] + ) + end + + context 'when FF ci_workflow_rules_variables is disabled' do + before do + stub_feature_flags(ci_workflow_rules_variables: false) + end + + it 'returns existing yaml variables' do + expect(subject[:yaml_variables]).to match_array( + [{ key: 'VAR2', value: 'var 2', public: true }, + { key: 'VAR3', value: 'var 3', public: true }] + ) + end + end + end + + context 'when root_variables_inheritance is false' do + let(:root_variables_inheritance) { false } + + 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 }] + ) + end + end + + context 'when root_variables_inheritance is an array' do + let(:root_variables_inheritance) { %w(VAR1 VAR2 VAR3) } + + 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 }] + ) + end + end + end + + context 'when the pipeline has not a variable' do + let(:root_variables_inheritance) { true } + + 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 }]) + end + end + end + + context 'when the job rule depends on variables' 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 }], + root_variables_inheritance: root_variables_inheritance, + rules: rules } + end + + let(:root_variables_inheritance) { true } + + context 'when the rules use job variables' do + let(:rules) do + [{ if: '$VAR1 == "var 1"', variables: { VAR1: 'overridden var 1', VAR2: 'new var 2' } }] + 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 }) + end + end + + context 'when the rules use root variables' do + let(:root_variables) do + [{ key: 'VAR2', value: 'var pipeline 2', public: true }] + end + + let(:rules) do + [{ if: '$VAR2 == "var pipeline 2"', variables: { VAR1: 'overridden var 1', VAR2: 'overridden var 2' } }] + 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 }) + 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 }) + end + end + end + end end describe '#bridge?' do @@ -377,7 +506,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'does not have environment' do expect(subject).not_to be_has_environment expect(subject.environment).to be_nil - expect(subject.metadata).to be_nil + expect(subject.metadata&.expanded_environment_name).to be_nil expect(Environment.exists?(name: expected_environment_name)).to eq(false) end end @@ -1080,7 +1209,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do end let(:stage_seed) do - Gitlab::Ci::Pipeline::Seed::Stage.new(pipeline, stage_attributes, []) + Gitlab::Ci::Pipeline::Seed::Stage.new(seed_context, stage_attributes, []) end let(:previous_stages) { [stage_seed] } diff --git a/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb index 860b07647bd..21be8660def 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb @@ -6,6 +6,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Pipeline do let_it_be(:project) { create(:project, :repository) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let(:seed_context) { double(pipeline: pipeline, root_variables: []) } + let(:stages_attributes) do [ { @@ -29,7 +31,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Pipeline do end subject(:seed) do - described_class.new(pipeline, stages_attributes) + described_class.new(seed_context, stages_attributes) end describe '#stages' do diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb index 4b9db9fa6c6..5b04d2abd88 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Stage do let(:project) { create(:project, :repository) } let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:previous_stages) { [] } + let(:seed_context) { double(pipeline: pipeline, root_variables: []) } let(:attributes) do { name: 'test', @@ -16,7 +17,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Stage do end subject do - described_class.new(pipeline, attributes, previous_stages) + described_class.new(seed_context, attributes, previous_stages) end describe '#size' do diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb index b322e55cb5a..8378d096fcf 100644 --- a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb @@ -6,15 +6,17 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do let(:comparer) { described_class.new(base_report, head_report) } let(:base_report) { Gitlab::Ci::Reports::CodequalityReports.new } let(:head_report) { Gitlab::Ci::Reports::CodequalityReports.new } - let(:degradation_1) { build(:codequality_degradation_1) } - let(:degradation_2) { build(:codequality_degradation_2) } + let(:major_degradation) { build(:codequality_degradation, :major) } + let(:minor_degradation) { build(:codequality_degradation, :major) } + let(:critical_degradation) { build(:codequality_degradation, :critical) } + let(:blocker_degradation) { build(:codequality_degradation, :blocker) } describe '#status' do subject(:report_status) { comparer.status } context 'when head report has an error' do before do - head_report.add_degradation(degradation_1) + head_report.add_degradation(major_degradation) end it 'returns status failed' do @@ -50,7 +52,7 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when head report has an error' do before do - head_report.add_degradation(degradation_1) + head_report.add_degradation(major_degradation) end it 'returns the number of new errors' do @@ -70,8 +72,8 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when base report has an error and head has a different error' do before do - base_report.add_degradation(degradation_1) - head_report.add_degradation(degradation_2) + base_report.add_degradation(major_degradation) + head_report.add_degradation(minor_degradation) end it 'counts the base report error as resolved' do @@ -81,7 +83,7 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when base report has errors head has no errors' do before do - base_report.add_degradation(degradation_1) + base_report.add_degradation(major_degradation) end it 'counts the base report errors as resolved' do @@ -91,8 +93,8 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when base report has errors and head has the same error' do before do - base_report.add_degradation(degradation_1) - head_report.add_degradation(degradation_1) + base_report.add_degradation(major_degradation) + head_report.add_degradation(major_degradation) end it 'returns zero' do @@ -102,7 +104,7 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when base report does not have errors and head has errors' do before do - head_report.add_degradation(degradation_1) + head_report.add_degradation(major_degradation) end it 'returns zero' do @@ -124,7 +126,7 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when base report has an error' do before do - base_report.add_degradation(degradation_1) + base_report.add_degradation(major_degradation) end it 'returns zero' do @@ -134,7 +136,7 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when head report has an error' do before do - head_report.add_degradation(degradation_1) + head_report.add_degradation(major_degradation) end it 'includes the head report error in the count' do @@ -144,8 +146,8 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when base report has errors and head report has errors' do before do - base_report.add_degradation(degradation_1) - head_report.add_degradation(degradation_2) + base_report.add_degradation(major_degradation) + head_report.add_degradation(minor_degradation) end it 'includes errors in the count' do @@ -155,9 +157,9 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when base report has errors and head report has the same error' do before do - base_report.add_degradation(degradation_1) - head_report.add_degradation(degradation_1) - head_report.add_degradation(degradation_2) + base_report.add_degradation(major_degradation) + head_report.add_degradation(major_degradation) + head_report.add_degradation(minor_degradation) end it 'includes errors in the count' do @@ -179,20 +181,28 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when base report has errors and head has the same error' do before do - base_report.add_degradation(degradation_1) - head_report.add_degradation(degradation_1) - head_report.add_degradation(degradation_2) - end - - it 'includes the base report errors' do - expect(existing_errors).to contain_exactly(degradation_1) + base_report.add_degradation(major_degradation) + base_report.add_degradation(critical_degradation) + base_report.add_degradation(blocker_degradation) + head_report.add_degradation(critical_degradation) + head_report.add_degradation(blocker_degradation) + head_report.add_degradation(major_degradation) + head_report.add_degradation(minor_degradation) + end + + it 'includes the base report errors sorted by severity' do + expect(existing_errors).to eq([ + blocker_degradation, + critical_degradation, + major_degradation + ]) end end context 'when base report has errors and head has a different error' do before do - base_report.add_degradation(degradation_1) - head_report.add_degradation(degradation_2) + base_report.add_degradation(major_degradation) + head_report.add_degradation(minor_degradation) end it 'returns an empty array' do @@ -202,7 +212,7 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when base report does not have errors and head has errors' do before do - head_report.add_degradation(degradation_1) + head_report.add_degradation(major_degradation) end it 'returns an empty array' do @@ -224,19 +234,25 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when base report has errors and head has more errors' do before do - base_report.add_degradation(degradation_1) - head_report.add_degradation(degradation_1) - head_report.add_degradation(degradation_2) + base_report.add_degradation(major_degradation) + head_report.add_degradation(critical_degradation) + head_report.add_degradation(minor_degradation) + head_report.add_degradation(blocker_degradation) + head_report.add_degradation(major_degradation) end - it 'includes errors not found in the base report' do - expect(new_errors).to eq([degradation_2]) + it 'includes errors not found in the base report sorted by severity' do + expect(new_errors).to eq([ + blocker_degradation, + critical_degradation, + minor_degradation + ]) end end context 'when base report has an error and head has no errors' do before do - base_report.add_degradation(degradation_1) + base_report.add_degradation(major_degradation) end it 'returns an empty array' do @@ -246,11 +262,11 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when base report does not have errors and head has errors' do before do - head_report.add_degradation(degradation_1) + head_report.add_degradation(major_degradation) end it 'returns the head report error' do - expect(new_errors).to eq([degradation_1]) + expect(new_errors).to eq([major_degradation]) end end @@ -268,9 +284,9 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when base report errors are still found in the head report' do before do - base_report.add_degradation(degradation_1) - head_report.add_degradation(degradation_1) - head_report.add_degradation(degradation_2) + base_report.add_degradation(major_degradation) + head_report.add_degradation(major_degradation) + head_report.add_degradation(minor_degradation) end it 'returns an empty array' do @@ -280,18 +296,25 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do context 'when base report has errors and head has a different error' do before do - base_report.add_degradation(degradation_1) - head_report.add_degradation(degradation_2) + base_report.add_degradation(major_degradation) + base_report.add_degradation(minor_degradation) + base_report.add_degradation(critical_degradation) + base_report.add_degradation(blocker_degradation) + head_report.add_degradation(major_degradation) end - it 'returns the base report error' do - expect(resolved_errors).to eq([degradation_1]) + it 'returns the base report errors not found in the head report, sorted by severity' do + expect(resolved_errors).to eq([ + blocker_degradation, + critical_degradation, + minor_degradation + ]) end end context 'when base report does not have errors and head has errors' do before do - head_report.add_degradation(degradation_1) + head_report.add_degradation(major_degradation) end it 'returns an empty array' do diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb index ae9b2f2c62b..3b0eaffc54e 100644 --- a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb @@ -34,8 +34,6 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do it 'sets location as an error' do codequality_report.add_degradation(invalid_degradation) - - expect(codequality_report.error_message).to eq("Invalid degradation format: The property '#/' did not contain a required property of 'location'") end end end @@ -79,4 +77,36 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do end end end + + describe '#sort_degradations!' do + let(:major) { build(:codequality_degradation, :major) } + let(:minor) { build(:codequality_degradation, :minor) } + let(:blocker) { build(:codequality_degradation, :blocker) } + let(:info) { build(:codequality_degradation, :info) } + let(:major_2) { build(:codequality_degradation, :major) } + let(:critical) { build(:codequality_degradation, :critical) } + let(:codequality_report) { described_class.new } + + before do + codequality_report.add_degradation(major) + codequality_report.add_degradation(minor) + codequality_report.add_degradation(blocker) + codequality_report.add_degradation(major_2) + codequality_report.add_degradation(info) + codequality_report.add_degradation(critical) + + codequality_report.sort_degradations! + end + + it 'sorts degradations based on severity' do + expect(codequality_report.degradations.values).to eq([ + blocker, + critical, + major, + major_2, + minor, + info + ]) + end + end end diff --git a/spec/lib/gitlab/ci/reports/test_failure_history_spec.rb b/spec/lib/gitlab/ci/reports/test_failure_history_spec.rb index 831bc5e9f37..9ee55177ca0 100644 --- a/spec/lib/gitlab/ci/reports/test_failure_history_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_failure_history_spec.rb @@ -13,9 +13,9 @@ RSpec.describe Gitlab::Ci::Reports::TestFailureHistory, :aggregate_failures do subject(:load_history) { described_class.new([failed_rspec, failed_java], project).load! } before do - allow(Ci::TestCaseFailure) + allow(Ci::UnitTestFailure) .to receive(:recent_failures_count) - .with(project: project, test_case_keys: [failed_rspec.key, failed_java.key]) + .with(project: project, unit_test_keys: [failed_rspec.key, failed_java.key]) .and_return( failed_rspec.key => 2, failed_java.key => 1 diff --git a/spec/lib/gitlab/ci/runner_instructions_spec.rb b/spec/lib/gitlab/ci/runner_instructions_spec.rb index d1020026fe6..f872c631a50 100644 --- a/spec/lib/gitlab/ci/runner_instructions_spec.rb +++ b/spec/lib/gitlab/ci/runner_instructions_spec.rb @@ -6,7 +6,6 @@ RSpec.describe Gitlab::Ci::RunnerInstructions do using RSpec::Parameterized::TableSyntax let(:params) { {} } - let(:user) { create(:user) } describe 'OS' do Gitlab::Ci::RunnerInstructions::OS.each do |name, subject| @@ -37,7 +36,7 @@ RSpec.describe Gitlab::Ci::RunnerInstructions do end describe '#install_script' do - subject { described_class.new(current_user: user, **params) } + subject { described_class.new(**params) } context 'invalid params' do where(:current_params, :expected_error_message) do @@ -106,117 +105,18 @@ RSpec.describe Gitlab::Ci::RunnerInstructions do end end - context 'group' do - let(:group) { create(:group) } - - subject { described_class.new(current_user: user, group: group, **params) } - - context 'user is owner' do - before do - group.add_owner(user) - end - - with_them do - let(:params) { { os: commands.each_key.first, arch: 'foo' } } - - it 'have correct configurations' do - result = subject.register_command - - expect(result).to include("#{commands[commands.each_key.first]} register") - expect(result).to include("--registration-token #{group.runners_token}") - expect(result).to include("--url #{Gitlab::Routing.url_helpers.root_url(only_path: false)}") - end - end - end - - context 'user is not owner' do - where(:user_permission) do - [:maintainer, :developer, :reporter, :guest] - end - - with_them do - before do - create(:group_member, user_permission, group: group, user: user) - end - - it 'raises error' do - result = subject.register_command - - expect(result).to be_nil - expect(subject.errors).to include("Gitlab::Access::AccessDeniedError") - end - end - end - end - - context 'project' do - let(:project) { create(:project) } - - subject { described_class.new(current_user: user, project: project, **params) } - - context 'user is maintainer' do - before do - project.add_maintainer(user) - end - - with_them do - let(:params) { { os: commands.each_key.first, arch: 'foo' } } - - it 'have correct configurations' do - result = subject.register_command - - expect(result).to include("#{commands[commands.each_key.first]} register") - expect(result).to include("--registration-token #{project.runners_token}") - expect(result).to include("--url #{Gitlab::Routing.url_helpers.root_url(only_path: false)}") - end - end - end - - context 'user is not maintainer' do - where(:user_permission) do - [:developer, :reporter, :guest] - end - - with_them do - before do - create(:project_member, user_permission, project: project, user: user) - end - - it 'raises error' do - result = subject.register_command - - expect(result).to be_nil - expect(subject.errors).to include("Gitlab::Access::AccessDeniedError") - end - end - end - end - context 'instance' do - subject { described_class.new(current_user: user, **params) } - - context 'user is admin' do - let(:user) { create(:user, :admin) } - - with_them do - let(:params) { { os: commands.each_key.first, arch: 'foo' } } + subject { described_class.new(**params) } - it 'have correct configurations' do - result = subject.register_command - - expect(result).to include("#{commands[commands.each_key.first]} register") - expect(result).to include("--registration-token #{Gitlab::CurrentSettings.runners_registration_token}") - expect(result).to include("--url #{Gitlab::Routing.url_helpers.root_url(only_path: false)}") - end - end - end + with_them do + let(:params) { { os: commands.each_key.first, arch: 'foo' } } - context 'user is not admin' do - it 'raises error' do + it 'have correct configurations' do result = subject.register_command - expect(result).to be_nil - expect(subject.errors).to include("Gitlab::Access::AccessDeniedError") + expect(result).to include("#{commands[commands.each_key.first]} register") + expect(result).to include("--registration-token $REGISTRATION_TOKEN") + expect(result).to include("--url #{Gitlab::Routing.url_helpers.root_url(only_path: false)}") end end end diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb index 924ee5ee1a4..c4e83c1796d 100644 --- a/spec/lib/gitlab/ci/status/build/common_spec.rb +++ b/spec/lib/gitlab/ci/status/build/common_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Common do context 'when user does not have access to read build' do before do - project.update(public_builds: false) + project.update!(public_builds: false) end it { is_expected.not_to have_details } diff --git a/spec/lib/gitlab/ci/status/composite_spec.rb b/spec/lib/gitlab/ci/status/composite_spec.rb index 543cfe874ca..2b9523bd83d 100644 --- a/spec/lib/gitlab/ci/status/composite_spec.rb +++ b/spec/lib/gitlab/ci/status/composite_spec.rb @@ -6,13 +6,13 @@ RSpec.describe Gitlab::Ci::Status::Composite do let_it_be(:pipeline) { create(:ci_pipeline) } before_all do - @statuses = Ci::HasStatus::STATUSES_ENUM.map do |status, idx| + @statuses = Ci::HasStatus::STATUSES_ENUM.to_h do |status, idx| [status, create(:ci_build, pipeline: pipeline, status: status, importing: true)] - end.to_h + end - @statuses_with_allow_failure = Ci::HasStatus::STATUSES_ENUM.map do |status, idx| + @statuses_with_allow_failure = Ci::HasStatus::STATUSES_ENUM.to_h do |status, idx| [status, create(:ci_build, pipeline: pipeline, status: status, allow_failure: true, importing: true)] - end.to_h + end end describe '#status' do diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb index 597e4ca9b03..0fe7c731f27 100644 --- a/spec/lib/gitlab/ci/trace_spec.rb +++ b/spec/lib/gitlab/ci/trace_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state, factory_default: :keep do let_it_be(:project) { create_default(:project).freeze } - let_it_be_with_reload(:build) { create(:ci_build) } + let_it_be_with_reload(:build) { create(:ci_build, :success) } let(:trace) { described_class.new(build) } describe "associations" do @@ -63,9 +63,7 @@ RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state, factory_defa describe '#update_interval' do context 'it is not being watched' do - it 'returns 30 seconds' do - expect(trace.update_interval).to eq(30.seconds) - end + it { expect(trace.update_interval).to eq(60.seconds) } end context 'it is being watched' do diff --git a/spec/lib/gitlab/ci/variables/helpers_spec.rb b/spec/lib/gitlab/ci/variables/helpers_spec.rb index b45abf8c0e1..f13b334c10e 100644 --- a/spec/lib/gitlab/ci/variables/helpers_spec.rb +++ b/spec/lib/gitlab/ci/variables/helpers_spec.rb @@ -100,4 +100,50 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do it { is_expected.to eq(result) } end end + + describe '.inherit_yaml_variables' do + let(:from) do + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }] + end + + let(:to) do + [{ key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] + end + + let(:inheritance) { true } + + let(:result) do + [{ key: 'key1', value: 'value1', public: true }, + { key: 'key2', value: 'value22', public: true }, + { key: 'key3', value: 'value3', public: true }] + end + + subject { described_class.inherit_yaml_variables(from: from, to: to, inheritance: inheritance) } + + it { is_expected.to eq(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 }] + end + + it { is_expected.to eq(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 }] + end + + it { is_expected.to eq(result) } + end + end end diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb index 7e3cd7ec254..e345cd4de9b 100644 --- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb @@ -24,7 +24,7 @@ module Gitlab let(:included_yml) do YAML.dump( - another_test: { stage: 'test', script: 'echo 2' } + { another_test: { stage: 'test', script: 'echo 2' } }.deep_stringify_keys ) end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 5462a587d16..ad94dfc9160 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -43,6 +43,8 @@ module Gitlab allow_failure: false, when: "on_success", yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage }) end @@ -74,6 +76,8 @@ module Gitlab allow_failure: false, when: 'on_success', yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage }) end @@ -111,7 +115,9 @@ module Gitlab tag_list: %w[A B], allow_failure: false, when: "on_success", - yaml_variables: [] + yaml_variables: [], + job_variables: [], + root_variables_inheritance: true }) end end @@ -158,6 +164,8 @@ module Gitlab allow_failure: false, when: "on_success", yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage }) end @@ -347,6 +355,8 @@ module Gitlab allow_failure: false, when: "on_success", yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage, options: { script: ["rspec"] }, only: { refs: ["branches"] } }] }, @@ -359,6 +369,8 @@ module Gitlab allow_failure: false, when: "on_success", yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage, options: { script: ["cap prod"] }, only: { refs: ["tags"] } }] }, @@ -372,7 +384,7 @@ module Gitlab end end - describe '#workflow_attributes' do + describe 'workflow attributes' do context 'with disallowed workflow:variables' do let(:config) do <<-EOYML @@ -403,11 +415,11 @@ module Gitlab end it 'parses the workflow:rules configuration' do - expect(subject.workflow_attributes[:rules]).to contain_exactly({ if: '$VAR == "value"' }) + expect(subject.workflow_rules).to contain_exactly({ if: '$VAR == "value"' }) end - it 'parses the root:variables as yaml_variables:' do - expect(subject.workflow_attributes[:yaml_variables]) + it 'parses the root:variables as #root_variables' do + expect(subject.root_variables) .to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true }) end end @@ -425,11 +437,11 @@ module Gitlab end it 'parses the workflow:rules configuration' do - expect(subject.workflow_attributes[:rules]).to contain_exactly({ if: '$VAR == "value"' }) + expect(subject.workflow_rules).to contain_exactly({ if: '$VAR == "value"' }) end - it 'parses the root:variables as yaml_variables:' do - expect(subject.workflow_attributes[:yaml_variables]).to eq([]) + it 'parses the root:variables as #root_variables' do + expect(subject.root_variables).to eq([]) end end @@ -445,11 +457,11 @@ module Gitlab end it 'parses the workflow:rules configuration' do - expect(subject.workflow_attributes[:rules]).to be_nil + expect(subject.workflow_rules).to be_nil end - it 'parses the root:variables as yaml_variables:' do - expect(subject.workflow_attributes[:yaml_variables]) + it 'parses the root:variables as #root_variables' do + expect(subject.root_variables) .to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true }) end end @@ -463,11 +475,11 @@ module Gitlab end it 'parses the workflow:rules configuration' do - expect(subject.workflow_attributes[:rules]).to be_nil + expect(subject.workflow_rules).to be_nil end - it 'parses the root:variables as yaml_variables:' do - expect(subject.workflow_attributes[:yaml_variables]).to eq([]) + it 'parses the root:variables as #root_variables' do + expect(subject.root_variables).to eq([]) end end end @@ -853,6 +865,8 @@ module Gitlab allow_failure: false, when: "on_success", yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage }) end @@ -861,7 +875,7 @@ module Gitlab config = YAML.dump({ image: "ruby:2.7", services: ["mysql"], before_script: ["pwd"], - rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + rspec: { image: { name: "ruby:3.0", entrypoint: ["/usr/local/bin/init", "run"] }, services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"], command: ["/usr/local/bin/init", "run"] }, "docker:dind"], @@ -878,7 +892,7 @@ module Gitlab options: { before_script: ["pwd"], script: ["rspec"], - image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + image: { name: "ruby:3.0", entrypoint: ["/usr/local/bin/init", "run"] }, services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"], command: ["/usr/local/bin/init", "run"] }, { name: "docker:dind" }] @@ -886,6 +900,8 @@ module Gitlab allow_failure: false, when: "on_success", yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage }) end @@ -915,6 +931,8 @@ module Gitlab allow_failure: false, when: "on_success", yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage }) end @@ -923,7 +941,7 @@ module Gitlab config = YAML.dump({ image: "ruby:2.7", services: ["mysql"], before_script: ["pwd"], - rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } }) + rspec: { image: "ruby:3.0", services: ["postgresql", "docker:dind"], script: "rspec" } }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute @@ -936,12 +954,14 @@ module Gitlab options: { before_script: ["pwd"], script: ["rspec"], - image: { name: "ruby:2.5" }, + image: { name: "ruby:3.0" }, services: [{ name: "postgresql" }, { name: "docker:dind" }] }, allow_failure: false, when: "on_success", yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage }) end @@ -951,7 +971,10 @@ module Gitlab describe 'Variables' do subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } - let(:build_variables) { subject.builds.first[:yaml_variables] } + let(:build) { subject.builds.first } + let(:yaml_variables) { build[:yaml_variables] } + let(:job_variables) { build[:job_variables] } + let(:root_variables_inheritance) { build[:root_variables_inheritance] } context 'when global variables are defined' do let(:variables) do @@ -967,10 +990,12 @@ module Gitlab end it 'returns global variables' do - expect(build_variables).to contain_exactly( + expect(yaml_variables).to contain_exactly( { key: 'VAR1', value: 'value1', public: true }, { key: 'VAR2', value: 'value2', public: true } ) + expect(job_variables).to eq([]) + expect(root_variables_inheritance).to eq(true) end end @@ -979,7 +1004,7 @@ module Gitlab { 'VAR1' => 'global1', 'VAR3' => 'global3', 'VAR4' => 'global4' } end - let(:job_variables) do + let(:build_variables) do { 'VAR1' => 'value1', 'VAR2' => 'value2' } end @@ -987,20 +1012,25 @@ module Gitlab { before_script: ['pwd'], variables: global_variables, - rspec: { script: 'rspec', variables: job_variables, inherit: inherit } + rspec: { script: 'rspec', variables: build_variables, inherit: inherit } } end context 'when no inheritance is specified' do let(:inherit) { } - it 'returns all unique variables' do - expect(build_variables).to contain_exactly( - { key: 'VAR4', value: 'global4', public: true }, + it 'returns all variables' do + expect(yaml_variables).to contain_exactly( + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true }, { key: 'VAR3', value: 'global3', public: true }, + { key: 'VAR4', value: 'global4', public: true } + ) + expect(job_variables).to contain_exactly( { key: 'VAR1', value: 'value1', public: true }, { key: 'VAR2', value: 'value2', public: true } ) + expect(root_variables_inheritance).to eq(true) end end @@ -1008,22 +1038,32 @@ module Gitlab let(:inherit) { { variables: false } } it 'does not inherit variables' do - expect(build_variables).to contain_exactly( + expect(yaml_variables).to contain_exactly( { key: 'VAR1', value: 'value1', public: true }, { key: 'VAR2', value: 'value2', public: true } ) + expect(job_variables).to contain_exactly( + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } + ) + expect(root_variables_inheritance).to eq(false) end end context 'when specific variables are to inherited' do let(:inherit) { { variables: %w[VAR1 VAR4] } } - it 'returns all unique variables and inherits only specified variables' do - expect(build_variables).to contain_exactly( - { key: 'VAR4', value: 'global4', public: true }, + it 'returns all variables and inherits only specified variables' do + expect(yaml_variables).to contain_exactly( + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true }, + { key: 'VAR4', value: 'global4', public: true } + ) + expect(job_variables).to contain_exactly( { key: 'VAR1', value: 'value1', public: true }, { key: 'VAR2', value: 'value2', public: true } ) + expect(root_variables_inheritance).to eq(%w[VAR1 VAR4]) end end end @@ -1042,10 +1082,15 @@ module Gitlab end it 'returns job variables' do - expect(build_variables).to contain_exactly( + expect(yaml_variables).to contain_exactly( + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } + ) + expect(job_variables).to contain_exactly( { key: 'VAR1', value: 'value1', public: true }, { key: 'VAR2', value: 'value2', public: true } ) + expect(root_variables_inheritance).to eq(true) end end @@ -1068,8 +1113,11 @@ module Gitlab # When variables config is empty, we assume this is a valid # configuration, see issue #18775 # - expect(build_variables).to be_an_instance_of(Array) - expect(build_variables).to be_empty + expect(yaml_variables).to be_an_instance_of(Array) + expect(yaml_variables).to be_empty + + expect(job_variables).to eq([]) + expect(root_variables_inheritance).to eq(true) end end end @@ -1084,8 +1132,11 @@ module Gitlab end it 'returns empty array' do - expect(build_variables).to be_an_instance_of(Array) - expect(build_variables).to be_empty + expect(yaml_variables).to be_an_instance_of(Array) + expect(yaml_variables).to be_empty + + expect(job_variables).to eq([]) + expect(root_variables_inheritance).to eq(true) end end end @@ -1717,6 +1768,8 @@ module Gitlab when: "on_success", allow_failure: false, yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage }) end @@ -2080,6 +2133,8 @@ module Gitlab when: "on_success", allow_failure: false, yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage ) expect(subject.builds[4]).to eq( @@ -2095,6 +2150,8 @@ module Gitlab when: "on_success", allow_failure: false, yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :dag ) end @@ -2122,6 +2179,8 @@ module Gitlab when: "on_success", allow_failure: false, yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage ) expect(subject.builds[4]).to eq( @@ -2139,6 +2198,8 @@ module Gitlab when: "on_success", allow_failure: false, yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :dag ) end @@ -2162,6 +2223,8 @@ module Gitlab when: "on_success", allow_failure: false, yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :dag ) end @@ -2193,6 +2256,8 @@ module Gitlab when: "on_success", allow_failure: false, yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :dag ) end @@ -2391,6 +2456,8 @@ module Gitlab when: "on_success", allow_failure: false, yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage }) end @@ -2438,6 +2505,8 @@ module Gitlab when: "on_success", allow_failure: false, yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage }) expect(subject.second).to eq({ @@ -2451,6 +2520,8 @@ module Gitlab when: "on_success", allow_failure: false, yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, scheduling_type: :stage }) end diff --git a/spec/lib/gitlab/composer/version_index_spec.rb b/spec/lib/gitlab/composer/version_index_spec.rb index 7b0ed703f42..a4d016636aa 100644 --- a/spec/lib/gitlab/composer/version_index_spec.rb +++ b/spec/lib/gitlab/composer/version_index_spec.rb @@ -27,6 +27,11 @@ RSpec.describe Gitlab::Composer::VersionIndex do 'type' => 'zip', 'url' => "http://localhost/api/v4/projects/#{project.id}/packages/composer/archives/#{package.name}.zip?sha=#{branch.target}" }, + 'source' => { + 'reference' => branch.target, + 'type' => 'git', + 'url' => project.http_url_to_repo + }, 'name' => package.name, 'uid' => package.id, 'version' => package.version diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index bb9bee763d8..46e5334cd81 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Gitlab::Conflict::File do let(:section_keys) { conflict_file.sections.map { |section| section[:id] }.compact } context 'when resolving everything to the same side' do - let(:resolution_hash) { section_keys.map { |key| [key, 'head'] }.to_h } + let(:resolution_hash) { section_keys.to_h { |key| [key, 'head'] } } let(:resolved_lines) { conflict_file.resolve_lines(resolution_hash) } let(:expected_lines) { conflict_file.lines.reject { |line| line.type == 'old' } } @@ -54,8 +54,8 @@ RSpec.describe Gitlab::Conflict::File do end it 'raises ResolutionError when passed a hash without resolutions for all sections' do - empty_hash = section_keys.map { |key| [key, nil] }.to_h - invalid_hash = section_keys.map { |key| [key, 'invalid'] }.to_h + empty_hash = section_keys.to_h { |key| [key, nil] } + invalid_hash = section_keys.to_h { |key| [key, 'invalid'] } expect { conflict_file.resolve_lines({}) } .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError) diff --git a/spec/lib/gitlab/crypto_helper_spec.rb b/spec/lib/gitlab/crypto_helper_spec.rb index 024564ea213..616a37a4cb9 100644 --- a/spec/lib/gitlab/crypto_helper_spec.rb +++ b/spec/lib/gitlab/crypto_helper_spec.rb @@ -20,22 +20,24 @@ RSpec.describe Gitlab::CryptoHelper do expect(encrypted).not_to include "\n" end - it 'does not save hashed token with iv value in database' do - expect { described_class.aes256_gcm_encrypt('some-value') }.not_to change { TokenWithIv.count } - end - it 'encrypts using static iv' do expect(Encryptor).to receive(:encrypt).with(described_class::AES256_GCM_OPTIONS.merge(value: 'some-value', iv: described_class::AES256_GCM_IV_STATIC)).and_return('hashed_value') described_class.aes256_gcm_encrypt('some-value') end - end - describe '.aes256_gcm_decrypt' do - before do - stub_feature_flags(dynamic_nonce_creation: false) + context 'with provided iv' do + let(:iv) { create_nonce } + + it 'encrypts using provided iv' do + expect(Encryptor).to receive(:encrypt).with(described_class::AES256_GCM_OPTIONS.merge(value: 'some-value', iv: iv)).and_return('hashed_value') + + described_class.aes256_gcm_encrypt('some-value', nonce: iv) + end end + end + describe '.aes256_gcm_decrypt' do context 'when token was encrypted using static nonce' do let(:encrypted) { described_class.aes256_gcm_encrypt('some-value', nonce: described_class::AES256_GCM_IV_STATIC) } @@ -50,54 +52,22 @@ RSpec.describe Gitlab::CryptoHelper do expect(decrypted).to eq 'some-value' end - - it 'does not save hashed token with iv value in database' do - expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count } - end - - context 'with feature flag switched on' do - before do - stub_feature_flags(dynamic_nonce_creation: true) - end - - it 'correctly decrypts encrypted string' do - decrypted = described_class.aes256_gcm_decrypt(encrypted) - - expect(decrypted).to eq 'some-value' - end - end end context 'when token was encrypted using random nonce' do let(:value) { 'random-value' } - - # for compatibility with tokens encrypted using dynamic nonce - let!(:encrypted) do - iv = create_nonce - encrypted_token = described_class.create_encrypted_token(value, iv) - TokenWithIv.create!(hashed_token: Digest::SHA256.digest(encrypted_token), hashed_plaintext_token: Digest::SHA256.digest(encrypted_token), iv: iv) - encrypted_token - end - - before do - stub_feature_flags(dynamic_nonce_creation: true) - end + let(:iv) { create_nonce } + let(:encrypted) { described_class.aes256_gcm_encrypt(value, nonce: iv) } it 'correctly decrypts encrypted string' do - decrypted = described_class.aes256_gcm_decrypt(encrypted) + decrypted = described_class.aes256_gcm_decrypt(encrypted, nonce: iv) expect(decrypted).to eq value end - - it 'does not save hashed token with iv value in database' do - expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count } - end end end def create_nonce - cipher = OpenSSL::Cipher.new('aes-256-gcm') - cipher.encrypt # Required before '#random_iv' can be called - cipher.random_iv # Ensures that the IV is the correct length respective to the algorithm used. + ::Digest::SHA256.hexdigest('my-value').bytes.take(TokenAuthenticatableStrategies::EncryptionHelper::NONCE_SIZE).pack('c*') end end diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index ab1728414bb..932238f281e 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -19,6 +19,9 @@ RSpec.describe Gitlab::DataBuilder::Build do it { expect(data[:tag]).to eq(build.tag) } it { expect(data[:build_id]).to eq(build.id) } it { expect(data[:build_status]).to eq(build.status) } + it { expect(data[:build_created_at]).to eq(build.created_at) } + it { expect(data[:build_started_at]).to eq(build.started_at) } + it { expect(data[:build_finished_at]).to eq(build.finished_at) } it { expect(data[:build_allow_failure]).to eq(false) } it { expect(data[:build_failure_reason]).to eq(build.failure_reason) } it { expect(data[:project_id]).to eq(build.project.id) } diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb index cf04f560ceb..bec1e612c02 100644 --- a/spec/lib/gitlab/data_builder/pipeline_spec.rb +++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb @@ -59,7 +59,6 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do expect(runner_data[:id]).to eq(ci_runner.id) expect(runner_data[:description]).to eq(ci_runner.description) expect(runner_data[:active]).to eq(ci_runner.active) - expect(runner_data[:is_shared]).to eq(ci_runner.instance_type?) expect(runner_data[:tags]).to match_array(tag_names) end end diff --git a/spec/lib/gitlab/database/background_migration/batch_metrics_spec.rb b/spec/lib/gitlab/database/background_migration/batch_metrics_spec.rb new file mode 100644 index 00000000000..e96862fbc2d --- /dev/null +++ b/spec/lib/gitlab/database/background_migration/batch_metrics_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Database::BackgroundMigration::BatchMetrics do + let(:batch_metrics) { described_class.new } + + describe '#time_operation' do + it 'tracks the duration of the operation using monotonic time' do + expect(batch_metrics.timings).to be_empty + + expect(Gitlab::Metrics::System).to receive(:monotonic_time) + .exactly(6).times + .and_return(0.0, 111.0, 200.0, 290.0, 300.0, 410.0) + + batch_metrics.time_operation(:my_label) do + # some operation + end + + batch_metrics.time_operation(:my_other_label) do + # some operation + end + + batch_metrics.time_operation(:my_label) do + # some operation + end + + expect(batch_metrics.timings).to eq(my_label: [111.0, 110.0], my_other_label: [90.0]) + end + end +end diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb new file mode 100644 index 00000000000..7d0e10b62c6 --- /dev/null +++ b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do + let(:migration_wrapper) { double('test wrapper') } + let(:runner) { described_class.new(migration_wrapper) } + + describe '#run_migration_job' do + shared_examples_for 'it has completed the migration' do + it 'does not create and run a migration job' do + expect(migration_wrapper).not_to receive(:perform) + + expect do + runner.run_migration_job(migration) + end.not_to change { Gitlab::Database::BackgroundMigration::BatchedJob.count } + end + + it 'marks the migration as finished' do + relation = Gitlab::Database::BackgroundMigration::BatchedMigration.finished.where(id: migration.id) + + expect { runner.run_migration_job(migration) }.to change { relation.count }.by(1) + end + end + + context 'when the migration has no previous jobs' do + let(:migration) { create(:batched_background_migration, :active, batch_size: 2) } + + let(:job_relation) do + Gitlab::Database::BackgroundMigration::BatchedJob.where(batched_background_migration_id: migration.id) + end + + context 'when the migration has batches to process' do + let!(:event1) { create(:event) } + let!(:event2) { create(:event) } + let!(:event3) { create(:event) } + + it 'runs the job for the first batch' do + migration.update!(min_value: event1.id, max_value: event2.id) + + expect(migration_wrapper).to receive(:perform) do |job_record| + expect(job_record).to eq(job_relation.first) + end + + expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(1) + + expect(job_relation.first).to have_attributes( + min_value: event1.id, + max_value: event2.id, + batch_size: migration.batch_size, + sub_batch_size: migration.sub_batch_size) + end + end + + context 'when the batch maximum exceeds the migration maximum' do + let!(:events) { create_list(:event, 3) } + let(:event1) { events[0] } + let(:event2) { events[1] } + + it 'clamps the batch maximum to the migration maximum' do + migration.update!(min_value: event1.id, max_value: event2.id, batch_size: 5) + + expect(migration_wrapper).to receive(:perform) + + expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(1) + + expect(job_relation.first).to have_attributes( + min_value: event1.id, + max_value: event2.id, + batch_size: migration.batch_size, + sub_batch_size: migration.sub_batch_size) + end + end + + context 'when the migration has no batches to process' do + it_behaves_like 'it has completed the migration' + end + end + + context 'when the migration has previous jobs' do + let!(:event1) { create(:event) } + let!(:event2) { create(:event) } + let!(:event3) { create(:event) } + + let!(:migration) do + create(:batched_background_migration, :active, batch_size: 2, min_value: event1.id, max_value: event3.id) + end + + let!(:previous_job) do + create(:batched_background_migration_job, + batched_migration: migration, + min_value: event1.id, + max_value: event2.id, + batch_size: 2, + sub_batch_size: 1) + end + + let(:job_relation) do + Gitlab::Database::BackgroundMigration::BatchedJob.where(batched_background_migration_id: migration.id) + end + + context 'when the migration has batches to process' do + it 'runs the migration job for the next batch' do + expect(migration_wrapper).to receive(:perform) do |job_record| + expect(job_record).to eq(job_relation.last) + end + + expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(1) + + expect(job_relation.last).to have_attributes( + min_value: event3.id, + max_value: event3.id, + batch_size: migration.batch_size, + sub_batch_size: migration.sub_batch_size) + end + + context 'when the batch minimum exceeds the migration maximum' do + before do + migration.update!(batch_size: 5, max_value: event2.id) + end + + it_behaves_like 'it has completed the migration' + end + end + + context 'when the migration has no batches remaining' do + before do + create(:batched_background_migration_job, + batched_migration: migration, + min_value: event3.id, + max_value: event3.id, + batch_size: 2, + sub_batch_size: 1) + end + + it_behaves_like 'it has completed the migration' + end + end + end + + describe '#run_entire_migration' do + context 'when not in a development or test environment' do + it 'raises an error' do + environment = double('environment', development?: false, test?: false) + migration = build(:batched_background_migration, :finished) + + allow(Rails).to receive(:env).and_return(environment) + + expect do + runner.run_entire_migration(migration) + end.to raise_error('this method is not intended for use in real environments') + end + end + + context 'when the given migration is not active' do + it 'does not create and run migration jobs' do + migration = build(:batched_background_migration, :finished) + + expect(migration_wrapper).not_to receive(:perform) + + expect do + runner.run_entire_migration(migration) + end.not_to change { Gitlab::Database::BackgroundMigration::BatchedJob.count } + end + end + + context 'when the given migration is active' do + let!(:event1) { create(:event) } + let!(:event2) { create(:event) } + let!(:event3) { create(:event) } + + let!(:migration) do + create(:batched_background_migration, :active, batch_size: 2, min_value: event1.id, max_value: event3.id) + end + + let(:job_relation) do + Gitlab::Database::BackgroundMigration::BatchedJob.where(batched_background_migration_id: migration.id) + end + + it 'runs all jobs inline until finishing the migration' do + expect(migration_wrapper).to receive(:perform) do |job_record| + expect(job_record).to eq(job_relation.first) + end + + expect(migration_wrapper).to receive(:perform) do |job_record| + expect(job_record).to eq(job_relation.last) + end + + expect { runner.run_entire_migration(migration) }.to change { job_relation.count }.by(2) + + expect(job_relation.first).to have_attributes(min_value: event1.id, max_value: event2.id) + expect(job_relation.last).to have_attributes(min_value: event3.id, max_value: event3.id) + + expect(migration.reload).to be_finished + end + end + end +end 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 f4a939e7c1f..261e23d0745 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb @@ -29,6 +29,16 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m end end + describe '.active_migration' do + let!(:migration1) { create(:batched_background_migration, :finished) } + let!(:migration2) { create(:batched_background_migration, :active) } + let!(:migration3) { create(:batched_background_migration, :active) } + + it 'returns the first active migration according to queue order' do + expect(described_class.active_migration).to eq(migration2) + end + end + describe '#interval_elapsed?' do context 'when the migration has no last_job' do let(:batched_migration) { build(:batched_background_migration) } @@ -77,6 +87,34 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m end end end + + context 'when an interval variance is given' do + let(:variance) { 2.seconds } + + context 'when the last job is less than an interval with variance old' do + it 'returns false' do + freeze_time do + create(:batched_background_migration_job, + batched_migration: batched_migration, + created_at: Time.current - 1.minute - 57.seconds) + + expect(batched_migration.interval_elapsed?(variance: variance)).to eq(false) + end + end + end + + context 'when the last job is more than an interval with variance old' do + it 'returns true' do + freeze_time do + create(:batched_background_migration_job, + batched_migration: batched_migration, + created_at: Time.current - 1.minute - 58.seconds) + + expect(batched_migration.interval_elapsed?(variance: variance)).to eq(true) + end + end + end + end end end @@ -157,4 +195,17 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m describe '#batch_class_name=' do it_behaves_like 'an attr_writer that demodulizes assigned class names', :batch_class_name end + + describe '#prometheus_labels' do + let(:batched_migration) { create(:batched_background_migration, job_class_name: 'TestMigration', table_name: 'foo', column_name: 'bar') } + + it 'returns a hash with labels for the migration' do + labels = { + migration_id: batched_migration.id, + migration_identifier: 'TestMigration/foo.bar' + } + + expect(batched_migration.prometheus_labels).to eq(labels) + end + end end diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb index 17cceb35ff7..00d13f23d36 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb @@ -3,43 +3,105 @@ require 'spec_helper' RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '#perform' do - let(:migration_wrapper) { described_class.new } + subject { described_class.new.perform(job_record) } + let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob } let_it_be(:active_migration) { create(:batched_background_migration, :active, job_arguments: [:id, :other_id]) } let!(:job_record) { create(:batched_background_migration_job, batched_migration: active_migration) } + let(:job_instance) { double('job instance', batch_metrics: {}) } + + before do + allow(job_class).to receive(:new).and_return(job_instance) + end it 'runs the migration job' do - expect_next_instance_of(job_class) do |job_instance| - expect(job_instance).to receive(:perform).with(1, 10, 'events', 'id', 1, 'id', 'other_id') - end + expect(job_instance).to receive(:perform).with(1, 10, 'events', 'id', 1, 'id', 'other_id') - migration_wrapper.perform(job_record) + subject end - it 'updates the the tracking record in the database' do + it 'updates the tracking record in the database' do + test_metrics = { 'my_metris' => 'some value' } + + expect(job_instance).to receive(:perform) + expect(job_instance).to receive(:batch_metrics).and_return(test_metrics) + expect(job_record).to receive(:update!).with(hash_including(attempts: 1, status: :running)).and_call_original freeze_time do - migration_wrapper.perform(job_record) + subject reloaded_job_record = job_record.reload expect(reloaded_job_record).not_to be_pending expect(reloaded_job_record.attempts).to eq(1) expect(reloaded_job_record.started_at).to eq(Time.current) + expect(reloaded_job_record.metrics).to eq(test_metrics) + end + end + + context 'reporting prometheus metrics' do + let(:labels) { job_record.batched_migration.prometheus_labels } + + before do + allow(job_instance).to receive(:perform) + end + + it 'reports batch_size' do + expect(described_class.metrics[:gauge_batch_size]).to receive(:set).with(labels, job_record.batch_size) + + subject + end + + it 'reports sub_batch_size' do + expect(described_class.metrics[:gauge_sub_batch_size]).to receive(:set).with(labels, job_record.sub_batch_size) + + subject + end + + it 'reports updated tuples (currently based on batch_size)' do + expect(described_class.metrics[:counter_updated_tuples]).to receive(:increment).with(labels, job_record.batch_size) + + subject + end + + it 'reports summary of query timings' do + metrics = { 'timings' => { 'update_all' => [1, 2, 3, 4, 5] } } + + expect(job_instance).to receive(:batch_metrics).and_return(metrics) + + metrics['timings'].each do |key, timings| + summary_labels = labels.merge(operation: key) + timings.each do |timing| + expect(described_class.metrics[:histogram_timings]).to receive(:observe).with(summary_labels, timing) + end + end + + subject + end + + it 'reports time efficiency' do + freeze_time do + expect(Time).to receive(:current).and_return(Time.zone.now - 5.seconds).ordered + expect(Time).to receive(:current).and_return(Time.zone.now).ordered + + ratio = 5 / job_record.batched_migration.interval.to_f + + expect(described_class.metrics[:histogram_time_efficiency]).to receive(:observe).with(labels, ratio) + + subject + end end end context 'when the migration job does not raise an error' do it 'marks the tracking record as succeeded' do - expect_next_instance_of(job_class) do |job_instance| - expect(job_instance).to receive(:perform).with(1, 10, 'events', 'id', 1, 'id', 'other_id') - end + expect(job_instance).to receive(:perform).with(1, 10, 'events', 'id', 1, 'id', 'other_id') freeze_time do - migration_wrapper.perform(job_record) + subject reloaded_job_record = job_record.reload @@ -51,14 +113,12 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' context 'when the migration job raises an error' do it 'marks the tracking record as failed before raising the error' do - expect_next_instance_of(job_class) do |job_instance| - expect(job_instance).to receive(:perform) - .with(1, 10, 'events', 'id', 1, 'id', 'other_id') - .and_raise(RuntimeError, 'Something broke!') - end + expect(job_instance).to receive(:perform) + .with(1, 10, 'events', 'id', 1, 'id', 'other_id') + .and_raise(RuntimeError, 'Something broke!') freeze_time do - expect { migration_wrapper.perform(job_record) }.to raise_error(RuntimeError, 'Something broke!') + expect { subject }.to raise_error(RuntimeError, 'Something broke!') reloaded_job_record = job_record.reload diff --git a/spec/lib/gitlab/database/background_migration/scheduler_spec.rb b/spec/lib/gitlab/database/background_migration/scheduler_spec.rb deleted file mode 100644 index ba745acdf8a..00000000000 --- a/spec/lib/gitlab/database/background_migration/scheduler_spec.rb +++ /dev/null @@ -1,182 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Database::BackgroundMigration::Scheduler, '#perform' do - let(:scheduler) { described_class.new } - - shared_examples_for 'it has no jobs to run' do - it 'does not create and run a migration job' do - test_wrapper = double('test wrapper') - - expect(test_wrapper).not_to receive(:perform) - - expect do - scheduler.perform(migration_wrapper: test_wrapper) - end.not_to change { Gitlab::Database::BackgroundMigration::BatchedJob.count } - end - end - - context 'when there are no active migrations' do - let!(:migration) { create(:batched_background_migration, :finished) } - - it_behaves_like 'it has no jobs to run' - end - - shared_examples_for 'it has completed the migration' do - it 'marks the migration as finished' do - relation = Gitlab::Database::BackgroundMigration::BatchedMigration.finished.where(id: first_migration.id) - - expect { scheduler.perform }.to change { relation.count }.by(1) - end - end - - context 'when there are active migrations' do - let!(:first_migration) { create(:batched_background_migration, :active, batch_size: 2) } - let!(:last_migration) { create(:batched_background_migration, :active) } - - let(:job_relation) do - Gitlab::Database::BackgroundMigration::BatchedJob.where(batched_background_migration_id: first_migration.id) - end - - context 'when the migration interval has not elapsed' do - before do - expect_next_found_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigration) do |migration| - expect(migration).to receive(:interval_elapsed?).and_return(false) - end - end - - it_behaves_like 'it has no jobs to run' - end - - context 'when the interval has elapsed' do - before do - expect_next_found_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigration) do |migration| - expect(migration).to receive(:interval_elapsed?).and_return(true) - end - end - - context 'when the first migration has no previous jobs' do - context 'when the migration has batches to process' do - let!(:event1) { create(:event) } - let!(:event2) { create(:event) } - let!(:event3) { create(:event) } - - it 'runs the job for the first batch' do - first_migration.update!(min_value: event1.id, max_value: event3.id) - - expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper) do |wrapper| - expect(wrapper).to receive(:perform).and_wrap_original do |_, job_record| - expect(job_record).to eq(job_relation.first) - end - end - - expect { scheduler.perform }.to change { job_relation.count }.by(1) - - expect(job_relation.first).to have_attributes( - min_value: event1.id, - max_value: event2.id, - batch_size: first_migration.batch_size, - sub_batch_size: first_migration.sub_batch_size) - end - end - - context 'when the migration has no batches to process' do - it_behaves_like 'it has no jobs to run' - it_behaves_like 'it has completed the migration' - end - end - - context 'when the first migration has previous jobs' do - let!(:event1) { create(:event) } - let!(:event2) { create(:event) } - let!(:event3) { create(:event) } - - let!(:previous_job) do - create(:batched_background_migration_job, - batched_migration: first_migration, - min_value: event1.id, - max_value: event2.id, - batch_size: 2, - sub_batch_size: 1) - end - - context 'when the migration is ready to process another job' do - it 'runs the migration job for the next batch' do - first_migration.update!(min_value: event1.id, max_value: event3.id) - - expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper) do |wrapper| - expect(wrapper).to receive(:perform).and_wrap_original do |_, job_record| - expect(job_record).to eq(job_relation.last) - end - end - - expect { scheduler.perform }.to change { job_relation.count }.by(1) - - expect(job_relation.last).to have_attributes( - min_value: event3.id, - max_value: event3.id, - batch_size: first_migration.batch_size, - sub_batch_size: first_migration.sub_batch_size) - end - end - - context 'when the migration has no batches remaining' do - let!(:final_job) do - create(:batched_background_migration_job, - batched_migration: first_migration, - min_value: event3.id, - max_value: event3.id, - batch_size: 2, - sub_batch_size: 1) - end - - it_behaves_like 'it has no jobs to run' - it_behaves_like 'it has completed the migration' - end - end - - context 'when the bounds of the next batch exceed the migration maximum value' do - let!(:events) { create_list(:event, 3) } - let(:event1) { events[0] } - let(:event2) { events[1] } - - context 'when the batch maximum exceeds the migration maximum' do - it 'clamps the batch maximum to the migration maximum' do - first_migration.update!(batch_size: 5, min_value: event1.id, max_value: event2.id) - - expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper) do |wrapper| - expect(wrapper).to receive(:perform) - end - - expect { scheduler.perform }.to change { job_relation.count }.by(1) - - expect(job_relation.first).to have_attributes( - min_value: event1.id, - max_value: event2.id, - batch_size: first_migration.batch_size, - sub_batch_size: first_migration.sub_batch_size) - end - end - - context 'when the batch minimum exceeds the migration maximum' do - let!(:previous_job) do - create(:batched_background_migration_job, - batched_migration: first_migration, - min_value: event1.id, - max_value: event2.id, - batch_size: 5, - sub_batch_size: 1) - end - - before do - first_migration.update!(batch_size: 5, min_value: 1, max_value: event2.id) - end - - it_behaves_like 'it has no jobs to run' - it_behaves_like 'it has completed the migration' - 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 29688b18e94..da13bc425d1 100644 --- a/spec/lib/gitlab/database/batch_count_spec.rb +++ b/spec/lib/gitlab/database/batch_count_spec.rb @@ -270,6 +270,8 @@ RSpec.describe Gitlab::Database::BatchCount do end it "defaults the batch size to #{Gitlab::Database::BatchCounter::DEFAULT_DISTINCT_BATCH_SIZE}" do + stub_feature_flags(loose_index_scan_for_distinct_values: false) + min_id = model.minimum(:id) relation = instance_double(ActiveRecord::Relation) allow(model).to receive_message_chain(:select, public_send: relation) @@ -315,13 +317,85 @@ RSpec.describe Gitlab::Database::BatchCount do end end - it_behaves_like 'when batch fetch query is canceled' do + context 'when the loose_index_scan_for_distinct_values feature flag is off' do + it_behaves_like 'when batch fetch query is canceled' do + let(:mode) { :distinct } + let(:operation) { :count } + let(:operation_args) { nil } + let(:column) { nil } + + subject { described_class.method(:batch_distinct_count) } + + before do + stub_feature_flags(loose_index_scan_for_distinct_values: false) + end + end + end + + context 'when the loose_index_scan_for_distinct_values feature flag is on' do let(:mode) { :distinct } let(:operation) { :count } let(:operation_args) { nil } let(:column) { nil } + let(:batch_size) { 10_000 } + subject { described_class.method(:batch_distinct_count) } + + before do + stub_feature_flags(loose_index_scan_for_distinct_values: true) + end + + it 'reduces batch size by half and retry fetch' do + too_big_batch_relation_mock = instance_double(ActiveRecord::Relation) + + count_method = double(send: 1) + + allow(too_big_batch_relation_mock).to receive(:send).and_raise(ActiveRecord::QueryCanceled) + allow(Gitlab::Database::LooseIndexScanDistinctCount).to receive_message_chain(:new, :build_query).with(from: 0, to: batch_size).and_return(too_big_batch_relation_mock) + allow(Gitlab::Database::LooseIndexScanDistinctCount).to receive_message_chain(:new, :build_query).with(from: 0, to: batch_size / 2).and_return(count_method) + allow(Gitlab::Database::LooseIndexScanDistinctCount).to receive_message_chain(:new, :build_query).with(from: batch_size / 2, to: batch_size).and_return(count_method) + + subject.call(model, column, batch_size: batch_size, start: 0, finish: batch_size - 1) + end + + context 'when all retries fail' do + let(:batch_count_query) { 'SELECT COUNT(id) FROM relation WHERE id BETWEEN 0 and 1' } + + before do + relation = instance_double(ActiveRecord::Relation) + allow(Gitlab::Database::LooseIndexScanDistinctCount).to receive_message_chain(:new, :build_query).and_return(relation) + allow(relation).to receive(:send).and_raise(ActiveRecord::QueryCanceled.new('query timed out')) + allow(relation).to receive(:to_sql).and_return(batch_count_query) + end + + it 'logs failing query' do + expect(Gitlab::AppJsonLogger).to receive(:error).with( + event: 'batch_count', + relation: model.table_name, + operation: operation, + operation_args: operation_args, + start: 0, + mode: mode, + 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) + end + end + + context 'when LooseIndexScanDistinctCount raises error' do + let(:column) { :creator_id } + let(:error_class) { Gitlab::Database::LooseIndexScanDistinctCount::ColumnConfigurationError } + + it 'rescues ColumnConfigurationError' do + allow(Gitlab::Database::LooseIndexScanDistinctCount).to receive(:new).and_raise(error_class.new('error message')) + + expect(Gitlab::AppJsonLogger).to receive(:error).with(a_hash_including(message: 'LooseIndexScanDistinctCount column error: error message')) + + expect(subject.call(Project, column, batch_size: 10_000, start: 0)).to eq(-1) + end + end end end diff --git a/spec/lib/gitlab/database/loose_index_scan_distinct_count_spec.rb b/spec/lib/gitlab/database/loose_index_scan_distinct_count_spec.rb new file mode 100644 index 00000000000..e0eac26e4d9 --- /dev/null +++ b/spec/lib/gitlab/database/loose_index_scan_distinct_count_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::LooseIndexScanDistinctCount do + context 'counting distinct users' do + let_it_be(:user) { create(:user) } + let_it_be(:other_user) { create(:user) } + + let(:column) { :creator_id } + + before_all do + create_list(:project, 3, creator: user) + create_list(:project, 1, creator: other_user) + end + + subject(:count) { described_class.new(Project, :creator_id).count(from: Project.minimum(:creator_id), to: Project.maximum(:creator_id) + 1) } + + it { is_expected.to eq(2) } + + context 'when STI model is queried' do + it 'does not raise error' do + expect { described_class.new(Group, :owner_id).count(from: 0, to: 1) }.not_to raise_error + end + end + + context 'when model with default_scope is queried' do + it 'does not raise error' do + expect { described_class.new(GroupMember, :id).count(from: 0, to: 1) }.not_to raise_error + end + end + + context 'when the fully qualified column is given' do + let(:column) { 'projects.creator_id' } + + it { is_expected.to eq(2) } + end + + context 'when AR attribute is given' do + let(:column) { Project.arel_table[:creator_id] } + + it { is_expected.to eq(2) } + end + + context 'when invalid value is given for the column' do + let(:column) { Class.new } + + it { expect { described_class.new(Group, column) }.to raise_error(Gitlab::Database::LooseIndexScanDistinctCount::ColumnConfigurationError) } + end + + context 'when null values are present' do + before do + create_list(:project, 2).each { |p| p.update_column(:creator_id, nil) } + end + + it { is_expected.to eq(2) } + end + end + + context 'counting STI models' do + let!(:groups) { create_list(:group, 3) } + let!(:namespaces) { create_list(:namespace, 2) } + + let(:max_id) { Namespace.maximum(:id) + 1 } + + it 'counts groups' do + count = described_class.new(Group, :id).count(from: 0, to: max_id) + expect(count).to eq(3) + end + end +end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 9178707a3d0..44293086e79 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -835,7 +835,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:check_trigger_permissions!).with(:users) expect(model).to receive(:install_rename_triggers_for_postgresql) - .with(trigger_name, '"users"', '"old"', '"new"') + .with(:users, :old, :new) expect(model).to receive(:add_column) .with(:users, :new, :integer, @@ -860,14 +860,18 @@ RSpec.describe Gitlab::Database::MigrationHelpers do context 'with existing records and type casting' do let(:trigger_name) { model.rename_trigger_name(:users, :id, :new) } let(:user) { create(:user) } + let(:copy_trigger) { double('copy trigger') } + + before do + expect(Gitlab::Database::UnidirectionalCopyTrigger).to receive(:on_table) + .with(:users).and_return(copy_trigger) + end it 'copies the value to the new column using the type_cast_function', :aggregate_failures do expect(model).to receive(:copy_indexes).with(:users, :id, :new) expect(model).to receive(:add_not_null_constraint).with(:users, :new) expect(model).to receive(:execute).with("UPDATE \"users\" SET \"new\" = cast_to_jsonb_with_default(\"users\".\"id\") WHERE \"users\".\"id\" >= #{user.id}") - expect(model).to receive(:execute).with("DROP TRIGGER IF EXISTS #{trigger_name}\nON \"users\"\n") - expect(model).to receive(:execute).with("CREATE TRIGGER #{trigger_name}\nBEFORE INSERT OR UPDATE\nON \"users\"\nFOR EACH ROW\nEXECUTE FUNCTION #{trigger_name}()\n") - expect(model).to receive(:execute).with("CREATE OR REPLACE FUNCTION #{trigger_name}()\nRETURNS trigger AS\n$BODY$\nBEGIN\n NEW.\"new\" := NEW.\"id\";\n RETURN NEW;\nEND;\n$BODY$\nLANGUAGE 'plpgsql'\nVOLATILE\n") + expect(copy_trigger).to receive(:create).with(:id, :new, trigger_name: nil) model.rename_column_concurrently(:users, :id, :new, type_cast_function: 'cast_to_jsonb_with_default') end @@ -996,7 +1000,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:check_trigger_permissions!).with(:users) expect(model).to receive(:install_rename_triggers_for_postgresql) - .with(trigger_name, '"users"', '"old"', '"new"') + .with(:users, :old, :new) expect(model).to receive(:add_column) .with(:users, :old, :integer, @@ -1156,7 +1160,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do .with(:users, temp_undo_cleanup_column, :old) expect(model).to receive(:install_rename_triggers_for_postgresql) - .with(trigger_name, '"users"', '"old"', '"old_for_type_change"') + .with(:users, :old, 'old_for_type_change') model.undo_cleanup_concurrent_column_type_change(:users, :old, :string) end @@ -1182,7 +1186,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do .with(:users, temp_undo_cleanup_column, :old) expect(model).to receive(:install_rename_triggers_for_postgresql) - .with(trigger_name, '"users"', '"old"', '"old_for_type_change"') + .with(:users, :old, 'old_for_type_change') model.undo_cleanup_concurrent_column_type_change( :users, @@ -1204,28 +1208,25 @@ RSpec.describe Gitlab::Database::MigrationHelpers do describe '#install_rename_triggers_for_postgresql' do it 'installs the triggers for PostgreSQL' do - expect(model).to receive(:execute) - .with(/CREATE OR REPLACE FUNCTION foo()/m) + copy_trigger = double('copy trigger') - expect(model).to receive(:execute) - .with(/DROP TRIGGER IF EXISTS foo/m) + expect(Gitlab::Database::UnidirectionalCopyTrigger).to receive(:on_table) + .with(:users).and_return(copy_trigger) - expect(model).to receive(:execute) - .with(/CREATE TRIGGER foo/m) + expect(copy_trigger).to receive(:create).with(:old, :new, trigger_name: 'foo') - model.install_rename_triggers_for_postgresql('foo', :users, :old, :new) - end - - it 'does not fail if trigger already exists' do - model.install_rename_triggers_for_postgresql('foo', :users, :old, :new) - model.install_rename_triggers_for_postgresql('foo', :users, :old, :new) + model.install_rename_triggers_for_postgresql(:users, :old, :new, trigger_name: 'foo') end end describe '#remove_rename_triggers_for_postgresql' do it 'removes the function and trigger' do - expect(model).to receive(:execute).with('DROP TRIGGER IF EXISTS foo ON bar') - expect(model).to receive(:execute).with('DROP FUNCTION IF EXISTS foo()') + copy_trigger = double('copy trigger') + + expect(Gitlab::Database::UnidirectionalCopyTrigger).to receive(:on_table) + .with('bar').and_return(copy_trigger) + + expect(copy_trigger).to receive(:drop).with('foo') model.remove_rename_triggers_for_postgresql('bar', 'foo') end @@ -1702,65 +1703,171 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end describe '#initialize_conversion_of_integer_to_bigint' do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:issue) { create(:issue, project: project) } - let!(:event) do - create(:event, :created, project: project, target: issue, author: user) + let(:table) { :test_table } + let(:column) { :id } + let(:tmp_column) { "#{column}_convert_to_bigint" } + + before do + model.create_table table, id: false do |t| + t.integer :id, primary_key: true + t.integer :non_nullable_column, null: false + t.integer :nullable_column + t.timestamps + end end - context 'in a transaction' do - it 'raises RuntimeError' do - allow(model).to receive(:transaction_open?).and_return(true) + context 'when the target table does not exist' do + it 'raises an error' do + expect { model.initialize_conversion_of_integer_to_bigint(:this_table_is_not_real, column) } + .to raise_error('Table this_table_is_not_real does not exist') + end + end - expect { model.initialize_conversion_of_integer_to_bigint(:events, :id) } - .to raise_error(RuntimeError) + context 'when the primary key does not exist' do + it 'raises an error' do + expect { model.initialize_conversion_of_integer_to_bigint(table, column, primary_key: :foobar) } + .to raise_error("Column foobar does not exist on #{table}") end end - context 'outside a transaction' do - before do - allow(model).to receive(:transaction_open?).and_return(false) + context 'when the column to convert does not exist' do + let(:column) { :foobar } + + it 'raises an error' do + expect { model.initialize_conversion_of_integer_to_bigint(table, column) } + .to raise_error("Column #{column} does not exist on #{table}") end + end - it 'creates a bigint column and starts backfilling it' do - expect(model) - .to receive(:add_column) - .with( - :events, - 'id_convert_to_bigint', - :bigint, - default: 0, - null: false - ) + context 'when the column to convert is the primary key' do + it 'creates a not-null bigint column and installs triggers' do + expect(model).to receive(:add_column).with(table, tmp_column, :bigint, default: 0, null: false) - expect(model) - .to receive(:install_rename_triggers) - .with(:events, :id, 'id_convert_to_bigint') + expect(model).to receive(:install_rename_triggers).with(table, column, tmp_column) - expect(model).to receive(:queue_background_migration_jobs_by_range_at_intervals).and_call_original + model.initialize_conversion_of_integer_to_bigint(table, column) + end + end - expect(BackgroundMigrationWorker) - .to receive(:perform_in) - .ordered - .with( - 2.minutes, - 'CopyColumnUsingBackgroundMigrationJob', - [event.id, event.id, :events, :id, 100, :id, 'id_convert_to_bigint'] - ) + context 'when the column to convert is not the primary key, but non-nullable' do + let(:column) { :non_nullable_column } + + it 'creates a not-null bigint column and installs triggers' do + expect(model).to receive(:add_column).with(table, tmp_column, :bigint, default: 0, null: false) + + expect(model).to receive(:install_rename_triggers).with(table, column, tmp_column) + + model.initialize_conversion_of_integer_to_bigint(table, column) + end + end + + context 'when the column to convert is not the primary key, but nullable' do + let(:column) { :nullable_column } + + it 'creates a nullable bigint column and installs triggers' do + expect(model).to receive(:add_column).with(table, tmp_column, :bigint, default: nil) + + expect(model).to receive(:install_rename_triggers).with(table, column, tmp_column) + + model.initialize_conversion_of_integer_to_bigint(table, column) + end + end + end + + describe '#backfill_conversion_of_integer_to_bigint' do + let(:table) { :_test_backfill_table } + let(:column) { :id } + let(:tmp_column) { "#{column}_convert_to_bigint" } + + before do + model.create_table table, id: false do |t| + t.integer :id, primary_key: true + t.text :message, null: false + t.timestamps + end - expect(Gitlab::BackgroundMigration) - .to receive(:steal) - .ordered - .with('CopyColumnUsingBackgroundMigrationJob') + allow(model).to receive(:perform_background_migration_inline?).and_return(false) + end - model.initialize_conversion_of_integer_to_bigint( - :events, - :id, - batch_size: 300, - sub_batch_size: 100 + context 'when the target table does not exist' do + it 'raises an error' do + expect { model.backfill_conversion_of_integer_to_bigint(:this_table_is_not_real, column) } + .to raise_error('Table this_table_is_not_real does not exist') + end + end + + context 'when the primary key does not exist' do + it 'raises an error' do + expect { model.backfill_conversion_of_integer_to_bigint(table, column, primary_key: :foobar) } + .to raise_error("Column foobar does not exist on #{table}") + end + end + + context 'when the column to convert does not exist' do + let(:column) { :foobar } + + it 'raises an error' do + expect { model.backfill_conversion_of_integer_to_bigint(table, column) } + .to raise_error("Column #{column} does not exist on #{table}") + end + end + + context 'when the temporary column does not exist' do + it 'raises an error' do + expect { model.backfill_conversion_of_integer_to_bigint(table, column) } + .to raise_error('The temporary column does not exist, initialize it with `initialize_conversion_of_integer_to_bigint`') + end + end + + context 'when the conversion is properly initialized' do + let(:model_class) do + Class.new(ActiveRecord::Base) do + self.table_name = :_test_backfill_table + end + end + + let(:migration_relation) { Gitlab::Database::BackgroundMigration::BatchedMigration.active } + + before do + model.initialize_conversion_of_integer_to_bigint(table, column) + + model_class.create!(message: 'hello') + model_class.create!(message: 'so long') + end + + it 'creates the batched migration tracking record' do + last_record = model_class.create!(message: 'goodbye') + + expect do + model.backfill_conversion_of_integer_to_bigint(table, column, batch_size: 2, sub_batch_size: 1) + end.to change { migration_relation.count }.by(1) + + expect(migration_relation.last).to have_attributes( + job_class_name: 'CopyColumnUsingBackgroundMigrationJob', + table_name: table.to_s, + column_name: column.to_s, + min_value: 1, + max_value: last_record.id, + interval: 120, + batch_size: 2, + sub_batch_size: 1, + job_arguments: [column.to_s, "#{column}_convert_to_bigint"] ) end + + context 'when the migration should be performed inline' do + it 'calls the runner to run the entire migration' do + expect(model).to receive(:perform_background_migration_inline?).and_return(true) + + expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |scheduler| + expect(scheduler).to receive(:run_entire_migration) do |batched_migration| + expect(batched_migration).to eq(migration_relation.last) + end + end + + model.backfill_conversion_of_integer_to_bigint(table, column, batch_size: 2, sub_batch_size: 1) + end + end end end @@ -1910,9 +2017,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do def setup namespace = namespaces.create!(name: 'foo', path: 'foo') - project = projects.create!(namespace_id: namespace.id) - - project + projects.create!(namespace_id: namespace.id) end it 'generates iids properly for models created after the migration' do diff --git a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb index e25e4af2e86..c6d456964cf 100644 --- a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb @@ -263,7 +263,15 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do end describe '#queue_batched_background_migration' do + let(:pgclass_info) { instance_double('Gitlab::Database::PgClass', cardinality_estimate: 42) } + + before do + allow(Gitlab::Database::PgClass).to receive(:for_table).and_call_original + end + it 'creates the database record for the migration' do + expect(Gitlab::Database::PgClass).to receive(:for_table).with(:projects).and_return(pgclass_info) + expect do model.queue_batched_background_migration( 'MyJobClass', @@ -288,7 +296,8 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do batch_size: 100, sub_batch_size: 10, job_arguments: %w[], - status: 'active') + status: 'active', + total_tuple_count: pgclass_info.cardinality_estimate) end context 'when the job interval is lower than the minimum' do @@ -431,4 +440,21 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do model.bulk_migrate_in(10.minutes, [%w(Class hello world)]) end end + + describe '#delete_queued_jobs' do + let(:job1) { double } + let(:job2) { double } + + it 'deletes all queued jobs for the given background migration' do + expect(Gitlab::BackgroundMigration).to receive(:steal).with('BackgroundMigrationClassName') do |&block| + expect(block.call(job1)).to be(false) + expect(block.call(job2)).to be(false) + end + + expect(job1).to receive(:delete) + expect(job2).to receive(:delete) + + model.delete_queued_jobs('BackgroundMigrationClassName') + 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 b5d741fc5e9..5b2a29d1d2d 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 @@ -704,6 +704,72 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end end + describe '#drop_nonpartitioned_archive_table' do + subject { migration.drop_nonpartitioned_archive_table source_table } + + let(:archived_table) { "#{source_table}_archived" } + + before do + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date + migration.replace_with_partitioned_table source_table + end + + it 'drops the archive table' do + expect(table_type(archived_table)).to eq('normal') + + subject + + expect(table_type(archived_table)).to eq(nil) + end + + it 'drops the trigger on the source table' do + expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update]) + + subject + + expect_trigger_not_to_exist(source_table, trigger_name) + end + + it 'drops the sync function' do + expect_function_to_exist(function_name) + + subject + + expect_function_not_to_exist(function_name) + end + end + + describe '#create_trigger_to_sync_tables' do + subject { migration.create_trigger_to_sync_tables(source_table, target_table, :id) } + + let(:target_table) { "#{source_table}_copy" } + + before do + migration.create_table target_table do |t| + t.string :name, null: false + t.integer :age, null: false + t.datetime partition_column + t.datetime :updated_at + end + end + + it 'creates the sync function' do + expect_function_not_to_exist(function_name) + + subject + + expect_function_to_exist(function_name) + end + + it 'installs the trigger' do + expect_trigger_not_to_exist(source_table, trigger_name) + + subject + + expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update]) + end + end + def filter_columns_by_name(columns, names) columns.reject { |c| names.include?(c.name) } end diff --git a/spec/lib/gitlab/database/pg_class_spec.rb b/spec/lib/gitlab/database/pg_class_spec.rb new file mode 100644 index 00000000000..83b50415a6c --- /dev/null +++ b/spec/lib/gitlab/database/pg_class_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::PgClass, type: :model do + describe '#cardinality_estimate' do + context 'when no information is available' do + subject { described_class.new(reltuples: 0.0).cardinality_estimate } + + it 'returns nil for the estimate' do + expect(subject).to be_nil + end + end + + context 'with reltuples available' do + subject { described_class.new(reltuples: 42.0).cardinality_estimate } + + it 'returns the reltuples for the estimate' do + expect(subject).to eq(42) + end + end + end + + describe '.for_table' do + let(:relname) { :projects } + + subject { described_class.for_table(relname) } + + it 'returns PgClass for this table' do + expect(subject).to be_a(described_class) + end + + it 'matches the relname' do + expect(subject.relname).to eq(relname.to_s) + end + end +end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb index 757da2d9092..1edcd890370 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb @@ -246,7 +246,8 @@ RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, : subject.track_rename('namespace', 'path/to/namespace', 'path/to/renamed') - old_path, new_path = [nil, nil] + old_path = nil + new_path = nil Gitlab::Redis::SharedState.with do |redis| rename_info = redis.lpop(key) old_path, new_path = Gitlab::Json.parse(rename_info) diff --git a/spec/lib/gitlab/database/unidirectional_copy_trigger_spec.rb b/spec/lib/gitlab/database/unidirectional_copy_trigger_spec.rb new file mode 100644 index 00000000000..2955c208f16 --- /dev/null +++ b/spec/lib/gitlab/database/unidirectional_copy_trigger_spec.rb @@ -0,0 +1,191 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::UnidirectionalCopyTrigger do + include Database::TriggerHelpers + + let(:table_name) { '_test_table' } + let(:connection) { ActiveRecord::Base.connection } + let(:copy_trigger) { described_class.on_table(table_name) } + + describe '#name' do + context 'when a single column name is given' do + subject(:trigger_name) { copy_trigger.name('id', 'other_id') } + + it 'returns the trigger name' do + expect(trigger_name).to eq('trigger_cfce7a56a9d6') + end + end + + context 'when multiple column names are given' do + subject(:trigger_name) { copy_trigger.name(%w[id fk_id], %w[other_id other_fk_id]) } + + it 'returns the trigger name' do + expect(trigger_name).to eq('trigger_166626e51481') + end + end + + context 'when a different number of new and old column names are given' do + it 'raises an error' do + expect do + copy_trigger.name(%w[id fk_id], %w[other_id]) + end.to raise_error(ArgumentError, 'number of source and destination columns must match') + end + end + end + + describe '#create' do + let(:model) { Class.new(ActiveRecord::Base) } + + before do + connection.execute(<<~SQL) + CREATE TABLE #{table_name} ( + id serial NOT NULL PRIMARY KEY, + other_id integer, + fk_id bigint, + other_fk_id bigint); + SQL + + model.table_name = table_name + end + + context 'when a single column name is given' do + let(:trigger_name) { 'trigger_cfce7a56a9d6' } + + it 'creates the trigger and function' do + expect_function_not_to_exist(trigger_name) + expect_trigger_not_to_exist(table_name, trigger_name) + + copy_trigger.create('id', 'other_id') + + expect_function_to_exist(trigger_name) + expect_valid_function_trigger(table_name, trigger_name, trigger_name, before: %w[insert update]) + end + + it 'properly copies the column data using the trigger function' do + copy_trigger.create('id', 'other_id') + + record = model.create!(id: 10) + expect(record.reload).to have_attributes(other_id: 10) + + record.update!({ id: 20 }) + expect(record.reload).to have_attributes(other_id: 20) + end + end + + context 'when multiple column names are given' do + let(:trigger_name) { 'trigger_166626e51481' } + + it 'creates the trigger and function to set all the columns' do + expect_function_not_to_exist(trigger_name) + expect_trigger_not_to_exist(table_name, trigger_name) + + copy_trigger.create(%w[id fk_id], %w[other_id other_fk_id]) + + expect_function_to_exist(trigger_name) + expect_valid_function_trigger(table_name, trigger_name, trigger_name, before: %w[insert update]) + end + + it 'properly copies the columns using the trigger function' do + copy_trigger.create(%w[id fk_id], %w[other_id other_fk_id]) + + record = model.create!(id: 10, fk_id: 20) + expect(record.reload).to have_attributes(other_id: 10, other_fk_id: 20) + + record.update!(id: 30, fk_id: 50) + expect(record.reload).to have_attributes(other_id: 30, other_fk_id: 50) + end + end + + context 'when a custom trigger name is given' do + let(:trigger_name) { '_test_trigger' } + + it 'creates the trigger and function with the custom name' do + expect_function_not_to_exist(trigger_name) + expect_trigger_not_to_exist(table_name, trigger_name) + + copy_trigger.create('id', 'other_id', trigger_name: trigger_name) + + expect_function_to_exist(trigger_name) + expect_valid_function_trigger(table_name, trigger_name, trigger_name, before: %w[insert update]) + end + end + + context 'when the trigger function already exists' do + let(:trigger_name) { 'trigger_cfce7a56a9d6' } + + it 'does not raise an error' do + expect_function_not_to_exist(trigger_name) + expect_trigger_not_to_exist(table_name, trigger_name) + + copy_trigger.create('id', 'other_id') + + expect_function_to_exist(trigger_name) + expect_valid_function_trigger(table_name, trigger_name, trigger_name, before: %w[insert update]) + + copy_trigger.create('id', 'other_id') + + expect_function_to_exist(trigger_name) + expect_valid_function_trigger(table_name, trigger_name, trigger_name, before: %w[insert update]) + end + end + + context 'when a different number of new and old column names are given' do + it 'raises an error' do + expect do + copy_trigger.create(%w[id fk_id], %w[other_id]) + end.to raise_error(ArgumentError, 'number of source and destination columns must match') + end + end + end + + describe '#drop' do + let(:trigger_name) { '_test_trigger' } + + before do + connection.execute(<<~SQL) + CREATE TABLE #{table_name} ( + id serial NOT NULL PRIMARY KEY, + other_id integer NOT NULL); + + CREATE FUNCTION #{trigger_name}() + RETURNS trigger + LANGUAGE plpgsql AS + $$ + BEGIN + RAISE NOTICE 'hello'; + RETURN NEW; + END + $$; + + CREATE TRIGGER #{trigger_name} + BEFORE INSERT OR UPDATE + ON #{table_name} + FOR EACH ROW + EXECUTE FUNCTION #{trigger_name}(); + SQL + end + + it 'drops the trigger and function for the given arguments' do + expect_function_to_exist(trigger_name) + expect_valid_function_trigger(table_name, trigger_name, trigger_name, before: %w[insert update]) + + copy_trigger.drop(trigger_name) + + expect_trigger_not_to_exist(table_name, trigger_name) + expect_function_not_to_exist(trigger_name) + end + + context 'when the trigger does not exist' do + it 'does not raise an error' do + copy_trigger.drop(trigger_name) + + expect_trigger_not_to_exist(table_name, trigger_name) + expect_function_not_to_exist(trigger_name) + + copy_trigger.drop(trigger_name) + end + end + end +end diff --git a/spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb b/spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb index 39029322e25..e70b34d6557 100644 --- a/spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb +++ b/spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb @@ -38,7 +38,7 @@ RSpec.describe Gitlab::DatabaseImporters::InstanceAdministrators::CreateGroup do end end - context 'with application settings and admin users' do + context 'with application settings and admin users', :do_not_mock_admin_mode_setting do let(:group) { result[:group] } let(:application_setting) { Gitlab::CurrentSettings.current_application_settings } diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 1553a989dba..b735ac7940b 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -407,13 +407,13 @@ RSpec.describe Gitlab::Database do expect(described_class.db_read_only?).to be_truthy end - it 'detects a read write database' do + it 'detects a read-write database' do allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "f" }]) expect(described_class.db_read_only?).to be_falsey end - it 'detects a read write database' do + it 'detects a read-write database' do allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => false }]) expect(described_class.db_read_only?).to be_falsey diff --git a/spec/lib/gitlab/diff/char_diff_spec.rb b/spec/lib/gitlab/diff/char_diff_spec.rb index e4e2a3ba050..d38008c16f2 100644 --- a/spec/lib/gitlab/diff/char_diff_spec.rb +++ b/spec/lib/gitlab/diff/char_diff_spec.rb @@ -49,15 +49,15 @@ RSpec.describe Gitlab::Diff::CharDiff do old_diffs, new_diffs = subject expect(old_diffs).to eq([]) - expect(new_diffs).to eq([0..12]) + expect(new_diffs).to eq([Gitlab::MarkerRange.new(0, 12, mode: :addition)]) end end it 'returns ranges of changes' do old_diffs, new_diffs = subject - expect(old_diffs).to eq([11..11]) - expect(new_diffs).to eq([3..3]) + expect(old_diffs).to eq([Gitlab::MarkerRange.new(11, 11, mode: :deletion)]) + expect(new_diffs).to eq([Gitlab::MarkerRange.new(3, 3, mode: :addition)]) end end diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb index d26bc5fc9a8..4c56911e665 100644 --- a/spec/lib/gitlab/diff/highlight_cache_spec.rb +++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb @@ -238,16 +238,36 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do subject { cache.key } it 'returns cache key' do - is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true") + is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:true:true") end - context 'when feature flag is disabled' do + context 'when the `introduce_marker_ranges` feature flag is disabled' do before do stub_feature_flags(introduce_marker_ranges: false) end it 'returns the original version of the cache' do - is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:false") + is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:false:true:true") + end + end + + context 'when the `use_marker_ranges` feature flag is disabled' do + before do + stub_feature_flags(use_marker_ranges: false) + end + + it 'returns the original version of the cache' do + is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:false:true") + end + end + + context 'when the `diff_line_syntax_highlighting` feature flag is disabled' do + before do + stub_feature_flags(diff_line_syntax_highlighting: false) + end + + it 'returns the original version of the cache' do + is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:true:false") end end end diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index e613674af3a..32ca6e4fde6 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -65,6 +65,14 @@ RSpec.describe Gitlab::Diff::Highlight do expect(subject[5].rich_text).to eq(code) end + + context 'when use_marker_ranges feature flag is false too' do + it 'does not affect the result' do + code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left">RuntimeError</span></span><span class="p"><span class="idiff">,</span></span><span class="idiff right"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n} + + expect(subject[5].rich_text).to eq(code) + end + end end context 'when no diff_refs' do @@ -132,6 +140,18 @@ RSpec.describe Gitlab::Diff::Highlight do end end + context 'when `use_marker_ranges` feature flag is disabled' do + it 'returns the same result' do + with_feature_flag = described_class.new(diff_file, repository: project.repository).highlight + + stub_feature_flags(use_marker_ranges: false) + + without_feature_flag = described_class.new(diff_file, repository: project.repository).highlight + + expect(with_feature_flag.map(&:rich_text)).to eq(without_feature_flag.map(&:rich_text)) + end + end + context 'when no inline diffs' do it_behaves_like 'without inline diffs' end diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb index 714b5d813c4..d7b50eb73ee 100644 --- a/spec/lib/gitlab/diff/inline_diff_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_spec.rb @@ -3,68 +3,30 @@ require 'spec_helper' RSpec.describe Gitlab::Diff::InlineDiff do - describe '.for_lines' do - let(:diff) do - <<-EOF.strip_heredoc - class Test - - def initialize(test = true) - + def initialize(test = false) - @test = test - - if true - - @foo = "bar" - + unless false - + @foo = "baz" - end - end - end - EOF - end - - let(:subject) { described_class.for_lines(diff.lines) } + describe '#inline_diffs' do + subject { described_class.new(old_line, new_line, offset: offset).inline_diffs } - it 'finds all inline diffs' do - expect(subject[0]).to be_nil - expect(subject[1]).to eq([25..27]) - expect(subject[2]).to eq([25..28]) - expect(subject[3]).to be_nil - expect(subject[4]).to eq([5..10]) - expect(subject[5]).to eq([17..17]) - expect(subject[6]).to eq([5..15]) - expect(subject[7]).to eq([17..17]) - expect(subject[8]).to be_nil - end + let(:old_line) { 'XXX def initialize(test = true)' } + let(:new_line) { 'YYY def initialize(test = false)' } + let(:offset) { 3 } - it 'can handle unchanged empty lines' do - expect { described_class.for_lines(['- bar', '+ baz', '']) }.not_to raise_error + it 'finds the inline diff', :aggregate_failures do + expect(subject[0]).to eq([Gitlab::MarkerRange.new(26, 28, mode: :deletion)]) + expect(subject[1]).to eq([Gitlab::MarkerRange.new(26, 29, mode: :addition)]) end context 'when lines have multiple changes' do - let(:diff) do - <<~EOF - - Hello, how are you? - + Hi, how are you doing? - EOF - end - - let(:subject) { described_class.for_lines(diff.lines) } - - it 'finds all inline diffs' do - expect(subject[0]).to eq([3..6]) - expect(subject[1]).to eq([3..3, 17..22]) + let(:old_line) { '- Hello, how are you?' } + let(:new_line) { '+ Hi, how are you doing?' } + let(:offset) { 1 } + + it 'finds all inline diffs', :aggregate_failures do + expect(subject[0]).to eq([Gitlab::MarkerRange.new(3, 6, mode: :deletion)]) + expect(subject[1]).to eq([ + Gitlab::MarkerRange.new(3, 3, mode: :addition), + Gitlab::MarkerRange.new(17, 22, mode: :addition) + ]) end end end - - describe "#inline_diffs" do - let(:old_line) { "XXX def initialize(test = true)" } - let(:new_line) { "YYY def initialize(test = false)" } - let(:subject) { described_class.new(old_line, new_line, offset: 3).inline_diffs } - - it "finds the inline diff" do - old_diffs, new_diffs = subject - - expect(old_diffs).to eq([26..28]) - expect(new_diffs).to eq([26..29]) - end - end end diff --git a/spec/lib/gitlab/diff/line_spec.rb b/spec/lib/gitlab/diff/line_spec.rb index e10a50afde9..949def599ae 100644 --- a/spec/lib/gitlab/diff/line_spec.rb +++ b/spec/lib/gitlab/diff/line_spec.rb @@ -17,6 +17,8 @@ RSpec.describe Gitlab::Diff::Line do rich_text: rich_text) end + let(:rich_text) { nil } + describe '.init_from_hash' do let(:rich_text) { '<input>' } @@ -43,6 +45,29 @@ RSpec.describe Gitlab::Diff::Line do end end + describe '#text' do + let(:line) { described_class.new(raw_diff, 'new', 0, 0, 0) } + let(:raw_diff) { '+Hello' } + + it 'returns raw diff text' do + expect(line.text).to eq('+Hello') + end + + context 'when prefix is disabled' do + it 'returns raw diff text without prefix' do + expect(line.text(prefix: false)).to eq('Hello') + end + + context 'when diff is empty' do + let(:raw_diff) { '' } + + it 'returns an empty raw diff' do + expect(line.text(prefix: false)).to eq('') + end + end + end + end + context "when setting rich text" do it 'escapes any HTML special characters in the diff chunk header' do subject = described_class.new("<input>", "", 0, 0, 0) @@ -51,4 +76,14 @@ RSpec.describe Gitlab::Diff::Line do expect(line[:rich_text]).to eq("<input>") end end + + describe '#set_marker_ranges' do + let(:marker_ranges) { [Gitlab::MarkerRange.new(1, 10, mode: :deletion)] } + + it 'stores MarkerRanges in Diff::Line object' do + line.set_marker_ranges(marker_ranges) + + expect(line.marker_ranges).to eq(marker_ranges) + end + end end diff --git a/spec/lib/gitlab/diff/lines_unfolder_spec.rb b/spec/lib/gitlab/diff/lines_unfolder_spec.rb index 4163c0eced5..8385cba3532 100644 --- a/spec/lib/gitlab/diff/lines_unfolder_spec.rb +++ b/spec/lib/gitlab/diff/lines_unfolder_spec.rb @@ -302,7 +302,8 @@ RSpec.describe Gitlab::Diff::LinesUnfolder do new_diff_lines = subject.unfolded_diff_lines new_diff_lines.each_with_index do |line, i| - old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1] + old_pos = expected_diff_lines[i][0] + new_pos = expected_diff_lines[i][1] unless line.type == 'match' expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos)) @@ -396,7 +397,8 @@ RSpec.describe Gitlab::Diff::LinesUnfolder do new_diff_lines = subject.unfolded_diff_lines new_diff_lines.each_with_index do |line, i| - old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1] + old_pos = expected_diff_lines[i][0] + new_pos = expected_diff_lines[i][1] unless line.type == 'match' expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos)) @@ -490,7 +492,8 @@ RSpec.describe Gitlab::Diff::LinesUnfolder do new_diff_lines = subject.unfolded_diff_lines new_diff_lines.each_with_index do |line, i| - old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1] + old_pos = expected_diff_lines[i][0] + new_pos = expected_diff_lines[i][1] unless line.type == 'match' expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos)) @@ -581,7 +584,8 @@ RSpec.describe Gitlab::Diff::LinesUnfolder do new_diff_lines = subject.unfolded_diff_lines new_diff_lines.each_with_index do |line, i| - old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1] + old_pos = expected_diff_lines[i][0] + new_pos = expected_diff_lines[i][1] unless line.type == 'match' expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos)) @@ -691,7 +695,8 @@ RSpec.describe Gitlab::Diff::LinesUnfolder do new_diff_lines = subject.unfolded_diff_lines new_diff_lines.each_with_index do |line, i| - old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1] + old_pos = expected_diff_lines[i][0] + new_pos = expected_diff_lines[i][1] unless line.type == 'match' expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos)) @@ -783,7 +788,8 @@ RSpec.describe Gitlab::Diff::LinesUnfolder do new_diff_lines = subject.unfolded_diff_lines new_diff_lines.each_with_index do |line, i| - old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1] + old_pos = expected_diff_lines[i][0] + new_pos = expected_diff_lines[i][1] unless line.type == 'match' expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos)) diff --git a/spec/lib/gitlab/diff/suggestions_parser_spec.rb b/spec/lib/gitlab/diff/suggestions_parser_spec.rb index 5efce414dc8..a00c55d4fb2 100644 --- a/spec/lib/gitlab/diff/suggestions_parser_spec.rb +++ b/spec/lib/gitlab/diff/suggestions_parser_spec.rb @@ -56,7 +56,8 @@ RSpec.describe Gitlab::Diff::SuggestionsParser do end it 'parsed suggestion has correct data' do - from_line, to_line = position.new_line, position.new_line + from_line = position.new_line + to_line = position.new_line expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line), to_content: " foo\n bar\n", diff --git a/spec/lib/gitlab/downtime_check/message_spec.rb b/spec/lib/gitlab/downtime_check/message_spec.rb deleted file mode 100644 index 2d82836db33..00000000000 --- a/spec/lib/gitlab/downtime_check/message_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::DowntimeCheck::Message do - describe '#to_s' do - it 'returns an ANSI formatted String for an offline migration' do - message = described_class.new('foo.rb', true, 'hello') - - expect(message.to_s).to eq("[\e[31moffline\e[0m]: foo.rb:\n\nhello\n\n") - end - - it 'returns an ANSI formatted String for an online migration' do - message = described_class.new('foo.rb') - - expect(message.to_s).to eq("[\e[32monline\e[0m]: foo.rb") - end - end - - describe '#reason?' do - it 'returns false when no reason is specified' do - message = described_class.new('foo.rb') - - expect(message.reason?).to eq(false) - end - - it 'returns true when a reason is specified' do - message = described_class.new('foo.rb', true, 'hello') - - expect(message.reason?).to eq(true) - end - end - - describe '#reason' do - it 'strips excessive whitespace from the returned String' do - message = described_class.new('foo.rb', true, " hello\n world\n\n foo") - - expect(message.reason).to eq("hello\nworld\n\nfoo") - end - end -end diff --git a/spec/lib/gitlab/downtime_check_spec.rb b/spec/lib/gitlab/downtime_check_spec.rb deleted file mode 100644 index 761519425f6..00000000000 --- a/spec/lib/gitlab/downtime_check_spec.rb +++ /dev/null @@ -1,116 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::DowntimeCheck do - subject { described_class.new } - - let(:path) { 'foo.rb' } - - describe '#check' do - before do - expect(subject).to receive(:require).with(path) - end - - context 'when a migration does not specify if downtime is required' do - it 'raises RuntimeError' do - expect(subject).to receive(:class_for_migration_file) - .with(path) - .and_return(Class.new) - - expect { subject.check([path]) } - .to raise_error(RuntimeError, /it requires downtime/) - end - end - - context 'when a migration requires downtime' do - context 'when no reason is specified' do - it 'raises RuntimeError' do - stub_const('TestMigration::DOWNTIME', true) - - expect(subject).to receive(:class_for_migration_file) - .with(path) - .and_return(TestMigration) - - expect { subject.check([path]) } - .to raise_error(RuntimeError, /no reason was given/) - end - end - - context 'when a reason is specified' do - it 'returns an Array of messages' do - stub_const('TestMigration::DOWNTIME', true) - stub_const('TestMigration::DOWNTIME_REASON', 'foo') - - expect(subject).to receive(:class_for_migration_file) - .with(path) - .and_return(TestMigration) - - messages = subject.check([path]) - - expect(messages).to be_an_instance_of(Array) - expect(messages[0]).to be_an_instance_of(Gitlab::DowntimeCheck::Message) - - message = messages[0] - - expect(message.path).to eq(path) - expect(message.offline).to eq(true) - expect(message.reason).to eq('foo') - end - end - end - end - - describe '#check_and_print' do - it 'checks the migrations and prints the results to STDOUT' do - stub_const('TestMigration::DOWNTIME', true) - stub_const('TestMigration::DOWNTIME_REASON', 'foo') - - expect(subject).to receive(:require).with(path) - - expect(subject).to receive(:class_for_migration_file) - .with(path) - .and_return(TestMigration) - - expect(subject).to receive(:puts).with(an_instance_of(String)) - - subject.check_and_print([path]) - end - end - - describe '#class_for_migration_file' do - it 'returns the class for a migration file path' do - expect(subject.class_for_migration_file('123_string.rb')).to eq(String) - end - end - - describe '#online?' do - it 'returns true when a migration can be performed online' do - stub_const('TestMigration::DOWNTIME', false) - - expect(subject.online?(TestMigration)).to eq(true) - end - - it 'returns false when a migration can not be performed online' do - stub_const('TestMigration::DOWNTIME', true) - - expect(subject.online?(TestMigration)).to eq(false) - end - end - - describe '#downtime_reason' do - context 'when a reason is defined' do - it 'returns the downtime reason' do - stub_const('TestMigration::DOWNTIME_REASON', 'hello') - - expect(subject.downtime_reason(TestMigration)).to eq('hello') - end - end - - context 'when a reason is not defined' do - it 'returns nil' do - expect(subject.downtime_reason(Class.new)).to be_nil - end - end - end -end diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 8872800069a..e76a5d3fe32 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -59,7 +59,7 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do end shared_examples 'a reply to existing comment' do - it 'creates a comment' do + it 'creates a discussion' do expect { receiver.execute }.to change { noteable.notes.count }.by(1) new_note = noteable.notes.last @@ -68,11 +68,7 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do expect(new_note.note).to include('I could not disagree more.') expect(new_note.in_reply_to?(note)).to be_truthy - if note.part_of_discussion? - expect(new_note.discussion_id).to eq(note.discussion_id) - else - expect(new_note.discussion_id).not_to eq(note.discussion_id) - end + expect(new_note.discussion_id).to eq(note.discussion_id) end end diff --git a/spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb index 94f28d3399a..d3535fa9bd3 100644 --- a/spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb @@ -22,7 +22,7 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteOnIssuableHandler do it_behaves_like :note_handler_shared_examples, true do let_it_be(:recipient) { user } - let(:update_commands_only) { email_reply_fixture('emails/update_commands_only_reply.eml') } + let(:update_commands_only) { email_reply_fixture('emails/update_commands_only.eml') } let(:no_content) { email_reply_fixture('emails/no_content_reply.eml') } let(:commands_in_reply) { email_reply_fixture('emails/commands_in_reply.eml') } let(:with_quick_actions) { email_reply_fixture('emails/valid_reply_with_quick_actions.eml') } diff --git a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb index 13ad9ddd8ef..2c1badbd113 100644 --- a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb @@ -74,7 +74,7 @@ RSpec.describe Gitlab::Email::Handler::UnsubscribeHandler do context 'when the noteable could not be found' do before do - noteable.destroy + noteable.destroy! end it 'raises a NoteableNotFoundError' do 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 0db40eca989..24f5299d357 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 @@ -1,45 +1,66 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' RSpec.describe Gitlab::ErrorTracking::Processor::ContextPayloadProcessor do - subject(:processor) { described_class.new } - - before do - allow_next_instance_of(Gitlab::ErrorTracking::ContextPayloadGenerator) do |generator| - allow(generator).to receive(:generate).and_return( - user: { username: 'root' }, - tags: { locale: 'en', program: 'test', feature_category: 'feature_a', correlation_id: 'cid' }, - extra: { some_info: 'info' } - ) + shared_examples 'processing an exception' do + before do + allow_next_instance_of(Gitlab::ErrorTracking::ContextPayloadGenerator) do |generator| + allow(generator).to receive(:generate).and_return( + user: { username: 'root' }, + tags: { locale: 'en', program: 'test', feature_category: 'feature_a', correlation_id: 'cid' }, + extra: { some_info: 'info' } + ) + end end - end - it 'merges the context payload into event payload' do - payload = { - user: { ip_address: '127.0.0.1' }, - tags: { priority: 'high' }, - extra: { sidekiq: { class: 'SomeWorker', args: ['[FILTERED]', 1, 2] } } - } - - processor.process(payload) - - expect(payload).to eql( - user: { - ip_address: '127.0.0.1', - username: 'root' - }, - tags: { - priority: 'high', - locale: 'en', - program: 'test', - feature_category: 'feature_a', - correlation_id: 'cid' - }, - extra: { - some_info: 'info', - sidekiq: { class: 'SomeWorker', args: ['[FILTERED]', 1, 2] } + let(:payload) do + { + user: { ip_address: '127.0.0.1' }, + tags: { priority: 'high' }, + extra: { sidekiq: { class: 'SomeWorker', args: ['[FILTERED]', 1, 2] } } } - ) + end + + it 'merges the context payload into event payload', :aggregate_failures do + expect(result_hash[:user]).to include(ip_address: '127.0.0.1', username: 'root') + + expect(result_hash[:tags]) + .to include(priority: 'high', + locale: 'en', + program: 'test', + feature_category: 'feature_a', + correlation_id: 'cid') + + expect(result_hash[:extra]) + .to include(some_info: 'info', + sidekiq: { class: 'SomeWorker', args: ['[FILTERED]', 1, 2] }) + end + end + + describe '.call' do + let(:event) { Raven::Event.new(payload) } + let(:result_hash) { described_class.call(event).to_hash } + + it_behaves_like 'processing an exception' + + context 'when followed by #process' do + let(:result_hash) { described_class.new.process(described_class.call(event).to_hash) } + + it_behaves_like 'processing an exception' + end + end + + describe '#process' do + let(:event) { Raven::Event.new(payload) } + let(:result_hash) { described_class.new.process(event.to_hash) } + + context 'with sentry_processors_before_send disabled' do + before do + stub_feature_flags(sentry_processors_before_send: false) + end + + it_behaves_like 'processing an exception' + end end end diff --git a/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb index 797707114a1..4808fdf2f06 100644 --- a/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb +++ b/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb @@ -3,73 +3,83 @@ require 'spec_helper' RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor do - describe '#process' do - subject { described_class.new } - + shared_examples 'processing an exception' do context 'when there is no GRPC exception' do + let(:exception) { RuntimeError.new } let(:data) { { fingerprint: ['ArgumentError', 'Missing arguments'] } } it 'leaves data unchanged' do - expect(subject.process(data)).to eq(data) + expect(result_hash).to include(data) end end context 'when there is a GPRC exception with a debug string' do + let(:exception) { GRPC::DeadlineExceeded.new('Deadline Exceeded', {}, '{"hello":1}') } + let(:data) do { - exception: { - values: [ - { - type: "GRPC::DeadlineExceeded", - value: "4:DeadlineExceeded. debug_error_string:{\"hello\":1}" - } - ] - }, extra: { caller: 'test' }, fingerprint: [ - "GRPC::DeadlineExceeded", - "4:Deadline Exceeded. debug_error_string:{\"created\":\"@1598938192.005782000\",\"description\":\"Error received from peer unix:/home/git/gitalypraefect.socket\",\"file\":\"src/core/lib/surface/call.cc\",\"file_line\":1055,\"grpc_message\":\"Deadline Exceeded\",\"grpc_status\":4}" + 'GRPC::DeadlineExceeded', + '4:Deadline Exceeded. debug_error_string:{"created":"@1598938192.005782000","description":"Error received from peer unix:/home/git/gitalypraefect.socket","file":"src/core/lib/surface/call.cc","file_line":1055,"grpc_message":"Deadline Exceeded","grpc_status":4}' ] } end - let(:expected) do - { - fingerprint: [ - "GRPC::DeadlineExceeded", - "4:Deadline Exceeded." - ], - exception: { - values: [ - { - type: "GRPC::DeadlineExceeded", - value: "4:DeadlineExceeded." - } - ] - }, - extra: { - caller: 'test', - grpc_debug_error_string: "{\"hello\":1}" - } - } - end - it 'removes the debug error string and stores it as an extra field' do - expect(subject.process(data)).to eq(expected) + expect(result_hash[:fingerprint]) + .to eq(['GRPC::DeadlineExceeded', '4:Deadline Exceeded.']) + + expect(result_hash[:exception][:values].first) + .to include(type: 'GRPC::DeadlineExceeded', value: '4:Deadline Exceeded.') + + expect(result_hash[:extra]) + .to include(caller: 'test', grpc_debug_error_string: '{"hello":1}') end context 'with no custom fingerprint' do - before do - data.delete(:fingerprint) - expected.delete(:fingerprint) + let(:data) do + { extra: { caller: 'test' } } end it 'removes the debug error string and stores it as an extra field' do - expect(subject.process(data)).to eq(expected) + expect(result_hash).not_to include(:fingerprint) + + expect(result_hash[:exception][:values].first) + .to include(type: 'GRPC::DeadlineExceeded', value: '4:Deadline Exceeded.') + + expect(result_hash[:extra]) + .to include(caller: 'test', grpc_debug_error_string: '{"hello":1}') end end end end + + describe '.call' do + let(:event) { Raven::Event.from_exception(exception, data) } + let(:result_hash) { described_class.call(event).to_hash } + + it_behaves_like 'processing an exception' + + context 'when followed by #process' do + let(:result_hash) { described_class.new.process(described_class.call(event).to_hash) } + + it_behaves_like 'processing an exception' + end + end + + describe '#process' do + let(:event) { Raven::Event.from_exception(exception, data) } + let(:result_hash) { described_class.new.process(event.to_hash) } + + context 'with sentry_processors_before_send disabled' do + before do + stub_feature_flags(sentry_processors_before_send: false) + end + + it_behaves_like 'processing an exception' + end + end end diff --git a/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb index da7205c7f4f..20fd5d085a9 100644 --- a/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb +++ b/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb @@ -94,28 +94,37 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor do end end - describe '#process' do + shared_examples 'processing an exception' do context 'when there is Sidekiq data' do + let(:wrapped_value) { { extra: { sidekiq: value } } } + shared_examples 'Sidekiq arguments' do |args_in_job_hash: true| let(:path) { [:extra, :sidekiq, args_in_job_hash ? :job : nil, 'args'].compact } let(:args) { [1, 'string', { a: 1 }, [1, 2]] } - it 'only allows numeric arguments for an unknown worker' do - value = { 'args' => args, 'class' => 'UnknownWorker' } + context 'for an unknown worker' do + let(:value) do + hash = { 'args' => args, 'class' => 'UnknownWorker' } - value = { job: value } if args_in_job_hash + args_in_job_hash ? { job: hash } : hash + end - expect(subject.process(extra_sidekiq(value)).dig(*path)) - .to eq([1, described_class::FILTERED_STRING, described_class::FILTERED_STRING, described_class::FILTERED_STRING]) + it 'only allows numeric arguments for an unknown worker' do + expect(result_hash.dig(*path)) + .to eq([1, described_class::FILTERED_STRING, described_class::FILTERED_STRING, described_class::FILTERED_STRING]) + end end - it 'allows all argument types for a permitted worker' do - value = { 'args' => args, 'class' => 'PostReceive' } + context 'for a permitted worker' do + let(:value) do + hash = { 'args' => args, 'class' => 'PostReceive' } - value = { job: value } if args_in_job_hash + args_in_job_hash ? { job: hash } : hash + end - expect(subject.process(extra_sidekiq(value)).dig(*path)) - .to eq(args) + it 'allows all argument types for a permitted worker' do + expect(result_hash.dig(*path)).to eq(args) + end end end @@ -127,39 +136,62 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor do include_examples 'Sidekiq arguments', args_in_job_hash: false end - it 'removes a jobstr field if present' do - value = { - job: { 'args' => [1] }, - jobstr: { 'args' => [1] }.to_json - } + context 'when a jobstr field is present' do + let(:value) do + { + job: { 'args' => [1] }, + jobstr: { 'args' => [1] }.to_json + } + end - expect(subject.process(extra_sidekiq(value))) - .to eq(extra_sidekiq(value.except(:jobstr))) + it 'removes the jobstr' do + expect(result_hash.dig(:extra, :sidekiq)).to eq(value.except(:jobstr)) + end end - it 'does nothing with no jobstr' do - value = { job: { 'args' => [1] } } + context 'when no jobstr value is present' do + let(:value) { { job: { 'args' => [1] } } } - expect(subject.process(extra_sidekiq(value))) - .to eq(extra_sidekiq(value)) + it 'does nothing' do + expect(result_hash.dig(:extra, :sidekiq)).to eq(value) + end end end context 'when there is no Sidekiq data' do - it 'does nothing' do - value = { - request: { - method: 'POST', - data: { 'key' => 'value' } - } - } + let(:value) { { tags: { foo: 'bar', baz: 'quux' } } } + let(:wrapped_value) { value } - expect(subject.process(value)).to eq(value) + it 'does nothing' do + expect(result_hash).to include(value) + expect(result_hash.dig(:extra, :sidekiq)).to be_nil end end + end + + describe '.call' do + let(:event) { Raven::Event.new(wrapped_value) } + let(:result_hash) { described_class.call(event).to_hash } + + it_behaves_like 'processing an exception' + + context 'when followed by #process' do + let(:result_hash) { described_class.new.process(described_class.call(event).to_hash) } + + it_behaves_like 'processing an exception' + end + end + + describe '#process' do + let(:event) { Raven::Event.new(wrapped_value) } + let(:result_hash) { described_class.new.process(event.to_hash) } + + context 'with sentry_processors_before_send disabled' do + before do + stub_feature_flags(sentry_processors_before_send: false) + end - def extra_sidekiq(hash) - { extra: { sidekiq: hash } } + it_behaves_like 'processing an exception' end end end diff --git a/spec/lib/gitlab/error_tracking_spec.rb b/spec/lib/gitlab/error_tracking_spec.rb index a905b9f8d40..2e67a9f0874 100644 --- a/spec/lib/gitlab/error_tracking_spec.rb +++ b/spec/lib/gitlab/error_tracking_spec.rb @@ -7,6 +7,7 @@ require 'raven/transports/dummy' RSpec.describe Gitlab::ErrorTracking do let(:exception) { RuntimeError.new('boom') } let(:issue_url) { 'http://gitlab.com/gitlab-org/gitlab-foss/issues/1' } + let(:extra) { { issue_url: issue_url, some_other_info: 'info' } } let(:user) { create(:user) } @@ -42,6 +43,8 @@ RSpec.describe Gitlab::ErrorTracking do } end + let(:sentry_event) { Gitlab::Json.parse(Raven.client.transport.events.last[1]) } + before do stub_sentry_settings @@ -133,8 +136,6 @@ RSpec.describe Gitlab::ErrorTracking do end describe '.track_exception' do - let(:extra) { { issue_url: issue_url, some_other_info: 'info' } } - subject(:track_exception) { described_class.track_exception(exception, extra) } before do @@ -195,6 +196,55 @@ RSpec.describe Gitlab::ErrorTracking do end end + context 'when the error is kind of an `ActiveRecord::StatementInvalid`' do + let(:exception) { ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1') } + + it 'injects the normalized sql query into extra' do + track_exception + + expect(sentry_event.dig('extra', 'sql')).to eq('SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1') + end + end + + context 'when the `ActiveRecord::StatementInvalid` is wrapped in another exception' do + it 'injects the normalized sql query into extra' do + allow(exception).to receive(:cause).and_return(ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1')) + + track_exception + + expect(sentry_event.dig('extra', 'sql')).to eq('SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1') + end + end + end + + shared_examples 'event processors' do + subject(:track_exception) { described_class.track_exception(exception, extra) } + + before do + allow(Raven).to receive(:capture_exception).and_call_original + allow(Gitlab::ErrorTracking::Logger).to receive(:error) + end + + context 'custom GitLab context when using Raven.capture_exception directly' do + subject(:raven_capture_exception) { Raven.capture_exception(exception) } + + it 'merges a default set of tags into the existing tags' do + allow(Raven.context).to receive(:tags).and_return(foo: 'bar') + + raven_capture_exception + + expect(sentry_event['tags']).to include('correlation_id', 'feature_category', 'foo', 'locale', 'program') + end + + it 'merges the current user information into the existing user information' do + Raven.user_context(id: -1) + + raven_capture_exception + + expect(sentry_event['user']).to eq('id' => -1, 'username' => user.username) + end + end + context 'with sidekiq args' do context 'when the args does not have anything sensitive' do let(:extra) { { sidekiq: { 'class' => 'PostReceive', 'args' => [1, { 'id' => 2, 'name' => 'hello' }, 'some-value', 'another-value'] } } } @@ -211,16 +261,20 @@ RSpec.describe Gitlab::ErrorTracking do ) ) end + + it 'does not filter parameters when sending to Sentry' do + track_exception + + expect(sentry_event.dig('extra', 'sidekiq', 'args')).to eq([1, { 'id' => 2, 'name' => 'hello' }, 'some-value', 'another-value']) + end end context 'when the args has sensitive information' do let(:extra) { { sidekiq: { 'class' => 'UnknownWorker', 'args' => ['sensitive string', 1, 2] } } } - it 'filters sensitive arguments before sending' do + it 'filters sensitive arguments before sending and logging' do track_exception - sentry_event = Gitlab::Json.parse(Raven.client.transport.events.last[1]) - expect(sentry_event.dig('extra', 'sidekiq', 'args')).to eq(['[FILTERED]', 1, 2]) expect(Gitlab::ErrorTracking::Logger).to have_received(:error).with( hash_including( @@ -234,28 +288,44 @@ RSpec.describe Gitlab::ErrorTracking do end end - context 'when the error is kind of an `ActiveRecord::StatementInvalid`' do - let(:exception) { ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1') } + context 'when the error is a GRPC error' do + context 'when the GRPC error contains a debug_error_string value' do + let(:exception) { GRPC::DeadlineExceeded.new('unknown cause', {}, '{"hello":1}') } - it 'injects the normalized sql query into extra' do - allow(Raven.client.transport).to receive(:send_event) do |event| - expect(event.extra).to include(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1') + it 'sets the GRPC debug error string in the Sentry event and adds a custom fingerprint' do + track_exception + + expect(sentry_event.dig('extra', 'grpc_debug_error_string')).to eq('{"hello":1}') + expect(sentry_event['fingerprint']).to eq(['GRPC::DeadlineExceeded', '4:unknown cause.']) end + end - track_exception + context 'when the GRPC error does not contain a debug_error_string value' do + let(:exception) { GRPC::DeadlineExceeded.new } + + it 'does not do any processing on the event' do + track_exception + + expect(sentry_event['extra']).not_to include('grpc_debug_error_string') + expect(sentry_event['fingerprint']).to eq(['GRPC::DeadlineExceeded', '4:unknown cause']) + end end end + end - context 'when the `ActiveRecord::StatementInvalid` is wrapped in another exception' do - let(:exception) { RuntimeError.new(cause: ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1')) } + context 'with sentry_processors_before_send enabled' do + before do + stub_feature_flags(sentry_processors_before_send: true) + end - it 'injects the normalized sql query into extra' do - allow(Raven.client.transport).to receive(:send_event) do |event| - expect(event.extra).to include(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1') - end + include_examples 'event processors' + end - track_exception - end + context 'with sentry_processors_before_send disabled' do + before do + stub_feature_flags(sentry_processors_before_send: false) end + + include_examples 'event processors' end end diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb index 83c6b556fc6..5fef14bd2a0 100644 --- a/spec/lib/gitlab/experimentation_spec.rb +++ b/spec/lib/gitlab/experimentation_spec.rb @@ -7,7 +7,6 @@ require 'spec_helper' RSpec.describe Gitlab::Experimentation::EXPERIMENTS do it 'temporarily ensures we know what experiments exist for backwards compatibility' do expected_experiment_keys = [ - :upgrade_link_in_user_menu_a, :invite_members_version_b, :invite_members_empty_group_version_a, :contact_sales_btn_in_app diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 1a3c332a21b..114b3d01952 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -31,6 +31,19 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do end end + let(:overflow_max_bytes) { false } + let(:overflow_max_files) { false } + let(:overflow_max_lines) { false } + + shared_examples 'overflow stuff' do + it 'returns the expected overflow values' do + subject.overflow? + expect(subject.overflow_max_bytes?).to eq(overflow_max_bytes) + expect(subject.overflow_max_files?).to eq(overflow_max_files) + expect(subject.overflow_max_lines?).to eq(overflow_max_lines) + end + end + subject do Gitlab::Git::DiffCollection.new( iterator, @@ -76,12 +89,19 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do end context 'overflow handling' do + subject { super() } + + let(:collapsed_safe_files) { false } + let(:collapsed_safe_lines) { false } + context 'adding few enough files' do let(:file_count) { 3 } context 'and few enough lines' do let(:line_count) { 10 } + it_behaves_like 'overflow stuff' + describe '#overflow?' do subject { super().overflow? } @@ -117,6 +137,11 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do context 'when limiting is disabled' do let(:limits) { false } + let(:overflow_max_bytes) { false } + let(:overflow_max_files) { false } + let(:overflow_max_lines) { false } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -155,6 +180,9 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do context 'and too many lines' do let(:line_count) { 1000 } + let(:overflow_max_lines) { true } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -184,6 +212,11 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do context 'when limiting is disabled' do let(:limits) { false } + let(:overflow_max_bytes) { false } + let(:overflow_max_files) { false } + let(:overflow_max_lines) { false } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -216,10 +249,13 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do context 'adding too many files' do let(:file_count) { 11 } + let(:overflow_max_files) { true } context 'and few enough lines' do let(:line_count) { 1 } + it_behaves_like 'overflow stuff' + describe '#overflow?' do subject { super().overflow? } @@ -248,6 +284,11 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do context 'when limiting is disabled' do let(:limits) { false } + let(:overflow_max_bytes) { false } + let(:overflow_max_files) { false } + let(:overflow_max_lines) { false } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -279,6 +320,10 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do context 'and too many lines' do let(:line_count) { 30 } + let(:overflow_max_lines) { true } + let(:overflow_max_files) { false } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -308,6 +353,11 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do context 'when limiting is disabled' do let(:limits) { false } + let(:overflow_max_bytes) { false } + let(:overflow_max_files) { false } + let(:overflow_max_lines) { false } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -344,6 +394,8 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do context 'and few enough lines' do let(:line_count) { 1 } + it_behaves_like 'overflow stuff' + describe '#overflow?' do subject { super().overflow? } @@ -375,6 +427,9 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do context 'adding too many bytes' do let(:file_count) { 10 } let(:line_length) { 5200 } + let(:overflow_max_bytes) { true } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -404,6 +459,11 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do context 'when limiting is disabled' do let(:limits) { false } + let(:overflow_max_bytes) { false } + let(:overflow_max_files) { false } + let(:overflow_max_lines) { false } + + it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } @@ -437,6 +497,8 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do describe 'empty collection' do subject { Gitlab::Git::DiffCollection.new([]) } + it_behaves_like 'overflow stuff' + describe '#overflow?' do subject { super().overflow? } @@ -555,7 +617,7 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do .and_return({ max_files: 2, max_lines: max_lines }) end - it 'prunes diffs by default even little ones' do + it 'prunes diffs by default even little ones and sets collapsed_safe_files true' do subject.each_with_index do |d, i| if i < 2 expect(d.diff).not_to eq('') @@ -563,6 +625,8 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do expect(d.diff).to eq('') end end + + expect(subject.collapsed_safe_files?).to eq(true) end end @@ -582,7 +646,7 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do .and_return({ max_files: max_files, max_lines: 80 }) end - it 'prunes diffs by default even little ones' do + it 'prunes diffs by default even little ones and sets collapsed_safe_lines true' do subject.each_with_index do |d, i| if i < 2 expect(d.diff).not_to eq('') @@ -590,26 +654,30 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do expect(d.diff).to eq('') end end + + expect(subject.collapsed_safe_lines?).to eq(true) end end context 'when go over safe limits on bytes' do let(:iterator) do [ - fake_diff(1, 45), - fake_diff(1, 45), - fake_diff(1, 20480), - fake_diff(1, 1) + fake_diff(5, 10), + fake_diff(5000, 10), + fake_diff(5, 10), + fake_diff(5, 10) ] end before do + allow(Gitlab::CurrentSettings).to receive(:diff_max_patch_bytes).and_return(1.megabyte) + allow(Gitlab::Git::DiffCollection) .to receive(:default_limits) - .and_return({ max_files: max_files, max_lines: 80 }) + .and_return({ max_files: 4, max_lines: 3000 }) end - it 'prunes diffs by default even little ones' do + it 'prunes diffs by default even little ones and sets collapsed_safe_bytes true' do subject.each_with_index do |d, i| if i < 2 expect(d.diff).not_to eq('') @@ -617,6 +685,8 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do expect(d.diff).to eq('') end end + + expect(subject.collapsed_safe_bytes?).to eq(true) end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index cc1b1ceadcf..1e259c9c153 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -564,6 +564,41 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end end + describe '#search_files_by_regexp' do + let(:ref) { 'master' } + + subject(:result) { mutable_repository.search_files_by_regexp(filter, ref) } + + context 'when sending a valid regexp' do + let(:filter) { 'files\/.*\/.*\.rb' } + + it 'returns matched files' do + expect(result).to contain_exactly('files/links/regex.rb', + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/ruby/version_info.rb') + end + end + + context 'when sending an ivalid regexp' do + let(:filter) { '*.rb' } + + it 'raises error' do + expect { result }.to raise_error(GRPC::InvalidArgument, + /missing argument to repetition operator: `*`/) + end + end + + context "when the ref doesn't exist" do + let(:filter) { 'files\/.*\/.*\.rb' } + let(:ref) { 'non-existing-branch' } + + it 'returns an empty array' do + expect(result).to eq([]) + 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) @@ -1711,14 +1746,15 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do let(:right_branch) { 'test-master' } let(:first_parent_ref) { 'refs/heads/test-master' } let(:target_ref) { 'refs/merge-requests/999/merge' } - let(:allow_conflicts) { false } before do repository.create_branch(right_branch, branch_head) unless repository.ref_exists?(first_parent_ref) end def merge_to_ref - repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message', first_parent_ref, allow_conflicts) + repository.merge_to_ref(user, + source_sha: left_sha, branch: right_branch, target_ref: target_ref, + message: 'Merge message', first_parent_ref: first_parent_ref) end it 'generates a commit in the target_ref' do diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb index f83ccc6cae0..b6ff76c5e1c 100644 --- a/spec/lib/gitlab/git/tag_spec.rb +++ b/spec/lib/gitlab/git/tag_spec.rb @@ -101,4 +101,17 @@ RSpec.describe Gitlab::Git::Tag, :seed_helper do end end end + + describe "#cache_key" do + subject { repository.tags.first } + + it "returns a cache key that changes based on changeable values" do + expect(subject).to receive(:name).and_return("v1.0.0") + expect(subject).to receive(:message).and_return("Initial release") + + digest = Digest::SHA1.hexdigest(["v1.0.0", "Initial release", subject.target, subject.target_commit.sha].join) + + expect(subject.cache_key).to eq("tag:#{digest}") + end + end end diff --git a/spec/lib/gitlab/gitaly_client/blob_service_spec.rb b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb index 037734f1b13..f0ec58f3c2d 100644 --- a/spec/lib/gitlab/gitaly_client/blob_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb @@ -14,14 +14,14 @@ RSpec.describe Gitlab::GitalyClient::BlobService do let(:limit) { 5 } let(:not_in) { %w[branch-a branch-b] } let(:expected_params) do - { revision: revision, limit: limit, not_in_refs: not_in, not_in_all: false } + { revisions: ["master", "--not", "branch-a", "branch-b"], limit: limit } end subject { client.get_new_lfs_pointers(revision, limit, not_in) } it 'sends a get_new_lfs_pointers message' do expect_any_instance_of(Gitaly::BlobService::Stub) - .to receive(:get_new_lfs_pointers) + .to receive(:list_lfs_pointers) .with(gitaly_request_with_params(expected_params), kind_of(Hash)) .and_return([]) @@ -31,12 +31,39 @@ RSpec.describe Gitlab::GitalyClient::BlobService do context 'with not_in = :all' do let(:not_in) { :all } let(:expected_params) do - { revision: revision, limit: limit, not_in_refs: [], not_in_all: true } + { revisions: ["master", "--not", "--all"], limit: limit } end it 'sends the correct message' do expect_any_instance_of(Gitaly::BlobService::Stub) - .to receive(:get_new_lfs_pointers) + .to receive(:list_lfs_pointers) + .with(gitaly_request_with_params(expected_params), kind_of(Hash)) + .and_return([]) + + subject + end + end + + context 'with hook environment' do + let(:git_env) do + { + 'GIT_OBJECT_DIRECTORY_RELATIVE' => '.git/objects', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['/dir/one', '/dir/two'] + } + end + + let(:expected_params) do + expected_repository = repository.gitaly_repository + expected_repository.git_alternate_object_directories = Google::Protobuf::RepeatedField.new(:string) + + { limit: limit, repository: expected_repository } + end + + it 'sends a list_all_lfs_pointers message' do + allow(Gitlab::Git::HookEnv).to receive(:all).with(repository.gl_repository).and_return(git_env) + + expect_any_instance_of(Gitaly::BlobService::Stub) + .to receive(:list_all_lfs_pointers) .with(gitaly_request_with_params(expected_params), kind_of(Hash)) .and_return([]) @@ -46,12 +73,16 @@ RSpec.describe Gitlab::GitalyClient::BlobService do end describe '#get_all_lfs_pointers' do + let(:expected_params) do + { revisions: ["--all"], limit: 0 } + end + subject { client.get_all_lfs_pointers } it 'sends a get_all_lfs_pointers message' do expect_any_instance_of(Gitaly::BlobService::Stub) - .to receive(:get_all_lfs_pointers) - .with(gitaly_request_with_params({}), kind_of(Hash)) + .to receive(:list_lfs_pointers) + .with(gitaly_request_with_params(expected_params), kind_of(Hash)) .and_return([]) subject diff --git a/spec/lib/gitlab/gitaly_client/call_spec.rb b/spec/lib/gitlab/gitaly_client/call_spec.rb index 5c33ac40460..099307fc4e1 100644 --- a/spec/lib/gitlab/gitaly_client/call_spec.rb +++ b/spec/lib/gitlab/gitaly_client/call_spec.rb @@ -24,11 +24,14 @@ RSpec.describe Gitlab::GitalyClient::Call do def expect_call_details_to_match(duration_higher_than: 0) expect(client.list_call_details.size).to eq(1) expect(client.list_call_details.first) - .to match a_hash_including(feature: "#{service}##{rpc}", - duration: a_value > duration_higher_than, - request: an_instance_of(Hash), - rpc: rpc, - backtrace: an_instance_of(Array)) + .to match a_hash_including( + start: a_value > 0, + feature: "#{service}##{rpc}", + duration: a_value > duration_higher_than, + request: an_instance_of(Hash), + rpc: rpc, + backtrace: an_instance_of(Array) + ) end context 'when the response is not an enumerator' do diff --git a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb index 15eebf62a39..9c3bc935acc 100644 --- a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::GitalyClient::ObjectPoolService do subject { described_class.new(object_pool) } before do - subject.create(raw_repository) + subject.create(raw_repository) # rubocop:disable Rails/SaveBang end describe '#create' do @@ -22,7 +22,7 @@ RSpec.describe Gitlab::GitalyClient::ObjectPoolService do context 'when the pool already exists' do it 'returns an error' do expect do - subject.create(raw_repository) + subject.create(raw_repository) # rubocop:disable Rails/SaveBang end.to raise_error(GRPC::FailedPrecondition) end end diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 22707c9a36b..9a17140a1e0 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -88,17 +88,29 @@ RSpec.describe Gitlab::GitalyClient::OperationService do let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } let(:ref) { 'refs/merge-requests/x/merge' } let(:message) { 'validación' } - let(:allow_conflicts) { false } let(:response) { Gitaly::UserMergeToRefResponse.new(commit_id: 'new-commit-id') } - subject { client.user_merge_to_ref(user, source_sha, nil, ref, message, first_parent_ref, allow_conflicts) } + let(:payload) do + { source_sha: source_sha, branch: 'branch', target_ref: ref, + message: message, first_parent_ref: first_parent_ref, allow_conflicts: true } + end it 'sends a user_merge_to_ref message' do - expect_any_instance_of(Gitaly::OperationService::Stub) - .to receive(:user_merge_to_ref).with(kind_of(Gitaly::UserMergeToRefRequest), kind_of(Hash)) - .and_return(response) - - subject + freeze_time do + expect_any_instance_of(Gitaly::OperationService::Stub).to receive(:user_merge_to_ref) do |_, request, options| + expect(options).to be_kind_of(Hash) + expect(request.to_h).to eq( + payload.merge({ + repository: repository.gitaly_repository.to_h, + message: message.dup.force_encoding(Encoding::ASCII_8BIT), + user: Gitlab::Git::User.from_gitlab(user).to_gitaly.to_h, + timestamp: { nanos: 0, seconds: Time.current.to_i } + }) + ) + end.and_return(response) + + client.user_merge_to_ref(user, **payload) + end end end diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index 7a382df1248..26ec194a2e7 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -246,6 +246,21 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do end end + describe '#search_files_by_regexp' do + subject(:result) { client.search_files_by_regexp('master', '.*') } + + before do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:search_files_by_name) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return([double(files: ['file1.txt']), double(files: ['file2.txt'])]) + end + + it 'sends a search_files_by_name message and returns a flatten array' do + expect(result).to contain_exactly('file1.txt', 'file2.txt') + end + end + describe '#disconnect_alternates' do let(:project) { create(:project, :repository) } let(:repository) { project.repository } @@ -255,7 +270,7 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do let(:object_pool_service) { Gitlab::GitalyClient::ObjectPoolService.new(object_pool) } before do - object_pool_service.create(repository) + object_pool_service.create(repository) # rubocop:disable Rails/SaveBang object_pool_service.link_repository(repository) end diff --git a/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb index e42b6d89c30..01d9edf0ba1 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::PullRequestMergedByImporter, :clean_gitlab_redis_cache do let_it_be(:merge_request) { create(:merged_merge_request) } + let(:project) { merge_request.project } let(:merged_at) { Time.new(2017, 1, 1, 12, 00).utc } let(:client_double) { double(user: double(id: 999, login: 'merger', email: 'merger@email.com')) } diff --git a/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb index 290f3f51202..5002e0384f3 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter, :clean using RSpec::Parameterized::TableSyntax let_it_be(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } let(:client_double) { double(user: double(id: 999, login: 'author', email: 'author@email.com')) } let(:submitted_at) { Time.new(2017, 1, 1, 12, 00).utc } diff --git a/spec/lib/gitlab/github_import/milestone_finder_spec.rb b/spec/lib/gitlab/github_import/milestone_finder_spec.rb index 5da45b1897f..fe8652eb5a2 100644 --- a/spec/lib/gitlab/github_import/milestone_finder_spec.rb +++ b/spec/lib/gitlab/github_import/milestone_finder_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::MilestoneFinder, :clean_gitlab_redis_cache do let_it_be(:project) { create(:project) } let_it_be(:milestone) { create(:milestone, project: project) } + let(:finder) { described_class.new(project) } describe '#id_for' do diff --git a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb deleted file mode 100644 index c88506899cd..00000000000 --- a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb +++ /dev/null @@ -1,253 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -# Also see spec/graphql/features/authorization_spec.rb for -# integration tests of AuthorizeFieldService -RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do - def type(type_authorizations = []) - Class.new(Types::BaseObject) do - graphql_name 'TestType' - - authorize type_authorizations - end - end - - def type_with_field(field_type, field_authorizations = [], resolved_value = 'Resolved value', **options) - Class.new(Types::BaseObject) do - graphql_name 'TestTypeWithField' - options.reverse_merge!(null: true) - field :test_field, field_type, - authorize: field_authorizations, - **options - - define_method :test_field do - resolved_value - end - end - end - - def resolve - service.authorized_resolve[type_instance, {}, context] - end - - subject(:service) { described_class.new(field) } - - describe '#authorized_resolve' do - let_it_be(:current_user) { build(:user) } - let_it_be(:presented_object) { 'presented object' } - let_it_be(:query_type) { GraphQL::ObjectType.new } - let_it_be(:schema) { GitlabSchema } - let_it_be(:query) { GraphQL::Query.new(schema, document: nil, context: {}, variables: {}) } - let_it_be(:context) { GraphQL::Query::Context.new(query: query, values: { current_user: current_user }, object: nil) } - - let(:type_class) { type_with_field(custom_type, :read_field, presented_object) } - let(:type_instance) { type_class.authorized_new(presented_object, context) } - let(:field) { type_class.fields['testField'].to_graphql } - - subject(:resolved) { ::Gitlab::Graphql::Lazy.force(resolve) } - - context 'reading the field of a lazy value' do - let(:ability) { :read_field } - let(:presented_object) { lazy_upcase('a') } - let(:type_class) { type_with_field(GraphQL::STRING_TYPE, ability) } - - let(:upcaser) do - Module.new do - def self.upcase(strs) - strs.map(&:upcase) - end - end - end - - def lazy_upcase(str) - ::BatchLoader::GraphQL.for(str).batch do |strs, found| - strs.zip(upcaser.upcase(strs)).each { |s, us| found[s, us] } - end - end - - it 'does not run authorizations until we force the resolved value' do - expect(Ability).not_to receive(:allowed?) - - expect(resolve).to respond_to(:force) - end - - it 'runs authorizations when we force the resolved value' do - spy_ability_check_for(ability, 'A') - - expect(resolved).to eq('Resolved value') - end - - it 'redacts values that fail the permissions check' do - spy_ability_check_for(ability, 'A', passed: false) - - expect(resolved).to be_nil - end - - context 'we batch two calls' do - def resolve(value) - instance = type_class.authorized_new(lazy_upcase(value), context) - service.authorized_resolve[instance, {}, context] - end - - it 'batches resolution, but authorizes each object separately' do - expect(upcaser).to receive(:upcase).once.and_call_original - spy_ability_check_for(:read_field, 'A', passed: true) - spy_ability_check_for(:read_field, 'B', passed: false) - spy_ability_check_for(:read_field, 'C', passed: true) - - a = resolve('a') - b = resolve('b') - c = resolve('c') - - expect(a.force).to be_present - expect(b.force).to be_nil - expect(c.force).to be_present - end - end - end - - shared_examples 'authorizing fields' do - context 'scalar types' do - shared_examples 'checking permissions on the presented object' do - it 'checks the abilities on the object being presented and returns the value' do - expected_permissions.each do |permission| - spy_ability_check_for(permission, presented_object, passed: true) - end - - expect(resolved).to eq('Resolved value') - end - - it 'returns nil if the value was not authorized' do - allow(Ability).to receive(:allowed?).and_return false - - expect(resolved).to be_nil - end - end - - context 'when the field is a built-in scalar type' do - let(:type_class) { type_with_field(GraphQL::STRING_TYPE, :read_field) } - let(:expected_permissions) { [:read_field] } - - it_behaves_like 'checking permissions on the presented object' - end - - context 'when the field is a list of scalar types' do - let(:type_class) { type_with_field([GraphQL::STRING_TYPE], :read_field) } - let(:expected_permissions) { [:read_field] } - - it_behaves_like 'checking permissions on the presented object' - end - - context 'when the field is sub-classed scalar type' do - let(:type_class) { type_with_field(Types::TimeType, :read_field) } - let(:expected_permissions) { [:read_field] } - - it_behaves_like 'checking permissions on the presented object' - end - - context 'when the field is a list of sub-classed scalar types' do - let(:type_class) { type_with_field([Types::TimeType], :read_field) } - let(:expected_permissions) { [:read_field] } - - it_behaves_like 'checking permissions on the presented object' - end - end - - context 'when the field is a connection' do - context 'when it resolves to nil' do - let(:type_class) { type_with_field(Types::QueryType.connection_type, :read_field, nil) } - - it 'does not fail when authorizing' do - expect(resolved).to be_nil - end - end - - context 'when it returns values' do - let(:objects) { [1, 2, 3] } - let(:field_type) { type([:read_object]).connection_type } - let(:type_class) { type_with_field(field_type, [], objects) } - - it 'filters out unauthorized values' do - spy_ability_check_for(:read_object, 1, passed: true) - spy_ability_check_for(:read_object, 2, passed: false) - spy_ability_check_for(:read_object, 3, passed: true) - - expect(resolved.nodes).to eq [1, 3] - end - end - end - - context 'when the field is a specific type' do - let(:custom_type) { type(:read_type) } - let(:object_in_field) { double('presented in field') } - - let(:type_class) { type_with_field(custom_type, :read_field, object_in_field) } - let(:type_instance) { type_class.authorized_new(object_in_field, context) } - - it 'checks both field & type permissions' do - spy_ability_check_for(:read_field, object_in_field, passed: true) - spy_ability_check_for(:read_type, object_in_field, passed: true) - - expect(resolved).to eq(object_in_field) - end - - it 'returns nil if viewing was not allowed' do - spy_ability_check_for(:read_field, object_in_field, passed: false) - spy_ability_check_for(:read_type, object_in_field, passed: true) - - expect(resolved).to be_nil - end - - context 'when the field is not nullable' do - let(:type_class) { type_with_field(custom_type, :read_field, object_in_field, null: false) } - - it 'returns nil when viewing is not allowed' do - spy_ability_check_for(:read_type, object_in_field, passed: false) - - expect(resolved).to be_nil - end - end - - context 'when the field is a list' do - let(:object_1) { double('presented in field 1') } - let(:object_2) { double('presented in field 2') } - let(:presented_types) { [double(object: object_1), double(object: object_2)] } - - let(:type_class) { type_with_field([custom_type], :read_field, presented_types) } - let(:type_instance) { type_class.authorized_new(presented_types, context) } - - it 'checks all permissions' do - allow(Ability).to receive(:allowed?) { true } - - spy_ability_check_for(:read_field, object_1, passed: true) - spy_ability_check_for(:read_type, object_1, passed: true) - spy_ability_check_for(:read_field, object_2, passed: true) - spy_ability_check_for(:read_type, object_2, passed: true) - - expect(resolved).to eq(presented_types) - end - - it 'filters out objects that the user cannot see' do - allow(Ability).to receive(:allowed?) { true } - - spy_ability_check_for(:read_type, object_1, passed: false) - - expect(resolved).to contain_exactly(have_attributes(object: object_2)) - end - end - end - end - - it_behaves_like 'authorizing fields' - end - - private - - def spy_ability_check_for(ability, object, passed: true) - expect(Ability) - .to receive(:allowed?) - .with(current_user, ability, object) - .and_return(passed) - end -end diff --git a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb index c5d7665c3b2..0c548e1ce32 100644 --- a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb +++ b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb @@ -12,7 +12,8 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do authorize :read_the_thing def initialize(user, found_object) - @user, @found_object = user, found_object + @user = user + @found_object = found_object end def find_object @@ -22,6 +23,14 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do def current_user user end + + def context + { current_user: user } + end + + def self.authorization + @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(required_permissions) + end end end @@ -30,11 +39,14 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do subject(:loading_resource) { fake_class.new(user, project) } + before do + # don't allow anything by default + allow(Ability).to receive(:allowed?).and_return(false) + end + context 'when the user is allowed to perform the action' do before do - allow(Ability).to receive(:allowed?).with(user, :read_the_thing, project, scope: :user) do - true - end + allow(Ability).to receive(:allowed?).with(user, :read_the_thing, project).and_return(true) end describe '#authorized_find!' do @@ -48,24 +60,12 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do expect { loading_resource.authorize!(project) }.not_to raise_error end end - - describe '#authorized_resource?' do - it 'is true' do - expect(loading_resource.authorized_resource?(project)).to be(true) - end - end end context 'when the user is not allowed to perform the action' do - before do - allow(Ability).to receive(:allowed?).with(user, :read_the_thing, project, scope: :user) do - false - end - end - describe '#authorized_find!' do it 'raises an error' do - expect { loading_resource.authorize!(project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + expect { loading_resource.authorized_find! }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) end end @@ -74,12 +74,6 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do expect { loading_resource.authorize!(project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) end end - - describe '#authorized_resource?' do - it 'is false' do - expect(loading_resource.authorized_resource?(project)).to be(false) - end - end end context 'when the class does not define #find_object' do @@ -92,46 +86,6 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do end end - context 'when the class does not define authorize' do - let(:fake_class) do - Class.new do - include Gitlab::Graphql::Authorize::AuthorizeResource - - attr_reader :user, :found_object - - def initialize(user, found_object) - @user, @found_object = user, found_object - end - - def find_object(*_args) - found_object - end - - def current_user - user - end - - def self.name - 'TestClass' - end - end - end - - let(:error) { /#{fake_class.name} has no authorizations/ } - - describe '#authorized_find!' do - it 'raises a comprehensive error message' do - expect { loading_resource.authorized_find! }.to raise_error(error) - end - end - - describe '#authorized_resource?' do - it 'raises a comprehensive error message' do - expect { loading_resource.authorized_resource?(project) }.to raise_error(error) - end - end - end - describe '#authorize' do it 'adds permissions from subclasses to those of superclasses when used on classes' do base_class = Class.new do diff --git a/spec/lib/gitlab/graphql/authorize/object_authorization_spec.rb b/spec/lib/gitlab/graphql/authorize/object_authorization_spec.rb new file mode 100644 index 00000000000..73e25f23848 --- /dev/null +++ b/spec/lib/gitlab/graphql/authorize/object_authorization_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe ::Gitlab::Graphql::Authorize::ObjectAuthorization do + describe '#ok?' do + subject { described_class.new(%i[go_fast go_slow]) } + + let(:user) { double(:User, id: 10001) } + + let(:policy) do + Class.new(::DeclarativePolicy::Base) do + condition(:fast, scope: :subject) { @subject.x >= 10 } + condition(:slow, scope: :subject) { @subject.y >= 10 } + + rule { fast }.policy do + enable :go_fast + end + + rule { slow }.policy do + enable :go_slow + end + end + end + + before do + stub_const('Foo', Struct.new(:x, :y)) + stub_const('FooPolicy', policy) + end + + context 'when there are no abilities' do + subject { described_class.new([]) } + + it { is_expected.to be_ok(double, double) } + end + + context 'when no ability should be allowed' do + let(:object) { Foo.new(0, 0) } + + it { is_expected.not_to be_ok(object, user) } + end + + context 'when go_fast should be allowed' do + let(:object) { Foo.new(100, 0) } + + it { is_expected.not_to be_ok(object, user) } + end + + context 'when go_fast and go_slow should be allowed' do + let(:object) { Foo.new(100, 100) } + + it { is_expected.to be_ok(object, user) } + end + + context 'when the object delegates to another subject' do + def proxy(foo) + double(:Proxy, declarative_policy_subject: foo) + end + + it { is_expected.to be_ok(proxy(Foo.new(100, 100)), user) } + it { is_expected.not_to be_ok(proxy(Foo.new(0, 100)), user) } + end + end +end diff --git a/spec/lib/gitlab/graphql/batch_key_spec.rb b/spec/lib/gitlab/graphql/batch_key_spec.rb index 881fba5c1be..7b73b27f24b 100644 --- a/spec/lib/gitlab/graphql/batch_key_spec.rb +++ b/spec/lib/gitlab/graphql/batch_key_spec.rb @@ -6,6 +6,7 @@ require 'test_prof/recipes/rspec/let_it_be' RSpec.describe ::Gitlab::Graphql::BatchKey do let_it_be(:rect) { Struct.new(:len, :width) } let_it_be(:circle) { Struct.new(:radius) } + let(:lookahead) { nil } let(:object) { rect.new(2, 3) } diff --git a/spec/lib/gitlab/graphql/deprecation_spec.rb b/spec/lib/gitlab/graphql/deprecation_spec.rb new file mode 100644 index 00000000000..8b41145b855 --- /dev/null +++ b/spec/lib/gitlab/graphql/deprecation_spec.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'active_model' + +RSpec.describe ::Gitlab::Graphql::Deprecation do + let(:options) { {} } + + subject(:deprecation) { described_class.parse(options) } + + describe '.parse' do + context 'with nil' do + let(:options) { nil } + + it 'parses to nil' do + expect(deprecation).to be_nil + end + end + + context 'with empty options' do + let(:options) { {} } + + it 'parses to an empty deprecation' do + expect(deprecation).to eq(described_class.new) + end + end + + context 'with defined options' do + let(:options) { { reason: :renamed, milestone: '10.10' } } + + it 'assigns the properties' do + expect(deprecation).to eq(described_class.new(reason: 'This was renamed', milestone: '10.10')) + end + end + end + + describe 'validations' do + let(:options) { { reason: :renamed, milestone: '10.10' } } + + it { is_expected.to be_valid } + + context 'when the milestone is absent' do + before do + options.delete(:milestone) + end + + it { is_expected.not_to be_valid } + end + + context 'when the milestone is not milestone-ish' do + before do + options[:milestone] = 'next year' + end + + it { is_expected.not_to be_valid } + end + + context 'when the milestone is not a string' do + before do + options[:milestone] = 10.01 + end + + it { is_expected.not_to be_valid } + end + + context 'when the reason is absent' do + before do + options.delete(:reason) + end + + it { is_expected.not_to be_valid } + end + + context 'when the reason is not a known reason' do + before do + options[:reason] = :not_stylish_enough + end + + it { is_expected.not_to be_valid } + end + + context 'when the reason is a string' do + before do + options[:reason] = 'not stylish enough' + end + + it { is_expected.to be_valid } + end + + context 'when the reason is a string ending with a period' do + before do + options[:reason] = 'not stylish enough.' + end + + it { is_expected.not_to be_valid } + end + end + + describe '#deprecation_reason' do + context 'when there is a replacement' do + let(:options) { { reason: :renamed, milestone: '10.10', replacement: 'X.y' } } + + it 'renders as reason-replacement-milestone' do + expect(deprecation.deprecation_reason).to eq('This was renamed. Please use `X.y`. Deprecated in 10.10.') + end + end + + context 'when there is no replacement' do + let(:options) { { reason: :renamed, milestone: '10.10' } } + + it 'renders as reason-milestone' do + expect(deprecation.deprecation_reason).to eq('This was renamed. Deprecated in 10.10.') + end + end + + describe 'processing of reason' do + described_class::REASONS.each_key do |known_reason| + context "when the reason is a known reason such as #{known_reason.inspect}" do + let(:options) { { reason: known_reason } } + + it 'renders the reason_text correctly' do + expect(deprecation.deprecation_reason).to start_with(described_class::REASONS[known_reason]) + end + end + end + + context 'when the reason is any other string' do + let(:options) { { reason: 'unhelpful' } } + + it 'appends a period' do + expect(deprecation.deprecation_reason).to start_with('unhelpful.') + end + end + end + end + + describe '#edit_description' do + let(:options) { { reason: :renamed, milestone: '10.10' } } + + it 'appends milestone:reason with a leading space if there is a description' do + desc = deprecation.edit_description('Some description.') + + expect(desc).to eq('Some description. Deprecated in 10.10: This was renamed.') + end + + it 'returns nil if there is no description' do + desc = deprecation.edit_description(nil) + + expect(desc).to be_nil + end + end + + describe '#original_description' do + it 'records the description passed to it' do + deprecation.edit_description('Some description.') + + expect(deprecation.original_description).to eq('Some description.') + end + end + + describe '#markdown' do + context 'when there is a replacement' do + let(:options) { { reason: :renamed, milestone: '10.10', replacement: 'X.y' } } + + context 'when the context is :inline' do + it 'renders on one line' do + expectation = '**Deprecated** in 10.10. This was renamed. Use: `X.y`.' + + expect(deprecation.markdown).to eq(expectation) + expect(deprecation.markdown(context: :inline)).to eq(expectation) + end + end + + context 'when the context is :block' do + it 'renders a warning note' do + expectation = <<~MD.chomp + WARNING: + **Deprecated** in 10.10. + This was renamed. + Use: `X.y`. + MD + + expect(deprecation.markdown(context: :block)).to eq(expectation) + end + end + end + + context 'when there is no replacement' do + let(:options) { { reason: 'Removed', milestone: '10.10' } } + + context 'when the context is :inline' do + it 'renders on one line' do + expectation = '**Deprecated** in 10.10. Removed.' + + expect(deprecation.markdown).to eq(expectation) + expect(deprecation.markdown(context: :inline)).to eq(expectation) + end + end + + context 'when the context is :block' do + it 'renders a warning note' do + expectation = <<~MD.chomp + WARNING: + **Deprecated** in 10.10. + Removed. + MD + + expect(deprecation.markdown(context: :block)).to eq(expectation) + end + end + end + end +end diff --git a/spec/lib/gitlab/graphql/docs/renderer_spec.rb b/spec/lib/gitlab/graphql/docs/renderer_spec.rb index 5afed8c3390..8c0f7aac081 100644 --- a/spec/lib/gitlab/graphql/docs/renderer_spec.rb +++ b/spec/lib/gitlab/graphql/docs/renderer_spec.rb @@ -1,32 +1,35 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Graphql::Docs::Renderer do describe '#contents' do - # Returns a Schema that uses the given `type` - def mock_schema(type, field_description) - query_type = Class.new(Types::BaseObject) do - graphql_name 'Query' + let(:template) { Rails.root.join('lib/gitlab/graphql/docs/templates/default.md.haml') } - field :foo, type, null: true do - description field_description + let(:query_type) do + Class.new(Types::BaseObject) { graphql_name 'Query' }.tap do |t| + # this keeps type and field_description in scope. + t.field :foo, type, null: true, description: field_description do argument :id, GraphQL::ID_TYPE, required: false, description: 'ID of the object.' end end + end - GraphQL::Schema.define( - query: query_type, - resolve_type: ->(obj, ctx) { raise 'Not a real schema' } - ) + let(:mock_schema) do + Class.new(GraphQL::Schema) do + def resolve_type(obj, ctx) + raise 'Not a real schema' + end + end end - let_it_be(:template) { Rails.root.join('lib/gitlab/graphql/docs/templates/default.md.haml') } let(:field_description) { 'List of objects.' } subject(:contents) do + mock_schema.query(query_type) + described_class.new( - mock_schema(type, field_description).graphql_definition, + mock_schema, output_dir: nil, template: template ).contents @@ -136,6 +139,22 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do null: false, deprecated: { reason: 'This is deprecated', milestone: '1.10' }, description: 'A description.' + field :foo_with_args, + type: GraphQL::STRING_TYPE, + null: false, + deprecated: { reason: 'Do not use', milestone: '1.10' }, + description: 'A description.' do + argument :fooity, ::GraphQL::INT_TYPE, required: false, description: 'X' + end + field :bar, + type: GraphQL::STRING_TYPE, + null: false, + description: 'A description.', + deprecated: { + reason: :renamed, + milestone: '1.10', + replacement: 'Query.boom' + } end end @@ -145,7 +164,40 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do | Field | Type | Description | | ----- | ---- | ----------- | - | `foo` **{warning-solid}** | [`String!`](#string) | **Deprecated:** This is deprecated. Deprecated in 1.10. | + | `bar` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. This was renamed. Use: `Query.boom`. | + | `foo` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. This is deprecated. | + | `fooWithArgs` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. Do not use. | + DOC + + is_expected.to include(expectation) + end + end + + context 'when a Query.field is deprecated' do + let(:type) { ::GraphQL::INT_TYPE } + + before do + query_type.field( + name: :bar, + type: type, + null: true, + description: 'A bar', + deprecated: { reason: :renamed, milestone: '10.11', replacement: 'Query.foo' } + ) + end + + it 'includes the deprecation' do + expectation = <<~DOC + ### `bar` + + A bar. + + WARNING: + **Deprecated** in 10.11. + This was renamed. + Use: `Query.foo`. + + Returns [`Int`](#int). DOC is_expected.to include(expectation) diff --git a/spec/lib/gitlab/graphql/loaders/batch_lfs_oid_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_lfs_oid_loader_spec.rb index ae5d9686c54..35750a87fb5 100644 --- a/spec/lib/gitlab/graphql/loaders/batch_lfs_oid_loader_spec.rb +++ b/spec/lib/gitlab/graphql/loaders/batch_lfs_oid_loader_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Gitlab::Graphql::Loaders::BatchLfsOidLoader do include GraphqlHelpers let_it_be(:project) { create(:project, :repository) } + let(:repository) { project.repository } let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(repository.blob_at('master', 'files/lfs/lfs_object.iso'), repository) } let(:otherblob) { Gitlab::Graphql::Representation::TreeEntry.new(repository.blob_at('master', 'README'), repository) } diff --git a/spec/lib/gitlab/graphql/markdown_field_spec.rb b/spec/lib/gitlab/graphql/markdown_field_spec.rb index 0e36ea14ac3..44ca23f547c 100644 --- a/spec/lib/gitlab/graphql/markdown_field_spec.rb +++ b/spec/lib/gitlab/graphql/markdown_field_spec.rb @@ -57,6 +57,7 @@ RSpec.describe Gitlab::Graphql::MarkdownField do describe 'basic verification that references work' do let_it_be(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } let(:note) { build(:note, note: "Referencing #{issue.to_reference(full: true)}") } diff --git a/spec/lib/gitlab/graphql/negatable_arguments_spec.rb b/spec/lib/gitlab/graphql/negatable_arguments_spec.rb new file mode 100644 index 00000000000..bc6e25eb018 --- /dev/null +++ b/spec/lib/gitlab/graphql/negatable_arguments_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Graphql::NegatableArguments do + let(:test_resolver) do + Class.new(Resolvers::BaseResolver).tap do |klass| + klass.extend described_class + allow(klass).to receive(:name).and_return('Resolvers::TestResolver') + end + end + + describe '#negated' do + it 'defines :not argument' do + test_resolver.negated {} + + expect(test_resolver.arguments['not'].type.name).to eq "Types::TestResolverNegatedParamsType" + end + + it 'defines any arguments passed as block' do + test_resolver.negated do + argument :foo, GraphQL::STRING_TYPE, required: false + end + + expect(test_resolver.arguments['not'].type.arguments.keys).to match_array(['foo']) + end + + it 'defines all arguments passed as block even if called multiple times' do + test_resolver.negated do + argument :foo, GraphQL::STRING_TYPE, required: false + end + test_resolver.negated do + argument :bar, GraphQL::STRING_TYPE, required: false + end + + expect(test_resolver.arguments['not'].type.arguments.keys).to match_array(%w[foo bar]) + end + + it 'allows to specify custom argument name' do + test_resolver.negated(param_key: :negative) {} + + expect(test_resolver.arguments).to include('negative') + 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 02e67488d3f..839ad9110cc 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb @@ -337,6 +337,7 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do 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 diff --git a/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb index ec2ec4bf50d..792cb03e8c7 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb @@ -4,6 +4,7 @@ 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) } diff --git a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb index 8450396284a..fc723138d88 100644 --- a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb +++ b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb @@ -3,43 +3,46 @@ require 'spec_helper' RSpec.describe Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer do - subject { described_class.new } - - describe '#initial_value' do - it 'filters out sensitive variables' do - doc = GraphQL.parse <<-GRAPHQL - mutation createNote($body: String!) { - createNote(input: {noteableId: "1", body: $body}) { - note { - id - } + let(:initial_value) { analyzer.initial_value(query) } + let(:analyzer) { described_class.new } + let(:query) { GraphQL::Query.new(GitlabSchema, document: document, context: {}, variables: { body: "some note" }) } + let(:document) do + GraphQL.parse <<-GRAPHQL + mutation createNote($body: String!) { + createNote(input: {noteableId: "1", body: $body}) { + note { + id } } - GRAPHQL + } + GRAPHQL + end - query = GraphQL::Query.new(GitlabSchema, document: doc, context: {}, variables: { body: "some note" }) + describe 'variables' do + subject { initial_value.fetch(:variables) } - expect(subject.initial_value(query)[:variables]).to eq('{:body=>"[FILTERED]"}') - end + it { is_expected.to eq('{:body=>"[FILTERED]"}') } end describe '#final_value' do let(:monotonic_time_before) { 42 } let(:monotonic_time_after) { 500 } let(:monotonic_time_duration) { monotonic_time_after - monotonic_time_before } + let(:memo) { initial_value } + + subject(:final_value) { analyzer.final_value(memo) } + + before do + RequestStore.store[:graphql_logs] = nil - it 'returns a duration in seconds' do allow(GraphQL::Analysis).to receive(:analyze_query).and_return([4, 2, [[], []]]) allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after) allow(Gitlab::GraphqlLogger).to receive(:info) + end - expected_duration = monotonic_time_duration - memo = subject.initial_value(spy('query')) - - subject.final_value(memo) - - expect(memo).to have_key(:duration_s) - expect(memo[:duration_s]).to eq(expected_duration) + it 'inserts duration in seconds to memo and sets request store' do + expect { final_value }.to change { memo[:duration_s] }.to(monotonic_time_duration) + .and change { RequestStore.store[:graphql_logs] }.to([memo]) end end end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index 9271b868e36..1a929373716 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -79,6 +79,21 @@ RSpec.describe Gitlab::Highlight do expect(result).to eq(expected) end + + context 'when start line number is set' do + let(:expected) do + %q(<span id="LC10" class="line" lang="diff"><span class="gi">+aaa</span></span> +<span id="LC11" class="line" lang="diff"><span class="gi">+bbb</span></span> +<span id="LC12" class="line" lang="diff"><span class="gd">- ccc</span></span> +<span id="LC13" class="line" lang="diff"> ddd</span>) + end + + it 'highlights each line properly' do + result = described_class.new(file_name, content).highlight(content, context: { line_number: 10 }) + + expect(result).to eq(expected) + end + end end describe 'with CRLF' do diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb index 8a2395d70b2..8f898d898de 100644 --- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::HookData::IssueBuilder do let_it_be(:label) { create(:label) } let_it_be(:issue) { create(:labeled_issue, labels: [label], project: label.project) } + let(:builder) { described_class.new(issue) } describe '#build' do diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index fede7f273f1..0339faa9fcf 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::HookData::MergeRequestBuilder do let_it_be(:merge_request) { create(:merge_request) } + let(:builder) { described_class.new(merge_request) } describe '#build' do diff --git a/spec/lib/gitlab/hook_data/release_builder_spec.rb b/spec/lib/gitlab/hook_data/release_builder_spec.rb index b630780b162..449965f5df1 100644 --- a/spec/lib/gitlab/hook_data/release_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/release_builder_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::HookData::ReleaseBuilder do let_it_be(:project) { create(:project, :public, :repository) } + let(:release) { create(:release, project: project) } let(:builder) { described_class.new(release) } diff --git a/spec/lib/gitlab/hook_data/user_builder_spec.rb b/spec/lib/gitlab/hook_data/user_builder_spec.rb new file mode 100644 index 00000000000..f971089850b --- /dev/null +++ b/spec/lib/gitlab/hook_data/user_builder_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::HookData::UserBuilder do + let_it_be(:user) { create(:user, name: 'John Doe', username: 'johndoe', email: 'john@example.com') } + + describe '#build' do + let(:data) { described_class.new(user).build(event) } + let(:event_name) { data[:event_name] } + let(:attributes) do + [ + :event_name, :created_at, :updated_at, :name, :email, :user_id, :username + ] + end + + context 'data' do + shared_examples_for 'includes the required attributes' do + it 'includes the required attributes' do + expect(data).to include(*attributes) + + expect(data[:name]).to eq('John Doe') + expect(data[:email]).to eq('john@example.com') + expect(data[:user_id]).to eq(user.id) + expect(data[:username]).to eq('johndoe') + expect(data[:created_at]).to eq(user.created_at.xmlschema) + expect(data[:updated_at]).to eq(user.updated_at.xmlschema) + end + end + + shared_examples_for 'does not include old username attributes' do + it 'does not include old username attributes' do + expect(data).not_to include(:old_username) + end + end + + shared_examples_for 'does not include state attributes' do + it 'does not include state attributes' do + expect(data).not_to include(:state) + end + end + + context 'on create' do + let(:event) { :create } + + it { expect(event_name).to eq('user_create') } + it_behaves_like 'includes the required attributes' + it_behaves_like 'does not include old username attributes' + it_behaves_like 'does not include state attributes' + end + + context 'on destroy' do + let(:event) { :destroy } + + it { expect(event_name).to eq('user_destroy') } + it_behaves_like 'includes the required attributes' + it_behaves_like 'does not include old username attributes' + it_behaves_like 'does not include state attributes' + end + + context 'on rename' do + let(:event) { :rename } + + it { expect(event_name).to eq('user_rename') } + it_behaves_like 'includes the required attributes' + it_behaves_like 'does not include state attributes' + + it 'includes old username details' do + allow(user).to receive(:username_before_last_save).and_return('old-username') + + expect(data[:old_username]).to eq(user.username_before_last_save) + end + end + + context 'on failed_login' do + let(:event) { :failed_login } + + it { expect(event_name).to eq('user_failed_login') } + it_behaves_like 'includes the required attributes' + it_behaves_like 'does not include old username attributes' + + it 'includes state details' do + user.ldap_block! + + expect(data[:state]).to eq('ldap_blocked') + end + end + end + end +end diff --git a/spec/lib/gitlab/http_connection_adapter_spec.rb b/spec/lib/gitlab/http_connection_adapter_spec.rb index 96e6e485841..7c57d162e9b 100644 --- a/spec/lib/gitlab/http_connection_adapter_spec.rb +++ b/spec/lib/gitlab/http_connection_adapter_spec.rb @@ -124,130 +124,5 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do expect(connection.port).to eq(443) end end - - context 'when proxy settings are configured' do - let(:options) do - { - http_proxyaddr: 'https://proxy.org', - http_proxyport: 1557, - http_proxyuser: 'user', - http_proxypass: 'pass' - } - end - - before do - stub_all_dns('https://proxy.org', ip_address: '166.84.12.54') - end - - it 'sets up the proxy settings' do - expect(connection.proxy_address).to eq('https://166.84.12.54') - expect(connection.proxy_port).to eq(1557) - expect(connection.proxy_user).to eq('user') - expect(connection.proxy_pass).to eq('pass') - end - - context 'when the address has path' do - before do - options[:http_proxyaddr] = 'https://proxy.org/path' - end - - it 'sets up the proxy settings' do - expect(connection.proxy_address).to eq('https://166.84.12.54/path') - expect(connection.proxy_port).to eq(1557) - end - end - - context 'when the port is in the address and port' do - before do - options[:http_proxyaddr] = 'https://proxy.org:1422' - end - - it 'sets up the proxy settings' do - expect(connection.proxy_address).to eq('https://166.84.12.54') - expect(connection.proxy_port).to eq(1557) - end - - context 'when the port is only in the address' do - before do - options[:http_proxyport] = nil - end - - it 'sets up the proxy settings' do - expect(connection.proxy_address).to eq('https://166.84.12.54') - expect(connection.proxy_port).to eq(1422) - end - end - end - - context 'when it is a request to local network' do - before do - options[:http_proxyaddr] = 'http://172.16.0.0/12' - end - - it 'raises error' do - expect { subject }.to raise_error( - Gitlab::HTTP::BlockedUrlError, - "URL 'http://172.16.0.0:1557/12' is blocked: Requests to the local network are not allowed" - ) - end - - context 'when local request allowed' do - before do - options[:allow_local_requests] = true - end - - it 'sets up the connection' do - expect(connection.proxy_address).to eq('http://172.16.0.0/12') - expect(connection.proxy_port).to eq(1557) - end - end - end - - context 'when it is a request to local address' do - before do - options[:http_proxyaddr] = 'http://127.0.0.1' - end - - it 'raises error' do - expect { subject }.to raise_error( - Gitlab::HTTP::BlockedUrlError, - "URL 'http://127.0.0.1:1557' is blocked: Requests to localhost are not allowed" - ) - end - - context 'when local request allowed' do - before do - options[:allow_local_requests] = true - end - - it 'sets up the connection' do - expect(connection.proxy_address).to eq('http://127.0.0.1') - expect(connection.proxy_port).to eq(1557) - end - end - end - - context 'when http(s) environment variable is set' do - before do - stub_env('https_proxy' => 'https://my.proxy') - end - - it 'sets up the connection' do - expect(connection.proxy_address).to eq('https://proxy.org') - expect(connection.proxy_port).to eq(1557) - end - end - - context 'when DNS rebinding protection is disabled' do - before do - stub_application_setting(dns_rebinding_protection_enabled: false) - end - - it 'sets up the connection' do - expect(connection.proxy_address).to eq('https://proxy.org') - expect(connection.proxy_port).to eq(1557) - end - end - end end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 37b43066a62..5d1e3c79474 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -256,6 +256,8 @@ ci_pipelines: - messages - pipeline_artifacts - latest_statuses +- dast_profile +- dast_profiles_pipeline ci_refs: - project - ci_pipelines @@ -269,6 +271,7 @@ stages: - builds - bridges - latest_statuses +- retried_statuses statuses: - project - pipeline @@ -740,3 +743,5 @@ status_page_published_incident: - issue issuable_sla: - issue +push_rule: + - group diff --git a/spec/lib/gitlab/import_export/design_repo_saver_spec.rb b/spec/lib/gitlab/import_export/design_repo_saver_spec.rb index 5501e3dee5a..fd3539ab99c 100644 --- a/spec/lib/gitlab/import_export/design_repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/design_repo_saver_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Gitlab::ImportExport::DesignRepoSaver do describe 'bundle a design Git repo' do let_it_be(:user) { create(:user) } let_it_be(:design) { create(:design, :with_file, versions_count: 1) } + let!(:project) { create(:project, :design_repo) } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { project.import_export_shared } diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb index d084b9d7f7e..29b192de809 100644 --- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb +++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb @@ -12,6 +12,7 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do let_it_be(:user) { create(:user) } let_it_be(:project) { setup_project } + let(:shared) { project.import_export_shared } let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) } let(:tree) { reader.project_tree } diff --git a/spec/lib/gitlab/import_export/project/export_task_spec.rb b/spec/lib/gitlab/import_export/project/export_task_spec.rb index 1048379a5d6..7fcd2187a90 100644 --- a/spec/lib/gitlab/import_export/project/export_task_spec.rb +++ b/spec/lib/gitlab/import_export/project/export_task_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Gitlab::ImportExport::Project::ExportTask do let_it_be(:username) { 'root' } let(:namespace_path) { username } let_it_be(:user) { create(:user, username: username) } + let(:measurement_enabled) { false } let(:file_path) { 'spec/fixtures/gitlab/import_export/test_project_export.tar.gz' } let(:project) { create(:project, creator: user, namespace: user.namespace) } 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 e2bf87bf29f..bc5e6ea7bb3 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -684,7 +684,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do it 'overrides project feature access levels' do access_level_keys = ProjectFeature.available_features.map { |feature| ProjectFeature.access_level_attribute(feature) } - disabled_access_levels = Hash[access_level_keys.collect { |item| [item, 'disabled'] }] + disabled_access_levels = access_level_keys.to_h { |item| [item, 'disabled'] } project.create_import_data(data: { override_params: disabled_access_levels }) diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb index 50494433c5d..fd6c66a10a7 100644 --- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb @@ -267,6 +267,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do describe '#saves project tree' do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } + let(:project) { setup_project } let(:full_path) do if ndjson_enabled diff --git a/spec/lib/gitlab/import_export/repo_saver_spec.rb b/spec/lib/gitlab/import_export/repo_saver_spec.rb index 52001e778d6..73e0e0a08b9 100644 --- a/spec/lib/gitlab/import_export/repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/repo_saver_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::ImportExport::RepoSaver do describe 'bundle a project Git repo' do let_it_be(:user) { create(:user) } + let!(:project) { create(:project, :repository) } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { project.import_export_shared } diff --git a/spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb b/spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb index 323ed9a746e..9f3e8d2fa86 100644 --- a/spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoSaver do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, namespace: user.namespace) } let_it_be(:snippet) { create(:project_snippet, :repository, project: project, author: user) } + let(:shared) { project.import_export_shared } let(:bundler) { described_class.new(project: project, shared: shared, repository: snippet.repository) } let(:bundle_path) { ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path) } diff --git a/spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb b/spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb index 8507c46ec83..aa284c60e73 100644 --- a/spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::ImportExport::SnippetsRepoSaver do describe 'bundle a project Git repo' do let_it_be(:user) { create(:user) } + let!(:project) { create(:project) } let(:shared) { project.import_export_shared } let(:bundler) { described_class.new(current_user: user, project: project, shared: shared) } diff --git a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb index 540f90e7804..c936d2bc27d 100644 --- a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Gitlab::ImportExport::WikiRepoSaver do describe 'bundle a wiki Git repo' do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :wiki_repo) } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { project.import_export_shared } let(:wiki_bundler) { described_class.new(exportable: project, shared: shared) } diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index a5c9cde4c37..488324ccddc 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -6,53 +6,6 @@ require 'rspec-parameterized' RSpec.describe Gitlab::InstrumentationHelper do using RSpec::Parameterized::TableSyntax - describe '.keys' do - it 'returns all available payload keys' do - expected_keys = [ - :cpu_s, - :gitaly_calls, - :gitaly_duration_s, - :rugged_calls, - :rugged_duration_s, - :elasticsearch_calls, - :elasticsearch_duration_s, - :elasticsearch_timed_out_count, - :mem_objects, - :mem_bytes, - :mem_mallocs, - :redis_calls, - :redis_duration_s, - :redis_read_bytes, - :redis_write_bytes, - :redis_action_cable_calls, - :redis_action_cable_duration_s, - :redis_action_cable_read_bytes, - :redis_action_cable_write_bytes, - :redis_cache_calls, - :redis_cache_duration_s, - :redis_cache_read_bytes, - :redis_cache_write_bytes, - :redis_queues_calls, - :redis_queues_duration_s, - :redis_queues_read_bytes, - :redis_queues_write_bytes, - :redis_shared_state_calls, - :redis_shared_state_duration_s, - :redis_shared_state_read_bytes, - :redis_shared_state_write_bytes, - :db_count, - :db_write_count, - :db_cached_count, - :external_http_count, - :external_http_duration_s, - :rack_attack_redis_count, - :rack_attack_redis_duration_s - ] - - expect(described_class.keys).to eq(expected_keys) - end - end - describe '.add_instrumentation_data', :request_store do let(:payload) { {} } diff --git a/spec/lib/gitlab/jira_import/base_importer_spec.rb b/spec/lib/gitlab/jira_import/base_importer_spec.rb index 1470bad2c4c..9d8143775f9 100644 --- a/spec/lib/gitlab/jira_import/base_importer_spec.rb +++ b/spec/lib/gitlab/jira_import/base_importer_spec.rb @@ -27,6 +27,7 @@ RSpec.describe Gitlab::JiraImport::BaseImporter do context 'when import data exists' do let_it_be(:project) { create(:project) } let_it_be(:jira_import) { create(:jira_import_state, project: project) } + let(:subject) { described_class.new(project) } context 'when #imported_items_cache_key is not implemented' do diff --git a/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb b/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb index 4e2c5afb077..b8c0dc64581 100644 --- a/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb +++ b/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb @@ -10,6 +10,7 @@ RSpec.describe Gitlab::JiraImport::HandleLabelsService do let_it_be(:project_label) { create(:label, project: project, title: 'bug') } let_it_be(:other_project_label) { create(:label, title: 'feature') } let_it_be(:group_label) { create(:group_label, group: group, title: 'dev') } + let(:jira_labels) { %w(bug feature dev group::new) } subject { described_class.new(project, jira_labels).execute } diff --git a/spec/lib/gitlab/jira_import_spec.rb b/spec/lib/gitlab/jira_import_spec.rb index 2b602c80640..94fdff984d5 100644 --- a/spec/lib/gitlab/jira_import_spec.rb +++ b/spec/lib/gitlab/jira_import_spec.rb @@ -9,6 +9,7 @@ RSpec.describe Gitlab::JiraImport do include JiraServiceHelper let_it_be(:project, reload: true) { create(:project) } + let(:additional_params) { {} } subject { described_class.validate_project_settings!(project, **additional_params) } diff --git a/spec/lib/gitlab/json_spec.rb b/spec/lib/gitlab/json_spec.rb index 59ec94f2855..42c4b315edf 100644 --- a/spec/lib/gitlab/json_spec.rb +++ b/spec/lib/gitlab/json_spec.rb @@ -348,6 +348,66 @@ RSpec.describe Gitlab::Json do subject end end + + context "precompiled JSON" do + let(:obj) { Gitlab::Json::PrecompiledJson.new(result) } + + it "renders the string directly" do + expect(subject).to eq(result) + end + + it "calls #to_s on the object" do + expect(obj).to receive(:to_s).once + + subject + end + + it "doesn't run the JSON formatter" do + expect(Gitlab::Json).not_to receive(:dump) + + subject + end + end + end + + describe Gitlab::Json::PrecompiledJson do + subject(:precompiled) { described_class.new(obj) } + + describe "#to_s" do + subject { precompiled.to_s } + + context "obj is a string" do + let(:obj) { "{}" } + + it "returns a string" do + expect(subject).to eq("{}") + end + end + + context "obj is an array" do + let(:obj) { ["{\"foo\": \"bar\"}", "{}"] } + + it "returns a string" do + expect(subject).to eq("[{\"foo\": \"bar\"},{}]") + end + end + + context "obj is an array of un-stringables" do + let(:obj) { [BasicObject.new] } + + it "raises an error" do + expect { subject }.to raise_error(NoMethodError) + end + end + + context "obj is something else" do + let(:obj) { {} } + + it "raises an error" do + expect { subject }.to raise_error(described_class::UnsupportedFormatError) + end + end + end end describe Gitlab::Json::LimitedEncoder do diff --git a/spec/lib/gitlab/legacy_github_import/importer_spec.rb b/spec/lib/gitlab/legacy_github_import/importer_spec.rb index 56074147854..9a4d7bd996e 100644 --- a/spec/lib/gitlab/legacy_github_import/importer_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/importer_spec.rb @@ -290,7 +290,7 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do subject { described_class.new(project) } before do - project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git") + project.update!(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git") end it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute' do diff --git a/spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb index 4b1e0d2c144..454bab8846c 100644 --- a/spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb @@ -152,7 +152,7 @@ RSpec.describe Gitlab::LegacyGithubImport::IssueFormatter do context 'when importing a Gitea project' do before do - project.update(import_type: 'gitea') + project.update!(import_type: 'gitea') end it_behaves_like 'Gitlab::LegacyGithubImport::IssueFormatter#attributes' diff --git a/spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb index 148b59dedab..64fcc46d304 100644 --- a/spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb @@ -92,7 +92,7 @@ RSpec.describe Gitlab::LegacyGithubImport::MilestoneFormatter do let(:iid_attr) { :id } before do - project.update(import_type: 'gitea') + project.update!(import_type: 'gitea') end it_behaves_like 'Gitlab::LegacyGithubImport::MilestoneFormatter#attributes' diff --git a/spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb index 3e6b9340d0b..7d8875e36c3 100644 --- a/spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb @@ -260,7 +260,7 @@ RSpec.describe Gitlab::LegacyGithubImport::PullRequestFormatter do context 'when importing a Gitea project' do before do - project.update(import_type: 'gitea') + project.update!(import_type: 'gitea') end it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#attributes' 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 be562d916d3..23dbd4a5bb3 100644 --- a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb +++ b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do end let(:cache_version) { Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 } - let(:thing) { klass.create(title: markdown, title_html: html, cached_markdown_version: cache_version) } + let(:thing) { klass.create!(title: markdown, title_html: html, cached_markdown_version: cache_version) } let(:markdown) { '`Foo`' } let(:html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Foo</code></p>' } @@ -28,7 +28,7 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do before do thing.title = thing.title - thing.save + thing.save! end it { expect(thing.title).to eq(markdown) } @@ -38,11 +38,11 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do end context 'a changed markdown field' do - let(:thing) { klass.create(title: markdown, title_html: html, cached_markdown_version: cache_version) } + let(:thing) { klass.create!(title: markdown, title_html: html, cached_markdown_version: cache_version) } before do thing.title = updated_markdown - thing.save + thing.save! end it { expect(thing.title_html).to eq(updated_html) } @@ -53,9 +53,9 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do it do expect(thing).to receive(:refresh_markdown_cache).once thing.title = '' - thing.save + thing.save! thing.title = '' - thing.save + thing.save! end end @@ -63,9 +63,9 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do it do expect(thing).to receive(:refresh_markdown_cache).once thing.title = '[//]: # (This is also a comment.)' - thing.save + thing.save! thing.title = '[//]: # (This is also a comment.)' - thing.save + thing.save! end end @@ -74,7 +74,7 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do before do thing.state_id = 2 - thing.save + thing.save! end it { expect(thing.state_id).to eq(2) } @@ -87,7 +87,7 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do let(:thing) { klass.new(title: updated_markdown, title_html: html, cached_markdown_version: nil) } before do - thing.save + thing.save! end it { expect(thing.title_html).to eq(updated_html) } @@ -99,7 +99,7 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do thing.project = :new_project allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html) - thing.save + thing.save! expect(thing.title_html).to eq(updated_html) expect(thing.description_html).to eq(updated_html) @@ -110,7 +110,7 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do thing.author = :new_author allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html) - thing.save + thing.save! expect(thing.title_html).to eq(updated_html) expect(thing.description_html).to eq(updated_html) @@ -125,7 +125,7 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do end describe '#cached_html_up_to_date?' do - let(:thing) { klass.create(title: updated_markdown, title_html: html, cached_markdown_version: nil) } + let(:thing) { klass.create!(title: updated_markdown, title_html: html, cached_markdown_version: nil) } subject { thing.cached_html_up_to_date?(:title) } diff --git a/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb index 3dcb9f160ba..b5d458f15fc 100644 --- a/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb +++ b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb @@ -7,7 +7,8 @@ RSpec.describe Gitlab::MarkdownCache::Redis::Extension, :clean_gitlab_redis_cach include CacheMarkdownField def initialize(title: nil, description: nil) - @title, @description = title, description + @title = title + @description = description end attr_reader :title, :description diff --git a/spec/lib/gitlab/markdown_cache/redis/store_spec.rb b/spec/lib/gitlab/markdown_cache/redis/store_spec.rb index bf40af8e62e..07a87b245c2 100644 --- a/spec/lib/gitlab/markdown_cache/redis/store_spec.rb +++ b/spec/lib/gitlab/markdown_cache/redis/store_spec.rb @@ -40,7 +40,7 @@ RSpec.describe Gitlab::MarkdownCache::Redis::Store, :clean_gitlab_redis_cache do describe '.bulk_read' do before do - store.save(field_1_html: "hello", field_2_html: "world", cached_markdown_version: 1) + store.save(field_1_html: "hello", field_2_html: "world", cached_markdown_version: 1) # rubocop:disable Rails/SaveBang end it 'returns a hash of values from store' do @@ -59,7 +59,7 @@ RSpec.describe Gitlab::MarkdownCache::Redis::Store, :clean_gitlab_redis_cache do it 'stores updates to html fields and version' do values_to_store = { field_1_html: "hello", field_2_html: "world", cached_markdown_version: 1 } - store.save(values_to_store) + store.save(values_to_store) # rubocop:disable Rails/SaveBang expect(read_values) .to eq(field_1_html: "hello", field_2_html: "world", cached_markdown_version: "1") diff --git a/spec/lib/gitlab/marker_range_spec.rb b/spec/lib/gitlab/marker_range_spec.rb index 5f73d2a5048..c4670ec58a8 100644 --- a/spec/lib/gitlab/marker_range_spec.rb +++ b/spec/lib/gitlab/marker_range_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::MarkerRange do let(:last) { 10 } let(:mode) { nil } - it { is_expected.to eq(first..last) } + it { expect(marker_range.to_range).to eq(first..last) } it 'behaves like a Range' do is_expected.to be_kind_of(Range) @@ -51,14 +51,14 @@ RSpec.describe Gitlab::MarkerRange do end it 'keeps correct range' do - is_expected.to eq(range) + is_expected.to eq(described_class.new(1, 3)) end context 'when range excludes end' do let(:range) { 1...3 } it 'keeps correct range' do - is_expected.to eq(range) + is_expected.to eq(described_class.new(1, 3, exclude_end: true)) end end @@ -68,4 +68,31 @@ RSpec.describe Gitlab::MarkerRange do it { is_expected.to be(marker_range) } end end + + describe '#==' do + subject { default_marker_range == another_marker_range } + + let(:default_marker_range) { described_class.new(0, 1, mode: :addition) } + let(:another_marker_range) { default_marker_range } + + it { is_expected.to be_truthy } + + context 'when marker ranges have different modes' do + let(:another_marker_range) { described_class.new(0, 1, mode: :deletion) } + + it { is_expected.to be_falsey } + end + + context 'when marker ranges have different ranges' do + let(:another_marker_range) { described_class.new(0, 2, mode: :addition) } + + it { is_expected.to be_falsey } + end + + context 'when marker ranges is a simple range' do + let(:another_marker_range) { (0..1) } + + it { is_expected.to be_falsey } + end + end end diff --git a/spec/lib/gitlab/metrics/background_transaction_spec.rb b/spec/lib/gitlab/metrics/background_transaction_spec.rb index b31a2f7549a..d36ee24fc50 100644 --- a/spec/lib/gitlab/metrics/background_transaction_spec.rb +++ b/spec/lib/gitlab/metrics/background_transaction_spec.rb @@ -29,19 +29,62 @@ RSpec.describe Gitlab::Metrics::BackgroundTransaction do end describe '#labels' do - it 'provides labels with endpoint_id and feature_category' do - Labkit::Context.with_context(feature_category: 'projects', caller_id: 'TestWorker') do - expect(transaction.labels).to eq({ endpoint_id: 'TestWorker', feature_category: 'projects' }) + context 'when the worker queue is accessible' do + before do + test_worker_class = Class.new do + def self.queue + 'test_worker' + end + end + stub_const('TestWorker', test_worker_class) + end + + it 'provides labels with endpoint_id, feature_category and queue' do + Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: 'TestWorker') do + expect(transaction.labels).to eq({ endpoint_id: 'TestWorker', feature_category: 'projects', queue: 'test_worker' }) + end + end + end + + context 'when the worker name does not exist' do + it 'provides labels with endpoint_id and feature_category' do + # 123TestWorker is an invalid constant + Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: '123TestWorker') do + expect(transaction.labels).to eq({ endpoint_id: '123TestWorker', feature_category: 'projects', queue: nil }) + end + end + end + + context 'when the worker queue is not accessible' do + before do + stub_const('TestWorker', Class.new) + end + + it 'provides labels with endpoint_id and feature_category' do + Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: 'TestWorker') do + expect(transaction.labels).to eq({ endpoint_id: 'TestWorker', feature_category: 'projects', queue: nil }) + end end end end RSpec.shared_examples 'metric with labels' do |metric_method| + before do + test_worker_class = Class.new do + def self.queue + 'test_worker' + end + end + stub_const('TestWorker', test_worker_class) + end + it 'measures with correct labels and value' do value = 1 - expect(prometheus_metric).to receive(metric_method).with({ endpoint_id: 'TestWorker', feature_category: 'projects' }, value) + expect(prometheus_metric).to receive(metric_method).with({ + endpoint_id: 'TestWorker', feature_category: 'projects', queue: 'test_worker' + }, value) - Labkit::Context.with_context(feature_category: 'projects', caller_id: 'TestWorker') do + Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: 'TestWorker') do transaction.send(metric_method, :test_metric, value) end end diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index dffd37eeb9d..6bfcfa21289 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -8,65 +8,146 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do let(:env) { {} } let(:subscriber) { described_class.new } let(:connection) { double(:connection) } - let(:payload) { { sql: 'SELECT * FROM users WHERE id = 10', connection: connection } } - - let(:event) do - double( - :event, - name: 'sql.active_record', - duration: 2, - payload: payload - ) - end - # Emulate Marginalia pre-pending comments - def sql(query, comments: true) - if comments && !%w[BEGIN COMMIT].include?(query) - "/*application:web,controller:badges,action:pipeline,correlation_id:01EYN39K9VMJC56Z7808N7RSRH*/ #{query}" - else - query + describe '#transaction' do + let(:web_transaction) { double('Gitlab::Metrics::WebTransaction') } + let(:background_transaction) { double('Gitlab::Metrics::WebTransaction') } + + let(:event) do + double( + :event, + name: 'transaction.active_record', + duration: 230, + payload: { connection: connection } + ) end - end - shared_examples 'track generic sql events' do - where(:name, :sql_query, :record_query, :record_write_query, :record_cached_query) do - 'SQL' | 'SELECT * FROM users WHERE id = 10' | true | false | false - 'SQL' | 'WITH active_milestones AS (SELECT COUNT(*), state FROM milestones GROUP BY state) SELECT * FROM active_milestones' | true | false | false - 'SQL' | 'SELECT * FROM users WHERE id = 10 FOR UPDATE' | true | true | false - 'SQL' | 'WITH archived_rows AS (SELECT * FROM users WHERE archived = true) INSERT INTO products_log SELECT * FROM archived_rows' | true | true | false - 'SQL' | 'DELETE FROM users where id = 10' | true | true | false - 'SQL' | 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' | true | true | false - 'SQL' | 'UPDATE users SET admin = true WHERE id = 10' | true | true | false - 'CACHE' | 'SELECT * FROM users WHERE id = 10' | true | false | true - 'SCHEMA' | "SELECT attr.attname FROM pg_attribute attr INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '\"projects\"'::regclass" | false | false | false - nil | 'BEGIN' | false | false | false - nil | 'COMMIT' | false | false | false + before do + allow(background_transaction).to receive(:observe) + allow(web_transaction).to receive(:observe) end - with_them do - let(:payload) { { name: name, sql: sql(sql_query, comments: comments), connection: connection } } + context 'when both web and background transaction are available' do + before do + allow(::Gitlab::Metrics::WebTransaction).to receive(:current) + .and_return(web_transaction) + allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current) + .and_return(background_transaction) + end + + it 'captures the metrics for web only' do + expect(web_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23) - it 'marks the current thread as using the database' do - # since it would already have been toggled by other specs - Thread.current[:uses_db_connection] = nil + expect(background_transaction).not_to receive(:observe) + expect(background_transaction).not_to receive(:increment) - expect { subscriber.sql(event) }.to change { Thread.current[:uses_db_connection] }.from(nil).to(true) + subscriber.transaction(event) end + end + + context 'when web transaction is available' do + let(:web_transaction) { double('Gitlab::Metrics::WebTransaction') } + + before do + allow(::Gitlab::Metrics::WebTransaction).to receive(:current) + .and_return(web_transaction) + allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current) + .and_return(nil) + end + + it 'captures the metrics for web only' do + expect(web_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23) - it_behaves_like 'record ActiveRecord metrics' - it_behaves_like 'store ActiveRecord info in RequestStore' + expect(background_transaction).not_to receive(:observe) + expect(background_transaction).not_to receive(:increment) + + subscriber.transaction(event) + end end - end - context 'without Marginalia comments' do - let(:comments) { false } + context 'when background transaction is available' do + let(:background_transaction) { double('Gitlab::Metrics::BackgroundTransaction') } + + before do + allow(::Gitlab::Metrics::WebTransaction).to receive(:current) + .and_return(nil) + allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current) + .and_return(background_transaction) + end - it_behaves_like 'track generic sql events' + it 'captures the metrics for web only' do + expect(background_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23) + + expect(web_transaction).not_to receive(:observe) + expect(web_transaction).not_to receive(:increment) + + subscriber.transaction(event) + end + end end - context 'with Marginalia comments' do - let(:comments) { true } + describe '#sql' do + let(:payload) { { sql: 'SELECT * FROM users WHERE id = 10', connection: connection } } - it_behaves_like 'track generic sql events' + let(:event) do + double( + :event, + name: 'sql.active_record', + duration: 2, + payload: payload + ) + end + + # Emulate Marginalia pre-pending comments + def sql(query, comments: true) + if comments && !%w[BEGIN COMMIT].include?(query) + "/*application:web,controller:badges,action:pipeline,correlation_id:01EYN39K9VMJC56Z7808N7RSRH*/ #{query}" + else + query + end + end + + shared_examples 'track generic sql events' do + where(:name, :sql_query, :record_query, :record_write_query, :record_cached_query) do + 'SQL' | 'SELECT * FROM users WHERE id = 10' | true | false | false + 'SQL' | 'WITH active_milestones AS (SELECT COUNT(*), state FROM milestones GROUP BY state) SELECT * FROM active_milestones' | true | false | false + 'SQL' | 'SELECT * FROM users WHERE id = 10 FOR UPDATE' | true | true | false + 'SQL' | 'WITH archived_rows AS (SELECT * FROM users WHERE archived = true) INSERT INTO products_log SELECT * FROM archived_rows' | true | true | false + 'SQL' | 'DELETE FROM users where id = 10' | true | true | false + 'SQL' | 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' | true | true | false + 'SQL' | 'UPDATE users SET admin = true WHERE id = 10' | true | true | false + 'CACHE' | 'SELECT * FROM users WHERE id = 10' | true | false | true + 'SCHEMA' | "SELECT attr.attname FROM pg_attribute attr INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '\"projects\"'::regclass" | false | false | false + nil | 'BEGIN' | false | false | false + nil | 'COMMIT' | false | false | false + end + + with_them do + let(:payload) { { name: name, sql: sql(sql_query, comments: comments), connection: connection } } + let(:record_wal_query) { false } + + it 'marks the current thread as using the database' do + # since it would already have been toggled by other specs + Thread.current[:uses_db_connection] = nil + + expect { subscriber.sql(event) }.to change { Thread.current[:uses_db_connection] }.from(nil).to(true) + end + + it_behaves_like 'record ActiveRecord metrics' + it_behaves_like 'store ActiveRecord info in RequestStore' + end + end + + context 'without Marginalia comments' do + let(:comments) { false } + + it_behaves_like 'track generic sql events' + end + + context 'with Marginalia comments' do + let(:comments) { true } + + it_behaves_like 'track generic sql events' + end end end diff --git a/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb b/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb index 5bcaf8fbc47..adbc05cb711 100644 --- a/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb @@ -6,29 +6,45 @@ RSpec.describe Gitlab::Metrics::Subscribers::ExternalHttp, :request_store do let(:transaction) { Gitlab::Metrics::Transaction.new } let(:subscriber) { described_class.new } + around do |example| + freeze_time { example.run } + end + let(:event_1) do - double(:event, payload: { - method: 'POST', code: "200", duration: 0.321, - scheme: 'https', host: 'gitlab.com', port: 80, path: '/api/v4/projects', - query: 'current=true' - }) + double( + :event, + payload: { + method: 'POST', code: "200", duration: 0.321, + scheme: 'https', host: 'gitlab.com', port: 80, path: '/api/v4/projects', + query: 'current=true' + }, + time: Time.current + ) end let(:event_2) do - double(:event, payload: { - method: 'GET', code: "301", duration: 0.12, - scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2', - query: 'current=true' - }) + double( + :event, + payload: { + method: 'GET', code: "301", duration: 0.12, + scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2', + query: 'current=true' + }, + time: Time.current + ) end let(:event_3) do - double(:event, payload: { - method: 'POST', duration: 5.3, - scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2/issues', - query: 'current=true', - exception_object: Net::ReadTimeout.new - }) + double( + :event, + payload: { + method: 'POST', duration: 5.3, + scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2/issues', + query: 'current=true', + exception_object: Net::ReadTimeout.new + }, + time: Time.current + ) end describe '.detail_store' do @@ -134,19 +150,22 @@ RSpec.describe Gitlab::Metrics::Subscribers::ExternalHttp, :request_store do subscriber.request(event_3) expect(Gitlab::SafeRequestStore[:external_http_detail_store].length).to eq(3) - expect(Gitlab::SafeRequestStore[:external_http_detail_store][0]).to include( + expect(Gitlab::SafeRequestStore[:external_http_detail_store][0]).to match a_hash_including( + start: be_like_time(Time.current), method: 'POST', code: "200", duration: 0.321, scheme: 'https', host: 'gitlab.com', port: 80, path: '/api/v4/projects', query: 'current=true', exception_object: nil, backtrace: be_a(Array) ) - expect(Gitlab::SafeRequestStore[:external_http_detail_store][1]).to include( + expect(Gitlab::SafeRequestStore[:external_http_detail_store][1]).to match a_hash_including( + start: be_like_time(Time.current), method: 'GET', code: "301", duration: 0.12, scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2', query: 'current=true', exception_object: nil, backtrace: be_a(Array) ) - expect(Gitlab::SafeRequestStore[:external_http_detail_store][2]).to include( + expect(Gitlab::SafeRequestStore[:external_http_detail_store][2]).to match a_hash_including( + start: be_like_time(Time.current), method: 'POST', duration: 5.3, scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2/issues', query: 'current=true', diff --git a/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb b/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb new file mode 100644 index 00000000000..b9d00b556c5 --- /dev/null +++ b/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rack' + +RSpec.describe Gitlab::Middleware::RackMultipartTempfileFactory do + let(:app) do + lambda do |env| + params = Rack::Request.new(env).params + + if params['file'] + [200, { 'Content-Type' => params['file'][:type] }, [params['file'][:tempfile].read]] + else + [204, {}, []] + end + end + end + + let(:file_contents) { '/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg' } + + let(:multipart_fixture) do + boundary = 'AaB03x' + data = <<~DATA + --#{boundary}\r + Content-Disposition: form-data; name="file"; filename="dj.jpg"\r + Content-Type: image/jpeg\r + Content-Transfer-Encoding: base64\r + \r + #{file_contents}\r + --#{boundary}--\r + DATA + + { + 'CONTENT_TYPE' => "multipart/form-data; boundary=#{boundary}", + 'CONTENT_LENGTH' => data.bytesize.to_s, + input: StringIO.new(data) + } + end + + subject { described_class.new(app) } + + context 'for a multipart request' do + let(:env) { Rack::MockRequest.env_for('/', multipart_fixture) } + + context 'when the environment variable is enabled' do + before do + stub_env('GITLAB_TEMPFILE_IMMEDIATE_UNLINK', '1') + end + + it 'immediately unlinks the temporary file' do + tempfile = Tempfile.new('foo') + + expect(tempfile.path).not_to be(nil) + expect(Rack::Multipart::Parser::TEMPFILE_FACTORY).to receive(:call).and_return(tempfile) + expect(tempfile).to receive(:unlink).and_call_original + + subject.call(env) + + expect(tempfile.path).to be(nil) + end + + it 'processes the request as normal' do + expect(subject.call(env)).to eq([200, { 'Content-Type' => 'image/jpeg' }, [file_contents]]) + end + end + + context 'when the environment variable is disabled' do + it 'does not immediately unlink the temporary file' do + tempfile = Tempfile.new('foo') + + expect(tempfile.path).not_to be(nil) + expect(Rack::Multipart::Parser::TEMPFILE_FACTORY).to receive(:call).and_return(tempfile) + expect(tempfile).not_to receive(:unlink).and_call_original + + subject.call(env) + + expect(tempfile.path).not_to be(nil) + end + + it 'processes the request as normal' do + expect(subject.call(env)).to eq([200, { 'Content-Type' => 'image/jpeg' }, [file_contents]]) + end + end + end + + context 'for a regular request' do + let(:env) { Rack::MockRequest.env_for('/', params: { 'foo' => 'bar' }) } + + it 'does nothing' do + expect(Rack::Multipart::Parser::TEMPFILE_FACTORY).not_to receive(:call) + expect(subject.call(env)).to eq([204, {}, []]) + end + end +end diff --git a/spec/lib/gitlab/object_hierarchy_spec.rb b/spec/lib/gitlab/object_hierarchy_spec.rb index 08e1a5ee0a3..eebd67695e0 100644 --- a/spec/lib/gitlab/object_hierarchy_spec.rb +++ b/spec/lib/gitlab/object_hierarchy_spec.rb @@ -3,14 +3,16 @@ require 'spec_helper' RSpec.describe Gitlab::ObjectHierarchy do - let!(:parent) { create(:group) } - let!(:child1) { create(:group, parent: parent) } - let!(:child2) { create(:group, parent: child1) } + let_it_be(:parent) { create(:group) } + let_it_be(:child1) { create(:group, parent: parent) } + let_it_be(:child2) { create(:group, parent: child1) } + + let(:options) { {} } shared_context 'Gitlab::ObjectHierarchy test cases' do describe '#base_and_ancestors' do let(:relation) do - described_class.new(Group.where(id: child2.id)).base_and_ancestors + described_class.new(Group.where(id: child2.id), options: options).base_and_ancestors end it 'includes the base rows' do @@ -22,13 +24,13 @@ RSpec.describe Gitlab::ObjectHierarchy do end it 'can find ancestors upto a certain level' do - relation = described_class.new(Group.where(id: child2)).base_and_ancestors(upto: child1) + relation = described_class.new(Group.where(id: child2), options: options).base_and_ancestors(upto: child1) expect(relation).to contain_exactly(child2) end it 'uses ancestors_base #initialize argument' do - relation = described_class.new(Group.where(id: child2.id), Group.none).base_and_ancestors + relation = described_class.new(Group.where(id: child2.id), Group.none, options: options).base_and_ancestors expect(relation).to include(parent, child1, child2) end @@ -40,7 +42,7 @@ RSpec.describe Gitlab::ObjectHierarchy do describe 'hierarchy_order option' do let(:relation) do - described_class.new(Group.where(id: child2.id)).base_and_ancestors(hierarchy_order: hierarchy_order) + described_class.new(Group.where(id: child2.id), options: options).base_and_ancestors(hierarchy_order: hierarchy_order) end context ':asc' do @@ -63,7 +65,7 @@ RSpec.describe Gitlab::ObjectHierarchy do describe '#base_and_descendants' do let(:relation) do - described_class.new(Group.where(id: parent.id)).base_and_descendants + described_class.new(Group.where(id: parent.id), options: options).base_and_descendants end it 'includes the base rows' do @@ -75,7 +77,7 @@ RSpec.describe Gitlab::ObjectHierarchy do end it 'uses descendants_base #initialize argument' do - relation = described_class.new(Group.none, Group.where(id: parent.id)).base_and_descendants + relation = described_class.new(Group.none, Group.where(id: parent.id), options: options).base_and_descendants expect(relation).to include(parent, child1, child2) end @@ -87,7 +89,7 @@ RSpec.describe Gitlab::ObjectHierarchy do context 'when with_depth is true' do let(:relation) do - described_class.new(Group.where(id: parent.id)).base_and_descendants(with_depth: true) + described_class.new(Group.where(id: parent.id), options: options).base_and_descendants(with_depth: true) end it 'includes depth in the results' do @@ -106,14 +108,14 @@ RSpec.describe Gitlab::ObjectHierarchy do describe '#descendants' do it 'includes only the descendants' do - relation = described_class.new(Group.where(id: parent)).descendants + relation = described_class.new(Group.where(id: parent), options: options).descendants expect(relation).to contain_exactly(child1, child2) end end describe '#max_descendants_depth' do - subject { described_class.new(base_relation).max_descendants_depth } + subject { described_class.new(base_relation, options: options).max_descendants_depth } context 'when base relation is empty' do let(:base_relation) { Group.where(id: nil) } @@ -136,13 +138,13 @@ RSpec.describe Gitlab::ObjectHierarchy do describe '#ancestors' do it 'includes only the ancestors' do - relation = described_class.new(Group.where(id: child2)).ancestors + relation = described_class.new(Group.where(id: child2), options: options).ancestors expect(relation).to contain_exactly(child1, parent) end it 'can find ancestors upto a certain level' do - relation = described_class.new(Group.where(id: child2)).ancestors(upto: child1) + relation = described_class.new(Group.where(id: child2), options: options).ancestors(upto: child1) expect(relation).to be_empty end @@ -150,7 +152,7 @@ RSpec.describe Gitlab::ObjectHierarchy do describe '#all_objects' do let(:relation) do - described_class.new(Group.where(id: child1.id)).all_objects + described_class.new(Group.where(id: child1.id), options: options).all_objects end it 'includes the base rows' do @@ -166,13 +168,13 @@ RSpec.describe Gitlab::ObjectHierarchy do end it 'uses ancestors_base #initialize argument for ancestors' do - relation = described_class.new(Group.where(id: child1.id), Group.where(id: non_existing_record_id)).all_objects + relation = described_class.new(Group.where(id: child1.id), Group.where(id: non_existing_record_id), options: options).all_objects expect(relation).to include(parent) end it 'uses descendants_base #initialize argument for descendants' do - relation = described_class.new(Group.where(id: non_existing_record_id), Group.where(id: child1.id)).all_objects + relation = described_class.new(Group.where(id: non_existing_record_id), Group.where(id: child1.id), options: options).all_objects expect(relation).to include(child2) end @@ -187,19 +189,78 @@ RSpec.describe Gitlab::ObjectHierarchy do context 'when the use_distinct_in_object_hierarchy feature flag is enabled' do before do stub_feature_flags(use_distinct_in_object_hierarchy: true) + stub_feature_flags(use_distinct_for_all_object_hierarchy: false) + end + + it_behaves_like 'Gitlab::ObjectHierarchy test cases' + + it 'calls DISTINCT' do + expect(child2.self_and_ancestors.to_sql).to include("DISTINCT") + end + + context 'when use_traversal_ids feature flag is enabled' do + it 'does not call DISTINCT' do + expect(parent.self_and_descendants.to_sql).not_to include("DISTINCT") + end + end + + context 'when use_traversal_ids feature flag is disabled' do + before do + stub_feature_flags(use_traversal_ids: false) + end + + it 'calls DISTINCT' do + expect(parent.self_and_descendants.to_sql).to include("DISTINCT") + end + end + end + + context 'when the use_distinct_for_all_object_hierarchy feature flag is enabled' do + before do + stub_feature_flags(use_distinct_in_object_hierarchy: false) + stub_feature_flags(use_distinct_for_all_object_hierarchy: true) end it_behaves_like 'Gitlab::ObjectHierarchy test cases' it 'calls DISTINCT' do - expect(parent.self_and_descendants.to_sql).to include("DISTINCT") expect(child2.self_and_ancestors.to_sql).to include("DISTINCT") end + + context 'when use_traversal_ids feature flag is enabled' do + it 'does not call DISTINCT' do + expect(parent.self_and_descendants.to_sql).not_to include("DISTINCT") + end + end + + context 'when use_traversal_ids feature flag is disabled' do + before do + stub_feature_flags(use_traversal_ids: false) + end + + it 'calls DISTINCT' do + expect(parent.self_and_descendants.to_sql).to include("DISTINCT") + end + + context 'when the skip_ordering option is set' do + let(:options) { { skip_ordering: true } } + + it_behaves_like 'Gitlab::ObjectHierarchy test cases' + + it 'does not include ROW_NUMBER()' do + query = described_class.new(Group.where(id: parent.id), options: options).base_and_descendants.to_sql + + expect(query).to include("DISTINCT") + expect(query).not_to include("ROW_NUMBER()") + end + end + end end context 'when the use_distinct_in_object_hierarchy feature flag is disabled' do before do stub_feature_flags(use_distinct_in_object_hierarchy: false) + stub_feature_flags(use_distinct_for_all_object_hierarchy: false) end it_behaves_like 'Gitlab::ObjectHierarchy test cases' diff --git a/spec/lib/gitlab/pages/settings_spec.rb b/spec/lib/gitlab/pages/settings_spec.rb index f5424a98153..c89bf9ff206 100644 --- a/spec/lib/gitlab/pages/settings_spec.rb +++ b/spec/lib/gitlab/pages/settings_spec.rb @@ -3,11 +3,11 @@ require 'spec_helper' RSpec.describe Gitlab::Pages::Settings do + let(:settings) { double(path: 'the path', local_store: 'local store') } + describe '#path' do subject { described_class.new(settings).path } - let(:settings) { double(path: 'the path') } - it { is_expected.to eq('the path') } context 'when running under a web server outside of test mode' do @@ -16,9 +16,43 @@ RSpec.describe Gitlab::Pages::Settings do allow(::Gitlab::Runtime).to receive(:web_server?).and_return(true) end - it 'raises a DiskAccessDenied exception' do - expect { subject }.to raise_error(described_class::DiskAccessDenied) + it 'logs a DiskAccessDenied error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + instance_of(described_class::DiskAccessDenied) + ) + + subject + end + end + + context 'when local_store settings does not exist yet' do + before do + allow(Settings.pages).to receive(:local_store).and_return(nil) end + + it { is_expected.to eq('the path') } + end + + context 'when local store exists but legacy storage is disabled' do + before do + allow(Settings.pages.local_store).to receive(:enabled).and_return(false) + end + + it 'logs a DiskAccessDenied error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + instance_of(described_class::DiskAccessDenied) + ) + + subject + end + end + end + + describe '#local_store' do + subject(:local_store) { described_class.new(settings).local_store } + + it 'is an instance of Gitlab::Pages::Stores::LocalStore' do + expect(local_store).to be_a(Gitlab::Pages::Stores::LocalStore) end end end diff --git a/spec/lib/gitlab/pages/stores/local_store_spec.rb b/spec/lib/gitlab/pages/stores/local_store_spec.rb new file mode 100644 index 00000000000..adab81b2589 --- /dev/null +++ b/spec/lib/gitlab/pages/stores/local_store_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Pages::Stores::LocalStore do + describe '#enabled' do + let(:local_store) { double(enabled: true) } + + subject(:local_store_enabled) { described_class.new(local_store).enabled } + + context 'when the pages_update_legacy_storage FF is disabled' do + before do + stub_feature_flags(pages_update_legacy_storage: false) + end + + it { is_expected.to be_falsey } + end + + context 'when the pages_update_legacy_storage FF is enabled' do + it 'is equal to the original value' do + expect(local_store_enabled).to eq(local_store.enabled) + end + end + end +end diff --git a/spec/lib/gitlab/pages_transfer_spec.rb b/spec/lib/gitlab/pages_transfer_spec.rb index 552a2e0701c..021d9cb7318 100644 --- a/spec/lib/gitlab/pages_transfer_spec.rb +++ b/spec/lib/gitlab/pages_transfer_spec.rb @@ -17,7 +17,7 @@ RSpec.describe Gitlab::PagesTransfer do end it 'does nothing if legacy storage is disabled' do - stub_feature_flags(pages_update_legacy_storage: false) + allow(Settings.pages.local_store).to receive(:enabled).and_return(false) described_class::METHODS.each do |meth| expect(PagesTransferWorker) @@ -72,7 +72,7 @@ RSpec.describe Gitlab::PagesTransfer do end it 'does nothing if legacy storage is disabled' do - stub_feature_flags(pages_update_legacy_storage: false) + allow(Settings.pages.local_store).to receive(:enabled).and_return(false) subject.public_send(meth, *args) diff --git a/spec/lib/gitlab/pagination/keyset/order_spec.rb b/spec/lib/gitlab/pagination/keyset/order_spec.rb index 665f790ee47..06a8aee1048 100644 --- a/spec/lib/gitlab/pagination/keyset/order_spec.rb +++ b/spec/lib/gitlab/pagination/keyset/order_spec.rb @@ -417,4 +417,59 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do end end end + + context 'extract and apply cursor attributes' do + let(:model) { Project.new(id: 100) } + let(:scope) { Project.all } + + shared_examples 'cursor attribute examples' do + describe '#cursor_attributes_for_node' do + it { expect(order.cursor_attributes_for_node(model)).to eq({ id: '100' }.with_indifferent_access) } + end + + describe '#apply_cursor_conditions' do + context 'when params with string keys are passed' do + subject(:sql) { order.apply_cursor_conditions(scope, { 'id' => '100' }).to_sql } + + it { is_expected.to include('"projects"."id" < 100)') } + end + + context 'when params with symbol keys are passed' do + subject(:sql) { order.apply_cursor_conditions(scope, { id: '100' }).to_sql } + + it { is_expected.to include('"projects"."id" < 100)') } + end + end + end + + context 'when string attribute name is given' do + let(:order) do + Gitlab::Pagination::Keyset::Order.build([ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'id', + order_expression: Project.arel_table['id'].desc, + nullable: :not_nullable, + distinct: true + ) + ]) + end + + it_behaves_like 'cursor attribute examples' + end + + context 'when symbol attribute name is given' do + let(:order) do + Gitlab::Pagination::Keyset::Order.build([ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :id, + order_expression: Project.arel_table['id'].desc, + nullable: :not_nullable, + distinct: true + ) + ]) + end + + it_behaves_like 'cursor attribute examples' + end + end end diff --git a/spec/lib/gitlab/pagination/offset_header_builder_with_controller_spec.rb b/spec/lib/gitlab/pagination/offset_header_builder_with_controller_spec.rb new file mode 100644 index 00000000000..85e4b621e83 --- /dev/null +++ b/spec/lib/gitlab/pagination/offset_header_builder_with_controller_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Pagination::OffsetHeaderBuilder, type: :controller do + controller(ActionController::Base) do + def index + relation = Project.where(archived: params[:archived]).page(params[:page]).order(:id).per(1) + + params_for_pagination = { archived: params[:archived], page: params[:page] } + + Gitlab::Pagination::OffsetHeaderBuilder.new( + request_context: self, + per_page: relation.limit_value, + page: relation.current_page, + next_page: relation.next_page, + prev_page: relation.prev_page, + params: params_for_pagination + ).execute(exclude_total_headers: true, data_without_counts: true) + + render json: relation.map(&:id) + end + end + + let_it_be(:projects) { create_list(:project, 2, archived: true).sort_by(&:id) } + + describe 'pagination' do + it 'returns correct result for the first page' do + get :index, params: { page: 1, archived: true } + + expect(json_response).to eq([projects.first.id]) + end + + it 'returns correct result for the second page' do + get :index, params: { page: 2, archived: true } + + expect(json_response).to eq([projects.last.id]) + end + end + + describe 'pagination heders' do + it 'adds next page header' do + get :index, params: { page: 1, archived: true } + + expect(response.headers['X-Next-Page']).to eq('2') + end + + it 'adds only the specified params to the lnk' do + get :index, params: { page: 1, archived: true, some_param: '1' } + + expect(response.headers['Link']).not_to include('some_param') + end + end +end diff --git a/spec/lib/gitlab/phabricator_import/cache/map_spec.rb b/spec/lib/gitlab/phabricator_import/cache/map_spec.rb index 08ac85c2625..157b3ca56c9 100644 --- a/spec/lib/gitlab/phabricator_import/cache/map_spec.rb +++ b/spec/lib/gitlab/phabricator_import/cache/map_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::PhabricatorImport::Cache::Map, :clean_gitlab_redis_cache do let_it_be(:project) { create(:project) } + let(:redis) { Gitlab::Redis::Cache } subject(:map) { described_class.new(project) } diff --git a/spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb b/spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb index 3cb15f08627..0539bacba44 100644 --- a/spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb +++ b/spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::PhabricatorImport::Issues::TaskImporter do let_it_be(:project) { create(:project) } + let(:task) do Gitlab::PhabricatorImport::Representation::Task.new( { diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb index 89917e515d0..48e2a2e9794 100644 --- a/spec/lib/gitlab/profiler_spec.rb +++ b/spec/lib/gitlab/profiler_spec.rb @@ -78,13 +78,8 @@ RSpec.describe Gitlab::Profiler do end it 'strips out the private token' do - expect(custom_logger).to receive(:add) do |severity, _progname, message| - next if message.include?('spec/') - - expect(severity).to eq(Logger::DEBUG) - expect(message).to include('public').and include(described_class::FILTERED_STRING) - expect(message).not_to include(private_token) - end.at_least(1) # This spec could be wrapped in more blocks in the future + allow(custom_logger).to receive(:add).and_call_original + expect(custom_logger).to receive(:add).with(Logger::DEBUG, anything, 'public [FILTERED]').at_least(1) custom_logger.debug("public #{private_token}") end diff --git a/spec/lib/gitlab/prometheus/adapter_spec.rb b/spec/lib/gitlab/prometheus/adapter_spec.rb index 4762e4ad108..9d4806ea73b 100644 --- a/spec/lib/gitlab/prometheus/adapter_spec.rb +++ b/spec/lib/gitlab/prometheus/adapter_spec.rb @@ -32,6 +32,14 @@ RSpec.describe Gitlab::Prometheus::Adapter do context "prometheus service can't execute queries" do let(:prometheus_service) { double(:prometheus_service, can_query?: false) } + context 'with cluster with prometheus integration' do + let!(:prometheus_integration) { create(:clusters_integrations_prometheus, cluster: cluster) } + + it 'returns the integration' do + expect(subject.prometheus_adapter).to eq(prometheus_integration) + end + end + context 'with cluster with prometheus not available' do let!(:prometheus) { create(:clusters_applications_prometheus, :installable, cluster: cluster) } @@ -46,6 +54,14 @@ RSpec.describe Gitlab::Prometheus::Adapter do it 'returns application handling all environments' do expect(subject.prometheus_adapter).to eq(prometheus) end + + context 'with cluster with prometheus integration' do + let!(:prometheus_integration) { create(:clusters_integrations_prometheus, cluster: cluster) } + + it 'returns the integration instead' do + expect(subject.prometheus_adapter).to eq(prometheus_integration) + end + end end context 'with cluster without prometheus installed' do diff --git a/spec/lib/gitlab/query_limiting/transaction_spec.rb b/spec/lib/gitlab/query_limiting/transaction_spec.rb index 40804736b86..76bb2b4c4cc 100644 --- a/spec/lib/gitlab/query_limiting/transaction_spec.rb +++ b/spec/lib/gitlab/query_limiting/transaction_spec.rb @@ -68,11 +68,15 @@ RSpec.describe Gitlab::QueryLimiting::Transaction do it 'increments the number of executed queries' do transaction = described_class.new - expect(transaction.count).to be_zero + expect { transaction.increment }.to change { transaction.count }.by(1) + end + + it 'does not increment the number of executed queries when query limiting is disabled' do + transaction = described_class.new - transaction.increment + allow(transaction).to receive(:enabled?).and_return(false) - expect(transaction.count).to eq(1) + expect { transaction.increment }.not_to change { transaction.count } end end diff --git a/spec/lib/gitlab/query_limiting_spec.rb b/spec/lib/gitlab/query_limiting_spec.rb index 4f70c65adca..fbb12629056 100644 --- a/spec/lib/gitlab/query_limiting_spec.rb +++ b/spec/lib/gitlab/query_limiting_spec.rb @@ -2,81 +2,85 @@ require 'spec_helper' -RSpec.describe Gitlab::QueryLimiting do - describe '.enable?' do +RSpec.describe Gitlab::QueryLimiting, :request_store do + describe '.enabled_for_env?' do it 'returns true in a test environment' do - expect(described_class.enable?).to eq(true) + expect(described_class.enabled_for_env?).to eq(true) end it 'returns true in a development environment' do stub_rails_env('development') stub_rails_env('development') - expect(described_class.enable?).to eq(true) + expect(described_class.enabled_for_env?).to eq(true) end it 'returns false on GitLab.com' do stub_rails_env('production') allow(Gitlab).to receive(:com?).and_return(true) - expect(described_class.enable?).to eq(false) + expect(described_class.enabled_for_env?).to eq(false) end it 'returns false in a non GitLab.com' do allow(Gitlab).to receive(:com?).and_return(false) stub_rails_env('production') - expect(described_class.enable?).to eq(false) + expect(described_class.enabled_for_env?).to eq(false) end end - describe '.whitelist' do - it 'raises ArgumentError when an invalid issue URL is given' do - expect { described_class.whitelist('foo') } - .to raise_error(ArgumentError) + shared_context 'disable and enable' do |result| + let(:transaction) { Gitlab::QueryLimiting::Transaction.new } + let(:code) do + proc do + 2.times { User.count } + end end - context 'without a transaction' do - it 'does nothing' do - expect { described_class.whitelist('https://example.com') } - .not_to raise_error - end + before do + allow(Gitlab::QueryLimiting::Transaction) + .to receive(:current) + .and_return(transaction) end + end - context 'with a transaction' do - let(:transaction) { Gitlab::QueryLimiting::Transaction.new } + describe '.disable!' do + include_context 'disable and enable' - before do - allow(Gitlab::QueryLimiting::Transaction) - .to receive(:current) - .and_return(transaction) - end + it 'raises an ArgumentError when an invalid issue URL is given' do + expect { described_class.disable!('foo') } + .to raise_error(ArgumentError) + end - it 'does not increment the number of SQL queries executed in the block' do - before = transaction.count + it 'stops the number of SQL queries from being incremented' do + described_class.disable!('https://example.com') - described_class.whitelist('https://example.com') + expect { code.call }.not_to change { transaction.count } + end + end - 2.times do - User.count - end + describe '.enable!' do + include_context 'disable and enable' - expect(transaction.count).to eq(before) - end + it 'allows the number of SQL queries to be incremented' do + described_class.enable! - it 'whitelists when enabled' do - described_class.whitelist('https://example.com') + expect { code.call }.to change { transaction.count }.by(2) + end + end - expect(transaction.whitelisted).to eq(true) - end + describe '#enabled?' do + it 'returns true when enabled' do + Gitlab::SafeRequestStore[:query_limiting_disabled] = nil - it 'does not whitelist when disabled' do - allow(described_class).to receive(:enable?).and_return(false) + expect(described_class).to be_enabled + end - described_class.whitelist('https://example.com') + it 'returns false when disabled' do + Gitlab::SafeRequestStore[:query_limiting_disabled] = true - expect(transaction.whitelisted).to eq(false) - end + expect(described_class).not_to be_enabled end end end diff --git a/spec/lib/gitlab/quick_actions/command_definition_spec.rb b/spec/lib/gitlab/quick_actions/command_definition_spec.rb index d63c21954f2..73629ce3da2 100644 --- a/spec/lib/gitlab/quick_actions/command_definition_spec.rb +++ b/spec/lib/gitlab/quick_actions/command_definition_spec.rb @@ -127,10 +127,10 @@ RSpec.describe Gitlab::QuickActions::CommandDefinition do subject.condition_block = proc { false } end - it "doesn't execute the command" do + it "counts the command as executed" do subject.execute(context, nil) - expect(context.commands_executed_count).to be_nil + expect(context.commands_executed_count).to eq(1) expect(context.run).to be false end end @@ -238,8 +238,8 @@ RSpec.describe Gitlab::QuickActions::CommandDefinition do subject.condition_block = proc { false } end - it 'returns nil' do - expect(subject.execute_message({}, nil)).to be_nil + it 'returns an error message' do + expect(subject.execute_message({}, nil)).to eq('Could not apply command command.') end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 1aca3dae41b..f62a3c74005 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -667,7 +667,14 @@ RSpec.describe Gitlab::Regex do it { is_expected.to match('1.2.3') } it { is_expected.to match('1.3.350') } - it { is_expected.not_to match('1.3.350-20201230123456') } + it { is_expected.to match('1.3.350-20201230123456') } + it { is_expected.to match('1.2.3-rc1') } + it { is_expected.to match('1.2.3g') } + it { is_expected.to match('1.2') } + it { is_expected.to match('1.2.bananas') } + it { is_expected.to match('v1.2.4-build') } + it { is_expected.to match('d50d836eb3de6177ce6c7a5482f27f9c2c84b672') } + it { is_expected.to match('this_is_a_string_only') } it { is_expected.not_to match('..1.2.3') } it { is_expected.not_to match(' 1.2.3') } it { is_expected.not_to match("1.2.3 \r\t") } diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb index 625dcf11546..d14c3f44c6f 100644 --- a/spec/lib/gitlab/repository_cache_adapter_spec.rb +++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb @@ -29,10 +29,19 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do def project end + + def cached_methods + [:letters] + end + + def exists? + true + end end end let(:fake_repository) { klass.new } + let(:redis_set_cache) { fake_repository.redis_set_cache } context 'with an existing repository' do it 'caches the output, sorting the results' do @@ -42,47 +51,43 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do expect(fake_repository.letters).to eq(%w(a b c)) end - expect(fake_repository.redis_set_cache.exist?(:letters)).to eq(true) + expect(redis_set_cache.exist?(:letters)).to eq(true) expect(fake_repository.instance_variable_get(:@letters)).to eq(%w(a b c)) end context 'membership checks' do context 'when the cache key does not exist' do it 'calls the original method and populates the cache' do - expect(fake_repository.redis_set_cache.exist?(:letters)).to eq(false) + expect(redis_set_cache.exist?(:letters)).to eq(false) expect(fake_repository).to receive(:_uncached_letters).once.and_call_original # This populates the cache and memoizes the full result expect(fake_repository.letters_include?('a')).to eq(true) expect(fake_repository.letters_include?('d')).to eq(false) - expect(fake_repository.redis_set_cache.exist?(:letters)).to eq(true) + expect(redis_set_cache.exist?(:letters)).to eq(true) end end context 'when the cache key exists' do before do - fake_repository.redis_set_cache.write(:letters, %w(b a c)) + redis_set_cache.write(:letters, %w(b a c)) end - it 'calls #include? on the set cache' do - expect(fake_repository.redis_set_cache) - .to receive(:include?).with(:letters, 'a').and_call_original - expect(fake_repository.redis_set_cache) - .to receive(:include?).with(:letters, 'd').and_call_original + it 'calls #try_include? on the set cache' do + expect(redis_set_cache).to receive(:try_include?).with(:letters, 'a').and_call_original + expect(redis_set_cache).to receive(:try_include?).with(:letters, 'd').and_call_original expect(fake_repository.letters_include?('a')).to eq(true) expect(fake_repository.letters_include?('d')).to eq(false) end it 'memoizes the result' do - expect(fake_repository.redis_set_cache) - .to receive(:include?).once.and_call_original + expect(redis_set_cache).to receive(:try_include?).once.and_call_original expect(fake_repository.letters_include?('a')).to eq(true) expect(fake_repository.letters_include?('a')).to eq(true) - expect(fake_repository.redis_set_cache) - .to receive(:include?).once.and_call_original + expect(redis_set_cache).to receive(:try_include?).once.and_call_original expect(fake_repository.letters_include?('d')).to eq(false) expect(fake_repository.letters_include?('d')).to eq(false) diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb index 07f4d7c462d..eaecbb0233d 100644 --- a/spec/lib/gitlab/repository_set_cache_spec.rb +++ b/spec/lib/gitlab/repository_set_cache_spec.rb @@ -124,6 +124,18 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do end end + describe '#search' do + subject do + cache.search(:foo, 'val*') do + %w[value helloworld notvalmatch] + end + end + + it 'returns search pattern matches from the key' do + is_expected.to contain_exactly('value') + end + end + describe '#include?' do it 'checks inclusion in the Redis set' do cache.write(:foo, ['value']) @@ -132,4 +144,15 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do expect(cache.include?(:foo, 'bar')).to be(false) end end + + describe '#try_include?' do + it 'checks existence of the redis set and inclusion' do + expect(cache.try_include?(:foo, 'value')).to eq([false, false]) + + cache.write(:foo, ['value']) + + expect(cache.try_include?(:foo, 'value')).to eq([true, true]) + expect(cache.try_include?(:foo, 'bar')).to eq([false, true]) + end + end end diff --git a/spec/lib/gitlab/sanitizers/exif_spec.rb b/spec/lib/gitlab/sanitizers/exif_spec.rb index 63b2f3fc693..fbda9e6d0be 100644 --- a/spec/lib/gitlab/sanitizers/exif_spec.rb +++ b/spec/lib/gitlab/sanitizers/exif_spec.rb @@ -113,7 +113,7 @@ RSpec.describe Gitlab::Sanitizers::Exif do it 'cleans only jpg/tiff images with the correct mime types' do expect(sanitizer).not_to receive(:extra_tags) - expect { subject }.to raise_error(RuntimeError, /File type text\/plain not supported/) + expect { subject }.to raise_error(RuntimeError, %r{File type text/plain not supported}) end end end diff --git a/spec/lib/gitlab/search_context/builder_spec.rb b/spec/lib/gitlab/search_context/builder_spec.rb index 5b4190fc67e..079477115bb 100644 --- a/spec/lib/gitlab/search_context/builder_spec.rb +++ b/spec/lib/gitlab/search_context/builder_spec.rb @@ -127,6 +127,35 @@ RSpec.describe Gitlab::SearchContext::Builder, type: :controller do it { is_expected.to be_for_group } it { is_expected.to be_search_context(group: group) } + + context 'with group scope' do + let(:action_name) { '' } + + before do + allow(controller).to receive(:controller_name).and_return('groups') + allow(controller).to receive(:action_name).and_return(action_name) + end + + it 'returns nil without groups controller action' do + expect(subject.scope).to be(nil) + end + + context 'when on issues scope' do + let(:action_name) { 'issues' } + + it 'search context returns issues scope' do + expect(subject.scope).to be('issues') + end + end + + context 'when on merge requests scope' do + let(:action_name) { 'merge_requests' } + + it 'search context returns issues scope' do + expect(subject.scope).to be('merge_requests') + end + end + end end end diff --git a/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb b/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb index 74834fb9014..43cbe71dd6b 100644 --- a/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb +++ b/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb @@ -214,7 +214,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do expect(Gitlab::SidekiqCluster).not_to receive(:start) expect { cli.run(%W(#{flag} unknown_field=chatops)) } - .to raise_error(Gitlab::SidekiqConfig::CliMethods::QueryError) + .to raise_error(Gitlab::SidekiqConfig::WorkerMatcher::QueryError) end end end diff --git a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb index 01e7c06249a..bc63289a344 100644 --- a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb +++ b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'fast_spec_helper' -require 'rspec-parameterized' RSpec.describe Gitlab::SidekiqConfig::CliMethods do let(:dummy_root) { '/tmp/' } @@ -122,10 +121,8 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do end end - describe '.query_workers' do - using RSpec::Parameterized::TableSyntax - - let(:queues) do + describe '.query_queues' do + let(:worker_metadatas) do [ { name: 'a', @@ -162,79 +159,16 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do ] end - context 'with valid input' do - where(:query, :selected_queues) do - # feature_category - 'feature_category=category_a' | %w(a a:2) - 'feature_category=category_a,category_c' | %w(a a:2 c) - 'feature_category=category_a|feature_category=category_c' | %w(a a:2 c) - 'feature_category!=category_a' | %w(b c) - - # has_external_dependencies - 'has_external_dependencies=true' | %w(b) - 'has_external_dependencies=false' | %w(a a:2 c) - 'has_external_dependencies=true,false' | %w(a a:2 b c) - 'has_external_dependencies=true|has_external_dependencies=false' | %w(a a:2 b c) - 'has_external_dependencies!=true' | %w(a a:2 c) - - # urgency - 'urgency=high' | %w(a:2 b) - 'urgency=low' | %w(a) - 'urgency=high,low,throttled' | %w(a a:2 b c) - 'urgency=low|urgency=throttled' | %w(a c) - 'urgency!=high' | %w(a c) - - # name - 'name=a' | %w(a) - 'name=a,b' | %w(a b) - 'name=a,a:2|name=b' | %w(a a:2 b) - 'name!=a,a:2' | %w(b c) - - # resource_boundary - 'resource_boundary=memory' | %w(b c) - 'resource_boundary=memory,cpu' | %w(a b c) - 'resource_boundary=memory|resource_boundary=cpu' | %w(a b c) - 'resource_boundary!=memory,cpu' | %w(a:2) - - # tags - 'tags=no_disk_io' | %w(a b) - 'tags=no_disk_io,git_access' | %w(a a:2 b) - 'tags=no_disk_io|tags=git_access' | %w(a a:2 b) - 'tags=no_disk_io&tags=git_access' | %w(a) - 'tags!=no_disk_io' | %w(a:2 c) - 'tags!=no_disk_io,git_access' | %w(c) - 'tags=unknown_tag' | [] - 'tags!=no_disk_io' | %w(a:2 c) - 'tags!=no_disk_io,git_access' | %w(c) - 'tags!=unknown_tag' | %w(a a:2 b c) - - # combinations - 'feature_category=category_a&urgency=high' | %w(a:2) - 'feature_category=category_a&urgency=high|feature_category=category_c' | %w(a:2 c) - end + let(:worker_matcher) { double(:WorkerMatcher) } + let(:query) { 'feature_category=category_a,category_c' } - with_them do - it do - expect(described_class.query_workers(query, queues)) - .to match_array(selected_queues) - end - end + before do + allow(::Gitlab::SidekiqConfig::WorkerMatcher).to receive(:new).with(query).and_return(worker_matcher) + allow(worker_matcher).to receive(:match?).and_return(true, true, false, true) end - context 'with invalid input' do - where(:query, :error) do - 'feature_category="category_a"' | described_class::InvalidTerm - 'feature_category=' | described_class::InvalidTerm - 'feature_category~category_a' | described_class::InvalidTerm - 'worker_name=a' | described_class::UnknownPredicate - end - - with_them do - it do - expect { described_class.query_workers(query, queues) } - .to raise_error(error) - end - end + it 'returns the queue names of matched workers' do + expect(described_class.query_queues(query, worker_metadatas)).to match(%w(a a:2 c)) end end end diff --git a/spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb b/spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb new file mode 100644 index 00000000000..75e9c8c100b --- /dev/null +++ b/spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' + +RSpec.describe Gitlab::SidekiqConfig::WorkerMatcher do + describe '#match?' do + using RSpec::Parameterized::TableSyntax + + let(:worker_metadatas) do + [ + { + name: 'a', + feature_category: :category_a, + has_external_dependencies: false, + urgency: :low, + resource_boundary: :cpu, + tags: [:no_disk_io, :git_access] + }, + { + name: 'a:2', + feature_category: :category_a, + has_external_dependencies: false, + urgency: :high, + resource_boundary: :none, + tags: [:git_access] + }, + { + name: 'b', + feature_category: :category_b, + has_external_dependencies: true, + urgency: :high, + resource_boundary: :memory, + tags: [:no_disk_io] + }, + { + name: 'c', + feature_category: :category_c, + has_external_dependencies: false, + urgency: :throttled, + resource_boundary: :memory, + tags: [] + } + ] + end + + context 'with valid input' do + where(:query, :expected_metadatas) do + # feature_category + 'feature_category=category_a' | %w(a a:2) + 'feature_category=category_a,category_c' | %w(a a:2 c) + 'feature_category=category_a|feature_category=category_c' | %w(a a:2 c) + 'feature_category!=category_a' | %w(b c) + + # has_external_dependencies + 'has_external_dependencies=true' | %w(b) + 'has_external_dependencies=false' | %w(a a:2 c) + 'has_external_dependencies=true,false' | %w(a a:2 b c) + 'has_external_dependencies=true|has_external_dependencies=false' | %w(a a:2 b c) + 'has_external_dependencies!=true' | %w(a a:2 c) + + # urgency + 'urgency=high' | %w(a:2 b) + 'urgency=low' | %w(a) + 'urgency=high,low,throttled' | %w(a a:2 b c) + 'urgency=low|urgency=throttled' | %w(a c) + 'urgency!=high' | %w(a c) + + # name + 'name=a' | %w(a) + 'name=a,b' | %w(a b) + 'name=a,a:2|name=b' | %w(a a:2 b) + 'name!=a,a:2' | %w(b c) + + # resource_boundary + 'resource_boundary=memory' | %w(b c) + 'resource_boundary=memory,cpu' | %w(a b c) + 'resource_boundary=memory|resource_boundary=cpu' | %w(a b c) + 'resource_boundary!=memory,cpu' | %w(a:2) + + # tags + 'tags=no_disk_io' | %w(a b) + 'tags=no_disk_io,git_access' | %w(a a:2 b) + 'tags=no_disk_io|tags=git_access' | %w(a a:2 b) + 'tags=no_disk_io&tags=git_access' | %w(a) + 'tags!=no_disk_io' | %w(a:2 c) + 'tags!=no_disk_io,git_access' | %w(c) + 'tags=unknown_tag' | [] + 'tags!=no_disk_io' | %w(a:2 c) + 'tags!=no_disk_io,git_access' | %w(c) + 'tags!=unknown_tag' | %w(a a:2 b c) + + # combinations + 'feature_category=category_a&urgency=high' | %w(a:2) + 'feature_category=category_a&urgency=high|feature_category=category_c' | %w(a:2 c) + + # Match all + '*' | %w(a a:2 b c) + end + + with_them do + it do + matched_metadatas = worker_metadatas.select do |metadata| + described_class.new(query).match?(metadata) + end + expect(matched_metadatas.map { |m| m[:name] }).to match_array(expected_metadatas) + end + end + end + + context 'with invalid input' do + where(:query, :error) do + 'feature_category="category_a"' | described_class::InvalidTerm + 'feature_category=' | described_class::InvalidTerm + 'feature_category~category_a' | described_class::InvalidTerm + 'worker_name=a' | described_class::UnknownPredicate + end + + with_them do + it do + worker_metadatas.each do |metadata| + expect { described_class.new(query).match?(metadata) } + .to raise_error(error) + end + end + end + end + end +end diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index 3e8e117ec71..537844df72f 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -10,80 +10,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do end describe '#call', :request_store do - let(:timestamp) { Time.iso8601('2018-01-01T12:00:00.000Z') } - let(:created_at) { timestamp - 1.second } - let(:scheduling_latency_s) { 1.0 } - - let(:job) do - { - "class" => "TestWorker", - "args" => [1234, 'hello', { 'key' => 'value' }], - "retry" => false, - "queue" => "cronjob:test_queue", - "queue_namespace" => "cronjob", - "jid" => "da883554ee4fe414012f5f42", - "created_at" => created_at.to_f, - "enqueued_at" => created_at.to_f, - "correlation_id" => 'cid', - "error_message" => "wrong number of arguments (2 for 3)", - "error_class" => "ArgumentError", - "error_backtrace" => [] - } - end - - let(:logger) { double } - let(:clock_realtime_start) { 0.222222299 } - let(:clock_realtime_end) { 1.333333799 } - let(:clock_thread_cputime_start) { 0.222222299 } - let(:clock_thread_cputime_end) { 1.333333799 } - let(:start_payload) do - job.except('error_backtrace', 'error_class', 'error_message').merge( - 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: start', - 'job_status' => 'start', - 'pid' => Process.pid, - 'created_at' => created_at.to_f, - 'enqueued_at' => created_at.to_f, - 'scheduling_latency_s' => scheduling_latency_s, - 'job_size_bytes' => be > 0 - ) - end - - let(:end_payload) do - start_payload.merge( - 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec', - 'job_status' => 'done', - 'duration_s' => 0.0, - 'completed_at' => timestamp.to_f, - 'cpu_s' => 1.111112, - 'db_duration_s' => 0.0, - 'db_cached_count' => 0, - 'db_count' => 0, - 'db_write_count' => 0 - ) - end - - let(:exception_payload) do - end_payload.merge( - 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec', - 'job_status' => 'fail', - 'error_class' => 'ArgumentError', - 'error_message' => 'Something went wrong', - 'error_backtrace' => be_a(Array).and(be_present) - ) - end - - before do - allow(Sidekiq).to receive(:logger).and_return(logger) - - allow(subject).to receive(:current_time).and_return(timestamp.to_f) - - allow(Process).to receive(:clock_gettime).with(Process::CLOCK_REALTIME, :float_second) - .and_return(clock_realtime_start, clock_realtime_end) - allow(Process).to receive(:clock_gettime).with(Process::CLOCK_THREAD_CPUTIME_ID, :float_second) - .and_return(clock_thread_cputime_start, clock_thread_cputime_end) - end - - subject { described_class.new } + include_context 'structured_logger' context 'with SIDEKIQ_LOG_ARGUMENTS enabled' do before do @@ -283,14 +210,19 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do end_payload.merge(timing_data.stringify_keys) end - it 'logs with Gitaly and Rugged timing data' do + before do + allow(::Gitlab::InstrumentationHelper).to receive(:add_instrumentation_data).and_wrap_original do |method, values| + method.call(values) + values.merge!(timing_data) + end + end + + it 'logs with Gitaly and Rugged timing data', :aggregate_failures do Timecop.freeze(timestamp) do expect(logger).to receive(:info).with(start_payload).ordered expect(logger).to receive(:info).with(expected_end_payload).ordered - call_subject(job, 'test_queue') do - job.merge!(timing_data) - end + call_subject(job, 'test_queue') { } end end end @@ -361,15 +293,6 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do end end end - - def call_subject(job, queue) - # This structured logger strongly depends on execution of `InstrumentationLogger` - subject.call(job, queue) do - ::Gitlab::SidekiqMiddleware::InstrumentationLogger.new.call('worker', job, queue) do - yield - end - end - end end describe '#add_time_keys!' do diff --git a/spec/lib/gitlab/sidekiq_middleware/admin_mode/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/admin_mode/client_spec.rb index 3ba08455d01..9d5d5f28eab 100644 --- a/spec/lib/gitlab/sidekiq_middleware/admin_mode/client_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/admin_mode/client_spec.rb @@ -74,9 +74,9 @@ RSpec.describe Gitlab::SidekiqMiddleware::AdminMode::Client, :request_store do end end - context 'admin mode feature disabled' do + context 'admin mode setting disabled' do before do - stub_feature_flags(user_mode_in_session: false) + stub_application_setting(admin_mode: false) end it 'yields block' do diff --git a/spec/lib/gitlab/sidekiq_middleware/admin_mode/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/admin_mode/server_spec.rb index e8322b11875..3ab1a9cd2f4 100644 --- a/spec/lib/gitlab/sidekiq_middleware/admin_mode/server_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/admin_mode/server_spec.rb @@ -52,9 +52,9 @@ RSpec.describe Gitlab::SidekiqMiddleware::AdminMode::Server, :request_store do end end - context 'admin mode feature disabled' do + context 'admin mode setting disabled' do before do - stub_feature_flags(user_mode_in_session: false) + stub_application_setting(admin_mode: false) end it 'yields block' do diff --git a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb index e2b36125b4e..82ca84f0697 100644 --- a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb @@ -3,156 +3,33 @@ require 'spec_helper' RSpec.describe Gitlab::SidekiqMiddleware::ClientMetrics do - context "with worker attribution" do - subject { described_class.new } + shared_examples "a metrics middleware" do + context "with mocked prometheus" do + let(:enqueued_jobs_metric) { double('enqueued jobs metric', increment: true) } - let(:queue) { :test } - let(:worker_class) { worker.class } - let(:job) { {} } - let(:default_labels) do - { queue: queue.to_s, - worker: worker_class.to_s, - boundary: "", - external_dependencies: "no", - feature_category: "", - urgency: "low" } - end - - shared_examples "a metrics client middleware" do - context "with mocked prometheus" do - let(:enqueued_jobs_metric) { double('enqueued jobs metric', increment: true) } - - before do - allow(Gitlab::Metrics).to receive(:counter).with(described_class::ENQUEUED, anything).and_return(enqueued_jobs_metric) - end - - describe '#call' do - it 'yields block' do - expect { |b| subject.call(worker_class, job, :test, double, &b) }.to yield_control.once - end - - it 'increments enqueued jobs metric with correct labels when worker is a string of the class' do - expect(enqueued_jobs_metric).to receive(:increment).with(labels, 1) - - subject.call(worker_class.to_s, job, :test, double) { nil } - end - - it 'increments enqueued jobs metric with correct labels' do - expect(enqueued_jobs_metric).to receive(:increment).with(labels, 1) - - subject.call(worker_class, job, :test, double) { nil } - end - end - end - end - - context "when workers are not attributed" do before do - stub_const('TestNonAttributedWorker', Class.new) - TestNonAttributedWorker.class_eval do - include Sidekiq::Worker - end - end - - it_behaves_like "a metrics client middleware" do - let(:worker) { TestNonAttributedWorker.new } - let(:labels) { default_labels.merge(urgency: "") } - end - end - - context "when a worker is wrapped into ActiveJob" do - before do - stub_const('TestWrappedWorker', Class.new) - TestWrappedWorker.class_eval do - include Sidekiq::Worker - end - end - - it_behaves_like "a metrics client middleware" do - let(:job) do - { - "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper, - "wrapped" => TestWrappedWorker - } - end - - let(:worker) { TestWrappedWorker.new } - let(:labels) { default_labels.merge(urgency: "") } - end - end - - context "when workers are attributed" do - def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category) - klass = Class.new do - include Sidekiq::Worker - include WorkerAttributes - - urgency urgency if urgency - worker_has_external_dependencies! if external_dependencies - worker_resource_boundary resource_boundary unless resource_boundary == :unknown - feature_category category unless category.nil? - end - stub_const("TestAttributedWorker", klass) - end - - let(:urgency) { nil } - let(:external_dependencies) { false } - let(:resource_boundary) { :unknown } - let(:feature_category) { nil } - let(:worker_class) { create_attributed_worker_class(urgency, external_dependencies, resource_boundary, feature_category) } - let(:worker) { worker_class.new } - - context "high urgency" do - it_behaves_like "a metrics client middleware" do - let(:urgency) { :high } - let(:labels) { default_labels.merge(urgency: "high") } - end + allow(Gitlab::Metrics).to receive(:counter).with(described_class::ENQUEUED, anything).and_return(enqueued_jobs_metric) end - context "no urgency" do - it_behaves_like "a metrics client middleware" do - let(:urgency) { :throttled } - let(:labels) { default_labels.merge(urgency: "throttled") } + describe '#call' do + it 'yields block' do + expect { |b| subject.call(worker_class, job, :test, double, &b) }.to yield_control.once end - end - context "external dependencies" do - it_behaves_like "a metrics client middleware" do - let(:external_dependencies) { true } - let(:labels) { default_labels.merge(external_dependencies: "yes") } - end - end + it 'increments enqueued jobs metric with correct labels when worker is a string of the class' do + expect(enqueued_jobs_metric).to receive(:increment).with(labels, 1) - context "cpu boundary" do - it_behaves_like "a metrics client middleware" do - let(:resource_boundary) { :cpu } - let(:labels) { default_labels.merge(boundary: "cpu") } + subject.call(worker_class.to_s, job, :test, double) { nil } end - end - context "memory boundary" do - it_behaves_like "a metrics client middleware" do - let(:resource_boundary) { :memory } - let(:labels) { default_labels.merge(boundary: "memory") } - end - end + it 'increments enqueued jobs metric with correct labels' do + expect(enqueued_jobs_metric).to receive(:increment).with(labels, 1) - context "feature category" do - it_behaves_like "a metrics client middleware" do - let(:feature_category) { :authentication } - let(:labels) { default_labels.merge(feature_category: "authentication") } - end - end - - context "combined" do - it_behaves_like "a metrics client middleware" do - let(:urgency) { :high } - let(:external_dependencies) { true } - let(:resource_boundary) { :cpu } - let(:feature_category) { :authentication } - let(:labels) { default_labels.merge(urgency: "high", external_dependencies: "yes", boundary: "cpu", feature_category: "authentication") } + subject.call(worker_class, job, :test, double) { nil } end end end end + + it_behaves_like 'metrics middleware with worker attribution' end diff --git a/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb b/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb new file mode 100644 index 00000000000..eb9ba50cdcd --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::SidekiqMiddleware::InstrumentationLogger do + let(:job) { { 'jid' => 123 } } + let(:queue) { 'test_queue' } + let(:worker) do + Class.new do + def self.name + 'TestDWorker' + end + + include ApplicationWorker + + def perform(*args) + end + end + end + + subject { described_class.new } + + before do + stub_const('TestWorker', worker) + end + + describe '.keys' do + it 'returns all available payload keys' do + expected_keys = [ + :cpu_s, + :gitaly_calls, + :gitaly_duration_s, + :rugged_calls, + :rugged_duration_s, + :elasticsearch_calls, + :elasticsearch_duration_s, + :elasticsearch_timed_out_count, + :mem_objects, + :mem_bytes, + :mem_mallocs, + :redis_calls, + :redis_duration_s, + :redis_read_bytes, + :redis_write_bytes, + :redis_action_cable_calls, + :redis_action_cable_duration_s, + :redis_action_cable_read_bytes, + :redis_action_cable_write_bytes, + :redis_cache_calls, + :redis_cache_duration_s, + :redis_cache_read_bytes, + :redis_cache_write_bytes, + :redis_queues_calls, + :redis_queues_duration_s, + :redis_queues_read_bytes, + :redis_queues_write_bytes, + :redis_shared_state_calls, + :redis_shared_state_duration_s, + :redis_shared_state_read_bytes, + :redis_shared_state_write_bytes, + :db_count, + :db_write_count, + :db_cached_count, + :external_http_count, + :external_http_duration_s, + :rack_attack_redis_count, + :rack_attack_redis_duration_s + ] + + expect(described_class.keys).to include(*expected_keys) + end + end + + describe '#call', :request_store do + let(:instrumentation_values) do + { + cpu_s: 10, + unknown_attribute: 123, + db_count: 0, + db_cached_count: 0, + db_write_count: 0, + gitaly_calls: 0, + redis_calls: 0 + } + end + + before do + allow(::Gitlab::InstrumentationHelper).to receive(:add_instrumentation_data) do |values| + values.merge!(instrumentation_values) + end + end + + it 'merges correct instrumentation data in the job' do + expect { |b| subject.call(worker, job, queue, &b) }.to yield_control + + expected_values = instrumentation_values.except(:unknown_attribute) + + expect(job[:instrumentation]).to eq(expected_values) + end + end +end diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb index 71f4f2a3b64..95be76ce351 100644 --- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb @@ -4,296 +4,108 @@ require 'spec_helper' # rubocop: disable RSpec/MultipleMemoizedHelpers RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do - context "with worker attribution" do - subject { described_class.new } + shared_examples "a metrics middleware" do + context "with mocked prometheus" do + include_context 'server metrics with mocked prometheus' - let(:queue) { :test } - let(:worker_class) { worker.class } - let(:job) { {} } - let(:job_status) { :done } - let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) } - let(:default_labels) do - { queue: queue.to_s, - worker: worker_class.to_s, - boundary: "", - external_dependencies: "no", - feature_category: "", - urgency: "low" } - end - - shared_examples "a metrics middleware" do - context "with mocked prometheus" do - let(:concurrency_metric) { double('concurrency metric') } - - let(:queue_duration_seconds) { double('queue duration seconds metric') } - let(:completion_seconds_metric) { double('completion seconds metric') } - let(:user_execution_seconds_metric) { double('user execution seconds metric') } - let(:db_seconds_metric) { double('db seconds metric') } - let(:gitaly_seconds_metric) { double('gitaly seconds metric') } - let(:failed_total_metric) { double('failed total metric') } - let(:retried_total_metric) { double('retried total metric') } - let(:redis_requests_total) { double('redis calls total metric') } - let(:running_jobs_metric) { double('running jobs metric') } - let(:redis_seconds_metric) { double('redis seconds metric') } - let(:elasticsearch_seconds_metric) { double('elasticsearch seconds metric') } - let(:elasticsearch_requests_total) { double('elasticsearch calls total metric') } + describe '#initialize' do + it 'sets concurrency metrics' do + expect(concurrency_metric).to receive(:set).with({}, Sidekiq.options[:concurrency].to_i) - before do - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_queue_duration_seconds, anything, anything, anything).and_return(queue_duration_seconds) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_cpu_seconds, anything, anything, anything).and_return(user_execution_seconds_metric) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_db_seconds, anything, anything, anything).and_return(db_seconds_metric) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_gitaly_seconds, anything, anything, anything).and_return(gitaly_seconds_metric) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_redis_requests_duration_seconds, anything, anything, anything).and_return(redis_seconds_metric) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_elasticsearch_requests_duration_seconds, anything, anything, anything).and_return(elasticsearch_seconds_metric) - allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric) - allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric) - allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_redis_requests_total, anything).and_return(redis_requests_total) - allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_elasticsearch_requests_total, anything).and_return(elasticsearch_requests_total) - allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric) - allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric) - - allow(concurrency_metric).to receive(:set) + subject end + end - describe '#initialize' do - it 'sets concurrency metrics' do - expect(concurrency_metric).to receive(:set).with({}, Sidekiq.options[:concurrency].to_i) + describe '#call' do + include_context 'server metrics call' - subject - end + it 'yields block' do + expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once end - describe '#call' do - let(:thread_cputime_before) { 1 } - let(:thread_cputime_after) { 2 } - let(:thread_cputime_duration) { thread_cputime_after - thread_cputime_before } - - let(:monotonic_time_before) { 11 } - let(:monotonic_time_after) { 20 } - let(:monotonic_time_duration) { monotonic_time_after - monotonic_time_before } - - let(:queue_duration_for_job) { 0.01 } - - let(:db_duration) { 3 } - let(:gitaly_duration) { 4 } - - let(:redis_calls) { 2 } - let(:redis_duration) { 0.01 } - - let(:elasticsearch_calls) { 8 } - let(:elasticsearch_duration) { 0.54 } - - before do - allow(subject).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after) - allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after) - allow(Gitlab::InstrumentationHelper).to receive(:queue_duration_for_job).with(job).and_return(queue_duration_for_job) - allow(ActiveRecord::LogSubscriber).to receive(:runtime).and_return(db_duration * 1000) - - job[:gitaly_duration_s] = gitaly_duration - job[:redis_calls] = redis_calls - job[:redis_duration_s] = redis_duration - - job[:elasticsearch_calls] = elasticsearch_calls - job[:elasticsearch_duration_s] = elasticsearch_duration - - allow(running_jobs_metric).to receive(:increment) - allow(redis_requests_total).to receive(:increment) - allow(elasticsearch_requests_total).to receive(:increment) - allow(queue_duration_seconds).to receive(:observe) - allow(user_execution_seconds_metric).to receive(:observe) - allow(db_seconds_metric).to receive(:observe) - allow(gitaly_seconds_metric).to receive(:observe) - allow(completion_seconds_metric).to receive(:observe) - allow(redis_seconds_metric).to receive(:observe) - allow(elasticsearch_seconds_metric).to receive(:observe) + it 'calls BackgroundTransaction' do + expect_next_instance_of(Gitlab::Metrics::BackgroundTransaction) do |instance| + expect(instance).to receive(:run) end - it 'yields block' do - expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once - end + subject.call(worker, job, :test) {} + end - it 'calls BackgroundTransaction' do - expect_next_instance_of(Gitlab::Metrics::BackgroundTransaction) do |instance| - expect(instance).to receive(:run) - end + it 'sets queue specific metrics' do + expect(running_jobs_metric).to receive(:increment).with(labels, -1) + expect(running_jobs_metric).to receive(:increment).with(labels, 1) + expect(queue_duration_seconds).to receive(:observe).with(labels, queue_duration_for_job) if queue_duration_for_job + expect(user_execution_seconds_metric).to receive(:observe).with(labels_with_job_status, thread_cputime_duration) + expect(db_seconds_metric).to receive(:observe).with(labels_with_job_status, db_duration) + expect(gitaly_seconds_metric).to receive(:observe).with(labels_with_job_status, gitaly_duration) + expect(completion_seconds_metric).to receive(:observe).with(labels_with_job_status, monotonic_time_duration) + expect(redis_seconds_metric).to receive(:observe).with(labels_with_job_status, redis_duration) + expect(elasticsearch_seconds_metric).to receive(:observe).with(labels_with_job_status, elasticsearch_duration) + expect(redis_requests_total).to receive(:increment).with(labels_with_job_status, redis_calls) + expect(elasticsearch_requests_total).to receive(:increment).with(labels_with_job_status, elasticsearch_calls) + + subject.call(worker, job, :test) { nil } + end - subject.call(worker, job, :test) {} - end + it 'sets the thread name if it was nil' do + allow(Thread.current).to receive(:name).and_return(nil) + expect(Thread.current).to receive(:name=).with(Gitlab::Metrics::Samplers::ThreadsSampler::SIDEKIQ_WORKER_THREAD_NAME) - it 'sets queue specific metrics' do - expect(running_jobs_metric).to receive(:increment).with(labels, -1) - expect(running_jobs_metric).to receive(:increment).with(labels, 1) - expect(queue_duration_seconds).to receive(:observe).with(labels, queue_duration_for_job) if queue_duration_for_job - expect(user_execution_seconds_metric).to receive(:observe).with(labels_with_job_status, thread_cputime_duration) - expect(db_seconds_metric).to receive(:observe).with(labels_with_job_status, db_duration) - expect(gitaly_seconds_metric).to receive(:observe).with(labels_with_job_status, gitaly_duration) - expect(completion_seconds_metric).to receive(:observe).with(labels_with_job_status, monotonic_time_duration) - expect(redis_seconds_metric).to receive(:observe).with(labels_with_job_status, redis_duration) - expect(elasticsearch_seconds_metric).to receive(:observe).with(labels_with_job_status, elasticsearch_duration) - expect(redis_requests_total).to receive(:increment).with(labels_with_job_status, redis_calls) - expect(elasticsearch_requests_total).to receive(:increment).with(labels_with_job_status, elasticsearch_calls) + subject.call(worker, job, :test) { nil } + end - subject.call(worker, job, :test) { nil } - end + context 'when job_duration is not available' do + let(:queue_duration_for_job) { nil } - it 'sets the thread name if it was nil' do - allow(Thread.current).to receive(:name).and_return(nil) - expect(Thread.current).to receive(:name=).with(Gitlab::Metrics::Samplers::ThreadsSampler::SIDEKIQ_WORKER_THREAD_NAME) + it 'does not set the queue_duration_seconds histogram' do + expect(queue_duration_seconds).not_to receive(:observe) subject.call(worker, job, :test) { nil } end + end - context 'when job_duration is not available' do - let(:queue_duration_for_job) { nil } - - it 'does not set the queue_duration_seconds histogram' do - expect(queue_duration_seconds).not_to receive(:observe) - - subject.call(worker, job, :test) { nil } - end - end - - context 'when error is raised' do - let(:job_status) { :fail } - - it 'sets sidekiq_jobs_failed_total and reraises' do - expect(failed_total_metric).to receive(:increment).with(labels, 1) - - expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") - end - end - - context 'when job is retried' do - let(:job) { { 'retry_count' => 1 } } + context 'when error is raised' do + let(:job_status) { :fail } - it 'sets sidekiq_jobs_retried_total metric' do - expect(retried_total_metric).to receive(:increment) + it 'sets sidekiq_jobs_failed_total and reraises' do + expect(failed_total_metric).to receive(:increment).with(labels, 1) - subject.call(worker, job, :test) { nil } - end + expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") end end - end - context "with prometheus integrated" do - describe '#call' do - it 'yields block' do - expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once - end + context 'when job is retried' do + let(:job) { { 'retry_count' => 1 } } - context 'when error is raised' do - let(:job_status) { :fail } + it 'sets sidekiq_jobs_retried_total metric' do + expect(retried_total_metric).to receive(:increment) - it 'sets sidekiq_jobs_failed_total and reraises' do - expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") - end + subject.call(worker, job, :test) { nil } end end end end - context "when workers are not attributed" do - before do - stub_const('TestNonAttributedWorker', Class.new) - TestNonAttributedWorker.class_eval do - include Sidekiq::Worker + context "with prometheus integrated" do + describe '#call' do + it 'yields block' do + expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once end - end - let(:worker) { TestNonAttributedWorker.new } - let(:labels) { default_labels.merge(urgency: "") } + context 'when error is raised' do + let(:job_status) { :fail } - it_behaves_like "a metrics middleware" - end - - context "when a worker is wrapped into ActiveJob" do - before do - stub_const('TestWrappedWorker', Class.new) - TestWrappedWorker.class_eval do - include Sidekiq::Worker + it 'sets sidekiq_jobs_failed_total and reraises' do + expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") + end end end - - let(:job) do - { - "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper, - "wrapped" => TestWrappedWorker - } - end - - let(:worker) { TestWrappedWorker.new } - let(:worker_class) { TestWrappedWorker } - let(:labels) { default_labels.merge(urgency: "") } - - it_behaves_like "a metrics middleware" end + end - context "when workers are attributed" do - def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category) - Class.new do - include Sidekiq::Worker - include WorkerAttributes - - urgency urgency if urgency - worker_has_external_dependencies! if external_dependencies - worker_resource_boundary resource_boundary unless resource_boundary == :unknown - feature_category category unless category.nil? - end - end - - let(:urgency) { nil } - let(:external_dependencies) { false } - let(:resource_boundary) { :unknown } - let(:feature_category) { nil } - let(:worker_class) { create_attributed_worker_class(urgency, external_dependencies, resource_boundary, feature_category) } - let(:worker) { worker_class.new } - - context "high urgency" do - let(:urgency) { :high } - let(:labels) { default_labels.merge(urgency: "high") } - - it_behaves_like "a metrics middleware" - end - - context "external dependencies" do - let(:external_dependencies) { true } - let(:labels) { default_labels.merge(external_dependencies: "yes") } - - it_behaves_like "a metrics middleware" - end - - context "cpu boundary" do - let(:resource_boundary) { :cpu } - let(:labels) { default_labels.merge(boundary: "cpu") } - - it_behaves_like "a metrics middleware" - end - - context "memory boundary" do - let(:resource_boundary) { :memory } - let(:labels) { default_labels.merge(boundary: "memory") } - - it_behaves_like "a metrics middleware" - end - - context "feature category" do - let(:feature_category) { :authentication } - let(:labels) { default_labels.merge(feature_category: "authentication") } - - it_behaves_like "a metrics middleware" - end - - context "combined" do - let(:urgency) { :throttled } - let(:external_dependencies) { true } - let(:resource_boundary) { :cpu } - let(:feature_category) { :authentication } - let(:labels) { default_labels.merge(urgency: "throttled", external_dependencies: "yes", boundary: "cpu", feature_category: "authentication") } - - it_behaves_like "a metrics middleware" - end - end + it_behaves_like 'metrics middleware with worker attribution' do + let(:job_status) { :done } + let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) } end end # rubocop: enable RSpec/MultipleMemoizedHelpers diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb index ca473462d2e..f736a7db774 100644 --- a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Server do worker_context user: nil def perform(identifier, *args) - self.class.contexts.merge!(identifier => Labkit::Context.current.to_h) + self.class.contexts.merge!(identifier => Gitlab::ApplicationContext.current) end end end diff --git a/spec/lib/gitlab/sidekiq_middleware_spec.rb b/spec/lib/gitlab/sidekiq_middleware_spec.rb index 755f6004e52..0efdef0c999 100644 --- a/spec/lib/gitlab/sidekiq_middleware_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware_spec.rb @@ -69,11 +69,13 @@ RSpec.describe Gitlab::SidekiqMiddleware do shared_examples "a server middleware chain" do it "passes through the right server middlewares" do enabled_sidekiq_middlewares.each do |middleware| - expect_any_instance_of(middleware).to receive(:call).with(*middleware_expected_args).once.and_call_original + expect_next_instance_of(middleware) do |middleware_instance| + expect(middleware_instance).to receive(:call).with(*middleware_expected_args).once.and_call_original + end end disabled_sidekiq_middlewares.each do |middleware| - expect_any_instance_of(middleware).not_to receive(:call) + expect(middleware).not_to receive(:new) end worker_class.perform_async(*job_args) diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_comment_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_comment_spec.rb index 109b4b8fee1..690ffb15a5d 100644 --- a/spec/lib/gitlab/slash_commands/presenters/issue_comment_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/issue_comment_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Gitlab::SlashCommands::Presenters::IssueComment do let_it_be(:project) { create(:project) } let_it_be(:issue) { create(:issue, project: project) } let_it_be(:note) { create(:note, project: project, noteable: issue) } + let(:author) { note.author } describe '#present' do diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb index a4d8e3957cf..7b3440b40a7 100644 --- a/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Gitlab::SlashCommands::Presenters::IssueMove do let_it_be(:project, reload: true) { create(:project) } let_it_be(:other_project) { create(:project) } let_it_be(:old_issue, reload: true) { create(:issue, project: project) } + let(:new_issue) { Issues::MoveService.new(project, user).execute(old_issue, other_project) } let(:attachment) { subject[:attachments].first } diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_new_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_new_spec.rb index 03a94ea5e29..21a983090fb 100644 --- a/spec/lib/gitlab/slash_commands/presenters/issue_new_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/issue_new_spec.rb @@ -1,19 +1,22 @@ # frozen_string_literal: true - require 'spec_helper' RSpec.describe Gitlab::SlashCommands::Presenters::IssueNew do + include Gitlab::Routing let(:project) { create(:project) } let(:issue) { create(:issue, project: project) } - let(:attachment) { subject[:attachments].first } subject { described_class.new(issue).present } it { is_expected.to be_a(Hash) } it 'shows the issue' do - expect(subject[:response_type]).to be(:in_channel) - expect(subject).to have_key(:attachments) - expect(attachment[:title]).to start_with(issue.title) + expected_text = "I created an issue on <#{url_for(issue.author)}|#{issue.author.to_reference}>'s behalf: *<#{project_issue_url(issue.project, issue)}|#{issue.to_reference}>* in <#{project.web_url}|#{project.full_name}>" + + expect(subject).to eq( + response_type: :in_channel, + status: 200, + text: expected_text + ) end end diff --git a/spec/lib/gitlab/slash_commands/run_spec.rb b/spec/lib/gitlab/slash_commands/run_spec.rb index c9ff580d586..9d204228d21 100644 --- a/spec/lib/gitlab/slash_commands/run_spec.rb +++ b/spec/lib/gitlab/slash_commands/run_spec.rb @@ -3,6 +3,26 @@ require 'spec_helper' RSpec.describe Gitlab::SlashCommands::Run do + describe '.match' do + it 'returns true for a run command' do + expect(described_class.match('run foo')).to be_an_instance_of(MatchData) + end + + it 'returns true for a run command with arguments' do + expect(described_class.match('run foo bar baz')) + .to be_an_instance_of(MatchData) + end + + it 'returns true for a command containing newlines' do + expect(described_class.match("run foo\nbar\nbaz")) + .to be_an_instance_of(MatchData) + end + + it 'returns false for an unrelated command' do + expect(described_class.match('foo bar')).to be_nil + end + end + describe '.available?' do it 'returns true when builds are enabled for the project' do project = double(:project, builds_enabled?: true) diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb index 2177b2be6d6..fc342b7e9b1 100644 --- a/spec/lib/gitlab/snippet_search_results_spec.rb +++ b/spec/lib/gitlab/snippet_search_results_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Gitlab::SnippetSearchResults do include SearchHelpers let_it_be(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') } + let(:results) { described_class.new(snippet.author, 'foo') } describe '#snippet_titles_count' do diff --git a/spec/lib/gitlab/sourcegraph_spec.rb b/spec/lib/gitlab/sourcegraph_spec.rb index ad947475f06..6bebd1ca3e6 100644 --- a/spec/lib/gitlab/sourcegraph_spec.rb +++ b/spec/lib/gitlab/sourcegraph_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Sourcegraph do let_it_be(:user) { create(:user) } + let(:feature_scope) { true } before do diff --git a/spec/lib/gitlab/sql/cte_spec.rb b/spec/lib/gitlab/sql/cte_spec.rb index fdc150cd4b9..4cf94f4dcab 100644 --- a/spec/lib/gitlab/sql/cte_spec.rb +++ b/spec/lib/gitlab/sql/cte_spec.rb @@ -14,7 +14,14 @@ RSpec.describe Gitlab::SQL::CTE do relation.except(:order).to_sql end - expect(sql).to eq("#{name} AS (#{sql1})") + expected = [ + "#{name} AS ", + Gitlab::Database::AsWithMaterialized.materialized_if_supported, + (' ' unless Gitlab::Database::AsWithMaterialized.materialized_if_supported.blank?), + "(#{sql1})" + ].join + + expect(sql).to eq(expected) end end @@ -41,4 +48,15 @@ RSpec.describe Gitlab::SQL::CTE do expect(relation.to_a).to eq(User.where(id: user.id).to_a) end end + + it_behaves_like 'CTE with MATERIALIZED keyword examples' do + let(:expected_query_block_with_materialized) { 'WITH "some_cte" AS MATERIALIZED (' } + let(:expected_query_block_without_materialized) { 'WITH "some_cte" AS (' } + + let(:query) do + cte = described_class.new(:some_cte, User.active, **options) + + User.with(cte.to_arel).to_sql + end + end end diff --git a/spec/lib/gitlab/sql/recursive_cte_spec.rb b/spec/lib/gitlab/sql/recursive_cte_spec.rb index 02611620989..edcacd404c2 100644 --- a/spec/lib/gitlab/sql/recursive_cte_spec.rb +++ b/spec/lib/gitlab/sql/recursive_cte_spec.rb @@ -57,4 +57,17 @@ RSpec.describe Gitlab::SQL::RecursiveCTE do expect(relation.to_a).to eq(User.where(id: user.id).to_a) end end + + it_behaves_like 'CTE with MATERIALIZED keyword examples' do + # MATERIALIZED keyword is not needed for recursive queries + let(:expected_query_block_with_materialized) { 'WITH RECURSIVE "some_cte" AS (' } + let(:expected_query_block_without_materialized) { 'WITH RECURSIVE "some_cte" AS (' } + + let(:query) do + recursive_cte = described_class.new(:some_cte) + recursive_cte << User.active + + User.with.recursive(recursive_cte.to_arel).to_sql + end + end end diff --git a/spec/lib/gitlab/subscription_portal_spec.rb b/spec/lib/gitlab/subscription_portal_spec.rb index 351af3c07d2..ad1affdac0b 100644 --- a/spec/lib/gitlab/subscription_portal_spec.rb +++ b/spec/lib/gitlab/subscription_portal_spec.rb @@ -3,39 +3,41 @@ require 'spec_helper' RSpec.describe ::Gitlab::SubscriptionPortal do - describe '.default_subscriptions_url' do - subject { described_class.default_subscriptions_url } - - context 'on non test and non dev environments' do - before do - allow(Rails).to receive_message_chain(:env, :test?).and_return(false) - allow(Rails).to receive_message_chain(:env, :development?).and_return(false) + unless Gitlab.jh? + describe '.default_subscriptions_url' do + subject { described_class.default_subscriptions_url } + + context 'on non test and non dev environments' do + before do + allow(Rails).to receive_message_chain(:env, :test?).and_return(false) + allow(Rails).to receive_message_chain(:env, :development?).and_return(false) + end + + it 'returns production subscriptions app URL' do + is_expected.to eq('https://customers.gitlab.com') + end end - it 'returns production subscriptions app URL' do - is_expected.to eq('https://customers.gitlab.com') - end - end + context 'on dev environment' do + before do + allow(Rails).to receive_message_chain(:env, :test?).and_return(false) + allow(Rails).to receive_message_chain(:env, :development?).and_return(true) + end - context 'on dev environment' do - before do - allow(Rails).to receive_message_chain(:env, :test?).and_return(false) - allow(Rails).to receive_message_chain(:env, :development?).and_return(true) + it 'returns staging subscriptions app url' do + is_expected.to eq('https://customers.stg.gitlab.com') + end end - it 'returns staging subscriptions app url' do - is_expected.to eq('https://customers.stg.gitlab.com') - end - end - - context 'on test environment' do - before do - allow(Rails).to receive_message_chain(:env, :test?).and_return(true) - allow(Rails).to receive_message_chain(:env, :development?).and_return(false) - end + context 'on test environment' do + before do + allow(Rails).to receive_message_chain(:env, :test?).and_return(true) + allow(Rails).to receive_message_chain(:env, :development?).and_return(false) + end - it 'returns staging subscriptions app url' do - is_expected.to eq('https://customers.stg.gitlab.com') + it 'returns staging subscriptions app url' do + is_expected.to eq('https://customers.stg.gitlab.com') + end end end end diff --git a/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb b/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb index 05f351be702..793ad1c1959 100644 --- a/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb +++ b/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Template::Finders::RepoTemplateFinder do let_it_be(:project) { create(:project, :repository) } + let(:categories) { { 'HTML' => 'html' } } subject(:finder) { described_class.new(project, 'files/', '.html', categories) } diff --git a/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb b/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb index 0e8647ad78a..65597e6568d 100644 --- a/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb +++ b/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb @@ -41,21 +41,6 @@ RSpec.describe Gitlab::Tracking::Destinations::Snowplow do .with('category', 'action', 'label', 'property', 1.5, nil, (Time.now.to_f * 1000).to_i) end end - - describe '#self_describing_event' do - it 'sends event to tracker' do - allow(tracker).to receive(:track_self_describing_event).and_call_original - - subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' }) - - expect(tracker).to have_received(:track_self_describing_event) do |event, context, timestamp| - expect(event.to_json[:schema]).to eq('iglu:com.gitlab/foo/jsonschema/1-0-0') - expect(event.to_json[:data]).to eq(foo: 'bar') - expect(context).to eq(nil) - expect(timestamp).to eq((Time.now.to_f * 1000).to_i) - end - end - end end context 'when snowplow is not enabled' do @@ -66,13 +51,5 @@ RSpec.describe Gitlab::Tracking::Destinations::Snowplow do subject.event('category', 'action', label: 'label', property: 'property', value: 1.5) end end - - describe '#self_describing_event' do - it 'does not send event to tracker' do - expect_any_instance_of(SnowplowTracker::Tracker).not_to receive(:track_self_describing_event) - - subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' }) - end - end end end diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb index 561edbd38f8..dacd08cf12b 100644 --- a/spec/lib/gitlab/tracking/standard_context_spec.rb +++ b/spec/lib/gitlab/tracking/standard_context_spec.rb @@ -58,10 +58,16 @@ RSpec.describe Gitlab::Tracking::StandardContext do end context 'with extra data' do - subject { described_class.new(foo: 'bar') } + subject { described_class.new(extra_key_1: 'extra value 1', extra_key_2: 'extra value 2') } - it 'creates a Snowplow context with the given data' do - expect(snowplow_context.to_json.dig(:data, :foo)).to eq('bar') + it 'includes extra data in `extra` hash' do + expect(snowplow_context.to_json.dig(:data, :extra)).to eq(extra_key_1: 'extra value 1', extra_key_2: 'extra value 2') + end + end + + context 'without extra data' do + it 'contains an empty `extra` hash' do + expect(snowplow_context.to_json.dig(:data, :extra)).to be_empty end end diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb index ac052bd7a80..4d856205609 100644 --- a/spec/lib/gitlab/tracking_spec.rb +++ b/spec/lib/gitlab/tracking_spec.rb @@ -36,12 +36,12 @@ RSpec.describe Gitlab::Tracking do end describe '.event' do - before do - allow_any_instance_of(Gitlab::Tracking::Destinations::Snowplow).to receive(:event) - allow_any_instance_of(Gitlab::Tracking::Destinations::ProductAnalytics).to receive(:event) - end - shared_examples 'delegates to destination' do |klass| + before do + allow_any_instance_of(Gitlab::Tracking::Destinations::Snowplow).to receive(:event) + allow_any_instance_of(Gitlab::Tracking::Destinations::ProductAnalytics).to receive(:event) + end + it "delegates to #{klass} destination" do other_context = double(:context) @@ -51,7 +51,7 @@ RSpec.describe Gitlab::Tracking do expect(Gitlab::Tracking::StandardContext) .to receive(:new) - .with(project: project, user: user, namespace: namespace) + .with(project: project, user: user, namespace: namespace, extra_key_1: 'extra value 1', extra_key_2: 'extra value 2') .and_call_original expect_any_instance_of(klass).to receive(:event) do |_, category, action, args| @@ -66,21 +66,21 @@ RSpec.describe Gitlab::Tracking do end described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5, - context: [other_context], project: project, user: user, namespace: namespace) + context: [other_context], project: project, user: user, namespace: namespace, + extra_key_1: 'extra value 1', extra_key_2: 'extra value 2') end end - include_examples 'delegates to destination', Gitlab::Tracking::Destinations::Snowplow - include_examples 'delegates to destination', Gitlab::Tracking::Destinations::ProductAnalytics - end + it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::Snowplow + it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::ProductAnalytics - describe '.self_describing_event' do - it 'delegates to snowplow destination' do - expect_any_instance_of(Gitlab::Tracking::Destinations::Snowplow) - .to receive(:self_describing_event) - .with('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' }, context: nil) + it 'tracks errors' do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with( + an_instance_of(ContractError), + snowplow_category: nil, snowplow_action: 'some_action' + ) - described_class.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' }) + described_class.event(nil, 'some_action') end end end diff --git a/spec/lib/gitlab/tree_summary_spec.rb b/spec/lib/gitlab/tree_summary_spec.rb index 661ef507a82..a86afa9cba5 100644 --- a/spec/lib/gitlab/tree_summary_spec.rb +++ b/spec/lib/gitlab/tree_summary_spec.rb @@ -226,6 +226,7 @@ RSpec.describe Gitlab::TreeSummary do describe 'References in commit messages' do let_it_be(:project) { create(:project, :empty_repo) } let_it_be(:issue) { create(:issue, project: project) } + let(:entries) { summary.summarize.first } let(:entry) { entries.find { |entry| entry[:file_name] == 'issue.txt' } } diff --git a/spec/lib/gitlab/untrusted_regexp_spec.rb b/spec/lib/gitlab/untrusted_regexp_spec.rb index aac3d5e27f5..270c4beec97 100644 --- a/spec/lib/gitlab/untrusted_regexp_spec.rb +++ b/spec/lib/gitlab/untrusted_regexp_spec.rb @@ -136,4 +136,22 @@ RSpec.describe Gitlab::UntrustedRegexp do end end end + + describe '#match' do + context 'when there are matches' do + it 'returns a match object' do + result = described_class.new('(?P<number>\d+)').match('hello 10') + + expect(result[:number]).to eq('10') + end + end + + context 'when there are no matches' do + it 'returns nil' do + result = described_class.new('(?P<number>\d+)').match('hello') + + expect(result).to be_nil + end + end + end end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 6d055fe3643..b359eb422d7 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -92,6 +92,7 @@ RSpec.describe Gitlab::UrlBuilder do context 'when passing a Snippet' do let_it_be(:personal_snippet) { create(:personal_snippet, :repository) } let_it_be(:project_snippet) { create(:project_snippet, :repository) } + let(:blob) { snippet.blobs.first } let(:ref) { blob.repository.root_ref } diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb index 8b592838f5d..e99d720058a 100644 --- a/spec/lib/gitlab/usage/metric_definition_spec.rb +++ b/spec/lib/gitlab/usage/metric_definition_spec.rb @@ -16,7 +16,8 @@ RSpec.describe Gitlab::Usage::MetricDefinition do time_frame: 'none', data_source: 'database', distribution: %w(ee ce), - tier: %w(free starter premium ultimate bronze silver gold) + tier: %w(free starter premium ultimate bronze silver gold), + name: 'count_boards' } end @@ -24,6 +25,13 @@ RSpec.describe Gitlab::Usage::MetricDefinition do let(:definition) { described_class.new(path, attributes) } let(:yaml_content) { attributes.deep_stringify_keys.to_yaml } + def write_metric(metric, path, content) + path = File.join(metric, path) + dir = File.dirname(path) + FileUtils.mkdir_p(dir) + File.write(path, content) + end + it 'has all definitons valid' do expect { described_class.definitions }.not_to raise_error(Gitlab::Usage::Metric::InvalidMetricError) end @@ -53,6 +61,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do :distribution | nil :distribution | 'test' :tier | %w(test ee) + :name | 'count_<adjective_describing>_boards' end with_them do @@ -82,6 +91,28 @@ RSpec.describe Gitlab::Usage::MetricDefinition do end end + describe 'statuses' do + using RSpec::Parameterized::TableSyntax + + where(:status, :skip_validation?) do + 'deprecated' | true + 'removed' | true + 'data_available' | false + 'implemented' | false + 'not_used' | false + end + + with_them do + subject(:validation) do + described_class.new(path, attributes.merge( { status: status } )).send(:skip_validation?) + end + + it 'returns true/false for skip_validation' do + expect(validation).to eq(skip_validation?) + end + end + end + describe '.load_all!' do let(:metric1) { Dir.mktmpdir('metric1') } let(:metric2) { Dir.mktmpdir('metric2') } @@ -121,12 +152,54 @@ RSpec.describe Gitlab::Usage::MetricDefinition do FileUtils.rm_rf(metric1) FileUtils.rm_rf(metric2) end + end + + describe 'dump_metrics_yaml' do + let(:other_attributes) do + { + description: 'Test metric definition', + value_type: 'string', + product_category: 'collection', + product_stage: 'growth', + status: 'data_available', + default_generation: 'generation_1', + key_path: 'counter.category.event', + product_group: 'group::product analytics', + time_frame: 'none', + data_source: 'database', + distribution: %w(ee ce), + tier: %w(free starter premium ultimate bronze silver gold) + } + end + + let(:other_yaml_content) { other_attributes.deep_stringify_keys.to_yaml } + let(:other_path) { File.join('metrics', 'test_metric.yml') } + let(:metric1) { Dir.mktmpdir('metric1') } + let(:metric2) { Dir.mktmpdir('metric2') } + + before do + allow(described_class).to receive(:paths).and_return( + [ + File.join(metric1, '**', '*.yml'), + File.join(metric2, '**', '*.yml') + ] + ) + # Reset memoized `definitions` result + described_class.instance_variable_set(:@definitions, nil) + end + + after do + FileUtils.rm_rf(metric1) + FileUtils.rm_rf(metric2) + end + + subject { described_class.dump_metrics_yaml } + + it 'returns a YAML with both metrics in a sequence' do + write_metric(metric1, path, yaml_content) + write_metric(metric2, other_path, other_yaml_content) - def write_metric(metric, path, content) - path = File.join(metric, path) - dir = File.dirname(path) - FileUtils.mkdir_p(dir) - File.write(path, content) + is_expected.to eq([attributes, other_attributes].map(&:deep_stringify_keys).to_yaml) end end end diff --git a/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb index a2a40f17269..db878828cd6 100644 --- a/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb +++ b/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::PostgresHll, :clean_ let_it_be(:end_date) { Date.current } let_it_be(:recorded_at) { Time.current } let_it_be(:time_period) { { created_at: (start_date..end_date) } } + let(:metric_1) { 'metric_1' } let(:metric_2) { 'metric_2' } let(:metric_names) { [metric_1, metric_2] } diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb index cd0413feab4..34b073b4729 100644 --- a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb +++ b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do describe '#generate' do shared_examples 'name suggestion' do it 'return correct name' do - expect(described_class.generate(key_path)).to eq name_suggestion + expect(described_class.generate(key_path)).to match name_suggestion end end @@ -20,7 +20,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do it_behaves_like 'name suggestion' do # corresponding metric is collected with count(Board) let(:key_path) { 'counts.boards' } - let(:name_suggestion) { 'count_boards' } + let(:name_suggestion) { /count_boards/ } end end @@ -28,7 +28,44 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do it_behaves_like 'name suggestion' do # corresponding metric is collected with distinct_count(ZoomMeeting, :issue_id) let(:key_path) { 'counts.issues_using_zoom_quick_actions' } - let(:name_suggestion) { 'count_distinct_issue_id_from_zoom_meetings' } + let(:name_suggestion) { /count_distinct_issue_id_from_zoom_meetings/ } + end + end + + context 'joined relations' do + context 'counted attribute comes from joined relation' do + it_behaves_like 'name suggestion' do + # corresponding metric is collected with: + # distinct_count( + # ::Clusters::Applications::Ingress.modsecurity_enabled.logging + # .joins(cluster: :deployments) + # .merge(::Clusters::Cluster.enabled) + # .merge(Deployment.success), + # ::Deployment.arel_table[:environment_id] + # ) + let(:key_path) { 'counts.ingress_modsecurity_logging' } + let(:name_suggestion) do + constrains = /'\(clusters_applications_ingress\.modsecurity_enabled = TRUE AND clusters_applications_ingress\.modsecurity_mode = \d+ AND clusters.enabled = TRUE AND deployments.status = \d+\)'/ + /count_distinct_environment_id_from_<adjective describing\: #{constrains}>_deployments_<with>_<adjective describing\: #{constrains}>_clusters_<having>_<adjective describing\: #{constrains}>_clusters_applications_ingress/ + end + end + end + + context 'counted attribute comes from source relation' do + it_behaves_like 'name suggestion' do + # corresponding metric is collected with count(Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot), start: issue_minimum_id, finish: issue_maximum_id) + let(:key_path) { 'counts.issues_created_manually_from_alerts' } + let(:name_suggestion) { /count_<adjective describing\: '\(issues\.author_id != \d+\)'>_issues_<with>_alert_management_alerts/ } + end + end + end + + context 'strips off time period constraint' do + it_behaves_like 'name suggestion' do + # corresponding metric is collected with distinct_count(::Clusters::Cluster.aws_installed.enabled.where(time_period), :user_id) + let(:key_path) { 'usage_activity_by_stage_monthly.configure.clusters_platforms_eks' } + let(:constraints) { /<adjective describing\: '\(clusters.provider_type = \d+ AND \(cluster_providers_aws\.status IN \(\d+\)\) AND clusters\.enabled = TRUE\)'>/ } + let(:name_suggestion) { /count_distinct_user_id_from_#{constraints}_clusters_<with>_#{constraints}_cluster_providers_aws/ } end end @@ -36,7 +73,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do it_behaves_like 'name suggestion' do # corresponding metric is collected with sum(JiraImportState.finished, :imported_issues_count) let(:key_path) { 'counts.jira_imports_total_imported_issues_count' } - let(:name_suggestion) { "sum_imported_issues_count_from_<adjective describing: '(jira_imports.status = 4)'>_jira_imports" } + let(:name_suggestion) { /sum_imported_issues_count_from_<adjective describing\: '\(jira_imports\.status = \d+\)'>_jira_imports/ } end end @@ -44,7 +81,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do it_behaves_like 'name suggestion' do # corresponding metric is collected with add(data[:personal_snippets], data[:project_snippets]) let(:key_path) { 'counts.snippets' } - let(:name_suggestion) { "add_count_<adjective describing: '(snippets.type = 'PersonalSnippet')'>_snippets_and_count_<adjective describing: '(snippets.type = 'ProjectSnippet')'>_snippets" } + let(:name_suggestion) { /add_count_<adjective describing\: '\(snippets\.type = 'PersonalSnippet'\)'>_snippets_and_count_<adjective describing\: '\(snippets\.type = 'ProjectSnippet'\)'>_snippets/ } end end @@ -52,7 +89,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do it_behaves_like 'name suggestion' do # corresponding metric is collected with redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics) } let(:key_path) { 'analytics_unique_visits.analytics_unique_visits_for_any_target' } - let(:name_suggestion) { '<please fill metric name>' } + let(:name_suggestion) { /<please fill metric name, suggested format is: {subject}_{verb}{ing|ed}_{object} eg: users_creating_epics or merge_requests_viewed_in_single_file_mode>/ } end end @@ -60,7 +97,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do it_behaves_like 'name suggestion' do # corresponding metric is collected with alt_usage_data(fallback: nil) { operating_system } let(:key_path) { 'settings.operating_system' } - let(:name_suggestion) { '<please fill metric name>' } + let(:name_suggestion) { /<please fill metric name>/ } end end end diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb new file mode 100644 index 00000000000..fb3bd564e34 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::RelationParsers::Joins do + describe '#accept' do + let(:collector) { Arel::Collectors::SubstituteBinds.new(ActiveRecord::Base.connection, Arel::Collectors::SQLString.new) } + + context 'with join added via string' do + it 'collects join parts' do + arel = Issue.joins('LEFT JOIN projects ON projects.id = issue.project_id') + + arel = arel.arel + result = described_class.new(ApplicationRecord.connection).accept(arel) + + expect(result).to match_array [{ source: "projects", constraints: "projects.id = issue.project_id" }] + end + end + + context 'with join added via arel node' do + it 'collects join parts' do + source_table = Arel::Table.new('records') + joined_table = Arel::Table.new('joins') + second_level_joined_table = Arel::Table.new('second_level_joins') + + arel = source_table + .from + .project(source_table['id'].count) + .join(joined_table, Arel::Nodes::OuterJoin) + .on(source_table[:id].eq(joined_table[:records_id])) + .join(second_level_joined_table, Arel::Nodes::OuterJoin) + .on(joined_table[:id].eq(second_level_joined_table[:joins_id])) + + result = described_class.new(ApplicationRecord.connection).accept(arel) + + expect(result).to match_array [{ source: "joins", constraints: "records.id = joins.records_id" }, { source: "second_level_joins", constraints: "joins.id = second_level_joins.joins_id" }] + end + end + end +end diff --git a/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb b/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb deleted file mode 100644 index 9aba86cdaf2..00000000000 --- a/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'aggregated metrics' do - RSpec::Matchers.define :be_known_event do - match do |event| - Gitlab::UsageDataCounters::HLLRedisCounter.known_event?(event) - end - - failure_message do |event| - "Event with name: `#{event}` can not be found within `#{Gitlab::UsageDataCounters::HLLRedisCounter::KNOWN_EVENTS_PATH}`" - end - end - - RSpec::Matchers.define :has_known_source do - match do |aggregate| - Gitlab::Usage::Metrics::Aggregates::SOURCES.include?(aggregate[:source]) - end - - failure_message do |aggregate| - "Aggregate with name: `#{aggregate[:name]}` uses not allowed source `#{aggregate[:source]}`" - end - end - - RSpec::Matchers.define :have_known_time_frame do - allowed_time_frames = [ - Gitlab::Utils::UsageData::ALL_TIME_TIME_FRAME_NAME, - Gitlab::Utils::UsageData::TWENTY_EIGHT_DAYS_TIME_FRAME_NAME, - Gitlab::Utils::UsageData::SEVEN_DAYS_TIME_FRAME_NAME - ] - - match do |aggregate| - (aggregate[:time_frame] - allowed_time_frames).empty? - end - - failure_message do |aggregate| - "Aggregate with name: `#{aggregate[:name]}` uses not allowed time_frame`#{aggregate[:time_frame] - allowed_time_frames}`" - end - end - - let_it_be(:known_events) do - Gitlab::UsageDataCounters::HLLRedisCounter.known_events - end - - Gitlab::Usage::Metrics::Aggregates::Aggregate.new(Time.current).send(:aggregated_metrics).tap do |aggregated_metrics| - it 'all events has unique name' do - event_names = aggregated_metrics&.map { |event| event[:name] } - - expect(event_names).to eq(event_names&.uniq) - end - - it 'all aggregated metrics has known source' do - expect(aggregated_metrics).to all has_known_source - end - - it 'all aggregated metrics has known source' do - expect(aggregated_metrics).to all have_known_time_frame - end - - aggregated_metrics&.select { |agg| agg[:source] == Gitlab::Usage::Metrics::Aggregates::REDIS_SOURCE }&.each do |aggregate| - context "for #{aggregate[:name]} aggregate of #{aggregate[:events].join(' ')}" do - let_it_be(:events_records) { known_events.select { |event| aggregate[:events].include?(event[:name]) } } - - it "does not include 'all' time frame for Redis sourced aggregate" do - expect(aggregate[:time_frame]).not_to include(Gitlab::Utils::UsageData::ALL_TIME_TIME_FRAME_NAME) - end - - it "only refers to known events" do - expect(aggregate[:events]).to all be_known_event - end - - it "has expected structure" do - expect(aggregate.keys).to include(*%w[name operator events]) - end - - it "uses allowed aggregation operators" do - expect(Gitlab::Usage::Metrics::Aggregates::ALLOWED_METRICS_AGGREGATIONS).to include aggregate[:operator] - end - - it "uses events from the same Redis slot" do - event_slots = events_records.map { |event| event[:redis_slot] }.uniq - - expect(event_slots).to contain_exactly(be_present) - end - - it "uses events with the same aggregation period" do - event_slots = events_records.map { |event| event[:aggregation] }.uniq - - expect(event_slots).to contain_exactly(be_present) - end - end - end - end -end diff --git a/spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb b/spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb index 664e7938a7e..a1dee442131 100644 --- a/spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' # If this spec fails, we need to add the new code review event to the correct aggregated metric RSpec.describe 'Code review events' do it 'the aggregated metrics contain all the code review metrics' do - path = Rails.root.join('lib/gitlab/usage_data_counters/aggregated_metrics/code_review.yml') + path = Rails.root.join('config/metrics/aggregates/code_review.yml') aggregated_events = YAML.safe_load(File.read(path), aliases: true)&.map(&:with_indifferent_access) code_review_aggregated_events = aggregated_events 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 d12dcdae955..9fc28f6c4ec 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 @@ -34,6 +34,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'source_code', 'incident_management', 'incident_management_alerts', + 'incident_management_oncall', 'testing', 'issues_edit', 'ci_secrets_management', @@ -43,7 +44,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'ci_templates', 'quickactions', 'pipeline_authoring', - 'epics_usage' + 'epics_usage', + 'secure' ) end end @@ -93,7 +95,25 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s end describe '.track_event' do - context 'with feature flag set' do + context 'with redis_hll_tracking' do + it 'tracks the event when feature enabled' do + stub_feature_flags(redis_hll_tracking: true) + + expect(Gitlab::Redis::HLL).to receive(:add) + + described_class.track_event(weekly_event, values: 1) + end + + it 'does not track the event with feature flag disabled' do + stub_feature_flags(redis_hll_tracking: false) + + expect(Gitlab::Redis::HLL).not_to receive(:add) + + described_class.track_event(weekly_event, values: 1) + end + end + + context 'with event feature flag set' do it 'tracks the event when feature enabled' do stub_feature_flags(feature => true) @@ -111,7 +131,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s end end - context 'with no feature flag set' do + context 'with no event feature flag set' do it 'tracks the event' do expect(Gitlab::Redis::HLL).to receive(:add) @@ -289,6 +309,11 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s described_class.track_event(daily_event, values: entity4, time: 29.days.ago) end + it 'returns 0 if there are no keys for the given events' do + expect(Gitlab::Redis::HLL).not_to receive(:count) + expect(described_class.unique_events(event_names: [weekly_event], start_date: Date.current, end_date: 4.weeks.ago)).to eq(-1) + end + it 'raise error if metrics are not in the same slot' do expect do described_class.unique_events(event_names: [compliance_slot_event, analytics_slot_event], start_date: 4.weeks.ago, end_date: Date.current) @@ -508,6 +533,11 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s expect { described_class.calculate_events_union(**time_range.merge(event_names: %w[event1_slot event4])) }.to raise_error described_class::SlotMismatch expect { described_class.calculate_events_union(**time_range.merge(event_names: %w[event5_slot event3_slot])) }.to raise_error described_class::AggregationMismatch end + + it 'returns 0 if there are no keys for given events' do + expect(Gitlab::Redis::HLL).not_to receive(:count) + expect(described_class.calculate_events_union(event_names: %w[event1_slot event2_slot event3_slot], start_date: Date.current, end_date: 4.weeks.ago)).to eq(-1) + end end describe '.weekly_time_range' do 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 f8f6494b92e..1b73e5269d7 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 @@ -3,9 +3,10 @@ require 'spec_helper' RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_gitlab_redis_shared_state do - let(:user1) { build(:user, id: 1) } - let(:user2) { build(:user, id: 2) } - let(:user3) { build(:user, id: 3) } + let_it_be(:user1) { build(:user, id: 1) } + let_it_be(:user2) { build(:user, id: 2) } + let_it_be(:user3) { build(:user, id: 3) } + let(:time) { Time.zone.now } context 'for Issue title edit actions' do @@ -272,10 +273,13 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git 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: user2, time: time - 2.days) - described_class.track_issue_title_changed_action(author: user3, time: time - 3.days) - described_class.track_issue_description_changed_action(author: user3, time: time - 3.days) - described_class.track_issue_assignee_changed_action(author: user3, time: time - 3.days) + + 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) + end events = Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(described_class::ISSUE_CATEGORY) today_count = Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: events, start_date: time, end_date: time) diff --git a/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb index 2df0f331f73..1940442d2ad 100644 --- a/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb @@ -115,6 +115,26 @@ RSpec.describe Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter, :cle end end + context 'tracking spent' do + let(:quickaction_name) { 'spent' } + + context 'adding time' do + let(:args) { '1d' } + + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_spend_add' } + end + end + + context 'removing time' do + let(:args) { '-1d' } + + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_spend_subtract' } + end + end + end + context 'tracking unassign' do let(:quickaction_name) { 'unassign' } diff --git a/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb b/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb new file mode 100644 index 00000000000..32d1288c59c --- /dev/null +++ b/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataNonSqlMetrics do + let(:default_count) { Gitlab::UsageDataNonSqlMetrics::SQL_METRIC_DEFAULT } + + describe '.count' do + it 'returns default value for count' do + expect(described_class.count(User)).to eq(default_count) + end + end + + describe '.distinct_count' do + it 'returns default value for distinct count' do + expect(described_class.distinct_count(User)).to eq(default_count) + end + end + + describe '.estimate_batch_distinct_count' do + it 'returns default value for estimate_batch_distinct_count' do + expect(described_class.estimate_batch_distinct_count(User)).to eq(default_count) + end + end + + describe '.sum' do + it 'returns default value for sum' do + expect(described_class.sum(JiraImportState.finished, :imported_issues_count)).to eq(default_count) + end + end + + describe '.histogram' do + it 'returns default value for histogram' do + expect(described_class.histogram(JiraImportState.finished, :imported_issues_count, buckets: [], bucket_size: 0)).to eq(default_count) + end + end + + describe 'min/max methods' do + using RSpec::Parameterized::TableSyntax + + where(:model, :result) do + User | nil + Issue | nil + Deployment | nil + Project | nil + end + + with_them do + it 'returns nil' do + expect(described_class.minimum_id(model)).to eq(result) + expect(described_class.maximum_id(model)).to eq(result) + end + end + end +end diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb index 12eac643383..718ab3b2d95 100644 --- a/spec/lib/gitlab/usage_data_queries_spec.rb +++ b/spec/lib/gitlab/usage_data_queries_spec.rb @@ -11,12 +11,24 @@ RSpec.describe Gitlab::UsageDataQueries do it 'returns the raw SQL' do expect(described_class.count(User)).to start_with('SELECT COUNT("users"."id") FROM "users"') end + + it 'does not mix a nil column with keyword arguments' do + expect(described_class).to receive(:raw_sql).with(User, nil) + + described_class.count(User, start: 1, finish: 2) + end end describe '.distinct_count' do it 'returns the raw SQL' do expect(described_class.distinct_count(Issue, :author_id)).to eq('SELECT COUNT(DISTINCT "issues"."author_id") FROM "issues"') end + + it 'does not mix a nil column with keyword arguments' do + expect(described_class).to receive(:raw_sql).with(Issue, nil, :distinct) + + described_class.distinct_count(Issue, nil, start: 1, finish: 2) + end end describe '.redis_usage_data' do @@ -46,4 +58,24 @@ RSpec.describe Gitlab::UsageDataQueries do .to eq('SELECT (SELECT COUNT("users"."id") FROM "users") + (SELECT COUNT("issues"."id") FROM "issues")') end end + + describe 'min/max methods' do + it 'returns nil' do + # user min/max + expect(described_class.minimum_id(User)).to eq(nil) + expect(described_class.maximum_id(User)).to eq(nil) + + # issue min/max + expect(described_class.minimum_id(Issue)).to eq(nil) + expect(described_class.maximum_id(Issue)).to eq(nil) + + # deployment min/max + expect(described_class.minimum_id(Deployment)).to eq(nil) + expect(described_class.maximum_id(Deployment)).to eq(nil) + + # project min/max + expect(described_class.minimum_id(Project)).to eq(nil) + expect(described_class.maximum_id(Project)).to eq(nil) + end + end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index b1581bf02a6..01701f7aebd 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -167,7 +167,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do create(:key, user: user) create(:project, creator: user, disable_overriding_approvers_per_merge_request: true) create(:project, creator: user, disable_overriding_approvers_per_merge_request: false) - create(:remote_mirror, project: project) + create(:remote_mirror, project: project, enabled: true) + another_user = create(:user) + another_project = create(:project, :repository, creator: another_user) + create(:remote_mirror, project: another_project, enabled: false) create(:snippet, author: user) end @@ -176,7 +179,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do keys: 2, merge_requests: 2, projects_with_disable_overriding_approvers_per_merge_request: 2, - projects_without_disable_overriding_approvers_per_merge_request: 4, + projects_without_disable_overriding_approvers_per_merge_request: 6, remote_mirrors: 2, snippets: 2 ) @@ -185,7 +188,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do keys: 1, merge_requests: 1, projects_with_disable_overriding_approvers_per_merge_request: 1, - projects_without_disable_overriding_approvers_per_merge_request: 2, + projects_without_disable_overriding_approvers_per_merge_request: 3, remote_mirrors: 1, snippets: 1 ) @@ -1288,6 +1291,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do 'p_analytics_repo' => 123, 'i_analytics_cohorts' => 123, 'i_analytics_dev_ops_score' => 123, + 'i_analytics_dev_ops_adoption' => 123, 'i_analytics_instance_statistics' => 123, 'p_analytics_merge_request' => 123, 'g_analytics_merge_request' => 123, @@ -1358,24 +1362,36 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories } let(:ineligible_total_categories) do - %w[source_code ci_secrets_management incident_management_alerts snippets terraform epics_usage] + %w[source_code ci_secrets_management incident_management_alerts snippets terraform incident_management_oncall secure] end - it 'has all known_events' do - expect(subject).to have_key(:redis_hll_counters) + context 'with redis_hll_tracking feature enabled' do + it 'has all known_events' do + stub_feature_flags(redis_hll_tracking: true) - expect(subject[:redis_hll_counters].keys).to match_array(categories) + expect(subject).to have_key(:redis_hll_counters) - categories.each do |category| - keys = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(category) + expect(subject[:redis_hll_counters].keys).to match_array(categories) - metrics = keys.map { |key| "#{key}_weekly" } + keys.map { |key| "#{key}_monthly" } + categories.each do |category| + keys = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(category) - if ineligible_total_categories.exclude?(category) - metrics.append("#{category}_total_unique_counts_weekly", "#{category}_total_unique_counts_monthly") + metrics = keys.map { |key| "#{key}_weekly" } + keys.map { |key| "#{key}_monthly" } + + if ineligible_total_categories.exclude?(category) + metrics.append("#{category}_total_unique_counts_weekly", "#{category}_total_unique_counts_monthly") + end + + expect(subject[:redis_hll_counters][category].keys).to match_array(metrics) end + end + end + + context 'with redis_hll_tracking disabled' do + it 'does not have redis_hll_tracking key' do + stub_feature_flags(redis_hll_tracking: false) - expect(subject[:redis_hll_counters][category].keys).to match_array(metrics) + expect(subject).not_to have_key(:redis_hll_counters) end end end diff --git a/spec/lib/gitlab/utils/lazy_attributes_spec.rb b/spec/lib/gitlab/utils/lazy_attributes_spec.rb index dfffe70defb..1ebc9b0d711 100644 --- a/spec/lib/gitlab/utils/lazy_attributes_spec.rb +++ b/spec/lib/gitlab/utils/lazy_attributes_spec.rb @@ -13,8 +13,10 @@ RSpec.describe Gitlab::Utils::LazyAttributes do def initialize @number = -> { 1 } - @reader_1, @reader_2 = 'reader_1', -> { 'reader_2' } - @incorrect_type, @accessor_2 = -> { :incorrect_type }, -> { 'accessor_2' } + @reader_1 = 'reader_1' + @reader_2 = -> { 'reader_2' } + @incorrect_type = -> { :incorrect_type } + @accessor_2 = -> { 'accessor_2' } end end end diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb index 6e1904c43e1..11b2a12f228 100644 --- a/spec/lib/gitlab/utils/usage_data_spec.rb +++ b/spec/lib/gitlab/utils/usage_data_spec.rb @@ -187,6 +187,7 @@ RSpec.describe Gitlab::Utils::UsageData do describe '#histogram' do let_it_be(:projects) { create_list(:project, 3) } + let(:project1) { projects.first } let(:project2) { projects.second } let(:project3) { projects.third } @@ -478,4 +479,22 @@ RSpec.describe Gitlab::Utils::UsageData do expect { described_class.track_usage_event(unknown_event, value) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent) end end + + describe 'min/max' do + let(:model) { double(:relation) } + + it 'returns min from the model' do + allow(model).to receive(:minimum).and_return(2) + allow(model).to receive(:name).and_return('sample_min_model') + + expect(described_class.minimum_id(model)).to eq(2) + end + + it 'returns max from the model' do + allow(model).to receive(:maximum).and_return(100) + allow(model).to receive(:name).and_return('sample_max_model') + + expect(described_class.maximum_id(model)).to eq(100) + end + end end diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 665eebdfd9e..11dba610faf 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -162,7 +162,7 @@ RSpec.describe Gitlab::Utils do describe '.nlbr' do it 'replaces new lines with <br>' do - expect(described_class.nlbr("<b>hello</b>\n<i>world</i>".freeze)).to eq("hello<br>world") + expect(described_class.nlbr("<b>hello</b>\n<i>world</i>")).to eq("hello<br>world") end end @@ -192,6 +192,7 @@ RSpec.describe Gitlab::Utils do expect(to_boolean('YeS')).to be(true) expect(to_boolean('t')).to be(true) expect(to_boolean('1')).to be(true) + expect(to_boolean(1)).to be(true) expect(to_boolean('ON')).to be(true) expect(to_boolean('FaLse')).to be(false) @@ -199,6 +200,7 @@ RSpec.describe Gitlab::Utils do expect(to_boolean('NO')).to be(false) expect(to_boolean('n')).to be(false) expect(to_boolean('0')).to be(false) + expect(to_boolean(0)).to be(false) expect(to_boolean('oFF')).to be(false) end @@ -388,8 +390,8 @@ RSpec.describe Gitlab::Utils do describe ".safe_downcase!" do where(:str, :result) do - "test".freeze | "test" - "Test".freeze | "test" + "test" | "test" + "Test" | "test" "test" | "test" "Test" | "test" end diff --git a/spec/lib/gitlab/web_ide/config/entry/global_spec.rb b/spec/lib/gitlab/web_ide/config/entry/global_spec.rb index 3e29bf89785..8dbe64af1c7 100644 --- a/spec/lib/gitlab/web_ide/config/entry/global_spec.rb +++ b/spec/lib/gitlab/web_ide/config/entry/global_spec.rb @@ -83,6 +83,7 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Global do expect(global.terminal_value).to eq({ tag_list: [], yaml_variables: [], + job_variables: [], options: { before_script: ['ls'], script: ['sleep 10s'], 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 0df0f56f440..d6d0fc4224d 100644 --- a/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb +++ b/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb @@ -132,7 +132,7 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Terminal do { before_script: %w[ls pwd], script: 'sleep 100', tags: ['webide'], - image: 'ruby:2.5', + image: 'ruby:3.0', services: ['mysql'], variables: { KEY: 'value' } } end @@ -142,8 +142,9 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Terminal do .to eq( tag_list: ['webide'], yaml_variables: [{ key: 'KEY', value: 'value', public: true }], + job_variables: [{ key: 'KEY', value: 'value', public: true }], options: { - image: { name: "ruby:2.5" }, + image: { name: "ruby:3.0" }, services: [{ name: "mysql" }], before_script: %w[ls pwd], script: ['sleep 100'] diff --git a/spec/lib/gitlab/word_diff/chunk_collection_spec.rb b/spec/lib/gitlab/word_diff/chunk_collection_spec.rb index aa837f760c1..73e9ff3974a 100644 --- a/spec/lib/gitlab/word_diff/chunk_collection_spec.rb +++ b/spec/lib/gitlab/word_diff/chunk_collection_spec.rb @@ -41,4 +41,27 @@ RSpec.describe Gitlab::WordDiff::ChunkCollection do expect(collection.content).to eq('') end end + + describe '#marker_ranges' do + let(:chunks) do + [ + Gitlab::WordDiff::Segments::Chunk.new(' Hello '), + Gitlab::WordDiff::Segments::Chunk.new('-World'), + Gitlab::WordDiff::Segments::Chunk.new('+GitLab'), + Gitlab::WordDiff::Segments::Chunk.new('+!!!') + ] + end + + it 'returns marker ranges for every chunk with changes' do + chunks.each { |chunk| collection.add(chunk) } + + expect(collection.marker_ranges).to eq( + [ + Gitlab::MarkerRange.new(6, 10, mode: :deletion), + Gitlab::MarkerRange.new(11, 16, mode: :addition), + Gitlab::MarkerRange.new(17, 19, mode: :addition) + ] + ) + end + end end diff --git a/spec/lib/gitlab/word_diff/parser_spec.rb b/spec/lib/gitlab/word_diff/parser_spec.rb index 3aeefb57a02..e793e44fd45 100644 --- a/spec/lib/gitlab/word_diff/parser_spec.rb +++ b/spec/lib/gitlab/word_diff/parser_spec.rb @@ -36,15 +36,26 @@ RSpec.describe Gitlab::WordDiff::Parser do aggregate_failures do expect(diff_lines.count).to eq(7) - expect(diff_lines.map(&:to_hash)).to match_array( + expect(diff_lines.map { |line| diff_line_attributes(line) }).to eq( [ - a_hash_including(index: 0, old_pos: 1, new_pos: 1, text: '', type: nil), - a_hash_including(index: 1, old_pos: 2, new_pos: 2, text: 'Unchanged line', type: nil), - a_hash_including(index: 2, old_pos: 3, new_pos: 3, text: '', type: nil), - a_hash_including(index: 3, old_pos: 4, new_pos: 4, text: 'Old changeNew addition unchanged content', type: nil), - a_hash_including(index: 4, old_pos: 50, new_pos: 50, text: '@@ -50,14 +50,13 @@', type: 'match'), - a_hash_including(index: 5, old_pos: 50, new_pos: 50, text: 'First change same same same_removed_added_end of the line', type: nil), - a_hash_including(index: 6, old_pos: 51, new_pos: 51, text: '', type: nil) + { index: 0, old_pos: 1, new_pos: 1, text: '', type: nil, marker_ranges: [] }, + { 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) + ] }, + + { 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) + ] }, + + { index: 6, old_pos: 51, new_pos: 51, text: '', type: nil, marker_ranges: [] } ] ) end @@ -64,4 +75,17 @@ RSpec.describe Gitlab::WordDiff::Parser do it { is_expected.to eq([]) } end end + + private + + def diff_line_attributes(diff_line) + { + index: diff_line.index, + old_pos: diff_line.old_pos, + new_pos: diff_line.new_pos, + text: diff_line.text, + type: diff_line.type, + marker_ranges: diff_line.marker_ranges + } + end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index c22df5dd063..d40ecc7e04e 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Workhorse do let_it_be(:project) { create(:project, :repository) } + let(:repository) { project.repository } def decode_workhorse_header(array) diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index c5738ae730f..4df00eaa439 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -247,75 +247,117 @@ RSpec.describe Gitlab do end end - describe '.ee?' do + describe 'ee? and jh?' do before do - stub_env('FOSS_ONLY', nil) # Make sure the ENV is clean + # Make sure the ENV is clean + stub_env('FOSS_ONLY', nil) + stub_env('EE_ONLY', nil) + described_class.instance_variable_set(:@is_ee, nil) + described_class.instance_variable_set(:@is_jh, nil) end after do described_class.instance_variable_set(:@is_ee, nil) + described_class.instance_variable_set(:@is_jh, nil) end - context 'for EE' do - before do - root = Pathname.new('dummy') - license_path = double(:path, exist?: true) + def stub_path(*paths, **arguments) + root = Pathname.new('dummy') + pathname = double(:path, **arguments) - allow(described_class) - .to receive(:root) - .and_return(root) + allow(described_class) + .to receive(:root) + .and_return(root) + allow(root).to receive(:join) + + paths.each do |path| allow(root) .to receive(:join) - .with('ee/app/models/license.rb') - .and_return(license_path) + .with(path) + .and_return(pathname) end + end - context 'when using FOSS_ONLY=1' do + describe '.ee?' do + context 'for EE' do before do - stub_env('FOSS_ONLY', '1') + stub_path('ee/app/models/license.rb', exist?: true) end - it 'returns not to be EE' do - expect(described_class).not_to be_ee + context 'when using FOSS_ONLY=1' do + before do + stub_env('FOSS_ONLY', '1') + end + + it 'returns not to be EE' do + expect(described_class).not_to be_ee + end end - end - context 'when using FOSS_ONLY=0' do - before do - stub_env('FOSS_ONLY', '0') + context 'when using FOSS_ONLY=0' do + before do + stub_env('FOSS_ONLY', '0') + end + + it 'returns to be EE' do + expect(described_class).to be_ee + end end - it 'returns to be EE' do - expect(described_class).to be_ee + context 'when using default FOSS_ONLY' do + it 'returns to be EE' do + expect(described_class).to be_ee + end end end - context 'when using default FOSS_ONLY' do - it 'returns to be EE' do - expect(described_class).to be_ee + context 'for CE' do + before do + stub_path('ee/app/models/license.rb', exist?: false) + end + + it 'returns not to be EE' do + expect(described_class).not_to be_ee end end end - context 'for CE' do - before do - root = double(:path) - license_path = double(:path, exists?: false) + describe '.jh?' do + context 'for JH' do + before do + stub_path( + 'ee/app/models/license.rb', + 'jh', + exist?: true) + end - allow(described_class) - .to receive(:root) - .and_return(Pathname.new('dummy')) + context 'when using default FOSS_ONLY and EE_ONLY' do + it 'returns to be JH' do + expect(described_class).to be_jh + end + end - allow(root) - .to receive(:join) - .with('ee/app/models/license.rb') - .and_return(license_path) - end + context 'when using FOSS_ONLY=1' do + before do + stub_env('FOSS_ONLY', '1') + end + + it 'returns not to be JH' do + expect(described_class).not_to be_jh + end + end + + context 'when using EE_ONLY=1' do + before do + stub_env('EE_ONLY', '1') + end - it 'returns not to be EE' do - expect(described_class).not_to be_ee + it 'returns not to be JH' do + expect(described_class).not_to be_jh + end + end end end end diff --git a/spec/lib/kramdown/kramdown_spec.rb b/spec/lib/kramdown/kramdown_spec.rb new file mode 100644 index 00000000000..986a8d9959e --- /dev/null +++ b/spec/lib/kramdown/kramdown_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Ensure kramdown detects invalid syntax highlighting formatters' do + subject { Kramdown::Document.new(options + "\n" + code).to_html } + + let(:code) do + <<-RUBY +~~~ ruby + def what? + 42 + end +~~~ + RUBY + end + + context 'with invalid formatter' do + let(:options) { %({::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: CSV, line_numbers: true\\}" /}) } + + it 'falls back to standard HTML and disallows CSV' do + expect(CSV).not_to receive(:new) + expect(::Rouge::Formatters::HTML).to receive(:new).and_call_original + + expect(subject).to be_present + end + end + + context 'with valid formatter' do + let(:options) { %({::options auto_ids="false" footnote_nr="5" syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: HTMLLegacy\\}" /}) } + + it 'allows formatter' do + expect(::Rouge::Formatters::HTMLLegacy).to receive(:new).and_call_original + + expect(subject).to be_present + end + end +end diff --git a/spec/lib/marginalia_spec.rb b/spec/lib/marginalia_spec.rb index 2ee27fbe20c..040f70236c6 100644 --- a/spec/lib/marginalia_spec.rb +++ b/spec/lib/marginalia_spec.rb @@ -3,18 +3,28 @@ require 'spec_helper' RSpec.describe 'Marginalia spec' do - class MarginaliaTestController < ActionController::Base + class MarginaliaTestController < ApplicationController + skip_before_action :authenticate_user!, :check_two_factor_requirement + def first_user User.first render body: nil end + + private + + [:auth_user, :current_user, :set_experimentation_subject_id_cookie, :signed_in?].each do |method| + define_method(method) { } + end end class MarginaliaTestJob include Sidekiq::Worker def perform - User.first + Gitlab::ApplicationContext.with_context(caller_id: self.class.name) do + User.first + end end end @@ -30,10 +40,9 @@ RSpec.describe 'Marginalia spec' do let(:component_map) do { - "application" => "test", - "controller" => "marginalia_test", - "action" => "first_user", - "correlation_id" => correlation_id + "application" => "test", + "endpoint_id" => "MarginaliaTestController#first_user", + "correlation_id" => correlation_id } end @@ -47,6 +56,7 @@ RSpec.describe 'Marginalia spec' do describe 'for Sidekiq worker jobs' do around do |example| with_sidekiq_server_middleware do |chain| + chain.add Labkit::Middleware::Sidekiq::Context::Server chain.add Marginalia::SidekiqInstrumentation::Middleware Marginalia.application_name = "sidekiq" example.run @@ -66,10 +76,10 @@ RSpec.describe 'Marginalia spec' do let(:component_map) do { - "application" => "sidekiq", - "job_class" => "MarginaliaTestJob", - "correlation_id" => sidekiq_job['correlation_id'], - "jid" => sidekiq_job['jid'] + "application" => "sidekiq", + "endpoint_id" => "MarginaliaTestJob", + "correlation_id" => sidekiq_job['correlation_id'], + "jid" => sidekiq_job['jid'] } end @@ -80,19 +90,33 @@ RSpec.describe 'Marginalia spec' do end describe 'for ActionMailer delivery jobs' do + # We need to ensure that this runs through Sidekiq to take + # advantage of the middleware. There is a Rails bug that means we + # have to do some extra steps to make this happen: + # https://github.com/rails/rails/issues/37270#issuecomment-553927324 + around do |example| + descendants = ActiveJob::Base.descendants + [ActiveJob::Base] + descendants.each(&:disable_test_adapter) + ActiveJob::Base.queue_adapter = :sidekiq + + example.run + + descendants.each { |a| a.queue_adapter = :test } + end + let(:delivery_job) { MarginaliaTestMailer.first_user.deliver_later } let(:recorded) do ActiveRecord::QueryRecorder.new do - delivery_job.perform_now + Sidekiq::Worker.drain_all end end let(:component_map) do { - "application" => "sidekiq", - "jid" => delivery_job.job_id, - "job_class" => delivery_job.arguments.first + "application" => "sidekiq", + "endpoint_id" => "ActionMailer::MailDeliveryJob", + "jid" => delivery_job.job_id } end diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb index 26d1ec32232..0f2711e0b11 100644 --- a/spec/lib/mattermost/command_spec.rb +++ b/spec/lib/mattermost/command_spec.rb @@ -19,7 +19,7 @@ RSpec.describe Mattermost::Command do trigger: 'gitlab' } end - subject { described_class.new(nil).create(params) } + subject { described_class.new(nil).create(params) } # rubocop:disable Rails/SaveBang context 'for valid trigger word' do before do diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index 93422b01ca7..67ccb48e3a7 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -39,7 +39,7 @@ RSpec.describe Mattermost::Session, type: :request do context 'with oauth_uri' do let!(:doorkeeper) do - Doorkeeper::Application.create( + Doorkeeper::Application.create!( name: 'GitLab Mattermost', redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete", scopes: '') diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index 0870114ca28..e3ef5ff5377 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -71,7 +71,7 @@ RSpec.describe Mattermost::Team do end describe '#create' do - subject { described_class.new(nil).create(name: "devteam", display_name: "Dev Team", type: "O") } + subject { described_class.new(nil).create(name: "devteam", display_name: "Dev Team", type: "O") } # rubocop:disable Rails/SaveBang context 'for a new team' do let(:response) do diff --git a/spec/lib/peek/views/active_record_spec.rb b/spec/lib/peek/views/active_record_spec.rb index dad5a2bf461..9eeeca4de61 100644 --- a/spec/lib/peek/views/active_record_spec.rb +++ b/spec/lib/peek/views/active_record_spec.rb @@ -5,14 +5,16 @@ require 'spec_helper' RSpec.describe Peek::Views::ActiveRecord, :request_store do subject { Peek.views.find { |v| v.instance_of?(Peek::Views::ActiveRecord) } } - let(:connection) { double(:connection) } + let(:connection_1) { double(:connection) } + let(:connection_2) { double(:connection) } + let(:connection_3) { double(:connection) } let(:event_1) do { name: 'SQL', sql: 'SELECT * FROM users WHERE id = 10', cached: false, - connection: connection + connection: connection_1 } end @@ -21,7 +23,7 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do name: 'SQL', sql: 'SELECT * FROM users WHERE id = 10', cached: true, - connection: connection + connection: connection_2 } end @@ -30,12 +32,15 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do name: 'SQL', sql: 'UPDATE users SET admin = true WHERE id = 10', cached: false, - connection: connection + connection: connection_3 } end before do allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true) + allow(connection_1).to receive(:transaction_open?).and_return(false) + allow(connection_2).to receive(:transaction_open?).and_return(false) + allow(connection_3).to receive(:transaction_open?).and_return(true) end it 'subscribes and store data into peek views' do @@ -46,22 +51,32 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do end expect(subject.results).to match( - calls: '3 (1 cached)', + calls: 3, + summary: { + "Cached" => 1, + "In a transaction" => 1 + }, duration: '6000.00ms', warnings: ["active-record duration: 6000.0 over 3000"], details: contain_exactly( a_hash_including( + start: be_a(Time), cached: '', + transaction: '', duration: 1000.0, sql: 'SELECT * FROM users WHERE id = 10' ), a_hash_including( - cached: 'cached', + start: be_a(Time), + cached: 'Cached', + transaction: '', duration: 2000.0, sql: 'SELECT * FROM users WHERE id = 10' ), a_hash_including( + start: be_a(Time), cached: '', + transaction: 'In a transaction', duration: 3000.0, sql: 'UPDATE users SET admin = true WHERE id = 10' ) diff --git a/spec/lib/peek/views/external_http_spec.rb b/spec/lib/peek/views/external_http_spec.rb index 98c4f771f33..18ae1326493 100644 --- a/spec/lib/peek/views/external_http_spec.rb +++ b/spec/lib/peek/views/external_http_spec.rb @@ -11,6 +11,10 @@ RSpec.describe Peek::Views::ExternalHttp, :request_store do allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true) end + around do |example| + freeze_time { example.run } + end + let(:event_1) do { method: 'POST', code: "200", duration: 0.03, @@ -44,9 +48,9 @@ RSpec.describe Peek::Views::ExternalHttp, :request_store do end it 'returns aggregated results' do - subscriber.request(double(:event, payload: event_1)) - subscriber.request(double(:event, payload: event_2)) - subscriber.request(double(:event, payload: event_3)) + subscriber.request(double(:event, payload: event_1, time: Time.current)) + subscriber.request(double(:event, payload: event_2, time: Time.current)) + subscriber.request(double(:event, payload: event_3, time: Time.current)) results = subject.results expect(results[:calls]).to eq(3) @@ -55,6 +59,7 @@ RSpec.describe Peek::Views::ExternalHttp, :request_store do expected = [ { + start: be_like_time(Time.current), duration: 30.0, label: "POST https://gitlab.com:80/api/v4/projects?current=true", code: "Response status: 200", @@ -63,6 +68,7 @@ RSpec.describe Peek::Views::ExternalHttp, :request_store do warnings: [] }, { + start: be_like_time(Time.current), duration: 1300, label: "POST http://gitlab.com:80/api/v4/projects/2/issues?current=true", code: nil, @@ -71,6 +77,7 @@ RSpec.describe Peek::Views::ExternalHttp, :request_store do warnings: ["1300.0 over 100"] }, { + start: be_like_time(Time.current), duration: 5.0, label: "GET http://gitlab.com:80/api/v4/projects/2?current=true", code: "Response status: 301", @@ -81,7 +88,7 @@ RSpec.describe Peek::Views::ExternalHttp, :request_store do ] expect( - results[:details].map { |data| data.slice(:duration, :label, :code, :proxy, :error, :warnings) } + results[:details].map { |data| data.slice(:start, :duration, :label, :code, :proxy, :error, :warnings) } ).to match_array(expected) end @@ -91,10 +98,11 @@ RSpec.describe Peek::Views::ExternalHttp, :request_store do end it 'displays IPv4 in the label' do - subscriber.request(double(:event, payload: event_1)) + subscriber.request(double(:event, payload: event_1, time: Time.current)) expect(subject.results[:details]).to contain_exactly( a_hash_including( + start: be_like_time(Time.current), duration: 30.0, label: "POST https://1.2.3.4:80/api/v4/projects?current=true", code: "Response status: 200", @@ -112,10 +120,11 @@ RSpec.describe Peek::Views::ExternalHttp, :request_store do end it 'displays IPv6 in the label' do - subscriber.request(double(:event, payload: event_1)) + subscriber.request(double(:event, payload: event_1, time: Time.current)) expect(subject.results[:details]).to contain_exactly( a_hash_including( + start: be_like_time(Time.current), duration: 30.0, label: "POST https://[2606:4700:90:0:f22e:fbec:5bed:a9b9]:80/api/v4/projects?current=true", code: "Response status: 200", @@ -133,10 +142,11 @@ RSpec.describe Peek::Views::ExternalHttp, :request_store do end it 'converts query hash into a query string' do - subscriber.request(double(:event, payload: event_1)) + subscriber.request(double(:event, payload: event_1, time: Time.current)) expect(subject.results[:details]).to contain_exactly( a_hash_including( + start: be_like_time(Time.current), duration: 30.0, label: "POST https://gitlab.com:80/api/v4/projects?current=true&item1=string&item2%5B%5D=1&item2%5B%5D=2", code: "Response status: 200", @@ -154,10 +164,11 @@ RSpec.describe Peek::Views::ExternalHttp, :request_store do end it 'displays unknown in the label' do - subscriber.request(double(:event, payload: event_1)) + subscriber.request(double(:event, payload: event_1, time: Time.current)) expect(subject.results[:details]).to contain_exactly( a_hash_including( + start: be_like_time(Time.current), duration: 30.0, label: "POST unknown", code: "Response status: 200", @@ -176,10 +187,11 @@ RSpec.describe Peek::Views::ExternalHttp, :request_store do end it 'displays unknown in the label' do - subscriber.request(double(:event, payload: event_1)) + subscriber.request(double(:event, payload: event_1, time: Time.current)) expect(subject.results[:details]).to contain_exactly( a_hash_including( + start: be_like_time(Time.current), duration: 30.0, label: "POST unknown", code: "Response status: 200", @@ -198,10 +210,11 @@ RSpec.describe Peek::Views::ExternalHttp, :request_store do end it 'displays unknown in the label' do - subscriber.request(double(:event, payload: event_1)) + subscriber.request(double(:event, payload: event_1, time: Time.current)) expect(subject.results[:details]).to contain_exactly( a_hash_including( + start: be_like_time(Time.current), duration: 30.0, label: "POST unknown", code: "Response status: 200", diff --git a/spec/lib/quality/test_level_spec.rb b/spec/lib/quality/test_level_spec.rb deleted file mode 100644 index 32960cd571b..00000000000 --- a/spec/lib/quality/test_level_spec.rb +++ /dev/null @@ -1,225 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Quality::TestLevel do - describe '#pattern' do - context 'when level is all' do - it 'returns a pattern' do - expect(subject.pattern(:all)) - .to eq("spec/**{,/**/}*_spec.rb") - end - end - - context 'when level is geo' do - it 'returns a pattern' do - expect(subject.pattern(:geo)) - .to eq("spec/**{,/**/}*_spec.rb") - end - end - - context 'when level is frontend_fixture' do - it 'returns a pattern' do - expect(subject.pattern(:frontend_fixture)) - .to eq("spec/{frontend/fixtures}{,/**/}*.rb") - end - end - - context 'when level is unit' do - it 'returns a pattern' do - expect(subject.pattern(:unit)) - .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb") - end - end - - context 'when level is migration' do - it 'returns a pattern' do - expect(subject.pattern(:migration)) - .to eq("spec/{migrations,lib/gitlab/background_migration,lib/ee/gitlab/background_migration}{,/**/}*_spec.rb") - end - end - - context 'when level is background_migration' do - it 'returns a pattern' do - expect(subject.pattern(:background_migration)) - .to eq("spec/{lib/gitlab/background_migration,lib/ee/gitlab/background_migration}{,/**/}*_spec.rb") - end - end - - context 'when level is integration' do - it 'returns a pattern' do - expect(subject.pattern(:integration)) - .to eq("spec/{controllers,mailers,requests}{,/**/}*_spec.rb") - end - end - - context 'when level is system' do - it 'returns a pattern' do - expect(subject.pattern(:system)) - .to eq("spec/{features}{,/**/}*_spec.rb") - end - end - - context 'with a prefix' do - it 'returns a pattern' do - expect(described_class.new('ee/').pattern(:system)) - .to eq("ee/spec/{features}{,/**/}*_spec.rb") - end - end - - describe 'performance' do - it 'memoizes the pattern for a given level' do - expect(subject.pattern(:system).object_id).to eq(subject.pattern(:system).object_id) - end - - it 'freezes the pattern for a given level' do - expect(subject.pattern(:system)).to be_frozen - end - end - end - - describe '#regexp' do - context 'when level is all' do - it 'returns a regexp' do - expect(subject.regexp(:all)) - .to eq(%r{spec/}) - end - end - - context 'when level is geo' do - it 'returns a regexp' do - expect(subject.regexp(:geo)) - .to eq(%r{spec/}) - end - end - - context 'when level is frontend_fixture' do - it 'returns a regexp' do - expect(subject.regexp(:frontend_fixture)) - .to eq(%r{spec/(frontend/fixtures)}) - end - end - - context 'when level is unit' do - it 'returns a regexp' do - expect(subject.regexp(:unit)) - .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)}) - end - end - - context 'when level is migration' do - it 'returns a regexp' do - expect(subject.regexp(:migration)) - .to eq(%r{spec/(migrations|lib/gitlab/background_migration|lib/ee/gitlab/background_migration)}) - end - end - - context 'when level is background_migration' do - it 'returns a regexp' do - expect(subject.regexp(:background_migration)) - .to eq(%r{spec/(lib/gitlab/background_migration|lib/ee/gitlab/background_migration)}) - end - end - - context 'when level is integration' do - it 'returns a regexp' do - expect(subject.regexp(:integration)) - .to eq(%r{spec/(controllers|mailers|requests)}) - end - end - - context 'when level is system' do - it 'returns a regexp' do - expect(subject.regexp(:system)) - .to eq(%r{spec/(features)}) - end - end - - context 'with a prefix' do - it 'returns a regexp' do - expect(described_class.new('ee/').regexp(:system)) - .to eq(%r{ee/spec/(features)}) - end - end - - describe 'performance' do - it 'memoizes the regexp for a given level' do - expect(subject.regexp(:system).object_id).to eq(subject.regexp(:system).object_id) - end - - it 'freezes the regexp for a given level' do - expect(subject.regexp(:system)).to be_frozen - end - end - end - - describe '#level_for' do - it 'returns the correct level for a unit test' do - expect(subject.level_for('spec/models/abuse_report_spec.rb')).to eq(:unit) - end - - it 'returns the correct level for a frontend fixture test' do - expect(subject.level_for('spec/frontend/fixtures/pipelines.rb')).to eq(:frontend_fixture) - end - - it 'returns the correct level for a tooling test' do - expect(subject.level_for('spec/tooling/lib/tooling/test_file_finder_spec.rb')).to eq(:unit) - end - - it 'returns the correct level for a migration test' do - expect(subject.level_for('spec/migrations/add_default_and_free_plans_spec.rb')).to eq(:migration) - end - - it 'returns the correct level for a background migration test' do - expect(subject.level_for('spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb')).to eq(:migration) - end - - it 'returns the correct level for an EE file without passing a prefix' do - expect(subject.level_for('ee/spec/migrations/geo/migrate_ci_job_artifacts_to_separate_registry_spec.rb')).to eq(:migration) - end - - it 'returns the correct level for a geo migration test' do - expect(described_class.new('ee/').level_for('ee/spec/migrations/geo/migrate_ci_job_artifacts_to_separate_registry_spec.rb')).to eq(:migration) - end - - it 'returns the correct level for a EE-namespaced background migration test' do - expect(described_class.new('ee/').level_for('ee/spec/lib/ee/gitlab/background_migration/prune_orphaned_geo_events_spec.rb')).to eq(:migration) - end - - it 'returns the correct level for an integration test' do - expect(subject.level_for('spec/mailers/abuse_report_mailer_spec.rb')).to eq(:integration) - end - - it 'returns the correct level for a system test' do - expect(subject.level_for('spec/features/abuse_report_spec.rb')).to eq(:system) - end - - it 'raises an error for an unknown level' do - expect { subject.level_for('spec/unknown/foo_spec.rb') } - .to raise_error(described_class::UnknownTestLevelError, - %r{Test level for spec/unknown/foo_spec.rb couldn't be set. Please rename the file properly or change the test level detection regexes in .+/lib/quality/test_level.rb.}) - end - end - - describe '#background_migration?' do - it 'returns false for a unit test' do - expect(subject.background_migration?('spec/models/abuse_report_spec.rb')).to be(false) - end - - it 'returns true for a migration test' do - expect(subject.background_migration?('spec/migrations/add_default_and_free_plans_spec.rb')).to be(false) - end - - it 'returns true for a background migration test' do - expect(subject.background_migration?('spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb')).to be(true) - end - - it 'returns true for a geo migration test' do - expect(described_class.new('ee/').background_migration?('ee/spec/migrations/geo/migrate_ci_job_artifacts_to_separate_registry_spec.rb')).to be(false) - end - - it 'returns true for a EE-namespaced background migration test' do - expect(described_class.new('ee/').background_migration?('ee/spec/lib/ee/gitlab/background_migration/prune_orphaned_geo_events_spec.rb')).to be(true) - end - end -end diff --git a/spec/lib/rouge/formatters/html_gitlab_spec.rb b/spec/lib/rouge/formatters/html_gitlab_spec.rb new file mode 100644 index 00000000000..d45c8c2a8c5 --- /dev/null +++ b/spec/lib/rouge/formatters/html_gitlab_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Rouge::Formatters::HTMLGitlab do + describe '#format' do + subject { described_class.format(tokens, options) } + + let(:lang) { 'ruby' } + let(:lexer) { Rouge::Lexer.find_fancy(lang) } + let(:tokens) { lexer.lex("def hello", continue: false) } + let(:options) { { tag: lang } } + + it 'returns highlighted ruby code' do + code = %q{<span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span></span>} + + is_expected.to eq(code) + end + + context 'when options are empty' do + let(:options) { {} } + + it 'returns highlighted code without language' do + code = %q{<span id="LC1" class="line" lang=""><span class="k">def</span> <span class="nf">hello</span></span>} + + is_expected.to eq(code) + end + end + + context 'when line number is provided' do + let(:options) { { tag: lang, line_number: 10 } } + + it 'returns highlighted ruby code with correct line number' do + code = %q{<span id="LC10" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span></span>} + + is_expected.to eq(code) + end + end + end +end |