Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'gems/gitlab-utils/spec')
-rw-r--r--gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb368
-rw-r--r--gems/gitlab-utils/spec/gitlab/utils_spec.rb479
-rw-r--r--gems/gitlab-utils/spec/gitlab/version_info_spec.rb193
-rw-r--r--gems/gitlab-utils/spec/spec_helper.rb23
4 files changed, 1063 insertions, 0 deletions
diff --git a/gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb b/gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb
new file mode 100644
index 00000000000..f23a12ca6a2
--- /dev/null
+++ b/gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb
@@ -0,0 +1,368 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'active_support/testing/time_helpers'
+
+RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do
+ include ActiveSupport::Testing::TimeHelpers
+
+ let(:klass) do
+ strong_memoize_class = described_class
+
+ Struct.new(:value) do
+ include strong_memoize_class
+
+ def self.method_added_list
+ @method_added_list ||= []
+ end
+
+ def self.method_added(name)
+ method_added_list << name
+ end
+
+ def method_name
+ strong_memoize(:method_name) do # rubocop:disable Gitlab/StrongMemoizeAttr
+ trace << value
+ value
+ end
+ end
+
+ def method_name_with_expiration
+ strong_memoize_with_expiration(:method_name_with_expiration, 1) do
+ trace << value
+ value
+ end
+ end
+
+ def method_name_attr
+ trace << value
+ value
+ end
+ strong_memoize_attr :method_name_attr
+
+ def enabled?
+ trace << value
+ value
+ end
+ strong_memoize_attr :enabled?
+
+ def method_name_with_args(*args)
+ strong_memoize_with(:method_name_with_args, args) do
+ trace << [value, args]
+ value
+ end
+ end
+
+ def trace
+ @trace ||= []
+ end
+
+ protected
+
+ def private_method; end
+ private :private_method
+ strong_memoize_attr :private_method
+
+ public
+
+ def protected_method; end
+ protected :protected_method
+ strong_memoize_attr :protected_method
+
+ private
+
+ def public_method; end
+ public :public_method
+ strong_memoize_attr :public_method
+ end
+ end
+
+ subject(:object) { klass.new(value) }
+
+ shared_examples 'caching the value' do
+ let(:member_name) { described_class.normalize_key(method_name) }
+
+ it 'only calls the block once' do
+ value0 = object.public_send(method_name)
+ value1 = object.public_send(method_name)
+
+ expect(value0).to eq(value)
+ expect(value1).to eq(value)
+ expect(object.trace).to contain_exactly(value)
+ end
+
+ it 'returns and defines the instance variable for the exact value' do
+ returned_value = object.public_send(method_name)
+ memoized_value = object.instance_variable_get(:"@#{member_name}")
+
+ expect(returned_value).to eql(value)
+ expect(memoized_value).to eql(value)
+ end
+ end
+
+ describe '#strong_memoize' do
+ [nil, false, true, 'value', 0, [0]].each do |value|
+ context "with value #{value}" do
+ let(:value) { value }
+ let(:method_name) { :method_name }
+
+ it_behaves_like 'caching the value'
+
+ it 'raises exception for invalid type as key' do
+ expect { object.strong_memoize(10) { 20 } }.to raise_error(/Invalid type of '10'/)
+ end
+
+ it 'raises exception for invalid characters in key' do
+ expect { object.strong_memoize(:enabled?) { 20 } }
+ .to raise_error(/is not allowed as an instance variable name/)
+ end
+ end
+ end
+
+ context "with memory allocation", type: :benchmark do
+ let(:value) { 'aaa' }
+
+ before do
+ object.method_name # warmup
+ end
+
+ [:method_name, "method_name"].each do |argument|
+ context "when argument is a #{argument.class}" do
+ it 'does allocate exactly one string when fetching value' do
+ expect do
+ object.strong_memoize(argument) { 10 }
+ end.to perform_allocation(1)
+ end
+
+ it 'does allocate exactly one string when storing value' do
+ object.clear_memoization(:method_name) # clear to force set
+
+ expect do
+ object.strong_memoize(argument) { 10 }
+ end.to perform_allocation(1)
+ end
+ end
+ end
+ end
+ end
+
+ describe '#strong_memoize_with_expiration' do
+ [nil, false, true, 'value', 0, [0]].each do |value|
+ context "with value #{value}" do
+ let(:value) { value }
+ let(:method_name) { :method_name_with_expiration }
+
+ it_behaves_like 'caching the value'
+
+ it 'raises exception for invalid type as key' do
+ expect { object.strong_memoize_with_expiration(10, 1) { 20 } }.to raise_error(/Invalid type of '10'/)
+ end
+
+ it 'raises exception for invalid characters in key' do
+ expect { object.strong_memoize_with_expiration(:enabled?, 1) { 20 } }
+ .to raise_error(/is not allowed as an instance variable name/)
+ end
+ end
+ end
+
+ context 'with value memoization test' do
+ let(:value) { 'value' }
+
+ it 'caches the value for specified number of seconds' do
+ object.method_name_with_expiration
+ object.method_name_with_expiration
+
+ expect(object.trace.count).to eq(1)
+
+ travel_to(Time.current + 2.seconds) do
+ object.method_name_with_expiration
+
+ expect(object.trace.count).to eq(2)
+ end
+ end
+ end
+ end
+
+ describe '#strong_memoize_with' do
+ [nil, false, true, 'value', 0, [0]].each do |value|
+ context "with value #{value}" do
+ let(:value) { value }
+
+ it 'only calls the block once' do
+ value0 = object.method_name_with_args(1)
+ value1 = object.method_name_with_args(1)
+ value2 = object.method_name_with_args([2, 3])
+ value3 = object.method_name_with_args([2, 3])
+
+ expect(value0).to eq(value)
+ expect(value1).to eq(value)
+ expect(value2).to eq(value)
+ expect(value3).to eq(value)
+
+ expect(object.trace).to contain_exactly([value, [1]], [value, [[2, 3]]])
+ end
+
+ it 'returns and defines the instance variable for the exact value' do
+ returned_value = object.method_name_with_args(1, 2, 3)
+ memoized_value = object.instance_variable_get(:@method_name_with_args)
+
+ expect(returned_value).to eql(value)
+ expect(memoized_value).to eql({ [[1, 2, 3]] => value })
+ end
+ end
+ end
+ end
+
+ describe '#strong_memoized?' do
+ shared_examples 'memoization check' do |method_name|
+ context "when method is :#{method_name}" do
+ let(:value) { :anything }
+
+ subject { object.strong_memoized?(method_name) }
+
+ it 'returns false if the value is uncached' do
+ expect(subject).to be(false)
+ end
+
+ it 'returns true if the value is cached' do
+ object.public_send(method_name)
+
+ expect(subject).to be(true)
+ end
+ end
+ end
+
+ it_behaves_like 'memoization check', :method_name
+ it_behaves_like 'memoization check', :enabled?
+ end
+
+ describe '#clear_memoization' do
+ shared_examples 'clearing memoization' do |method_name|
+ let(:member_name) { described_class.normalize_key(method_name) }
+ let(:value) { 'mepmep' }
+
+ it 'removes the instance variable' do
+ object.public_send(method_name)
+
+ object.clear_memoization(method_name)
+
+ expect(object.instance_variable_defined?(:"@#{member_name}")).to be(false)
+ end
+ end
+
+ it_behaves_like 'clearing memoization', :method_name
+ it_behaves_like 'clearing memoization', :enabled?
+ end
+
+ describe '.strong_memoize_attr' do
+ [nil, false, true, 'value', 0, [0]].each do |value|
+ context "with value '#{value}'" do
+ let(:value) { value }
+
+ context 'with memoized after method definition' do
+ let(:method_name) { :method_name_attr }
+
+ it_behaves_like 'caching the value'
+
+ it 'calls the existing .method_added' do
+ expect(klass.method_added_list).to include(:method_name_attr)
+ end
+
+ it 'retains method arity' do
+ expect(klass.instance_method(method_name).arity).to eq(0)
+ end
+ end
+ end
+ end
+
+ describe 'method visibility' do
+ it 'sets private visibility' do
+ expect(klass.private_instance_methods).to include(:private_method)
+ expect(klass.protected_instance_methods).not_to include(:private_method)
+ expect(klass.public_instance_methods).not_to include(:private_method)
+ end
+
+ it 'sets protected visibility' do
+ expect(klass.private_instance_methods).not_to include(:protected_method)
+ expect(klass.protected_instance_methods).to include(:protected_method)
+ expect(klass.public_instance_methods).not_to include(:protected_method)
+ end
+
+ it 'sets public visibility' do
+ expect(klass.private_instance_methods).not_to include(:public_method)
+ expect(klass.protected_instance_methods).not_to include(:public_method)
+ expect(klass.public_instance_methods).to include(:public_method)
+ end
+ end
+
+ context "when method doesn't exist" do
+ let(:klass) do
+ strong_memoize_class = described_class
+
+ Struct.new(:value) do
+ include strong_memoize_class
+ end
+ end
+
+ subject { klass.strong_memoize_attr(:nonexistent_method) }
+
+ it 'fails when strong-memoizing a nonexistent method' do
+ expect { subject }.to raise_error(NameError, /undefined method `nonexistent_method' for class/)
+ end
+ end
+
+ context 'when memoized method has parameters' do
+ it 'raises an error' do
+ expected_message = /Using `strong_memoize_attr` on methods with parameters is not supported/
+
+ expect do
+ strong_memoize_class = described_class
+
+ Class.new do
+ include strong_memoize_class
+
+ def method_with_parameters(params); end
+ strong_memoize_attr :method_with_parameters
+ end
+ end.to raise_error(RuntimeError, expected_message)
+ end
+ end
+ end
+
+ describe '.normalize_key' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.normalize_key(input) }
+
+ where(:input, :output, :valid) do
+ :key | :key | true
+ "key" | "key" | true
+ :key? | "key?" | true
+ "key?" | "key?" | true
+ :key! | "key!" | true
+ "key!" | "key!" | true
+ # invalid cases caught elsewhere
+ :"ke?y" | :"ke?y" | false
+ "ke?y" | "ke?y" | false
+ :"ke!y" | :"ke!y" | false
+ "ke!y" | "ke!y" | false
+ end
+
+ with_them do
+ let(:ivar) { "@#{output}" }
+
+ it { is_expected.to eq(output) }
+
+ if params[:valid]
+ it 'is a valid ivar name' do
+ expect { instance_variable_defined?(ivar) }.not_to raise_error
+ end
+ else
+ it 'raises a NameError error' do
+ expect { instance_variable_defined?(ivar) }
+ .to raise_error(NameError, /not allowed as an instance/)
+ end
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-utils/spec/gitlab/utils_spec.rb b/gems/gitlab-utils/spec/gitlab/utils_spec.rb
new file mode 100644
index 00000000000..53593190eea
--- /dev/null
+++ b/gems/gitlab-utils/spec/gitlab/utils_spec.rb
@@ -0,0 +1,479 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Utils, feature_category: :shared do
+ using RSpec::Parameterized::TableSyntax
+ include StubENV
+
+ delegate :to_boolean, :boolean_to_yes_no, :slugify, :which,
+ :ensure_array_from_string, :bytes_to_megabytes,
+ :append_path, :remove_leading_slashes, :allowlisted?,
+ :decode_path, :ms_to_round_sec, to: :described_class
+
+ describe '.allowlisted?' do
+ let(:allowed_paths) { ['/home/foo', '/foo/bar', '/etc/passwd'] }
+
+ it 'returns true if path is allowed' do
+ expect(allowlisted?('/foo/bar', allowed_paths)).to be(true)
+ end
+
+ it 'returns false if path is not allowed' do
+ expect(allowlisted?('/test/test', allowed_paths)).to be(false)
+ end
+ end
+
+ describe '.decode_path' do
+ it 'returns path unencoded for singled-encoded paths' do
+ expect(decode_path('%2Fhome%2Fbar%3Fasd%3Dqwe')).to eq('/home/bar?asd=qwe')
+ end
+
+ it 'returns path when it is unencoded' do
+ expect(decode_path('/home/bar?asd=qwe')).to eq('/home/bar?asd=qwe')
+ end
+
+ [
+ '..%252F..%252F..%252Fetc%252Fpasswd',
+ '%25252Fresult%25252Fchosennickname%25253D%252522jj%252522'
+ ].each do |multiple_encoded_path|
+ it 'raises an exception when the path is multiple-encoded' do
+ expect { decode_path(multiple_encoded_path) }.to raise_error(/path #{multiple_encoded_path} is not allowed/)
+ end
+ end
+ end
+
+ describe '.slugify' do
+ {
+ 'TEST' => 'test',
+ 'project_with_underscores' => 'project-with-underscores',
+ 'namespace/project' => 'namespace-project',
+ 'a' * 70 => 'a' * 63,
+ 'test_trailing_' => 'test-trailing'
+ }.each do |original, expected|
+ it "slugifies #{original} to #{expected}" do
+ expect(slugify(original)).to eq(expected)
+ end
+ end
+ end
+
+ describe '.ms_to_round_sec' do
+ where(:original, :expected) do
+ 1999.8999 | 1.9999
+ 12384 | 12.384
+ 333 | 0.333
+ 1333.33333333 | 1.333333
+ end
+
+ with_them do
+ it "returns rounded seconds" do
+ expect(ms_to_round_sec(original)).to eq(expected)
+ end
+ end
+ end
+
+ describe '.nlbr' do
+ it 'replaces new lines with <br>' do
+ expect(described_class.nlbr("<b>hello</b>\n<i>world</i>")).to eq("hello<br>world")
+ end
+ end
+
+ describe '.remove_line_breaks' do
+ where(:original, :expected) do
+ "foo\nbar\nbaz" | "foobarbaz"
+ "foo\r\nbar\r\nbaz" | "foobarbaz"
+ "foobar" | "foobar"
+ end
+
+ with_them do
+ it "replace line breaks with an empty string" do
+ expect(described_class.remove_line_breaks(original)).to eq(expected)
+ end
+ end
+ end
+
+ describe '.to_boolean' do
+ it 'accepts booleans' do
+ expect(to_boolean(true)).to be(true)
+ expect(to_boolean(false)).to be(false)
+ end
+
+ it 'converts a valid value to a boolean' do
+ expect(to_boolean(true)).to be(true)
+ expect(to_boolean('true')).to be(true)
+ 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)
+ expect(to_boolean('F')).to be(false)
+ 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
+
+ it 'converts an invalid value to nil' do
+ expect(to_boolean('fals')).to be_nil
+ expect(to_boolean('yeah')).to be_nil
+ expect(to_boolean('')).to be_nil
+ expect(to_boolean(nil)).to be_nil
+ end
+
+ it 'accepts a default value, and does not return it when a valid value is given' do
+ expect(to_boolean(true, default: false)).to be(true)
+ expect(to_boolean('true', default: false)).to be(true)
+ expect(to_boolean('YeS', default: false)).to be(true)
+ expect(to_boolean('t', default: false)).to be(true)
+ expect(to_boolean('1', default: 'any value')).to be(true)
+ expect(to_boolean('ON', default: 42)).to be(true)
+
+ expect(to_boolean('FaLse', default: true)).to be(false)
+ expect(to_boolean('F', default: true)).to be(false)
+ expect(to_boolean('NO', default: true)).to be(false)
+ expect(to_boolean('n', default: true)).to be(false)
+ expect(to_boolean('0', default: 'any value')).to be(false)
+ expect(to_boolean('oFF', default: 42)).to be(false)
+ end
+
+ it 'accepts a default value, and returns it when an invalid value is given' do
+ expect(to_boolean('fals', default: true)).to eq(true)
+ expect(to_boolean('yeah', default: false)).to eq(false)
+ expect(to_boolean('', default: 'any value')).to eq('any value')
+ expect(to_boolean(nil, default: 42)).to eq(42)
+ end
+ end
+
+ describe '.boolean_to_yes_no' do
+ it 'converts booleans to Yes or No' do
+ expect(boolean_to_yes_no(true)).to eq('Yes')
+ expect(boolean_to_yes_no(false)).to eq('No')
+ end
+ end
+
+ describe '.which' do
+ before do
+ stub_env('PATH', '/sbin:/usr/bin:/home/joe/bin')
+ end
+
+ it 'finds the full path to an executable binary in order of appearance' do
+ expect(File).to receive(:executable?).with('/sbin/tool').ordered.and_return(false)
+ expect(File).to receive(:executable?).with('/usr/bin/tool').ordered.and_return(true)
+ expect(File).not_to receive(:executable?).with('/home/joe/bin/tool')
+
+ expect(which('tool')).to eq('/usr/bin/tool')
+ end
+ end
+
+ describe '.ensure_array_from_string' do
+ it 'returns the same array if given one' do
+ arr = ['a', 4, true, { test: 1 }]
+
+ expect(ensure_array_from_string(arr)).to eq(arr)
+ end
+
+ it 'turns comma-separated strings into arrays' do
+ str = 'seven, eight, 9, 10'
+
+ expect(ensure_array_from_string(str)).to eq(%w[seven eight 9 10])
+ end
+ end
+
+ describe '.bytes_to_megabytes' do
+ it 'converts bytes to megabytes' do
+ bytes = 1.megabyte
+
+ expect(bytes_to_megabytes(bytes)).to eq(1)
+ end
+ end
+
+ describe '.append_path' do
+ where(:host, :path, :result) do
+ 'http://test/' | '/foo/bar' | 'http://test/foo/bar'
+ 'http://test/' | '//foo/bar' | 'http://test/foo/bar'
+ 'http://test//' | '/foo/bar' | 'http://test/foo/bar'
+ 'http://test' | 'foo/bar' | 'http://test/foo/bar'
+ 'http://test//' | '' | 'http://test/'
+ 'http://test//' | nil | 'http://test/'
+ '' | '/foo/bar' | '/foo/bar'
+ nil | '/foo/bar' | '/foo/bar'
+ end
+
+ with_them do
+ it 'makes sure there is only one slash as path separator' do
+ expect(append_path(host, path)).to eq(result)
+ end
+ end
+ end
+
+ describe '.remove_leading_slashes' do
+ where(:str, :result) do
+ '/foo/bar' | 'foo/bar'
+ '//foo/bar' | 'foo/bar'
+ '/foo/bar/' | 'foo/bar/'
+ 'foo/bar' | 'foo/bar'
+ '' | ''
+ nil | ''
+ end
+
+ with_them do
+ it 'removes leading slashes' do
+ expect(remove_leading_slashes(str)).to eq(result)
+ end
+ end
+ end
+
+ describe '.ensure_utf8_size' do
+ context 'with string is has less bytes than expected' do
+ it 'backfills string with null characters' do
+ transformed = described_class.ensure_utf8_size('a' * 10, bytes: 32)
+
+ expect(transformed.bytesize).to eq 32
+ expect(transformed).to eq(('a' * 10) + ('0' * 22))
+ end
+ end
+
+ context 'with string size is exactly the one that is expected' do
+ it 'returns original value' do
+ transformed = described_class.ensure_utf8_size('a' * 32, bytes: 32)
+
+ expect(transformed).to eq 'a' * 32
+ expect(transformed.bytesize).to eq 32
+ end
+ end
+
+ context 'when string contains a few multi-byte UTF characters' do
+ it 'backfills string with null characters' do
+ transformed = described_class.ensure_utf8_size('❤' * 6, bytes: 32)
+
+ expect(transformed).to eq '❤❤❤❤❤❤' + ('0' * 14) # rubocop:disable Style/StringConcatenation
+ expect(transformed.bytesize).to eq 32
+ end
+ end
+
+ context 'when string has multiple multi-byte UTF chars exceeding 32 bytes' do
+ it 'truncates string to 32 characters and backfills it if needed' do
+ transformed = described_class.ensure_utf8_size('❤' * 18, bytes: 32)
+
+ expect(transformed).to eq(('❤' * 10) + ('0' * 2))
+ expect(transformed.bytesize).to eq 32
+ end
+ end
+ end
+
+ describe '.deep_indifferent_access' do
+ let(:hash) do
+ { "variables" => [{ "key" => "VAR1", "value" => "VALUE2" }] }
+ end
+
+ subject { described_class.deep_indifferent_access(hash) }
+
+ it 'allows to access hash keys with symbols' do
+ expect(subject[:variables]).to be_a(Array)
+ end
+
+ it 'allows to access array keys with symbols' do
+ expect(subject[:variables].first[:key]).to eq('VAR1')
+ end
+ end
+
+ describe '.deep_symbolized_access' do
+ let(:hash) do
+ { "variables" => [{ "key" => "VAR1", "value" => "VALUE2" }] }
+ end
+
+ subject { described_class.deep_symbolized_access(hash) }
+
+ it 'allows to access hash keys with symbols' do
+ expect(subject[:variables]).to be_a(Array)
+ end
+
+ it 'allows to access array keys with symbols' do
+ expect(subject[:variables].first[:key]).to eq('VAR1')
+ end
+ end
+
+ describe '.try_megabytes_to_bytes' do
+ context 'when the size can be converted to megabytes' do
+ it 'returns the size in megabytes' do
+ size = described_class.try_megabytes_to_bytes(1)
+
+ expect(size).to eq(1.megabytes)
+ end
+ end
+
+ context 'when the size can not be converted to megabytes' do
+ it 'returns the input size' do
+ size = described_class.try_megabytes_to_bytes('foo')
+
+ expect(size).to eq('foo')
+ end
+ end
+ end
+
+ describe '.string_to_ip_object' do
+ it 'returns nil when string is nil' do
+ expect(described_class.string_to_ip_object(nil)).to eq(nil)
+ end
+
+ it 'returns nil when string is invalid IP' do
+ expect(described_class.string_to_ip_object('invalid ip')).to eq(nil)
+ expect(described_class.string_to_ip_object('')).to eq(nil)
+ end
+
+ it 'returns IP object when string is valid IP' do
+ expect(described_class.string_to_ip_object('192.168.1.1')).to eq(IPAddr.new('192.168.1.1'))
+ expect(described_class.string_to_ip_object('::ffff:a9fe:a864')).to eq(IPAddr.new('::ffff:a9fe:a864'))
+ expect(described_class.string_to_ip_object('[::ffff:a9fe:a864]')).to eq(IPAddr.new('::ffff:a9fe:a864'))
+ expect(described_class.string_to_ip_object('127.0.0.0/28')).to eq(IPAddr.new('127.0.0.0/28'))
+ expect(described_class.string_to_ip_object('1:0:0:0:0:0:0:0/124')).to eq(IPAddr.new('1:0:0:0:0:0:0:0/124'))
+ end
+ end
+
+ describe ".safe_downcase!" do
+ where(:str, :result) do
+ "test" | "test"
+ "Test" | "test"
+ "test" | "test"
+ "Test" | "test"
+ end
+
+ with_them do
+ it "downcases the string" do
+ expect(described_class.safe_downcase!(str)).to eq(result)
+ end
+ end
+ end
+
+ describe '.parse_url' do
+ it 'returns Addressable::URI object' do
+ expect(described_class.parse_url('http://gitlab.com')).to be_instance_of(Addressable::URI)
+ end
+
+ it 'returns nil when URI cannot be parsed' do
+ expect(described_class.parse_url('://gitlab.com')).to be nil
+ end
+
+ it 'returns nil with invalid parameter' do
+ expect(described_class.parse_url(1)).to be nil
+ end
+ end
+
+ describe '.add_url_parameters' do
+ subject { described_class.add_url_parameters(url, params) }
+
+ where(:url, :params, :expected_url) do
+ nil | nil | ''
+ nil | { b: 3, a: 2 } | '?a=2&b=3'
+ 'https://gitlab.com' | nil | 'https://gitlab.com'
+ 'https://gitlab.com' | { b: 3, a: 2 } | 'https://gitlab.com?a=2&b=3'
+ 'https://gitlab.com?a=1#foo' | { b: 3, 'a' => 2 } | 'https://gitlab.com?a=2&b=3#foo'
+ 'https://gitlab.com?a=1#foo' | [[:b, 3], [:a, 2]] | 'https://gitlab.com?a=2&b=3#foo'
+ end
+
+ with_them do
+ it { is_expected.to eq(expected_url) }
+ end
+ end
+
+ describe '.removes_sensitive_data_from_url' do
+ it 'returns string object' do
+ expect(described_class.removes_sensitive_data_from_url('http://gitlab.com')).to be_instance_of(String)
+ end
+
+ it 'returns nil when URI cannot be parsed' do
+ expect(described_class.removes_sensitive_data_from_url('://gitlab.com')).to be nil
+ end
+
+ it 'returns nil with invalid parameter' do
+ expect(described_class.removes_sensitive_data_from_url(1)).to be nil
+ end
+
+ it 'returns string with filtered access_token param' do
+ expect(described_class.removes_sensitive_data_from_url('http://gitlab.com/auth.html#access_token=secret_token'))
+ .to eq('http://gitlab.com/auth.html#access_token=filtered')
+ end
+
+ it 'returns string with filtered access_token param but other params preserved' do
+ expect(described_class.removes_sensitive_data_from_url('http://gitlab.com/auth.html#access_token=secret_token&token_type=Bearer&state=test'))
+ .to include('&token_type=Bearer', '&state=test')
+ end
+ end
+
+ describe 'multiple_key_invert' do
+ it 'invert keys with array values' do
+ hash = {
+ dast: [:vulnerabilities_count, :scanned_resources_count],
+ sast: [:vulnerabilities_count]
+ }
+ expect(described_class.multiple_key_invert(hash)).to eq({
+ vulnerabilities_count: [:dast, :sast],
+ scanned_resources_count: [:dast]
+ })
+ end
+ end
+
+ describe '.stable_sort_by' do
+ subject(:sorted_list) { described_class.stable_sort_by(list) { |obj| obj[:priority] } }
+
+ context 'when items have the same priority' do
+ let(:list) do
+ [
+ { name: 'obj 1', priority: 1 },
+ { name: 'obj 2', priority: 1 },
+ { name: 'obj 3', priority: 1 }
+ ]
+ end
+
+ it 'does not change order in cases of ties' do
+ expect(sorted_list).to eq(list)
+ end
+ end
+
+ context 'when items have different priorities' do
+ let(:list) do
+ [
+ { name: 'obj 1', priority: 2 },
+ { name: 'obj 2', priority: 1 },
+ { name: 'obj 3', priority: 3 }
+ ]
+ end
+
+ it 'sorts items like the regular sort_by' do
+ expect(sorted_list).to eq(
+ [
+ { name: 'obj 2', priority: 1 },
+ { name: 'obj 1', priority: 2 },
+ { name: 'obj 3', priority: 3 }
+ ])
+ end
+ end
+ end
+
+ describe '.valid_brackets?' do
+ where(:input, :allow_nested, :valid) do
+ 'no brackets' | true | true
+ 'no brackets' | false | true
+ 'user[avatar]' | true | true
+ 'user[avatar]' | false | true
+ 'user[avatar][friends]' | true | true
+ 'user[avatar][friends]' | false | true
+ 'user[avatar[image[url]]]' | true | true
+ 'user[avatar[image[url]]]' | false | false
+ 'user[avatar[]friends]' | true | true
+ 'user[avatar[]friends]' | false | false
+ 'user[avatar]]' | true | false
+ 'user[avatar]]' | false | false
+ 'user][avatar]]' | true | false
+ 'user][avatar]]' | false | false
+ 'user[avatar' | true | false
+ 'user[avatar' | false | false
+ end
+
+ with_them do
+ it { expect(described_class.valid_brackets?(input, allow_nested: allow_nested)).to eq(valid) }
+ end
+ end
+end
diff --git a/gems/gitlab-utils/spec/gitlab/version_info_spec.rb b/gems/gitlab-utils/spec/gitlab/version_info_spec.rb
new file mode 100644
index 00000000000..2b5f6bcb4c1
--- /dev/null
+++ b/gems/gitlab-utils/spec/gitlab/version_info_spec.rb
@@ -0,0 +1,193 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::VersionInfo, feature_category: :shared do
+ before do
+ @unknown = described_class.new
+ @v0_0_1 = described_class.new(0, 0, 1)
+ @v0_1_0 = described_class.new(0, 1, 0)
+ @v1_0_0 = described_class.new(1, 0, 0)
+ @v1_0_1 = described_class.new(1, 0, 1)
+ @v1_0_1_b1 = described_class.new(1, 0, 1, '-b1')
+ @v1_0_1_rc1 = described_class.new(1, 0, 1, '-rc1')
+ @v1_0_1_rc2 = described_class.new(1, 0, 1, '-rc2')
+ @v1_1_0 = described_class.new(1, 1, 0)
+ @v1_1_0_beta1 = described_class.new(1, 1, 0, '-beta1')
+ @v2_0_0 = described_class.new(2, 0, 0)
+ @v13_10_1_1574_89 = described_class.parse("v13.10.1~beta.1574.gf6ea9389", parse_suffix: true)
+ @v13_10_1_1575_89 = described_class.parse("v13.10.1~beta.1575.gf6ea9389", parse_suffix: true)
+ @v13_10_1_1575_90 = described_class.parse("v13.10.1~beta.1575.gf6ea9390", parse_suffix: true)
+ end
+
+ describe '>' do
+ it { expect(@v2_0_0).to be > @v1_1_0 }
+ it { expect(@v1_1_0).to be > @v1_0_1 }
+ it { expect(@v1_0_1_b1).to be > @v1_0_0 }
+ it { expect(@v1_0_1_rc1).to be > @v1_0_0 }
+ it { expect(@v1_0_1_rc1).to be > @v1_0_1_b1 }
+ it { expect(@v1_0_1_rc2).to be > @v1_0_1_rc1 }
+ it { expect(@v1_0_1).to be > @v1_0_1_rc1 }
+ it { expect(@v1_0_1).to be > @v1_0_1_rc2 }
+ it { expect(@v1_0_1).to be > @v1_0_0 }
+ it { expect(@v1_0_0).to be > @v0_1_0 }
+ it { expect(@v1_1_0_beta1).to be > @v1_0_1_rc2 }
+ it { expect(@v1_1_0).to be > @v1_1_0_beta1 }
+ it { expect(@v0_1_0).to be > @v0_0_1 }
+ end
+
+ describe '>=' do
+ it { expect(@v2_0_0).to be >= described_class.new(2, 0, 0) }
+ it { expect(@v2_0_0).to be >= @v1_1_0 }
+ it { expect(@v1_0_1_rc2).to be >= @v1_0_1_rc1 }
+ end
+
+ describe '<' do
+ it { expect(@v0_0_1).to be < @v0_1_0 }
+ it { expect(@v0_1_0).to be < @v1_0_0 }
+ it { expect(@v1_0_0).to be < @v1_0_1 }
+ it { expect(@v1_0_1).to be < @v1_1_0 }
+ it { expect(@v1_0_0).to be < @v1_0_1_rc2 }
+ it { expect(@v1_0_1_rc1).to be < @v1_0_1 }
+ it { expect(@v1_0_1_rc1).to be < @v1_0_1_rc2 }
+ it { expect(@v1_0_1_rc2).to be < @v1_0_1 }
+ it { expect(@v1_1_0).to be < @v2_0_0 }
+ it { expect(@v13_10_1_1574_89).to be < @v13_10_1_1575_89 }
+ it { expect(@v13_10_1_1575_89).to be < @v13_10_1_1575_90 }
+ end
+
+ describe '<=' do
+ it { expect(@v0_0_1).to be <= described_class.new(0, 0, 1) }
+ it { expect(@v0_0_1).to be <= @v0_1_0 }
+ it { expect(@v1_0_1_b1).to be <= @v1_0_1_rc1 }
+ it { expect(@v1_0_1_rc1).to be <= @v1_0_1_rc2 }
+ it { expect(@v1_1_0_beta1).to be <= @v1_1_0 }
+ end
+
+ describe '==' do
+ it { expect(@v0_0_1).to eq(described_class.new(0, 0, 1)) }
+ it { expect(@v0_1_0).to eq(described_class.new(0, 1, 0)) }
+ it { expect(@v1_0_0).to eq(described_class.new(1, 0, 0)) }
+ it { expect(@v1_0_1_rc1).to eq(described_class.new(1, 0, 1, '-rc1')) }
+ end
+
+ describe '!=' do
+ it { expect(@v0_0_1).not_to eq(@v0_1_0) }
+ it { expect(@v1_0_1_rc1).not_to eq(@v1_0_1_rc2) }
+ end
+
+ describe '.unknown' do
+ it { expect(@unknown).not_to be @v0_0_1 }
+ it { expect(@unknown).not_to be described_class.new }
+ it { expect { @unknown > @v0_0_1 }.to raise_error(ArgumentError) }
+ it { expect { @unknown < @v0_0_1 }.to raise_error(ArgumentError) }
+ end
+
+ describe '.parse' do
+ it { expect(described_class.parse(described_class.new(1, 0, 0))).to eq(@v1_0_0) }
+ it { expect(described_class.parse("1.0.0")).to eq(@v1_0_0) }
+ it { expect(described_class.parse("1.0.0.1")).to eq(@v1_0_0) }
+ it { expect(described_class.parse("1.0.0-ee")).to eq(@v1_0_0) }
+ it { expect(described_class.parse("1.0.0-rc1")).to eq(@v1_0_0) }
+ it { expect(described_class.parse("1.0.0-rc1-ee")).to eq(@v1_0_0) }
+ it { expect(described_class.parse("git 1.0.0b1")).to eq(@v1_0_0) }
+ it { expect(described_class.parse("git 1.0b1")).not_to be_valid }
+ it { expect(described_class.parse("1.1.#{'1' * described_class::MAX_VERSION_LENGTH}")).not_to be_valid }
+ it { expect(described_class.parse(nil)).not_to be_valid }
+
+ context 'with parse_suffix: true' do
+ let(:versions) do
+ <<-VERSIONS.lines
+ 0.0.1
+ 0.1.0
+ 1.0.0
+ 1.0.1-b1
+ 1.0.1-rc1
+ 1.0.1-rc2
+ 1.0.1
+ 1.1.0-beta1
+ 1.1.0
+ 2.0.0
+ v13.10.0-pre
+ v13.10.0-rc1
+ v13.10.0-rc2
+ v13.10.0
+ v13.10.1~beta.1574.gf6ea9389
+ v13.10.1~beta.1575.gf6ea9389
+ v13.10.1-rc1
+ v13.10.1-rc2
+ v13.10.1
+ VERSIONS
+ end
+
+ let(:parsed_versions) do
+ versions.map(&:strip).map { |version| described_class.parse(version, parse_suffix: true) }
+ end
+
+ it 'versions are returned in a correct order' do
+ expect(parsed_versions.shuffle.sort).to eq(parsed_versions)
+ end
+ end
+ end
+
+ describe '.to_s' do
+ it { expect(@v1_0_0.to_s).to eq("1.0.0") }
+ it { expect(@v1_0_1_rc1.to_s).to eq("1.0.1-rc1") }
+ it { expect(@unknown.to_s).to eq("Unknown") }
+ end
+
+ describe '.to_json' do
+ let(:correct_version) do
+ "{\"major\":1,\"minor\":0,\"patch\":1}"
+ end
+
+ let(:unknown_version) do
+ "{\"major\":0,\"minor\":0,\"patch\":0}"
+ end
+
+ it { expect(@v1_0_1.to_json).to eq(correct_version) }
+ it { expect(@v1_0_1_rc2.to_json).to eq(correct_version) }
+ it { expect(@unknown.to_json).to eq(unknown_version) }
+ end
+
+ describe '.hash' do
+ it { expect(described_class.parse("1.0.0").hash).to eq(@v1_0_0.hash) }
+ it { expect(described_class.parse("1.0.0.1").hash).to eq(@v1_0_0.hash) }
+ it { expect(described_class.parse("1.0.1b1").hash).to eq(@v1_0_1.hash) }
+ it { expect(described_class.parse("1.0.1-rc1", parse_suffix: true).hash).to eq(@v1_0_1_rc1.hash) }
+ end
+
+ describe '.eql?' do
+ it { expect(described_class.parse("1.0.0").eql?(@v1_0_0)).to be_truthy }
+ it { expect(described_class.parse("1.0.0.1").eql?(@v1_0_0)).to be_truthy }
+ it { expect(@v1_0_1_rc1.eql?(@v1_0_1_rc1)).to be_truthy }
+ it { expect(@v1_0_1_rc1.eql?(@v1_0_1_rc2)).to be_falsey }
+ it { expect(@v1_0_1_rc1.eql?(@v1_0_1)).to be_falsey }
+ it { expect(@v1_0_1.eql?(@v1_0_0)).to be_falsey }
+ it { expect(@v1_1_0.eql?(@v1_0_0)).to be_falsey }
+ it { expect(@v1_0_0.eql?(@v1_0_0)).to be_truthy }
+ it { expect([@v1_0_0, @v1_1_0, @v1_0_0, @v1_0_1_rc1, @v1_0_1_rc1].uniq).to eq [@v1_0_0, @v1_1_0, @v1_0_1_rc1] }
+ end
+
+ describe '.same_minor_version?' do
+ it { expect(@v0_1_0.same_minor_version?(@v0_0_1)).to be_falsey }
+ it { expect(@v1_0_1.same_minor_version?(@v1_0_0)).to be_truthy }
+ it { expect(@v1_0_1_rc1.same_minor_version?(@v1_0_0)).to be_truthy }
+ it { expect(@v1_0_0.same_minor_version?(@v1_0_1)).to be_truthy }
+ it { expect(@v1_1_0.same_minor_version?(@v1_0_0)).to be_falsey }
+ it { expect(@v2_0_0.same_minor_version?(@v1_0_0)).to be_falsey }
+ end
+
+ describe '.without_patch' do
+ it { expect(@v0_1_0.without_patch).to eq(@v0_1_0) }
+ it { expect(@v1_0_0.without_patch).to eq(@v1_0_0) }
+ it { expect(@v1_0_1.without_patch).to eq(@v1_0_0) }
+ it { expect(@v1_0_1_rc1.without_patch).to eq(@v1_0_0) }
+ end
+
+ describe 'MAX_VERSION_LENGTH' do
+ subject { described_class::MAX_VERSION_LENGTH }
+
+ it { is_expected.to eq(128) }
+ end
+end
diff --git a/gems/gitlab-utils/spec/spec_helper.rb b/gems/gitlab-utils/spec/spec_helper.rb
new file mode 100644
index 00000000000..5dc3859f77d
--- /dev/null
+++ b/gems/gitlab-utils/spec/spec_helper.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails'
+require 'rspec/mocks'
+require 'rspec-benchmark'
+require 'rspec-parameterized'
+
+require 'gitlab/rspec/all'
+require 'gitlab/utils/all'
+
+RSpec.configure do |config|
+ config.include RSpec::Benchmark::Matchers
+
+ # Enable flags like --only-failures and --next-failure
+ config.example_status_persistence_file_path = ".rspec_status"
+
+ # Disable RSpec exposing methods globally on `Module` and `main`
+ config.disable_monkey_patching!
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end