diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-05 19:20:45 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-05 19:20:45 +0300 |
commit | d298fad0c0564454271cba11e6f20c19681534ac (patch) | |
tree | 0a19d07d8b3bdd2574617305c300e404f2ace581 /spec/lib | |
parent | c9f9eec79cab801a50db698f682aacffbedf07f7 (diff) |
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc41
Diffstat (limited to 'spec/lib')
150 files changed, 5149 insertions, 3416 deletions
diff --git a/spec/lib/api/entities/user_spec.rb b/spec/lib/api/entities/user_spec.rb index 99ffe0eb925..e35deeb6263 100644 --- a/spec/lib/api/entities/user_spec.rb +++ b/spec/lib/api/entities/user_spec.rb @@ -23,4 +23,16 @@ RSpec.describe API::Entities::User do expect(subject).not_to include(:created_at) end + + it 'exposes user as not a bot' do + expect(subject[:bot]).to be_falsey + end + + context 'with bot user' do + let(:user) { create(:user, :security_bot) } + + it 'exposes user as a bot' do + expect(subject[:bot]).to eq(true) + end + end end diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb index 21ee40f22fe..5c8d4282118 100644 --- a/spec/lib/atlassian/jira_connect/client_spec.rb +++ b/spec/lib/atlassian/jira_connect/client_spec.rb @@ -18,15 +18,15 @@ RSpec.describe Atlassian::JiraConnect::Client do end end - around do |example| - freeze_time { example.run } - end - describe '.generate_update_sequence_id' do - it 'returns monotonic_time converted it to integer' do - allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(1.0) + it 'returns unix time in microseconds as integer', :aggregate_failures do + travel_to(Time.utc(1970, 1, 1, 0, 0, 1)) do + expect(described_class.generate_update_sequence_id).to eq(1000) + end - expect(described_class.generate_update_sequence_id).to eq(1) + travel_to(Time.utc(1970, 1, 1, 0, 0, 5)) do + expect(described_class.generate_update_sequence_id).to eq(5000) + end end end @@ -238,22 +238,6 @@ RSpec.describe Atlassian::JiraConnect::Client do expect(response['errorMessages']).to eq(%w(X Y Z)) end end - - it 'does not call the API if the feature flag is not enabled' do - stub_feature_flags(jira_sync_deployments: false) - - expect(subject).not_to receive(:post) - - subject.send(:store_deploy_info, project: project, deployments: deployments) - end - - it 'does call the API if the feature flag enabled for the project' do - stub_feature_flags(jira_sync_deployments: project) - - expect(subject).to receive(:post).with('/rest/deployments/0.1/bulk', { deployments: Array }).and_call_original - - subject.send(:store_deploy_info, project: project, deployments: deployments) - end end describe '#store_ff_info' do @@ -319,24 +303,6 @@ RSpec.describe Atlassian::JiraConnect::Client do expect(response['errorMessages']).to eq(['a: X', 'a: Y', 'b: Z']) end end - - it 'does not call the API if the feature flag is not enabled' do - stub_feature_flags(jira_sync_feature_flags: false) - - expect(subject).not_to receive(:post) - - subject.send(:store_ff_info, project: project, feature_flags: feature_flags) - end - - it 'does call the API if the feature flag enabled for the project' do - stub_feature_flags(jira_sync_feature_flags: project) - - expect(subject).to receive(:post).with('/rest/featureflags/0.1/bulk', { - flags: Array, properties: Hash - }).and_call_original - - subject.send(:store_ff_info, project: project, feature_flags: feature_flags) - end end describe '#store_build_info' do @@ -384,24 +350,6 @@ RSpec.describe Atlassian::JiraConnect::Client do subject.send(:store_build_info, project: project, pipelines: pipelines.take(1)) end - it 'does not call the API if the feature flag is not enabled' do - stub_feature_flags(jira_sync_builds: false) - - expect(subject).not_to receive(:post) - - subject.send(:store_build_info, project: project, pipelines: pipelines) - end - - it 'does call the API if the feature flag enabled for the project' do - stub_feature_flags(jira_sync_builds: project) - - expect(subject).to receive(:post) - .with('/rest/builds/0.1/bulk', { builds: Array }) - .and_call_original - - subject.send(:store_build_info, project: project, pipelines: pipelines) - end - context 'there are errors' do let(:failures) do [{ errors: [{ message: 'X' }, { message: 'Y' }] }, { errors: [{ message: 'Z' }] }] diff --git a/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb index 964801338cf..2d12cd1ed0a 100644 --- a/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb +++ b/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::FeatureFlagEntity do subject { described_class.represent(feature_flag) } context 'when the feature flag does not belong to any Jira issue' do - let_it_be(:feature_flag) { create(:operations_feature_flag) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) } describe '#issue_keys' do it 'is empty' do @@ -30,7 +30,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::FeatureFlagEntity do context 'when the feature flag does belong to a Jira issue' do let(:feature_flag) do - create(:operations_feature_flag, description: 'THING-123') + create(:operations_feature_flag, project: project, description: 'THING-123') end describe '#issue_keys' do @@ -66,6 +66,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::FeatureFlagEntity do end it 'has the correct summary' do + expect(entity.dig('summary', 'url')).to eq "http://localhost/#{project.full_path}/-/feature_flags/#{feature_flag.iid}/edit" expect(entity.dig('summary', 'status')).to eq( 'enabled' => true, 'defaultValue' => '', diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb index 450e396a389..90cd08ae9cc 100644 --- a/spec/lib/backup/files_spec.rb +++ b/spec/lib/backup/files_spec.rb @@ -150,7 +150,7 @@ RSpec.describe Backup::Files do it 'excludes tmp dirs from rsync' do expect(Gitlab::Popen).to receive(:popen) - .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup)) + .with(%w(rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup)) .and_return(['', 0]) subject.dump @@ -158,7 +158,7 @@ RSpec.describe Backup::Files do it 'retries if rsync fails due to vanishing files' do expect(Gitlab::Popen).to receive(:popen) - .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup)) + .with(%w(rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup)) .and_return(['rsync failed', 24], ['', 0]) expect do @@ -168,7 +168,7 @@ RSpec.describe Backup::Files do it 'raises an error and outputs an error message if rsync failed' do allow(Gitlab::Popen).to receive(:popen) - .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup)) + .with(%w(rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup)) .and_return(['rsync failed', 1]) expect do @@ -186,8 +186,8 @@ RSpec.describe Backup::Files do expect(subject.exclude_dirs(:tar)).to eq(['--exclude=lost+found', '--exclude=./@pages.tmp']) end - it 'prepends a leading slash to rsync excludes' do - expect(subject.exclude_dirs(:rsync)).to eq(['--exclude=lost+found', '--exclude=/@pages.tmp']) + it 'prepends a leading slash and app_files_dir basename to rsync excludes' do + expect(subject.exclude_dirs(:rsync)).to eq(['--exclude=lost+found', '--exclude=/gitlab-pages/@pages.tmp']) end end diff --git a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb index 1f886059bf6..81aa8d35ebc 100644 --- a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb +++ b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do stub_application_setting(asset_proxy_enabled: true) stub_application_setting(asset_proxy_secret_key: 'shared-secret') stub_application_setting(asset_proxy_url: 'https://assets.example.com') - stub_application_setting(asset_proxy_whitelist: %w(gitlab.com *.mydomain.com)) + stub_application_setting(asset_proxy_allowlist: %w(gitlab.com *.mydomain.com)) described_class.initialize_settings @@ -39,16 +39,26 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do expect(Gitlab.config.asset_proxy.domain_regexp).to eq(/^(gitlab\.com|.*?\.mydomain\.com)$/i) end - context 'when whitelist is empty' do + context 'when allowlist is empty' do it 'defaults to the install domain' do stub_application_setting(asset_proxy_enabled: true) - stub_application_setting(asset_proxy_whitelist: []) + stub_application_setting(asset_proxy_allowlist: []) described_class.initialize_settings expect(Gitlab.config.asset_proxy.allowlist).to eq [Gitlab.config.gitlab.host] end end + + it 'supports deprecated whitelist settings' do + stub_application_setting(asset_proxy_enabled: true) + stub_application_setting(asset_proxy_whitelist: %w(foo.com bar.com)) + stub_application_setting(asset_proxy_allowlist: []) + + described_class.initialize_settings + + expect(Gitlab.config.asset_proxy.allowlist).to eq %w(foo.com bar.com) + end end context 'when properly configured' do diff --git a/spec/lib/banzai/filter/truncate_source_filter_spec.rb b/spec/lib/banzai/filter/truncate_source_filter_spec.rb index b0c6d91daa8..d5eb8b738b1 100644 --- a/spec/lib/banzai/filter/truncate_source_filter_spec.rb +++ b/spec/lib/banzai/filter/truncate_source_filter_spec.rb @@ -22,7 +22,7 @@ RSpec.describe Banzai::Filter::TruncateSourceFilter do it 'truncates UTF-8 text by bytes, on a character boundary' do utf8_text = '日本語の文字が大きい' - truncated = '日…' + truncated = '日...' expect(filter(utf8_text, limit: truncated.bytesize)).to eq(truncated) expect(filter(utf8_text, limit: utf8_text.bytesize)).to eq(utf8_text) diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb index 9391ca386cf..bcee6f8f65d 100644 --- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb @@ -131,4 +131,16 @@ RSpec.describe Banzai::Pipeline::FullPipeline do expect(output).to include("test [[<em>TOC</em>]]") end end + + describe 'backslash escapes' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:issue) { create(:issue, project: project) } + + it 'does not convert an escaped reference' do + markdown = "\\#{issue.to_reference}" + output = described_class.to_html(markdown, project: project) + + expect(output).to include("<span>#</span>#{issue.iid}") + end + end end diff --git a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb new file mode 100644 index 00000000000..241d6db4f11 --- /dev/null +++ b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do + using RSpec::Parameterized::TableSyntax + + describe 'backslash escapes' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:issue) { create(:issue, project: project) } + + def correct_html_included(markdown, expected) + result = described_class.call(markdown, {}) + + expect(result[:output].to_html).to include(expected) + + result + end + + context 'when feature flag honor_escaped_markdown is disabled' do + before do + stub_feature_flags(honor_escaped_markdown: false) + end + + it 'does not escape the markdown' do + result = described_class.call(%q(\!), project: project) + output = result[:output].to_html + + expect(output).to eq('<p data-sourcepos="1:1-1:2">!</p>') + expect(result[:escaped_literals]).to be_falsey + end + end + + # Test strings taken from https://spec.commonmark.org/0.29/#backslash-escapes + describe 'CommonMark tests', :aggregate_failures do + it 'converts all ASCII punctuation to literals' do + markdown = %q(\!\"\#\$\%\&\'\*\+\,\-\.\/\:\;\<\=\>\?\@\[\]\^\_\`\{\|\}\~) + %q[\(\)\\\\] + punctuation = %w(! " # $ % & ' * + , - . / : ; < = > ? @ [ \\ ] ^ _ ` { | } ~) + %w[( )] + + result = described_class.call(markdown, project: project) + output = result[:output].to_html + + punctuation.each { |char| expect(output).to include("<span>#{char}</span>") } + expect(result[:escaped_literals]).to be_truthy + end + + it 'does not convert other characters to literals' do + markdown = %q(\→\A\a\ \3\φ\«) + expected = '\→\A\a\ \3\φ\«' + + result = correct_html_included(markdown, expected) + expect(result[:escaped_literals]).to be_falsey + end + + describe 'escaped characters are treated as regular characters and do not have their usual Markdown meanings' do + where(:markdown, :expected) do + %q(\*not emphasized*) | %q(<span>*</span>not emphasized*) + %q(\<br/> not a tag) | %q(<span><</span>br/> not a tag) + %q!\[not a link](/foo)! | %q!<span>[</span>not a link](/foo)! + %q(\`not code`) | %q(<span>`</span>not code`) + %q(1\. not a list) | %q(1<span>.</span> not a list) + %q(\# not a heading) | %q(<span>#</span> not a heading) + %q(\[foo]: /url "not a reference") | %q(<span>[</span>foo]: /url "not a reference") + %q(\ö not a character entity) | %q(<span>&</span>ouml; not a character entity) + end + + with_them do + it 'keeps them as literals' do + correct_html_included(markdown, expected) + end + end + end + + it 'backslash is itself escaped, the following character is not' do + markdown = %q(\\\\*emphasis*) + expected = %q(<span>\</span><em>emphasis</em>) + + correct_html_included(markdown, expected) + end + + it 'backslash at the end of the line is a hard line break' do + markdown = <<~MARKDOWN + foo\\ + bar + MARKDOWN + expected = "foo<br>\nbar" + + correct_html_included(markdown, expected) + end + + describe 'backslash escapes do not work in code blocks, code spans, autolinks, or raw HTML' do + where(:markdown, :expected) do + %q(`` \[\` ``) | %q(<code>\[\`</code>) + %q( \[\]) | %Q(<code>\\[\\]\n</code>) + %Q(~~~\n\\[\\]\n~~~) | %Q(<code>\\[\\]\n</code>) + %q(<http://example.com?find=\*>) | %q(<a href="http://example.com?find=%5C*">http://example.com?find=\*</a>) + %q[<a href="/bar\/)">] | %q[<a href="/bar%5C/)">] + end + + with_them do + it { correct_html_included(markdown, expected) } + end + end + + describe 'work in all other contexts, including URLs and link titles, link references, and info strings in fenced code blocks' do + where(:markdown, :expected) do + %q![foo](/bar\* "ti\*tle")! | %q(<a href="/bar*" title="ti*tle">foo</a>) + %Q![foo]\n\n[foo]: /bar\\* "ti\\*tle"! | %q(<a href="/bar*" title="ti*tle">foo</a>) + %Q(``` foo\\+bar\nfoo\n```) | %Q(<code lang="foo+bar">foo\n</code>) + end + + with_them do + it { correct_html_included(markdown, expected) } + end + end + end + end +end diff --git a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb index f0498f41b61..c628d8d5b41 100644 --- a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb @@ -30,6 +30,6 @@ RSpec.describe Banzai::Pipeline::PreProcessPipeline do result = described_class.call(text, limit: 12) - expect(result[:output]).to eq('foo foo f…') + expect(result[:output]).to eq('foo foo f...') end end diff --git a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb index 2abd3df20fd..80607485b6e 100644 --- a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb +++ b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb @@ -5,8 +5,18 @@ require 'spec_helper' RSpec.describe BulkImports::Common::Extractors::GraphqlExtractor do let(:graphql_client) { instance_double(BulkImports::Clients::Graphql) } let(:import_entity) { create(:bulk_import_entity) } - let(:response) { double(original_hash: { foo: :bar }) } - let(:query) { { query: double(to_s: 'test', variables: {}) } } + let(:response) { double(original_hash: { 'data' => { 'foo' => 'bar' }, 'page_info' => {} }) } + let(:options) do + { + query: double( + to_s: 'test', + variables: {}, + data_path: %w[data foo], + page_info_path: %w[data page_info] + ) + } + end + let(:context) do instance_double( BulkImports::Pipeline::Context, @@ -14,58 +24,20 @@ RSpec.describe BulkImports::Common::Extractors::GraphqlExtractor do ) end - subject { described_class.new(query) } - - before do - allow(subject).to receive(:graphql_client).and_return(graphql_client) - allow(graphql_client).to receive(:parse) - end + subject { described_class.new(options) } describe '#extract' do before do - allow(subject).to receive(:query_variables).and_return({}) - allow(graphql_client).to receive(:execute).and_return(response) - end - - it 'returns original hash' do - expect(subject.extract(context)).to eq({ foo: :bar }) - end - end - - describe 'query variables' do - before do + allow(subject).to receive(:graphql_client).and_return(graphql_client) + allow(graphql_client).to receive(:parse) allow(graphql_client).to receive(:execute).and_return(response) end - context 'when variables are present' do - let(:variables) { { foo: :bar } } - let(:query) { { query: double(to_s: 'test', variables: variables) } } - - it 'builds graphql query variables for import entity' do - expect(graphql_client).to receive(:execute).with(anything, variables) - - subject.extract(context).first - end - end - - context 'when no variables are present' do - let(:query) { { query: double(to_s: 'test', variables: nil) } } - - it 'returns empty hash' do - expect(graphql_client).to receive(:execute).with(anything, nil) - - subject.extract(context).first - end - end - - context 'when variables are empty hash' do - let(:query) { { query: double(to_s: 'test', variables: {}) } } - - it 'makes graphql request with empty hash' do - expect(graphql_client).to receive(:execute).with(anything, {}) + it 'returns ExtractedData' do + extracted_data = subject.extract(context) - subject.extract(context).first - end + expect(extracted_data).to be_instance_of(BulkImports::Pipeline::ExtractedData) + expect(extracted_data.data).to contain_exactly('bar') end end end diff --git a/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb b/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb index 4de7d95172f..57ffdfa9aee 100644 --- a/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb +++ b/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb @@ -7,7 +7,7 @@ RSpec.describe BulkImports::Common::Loaders::EntityLoader do it "creates entities for the given data" do group = create(:group, path: "imported-group") parent_entity = create(:bulk_import_entity, group: group, bulk_import: create(:bulk_import)) - context = instance_double(BulkImports::Pipeline::Context, entity: parent_entity) + context = BulkImports::Pipeline::Context.new(parent_entity) data = { source_type: :group_entity, diff --git a/spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb b/spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb deleted file mode 100644 index 2b33701653e..00000000000 --- a/spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe BulkImports::Common::Transformers::HashKeyDigger do - describe '#transform' do - it 'when the key_path is an array' do - data = { foo: { bar: :value } } - key_path = %i[foo bar] - transformed = described_class.new(key_path: key_path).transform(nil, data) - - expect(transformed).to eq(:value) - end - - it 'when the key_path is not an array' do - data = { foo: { bar: :value } } - key_path = :foo - transformed = described_class.new(key_path: key_path).transform(nil, data) - - expect(transformed).to eq({ bar: :value }) - end - - it "when the data is not a hash" do - expect { described_class.new(key_path: nil).transform(nil, nil) } - .to raise_error(ArgumentError, "Given data must be a Hash") - end - end -end diff --git a/spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb deleted file mode 100644 index cdffa750694..00000000000 --- a/spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe BulkImports::Common::Transformers::UnderscorifyKeysTransformer do - describe '#transform' do - it 'deep underscorifies hash keys' do - data = { - 'fullPath' => 'Foo', - 'snakeKeys' => { - 'snakeCaseKey' => 'Bar', - 'moreKeys' => { - 'anotherSnakeCaseKey' => 'Test' - } - } - } - - transformed_data = described_class.new.transform(nil, data) - - expect(transformed_data).to have_key('full_path') - expect(transformed_data).to have_key('snake_keys') - expect(transformed_data['snake_keys']).to have_key('snake_case_key') - expect(transformed_data['snake_keys']).to have_key('more_keys') - expect(transformed_data.dig('snake_keys', 'more_keys')).to have_key('another_snake_case_key') - 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 new file mode 100644 index 00000000000..627247c04ab --- /dev/null +++ b/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Extractors::SubgroupsExtractor do + describe '#extract' do + it 'returns ExtractedData response' do + bulk_import = create(:bulk_import) + create(:bulk_import_configuration, bulk_import: bulk_import) + entity = create(:bulk_import_entity, bulk_import: bulk_import) + response = [{ 'test' => 'group' }] + context = BulkImports::Pipeline::Context.new(entity) + + allow_next_instance_of(BulkImports::Clients::Http) do |client| + allow(client).to receive(:each_page).and_return(response) + end + + extracted_data = subject.extract(context) + + expect(extracted_data).to be_instance_of(BulkImports::Pipeline::ExtractedData) + expect(extracted_data.data).to eq(response) + end + end +end 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 new file mode 100644 index 00000000000..78f99c19346 --- /dev/null +++ b/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Graphql::GetGroupQuery do + describe '#variables' do + let(:entity) { double(source_full_path: 'test') } + + it 'returns query variables based on entity information' do + expected = { full_path: entity.source_full_path } + + expect(described_class.variables(entity)).to eq(expected) + end + end + + describe '#data_path' do + it 'returns data path' do + expected = %w[data group] + + expect(described_class.data_path).to eq(expected) + end + end + + describe '#page_info_path' do + it 'returns pagination information path' do + expected = %w[data group page_info] + + expect(described_class.page_info_path).to eq(expected) + end + end +end 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 new file mode 100644 index 00000000000..2d8f4fb399a --- /dev/null +++ b/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Graphql::GetLabelsQuery do + describe '#variables' do + let(:entity) { double(source_full_path: 'test', next_page_for: 'next_page') } + + it 'returns query variables based on entity information' do + expected = { full_path: entity.source_full_path, cursor: entity.next_page_for } + + expect(described_class.variables(entity)).to eq(expected) + end + end + + describe '#data_path' do + it 'returns data path' do + expected = %w[data group labels nodes] + + expect(described_class.data_path).to eq(expected) + end + end + + describe '#page_info_path' do + it 'returns pagination information path' do + expected = %w[data group labels page_info] + + expect(described_class.page_info_path).to eq(expected) + end + end +end 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 b14dfc615a9..183292722d2 100644 --- a/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb +++ b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb @@ -7,21 +7,20 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do let(:user) { create(:user) } let(:data) { { foo: :bar } } let(:service_double) { instance_double(::Groups::CreateService) } - let(:entity) { create(:bulk_import_entity) } - let(:context) do - instance_double( - BulkImports::Pipeline::Context, - entity: entity, - current_user: user - ) - end + 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) } subject { described_class.new } context 'when user can create group' do shared_examples 'calls Group Create Service to create a new group' do it 'calls Group Create Service to create a new group' do - expect(::Groups::CreateService).to receive(:new).with(context.current_user, data).and_return(service_double) + expect(::Groups::CreateService) + .to receive(:new) + .with(context.current_user, data) + .and_return(service_double) + expect(service_double).to receive(:execute) expect(entity).to receive(:update!) diff --git a/spec/lib/bulk_imports/groups/loaders/labels_loader_spec.rb b/spec/lib/bulk_imports/groups/loaders/labels_loader_spec.rb new file mode 100644 index 00000000000..ac2f9c8cb1d --- /dev/null +++ b/spec/lib/bulk_imports/groups/loaders/labels_loader_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Loaders::LabelsLoader do + describe '#load' do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:entity) { create(:bulk_import_entity, group: group) } + let(:context) { BulkImports::Pipeline::Context.new(entity) } + + let(:data) do + { + 'title' => 'label', + 'description' => 'description', + 'color' => '#FFFFFF' + } + end + + it 'creates the label' do + expect { subject.load(context, data) }.to change(Label, :count).by(1) + + label = group.labels.first + + expect(label.title).to eq(data['title']) + expect(label.description).to eq(data['description']) + expect(label.color).to eq(data['color']) + end + 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 1a91f3d7a78..1a6a955544a 100644 --- a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb @@ -6,42 +6,33 @@ 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 create( :bulk_import_entity, + bulk_import: bulk_import, source_full_path: 'source/full/path', destination_name: 'My Destination Group', destination_namespace: parent.full_path ) end - let(:context) do - BulkImports::Pipeline::Context.new( - current_user: user, - entity: entity - ) - end + let(:context) { BulkImports::Pipeline::Context.new(entity) } let(:group_data) do { - 'data' => { - 'group' => { - 'name' => 'source_name', - 'fullPath' => 'source/full/path', - 'visibility' => 'private', - 'projectCreationLevel' => 'developer', - 'subgroupCreationLevel' => 'maintainer', - 'description' => 'Group Description', - 'emailsDisabled' => true, - 'lfsEnabled' => false, - 'mentionsDisabled' => true - } - } + 'name' => 'source_name', + 'full_path' => 'source/full/path', + 'visibility' => 'private', + 'project_creation_level' => 'developer', + 'subgroup_creation_level' => 'maintainer', + 'description' => 'Group Description', + 'emails_disabled' => true, + 'lfs_enabled' => false, + 'mentions_disabled' => true } end - subject { described_class.new } - before do allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| allow(extractor).to receive(:extract).and_return([group_data]) @@ -60,13 +51,13 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do expect(imported_group).not_to be_nil expect(imported_group.parent).to eq(parent) expect(imported_group.path).to eq(group_path) - expect(imported_group.description).to eq(group_data.dig('data', 'group', 'description')) - expect(imported_group.visibility).to eq(group_data.dig('data', 'group', 'visibility')) - expect(imported_group.project_creation_level).to eq(Gitlab::Access.project_creation_string_options[group_data.dig('data', 'group', 'projectCreationLevel')]) - expect(imported_group.subgroup_creation_level).to eq(Gitlab::Access.subgroup_creation_string_options[group_data.dig('data', 'group', 'subgroupCreationLevel')]) - expect(imported_group.lfs_enabled?).to eq(group_data.dig('data', 'group', 'lfsEnabled')) - expect(imported_group.emails_disabled?).to eq(group_data.dig('data', 'group', 'emailsDisabled')) - expect(imported_group.mentions_disabled?).to eq(group_data.dig('data', 'group', 'mentionsDisabled')) + expect(imported_group.description).to eq(group_data['description']) + expect(imported_group.visibility).to eq(group_data['visibility']) + expect(imported_group.project_creation_level).to eq(Gitlab::Access.project_creation_string_options[group_data['project_creation_level']]) + expect(imported_group.subgroup_creation_level).to eq(Gitlab::Access.subgroup_creation_string_options[group_data['subgroup_creation_level']]) + expect(imported_group.lfs_enabled?).to eq(group_data['lfs_enabled']) + expect(imported_group.emails_disabled?).to eq(group_data['emails_disabled']) + expect(imported_group.mentions_disabled?).to eq(group_data['mentions_disabled']) end end @@ -87,8 +78,6 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do it 'has transformers' do expect(described_class.transformers) .to contain_exactly( - { klass: BulkImports::Common::Transformers::HashKeyDigger, options: { key_path: %w[data group] } }, - { klass: BulkImports::Common::Transformers::UnderscorifyKeysTransformer, options: nil }, { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil }, { klass: BulkImports::Groups::Transformers::GroupAttributesTransformer, options: nil } ) diff --git a/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb new file mode 100644 index 00000000000..7c96967971f --- /dev/null +++ b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:cursor) { 'cursor' } + let(: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(:context) { BulkImports::Pipeline::Context.new(entity) } + + def extractor_data(title:, has_next_page:, cursor: nil) + data = [ + { + 'title' => title, + 'description' => 'desc', + 'color' => '#428BCA' + } + ] + + page_info = { + 'end_cursor' => cursor, + 'has_next_page' => has_next_page + } + + BulkImports::Pipeline::ExtractedData.new(data: data, 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) + + allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| + allow(extractor) + .to receive(:extract) + .and_return(first_page, last_page) + end + + expect { subject.run(context) }.to change(Label, :count).by(2) + + label = group.labels.order(:created_at).last + + expect(label.title).to eq('label2') + expect(label.description).to eq('desc') + expect(label.color).to eq('#428BCA') + 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).with(context) + + subject.after_run(context, 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).with(context) + + subject.after_run(context, 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 '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::GraphqlExtractor, + options: { + query: BulkImports::Groups::Graphql::GetLabelsQuery + } + ) + end + + it 'has transformers' do + expect(described_class.transformers) + .to contain_exactly( + { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil } + ) + end + + it 'has loaders' do + expect(described_class.get_loader).to eq(klass: BulkImports::Groups::Loaders::LabelsLoader, options: nil) + end + 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 e5a8ed7f47d..71d1ffdeba3 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 @@ -14,13 +14,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do ) end - let(:context) do - instance_double( - BulkImports::Pipeline::Context, - current_user: user, - entity: parent_entity - ) - end + let(:context) { BulkImports::Pipeline::Context.new(parent_entity) } let(:subgroup_data) do [ 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 28a7859915d..5a7a51675d6 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 @@ -7,22 +7,18 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer 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 - instance_double( - BulkImports::Entity, + create( + :bulk_import_entity, + bulk_import: bulk_import, source_full_path: 'source/full/path', destination_name: group.name, destination_namespace: parent.full_path ) end - let(:context) do - instance_double( - BulkImports::Pipeline::Context, - current_user: user, - entity: entity - ) - end + let(:context) { BulkImports::Pipeline::Context.new(entity) } let(:data) do { @@ -85,16 +81,16 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do end context 'when destination namespace is user namespace' do - let(:entity) do - instance_double( - BulkImports::Entity, + 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: user.namespace.full_path ) - end + context = BulkImports::Pipeline::Context.new(entity) - it 'does not set parent id' do transformed_data = subject.transform(context, data) expect(transformed_data).not_to have_key('parent_id') diff --git a/spec/lib/bulk_imports/importers/group_importer_spec.rb b/spec/lib/bulk_imports/importers/group_importer_spec.rb index 87baf1b8026..0884a51ce7d 100644 --- a/spec/lib/bulk_imports/importers/group_importer_spec.rb +++ b/spec/lib/bulk_imports/importers/group_importer_spec.rb @@ -7,23 +7,18 @@ RSpec.describe BulkImports::Importers::GroupImporter do let(:bulk_import) { create(:bulk_import) } let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import) } let(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) } - let(:context) do - BulkImports::Pipeline::Context.new( - current_user: user, - entity: bulk_import_entity, - configuration: bulk_import_configuration - ) - end - - subject { described_class.new(bulk_import_entity) } + 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::LabelsPipeline, context: context expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) if Gitlab.ee? expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context diff --git a/spec/lib/bulk_imports/pipeline/context_spec.rb b/spec/lib/bulk_imports/pipeline/context_spec.rb index e9af6313ca4..c8c3fe3a861 100644 --- a/spec/lib/bulk_imports/pipeline/context_spec.rb +++ b/spec/lib/bulk_imports/pipeline/context_spec.rb @@ -3,25 +3,29 @@ require 'spec_helper' RSpec.describe BulkImports::Pipeline::Context do - describe '#initialize' do - it 'initializes with permitted attributes' do - args = { - current_user: create(:user), - entity: create(:bulk_import_entity), - configuration: create(:bulk_import_configuration) - } + let(:group) { instance_double(Group) } + let(:user) { instance_double(User) } + let(:bulk_import) { instance_double(BulkImport, user: user, configuration: :config) } - context = described_class.new(args) + let(:entity) do + instance_double( + BulkImports::Entity, + bulk_import: bulk_import, + group: group + ) + end + + subject { described_class.new(entity) } - args.each do |k, v| - expect(context.public_send(k)).to eq(v) - end - end + describe '#group' do + it { expect(subject.group).to eq(group) } + end + + describe '#current_user' do + it { expect(subject.current_user).to eq(user) } + end - context 'when invalid argument is passed' do - it 'raises NoMethodError' do - expect { described_class.new(test: 'test').test }.to raise_exception(NoMethodError) - end - end + describe '#current_user' do + it { expect(subject.configuration).to eq(bulk_import.configuration) } end end diff --git a/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb b/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb new file mode 100644 index 00000000000..25c5178227a --- /dev/null +++ b/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Pipeline::ExtractedData do + let(:data) { 'data' } + let(:has_next_page) { true } + let(:cursor) { 'cursor' } + let(:page_info) do + { + 'has_next_page' => has_next_page, + 'end_cursor' => cursor + } + end + + subject { described_class.new(data: data, page_info: page_info) } + + describe '#has_next_page?' do + context 'when next page is present' do + it 'returns true' do + expect(subject.has_next_page?).to eq(true) + end + end + + context 'when next page is not present' do + let(:has_next_page) { false } + + it 'returns false' do + expect(subject.has_next_page?).to eq(false) + end + end + end + + describe '#next_page' do + it 'returns next page cursor information' do + expect(subject.next_page).to eq(cursor) + end + end + + describe '#each' do + context 'when block is present' do + it 'yields each data item' do + expect { |b| subject.each(&b) }.to yield_control + end + end + + context 'when block is not present' do + it 'returns enumerator' do + expect(subject.each).to be_instance_of(Enumerator) + end + end + end +end diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb index 60833e83dcc..3f6a172beee 100644 --- a/spec/lib/bulk_imports/pipeline/runner_spec.rb +++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb @@ -45,42 +45,71 @@ RSpec.describe BulkImports::Pipeline::Runner do end context 'when entity is not marked as failed' do - let(:context) do - instance_double( - BulkImports::Pipeline::Context, - entity: instance_double(BulkImports::Entity, id: 1, source_type: 'group', failed?: false) - ) - end + let(:entity) { create(:bulk_import_entity) } + let(:context) { BulkImports::Pipeline::Context.new(entity) } it 'runs pipeline extractor, transformer, loader' do - entries = [{ foo: :bar }] + extracted_data = BulkImports::Pipeline::ExtractedData.new(data: { foo: :bar }) expect_next_instance_of(BulkImports::Extractor) do |extractor| - expect(extractor).to receive(:extract).with(context).and_return(entries) + expect(extractor) + .to receive(:extract) + .with(context) + .and_return(extracted_data) end expect_next_instance_of(BulkImports::Transformer) do |transformer| - expect(transformer).to receive(:transform).with(context, entries.first).and_return(entries.first) + expect(transformer) + .to receive(:transform) + .with(context, extracted_data.data.first) + .and_return(extracted_data.data.first) end expect_next_instance_of(BulkImports::Loader) do |loader| - expect(loader).to receive(:load).with(context, entries.first) + expect(loader) + .to receive(:load) + .with(context, extracted_data.data.first) end expect_next_instance_of(Gitlab::Import::Logger) do |logger| expect(logger).to receive(:info) .with( + bulk_import_entity_id: entity.id, + bulk_import_entity_type: 'group_entity', message: 'Pipeline started', + pipeline_class: 'BulkImports::MyPipeline' + ) + expect(logger).to receive(:info) + .with( + bulk_import_entity_id: entity.id, + bulk_import_entity_type: 'group_entity', pipeline_class: 'BulkImports::MyPipeline', - bulk_import_entity_id: 1, - bulk_import_entity_type: 'group' + pipeline_step: :extractor, + step_class: 'BulkImports::Extractor' ) expect(logger).to receive(:info) - .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', extractor: 'BulkImports::Extractor') + .with( + bulk_import_entity_id: entity.id, + bulk_import_entity_type: 'group_entity', + pipeline_class: 'BulkImports::MyPipeline', + pipeline_step: :transformer, + step_class: 'BulkImports::Transformer' + ) expect(logger).to receive(:info) - .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', transformer: 'BulkImports::Transformer') + .with( + bulk_import_entity_id: entity.id, + bulk_import_entity_type: 'group_entity', + pipeline_class: 'BulkImports::MyPipeline', + pipeline_step: :loader, + step_class: 'BulkImports::Loader' + ) expect(logger).to receive(:info) - .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', loader: 'BulkImports::Loader') + .with( + bulk_import_entity_id: entity.id, + bulk_import_entity_type: 'group_entity', + message: 'Pipeline finished', + pipeline_class: 'BulkImports::MyPipeline' + ) end BulkImports::MyPipeline.new.run(context) @@ -88,7 +117,7 @@ RSpec.describe BulkImports::Pipeline::Runner do context 'when exception is raised' do let(:entity) { create(:bulk_import_entity, :created) } - let(:context) { BulkImports::Pipeline::Context.new(entity: entity) } + let(:context) { BulkImports::Pipeline::Context.new(entity) } before do allow_next_instance_of(BulkImports::Extractor) do |extractor| @@ -103,6 +132,7 @@ RSpec.describe BulkImports::Pipeline::Runner do expect(failure).to be_present expect(failure.pipeline_class).to eq('BulkImports::MyPipeline') + expect(failure.pipeline_step).to eq('extractor') expect(failure.exception_class).to eq('StandardError') expect(failure.exception_message).to eq('Error!') end @@ -144,21 +174,19 @@ RSpec.describe BulkImports::Pipeline::Runner do end context 'when entity is marked as failed' do - let(:context) do - instance_double( - BulkImports::Pipeline::Context, - entity: instance_double(BulkImports::Entity, id: 1, source_type: 'group', failed?: true) - ) - end + let(:entity) { create(:bulk_import_entity) } + let(:context) { BulkImports::Pipeline::Context.new(entity) } it 'logs and returns without execution' do + allow(entity).to receive(:failed?).and_return(true) + expect_next_instance_of(Gitlab::Import::Logger) do |logger| expect(logger).to receive(:info) .with( message: 'Skipping due to failed pipeline status', pipeline_class: 'BulkImports::MyPipeline', - bulk_import_entity_id: 1, - bulk_import_entity_type: 'group' + bulk_import_entity_id: entity.id, + bulk_import_entity_type: 'group_entity' ) end diff --git a/spec/lib/gitlab/access/branch_protection_spec.rb b/spec/lib/gitlab/access/branch_protection_spec.rb index 9b736a30c7e..44c30d1f596 100644 --- a/spec/lib/gitlab/access/branch_protection_spec.rb +++ b/spec/lib/gitlab/access/branch_protection_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe Gitlab::Access::BranchProtection do - describe '#any?' do - using RSpec::Parameterized::TableSyntax + using RSpec::Parameterized::TableSyntax + describe '#any?' do where(:level, :result) do Gitlab::Access::PROTECTION_NONE | false Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true @@ -19,8 +19,6 @@ RSpec.describe Gitlab::Access::BranchProtection do end describe '#developer_can_push?' do - using RSpec::Parameterized::TableSyntax - where(:level, :result) do Gitlab::Access::PROTECTION_NONE | false Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true @@ -36,8 +34,6 @@ RSpec.describe Gitlab::Access::BranchProtection do end describe '#developer_can_merge?' do - using RSpec::Parameterized::TableSyntax - where(:level, :result) do Gitlab::Access::PROTECTION_NONE | false Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false @@ -53,8 +49,6 @@ RSpec.describe Gitlab::Access::BranchProtection do end describe '#fully_protected?' do - using RSpec::Parameterized::TableSyntax - where(:level, :result) do Gitlab::Access::PROTECTION_NONE | false Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false diff --git a/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb b/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb index 3d782272d7e..f23fdd3fbcb 100644 --- a/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb +++ b/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb @@ -19,6 +19,9 @@ RSpec.describe Gitlab::Auth::IpRateLimiter, :use_clean_rails_memory_store_cachin before do stub_rack_attack_setting(options) + Rack::Attack.reset! + Rack::Attack.clear_configuration + Gitlab::RackAttack.configure(Rack::Attack) end after do diff --git a/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb b/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb new file mode 100644 index 00000000000..deddc7f5294 --- /dev/null +++ b/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::U2fWebauthnConverter do + let_it_be(:u2f_registration) do + device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5)) + create(:u2f_registration, name: 'u2f_device', + certificate: Base64.strict_encode64(device.cert_raw), + key_handle: U2F.urlsafe_encode64(device.key_handle_raw), + public_key: Base64.strict_encode64(device.origin_public_key_raw)) + end + + it 'converts u2f registration' do + webauthn_credential = WebAuthn::U2fMigrator.new( + app_id: Gitlab.config.gitlab.url, + certificate: u2f_registration.certificate, + key_handle: u2f_registration.key_handle, + public_key: u2f_registration.public_key, + counter: u2f_registration.counter + ).credential + + converted_webauthn = described_class.new(u2f_registration).convert + + expect(converted_webauthn).to( + include(user_id: u2f_registration.user_id, + credential_xid: Base64.strict_encode64(webauthn_credential.id))) + end +end diff --git a/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb new file mode 100644 index 00000000000..47e1d4620cd --- /dev/null +++ b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindings do + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:users) { table(:users) } + let(:user) { create_user! } + let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) } + let(:scanners) { table(:vulnerability_scanners) } + let!(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') } + let!(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') } + let!(:scanner3) { scanners.create!(project_id: project.id, external_id: 'test 3', name: 'test scanner 3') } + let!(:unrelated_scanner) { scanners.create!(project_id: project.id, external_id: 'unreleated_scanner', name: 'unrelated scanner') } + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerability_findings) { table(:vulnerability_occurrences) } + let(:vulnerability_identifiers) { table(:vulnerability_identifiers) } + let(:vulnerability_identifier) do + vulnerability_identifiers.create!( + project_id: project.id, + external_type: 'vulnerability-identifier', + external_id: 'vulnerability-identifier', + fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a', + name: 'vulnerability identifier') + end + + let!(:first_finding) do + create_finding!( + uuid: "test1", + vulnerability_id: nil, + report_type: 0, + location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76', + primary_identifier_id: vulnerability_identifier.id, + scanner_id: scanner.id, + project_id: project.id + ) + end + + let!(:first_duplicate) do + create_finding!( + uuid: "test2", + vulnerability_id: nil, + report_type: 0, + location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76', + primary_identifier_id: vulnerability_identifier.id, + scanner_id: scanner2.id, + project_id: project.id + ) + end + + let!(:second_duplicate) do + create_finding!( + uuid: "test3", + vulnerability_id: nil, + report_type: 0, + location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76', + primary_identifier_id: vulnerability_identifier.id, + scanner_id: scanner3.id, + project_id: project.id + ) + end + + let!(:unrelated_finding) do + create_finding!( + uuid: "unreleated_finding", + vulnerability_id: nil, + report_type: 1, + location_fingerprint: 'random_location_fingerprint', + primary_identifier_id: vulnerability_identifier.id, + scanner_id: unrelated_scanner.id, + project_id: project.id + ) + end + + subject { described_class.new.perform(first_finding.id, unrelated_finding.id) } + + before do + stub_const("#{described_class}::DELETE_BATCH_SIZE", 1) + end + + it "removes entries which would result in duplicate UUIDv5" do + expect(vulnerability_findings.count).to eq(4) + + expect { subject }.to change { vulnerability_findings.count }.from(4).to(2) + + expect(vulnerability_findings.pluck(:id)).to eq([second_duplicate.id, unrelated_finding.id]) + end + + private + + def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type + ) + end + + # rubocop:disable Metrics/ParameterLists + def create_finding!( + vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, + name: "test", severity: 7, confidence: 7, report_type: 0, + project_fingerprint: '123qweasdzxc', location_fingerprint: 'test', + metadata_version: 'test', raw_metadata: 'test', uuid: 'test') + vulnerability_findings.create!( + vulnerability_id: vulnerability_id, + project_id: project_id, + name: name, + severity: severity, + confidence: confidence, + report_type: report_type, + project_fingerprint: project_fingerprint, + scanner_id: scanner_id, + primary_identifier_id: vulnerability_identifier.id, + location_fingerprint: location_fingerprint, + metadata_version: metadata_version, + raw_metadata: raw_metadata, + uuid: uuid + ) + end + # rubocop:enable Metrics/ParameterLists + + def create_user!(name: "Example User", email: "user@example.com", user_type: nil, created_at: Time.zone.now, confirmed_at: Time.zone.now) + users.create!( + name: name, + email: email, + username: name, + projects_limit: 0, + user_type: user_type, + confirmed_at: confirmed_at + ) + end +end diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb index 052a01a8dd8..5b20572578c 100644 --- a/spec/lib/gitlab/background_migration_spec.rb +++ b/spec/lib/gitlab/background_migration_spec.rb @@ -55,7 +55,7 @@ RSpec.describe Gitlab::BackgroundMigration do expect(described_class).to receive(:perform) .with('Foo', [10, 20]) - described_class.steal('Foo') { |(arg1, arg2)| arg1 == 10 && arg2 == 20 } + described_class.steal('Foo') { |job| job.args.second.first == 10 && job.args.second.second == 20 } end it 'does not steal jobs that do not match the predicate' do diff --git a/spec/lib/gitlab/changelog/committer_spec.rb b/spec/lib/gitlab/changelog/committer_spec.rb new file mode 100644 index 00000000000..f0d6bc2b6b5 --- /dev/null +++ b/spec/lib/gitlab/changelog/committer_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Changelog::Committer do + let(:project) { create(:project, :repository) } + let(:user) { project.creator } + let(:committer) { described_class.new(project, user) } + let(:config) { Gitlab::Changelog::Config.new(project) } + + describe '#commit' do + context "when the release isn't in the changelog" do + it 'commits the changes' do + release = Gitlab::Changelog::Release + .new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config) + + committer.commit( + release: release, + file: 'CHANGELOG.md', + branch: 'master', + message: 'Test commit' + ) + + content = project.repository.blob_at('master', 'CHANGELOG.md').data + + expect(content).to eq(<<~MARKDOWN) + ## 1.0.0 (2020-01-01) + + No changes. + MARKDOWN + end + end + + context 'when the release is already in the changelog' do + it "doesn't commit the changes" do + release = Gitlab::Changelog::Release + .new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config) + + 2.times do + committer.commit( + release: release, + file: 'CHANGELOG.md', + branch: 'master', + message: 'Test commit' + ) + end + + content = project.repository.blob_at('master', 'CHANGELOG.md').data + + expect(content).to eq(<<~MARKDOWN) + ## 1.0.0 (2020-01-01) + + No changes. + MARKDOWN + end + end + + context 'when committing the changes fails' do + it 'retries the operation' do + release = Gitlab::Changelog::Release + .new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config) + + service = instance_spy(Files::MultiService) + errored = false + + allow(Files::MultiService) + .to receive(:new) + .and_return(service) + + allow(service).to receive(:execute) do + if errored + { status: :success } + else + errored = true + { status: :error } + end + end + + expect do + committer.commit( + release: release, + file: 'CHANGELOG.md', + branch: 'master', + message: 'Test commit' + ) + end.not_to raise_error + end + end + + context "when the changelog changes before saving the changes" do + it 'raises a CommitError' do + release1 = Gitlab::Changelog::Release + .new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config) + + release2 = Gitlab::Changelog::Release + .new(version: '2.0.0', date: Time.utc(2020, 1, 1), config: config) + + # This creates the initial commit we'll later use to see if the + # changelog changed before saving our changes. + committer.commit( + release: release1, + file: 'CHANGELOG.md', + branch: 'master', + message: 'Initial commit' + ) + + allow(Gitlab::Git::Commit) + .to receive(:last_for_path) + .with( + project.repository, + 'master', + 'CHANGELOG.md', + literal_pathspec: true + ) + .and_return(double(:commit, sha: 'foo')) + + expect do + committer.commit( + release: release2, + file: 'CHANGELOG.md', + branch: 'master', + message: 'Test commit' + ) + end.to raise_error(described_class::CommitError) + end + end + end +end diff --git a/spec/lib/gitlab/changelog/config_spec.rb b/spec/lib/gitlab/changelog/config_spec.rb new file mode 100644 index 00000000000..adf82fa3ac2 --- /dev/null +++ b/spec/lib/gitlab/changelog/config_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Changelog::Config do + let(:project) { build_stubbed(:project) } + + describe '.from_git' do + it 'retrieves the configuration from Git' do + allow(project.repository) + .to receive(:changelog_config) + .and_return("---\ndate_format: '%Y'") + + expect(described_class) + .to receive(:from_hash) + .with(project, 'date_format' => '%Y') + + described_class.from_git(project) + end + + it 'returns the default configuration when no YAML file exists in Git' do + allow(project.repository) + .to receive(:changelog_config) + .and_return(nil) + + expect(described_class) + .to receive(:new) + .with(project) + + described_class.from_git(project) + end + end + + describe '.from_hash' do + it 'sets the configuration according to a Hash' do + config = described_class.from_hash( + project, + 'date_format' => 'foo', + 'template' => 'bar', + 'categories' => { 'foo' => 'bar' } + ) + + expect(config.date_format).to eq('foo') + expect(config.template).to be_instance_of(Gitlab::Changelog::Template::Template) + expect(config.categories).to eq({ 'foo' => 'bar' }) + end + + it 'raises ConfigError when the categories are not a Hash' do + expect { described_class.from_hash(project, 'categories' => 10) } + .to raise_error(described_class::ConfigError) + end + end + + describe '#contributor?' do + it 'returns true if a user is a contributor' do + user = build_stubbed(:author) + + allow(project.team).to receive(:contributor?).with(user).and_return(true) + + expect(described_class.new(project).contributor?(user)).to eq(true) + end + + it "returns true if a user isn't a contributor" do + user = build_stubbed(:author) + + allow(project.team).to receive(:contributor?).with(user).and_return(false) + + expect(described_class.new(project).contributor?(user)).to eq(false) + end + end + + describe '#category' do + it 'returns the name of a category' do + config = described_class.new(project) + + config.categories['foo'] = 'Foo' + + expect(config.category('foo')).to eq('Foo') + end + + it 'returns the raw category name when no alternative name is configured' do + config = described_class.new(project) + + expect(config.category('bla')).to eq('bla') + end + end + + describe '#format_date' do + it 'formats a date according to the configured date format' do + config = described_class.new(project) + time = Time.utc(2021, 1, 5) + + expect(config.format_date(time)).to eq('2021-01-05') + end + end +end diff --git a/spec/lib/gitlab/changelog/generator_spec.rb b/spec/lib/gitlab/changelog/generator_spec.rb new file mode 100644 index 00000000000..bc4a7c5dd6b --- /dev/null +++ b/spec/lib/gitlab/changelog/generator_spec.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Changelog::Generator do + describe '#add' do + let(:project) { build_stubbed(:project) } + let(:author) { build_stubbed(:user) } + let(:commit) { build_stubbed(:commit) } + let(:config) { Gitlab::Changelog::Config.new(project) } + + it 'generates the Markdown for the first release' do + release = Gitlab::Changelog::Release.new( + version: '1.0.0', + date: Time.utc(2021, 1, 5), + config: config + ) + + release.add_entry( + title: 'This is a new change', + commit: commit, + category: 'added', + author: author + ) + + gen = described_class.new('') + + expect(gen.add(release)).to eq(<<~MARKDOWN) + ## 1.0.0 (2021-01-05) + + ### added (1 change) + + - [This is a new change](#{commit.to_reference(full: true)}) + MARKDOWN + end + + it 'generates the Markdown for a newer release' do + release = Gitlab::Changelog::Release.new( + version: '2.0.0', + date: Time.utc(2021, 1, 5), + config: config + ) + + release.add_entry( + title: 'This is a new change', + commit: commit, + category: 'added', + author: author + ) + + gen = described_class.new(<<~MARKDOWN) + This is a changelog file. + + ## 1.0.0 + + This is the changelog for version 1.0.0. + MARKDOWN + + expect(gen.add(release)).to eq(<<~MARKDOWN) + This is a changelog file. + + ## 2.0.0 (2021-01-05) + + ### added (1 change) + + - [This is a new change](#{commit.to_reference(full: true)}) + + ## 1.0.0 + + This is the changelog for version 1.0.0. + MARKDOWN + end + + it 'generates the Markdown for a patch release' do + release = Gitlab::Changelog::Release.new( + version: '1.1.0', + date: Time.utc(2021, 1, 5), + config: config + ) + + release.add_entry( + title: 'This is a new change', + commit: commit, + category: 'added', + author: author + ) + + gen = described_class.new(<<~MARKDOWN) + This is a changelog file. + + ## 2.0.0 + + This is another release. + + ## 1.0.0 + + This is the changelog for version 1.0.0. + MARKDOWN + + expect(gen.add(release)).to eq(<<~MARKDOWN) + This is a changelog file. + + ## 2.0.0 + + This is another release. + + ## 1.1.0 (2021-01-05) + + ### added (1 change) + + - [This is a new change](#{commit.to_reference(full: true)}) + + ## 1.0.0 + + This is the changelog for version 1.0.0. + MARKDOWN + end + + it 'generates the Markdown for an old release' do + release = Gitlab::Changelog::Release.new( + version: '0.5.0', + date: Time.utc(2021, 1, 5), + config: config + ) + + release.add_entry( + title: 'This is a new change', + commit: commit, + category: 'added', + author: author + ) + + gen = described_class.new(<<~MARKDOWN) + This is a changelog file. + + ## 2.0.0 + + This is another release. + + ## 1.0.0 + + This is the changelog for version 1.0.0. + MARKDOWN + + expect(gen.add(release)).to eq(<<~MARKDOWN) + This is a changelog file. + + ## 2.0.0 + + This is another release. + + ## 1.0.0 + + This is the changelog for version 1.0.0. + + ## 0.5.0 (2021-01-05) + + ### added (1 change) + + - [This is a new change](#{commit.to_reference(full: true)}) + MARKDOWN + end + end +end diff --git a/spec/lib/gitlab/changelog/release_spec.rb b/spec/lib/gitlab/changelog/release_spec.rb new file mode 100644 index 00000000000..50a23d23299 --- /dev/null +++ b/spec/lib/gitlab/changelog/release_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Changelog::Release do + describe '#to_markdown' do + let(:config) { Gitlab::Changelog::Config.new(build_stubbed(:project)) } + let(:commit) { build_stubbed(:commit) } + let(:author) { build_stubbed(:user) } + let(:mr) { build_stubbed(:merge_request) } + let(:release) do + described_class + .new(version: '1.0.0', date: Time.utc(2021, 1, 5), config: config) + end + + context 'when there are no entries' do + it 'includes a notice about the lack of entries' do + expect(release.to_markdown).to eq(<<~OUT) + ## 1.0.0 (2021-01-05) + + No changes. + + OUT + end + end + + context 'when all data is present' do + it 'includes all data' do + allow(config).to receive(:contributor?).with(author).and_return(true) + + release.add_entry( + title: 'Entry title', + commit: commit, + category: 'fixed', + author: author, + merge_request: mr + ) + + expect(release.to_markdown).to eq(<<~OUT) + ## 1.0.0 (2021-01-05) + + ### fixed (1 change) + + - [Entry title](#{commit.to_reference(full: true)}) \ + by #{author.to_reference(full: true)} \ + ([merge request](#{mr.to_reference(full: true)})) + + OUT + end + end + + context 'when no merge request is present' do + it "doesn't include a merge request link" do + allow(config).to receive(:contributor?).with(author).and_return(true) + + release.add_entry( + title: 'Entry title', + commit: commit, + category: 'fixed', + author: author + ) + + expect(release.to_markdown).to eq(<<~OUT) + ## 1.0.0 (2021-01-05) + + ### fixed (1 change) + + - [Entry title](#{commit.to_reference(full: true)}) \ + by #{author.to_reference(full: true)} + + OUT + end + end + + context 'when the author is not a contributor' do + it "doesn't include the author" do + allow(config).to receive(:contributor?).with(author).and_return(false) + + release.add_entry( + title: 'Entry title', + commit: commit, + category: 'fixed', + author: author + ) + + expect(release.to_markdown).to eq(<<~OUT) + ## 1.0.0 (2021-01-05) + + ### fixed (1 change) + + - [Entry title](#{commit.to_reference(full: true)}) + + OUT + end + end + end + + describe '#header_start_position' do + it 'returns a regular expression for finding the start of a release section' do + config = Gitlab::Changelog::Config.new(build_stubbed(:project)) + release = described_class + .new(version: '1.0.0', date: Time.utc(2021, 1, 5), config: config) + + expect(release.header_start_pattern).to eq(/^##\s*1\.0\.0/) + end + end +end diff --git a/spec/lib/gitlab/changelog/template/compiler_spec.rb b/spec/lib/gitlab/changelog/template/compiler_spec.rb new file mode 100644 index 00000000000..8b09bc90529 --- /dev/null +++ b/spec/lib/gitlab/changelog/template/compiler_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Changelog::Template::Compiler do + def compile(template, data = {}) + Gitlab::Changelog::Template::Compiler.new.compile(template).render(data) + end + + describe '#compile' do + it 'compiles an empty template' do + expect(compile('')).to eq('') + end + + it 'compiles a template with an undefined variable' do + expect(compile('{{number}}')).to eq('') + end + + it 'compiles a template with a defined variable' do + expect(compile('{{number}}', 'number' => 42)).to eq('42') + end + + it 'compiles a template with the special "it" variable' do + expect(compile('{{it}}', 'values' => 10)).to eq({ 'values' => 10 }.to_s) + end + + it 'compiles a template containing an if statement' do + expect(compile('{% if foo %}yes{% end %}', 'foo' => true)).to eq('yes') + end + + it 'compiles a template containing an if/else statement' do + expect(compile('{% if foo %}yes{% else %}no{% end %}', 'foo' => false)) + .to eq('no') + end + + it 'compiles a template that iterates over an Array' do + expect(compile('{% each numbers %}{{it}}{% end %}', 'numbers' => [1, 2, 3])) + .to eq('123') + end + + it 'compiles a template that iterates over a Hash' do + output = compile( + '{% each pairs %}{{0}}={{1}}{% end %}', + 'pairs' => { 'key' => 'value' } + ) + + expect(output).to eq('key=value') + end + + it 'compiles a template that iterates over a Hash of Arrays' do + output = compile( + '{% each values %}{{key}}{% end %}', + 'values' => [{ 'key' => 'value' }] + ) + + expect(output).to eq('value') + end + + it 'compiles a template with a variable path' do + output = compile('{{foo.bar}}', 'foo' => { 'bar' => 10 }) + + expect(output).to eq('10') + end + + it 'compiles a template with a variable path that uses an Array index' do + output = compile('{{foo.values.1}}', 'foo' => { 'values' => [10, 20] }) + + expect(output).to eq('20') + end + + it 'compiles a template with a variable path that uses a Hash and a numeric index' do + output = compile('{{foo.1}}', 'foo' => { 'key' => 'value' }) + + expect(output).to eq('') + end + + it 'compiles a template with a variable path that uses an Array and a String based index' do + output = compile('{{foo.numbers.bla}}', 'foo' => { 'numbers' => [10, 20] }) + + expect(output).to eq('') + end + + it 'ignores ERB tags provided by the user' do + input = '<% exit %> <%= exit %> <%= foo -%>' + + expect(compile(input)).to eq(input) + end + + it 'removes newlines introduced by end statements on their own lines' do + output = compile(<<~TPL, 'foo' => true) + {% if foo %} + foo + {% end %} + TPL + + expect(output).to eq("foo\n") + end + + it 'supports escaping of trailing newlines' do + output = compile(<<~TPL) + foo \ + bar\ + baz + TPL + + expect(output).to eq("foo barbaz\n") + end + + # rubocop: disable Lint/InterpolationCheck + it 'ignores embedded Ruby expressions' do + input = '#{exit}' + + expect(compile(input)).to eq(input) + end + # rubocop: enable Lint/InterpolationCheck + + it 'ignores ERB tags inside variable tags' do + input = '{{<%= exit %>}}' + + expect(compile(input)).to eq(input) + end + + it 'ignores malicious code that tries to escape a variable' do + input = "{{') ::Kernel.exit # '}}" + + expect(compile(input)).to eq(input) + end + + it 'ignores malicious code that makes use of whitespace' do + input = "x<\\\n%::Kernel.system(\"id\")%>" + + expect(Kernel).not_to receive(:system).with('id') + expect(compile(input)).to eq('x<%::Kernel.system("id")%>') + end + end +end diff --git a/spec/lib/gitlab/badge/coverage/metadata_spec.rb b/spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb index 725ae03ad74..6d272f060ab 100644 --- a/spec/lib/gitlab/badge/coverage/metadata_spec.rb +++ b/spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/badge/shared/metadata' +require 'lib/gitlab/ci/badge/shared/metadata' -RSpec.describe Gitlab::Badge::Coverage::Metadata do +RSpec.describe Gitlab::Ci::Badge::Coverage::Metadata do let(:badge) do double(project: create(:project), ref: 'feature', job: 'test') end diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/ci/badge/coverage/report_spec.rb index 3b5ea3291e4..13696d815aa 100644 --- a/spec/lib/gitlab/badge/coverage/report_spec.rb +++ b/spec/lib/gitlab/ci/badge/coverage/report_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Badge::Coverage::Report do +RSpec.describe Gitlab::Ci::Badge::Coverage::Report do let_it_be(:project) { create(:project) } let_it_be(:success_pipeline) { create(:ci_pipeline, :success, project: project) } let_it_be(:running_pipeline) { create(:ci_pipeline, :running, project: project) } diff --git a/spec/lib/gitlab/badge/coverage/template_spec.rb b/spec/lib/gitlab/ci/badge/coverage/template_spec.rb index ba5c1b2ce6e..f010d1bce50 100644 --- a/spec/lib/gitlab/badge/coverage/template_spec.rb +++ b/spec/lib/gitlab/ci/badge/coverage/template_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Badge::Coverage::Template do +RSpec.describe Gitlab::Ci::Badge::Coverage::Template do let(:badge) { double(entity: 'coverage', status: 90.00, customization: {}) } let(:template) { described_class.new(badge) } diff --git a/spec/lib/gitlab/badge/pipeline/metadata_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb index c8ed0c8ea29..2f677237fad 100644 --- a/spec/lib/gitlab/badge/pipeline/metadata_spec.rb +++ b/spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/badge/shared/metadata' +require 'lib/gitlab/ci/badge/shared/metadata' -RSpec.describe Gitlab::Badge::Pipeline::Metadata do +RSpec.describe Gitlab::Ci::Badge::Pipeline::Metadata do let(:badge) { double(project: create(:project), ref: 'feature') } let(:metadata) { described_class.new(badge) } diff --git a/spec/lib/gitlab/badge/pipeline/status_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb index b5dabca0477..45d0d781090 100644 --- a/spec/lib/gitlab/badge/pipeline/status_spec.rb +++ b/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Badge::Pipeline::Status do +RSpec.describe Gitlab::Ci::Badge::Pipeline::Status do let(:project) { create(:project, :repository) } let(:sha) { project.commit.sha } let(:branch) { 'master' } diff --git a/spec/lib/gitlab/badge/pipeline/template_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/template_spec.rb index c78e95852f3..696bb62b4d6 100644 --- a/spec/lib/gitlab/badge/pipeline/template_spec.rb +++ b/spec/lib/gitlab/ci/badge/pipeline/template_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Badge::Pipeline::Template do +RSpec.describe Gitlab::Ci::Badge::Pipeline::Template do let(:badge) { double(entity: 'pipeline', status: 'success', customization: {}) } let(:template) { described_class.new(badge) } diff --git a/spec/lib/gitlab/badge/shared/metadata.rb b/spec/lib/gitlab/ci/badge/shared/metadata.rb index c99a65bb2f4..c99a65bb2f4 100644 --- a/spec/lib/gitlab/badge/shared/metadata.rb +++ b/spec/lib/gitlab/ci/badge/shared/metadata.rb diff --git a/spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb new file mode 100644 index 00000000000..f50c6e99e99 --- /dev/null +++ b/spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Build::Credentials::Registry::DependencyProxy do + let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) } + let(:gitlab_url) { 'gitlab.example.com:443' } + + subject { described_class.new(build) } + + before do + stub_config_setting(host: 'gitlab.example.com', port: 443) + end + + it 'contains valid dependency proxy credentials' do + expect(subject).to be_kind_of(described_class) + + expect(subject.username).to eq 'gitlab-ci-token' + expect(subject.password).to eq build.token + expect(subject.url).to eq gitlab_url + expect(subject.type).to eq 'registry' + end + + describe '.valid?' do + subject { described_class.new(build).valid? } + + context 'when dependency proxy is enabled' do + before do + stub_config(dependency_proxy: { enabled: true }) + end + + it { is_expected.to be_truthy } + end + + context 'when dependency proxy is disabled' do + before do + stub_config(dependency_proxy: { enabled: false }) + end + + it { is_expected.to be_falsey } + end + end +end diff --git a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb index c0a76973f60..43913e91085 100644 --- a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb +++ b/spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Build::Credentials::Registry do +RSpec.describe Gitlab::Ci::Build::Credentials::Registry::GitlabRegistry do let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) } let(:registry_url) { 'registry.example.com:5005' } diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb index a1af5b75f87..0b50def05d4 100644 --- a/spec/lib/gitlab/ci/build/rules_spec.rb +++ b/spec/lib/gitlab/ci/build/rules_spec.rb @@ -201,40 +201,13 @@ RSpec.describe Gitlab::Ci::Build::Rules do end describe '#build_attributes' do - let(:seed_attributes) { {} } - subject(:build_attributes) do - result.build_attributes(seed_attributes) + result.build_attributes end it 'compacts nil values' do is_expected.to eq(options: {}, when: 'on_success') end - - context 'when there are variables in rules' do - let(:variables) { { VAR1: 'new var 1', VAR3: 'var 3' } } - - context 'when there are seed variables' do - let(:seed_attributes) do - { yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }] } - end - - it 'returns yaml_variables with override' do - is_expected.to include( - yaml_variables: [{ key: 'VAR1', value: 'new var 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }] - ) - end - end - - context 'when there is not seed variables' do - it 'does not return yaml_variables' do - is_expected.not_to have_key(:yaml_variables) - end - end - end end describe '#pass?' do diff --git a/spec/lib/gitlab/ci/charts_spec.rb b/spec/lib/gitlab/ci/charts_spec.rb index cfc2019a89b..46d7d4a58f0 100644 --- a/spec/lib/gitlab/ci/charts_spec.rb +++ b/spec/lib/gitlab/ci/charts_spec.rb @@ -9,6 +9,10 @@ RSpec.describe Gitlab::Ci::Charts do subject { chart.to } + before do + create(:ci_empty_pipeline, project: project, duration: 120) + end + it 'goes until the end of the current month (including the whole last day of the month)' do is_expected.to eq(Date.today.end_of_month.end_of_day) end @@ -20,6 +24,10 @@ RSpec.describe Gitlab::Ci::Charts do it 'uses %B %Y as labels format' do expect(chart.labels).to include(chart.from.strftime('%B %Y')) end + + it 'returns count of pipelines run each day in the current year' do + expect(chart.total.sum).to eq(1) + end end context 'monthchart' do @@ -28,6 +36,10 @@ RSpec.describe Gitlab::Ci::Charts do subject { chart.to } + before do + create(:ci_empty_pipeline, project: project, duration: 120) + end + it 'includes the whole current day' do is_expected.to eq(Date.today.end_of_day) end @@ -39,6 +51,10 @@ RSpec.describe Gitlab::Ci::Charts do it 'uses %d %B as labels format' do expect(chart.labels).to include(chart.from.strftime('%d %B')) end + + it 'returns count of pipelines run each day in the current month' do + expect(chart.total.sum).to eq(1) + end end context 'weekchart' do @@ -47,6 +63,10 @@ RSpec.describe Gitlab::Ci::Charts do subject { chart.to } + before do + create(:ci_empty_pipeline, project: project, duration: 120) + end + it 'includes the whole current day' do is_expected.to eq(Date.today.end_of_day) end @@ -58,6 +78,68 @@ RSpec.describe Gitlab::Ci::Charts do it 'uses %d %B as labels format' do expect(chart.labels).to include(chart.from.strftime('%d %B')) end + + it 'returns count of pipelines run each day in the current week' do + expect(chart.total.sum).to eq(1) + end + end + + context 'weekchart_utc' do + today = Date.today + end_of_today = Time.use_zone(Time.find_zone('UTC')) { today.end_of_day } + + let(:project) { create(:project) } + let(:chart) do + allow(Date).to receive(:today).and_return(today) + allow(today).to receive(:end_of_day).and_return(end_of_today) + Gitlab::Ci::Charts::WeekChart.new(project) + end + + subject { chart.total } + + before do + create(:ci_empty_pipeline, project: project, duration: 120) + end + + it 'uses a utc time zone for range times' do + expect(chart.to.zone).to eq(end_of_today.zone) + expect(chart.from.zone).to eq(end_of_today.zone) + end + + it 'returns count of pipelines run each day in the current week' do + expect(chart.total.sum).to eq(1) + end + end + + context 'weekchart_non_utc' do + today = Date.today + end_of_today = Time.use_zone(Time.find_zone('Asia/Dubai')) { today.end_of_day } + + let(:project) { create(:project) } + let(:chart) do + allow(Date).to receive(:today).and_return(today) + allow(today).to receive(:end_of_day).and_return(end_of_today) + Gitlab::Ci::Charts::WeekChart.new(project) + end + + subject { chart.total } + + before do + # The DB uses UTC always, so our use of a Time Zone in the application + # can cause the creation date of the pipeline to go unmatched depending + # on the offset. We can work around this by requesting the pipeline be + # created a with the `created_at` field set to a day ago in the same week. + create(:ci_empty_pipeline, project: project, duration: 120, created_at: today - 1.day) + end + + it 'uses a non-utc time zone for range times' do + expect(chart.to.zone).to eq(end_of_today.zone) + expect(chart.from.zone).to eq(end_of_today.zone) + end + + it 'returns count of pipelines run each day in the current week' do + expect(chart.total.sum).to eq(1) + end end context 'pipeline_times' do diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb index 80427eaa6ee..247f4b63910 100644 --- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Cache do + using RSpec::Parameterized::TableSyntax + subject(:entry) { described_class.new(config) } describe 'validations' do @@ -56,8 +58,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end context 'with `policy`' do - using RSpec::Parameterized::TableSyntax - where(:policy, :result) do 'pull-push' | 'pull-push' 'push' | 'push' @@ -77,8 +77,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end context 'with `when`' do - using RSpec::Parameterized::TableSyntax - where(:when_config, :result) do 'on_success' | 'on_success' 'on_failure' | 'on_failure' @@ -109,8 +107,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end context 'with `policy`' do - using RSpec::Parameterized::TableSyntax - where(:policy, :valid) do 'pull-push' | true 'push' | true @@ -126,8 +122,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end context 'with `when`' do - using RSpec::Parameterized::TableSyntax - where(:when_config, :valid) do 'on_success' | true 'on_failure' | true diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 7834a1a94f2..a3b5f32b9f9 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -763,16 +763,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it 'returns allow_failure_criteria' do expect(entry.value[:allow_failure_criteria]).to match(exit_codes: [42]) end - - context 'with ci_allow_failure_with_exit_codes disabled' do - before do - stub_feature_flags(ci_allow_failure_with_exit_codes: false) - end - - it 'does not return allow_failure_criteria' do - expect(entry.value.key?(:allow_failure_criteria)).to be_falsey - end - end end end end diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb index aadf94365c6..04e80450263 100644 --- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb @@ -73,6 +73,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do end end + context 'when resource_group key is not a string' do + let(:config) { { resource_group: 123 } } + + it 'returns error about wrong value type' do + expect(entry).not_to be_valid + expect(entry.errors).to include "job resource group should be a string" + end + end + context 'when it uses both "when:" and "rules:"' do let(:config) do { @@ -340,6 +349,26 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do end end + context 'with resource group' do + using RSpec::Parameterized::TableSyntax + + where(:resource_group, :result) do + 'iOS' | 'iOS' + 'review/$CI_COMMIT_REF_NAME' | 'review/$CI_COMMIT_REF_NAME' + nil | nil + end + + with_them do + let(:config) { { script: 'ls', resource_group: resource_group }.compact } + + it do + entry.compose!(deps) + + expect(entry.resource_group).to eq(result) + end + end + end + context 'with inheritance' do context 'of variables' do let(:config) do diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index 4fdaaca8316..99f546ceb37 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -323,20 +323,6 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do expect { subject }.to raise_error(described_class::AmbigiousSpecificationError) end end - - context 'when feature flag is turned off' do - let(:values) do - { include: full_local_file_path } - end - - before do - stub_feature_flags(variables_in_include_section_ci: false) - end - - it 'does not expand the variables' do - expect(subject[0].location).to eq('$CI_PROJECT_PATH' + local_file) - end - end end end end diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb index dd27b4045c9..15293429354 100644 --- a/spec/lib/gitlab/ci/cron_parser_spec.rb +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -63,6 +63,17 @@ RSpec.describe Gitlab::Ci::CronParser do end end + context 'when range and slash used' do + let(:cron) { '3-59/10 * * * *' } + let(:cron_timezone) { 'UTC' } + + it_behaves_like returns_time_for_epoch + + it 'returns specific time' do + expect(subject.min).to be_in([3, 13, 23, 33, 43, 53]) + end + end + context 'when cron_timezone is TZInfo format' do before do allow(Time).to receive(:zone) diff --git a/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb b/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb new file mode 100644 index 00000000000..30bcce21be2 --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Parsers::Instrumentation do + describe '#parse!' do + let(:parser_class) do + Class.new do + prepend Gitlab::Ci::Parsers::Instrumentation + + def parse!(arg1, arg2) + "parse #{arg1} #{arg2}" + end + end + end + + it 'sets metrics for duration of parsing' do + result = parser_class.new.parse!('hello', 'world') + + expect(result).to eq('parse hello world') + + metrics = Gitlab::Metrics.registry.get(:ci_report_parser_duration_seconds).get({ parser: parser_class.name }) + + expect(metrics.keys).to match_array(described_class::BUCKETS) + end + end +end diff --git a/spec/lib/gitlab/ci/parsers_spec.rb b/spec/lib/gitlab/ci/parsers_spec.rb index b932cd81272..c9891c06507 100644 --- a/spec/lib/gitlab/ci/parsers_spec.rb +++ b/spec/lib/gitlab/ci/parsers_spec.rb @@ -54,4 +54,12 @@ RSpec.describe Gitlab::Ci::Parsers do end end end + + describe '.instrument!' do + it 'prepends the Instrumentation module into each parser' do + expect(described_class.parsers.values).to all( receive(:prepend).with(Gitlab::Ci::Parsers::Instrumentation) ) + + described_class.instrument! + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb index 3eaecb11ae0..1d17244e519 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb @@ -58,20 +58,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines do expect(build_statuses(child_pipeline)).to contain_exactly('canceled') end - - context 'when FF ci_auto_cancel_all_pipelines is disabled' do - before do - stub_feature_flags(ci_auto_cancel_all_pipelines: false) - end - - it 'does not cancel interruptible builds of child pipeline' do - expect(build_statuses(child_pipeline)).to contain_exactly('running') - - perform - - expect(build_statuses(child_pipeline)).to contain_exactly('running') - end - end end context 'when the child pipeline has not an interruptible job' do diff --git a/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb b/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb new file mode 100644 index 00000000000..8b177fa7fc1 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::CodequalityMrDiff do + let(:codequality_report) { Gitlab::Ci::Reports::CodequalityReports.new } + let(:degradation_1) { build(:codequality_degradation_1) } + let(:degradation_2) { build(:codequality_degradation_2) } + let(:degradation_3) { build(:codequality_degradation_3) } + + describe '#initialize!' do + subject(:report) { described_class.new(codequality_report) } + + context 'when quality has degradations' do + context 'with several degradations on the same line' do + before do + codequality_report.add_degradation(degradation_1) + codequality_report.add_degradation(degradation_2) + end + + it 'generates quality report for mr diff' do + expect(report.files).to match( + "file_a.rb" => [ + { line: 10, description: "Avoid parameter lists longer than 5 parameters. [12/5]", severity: "major" }, + { line: 10, description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", severity: "major" } + ] + ) + end + end + + context 'with several degradations on several files' do + before do + codequality_report.add_degradation(degradation_1) + codequality_report.add_degradation(degradation_2) + codequality_report.add_degradation(degradation_3) + end + + it 'returns quality report for mr diff' do + expect(report.files).to match( + "file_a.rb" => [ + { line: 10, description: "Avoid parameter lists longer than 5 parameters. [12/5]", severity: "major" }, + { line: 10, description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", severity: "major" } + ], + "file_b.rb" => [ + { line: 10, description: "Avoid parameter lists longer than 5 parameters. [12/5]", severity: "minor" } + ] + ) + end + end + end + + context 'when quality has no degradation' do + it 'returns an empty hash' do + expect(report.files).to match({}) + end + end + end +end 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 7053d54381b..90188b56f5a 100644 --- a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb @@ -6,62 +6,8 @@ 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) do - { - "categories": [ - "Complexity" - ], - "check_name": "argument_count", - "content": { - "body": "" - }, - "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", - "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547", - "location": { - "path": "foo.rb", - "lines": { - "begin": 10, - "end": 10 - } - }, - "other_locations": [], - "remediation_points": 900000, - "severity": "major", - "type": "issue", - "engine_name": "structure" - }.with_indifferent_access - end - - let(:degradation_2) do - { - "type": "Issue", - "check_name": "Rubocop/Metrics/ParameterLists", - "description": "Avoid parameter lists longer than 5 parameters. [12/5]", - "categories": [ - "Complexity" - ], - "remediation_points": 550000, - "location": { - "path": "foo.rb", - "positions": { - "begin": { - "column": 14, - "line": 10 - }, - "end": { - "column": 39, - "line": 10 - } - } - }, - "content": { - "body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count." - }, - "engine_name": "rubocop", - "fingerprint": "ab5f8b935886b942d621399f5a2ca16e", - "severity": "minor" - }.with_indifferent_access - end + let(:degradation_1) { build(:codequality_degradation_1) } + let(:degradation_2) { build(:codequality_degradation_2) } describe '#status' do subject(:report_status) { comparer.status } diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb index 44e67259369..ae9b2f2c62b 100644 --- a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb @@ -4,62 +4,8 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Reports::CodequalityReports do let(:codequality_report) { described_class.new } - let(:degradation_1) do - { - "categories": [ - "Complexity" - ], - "check_name": "argument_count", - "content": { - "body": "" - }, - "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", - "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547", - "location": { - "path": "foo.rb", - "lines": { - "begin": 10, - "end": 10 - } - }, - "other_locations": [], - "remediation_points": 900000, - "severity": "major", - "type": "issue", - "engine_name": "structure" - }.with_indifferent_access - end - - let(:degradation_2) do - { - "type": "Issue", - "check_name": "Rubocop/Metrics/ParameterLists", - "description": "Avoid parameter lists longer than 5 parameters. [12/5]", - "categories": [ - "Complexity" - ], - "remediation_points": 550000, - "location": { - "path": "foo.rb", - "positions": { - "begin": { - "column": 14, - "line": 10 - }, - "end": { - "column": 39, - "line": 10 - } - } - }, - "content": { - "body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count." - }, - "engine_name": "rubocop", - "fingerprint": "ab5f8b935886b942d621399f5a2ca16e", - "severity": "minor" - }.with_indifferent_access - end + let(:degradation_1) { build(:codequality_degradation_1) } + let(:degradation_2) { build(:codequality_degradation_2) } it { expect(codequality_report.degradations).to eq({}) } diff --git a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb index a2903391c6f..f09e03b4d55 100644 --- a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb +++ b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::Ci::Trace::ChunkedIO, :clean_gitlab_redis_cache do let(:chunked_io) { described_class.new(build) } before do - stub_feature_flags(ci_enable_live_trace: true) + stub_feature_flags(ci_enable_live_trace: true, gitlab_ci_trace_read_consistency: true) end describe "#initialize" do diff --git a/spec/lib/gitlab/ci/variables/helpers_spec.rb b/spec/lib/gitlab/ci/variables/helpers_spec.rb new file mode 100644 index 00000000000..b45abf8c0e1 --- /dev/null +++ b/spec/lib/gitlab/ci/variables/helpers_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Variables::Helpers do + describe '.merge_variables' do + let(:current_variables) do + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }] + end + + let(:new_variables) do + [{ key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] + end + + 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.merge_variables(current_variables, new_variables) } + + it { is_expected.to eq(result) } + + context 'when new variables is a hash' do + let(:new_variables) do + { 'key2' => 'value22', 'key3' => 'value3' } + end + + it { is_expected.to eq(result) } + end + + context 'when new variables is a hash with symbol keys' do + let(:new_variables) do + { key2: 'value22', key3: 'value3' } + end + + it { is_expected.to eq(result) } + end + + context 'when new variables is nil' do + let(:new_variables) {} + let(:result) do + [{ key: 'key1', value: 'value1', public: true }, + { key: 'key2', value: 'value2', public: true }] + end + + it { is_expected.to eq(result) } + end + end + + describe '.transform_to_yaml_variables' do + let(:variables) do + { 'key1' => 'value1', 'key2' => 'value2' } + end + + let(:result) do + [{ key: 'key1', value: 'value1', public: true }, + { key: 'key2', value: 'value2', public: true }] + end + + subject { described_class.transform_to_yaml_variables(variables) } + + it { is_expected.to eq(result) } + + context 'when variables is nil' do + let(:variables) {} + + it { is_expected.to eq([]) } + end + end + + describe '.transform_from_yaml_variables' do + let(:variables) do + [{ key: 'key1', value: 'value1', public: true }, + { key: 'key2', value: 'value2', public: true }] + end + + let(:result) do + { 'key1' => 'value1', 'key2' => 'value2' } + end + + subject { described_class.transform_from_yaml_variables(variables) } + + it { is_expected.to eq(result) } + + context 'when variables is nil' do + let(:variables) {} + + it { is_expected.to eq({}) } + end + + context 'when variables is a hash' do + let(:variables) do + { key1: 'value1', 'key2' => 'value2' } + end + + it { is_expected.to eq(result) } + end + end +end diff --git a/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb b/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb index 8a7425a4156..b5adb603dab 100644 --- a/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb +++ b/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb @@ -42,7 +42,8 @@ RSpec.describe Gitlab::Cleanup::OrphanJobArtifactFiles do end it 'stops when limit is reached' do - cleanup = described_class.new(limit: 1) + stub_env('LIMIT', 1) + cleanup = described_class.new mock_artifacts_found(cleanup, 'tmp/foo/bar/1', 'tmp/foo/bar/2') diff --git a/spec/lib/gitlab/cluster/lifecycle_events_spec.rb b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb new file mode 100644 index 00000000000..4ed68d54680 --- /dev/null +++ b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' + +RSpec.describe Gitlab::Cluster::LifecycleEvents do + # we create a new instance to ensure that we do not touch existing hooks + let(:replica) { Class.new(described_class) } + + context 'hooks execution' do + using RSpec::Parameterized::TableSyntax + + where(:method, :hook_names) do + :do_worker_start | %i[worker_start_hooks] + :do_before_fork | %i[before_fork_hooks] + :do_before_graceful_shutdown | %i[master_blackout_period master_graceful_shutdown] + :do_before_master_restart | %i[master_restart_hooks] + end + + before do + # disable blackout period to speed-up tests + stub_config(shutdown: { blackout_seconds: 0 }) + end + + with_them do + subject { replica.public_send(method) } + + it 'executes all hooks' do + hook_names.each do |hook_name| + hook = double + replica.instance_variable_set(:"@#{hook_name}", [hook]) + + # ensure that proper hooks are called + expect(hook).to receive(:call) + expect(replica).to receive(:call).with(hook_name, anything).and_call_original + end + + subject + end + end + end + + describe '#call' do + let(:name) { :my_hooks } + + subject { replica.send(:call, name, hooks) } + + context 'when many hooks raise exception' do + let(:hooks) do + [ + -> { raise 'Exception A' }, + -> { raise 'Exception B' } + ] + end + + context 'USE_FATAL_LIFECYCLE_EVENTS is set to default' do + it 'only first hook is executed and is fatal' do + expect(hooks[0]).to receive(:call).and_call_original + expect(hooks[1]).not_to receive(:call) + + expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original + expect(replica).to receive(:warn).with('ERROR: The hook my_hooks failed with exception (RuntimeError) "Exception A".') + + expect { subject }.to raise_error(described_class::FatalError, 'Exception A') + end + end + + context 'when USE_FATAL_LIFECYCLE_EVENTS is disabled' do + before do + stub_const('Gitlab::Cluster::LifecycleEvents::USE_FATAL_LIFECYCLE_EVENTS', false) + end + + it 'many hooks are executed and all exceptions are logged' do + expect(hooks[0]).to receive(:call).and_call_original + expect(hooks[1]).to receive(:call).and_call_original + + expect(Gitlab::ErrorTracking).to receive(:track_exception).twice.and_call_original + expect(replica).to receive(:warn).twice.and_call_original + + expect { subject }.not_to raise_error + end + end + end + end +end diff --git a/spec/lib/gitlab/composer/cache_spec.rb b/spec/lib/gitlab/composer/cache_spec.rb new file mode 100644 index 00000000000..00318ac14f9 --- /dev/null +++ b/spec/lib/gitlab/composer/cache_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Composer::Cache do + let_it_be(:package_name) { 'sample-project' } + let_it_be(:json) { { 'name' => package_name } } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json.to_json }, group: group) } + let(:branch) { project.repository.find_branch('master') } + let(:sha_regex) { /^[A-Fa-f0-9]{64}$/ } + + shared_examples 'Composer create cache page' do + let(:expected_json) { ::Gitlab::Composer::VersionIndex.new(packages).to_json } + + before do + stub_composer_cache_object_storage + end + + it 'creates the cached page' do + expect { subject }.to change { Packages::Composer::CacheFile.count }.by(1) + cache_file = Packages::Composer::CacheFile.last + expect(cache_file.file_sha256).to eq package.reload.composer_metadatum.version_cache_sha + expect(cache_file.file.read).to eq expected_json + end + end + + shared_examples 'Composer marks cache page for deletion' do + it 'marks the page for deletion' do + cache_file = Packages::Composer::CacheFile.last + + freeze_time do + expect { subject }.to change { cache_file.reload.delete_at}.from(nil).to(1.day.from_now) + end + end + end + + describe '#execute' do + subject { described_class.new(project: project, name: package_name).execute } + + context 'creating packages' do + context 'with a pre-existing package' do + let(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } + let(:package2) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '2.0.0', json: json) } + let(:packages) { [package, package2] } + + before do + package + described_class.new(project: project, name: package_name).execute + package.reload + package2 + end + + it 'updates the sha and creates the cache page' do + expect { subject }.to change { package2.reload.composer_metadatum.version_cache_sha }.from(nil).to(sha_regex) + .and change { package.reload.composer_metadatum.version_cache_sha }.to(sha_regex) + end + + it_behaves_like 'Composer create cache page' + it_behaves_like 'Composer marks cache page for deletion' + end + + context 'first package' do + let!(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } + let(:packages) { [package] } + + it 'updates the sha and creates the cache page' do + expect { subject }.to change { package.reload.composer_metadatum.version_cache_sha }.from(nil).to(sha_regex) + end + + it_behaves_like 'Composer create cache page' + end + end + + context 'updating packages' do + let(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } + let(:package2) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '2.0.0', json: json) } + let(:packages) { [package, package2] } + + before do + packages + + described_class.new(project: project, name: package_name).execute + + package.update!(version: '1.2.0') + package.reload + end + + it_behaves_like 'Composer create cache page' + it_behaves_like 'Composer marks cache page for deletion' + end + + context 'deleting packages' do + context 'when it is not the last package' do + let(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } + let(:package2) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '2.0.0', json: json) } + let(:packages) { [package] } + + before do + package + package2 + + described_class.new(project: project, name: package_name).execute + + package2.destroy! + end + + it_behaves_like 'Composer create cache page' + it_behaves_like 'Composer marks cache page for deletion' + end + + context 'when it is the last package' do + let!(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } + let!(:last_sha) do + described_class.new(project: project, name: package_name).execute + package.reload.composer_metadatum.version_cache_sha + end + + before do + package.destroy! + end + + subject { described_class.new(project: project, name: package_name, last_page_sha: last_sha).execute } + + it_behaves_like 'Composer marks cache page for deletion' + + it 'does not create a new page' do + expect { subject }.not_to change { Packages::Composer::CacheFile.count } + end + end + end + end +end diff --git a/spec/lib/gitlab/composer/version_index_spec.rb b/spec/lib/gitlab/composer/version_index_spec.rb index 4c4742d9f59..7b0ed703f42 100644 --- a/spec/lib/gitlab/composer/version_index_spec.rb +++ b/spec/lib/gitlab/composer/version_index_spec.rb @@ -15,7 +15,9 @@ RSpec.describe Gitlab::Composer::VersionIndex do let(:packages) { [package1, package2] } describe '#as_json' do - subject(:index) { described_class.new(packages).as_json } + subject(:package_index) { index['packages'][package_name] } + + let(:index) { described_class.new(packages).as_json } def expected_json(package) { @@ -32,10 +34,16 @@ RSpec.describe Gitlab::Composer::VersionIndex do end it 'returns the packages json' do - packages = index['packages'][package_name] + expect(package_index['1.0.0']).to eq(expected_json(package1)) + expect(package_index['2.0.0']).to eq(expected_json(package2)) + end + + context 'with an unordered list of packages' do + let(:packages) { [package2, package1] } - expect(packages['1.0.0']).to eq(expected_json(package1)) - expect(packages['2.0.0']).to eq(expected_json(package2)) + it 'returns the packages sorted by version' do + expect(package_index.keys).to eq ['1.0.0', '2.0.0'] + end end end diff --git a/spec/lib/gitlab/conan_token_spec.rb b/spec/lib/gitlab/conan_token_spec.rb index be1d3e757f5..00683cf6e47 100644 --- a/spec/lib/gitlab/conan_token_spec.rb +++ b/spec/lib/gitlab/conan_token_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::ConanToken do let(:jwt_secret) do OpenSSL::HMAC.hexdigest( - OpenSSL::Digest::SHA256.new, + OpenSSL::Digest.new('SHA256'), base_secret, described_class::HMAC_KEY ) diff --git a/spec/lib/gitlab/crypto_helper_spec.rb b/spec/lib/gitlab/crypto_helper_spec.rb index c07089d8ef0..024564ea213 100644 --- a/spec/lib/gitlab/crypto_helper_spec.rb +++ b/spec/lib/gitlab/crypto_helper_spec.rb @@ -19,21 +19,85 @@ RSpec.describe Gitlab::CryptoHelper do expect(encrypted).to match %r{\A[A-Za-z0-9+/=]+\z} 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 - let(:encrypted) { described_class.aes256_gcm_encrypt('some-value') } + before do + stub_feature_flags(dynamic_nonce_creation: false) + end + + 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) } + + it 'correctly decrypts encrypted string' do + decrypted = described_class.aes256_gcm_decrypt(encrypted) + + expect(decrypted).to eq 'some-value' + end + + it 'decrypts a value when it ends with a new line character' do + decrypted = described_class.aes256_gcm_decrypt(encrypted + "\n") - it 'correctly decrypts encrypted string' do - decrypted = described_class.aes256_gcm_decrypt(encrypted) + expect(decrypted).to eq 'some-value' + end - expect(decrypted).to eq 'some-value' + 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 - it 'decrypts a value when it ends with a new line character' do - decrypted = described_class.aes256_gcm_decrypt(encrypted + "\n") + 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 - expect(decrypted).to eq 'some-value' + it 'correctly decrypts encrypted string' do + decrypted = described_class.aes256_gcm_decrypt(encrypted) + + 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. + end end diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index 786db23ffc4..01aceec12c5 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -194,4 +194,32 @@ RSpec.describe Gitlab::CurrentSettings do end end end + + describe '#current_application_settings?', :use_clean_rails_memory_store_caching do + before do + allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_call_original + end + + it 'returns true when settings exist' do + create(:application_setting, + home_page_url: 'http://mydomain.com', + signup_enabled: false) + + expect(described_class.current_application_settings?).to eq(true) + end + + it 'returns false when settings do not exist' do + expect(described_class.current_application_settings?).to eq(false) + end + + context 'with cache', :request_store do + include_context 'with settings in cache' + + it 'returns an in-memory ApplicationSetting object' do + expect(ApplicationSetting).not_to receive(:current) + + expect(described_class.current_application_settings?).to eq(true) + end + end + end end diff --git a/spec/lib/gitlab/danger/base_linter_spec.rb b/spec/lib/gitlab/danger/base_linter_spec.rb deleted file mode 100644 index 0136a0278ae..00000000000 --- a/spec/lib/gitlab/danger/base_linter_spec.rb +++ /dev/null @@ -1,193 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' -require 'rspec-parameterized' -require_relative 'danger_spec_helper' - -require 'gitlab/danger/base_linter' - -RSpec.describe Gitlab::Danger::BaseLinter do - let(:commit_class) do - Struct.new(:message, :sha, :diff_parent) - end - - let(:commit_message) { 'A commit message' } - let(:commit) { commit_class.new(commit_message, anything, anything) } - - subject(:commit_linter) { described_class.new(commit) } - - describe '#failed?' do - context 'with no failures' do - it { expect(commit_linter).not_to be_failed } - end - - context 'with failures' do - before do - commit_linter.add_problem(:subject_too_long, described_class.subject_description) - end - - it { expect(commit_linter).to be_failed } - end - end - - describe '#add_problem' do - it 'stores messages in #failures' do - commit_linter.add_problem(:subject_too_long, '%s') - - expect(commit_linter.problems).to eq({ subject_too_long: described_class.problems_mapping[:subject_too_long] }) - end - end - - shared_examples 'a valid commit' do - it 'does not have any problem' do - commit_linter.lint_subject - - expect(commit_linter.problems).to be_empty - end - end - - describe '#lint_subject' do - context 'when subject valid' do - it_behaves_like 'a valid commit' - end - - context 'when subject is too short' do - let(:commit_message) { 'A B' } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:subject_too_short, described_class.subject_description) - - commit_linter.lint_subject - end - end - - context 'when subject is too long' do - let(:commit_message) { 'A B ' + 'C' * described_class::MAX_LINE_LENGTH } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:subject_too_long, described_class.subject_description) - - commit_linter.lint_subject - end - end - - context 'when ignoring length issues for subject having not-ready wording' do - using RSpec::Parameterized::TableSyntax - - let(:final_message) { 'A B C' } - - context 'when used as prefix' do - where(prefix: [ - 'WIP: ', - 'WIP:', - 'wIp:', - '[WIP] ', - '[WIP]', - '[draft]', - '[draft] ', - '(draft)', - '(draft) ', - 'draft - ', - 'draft: ', - 'draft:', - 'DRAFT:' - ]) - - with_them do - it 'does not have any problems' do - commit_message = prefix + final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size) - commit = commit_class.new(commit_message, anything, anything) - - linter = described_class.new(commit).lint_subject - - expect(linter.problems).to be_empty - end - end - end - - context 'when used as suffix' do - where(suffix: %w[WIP draft]) - - with_them do - it 'does not have any problems' do - commit_message = final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size) + suffix - commit = commit_class.new(commit_message, anything, anything) - - linter = described_class.new(commit).lint_subject - - expect(linter.problems).to be_empty - end - end - end - end - - context 'when subject does not have enough words and is too long' do - let(:commit_message) { 'A ' + 'B' * described_class::MAX_LINE_LENGTH } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:subject_too_short, described_class.subject_description) - expect(commit_linter).to receive(:add_problem).with(:subject_too_long, described_class.subject_description) - - commit_linter.lint_subject - end - end - - context 'when subject starts with lowercase' do - let(:commit_message) { 'a B C' } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:subject_starts_with_lowercase, described_class.subject_description) - - commit_linter.lint_subject - end - end - - [ - '[ci skip] A commit message', - '[Ci skip] A commit message', - '[API] A commit message', - 'api: A commit message', - 'API: A commit message', - 'API: a commit message', - 'API: a commit message' - ].each do |message| - context "when subject is '#{message}'" do - let(:commit_message) { message } - - it 'does not add a problem' do - expect(commit_linter).not_to receive(:add_problem) - - commit_linter.lint_subject - end - end - end - - [ - '[ci skip]A commit message', - '[Ci skip] A commit message', - '[ci skip] a commit message', - 'api: a commit message', - '! A commit message' - ].each do |message| - context "when subject is '#{message}'" do - let(:commit_message) { message } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:subject_starts_with_lowercase, described_class.subject_description) - - commit_linter.lint_subject - end - end - end - - context 'when subject ends with a period' do - let(:commit_message) { 'A B C.' } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:subject_ends_with_a_period, described_class.subject_description) - - commit_linter.lint_subject - end - end - end -end diff --git a/spec/lib/gitlab/danger/changelog_spec.rb b/spec/lib/gitlab/danger/changelog_spec.rb deleted file mode 100644 index 04c515f1205..00000000000 --- a/spec/lib/gitlab/danger/changelog_spec.rb +++ /dev/null @@ -1,229 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' -require_relative 'danger_spec_helper' - -require 'gitlab/danger/changelog' - -RSpec.describe Gitlab::Danger::Changelog do - include DangerSpecHelper - - let(:added_files) { nil } - let(:fake_git) { double('fake-git', added_files: added_files) } - - let(:mr_labels) { nil } - let(:mr_json) { nil } - let(:fake_gitlab) { double('fake-gitlab', mr_labels: mr_labels, mr_json: mr_json) } - - let(:changes_by_category) { nil } - let(:sanitize_mr_title) { nil } - let(:ee?) { false } - let(:fake_helper) { double('fake-helper', changes_by_category: changes_by_category, sanitize_mr_title: sanitize_mr_title, ee?: ee?) } - - let(:fake_danger) { new_fake_danger.include(described_class) } - - subject(:changelog) { fake_danger.new(git: fake_git, gitlab: fake_gitlab, helper: fake_helper) } - - describe '#required?' do - subject { changelog.required? } - - context 'added files contain a migration' do - [ - 'db/migrate/20200000000000_new_migration.rb', - 'db/post_migrate/20200000000000_new_migration.rb' - ].each do |file_path| - let(:added_files) { [file_path] } - - it { is_expected.to be_truthy } - end - end - - context 'added files do not contain a migration' do - [ - 'app/models/model.rb', - 'app/assets/javascripts/file.js' - ].each do |file_path| - let(:added_files) { [file_path] } - - it { is_expected.to be_falsey } - end - end - end - - describe '#optional?' do - let(:category_with_changelog) { :backend } - let(:label_with_changelog) { 'frontend' } - let(:category_without_changelog) { Gitlab::Danger::Changelog::NO_CHANGELOG_CATEGORIES.first } - let(:label_without_changelog) { Gitlab::Danger::Changelog::NO_CHANGELOG_LABELS.first } - - subject { changelog.optional? } - - context 'when MR contains only categories requiring no changelog' do - let(:changes_by_category) { { category_without_changelog => nil } } - let(:mr_labels) { [] } - - it 'is falsey' do - is_expected.to be_falsy - end - end - - context 'when MR contains a label that require no changelog' do - let(:changes_by_category) { { category_with_changelog => nil } } - let(:mr_labels) { [label_with_changelog, label_without_changelog] } - - it 'is falsey' do - is_expected.to be_falsy - end - end - - context 'when MR contains a category that require changelog and a category that require no changelog' do - let(:changes_by_category) { { category_with_changelog => nil, category_without_changelog => nil } } - let(:mr_labels) { [] } - - it 'is truthy' do - is_expected.to be_truthy - end - end - - context 'when MR contains a category that require changelog and a category that require no changelog with changelog label' do - let(:changes_by_category) { { category_with_changelog => nil, category_without_changelog => nil } } - let(:mr_labels) { ['feature'] } - - it 'is truthy' do - is_expected.to be_truthy - end - end - - context 'when MR contains a category that require changelog and a category that require no changelog with no changelog label' do - let(:changes_by_category) { { category_with_changelog => nil, category_without_changelog => nil } } - let(:mr_labels) { ['tooling'] } - - it 'is truthy' do - is_expected.to be_falsey - end - end - end - - describe '#found' do - subject { changelog.found } - - context 'added files contain a changelog' do - [ - 'changelogs/unreleased/entry.yml', - 'ee/changelogs/unreleased/entry.yml' - ].each do |file_path| - let(:added_files) { [file_path] } - - it { is_expected.to be_truthy } - end - end - - context 'added files do not contain a changelog' do - [ - 'app/models/model.rb', - 'app/assets/javascripts/file.js' - ].each do |file_path| - let(:added_files) { [file_path] } - it { is_expected.to eq(nil) } - end - end - end - - describe '#ee_changelog?' do - subject { changelog.ee_changelog? } - - before do - allow(changelog).to receive(:found).and_return(file_path) - end - - context 'is ee changelog' do - let(:file_path) { 'ee/changelogs/unreleased/entry.yml' } - - it { is_expected.to be_truthy } - end - - context 'is not ee changelog' do - let(:file_path) { 'changelogs/unreleased/entry.yml' } - - it { is_expected.to be_falsy } - end - end - - describe '#modified_text' do - let(:mr_json) { { "iid" => 1234, "title" => sanitize_mr_title } } - - subject { changelog.modified_text } - - context "when title is not changed from sanitization", :aggregate_failures do - let(:sanitize_mr_title) { 'Fake Title' } - - specify do - expect(subject).to include('CHANGELOG.md was edited') - expect(subject).to include('bin/changelog -m 1234 "Fake Title"') - expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"') - end - end - - context "when title needs sanitization", :aggregate_failures do - let(:sanitize_mr_title) { 'DRAFT: Fake Title' } - - specify do - expect(subject).to include('CHANGELOG.md was edited') - expect(subject).to include('bin/changelog -m 1234 "Fake Title"') - expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"') - end - end - end - - describe '#required_text' do - let(:mr_json) { { "iid" => 1234, "title" => sanitize_mr_title } } - - subject { changelog.required_text } - - context "when title is not changed from sanitization", :aggregate_failures do - let(:sanitize_mr_title) { 'Fake Title' } - - specify do - expect(subject).to include('CHANGELOG missing') - expect(subject).to include('bin/changelog -m 1234 "Fake Title"') - expect(subject).not_to include('--ee') - end - end - - context "when title needs sanitization", :aggregate_failures do - let(:sanitize_mr_title) { 'DRAFT: Fake Title' } - - specify do - expect(subject).to include('CHANGELOG missing') - expect(subject).to include('bin/changelog -m 1234 "Fake Title"') - expect(subject).not_to include('--ee') - end - end - end - - describe '#optional_text' do - let(:mr_json) { { "iid" => 1234, "title" => sanitize_mr_title } } - - subject { changelog.optional_text } - - context "when title is not changed from sanitization", :aggregate_failures do - let(:sanitize_mr_title) { 'Fake Title' } - - specify do - expect(subject).to include('CHANGELOG missing') - expect(subject).to include('bin/changelog -m 1234 "Fake Title"') - expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"') - end - end - - context "when title needs sanitization", :aggregate_failures do - let(:sanitize_mr_title) { 'DRAFT: Fake Title' } - - specify do - expect(subject).to include('CHANGELOG missing') - expect(subject).to include('bin/changelog -m 1234 "Fake Title"') - expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"') - end - end - end -end diff --git a/spec/lib/gitlab/danger/commit_linter_spec.rb b/spec/lib/gitlab/danger/commit_linter_spec.rb deleted file mode 100644 index d3d86037a53..00000000000 --- a/spec/lib/gitlab/danger/commit_linter_spec.rb +++ /dev/null @@ -1,242 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' -require 'rspec-parameterized' -require_relative 'danger_spec_helper' - -require 'gitlab/danger/commit_linter' - -RSpec.describe Gitlab::Danger::CommitLinter do - using RSpec::Parameterized::TableSyntax - - let(:total_files_changed) { 2 } - let(:total_lines_changed) { 10 } - let(:stats) { { total: { files: total_files_changed, lines: total_lines_changed } } } - let(:diff_parent) { Struct.new(:stats).new(stats) } - let(:commit_class) do - Struct.new(:message, :sha, :diff_parent) - end - - let(:commit_message) { 'A commit message' } - let(:commit_sha) { 'abcd1234' } - let(:commit) { commit_class.new(commit_message, commit_sha, diff_parent) } - - subject(:commit_linter) { described_class.new(commit) } - - describe '#fixup?' do - where(:commit_message, :is_fixup) do - 'A commit message' | false - 'fixup!' | true - 'fixup! A commit message' | true - 'squash!' | true - 'squash! A commit message' | true - end - - with_them do - it 'is true when commit message starts with "fixup!" or "squash!"' do - expect(commit_linter.fixup?).to be(is_fixup) - end - end - end - - describe '#suggestion?' do - where(:commit_message, :is_suggestion) do - 'A commit message' | false - 'Apply suggestion to' | true - 'Apply suggestion to "A commit message"' | true - end - - with_them do - it 'is true when commit message starts with "Apply suggestion to"' do - expect(commit_linter.suggestion?).to be(is_suggestion) - end - end - end - - describe '#merge?' do - where(:commit_message, :is_merge) do - 'A commit message' | false - 'Merge branch' | true - 'Merge branch "A commit message"' | true - end - - with_them do - it 'is true when commit message starts with "Merge branch"' do - expect(commit_linter.merge?).to be(is_merge) - end - end - end - - describe '#revert?' do - where(:commit_message, :is_revert) do - 'A commit message' | false - 'Revert' | false - 'Revert "' | true - 'Revert "A commit message"' | true - end - - with_them do - it 'is true when commit message starts with "Revert \""' do - expect(commit_linter.revert?).to be(is_revert) - end - end - end - - describe '#multi_line?' do - where(:commit_message, :is_multi_line) do - "A commit message" | false - "A commit message\n" | false - "A commit message\n\n" | false - "A commit message\n\nSigned-off-by: User Name <user@name.me>" | false - "A commit message\n\nWith details" | true - end - - with_them do - it 'is true when commit message contains details' do - expect(commit_linter.multi_line?).to be(is_multi_line) - end - end - end - - shared_examples 'a valid commit' do - it 'does not have any problem' do - commit_linter.lint - - expect(commit_linter.problems).to be_empty - end - end - - describe '#lint' do - describe 'separator' do - context 'when separator is missing' do - let(:commit_message) { "A B C\n" } - - it_behaves_like 'a valid commit' - end - - context 'when separator is a blank line' do - let(:commit_message) { "A B C\n\nMore details." } - - it_behaves_like 'a valid commit' - end - - context 'when separator is missing' do - let(:commit_message) { "A B C\nMore details." } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:separator_missing) - - commit_linter.lint - end - end - end - - describe 'details' do - context 'when details are valid' do - let(:commit_message) { "A B C\n\nMore details." } - - it_behaves_like 'a valid commit' - end - - context 'when no details are given and many files are changed' do - let(:total_files_changed) { described_class::MAX_CHANGED_FILES_IN_COMMIT + 1 } - - it_behaves_like 'a valid commit' - end - - context 'when no details are given and many lines are changed' do - let(:total_lines_changed) { described_class::MAX_CHANGED_LINES_IN_COMMIT + 1 } - - it_behaves_like 'a valid commit' - end - - context 'when no details are given and many files and lines are changed' do - let(:total_files_changed) { described_class::MAX_CHANGED_FILES_IN_COMMIT + 1 } - let(:total_lines_changed) { described_class::MAX_CHANGED_LINES_IN_COMMIT + 1 } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:details_too_many_changes) - - commit_linter.lint - end - end - - context 'when details exceeds the max line length' do - let(:commit_message) { "A B C\n\n" + 'D' * (described_class::MAX_LINE_LENGTH + 1) } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:details_line_too_long) - - commit_linter.lint - end - end - - context 'when details exceeds the max line length including URLs' do - let(:commit_message) do - "A B C\n\nsome message with https://example.com and https://gitlab.com" + 'D' * described_class::MAX_LINE_LENGTH - end - - it_behaves_like 'a valid commit' - end - end - - describe 'message' do - context 'when message includes a text emoji' do - let(:commit_message) { "A commit message :+1:" } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:message_contains_text_emoji) - - commit_linter.lint - end - end - - context 'when message includes a unicode emoji' do - let(:commit_message) { "A commit message 🚀" } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:message_contains_unicode_emoji) - - commit_linter.lint - end - end - - context 'when message includes a value that is surrounded by backticks' do - let(:commit_message) { "A commit message `%20`" } - - it 'does not add a problem' do - expect(commit_linter).not_to receive(:add_problem) - - commit_linter.lint - end - end - - context 'when message includes a short reference' do - [ - 'A commit message to fix #1234', - 'A commit message to fix !1234', - 'A commit message to fix &1234', - 'A commit message to fix %1234', - 'A commit message to fix gitlab#1234', - 'A commit message to fix gitlab!1234', - 'A commit message to fix gitlab&1234', - 'A commit message to fix gitlab%1234', - 'A commit message to fix gitlab-org/gitlab#1234', - 'A commit message to fix gitlab-org/gitlab!1234', - 'A commit message to fix gitlab-org/gitlab&1234', - 'A commit message to fix gitlab-org/gitlab%1234', - 'A commit message to fix "gitlab-org/gitlab%1234"', - 'A commit message to fix `gitlab-org/gitlab%1234' - ].each do |message| - let(:commit_message) { message } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:message_contains_short_reference) - - commit_linter.lint - end - end - end - end - end -end diff --git a/spec/lib/gitlab/danger/danger_spec_helper.rb b/spec/lib/gitlab/danger/danger_spec_helper.rb deleted file mode 100644 index b1e84b3c13d..00000000000 --- a/spec/lib/gitlab/danger/danger_spec_helper.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module DangerSpecHelper - def new_fake_danger - Class.new do - attr_reader :git, :gitlab, :helper - - # rubocop:disable Gitlab/ModuleWithInstanceVariables - def initialize(git: nil, gitlab: nil, helper: nil) - @git = git - @gitlab = gitlab - @helper = helper - end - # rubocop:enable Gitlab/ModuleWithInstanceVariables - end - end -end diff --git a/spec/lib/gitlab/danger/emoji_checker_spec.rb b/spec/lib/gitlab/danger/emoji_checker_spec.rb deleted file mode 100644 index 6092c751e1c..00000000000 --- a/spec/lib/gitlab/danger/emoji_checker_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' -require 'rspec-parameterized' - -require 'gitlab/danger/emoji_checker' - -RSpec.describe Gitlab::Danger::EmojiChecker do - using RSpec::Parameterized::TableSyntax - - describe '#includes_text_emoji?' do - where(:text, :includes_emoji) do - 'Hello World!' | false - ':+1:' | true - 'Hello World! :+1:' | true - end - - with_them do - it 'is true when text includes a text emoji' do - expect(subject.includes_text_emoji?(text)).to be(includes_emoji) - end - end - end - - describe '#includes_unicode_emoji?' do - where(:text, :includes_emoji) do - 'Hello World!' | false - '🚀' | true - 'Hello World! 🚀' | true - end - - with_them do - it 'is true when text includes a text emoji' do - expect(subject.includes_unicode_emoji?(text)).to be(includes_emoji) - end - end - end -end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb deleted file mode 100644 index bd5c746dd54..00000000000 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ /dev/null @@ -1,602 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' -require 'rspec-parameterized' -require_relative 'danger_spec_helper' - -require 'gitlab/danger/helper' - -RSpec.describe Gitlab::Danger::Helper do - using RSpec::Parameterized::TableSyntax - include DangerSpecHelper - - let(:fake_git) { double('fake-git') } - - let(:mr_author) { nil } - let(:fake_gitlab) { double('fake-gitlab', mr_author: mr_author) } - - let(:fake_danger) { new_fake_danger.include(described_class) } - - subject(:helper) { fake_danger.new(git: fake_git, gitlab: fake_gitlab) } - - describe '#gitlab_helper' do - context 'when gitlab helper is not available' do - let(:fake_gitlab) { nil } - - it 'returns nil' do - expect(helper.gitlab_helper).to be_nil - end - end - - context 'when gitlab helper is available' do - it 'returns the gitlab helper' do - expect(helper.gitlab_helper).to eq(fake_gitlab) - end - end - - context 'when danger gitlab plugin is not available' do - it 'returns nil' do - invalid_danger = Class.new do - include Gitlab::Danger::Helper - end.new - - expect(invalid_danger.gitlab_helper).to be_nil - end - end - end - - describe '#release_automation?' do - context 'when gitlab helper is not available' do - it 'returns false' do - expect(helper.release_automation?).to be_falsey - end - end - - context 'when gitlab helper is available' do - context "but the MR author isn't the RELEASE_TOOLS_BOT" do - let(:mr_author) { 'johnmarston' } - - it 'returns false' do - expect(helper.release_automation?).to be_falsey - end - end - - context 'and the MR author is the RELEASE_TOOLS_BOT' do - let(:mr_author) { described_class::RELEASE_TOOLS_BOT } - - it 'returns true' do - expect(helper.release_automation?).to be_truthy - end - end - end - end - - describe '#all_changed_files' do - subject { helper.all_changed_files } - - it 'interprets a list of changes from the danger git plugin' do - expect(fake_git).to receive(:added_files) { %w[a b c.old] } - expect(fake_git).to receive(:modified_files) { %w[d e] } - expect(fake_git) - .to receive(:renamed_files) - .at_least(:once) - .and_return([{ before: 'c.old', after: 'c.new' }]) - - is_expected.to contain_exactly('a', 'b', 'c.new', 'd', 'e') - end - end - - describe '#changed_lines' do - subject { helper.changed_lines('changed_file.rb') } - - before do - allow(fake_git).to receive(:diff_for_file).with('changed_file.rb').and_return(diff) - end - - context 'when file has diff' do - let(:diff) { double(:diff, patch: "+ # New change here\n+ # New change there") } - - it 'returns file changes' do - is_expected.to eq(['+ # New change here', '+ # New change there']) - end - end - - context 'when file has no diff (renamed without changes)' do - let(:diff) { nil } - - it 'returns a blank array' do - is_expected.to eq([]) - end - end - end - - describe "changed_files" do - it 'returns list of changed files matching given regex' do - expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb usage_data.rb]) - - expect(helper.changed_files(/usage_data/)).to contain_exactly('usage_data.rb') - end - end - - describe '#all_ee_changes' do - subject { helper.all_ee_changes } - - it 'returns all changed files starting with ee/' do - expect(helper).to receive(:all_changed_files).and_return(%w[fr/ee/beer.rb ee/wine.rb ee/lib/ido.rb ee.k]) - - is_expected.to match_array(%w[ee/wine.rb ee/lib/ido.rb]) - end - end - - describe '#ee?' do - subject { helper.ee? } - - it 'returns true if CI_PROJECT_NAME if set to gitlab' do - stub_env('CI_PROJECT_NAME', 'gitlab') - expect(Dir).not_to receive(:exist?) - - is_expected.to be_truthy - end - - it 'delegates to CHANGELOG-EE.md existence if CI_PROJECT_NAME is set to something else' do - stub_env('CI_PROJECT_NAME', 'something else') - expect(Dir).to receive(:exist?).with(File.expand_path('../../../../ee', __dir__)) { true } - - is_expected.to be_truthy - end - - it 'returns true if ee exists' do - stub_env('CI_PROJECT_NAME', nil) - expect(Dir).to receive(:exist?).with(File.expand_path('../../../../ee', __dir__)) { true } - - is_expected.to be_truthy - end - - it "returns false if ee doesn't exist" do - stub_env('CI_PROJECT_NAME', nil) - expect(Dir).to receive(:exist?).with(File.expand_path('../../../../ee', __dir__)) { false } - - is_expected.to be_falsy - end - end - - describe '#project_name' do - subject { helper.project_name } - - it 'returns gitlab if ee? returns true' do - expect(helper).to receive(:ee?) { true } - - is_expected.to eq('gitlab') - end - - it 'returns gitlab-ce if ee? returns false' do - expect(helper).to receive(:ee?) { false } - - is_expected.to eq('gitlab-foss') - end - end - - describe '#markdown_list' do - it 'creates a markdown list of items' do - items = %w[a b] - - expect(helper.markdown_list(items)).to eq("* `a`\n* `b`") - end - - it 'wraps items in <details> when there are more than 10 items' do - items = ('a'..'k').to_a - - expect(helper.markdown_list(items)).to match(%r{<details>[^<]+</details>}) - end - end - - describe '#changes_by_category' do - it 'categorizes changed files' do - expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/migrate/foo lib/gitlab/database/foo.rb qa/foo ee/changelogs/foo.yml] } - allow(fake_git).to receive(:modified_files) { [] } - allow(fake_git).to receive(:renamed_files) { [] } - - expect(helper.changes_by_category).to eq( - backend: %w[foo.rb], - database: %w[db/migrate/foo lib/gitlab/database/foo.rb], - frontend: %w[foo.js], - none: %w[ee/changelogs/foo.yml foo.md], - qa: %w[qa/foo], - unknown: %w[foo] - ) - end - end - - describe '#categories_for_file' do - before do - allow(fake_git).to receive(:diff_for_file).with('usage_data.rb') { double(:diff, patch: "+ count(User.active)") } - end - - where(:path, :expected_categories) do - 'usage_data.rb' | [:database, :backend] - 'doc/foo.md' | [:docs] - 'CONTRIBUTING.md' | [:docs] - 'LICENSE' | [:docs] - 'MAINTENANCE.md' | [:docs] - 'PHILOSOPHY.md' | [:docs] - 'PROCESS.md' | [:docs] - 'README.md' | [:docs] - - 'ee/doc/foo' | [:unknown] - 'ee/README' | [:unknown] - - 'app/assets/foo' | [:frontend] - 'app/views/foo' | [:frontend] - 'public/foo' | [:frontend] - 'scripts/frontend/foo' | [:frontend] - 'spec/javascripts/foo' | [:frontend] - 'spec/frontend/bar' | [:frontend] - 'vendor/assets/foo' | [:frontend] - 'babel.config.js' | [:frontend] - 'jest.config.js' | [:frontend] - 'package.json' | [:frontend] - 'yarn.lock' | [:frontend] - 'config/foo.js' | [:frontend] - 'config/deep/foo.js' | [:frontend] - - 'ee/app/assets/foo' | [:frontend] - 'ee/app/views/foo' | [:frontend] - 'ee/spec/javascripts/foo' | [:frontend] - 'ee/spec/frontend/bar' | [:frontend] - - '.gitlab/ci/frontend.gitlab-ci.yml' | %i[frontend engineering_productivity] - - 'app/models/foo' | [:backend] - 'bin/foo' | [:backend] - 'config/foo' | [:backend] - 'lib/foo' | [:backend] - 'rubocop/foo' | [:backend] - '.rubocop.yml' | [:backend] - '.rubocop_todo.yml' | [:backend] - '.rubocop_manual_todo.yml' | [:backend] - 'spec/foo' | [:backend] - 'spec/foo/bar' | [:backend] - - 'ee/app/foo' | [:backend] - 'ee/bin/foo' | [:backend] - 'ee/spec/foo' | [:backend] - 'ee/spec/foo/bar' | [:backend] - - 'spec/features/foo' | [:test] - 'ee/spec/features/foo' | [:test] - 'spec/support/shared_examples/features/foo' | [:test] - 'ee/spec/support/shared_examples/features/foo' | [:test] - 'spec/support/shared_contexts/features/foo' | [:test] - 'ee/spec/support/shared_contexts/features/foo' | [:test] - 'spec/support/helpers/features/foo' | [:test] - 'ee/spec/support/helpers/features/foo' | [:test] - - 'generator_templates/foo' | [:backend] - 'vendor/languages.yml' | [:backend] - 'file_hooks/examples/' | [:backend] - - 'Gemfile' | [:backend] - 'Gemfile.lock' | [:backend] - 'Rakefile' | [:backend] - 'FOO_VERSION' | [:backend] - - 'Dangerfile' | [:engineering_productivity] - 'danger/commit_messages/Dangerfile' | [:engineering_productivity] - 'ee/danger/commit_messages/Dangerfile' | [:engineering_productivity] - 'danger/commit_messages/' | [:engineering_productivity] - 'ee/danger/commit_messages/' | [:engineering_productivity] - '.gitlab-ci.yml' | [:engineering_productivity] - '.gitlab/ci/cng.gitlab-ci.yml' | [:engineering_productivity] - '.gitlab/ci/ee-specific-checks.gitlab-ci.yml' | [:engineering_productivity] - 'scripts/foo' | [:engineering_productivity] - 'lib/gitlab/danger/foo' | [:engineering_productivity] - 'ee/lib/gitlab/danger/foo' | [:engineering_productivity] - 'lefthook.yml' | [:engineering_productivity] - '.editorconfig' | [:engineering_productivity] - 'tooling/bin/find_foss_tests' | [:engineering_productivity] - '.codeclimate.yml' | [:engineering_productivity] - '.gitlab/CODEOWNERS' | [:engineering_productivity] - - 'lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' | [:ci_template] - 'lib/gitlab/ci/templates/dotNET-Core.yml' | [:ci_template] - - 'ee/FOO_VERSION' | [:unknown] - - 'db/schema.rb' | [:database] - 'db/structure.sql' | [:database] - 'db/migrate/foo' | [:database] - 'db/post_migrate/foo' | [:database] - 'ee/db/migrate/foo' | [:database] - 'ee/db/post_migrate/foo' | [:database] - 'ee/db/geo/migrate/foo' | [:database] - 'ee/db/geo/post_migrate/foo' | [:database] - 'app/models/project_authorization.rb' | [:database] - 'app/services/users/refresh_authorized_projects_service.rb' | [:database] - 'lib/gitlab/background_migration.rb' | [:database] - 'lib/gitlab/background_migration/foo' | [:database] - 'ee/lib/gitlab/background_migration/foo' | [:database] - 'lib/gitlab/database.rb' | [:database] - 'lib/gitlab/database/foo' | [:database] - 'ee/lib/gitlab/database/foo' | [:database] - 'lib/gitlab/github_import.rb' | [:database] - 'lib/gitlab/github_import/foo' | [:database] - 'lib/gitlab/sql/foo' | [:database] - 'rubocop/cop/migration/foo' | [:database] - - 'db/fixtures/foo.rb' | [:backend] - 'ee/db/fixtures/foo.rb' | [:backend] - 'doc/api/graphql/reference/gitlab_schema.graphql' | [:backend] - 'doc/api/graphql/reference/gitlab_schema.json' | [:backend] - - 'qa/foo' | [:qa] - 'ee/qa/foo' | [:qa] - - 'changelogs/foo' | [:none] - 'ee/changelogs/foo' | [:none] - 'locale/gitlab.pot' | [:none] - - 'FOO' | [:unknown] - 'foo' | [:unknown] - - 'foo/bar.rb' | [:backend] - 'foo/bar.js' | [:frontend] - 'foo/bar.txt' | [:none] - 'foo/bar.md' | [:none] - end - - with_them do - subject { helper.categories_for_file(path) } - - it { is_expected.to eq(expected_categories) } - end - - context 'having specific changes' do - where(:expected_categories, :patch, :changed_files) do - [:database, :backend] | '+ count(User.active)' | ['usage_data.rb', 'lib/gitlab/usage_data.rb', 'ee/lib/ee/gitlab/usage_data.rb'] - [:database, :backend] | '+ estimate_batch_distinct_count(User.active)' | ['usage_data.rb'] - [:backend] | '+ alt_usage_data(User.active)' | ['usage_data.rb'] - [:backend] | '+ count(User.active)' | ['user.rb'] - [:backend] | '+ count(User.active)' | ['usage_data/topology.rb'] - [:backend] | '+ foo_count(User.active)' | ['usage_data.rb'] - end - - with_them do - it 'has the correct categories' do - changed_files.each do |file| - allow(fake_git).to receive(:diff_for_file).with(file) { double(:diff, patch: patch) } - - expect(helper.categories_for_file(file)).to eq(expected_categories) - end - end - end - end - end - - describe '#label_for_category' do - where(:category, :expected_label) do - :backend | '~backend' - :database | '~database' - :docs | '~documentation' - :foo | '~foo' - :frontend | '~frontend' - :none | '' - :qa | '~QA' - :engineering_productivity | '~"Engineering Productivity" for CI, Danger' - :ci_template | '~"ci::templates"' - end - - with_them do - subject { helper.label_for_category(category) } - - it { is_expected.to eq(expected_label) } - end - end - - describe '#new_teammates' do - it 'returns an array of Teammate' do - usernames = %w[filipa iamphil] - - teammates = helper.new_teammates(usernames) - - expect(teammates.map(&:username)).to eq(usernames) - end - end - - describe '#security_mr?' do - it 'returns false when `gitlab_helper` is unavailable' do - expect(helper).to receive(:gitlab_helper).and_return(nil) - - expect(helper).not_to be_security_mr - end - - it 'returns false when on a normal merge request' do - expect(fake_gitlab).to receive(:mr_json) - .and_return('web_url' => 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1') - - expect(helper).not_to be_security_mr - end - - it 'returns true when on a security merge request' do - expect(fake_gitlab).to receive(:mr_json) - .and_return('web_url' => 'https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/1') - - expect(helper).to be_security_mr - end - end - - describe '#draft_mr?' do - it 'returns false when `gitlab_helper` is unavailable' do - expect(helper).to receive(:gitlab_helper).and_return(nil) - - expect(helper).not_to be_draft_mr - end - - it 'returns true for a draft MR' do - expect(fake_gitlab).to receive(:mr_json) - .and_return('title' => 'Draft: My MR title') - - expect(helper).to be_draft_mr - end - - it 'returns false for non draft MR' do - expect(fake_gitlab).to receive(:mr_json) - .and_return('title' => 'My MR title') - - expect(helper).not_to be_draft_mr - end - end - - describe '#cherry_pick_mr?' do - it 'returns false when `gitlab_helper` is unavailable' do - expect(helper).to receive(:gitlab_helper).and_return(nil) - - expect(helper).not_to be_cherry_pick_mr - end - - context 'when MR title does not mention a cherry-pick' do - it 'returns false' do - expect(fake_gitlab).to receive(:mr_json) - .and_return('title' => 'Add feature xyz') - - expect(helper).not_to be_cherry_pick_mr - end - end - - context 'when MR title mentions a cherry-pick' do - [ - 'Cherry Pick !1234', - 'cherry-pick !1234', - 'CherryPick !1234' - ].each do |mr_title| - it 'returns true' do - expect(fake_gitlab).to receive(:mr_json) - .and_return('title' => mr_title) - - expect(helper).to be_cherry_pick_mr - end - end - end - end - - describe '#stable_branch?' do - it 'returns false when `gitlab_helper` is unavailable' do - expect(helper).to receive(:gitlab_helper).and_return(nil) - - expect(helper).not_to be_stable_branch - end - - context 'when MR target branch is not a stable branch' do - it 'returns false' do - expect(fake_gitlab).to receive(:mr_json) - .and_return('target_branch' => 'my-feature-branch') - - expect(helper).not_to be_stable_branch - end - end - - context 'when MR target branch is a stable branch' do - %w[ - 13-1-stable-ee - 13-1-stable-ee-patch-1 - ].each do |target_branch| - it 'returns true' do - expect(fake_gitlab).to receive(:mr_json) - .and_return('target_branch' => target_branch) - - expect(helper).to be_stable_branch - end - end - end - end - - describe '#mr_has_label?' do - it 'returns false when `gitlab_helper` is unavailable' do - expect(helper).to receive(:gitlab_helper).and_return(nil) - - expect(helper.mr_has_labels?('telemetry')).to be_falsey - end - - context 'when mr has labels' do - before do - mr_labels = ['telemetry', 'telemetry::reviewed'] - expect(fake_gitlab).to receive(:mr_labels).and_return(mr_labels) - end - - it 'returns true with a matched label' do - expect(helper.mr_has_labels?('telemetry')).to be_truthy - end - - it 'returns false with unmatched label' do - expect(helper.mr_has_labels?('database')).to be_falsey - end - - it 'returns true with an array of labels' do - expect(helper.mr_has_labels?(['telemetry', 'telemetry::reviewed'])).to be_truthy - end - - it 'returns true with multi arguments with matched labels' do - expect(helper.mr_has_labels?('telemetry', 'telemetry::reviewed')).to be_truthy - end - - it 'returns false with multi arguments with unmatched labels' do - expect(helper.mr_has_labels?('telemetry', 'telemetry::non existing')).to be_falsey - end - end - end - - describe '#labels_list' do - let(:labels) { ['telemetry', 'telemetry::reviewed'] } - - it 'composes the labels string' do - expect(helper.labels_list(labels)).to eq('~"telemetry", ~"telemetry::reviewed"') - end - - context 'when passing a separator' do - it 'composes the labels string with the given separator' do - expect(helper.labels_list(labels, sep: ' ')).to eq('~"telemetry" ~"telemetry::reviewed"') - end - end - - it 'returns empty string for empty array' do - expect(helper.labels_list([])).to eq('') - end - end - - describe '#prepare_labels_for_mr' do - it 'composes the labels string' do - mr_labels = ['telemetry', 'telemetry::reviewed'] - - expect(helper.prepare_labels_for_mr(mr_labels)).to eq('/label ~"telemetry" ~"telemetry::reviewed"') - end - - it 'returns empty string for empty array' do - expect(helper.prepare_labels_for_mr([])).to eq('') - end - end - - describe '#has_ci_changes?' do - context 'when .gitlab/ci is changed' do - it 'returns true' do - expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb .gitlab/ci/test.yml]) - - expect(helper.has_ci_changes?).to be_truthy - end - end - - context 'when .gitlab-ci.yml is changed' do - it 'returns true' do - expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb .gitlab-ci.yml]) - - expect(helper.has_ci_changes?).to be_truthy - end - end - - context 'when neither .gitlab/ci/ or .gitlab-ci.yml is changed' do - it 'returns false' do - expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb nested/.gitlab-ci.yml]) - - expect(helper.has_ci_changes?).to be_falsey - end - end - end -end diff --git a/spec/lib/gitlab/danger/merge_request_linter_spec.rb b/spec/lib/gitlab/danger/merge_request_linter_spec.rb deleted file mode 100644 index 29facc9fdd6..00000000000 --- a/spec/lib/gitlab/danger/merge_request_linter_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' -require 'rspec-parameterized' -require_relative 'danger_spec_helper' - -require 'gitlab/danger/merge_request_linter' - -RSpec.describe Gitlab::Danger::MergeRequestLinter do - using RSpec::Parameterized::TableSyntax - - let(:mr_class) do - Struct.new(:message, :sha, :diff_parent) - end - - let(:mr_title) { 'A B ' + 'C' } - let(:merge_request) { mr_class.new(mr_title, anything, anything) } - - describe '#lint_subject' do - subject(:mr_linter) { described_class.new(merge_request) } - - shared_examples 'a valid mr title' do - it 'does not have any problem' do - mr_linter.lint - - expect(mr_linter.problems).to be_empty - end - end - - context 'when subject valid' do - it_behaves_like 'a valid mr title' - end - - context 'when it is too long' do - let(:mr_title) { 'A B ' + 'C' * described_class::MAX_LINE_LENGTH } - - it 'adds a problem' do - expect(mr_linter).to receive(:add_problem).with(:subject_too_long, described_class.subject_description) - - mr_linter.lint - end - end - - describe 'using magic mr run options' do - where(run_option: described_class.mr_run_options_regex.split('|') + - described_class.mr_run_options_regex.split('|').map! { |x| "[#{x}]" }) - - with_them do - let(:mr_title) { run_option + ' A B ' + 'C' * (described_class::MAX_LINE_LENGTH - 5) } - - it_behaves_like 'a valid mr title' - end - end - end -end diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb deleted file mode 100644 index 59ac3b12b6b..00000000000 --- a/spec/lib/gitlab/danger/roulette_spec.rb +++ /dev/null @@ -1,413 +0,0 @@ -# frozen_string_literal: true - -require 'webmock/rspec' -require 'timecop' - -require 'gitlab/danger/roulette' -require 'active_support/testing/time_helpers' - -RSpec.describe Gitlab::Danger::Roulette do - include ActiveSupport::Testing::TimeHelpers - - around do |example| - travel_to(Time.utc(2020, 06, 22, 10)) { example.run } - end - - let(:backend_available) { true } - let(:backend_tz_offset_hours) { 2.0 } - let(:backend_maintainer) do - Gitlab::Danger::Teammate.new( - 'username' => 'backend-maintainer', - 'name' => 'Backend maintainer', - 'role' => 'Backend engineer', - 'projects' => { 'gitlab' => 'maintainer backend' }, - 'available' => backend_available, - 'tz_offset_hours' => backend_tz_offset_hours - ) - end - - let(:frontend_reviewer) do - Gitlab::Danger::Teammate.new( - 'username' => 'frontend-reviewer', - 'name' => 'Frontend reviewer', - 'role' => 'Frontend engineer', - 'projects' => { 'gitlab' => 'reviewer frontend' }, - 'available' => true, - 'tz_offset_hours' => 2.0 - ) - end - - let(:frontend_maintainer) do - Gitlab::Danger::Teammate.new( - 'username' => 'frontend-maintainer', - 'name' => 'Frontend maintainer', - 'role' => 'Frontend engineer', - 'projects' => { 'gitlab' => "maintainer frontend" }, - 'available' => true, - 'tz_offset_hours' => 2.0 - ) - end - - let(:software_engineer_in_test) do - Gitlab::Danger::Teammate.new( - 'username' => 'software-engineer-in-test', - 'name' => 'Software Engineer in Test', - 'role' => 'Software Engineer in Test, Create:Source Code', - 'projects' => { 'gitlab' => 'reviewer qa', 'gitlab-qa' => 'maintainer' }, - 'available' => true, - 'tz_offset_hours' => 2.0 - ) - end - - let(:engineering_productivity_reviewer) do - Gitlab::Danger::Teammate.new( - 'username' => 'eng-prod-reviewer', - 'name' => 'EP engineer', - 'role' => 'Engineering Productivity', - 'projects' => { 'gitlab' => 'reviewer backend' }, - 'available' => true, - 'tz_offset_hours' => 2.0 - ) - end - - let(:ci_template_reviewer) do - Gitlab::Danger::Teammate.new( - 'username' => 'ci-template-maintainer', - 'name' => 'CI Template engineer', - 'role' => '~"ci::templates"', - 'projects' => { 'gitlab' => 'reviewer ci_template' }, - 'available' => true, - 'tz_offset_hours' => 2.0 - ) - end - - let(:teammates) do - [ - backend_maintainer.to_h, - frontend_maintainer.to_h, - frontend_reviewer.to_h, - software_engineer_in_test.to_h, - engineering_productivity_reviewer.to_h, - ci_template_reviewer.to_h - ] - end - - let(:teammate_json) do - teammates.to_json - end - - subject(:roulette) { Object.new.extend(described_class) } - - describe 'Spin#==' do - it 'compares Spin attributes' do - spin1 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, false, false) - spin2 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, false, false) - spin3 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, false, true) - spin4 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, true, false) - spin5 = described_class::Spin.new(:backend, frontend_reviewer, backend_maintainer, false, false) - spin6 = described_class::Spin.new(:backend, backend_maintainer, frontend_maintainer, false, false) - spin7 = described_class::Spin.new(:frontend, frontend_reviewer, frontend_maintainer, false, false) - - expect(spin1).to eq(spin2) - expect(spin1).not_to eq(spin3) - expect(spin1).not_to eq(spin4) - expect(spin1).not_to eq(spin5) - expect(spin1).not_to eq(spin6) - expect(spin1).not_to eq(spin7) - end - end - - describe '#spin' do - let!(:project) { 'gitlab' } - let!(:mr_source_branch) { 'a-branch' } - let!(:mr_labels) { ['backend', 'devops::create'] } - let!(:author) { Gitlab::Danger::Teammate.new('username' => 'johndoe') } - let(:timezone_experiment) { false } - let(:spins) do - # Stub the request at the latest time so that we can modify the raw data, e.g. available fields. - WebMock - .stub_request(:get, described_class::ROULETTE_DATA_URL) - .to_return(body: teammate_json) - - subject.spin(project, categories, timezone_experiment: timezone_experiment) - end - - before do - allow(subject).to receive(:mr_author_username).and_return(author.username) - allow(subject).to receive(:mr_labels).and_return(mr_labels) - allow(subject).to receive(:mr_source_branch).and_return(mr_source_branch) - end - - context 'when timezone_experiment == false' do - context 'when change contains backend category' do - let(:categories) { [:backend] } - - it 'assigns backend reviewer and maintainer' do - expect(spins[0].reviewer).to eq(engineering_productivity_reviewer) - expect(spins[0].maintainer).to eq(backend_maintainer) - expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false)]) - end - - context 'when teammate is not available' do - let(:backend_available) { false } - - it 'assigns backend reviewer and no maintainer' do - expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, nil, false, false)]) - end - end - end - - context 'when change contains frontend category' do - let(:categories) { [:frontend] } - - it 'assigns frontend reviewer and maintainer' do - expect(spins).to eq([described_class::Spin.new(:frontend, frontend_reviewer, frontend_maintainer, false, false)]) - end - end - - context 'when change contains many categories' do - let(:categories) { [:frontend, :test, :qa, :engineering_productivity, :ci_template, :backend] } - - it 'has a deterministic sorting order' do - expect(spins.map(&:category)).to eq categories.sort - end - end - - context 'when change contains QA category' do - let(:categories) { [:qa] } - - it 'assigns QA reviewer' do - expect(spins).to eq([described_class::Spin.new(:qa, software_engineer_in_test, nil, false, false)]) - end - end - - context 'when change contains Engineering Productivity category' do - let(:categories) { [:engineering_productivity] } - - it 'assigns Engineering Productivity reviewer and fallback to backend maintainer' do - expect(spins).to eq([described_class::Spin.new(:engineering_productivity, engineering_productivity_reviewer, backend_maintainer, false, false)]) - end - end - - context 'when change contains CI/CD Template category' do - let(:categories) { [:ci_template] } - - it 'assigns CI/CD Template reviewer and fallback to backend maintainer' do - expect(spins).to eq([described_class::Spin.new(:ci_template, ci_template_reviewer, backend_maintainer, false, false)]) - end - end - - context 'when change contains test category' do - let(:categories) { [:test] } - - it 'assigns corresponding SET' do - expect(spins).to eq([described_class::Spin.new(:test, software_engineer_in_test, nil, :maintainer, false)]) - end - end - end - - context 'when timezone_experiment == true' do - let(:timezone_experiment) { true } - - context 'when change contains backend category' do - let(:categories) { [:backend] } - - it 'assigns backend reviewer and maintainer' do - expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, true)]) - end - - context 'when teammate is not in a good timezone' do - let(:backend_tz_offset_hours) { 5.0 } - - it 'assigns backend reviewer and no maintainer' do - expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, nil, false, true)]) - end - end - end - - context 'when change includes a category with timezone disabled' do - let(:categories) { [:backend] } - - before do - stub_const("#{described_class}::INCLUDE_TIMEZONE_FOR_CATEGORY", backend: false) - end - - it 'assigns backend reviewer and maintainer' do - expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false)]) - end - - context 'when teammate is not in a good timezone' do - let(:backend_tz_offset_hours) { 5.0 } - - it 'assigns backend reviewer and maintainer' do - expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false)]) - end - end - end - end - end - - RSpec::Matchers.define :match_teammates do |expected| - match do |actual| - expected.each do |expected_person| - actual_person_found = actual.find { |actual_person| actual_person.name == expected_person.username } - - actual_person_found && - actual_person_found.name == expected_person.name && - actual_person_found.role == expected_person.role && - actual_person_found.projects == expected_person.projects - end - end - end - - describe '#team' do - subject(:team) { roulette.team } - - context 'HTTP failure' do - before do - WebMock - .stub_request(:get, described_class::ROULETTE_DATA_URL) - .to_return(status: 404) - end - - it 'raises a pretty error' do - expect { team }.to raise_error(/Failed to read/) - end - end - - context 'JSON failure' do - before do - WebMock - .stub_request(:get, described_class::ROULETTE_DATA_URL) - .to_return(body: 'INVALID JSON') - end - - it 'raises a pretty error' do - expect { team }.to raise_error(/Failed to parse/) - end - end - - context 'success' do - before do - WebMock - .stub_request(:get, described_class::ROULETTE_DATA_URL) - .to_return(body: teammate_json) - end - - it 'returns an array of teammates' do - is_expected.to match_teammates([ - backend_maintainer, - frontend_reviewer, - frontend_maintainer, - software_engineer_in_test, - engineering_productivity_reviewer, - ci_template_reviewer - ]) - end - - it 'memoizes the result' do - expect(team.object_id).to eq(roulette.team.object_id) - end - end - end - - describe '#project_team' do - subject { roulette.project_team('gitlab-qa') } - - before do - WebMock - .stub_request(:get, described_class::ROULETTE_DATA_URL) - .to_return(body: teammate_json) - end - - it 'filters team by project_name' do - is_expected.to match_teammates([ - software_engineer_in_test - ]) - end - end - - describe '#spin_for_person' do - let(:person_tz_offset_hours) { 0.0 } - let(:person1) do - Gitlab::Danger::Teammate.new( - 'username' => 'user1', - 'available' => true, - 'tz_offset_hours' => person_tz_offset_hours - ) - end - - let(:person2) do - Gitlab::Danger::Teammate.new( - 'username' => 'user2', - 'available' => true, - 'tz_offset_hours' => person_tz_offset_hours) - end - - let(:author) do - Gitlab::Danger::Teammate.new( - 'username' => 'johndoe', - 'available' => true, - 'tz_offset_hours' => 0.0) - end - - let(:unavailable) do - Gitlab::Danger::Teammate.new( - 'username' => 'janedoe', - 'available' => false, - 'tz_offset_hours' => 0.0) - end - - before do - allow(subject).to receive(:mr_author_username).and_return(author.username) - end - - (-4..4).each do |utc_offset| - context "when local hour for person is #{10 + utc_offset} (offset: #{utc_offset})" do - let(:person_tz_offset_hours) { utc_offset } - - [false, true].each do |timezone_experiment| - context "with timezone_experiment == #{timezone_experiment}" do - it 'returns a random person' do - persons = [person1, person2] - - selected = subject.spin_for_person(persons, random: Random.new, timezone_experiment: timezone_experiment) - - expect(persons.map(&:username)).to include(selected.username) - end - end - end - end - end - - ((-12..-5).to_a + (5..12).to_a).each do |utc_offset| - context "when local hour for person is #{10 + utc_offset} (offset: #{utc_offset})" do - let(:person_tz_offset_hours) { utc_offset } - - [false, true].each do |timezone_experiment| - context "with timezone_experiment == #{timezone_experiment}" do - it 'returns a random person or nil' do - persons = [person1, person2] - - selected = subject.spin_for_person(persons, random: Random.new, timezone_experiment: timezone_experiment) - - if timezone_experiment - expect(selected).to be_nil - else - expect(persons.map(&:username)).to include(selected.username) - end - end - end - end - end - end - - it 'excludes unavailable persons' do - expect(subject.spin_for_person([unavailable], random: Random.new)).to be_nil - end - - it 'excludes mr.author' do - expect(subject.spin_for_person([author], random: Random.new)).to be_nil - end - end -end diff --git a/spec/lib/gitlab/danger/sidekiq_queues_spec.rb b/spec/lib/gitlab/danger/sidekiq_queues_spec.rb deleted file mode 100644 index 7dd1a2e6924..00000000000 --- a/spec/lib/gitlab/danger/sidekiq_queues_spec.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' -require 'rspec-parameterized' -require_relative 'danger_spec_helper' - -require 'gitlab/danger/sidekiq_queues' - -RSpec.describe Gitlab::Danger::SidekiqQueues do - using RSpec::Parameterized::TableSyntax - include DangerSpecHelper - - let(:fake_git) { double('fake-git') } - let(:fake_danger) { new_fake_danger.include(described_class) } - - subject(:sidekiq_queues) { fake_danger.new(git: fake_git) } - - describe '#changed_queue_files' do - where(:modified_files, :changed_queue_files) do - %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml foo) | %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml) - %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml) | %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml) - %w(app/workers/all_queues.yml foo) | %w(app/workers/all_queues.yml) - %w(ee/app/workers/all_queues.yml foo) | %w(ee/app/workers/all_queues.yml) - %w(foo) | %w() - %w() | %w() - end - - with_them do - it do - allow(fake_git).to receive(:modified_files).and_return(modified_files) - - expect(sidekiq_queues.changed_queue_files).to match_array(changed_queue_files) - end - end - end - - describe '#added_queue_names' do - it 'returns queue names added by this change' do - old_queues = { post_receive: nil } - - allow(sidekiq_queues).to receive(:old_queues).and_return(old_queues) - allow(sidekiq_queues).to receive(:new_queues).and_return(old_queues.merge(merge: nil, process_commit: nil)) - - expect(sidekiq_queues.added_queue_names).to contain_exactly(:merge, :process_commit) - end - end - - describe '#changed_queue_names' do - it 'returns names for queues whose attributes were changed' do - old_queues = { - merge: { name: :merge, urgency: :low }, - post_receive: { name: :post_receive, urgency: :high }, - process_commit: { name: :process_commit, urgency: :high } - } - - new_queues = old_queues.merge(mailers: { name: :mailers, urgency: :high }, - post_receive: { name: :post_receive, urgency: :low }, - process_commit: { name: :process_commit, urgency: :low }) - - allow(sidekiq_queues).to receive(:old_queues).and_return(old_queues) - allow(sidekiq_queues).to receive(:new_queues).and_return(new_queues) - - expect(sidekiq_queues.changed_queue_names).to contain_exactly(:post_receive, :process_commit) - end - - it 'ignores removed queues' do - old_queues = { - merge: { name: :merge, urgency: :low }, - post_receive: { name: :post_receive, urgency: :high } - } - - new_queues = { - post_receive: { name: :post_receive, urgency: :low } - } - - allow(sidekiq_queues).to receive(:old_queues).and_return(old_queues) - allow(sidekiq_queues).to receive(:new_queues).and_return(new_queues) - - expect(sidekiq_queues.changed_queue_names).to contain_exactly(:post_receive) - end - end -end diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb deleted file mode 100644 index 9c066ba4c1b..00000000000 --- a/spec/lib/gitlab/danger/teammate_spec.rb +++ /dev/null @@ -1,220 +0,0 @@ -# frozen_string_literal: true - -require 'timecop' -require 'rspec-parameterized' - -require 'gitlab/danger/teammate' -require 'active_support/testing/time_helpers' - -RSpec.describe Gitlab::Danger::Teammate do - using RSpec::Parameterized::TableSyntax - - subject { described_class.new(options) } - - let(:tz_offset_hours) { 2.0 } - let(:options) do - { - 'username' => 'luigi', - 'projects' => projects, - 'role' => role, - 'markdown_name' => '[Luigi](https://gitlab.com/luigi) (`@luigi`)', - 'tz_offset_hours' => tz_offset_hours - } - end - - let(:capabilities) { ['reviewer backend'] } - let(:projects) { { project => capabilities } } - let(:role) { 'Engineer, Manage' } - let(:labels) { [] } - let(:project) { double } - - describe '#==' do - it 'compares Teammate username' do - joe1 = described_class.new('username' => 'joe', 'projects' => projects) - joe2 = described_class.new('username' => 'joe', 'projects' => []) - jane1 = described_class.new('username' => 'jane', 'projects' => projects) - jane2 = described_class.new('username' => 'jane', 'projects' => []) - - expect(joe1).to eq(joe2) - expect(jane1).to eq(jane2) - expect(jane1).not_to eq(nil) - expect(described_class.new('username' => nil)).not_to eq(nil) - end - end - - describe '#to_h' do - it 'returns the given options' do - expect(subject.to_h).to eq(options) - end - end - - context 'when having multiple capabilities' do - let(:capabilities) { ['reviewer backend', 'maintainer frontend', 'trainee_maintainer qa'] } - - it '#reviewer? supports multiple roles per project' do - expect(subject.reviewer?(project, :backend, labels)).to be_truthy - end - - it '#traintainer? supports multiple roles per project' do - expect(subject.traintainer?(project, :qa, labels)).to be_truthy - end - - it '#maintainer? supports multiple roles per project' do - expect(subject.maintainer?(project, :frontend, labels)).to be_truthy - end - - context 'when labels contain devops::create and the category is test' do - let(:labels) { ['devops::create'] } - - context 'when role is Software Engineer in Test, Create' do - let(:role) { 'Software Engineer in Test, Create' } - - it '#reviewer? returns true' do - expect(subject.reviewer?(project, :test, labels)).to be_truthy - end - - it '#maintainer? returns false' do - expect(subject.maintainer?(project, :test, labels)).to be_falsey - end - - context 'when hyperlink is mangled in the role' do - let(:role) { '<a href="#">Software Engineer in Test</a>, Create' } - - it '#reviewer? returns true' do - expect(subject.reviewer?(project, :test, labels)).to be_truthy - end - end - end - - context 'when role is Software Engineer in Test' do - let(:role) { 'Software Engineer in Test' } - - it '#reviewer? returns false' do - expect(subject.reviewer?(project, :test, labels)).to be_falsey - end - end - - context 'when role is Software Engineer in Test, Manage' do - let(:role) { 'Software Engineer in Test, Manage' } - - it '#reviewer? returns false' do - expect(subject.reviewer?(project, :test, labels)).to be_falsey - end - end - - context 'when role is Backend Engineer, Engineering Productivity' do - let(:role) { 'Backend Engineer, Engineering Productivity' } - - it '#reviewer? returns true' do - expect(subject.reviewer?(project, :engineering_productivity, labels)).to be_truthy - end - - it '#maintainer? returns false' do - expect(subject.maintainer?(project, :engineering_productivity, labels)).to be_falsey - end - - context 'when capabilities include maintainer backend' do - let(:capabilities) { ['maintainer backend'] } - - it '#maintainer? returns true' do - expect(subject.maintainer?(project, :engineering_productivity, labels)).to be_truthy - end - end - - context 'when capabilities include maintainer engineering productivity' do - let(:capabilities) { ['maintainer engineering_productivity'] } - - it '#maintainer? returns true' do - expect(subject.maintainer?(project, :engineering_productivity, labels)).to be_truthy - end - end - - context 'when capabilities include trainee_maintainer backend' do - let(:capabilities) { ['trainee_maintainer backend'] } - - it '#traintainer? returns true' do - expect(subject.traintainer?(project, :engineering_productivity, labels)).to be_truthy - end - end - end - end - end - - context 'when having single capability' do - let(:capabilities) { 'reviewer backend' } - - it '#reviewer? supports one role per project' do - expect(subject.reviewer?(project, :backend, labels)).to be_truthy - end - - it '#traintainer? supports one role per project' do - expect(subject.traintainer?(project, :database, labels)).to be_falsey - end - - it '#maintainer? supports one role per project' do - expect(subject.maintainer?(project, :frontend, labels)).to be_falsey - end - end - - describe '#local_hour' do - include ActiveSupport::Testing::TimeHelpers - - around do |example| - travel_to(Time.utc(2020, 6, 23, 10)) { example.run } - end - - context 'when author is given' do - where(:tz_offset_hours, :expected_local_hour) do - -12 | 22 - -10 | 0 - 2 | 12 - 4 | 14 - 12 | 22 - end - - with_them do - it 'returns the correct local_hour' do - expect(subject.local_hour).to eq(expected_local_hour) - end - end - end - end - - describe '#markdown_name' do - it 'returns markdown name with timezone info' do - expect(subject.markdown_name).to eq("#{options['markdown_name']} (UTC+2)") - end - - context 'when offset is 1.5' do - let(:tz_offset_hours) { 1.5 } - - it 'returns markdown name with timezone info, not truncated' do - expect(subject.markdown_name).to eq("#{options['markdown_name']} (UTC+1.5)") - end - end - - context 'when author is given' do - where(:tz_offset_hours, :author_offset, :diff_text) do - -12 | -10 | "2 hours behind `@mario`" - -10 | -12 | "2 hours ahead of `@mario`" - -10 | 2 | "12 hours behind `@mario`" - 2 | 4 | "2 hours behind `@mario`" - 4 | 2 | "2 hours ahead of `@mario`" - 2 | 3 | "1 hour behind `@mario`" - 3 | 2 | "1 hour ahead of `@mario`" - 2 | 2 | "same timezone as `@mario`" - end - - with_them do - it 'returns markdown name with timezone info' do - author = described_class.new(options.merge('username' => 'mario', 'tz_offset_hours' => author_offset)) - - floored_offset_hours = subject.__send__(:floored_offset_hours) - utc_offset = floored_offset_hours >= 0 ? "+#{floored_offset_hours}" : floored_offset_hours - - expect(subject.markdown_name(author: author)).to eq("#{options['markdown_name']} (UTC#{utc_offset}, #{diff_text})") - end - end - end - end -end diff --git a/spec/lib/gitlab/danger/title_linting_spec.rb b/spec/lib/gitlab/danger/title_linting_spec.rb deleted file mode 100644 index b48d2c5e53d..00000000000 --- a/spec/lib/gitlab/danger/title_linting_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' -require 'rspec-parameterized' - -require 'gitlab/danger/title_linting' - -RSpec.describe Gitlab::Danger::TitleLinting do - using RSpec::Parameterized::TableSyntax - - describe '#sanitize_mr_title' do - where(:mr_title, :expected_mr_title) do - '`My MR title`' | "\\`My MR title\\`" - 'WIP: My MR title' | 'My MR title' - 'Draft: My MR title' | 'My MR title' - '(Draft) My MR title' | 'My MR title' - '[Draft] My MR title' | 'My MR title' - '[DRAFT] My MR title' | 'My MR title' - 'DRAFT: My MR title' | 'My MR title' - 'DRAFT: `My MR title`' | "\\`My MR title\\`" - end - - with_them do - subject { described_class.sanitize_mr_title(mr_title) } - - it { is_expected.to eq(expected_mr_title) } - end - end - - describe '#remove_draft_flag' do - where(:mr_title, :expected_mr_title) do - 'WIP: My MR title' | 'My MR title' - 'Draft: My MR title' | 'My MR title' - '(Draft) My MR title' | 'My MR title' - '[Draft] My MR title' | 'My MR title' - '[DRAFT] My MR title' | 'My MR title' - 'DRAFT: My MR title' | 'My MR title' - end - - with_them do - subject { described_class.remove_draft_flag(mr_title) } - - it { is_expected.to eq(expected_mr_title) } - end - end - - describe '#has_draft_flag?' do - it 'returns true for a draft title' do - expect(described_class.has_draft_flag?('Draft: My MR title')).to be true - end - - it 'returns false for non draft title' do - expect(described_class.has_draft_flag?('My MR title')).to be false - end - end -end diff --git a/spec/lib/gitlab/danger/weightage/maintainers_spec.rb b/spec/lib/gitlab/danger/weightage/maintainers_spec.rb deleted file mode 100644 index 066bb487fa2..00000000000 --- a/spec/lib/gitlab/danger/weightage/maintainers_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'gitlab/danger/weightage/maintainers' - -RSpec.describe Gitlab::Danger::Weightage::Maintainers do - let(:multiplier) { Gitlab::Danger::Weightage::CAPACITY_MULTIPLIER } - let(:regular_maintainer) { double('Teammate', reduced_capacity: false) } - let(:reduced_capacity_maintainer) { double('Teammate', reduced_capacity: true) } - let(:maintainers) do - [ - regular_maintainer, - reduced_capacity_maintainer - ] - end - - let(:maintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier } - let(:reduced_capacity_maintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT } - - subject(:weighted_maintainers) { described_class.new(maintainers).execute } - - describe '#execute' do - it 'weights the maintainers overall' do - expect(weighted_maintainers.count).to eq maintainer_count + reduced_capacity_maintainer_count - end - - it 'has total count of regular maintainers' do - expect(weighted_maintainers.count { |r| r.object_id == regular_maintainer.object_id }).to eq maintainer_count - end - - it 'has count of reduced capacity maintainers' do - expect(weighted_maintainers.count { |r| r.object_id == reduced_capacity_maintainer.object_id }).to eq reduced_capacity_maintainer_count - end - end -end diff --git a/spec/lib/gitlab/danger/weightage/reviewers_spec.rb b/spec/lib/gitlab/danger/weightage/reviewers_spec.rb deleted file mode 100644 index cca81f4d9b5..00000000000 --- a/spec/lib/gitlab/danger/weightage/reviewers_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require 'gitlab/danger/weightage/reviewers' - -RSpec.describe Gitlab::Danger::Weightage::Reviewers do - let(:multiplier) { Gitlab::Danger::Weightage::CAPACITY_MULTIPLIER } - let(:regular_reviewer) { double('Teammate', hungry: false, reduced_capacity: false) } - let(:hungry_reviewer) { double('Teammate', hungry: true, reduced_capacity: false) } - let(:reduced_capacity_reviewer) { double('Teammate', hungry: false, reduced_capacity: true) } - let(:reviewers) do - [ - hungry_reviewer, - regular_reviewer, - reduced_capacity_reviewer - ] - end - - let(:regular_traintainer) { double('Teammate', hungry: false, reduced_capacity: false) } - let(:hungry_traintainer) { double('Teammate', hungry: true, reduced_capacity: false) } - let(:reduced_capacity_traintainer) { double('Teammate', hungry: false, reduced_capacity: true) } - let(:traintainers) do - [ - hungry_traintainer, - regular_traintainer, - reduced_capacity_traintainer - ] - end - - let(:hungry_reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier + described_class::DEFAULT_REVIEWER_WEIGHT } - let(:hungry_traintainer_count) { described_class::TRAINTAINER_WEIGHT * multiplier + described_class::DEFAULT_REVIEWER_WEIGHT } - let(:reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier } - let(:traintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * described_class::TRAINTAINER_WEIGHT * multiplier } - let(:reduced_capacity_reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT } - let(:reduced_capacity_traintainer_count) { described_class::TRAINTAINER_WEIGHT } - - subject(:weighted_reviewers) { described_class.new(reviewers, traintainers).execute } - - describe '#execute', :aggregate_failures do - it 'weights the reviewers overall' do - reviewers_count = hungry_reviewer_count + reviewer_count + reduced_capacity_reviewer_count - traintainers_count = hungry_traintainer_count + traintainer_count + reduced_capacity_traintainer_count - - expect(weighted_reviewers.count).to eq reviewers_count + traintainers_count - end - - it 'has total count of hungry reviewers and traintainers' do - expect(weighted_reviewers.count(&:hungry)).to eq hungry_reviewer_count + hungry_traintainer_count - expect(weighted_reviewers.count { |r| r.object_id == hungry_reviewer.object_id }).to eq hungry_reviewer_count - expect(weighted_reviewers.count { |r| r.object_id == hungry_traintainer.object_id }).to eq hungry_traintainer_count - end - - it 'has total count of regular reviewers and traintainers' do - expect(weighted_reviewers.count { |r| r.object_id == regular_reviewer.object_id }).to eq reviewer_count - expect(weighted_reviewers.count { |r| r.object_id == regular_traintainer.object_id }).to eq traintainer_count - end - - it 'has count of reduced capacity reviewers' do - expect(weighted_reviewers.count(&:reduced_capacity)).to eq reduced_capacity_reviewer_count + reduced_capacity_traintainer_count - expect(weighted_reviewers.count { |r| r.object_id == reduced_capacity_reviewer.object_id }).to eq reduced_capacity_reviewer_count - expect(weighted_reviewers.count { |r| r.object_id == reduced_capacity_traintainer.object_id }).to eq reduced_capacity_traintainer_count - end - end -end diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index 2f74e766a11..4242469b3db 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::DataBuilder::Build do - let(:runner) { create(:ci_runner, :instance) } + let!(:tag_names) { %w(tag-1 tag-2) } + let(:runner) { create(:ci_runner, :instance, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n)}) } let(:user) { create(:user) } let(:build) { create(:ci_build, :running, runner: runner, user: user) } @@ -35,6 +36,7 @@ RSpec.describe Gitlab::DataBuilder::Build do } it { expect(data[:commit][:id]).to eq(build.pipeline.id) } it { expect(data[:runner][:id]).to eq(build.runner.id) } + it { expect(data[:runner][:tags]).to match_array(tag_names) } it { expect(data[:runner][:description]).to eq(build.runner.description) } context 'commit author_url' do diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb index 297d87708d8..32619fc4c37 100644 --- a/spec/lib/gitlab/data_builder/pipeline_spec.rb +++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb @@ -51,13 +51,15 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do context 'build with runner' do let!(:build) { create(:ci_build, pipeline: pipeline, runner: ci_runner) } - let(:ci_runner) { create(:ci_runner) } + let!(:tag_names) { %w(tag-1 tag-2) } + let(:ci_runner) { create(:ci_runner, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n)}) } it 'has runner attributes', :aggregate_failures 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/migration_helpers/v2_spec.rb b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb new file mode 100644 index 00000000000..f132ecbf13b --- /dev/null +++ b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb @@ -0,0 +1,221 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::MigrationHelpers::V2 do + include Database::TriggerHelpers + + let(:migration) do + ActiveRecord::Migration.new.extend(described_class) + end + + before do + allow(migration).to receive(:puts) + end + + shared_examples_for 'Setting up to rename a column' do + let(:model) { Class.new(ActiveRecord::Base) } + + before do + model.table_name = :test_table + end + + context 'when called inside a transaction block' do + before do + allow(migration).to receive(:transaction_open?).and_return(true) + end + + it 'raises an error' do + expect do + migration.public_send(operation, :test_table, :original, :renamed) + end.to raise_error("#{operation} can not be run inside a transaction") + end + end + + context 'when the existing column has a default value' do + before do + migration.change_column_default :test_table, existing_column, 'default value' + end + + it 'raises an error' do + expect do + migration.public_send(operation, :test_table, :original, :renamed) + end.to raise_error("#{operation} does not currently support columns with default values") + end + end + + context 'when passing a batch column' do + context 'when the batch column does not exist' do + it 'raises an error' do + expect do + migration.public_send(operation, :test_table, :original, :renamed, batch_column_name: :missing) + end.to raise_error('Column missing does not exist on test_table') + end + end + + context 'when the batch column does exist' do + it 'passes it when creating the column' do + expect(migration).to receive(:create_column_from) + .with(:test_table, existing_column, added_column, type: nil, batch_column_name: :status) + .and_call_original + + migration.public_send(operation, :test_table, :original, :renamed, batch_column_name: :status) + end + end + end + + it 'creates the renamed column, syncing existing data' do + existing_record_1 = model.create!(status: 0, existing_column => 'existing') + existing_record_2 = model.create!(status: 0, existing_column => nil) + + migration.send(operation, :test_table, :original, :renamed) + model.reset_column_information + + expect(migration.column_exists?(:test_table, added_column)).to eq(true) + + expect(existing_record_1.reload).to have_attributes(status: 0, original: 'existing', renamed: 'existing') + expect(existing_record_2.reload).to have_attributes(status: 0, original: nil, renamed: nil) + end + + it 'installs triggers to sync new data' do + migration.public_send(operation, :test_table, :original, :renamed) + model.reset_column_information + + new_record_1 = model.create!(status: 1, original: 'first') + new_record_2 = model.create!(status: 1, renamed: 'second') + + expect(new_record_1.reload).to have_attributes(status: 1, original: 'first', renamed: 'first') + expect(new_record_2.reload).to have_attributes(status: 1, original: 'second', renamed: 'second') + + new_record_1.update!(original: 'updated') + new_record_2.update!(renamed: nil) + + expect(new_record_1.reload).to have_attributes(status: 1, original: 'updated', renamed: 'updated') + expect(new_record_2.reload).to have_attributes(status: 1, original: nil, renamed: nil) + end + end + + describe '#rename_column_concurrently' do + before do + allow(migration).to receive(:transaction_open?).and_return(false) + + migration.create_table :test_table do |t| + t.integer :status, null: false + t.text :original + t.text :other_column + end + end + + it_behaves_like 'Setting up to rename a column' do + let(:operation) { :rename_column_concurrently } + let(:existing_column) { :original } + let(:added_column) { :renamed } + end + + context 'when the column to rename does not exist' do + it 'raises an error' do + expect do + migration.rename_column_concurrently :test_table, :missing_column, :renamed + end.to raise_error('Column missing_column does not exist on test_table') + end + end + end + + describe '#undo_cleanup_concurrent_column_rename' do + before do + allow(migration).to receive(:transaction_open?).and_return(false) + + migration.create_table :test_table do |t| + t.integer :status, null: false + t.text :other_column + t.text :renamed + end + end + + it_behaves_like 'Setting up to rename a column' do + let(:operation) { :undo_cleanup_concurrent_column_rename } + let(:existing_column) { :renamed } + let(:added_column) { :original } + end + + context 'when the renamed column does not exist' do + it 'raises an error' do + expect do + migration.undo_cleanup_concurrent_column_rename :test_table, :original, :missing_column + end.to raise_error('Column missing_column does not exist on test_table') + end + end + end + + shared_examples_for 'Cleaning up from renaming a column' do + let(:connection) { migration.connection } + + before do + allow(migration).to receive(:transaction_open?).and_return(false) + + migration.create_table :test_table do |t| + t.integer :status, null: false + t.text :original + t.text :other_column + end + + migration.rename_column_concurrently :test_table, :original, :renamed + end + + context 'when the helper is called repeatedly' do + before do + migration.public_send(operation, :test_table, :original, :renamed) + end + + it 'does not make repeated attempts to cleanup' do + expect(migration).not_to receive(:remove_column) + + expect do + migration.public_send(operation, :test_table, :original, :renamed) + end.not_to raise_error + end + end + + context 'when the renamed column exists' do + let(:triggers) do + [ + ['trigger_7cc71f92fd63', 'function_for_trigger_7cc71f92fd63', before: 'insert'], + ['trigger_f1a1f619636a', 'function_for_trigger_f1a1f619636a', before: 'update'], + ['trigger_769a49938884', 'function_for_trigger_769a49938884', before: 'update'] + ] + end + + it 'removes the sync triggers and renamed columns' do + triggers.each do |(trigger_name, function_name, event)| + expect_function_to_exist(function_name) + expect_valid_function_trigger(:test_table, trigger_name, function_name, event) + end + + expect(migration.column_exists?(:test_table, added_column)).to eq(true) + + migration.public_send(operation, :test_table, :original, :renamed) + + expect(migration.column_exists?(:test_table, added_column)).to eq(false) + + triggers.each do |(trigger_name, function_name, _)| + expect_trigger_not_to_exist(:test_table, trigger_name) + expect_function_not_to_exist(function_name) + end + end + end + end + + describe '#undo_rename_column_concurrently' do + it_behaves_like 'Cleaning up from renaming a column' do + let(:operation) { :undo_rename_column_concurrently } + let(:added_column) { :renamed } + end + end + + describe '#cleanup_concurrent_column_rename' do + it_behaves_like 'Cleaning up from renaming a column' do + let(:operation) { :cleanup_concurrent_column_rename } + let(:added_column) { :original } + end + end +end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 6b709cba5b3..6de7fc3a50e 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -1874,7 +1874,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do has_internal_id :iid, scope: :project, init: ->(s, _scope) { s&.project&.issues&.maximum(:iid) }, - backfill: true, presence: false end end @@ -1928,258 +1927,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(issue_b.iid).to eq(3) end - context 'when the new code creates a row post deploy but before the migration runs' do - it 'does not change the row iid' do - project = setup - issue = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - expect(issue.reload.iid).to eq(1) - end - - it 'backfills iids for rows already in the database' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_c.reload.iid).to eq(3) - end - - it 'backfills iids across multiple projects' do - project_a = setup - project_b = setup - issue_a = issues.create!(project_id: project_a.id) - issue_b = issues.create!(project_id: project_b.id) - issue_c = Issue.create!(project_id: project_a.id) - issue_d = Issue.create!(project_id: project_b.id) - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(1) - expect(issue_c.reload.iid).to eq(2) - expect(issue_d.reload.iid).to eq(2) - end - - it 'generates iids properly for models created after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - issue_d = Issue.create!(project_id: project.id) - issue_e = Issue.create!(project_id: project.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_c.reload.iid).to eq(3) - expect(issue_d.iid).to eq(4) - expect(issue_e.iid).to eq(5) - end - - it 'backfills iids and properly generates iids for new models across multiple projects' do - project_a = setup - project_b = setup - issue_a = issues.create!(project_id: project_a.id) - issue_b = issues.create!(project_id: project_b.id) - issue_c = Issue.create!(project_id: project_a.id) - issue_d = Issue.create!(project_id: project_b.id) - - model.backfill_iids('issues') - - issue_e = Issue.create!(project_id: project_a.id) - issue_f = Issue.create!(project_id: project_b.id) - issue_g = Issue.create!(project_id: project_a.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(1) - expect(issue_c.reload.iid).to eq(2) - expect(issue_d.reload.iid).to eq(2) - expect(issue_e.iid).to eq(3) - expect(issue_f.iid).to eq(3) - expect(issue_g.iid).to eq(4) - end - end - - context 'when the new code creates a model and then old code creates a model post deploy but before the migration runs' do - it 'backfills iids' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = Issue.create!(project_id: project.id) - issue_c = issues.create!(project_id: project.id) - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_c.reload.iid).to eq(3) - end - - it 'generates an iid for a new model after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_d = issues.create!(project_id: project.id) - - model.backfill_iids('issues') - - issue_e = Issue.create!(project_id: project.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_c.reload.iid).to eq(3) - expect(issue_d.reload.iid).to eq(4) - expect(issue_e.iid).to eq(5) - end - end - - context 'when the new code and old code alternate creating models post deploy but before the migration runs' do - it 'backfills iids' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = Issue.create!(project_id: project.id) - issue_c = issues.create!(project_id: project.id) - issue_d = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_c.reload.iid).to eq(3) - expect(issue_d.reload.iid).to eq(4) - end - - it 'generates an iid for a new model after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_d = issues.create!(project_id: project.id) - issue_e = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - issue_f = Issue.create!(project_id: project.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_c.reload.iid).to eq(3) - expect(issue_d.reload.iid).to eq(4) - expect(issue_e.reload.iid).to eq(5) - expect(issue_f.iid).to eq(6) - end - end - - context 'when the new code creates and deletes a model post deploy but before the migration runs' do - it 'backfills iids for rows already in the database' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_c.delete - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - end - - it 'successfully creates a new model after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_c.delete - - model.backfill_iids('issues') - - issue_d = Issue.create!(project_id: project.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_d.iid).to eq(3) - end - end - - context 'when the new code creates and deletes a model and old code creates a model post deploy but before the migration runs' do - it 'backfills iids' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_c.delete - issue_d = issues.create!(project_id: project.id) - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_d.reload.iid).to eq(3) - end - - it 'successfully creates a new model after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_c.delete - issue_d = issues.create!(project_id: project.id) - - model.backfill_iids('issues') - - issue_e = Issue.create!(project_id: project.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_d.reload.iid).to eq(3) - expect(issue_e.iid).to eq(4) - end - end - - context 'when the new code creates and deletes a model and then creates another model post deploy but before the migration runs' do - it 'successfully generates an iid for a new model after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_c.delete - issue_d = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_d.reload.iid).to eq(3) - end - - it 'successfully generates an iid for a new model after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_c.delete - issue_d = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - issue_e = Issue.create!(project_id: project.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_d.reload.iid).to eq(3) - expect(issue_e.iid).to eq(4) - end - end - context 'when the first model is created for a project after the migration' do it 'generates an iid' do project_a = setup 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 b50e02c7043..b5d741fc5e9 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 @@ -513,6 +513,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe context 'finishing pending background migration jobs' do let(:source_table_double) { double('table name') } let(:raw_arguments) { [1, 50_000, source_table_double, partitioned_table, source_column] } + let(:background_job) { double('background job', args: ['background jobs', raw_arguments]) } before do allow(migration).to receive(:table_exists?).with(partitioned_table).and_return(true) @@ -528,7 +529,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe expect(Gitlab::BackgroundMigration).to receive(:steal) .with(described_class::MIGRATION_CLASS_NAME) - .and_yield(raw_arguments) + .and_yield(background_job) expect(source_table_double).to receive(:==).with(source_table.to_s) diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb index 220ae705e71..563399ff0d9 100644 --- a/spec/lib/gitlab/database/with_lock_retries_spec.rb +++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb @@ -54,6 +54,10 @@ RSpec.describe Gitlab::Database::WithLockRetries do lock_fiber.resume # start the transaction and lock the table end + after do + lock_fiber.resume if lock_fiber.alive? + end + context 'lock_fiber' do it 'acquires lock successfully' do check_exclusive_lock_query = """ diff --git a/spec/lib/gitlab/diff/char_diff_spec.rb b/spec/lib/gitlab/diff/char_diff_spec.rb new file mode 100644 index 00000000000..e4e2a3ba050 --- /dev/null +++ b/spec/lib/gitlab/diff/char_diff_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'diff_match_patch' + +RSpec.describe Gitlab::Diff::CharDiff do + let(:old_string) { "Helo \n Worlld" } + let(:new_string) { "Hello \n World" } + + subject(:diff) { described_class.new(old_string, new_string) } + + describe '#generate_diff' do + context 'when old string is nil' do + let(:old_string) { nil } + + it 'does not raise an error' do + expect { subject.generate_diff }.not_to raise_error + end + + it 'treats nil values as blank strings' do + changes = subject.generate_diff + + expect(changes).to eq([ + [:insert, "Hello \n World"] + ]) + end + end + + it 'generates an array of changes' do + changes = subject.generate_diff + + expect(changes).to eq([ + [:equal, "Hel"], + [:insert, "l"], + [:equal, "o \n Worl"], + [:delete, "l"], + [:equal, "d"] + ]) + end + end + + describe '#changed_ranges' do + subject { diff.changed_ranges } + + context 'when old string is nil' do + let(:old_string) { nil } + + it 'returns lists of changes' do + old_diffs, new_diffs = subject + + expect(old_diffs).to eq([]) + expect(new_diffs).to eq([0..12]) + 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]) + end + end + + describe '#to_html' do + it 'returns an HTML representation of the diff' do + subject.generate_diff + + expect(subject.to_html).to eq( + '<span class="idiff">Hel</span>' \ + '<span class="idiff addition">l</span>' \ + "<span class=\"idiff\">o \n Worl</span>" \ + '<span class="idiff deletion">l</span>' \ + '<span class="idiff">d</span>' + ) + end + end +end diff --git a/spec/lib/gitlab/diff/file_collection_sorter_spec.rb b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb index 8822fc55c6e..9ba9271cefc 100644 --- a/spec/lib/gitlab/diff/file_collection_sorter_spec.rb +++ b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb @@ -5,11 +5,14 @@ require 'spec_helper' RSpec.describe Gitlab::Diff::FileCollectionSorter do let(:diffs) do [ + double(new_path: 'README', old_path: 'README'), double(new_path: '.dir/test', old_path: '.dir/test'), double(new_path: '', old_path: '.file'), double(new_path: '1-folder/A-file.ext', old_path: '1-folder/A-file.ext'), + double(new_path: '1-folder/README', old_path: '1-folder/README'), double(new_path: nil, old_path: '1-folder/M-file.ext'), double(new_path: '1-folder/Z-file.ext', old_path: '1-folder/Z-file.ext'), + double(new_path: '1-folder/README', old_path: '1-folder/README'), double(new_path: '', old_path: '1-folder/nested/A-file.ext'), double(new_path: '1-folder/nested/M-file.ext', old_path: '1-folder/nested/M-file.ext'), double(new_path: nil, old_path: '1-folder/nested/Z-file.ext'), @@ -19,7 +22,8 @@ RSpec.describe Gitlab::Diff::FileCollectionSorter do double(new_path: nil, old_path: '2-folder/nested/A-file.ext'), double(new_path: 'A-file.ext', old_path: 'A-file.ext'), double(new_path: '', old_path: 'M-file.ext'), - double(new_path: 'Z-file.ext', old_path: 'Z-file.ext') + double(new_path: 'Z-file.ext', old_path: 'Z-file.ext'), + double(new_path: 'README', old_path: 'README') ] end @@ -36,6 +40,8 @@ RSpec.describe Gitlab::Diff::FileCollectionSorter do '1-folder/nested/Z-file.ext', '1-folder/A-file.ext', '1-folder/M-file.ext', + '1-folder/README', + '1-folder/README', '1-folder/Z-file.ext', '2-folder/nested/A-file.ext', '2-folder/A-file.ext', @@ -44,6 +50,8 @@ RSpec.describe Gitlab::Diff::FileCollectionSorter do '.file', 'A-file.ext', 'M-file.ext', + 'README', + 'README', 'Z-file.ext' ]) end diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb index f6810d7a966..94717152488 100644 --- a/spec/lib/gitlab/diff/highlight_cache_spec.rb +++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb @@ -233,4 +233,22 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do cache.write_if_empty end end + + describe '#key' do + subject { cache.key } + + it 'returns the next version of the cache' do + is_expected.to start_with("highlighted-diff-files:#{cache.diffable.cache_key}:2") + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(improved_merge_diff_highlighting: false) + end + + it 'returns the original version of the cache' do + is_expected.to start_with("highlighted-diff-files:#{cache.diffable.cache_key}:1") + end + end + end end diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb index 35284e952f7..dce655d5690 100644 --- a/spec/lib/gitlab/diff/inline_diff_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_spec.rb @@ -37,6 +37,33 @@ RSpec.describe Gitlab::Diff::InlineDiff do it 'can handle unchanged empty lines' do expect { described_class.for_lines(['- bar', '+ baz', '']) }.not_to raise_error 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]) + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(improved_merge_diff_highlighting: false) + end + + it 'finds all inline diffs' do + expect(subject[0]).to eq([3..19]) + expect(subject[1]).to eq([3..22]) + end + end + end end describe "#inline_diffs" do diff --git a/spec/lib/gitlab/experimentation/controller_concern_spec.rb b/spec/lib/gitlab/experimentation/controller_concern_spec.rb index c47f71c207d..1cebe37bea5 100644 --- a/spec/lib/gitlab/experimentation/controller_concern_spec.rb +++ b/spec/lib/gitlab/experimentation/controller_concern_spec.rb @@ -10,6 +10,10 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do use_backwards_compatible_subject_index: true }, test_experiment: { + tracking_category: 'Team', + rollout_strategy: rollout_strategy + }, + my_experiment: { tracking_category: 'Team' } } @@ -20,6 +24,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do end let(:enabled_percentage) { 10 } + let(:rollout_strategy) { nil } controller(ApplicationController) do include Gitlab::Experimentation::ControllerConcern @@ -117,6 +122,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do end context 'when subject is given' do + let(:rollout_strategy) { :user } let(:user) { build(:user) } it 'uses the subject' do @@ -244,6 +250,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do it "provides the subject's hashed global_id as label" do experiment_subject = double(:subject, to_global_id: 'abc') + allow(Gitlab::Experimentation).to receive(:valid_subject_for_rollout_strategy?).and_return(true) controller.track_experiment_event(:test_experiment, 'start', 1, subject: experiment_subject) @@ -420,6 +427,26 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do controller.record_experiment_user(:test_experiment, context) end + + context 'with a cookie based rollout strategy' do + it 'calls tracking_group with a nil subject' do + expect(controller).to receive(:tracking_group).with(:test_experiment, nil, subject: nil).and_return(:experimental) + allow(::Experiment).to receive(:add_user).with(:test_experiment, :experimental, user, context) + + controller.record_experiment_user(:test_experiment, context) + end + end + + context 'with a user based rollout strategy' do + let(:rollout_strategy) { :user } + + it 'calls tracking_group with a user subject' do + expect(controller).to receive(:tracking_group).with(:test_experiment, nil, subject: user).and_return(:experimental) + allow(::Experiment).to receive(:add_user).with(:test_experiment, :experimental, user, context) + + controller.record_experiment_user(:test_experiment, context) + end + end end context 'the user is part of the control group' do diff --git a/spec/lib/gitlab/experimentation/experiment_spec.rb b/spec/lib/gitlab/experimentation/experiment_spec.rb index 008e6699597..94dbf1d7e4b 100644 --- a/spec/lib/gitlab/experimentation/experiment_spec.rb +++ b/spec/lib/gitlab/experimentation/experiment_spec.rb @@ -9,7 +9,8 @@ RSpec.describe Gitlab::Experimentation::Experiment do let(:params) do { tracking_category: 'Category1', - use_backwards_compatible_subject_index: true + use_backwards_compatible_subject_index: true, + rollout_strategy: nil } end diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb index b503960b8c7..4ef8a75de4f 100644 --- a/spec/lib/gitlab/experimentation_spec.rb +++ b/spec/lib/gitlab/experimentation_spec.rb @@ -15,8 +15,7 @@ RSpec.describe Gitlab::Experimentation::EXPERIMENTS do :invite_members_empty_group_version_a, :contact_sales_btn_in_app, :customize_homepage, - :group_only_trials, - :default_to_issues_board + :group_only_trials ] backwards_compatible_experiment_keys = described_class.filter { |_, v| v[:use_backwards_compatible_subject_index] }.keys @@ -27,6 +26,8 @@ RSpec.describe Gitlab::Experimentation::EXPERIMENTS do end RSpec.describe Gitlab::Experimentation do + using RSpec::Parameterized::TableSyntax + before do stub_const('Gitlab::Experimentation::EXPERIMENTS', { backwards_compatible_test_experiment: { @@ -35,6 +36,10 @@ RSpec.describe Gitlab::Experimentation do }, test_experiment: { tracking_category: 'Team' + }, + tabular_experiment: { + tracking_category: 'Team', + rollout_strategy: rollout_strategy } }) @@ -46,6 +51,7 @@ RSpec.describe Gitlab::Experimentation do end let(:enabled_percentage) { 10 } + let(:rollout_strategy) { nil } describe '.get_experiment' do subject { described_class.get_experiment(:test_experiment) } @@ -175,4 +181,59 @@ RSpec.describe Gitlab::Experimentation do end end end + + describe '.log_invalid_rollout' do + subject { described_class.log_invalid_rollout(:test_experiment, 1) } + + before do + allow(described_class).to receive(:valid_subject_for_rollout_strategy?).and_return(valid) + end + + context 'subject is not valid for experiment' do + let(:valid) { false } + + it 'logs a warning message' do + expect_next_instance_of(Gitlab::ExperimentationLogger) do |logger| + expect(logger) + .to receive(:warn) + .with( + message: 'Subject must conform to the rollout strategy', + experiment_key: :test_experiment, + subject: 'Integer', + rollout_strategy: :cookie + ) + end + + subject + end + end + + context 'subject is valid for experiment' do + let(:valid) { true } + + it 'does not log a warning message' do + expect(Gitlab::ExperimentationLogger).not_to receive(:build) + + subject + end + end + end + + describe '.valid_subject_for_rollout_strategy?' do + subject { described_class.valid_subject_for_rollout_strategy?(:tabular_experiment, experiment_subject) } + + where(:rollout_strategy, :experiment_subject, :result) do + :cookie | nil | true + nil | nil | true + :cookie | 'string' | true + nil | User.new | false + :user | User.new | true + :group | User.new | false + :group | Group.new | true + end + + with_them do + it { is_expected.to be(result) } + end + end end diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb index 8d6df62b3f6..0b5303f22b4 100644 --- a/spec/lib/gitlab/file_finder_spec.rb +++ b/spec/lib/gitlab/file_finder_spec.rb @@ -53,6 +53,14 @@ RSpec.describe Gitlab::FileFinder do end end + context 'with white space in the path' do + it 'filters by path correctly' do + results = subject.find('directory path:"with space/README.md"') + + expect(results.count).to eq(1) + end + end + it 'does not cause N+1 query' do expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 8961cdcae7d..49f1e6e994f 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -720,7 +720,8 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do committer_name: "Dmitriy Zaporozhets", id: SeedRepo::Commit::ID, message: "tree css fixes", - parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"] + parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"], + trailers: {} } end end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 783f0a9ccf7..17bb83d0f2f 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -100,6 +100,13 @@ EOT expect(diff.diff).to be_empty expect(diff).to be_too_large end + + it 'logs the event' do + expect(Gitlab::Metrics).to receive(:add_event) + .with(:patch_hard_limit_bytes_hit) + + diff + end end context 'using a collapsable diff that is too large' do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index ef9b5a30c86..cc1b1ceadcf 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1894,8 +1894,11 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it 'removes the remote' do repository_rugged.remotes.create(remote_name, url) - repository.remove_remote(remote_name) + expect(repository.remove_remote(remote_name)).to be true + # Since we deleted the remote via Gitaly, Rugged doesn't know + # this changed underneath it. Let's refresh the Rugged repo. + repository_rugged = Rugged::Repository.new(repository_path) expect(repository_rugged.remotes[remote_name]).to be_nil end end diff --git a/spec/lib/gitlab/graphql/pagination/connections_spec.rb b/spec/lib/gitlab/graphql/pagination/connections_spec.rb new file mode 100644 index 00000000000..e89e5c17644 --- /dev/null +++ b/spec/lib/gitlab/graphql/pagination/connections_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Tests that our connections are correctly mapped. +RSpec.describe ::Gitlab::Graphql::Pagination::Connections do + include GraphqlHelpers + + before(:all) do + ActiveRecord::Schema.define do + create_table :testing_pagination_nodes, force: true do |t| + t.integer :value, null: false + end + end + end + + after(:all) do + ActiveRecord::Schema.define do + drop_table :testing_pagination_nodes, force: true + end + end + + let_it_be(:node_model) do + Class.new(ActiveRecord::Base) do + self.table_name = 'testing_pagination_nodes' + end + end + + let(:query_string) { 'query { items(first: 2) { nodes { value } } }' } + let(:user) { nil } + + let(:node) { Struct.new(:value) } + let(:node_type) do + Class.new(::GraphQL::Schema::Object) do + graphql_name 'Node' + field :value, GraphQL::INT_TYPE, null: false + end + end + + let(:query_type) do + item_values = nodes + + query_factory do |t| + t.field :items, node_type.connection_type, null: true + + t.define_method :items do + item_values + end + end + end + + shared_examples 'it maps to a specific connection class' do |connection_type| + let(:raw_values) { [1, 7, 42] } + + it "maps to #{connection_type.name}" do + expect(connection_type).to receive(:new).and_call_original + + results = execute_query(query_type).to_h + + expect(graphql_dig_at(results, :data, :items, :nodes, :value)).to eq [1, 7] + end + end + + describe 'OffsetPaginatedRelation' do + before do + # Expect to be ordered by an explicit ordering. + raw_values.each_with_index { |value, id| node_model.create!(id: id, value: value) } + end + + let(:nodes) { ::Gitlab::Graphql::Pagination::OffsetPaginatedRelation.new(node_model.order(value: :asc)) } + + include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection + end + + describe 'ActiveRecord::Relation' do + before do + # Expect to be ordered by ID descending + [3, 2, 1].zip(raw_values) { |id, value| node_model.create!(id: id, value: value) } + end + + let(:nodes) { node_model.all } + + include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::Keyset::Connection + end + + describe 'ExternallyPaginatedArray' do + let(:nodes) { ::Gitlab::Graphql::ExternallyPaginatedArray.new(nil, nil, node.new(1), node.new(7)) } + + include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection + end + + describe 'Array' do + let(:nodes) { raw_values.map { |x| node.new(x) } } + + include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::ArrayConnection + end +end diff --git a/spec/lib/gitlab/hook_data/group_builder_spec.rb b/spec/lib/gitlab/hook_data/group_builder_spec.rb new file mode 100644 index 00000000000..d7347ff99d4 --- /dev/null +++ b/spec/lib/gitlab/hook_data/group_builder_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::HookData::GroupBuilder do + let_it_be(:group) { create(:group) } + + describe '#build' do + let(:data) { described_class.new(group).build(event) } + let(:event_name) { data[:event_name] } + let(:attributes) do + [ + :event_name, :created_at, :updated_at, :name, :path, :full_path, :group_id + ] + 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(group.name) + expect(data[:path]).to eq(group.path) + expect(data[:full_path]).to eq(group.full_path) + expect(data[:group_id]).to eq(group.id) + expect(data[:created_at]).to eq(group.created_at.xmlschema) + expect(data[:updated_at]).to eq(group.updated_at.xmlschema) + end + end + + shared_examples_for 'does not include old path attributes' do + it 'does not include old path attributes' do + expect(data).not_to include(:old_path, :old_full_path) + end + end + + context 'on create' do + let(:event) { :create } + + it { expect(event_name).to eq('group_create') } + it_behaves_like 'includes the required attributes' + it_behaves_like 'does not include old path attributes' + end + + context 'on destroy' do + let(:event) { :destroy } + + it { expect(event_name).to eq('group_destroy') } + it_behaves_like 'includes the required attributes' + it_behaves_like 'does not include old path attributes' + end + + context 'on rename' do + let(:event) { :rename } + + it { expect(event_name).to eq('group_rename') } + it_behaves_like 'includes the required attributes' + + it 'includes old path details' do + allow(group).to receive(:path_before_last_save).and_return('old-path') + + expect(data[:old_path]).to eq(group.path_before_last_save) + expect(data[:old_full_path]).to eq(group.path_before_last_save) + end + end + end + end +end diff --git a/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb b/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb new file mode 100644 index 00000000000..89e5dffd7b4 --- /dev/null +++ b/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::HookData::SubgroupBuilder do + let_it_be(:parent_group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: parent_group) } + + describe '#build' do + let(:data) { described_class.new(subgroup).build(event) } + let(:event_name) { data[:event_name] } + let(:attributes) do + [ + :event_name, :created_at, :updated_at, :name, :path, :full_path, :group_id, + :parent_group_id, :parent_name, :parent_path, :parent_full_path + ] + 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(subgroup.name) + expect(data[:path]).to eq(subgroup.path) + expect(data[:full_path]).to eq(subgroup.full_path) + expect(data[:group_id]).to eq(subgroup.id) + expect(data[:created_at]).to eq(subgroup.created_at.xmlschema) + expect(data[:updated_at]).to eq(subgroup.updated_at.xmlschema) + expect(data[:parent_name]).to eq(parent_group.name) + expect(data[:parent_path]).to eq(parent_group.path) + expect(data[:parent_full_path]).to eq(parent_group.full_path) + expect(data[:parent_group_id]).to eq(parent_group.id) + end + end + + context 'on create' do + let(:event) { :create } + + it { expect(event_name).to eq('subgroup_create') } + it_behaves_like 'includes the required attributes' + end + + context 'on destroy' do + let(:event) { :destroy } + + it { expect(event_name).to eq('subgroup_destroy') } + it_behaves_like 'includes the required attributes' + 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 825513bdfc5..2d616ec8862 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -146,6 +146,7 @@ merge_requests: - merge_user - merge_request_diffs - merge_request_diff +- merge_head_diff - merge_request_context_commits - merge_request_context_commit_diff_files - events @@ -544,7 +545,7 @@ project: - daily_build_group_report_results - jira_imports - compliance_framework_setting -- compliance_management_frameworks +- compliance_management_framework - metrics_users_starred_dashboards - alert_management_alerts - repository_storage_moves @@ -561,6 +562,7 @@ project: - exported_protected_branches - incident_management_oncall_schedules - debian_distributions +- merge_request_metrics award_emoji: - awardable - user @@ -589,6 +591,7 @@ lfs_file_locks: project_badges: - project metrics: +- target_project - merge_request - latest_closed_by - merged_by diff --git a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb index b311a02833c..6680f4e7a03 100644 --- a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb @@ -11,12 +11,12 @@ RSpec.describe Gitlab::ImportExport::DesignRepoRestorer do let!(:project) { create(:project) } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { project.import_export_shared } - let(:bundler) { Gitlab::ImportExport::DesignRepoSaver.new(project: project_with_design_repo, shared: shared) } + let(:bundler) { Gitlab::ImportExport::DesignRepoSaver.new(exportable: project_with_design_repo, shared: shared) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.design_repo_bundle_filename) } let(:restorer) do described_class.new(path_to_bundle: bundle_path, shared: shared, - project: project) + importable: project) end before do 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 2575d209db5..5501e3dee5a 100644 --- a/spec/lib/gitlab/import_export/design_repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/design_repo_saver_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::ImportExport::DesignRepoSaver do let!(:project) { create(:project, :design_repo) } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { project.import_export_shared } - let(:design_bundler) { described_class.new(project: project, shared: shared) } + let(:design_bundler) { described_class.new(exportable: project, shared: shared) } before do project.add_maintainer(user) diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb index ef7394053b9..65c28a8b8a2 100644 --- a/spec/lib/gitlab/import_export/fork_spec.rb +++ b/spec/lib/gitlab/import_export/fork_spec.rb @@ -12,11 +12,11 @@ RSpec.describe 'forked project import' do let(:shared) { project.import_export_shared } let(:forked_from_project) { create(:project, :repository) } let(:forked_project) { fork_project(project_with_repo, nil, repository: true) } - let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } + let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(exportable: project_with_repo, shared: shared) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } let(:repo_restorer) do - Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: bundle_path, shared: shared, project: project) + Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: bundle_path, shared: shared, importable: project) end let!(:merge_request) do diff --git a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb index 2794acb8980..d2153221e8f 100644 --- a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb @@ -21,6 +21,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer do group_tree_restorer = described_class.new(user: user, shared: @shared, group: @group) expect(group_tree_restorer.restore).to be_truthy + expect(group_tree_restorer.groups_mapping).not_to be_empty end end diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb index 75db3167ebc..20f0f6af6f3 100644 --- a/spec/lib/gitlab/import_export/importer_spec.rb +++ b/spec/lib/gitlab/import_export/importer_spec.rb @@ -69,8 +69,8 @@ RSpec.describe Gitlab::ImportExport::Importer do repo_path = File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) restorer = double(Gitlab::ImportExport::RepoRestorer) - expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: repo_path, shared: shared, project: project).and_return(restorer) - expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: wiki_repo_path, shared: shared, project: ProjectWiki.new(project)).and_return(restorer) + expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: repo_path, shared: shared, importable: project).and_return(restorer) + expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: wiki_repo_path, shared: shared, importable: ProjectWiki.new(project)).and_return(restorer) expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).and_call_original expect(restorer).to receive(:restore).and_return(true).twice diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index a6b917457c2..fe43a23e242 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -27,10 +27,10 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do end describe 'bundle a project Git repo' do - let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } + let(:bundler) { Gitlab::ImportExport::RepoSaver.new(exportable: project_with_repo, shared: shared) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } - subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: project) } + subject { described_class.new(path_to_bundle: bundle_path, shared: shared, importable: project) } after do Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path) @@ -62,10 +62,10 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do end describe 'restore a wiki Git repo' do - let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_repo, shared: shared) } + let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(exportable: project_with_repo, shared: shared) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename) } - subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: ProjectWiki.new(project)) } + subject { described_class.new(path_to_bundle: bundle_path, shared: shared, importable: ProjectWiki.new(project)) } after do Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path) @@ -83,7 +83,7 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do describe 'no wiki in the bundle' do let!(:project_without_wiki) { create(:project) } - let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_without_wiki, shared: shared) } + let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(exportable: project_without_wiki, shared: shared) } it 'does not creates an empty wiki' do expect(subject.restore).to be true diff --git a/spec/lib/gitlab/import_export/repo_saver_spec.rb b/spec/lib/gitlab/import_export/repo_saver_spec.rb index 73d51000c67..52001e778d6 100644 --- a/spec/lib/gitlab/import_export/repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/repo_saver_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::ImportExport::RepoSaver do let!(:project) { create(:project, :repository) } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { project.import_export_shared } - let(:bundler) { described_class.new(project: project, shared: shared) } + let(:bundler) { described_class.new(exportable: project, shared: shared) } before do project.add_maintainer(user) @@ -25,6 +25,14 @@ RSpec.describe Gitlab::ImportExport::RepoSaver do expect(bundler.save).to be true end + it 'creates the directory for the repository' do + allow(bundler).to receive(:bundle_full_path).and_return('/foo/bar/file.tar.gz') + + expect(FileUtils).to receive(:mkdir_p).with('/foo/bar', anything) + + bundler.save # rubocop:disable Rails/SaveBang + end + context 'when the repo is empty' do let!(:project) { create(:project) } diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index a93ee051ccf..e301be47d68 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -220,6 +220,7 @@ MergeRequestDiff: - commits_count - files_count - sorted +- diff_type MergeRequestDiffCommit: - merge_request_diff_id - relative_order @@ -231,6 +232,7 @@ MergeRequestDiffCommit: - committer_name - committer_email - message +- trailers MergeRequestDiffFile: - merge_request_diff_id - relative_order @@ -255,6 +257,7 @@ MergeRequestContextCommit: - committer_email - message - merge_request_id +- trailers MergeRequestContextCommitDiffFile: - sha - relative_order @@ -580,6 +583,7 @@ ProjectFeature: - requirements_access_level - analytics_access_level - operations_access_level +- security_and_compliance_access_level - created_at - updated_at ProtectedBranch::MergeAccessLevel: diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb index 865c7e57b5a..877474dd862 100644 --- a/spec/lib/gitlab/import_export/saver_spec.rb +++ b/spec/lib/gitlab/import_export/saver_spec.rb @@ -6,7 +6,8 @@ require 'fileutils' RSpec.describe Gitlab::ImportExport::Saver do let!(:project) { create(:project, :public, name: 'project') } let(:base_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } - let(:export_path) { "#{base_path}/project_tree_saver_spec/export" } + let(:archive_path) { "#{base_path}/archive" } + let(:export_path) { "#{archive_path}/export" } let(:shared) { project.import_export_shared } subject { described_class.new(exportable: project, shared: shared) } @@ -35,10 +36,13 @@ RSpec.describe Gitlab::ImportExport::Saver do .to match(%r[\/uploads\/-\/system\/import_export_upload\/export_file.*]) end - it 'removes tmp files' do + it 'removes archive path and keeps base path untouched' do + allow(shared).to receive(:archive_path).and_return(archive_path) + subject.save - expect(FileUtils).to have_received(:rm_rf).with(base_path) - expect(Dir.exist?(base_path)).to eq(false) + expect(FileUtils).not_to have_received(:rm_rf).with(base_path) + expect(FileUtils).to have_received(:rm_rf).with(archive_path) + expect(Dir.exist?(archive_path)).to eq(false) end end 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 778d0859bf1..540f90e7804 100644 --- a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::ImportExport::WikiRepoSaver do 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(project: project, shared: shared) } + let(:wiki_bundler) { described_class.new(exportable: project, shared: shared) } let!(:project_wiki) { ProjectWiki.new(project, user) } before do diff --git a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb index 2ca7465e775..e4af3f77d5d 100644 --- a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb +++ b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb @@ -53,6 +53,7 @@ RSpec.describe Gitlab::Instrumentation::RedisClusterValidator do :del | [%w(foo bar)] | true # Arguments can be a nested array :del | %w(foo foo) | false :hset | %w(foo bar) | false # Not a multi-key command + :mget | [] | false # This is invalid, but not because it's a cross-slot command end with_them do diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index c00b0fdf043..48b76c4cdbf 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -37,7 +37,11 @@ RSpec.describe Gitlab::InstrumentationHelper do :redis_shared_state_write_bytes, :db_count, :db_write_count, - :db_cached_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) diff --git a/spec/lib/gitlab/kas_spec.rb b/spec/lib/gitlab/kas_spec.rb index ce22f36e9fd..01ced407883 100644 --- a/spec/lib/gitlab/kas_spec.rb +++ b/spec/lib/gitlab/kas_spec.rb @@ -58,4 +58,48 @@ RSpec.describe Gitlab::Kas do end end end + + describe '.included_in_gitlab_com_rollout?' do + let_it_be(:project) { create(:project) } + + context 'not GitLab.com' do + before do + allow(Gitlab).to receive(:com?).and_return(false) + end + + it 'returns true' do + expect(described_class.included_in_gitlab_com_rollout?(project)).to be_truthy + end + end + + context 'GitLab.com' do + before do + allow(Gitlab).to receive(:com?).and_return(true) + end + + context 'kubernetes_agent_on_gitlab_com feature flag disabled' do + before do + stub_feature_flags(kubernetes_agent_on_gitlab_com: false) + end + + it 'returns false' do + expect(described_class.included_in_gitlab_com_rollout?(project)).to be_falsey + end + end + + context 'kubernetes_agent_on_gitlab_com feature flag enabled' do + before do + stub_feature_flags(kubernetes_agent_on_gitlab_com: project) + end + + it 'returns true' do + expect(described_class.included_in_gitlab_com_rollout?(project)).to be_truthy + end + + it 'returns false for another project' do + expect(described_class.included_in_gitlab_com_rollout?(create(:project))).to be_falsey + end + end + 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 new file mode 100644 index 00000000000..5bcaf8fbc47 --- /dev/null +++ b/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::Subscribers::ExternalHttp, :request_store do + let(:transaction) { Gitlab::Metrics::Transaction.new } + let(:subscriber) { described_class.new } + + 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' + }) + 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' + }) + 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 + }) + end + + describe '.detail_store' do + context 'when external HTTP detail store is empty' do + before do + Gitlab::SafeRequestStore[:peek_enabled] = true + end + + it 'returns an empty array' do + expect(described_class.detail_store).to eql([]) + end + end + + context 'when the performance bar is not enabled' do + it 'returns an empty array' do + expect(described_class.detail_store).to eql([]) + end + end + + context 'when external HTTP detail store has some values' do + before do + Gitlab::SafeRequestStore[:peek_enabled] = true + Gitlab::SafeRequestStore[:external_http_detail_store] = [{ + method: 'POST', code: "200", duration: 0.321 + }] + end + + it 'returns the external http detailed store' do + expect(described_class.detail_store).to eql([{ method: 'POST', code: "200", duration: 0.321 }]) + end + end + end + + describe '.payload' do + context 'when SafeRequestStore does not have any item from external HTTP' do + it 'returns an empty array' do + expect(described_class.payload).to eql(external_http_count: 0, external_http_duration_s: 0.0) + end + end + + context 'when external HTTP recorded some values' do + before do + Gitlab::SafeRequestStore[:external_http_count] = 7 + Gitlab::SafeRequestStore[:external_http_duration_s] = 1.2 + end + + it 'returns the external http detailed store' do + expect(described_class.payload).to eql(external_http_count: 7, external_http_duration_s: 1.2) + end + end + end + + describe '#request' do + before do + Gitlab::SafeRequestStore[:peek_enabled] = true + allow(subscriber).to receive(:current_transaction).and_return(transaction) + end + + it 'tracks external HTTP request count' do + expect(transaction).to receive(:increment) + .with(:gitlab_external_http_total, 1, { code: "200", method: "POST" }) + expect(transaction).to receive(:increment) + .with(:gitlab_external_http_total, 1, { code: "301", method: "GET" }) + + subscriber.request(event_1) + subscriber.request(event_2) + end + + it 'tracks external HTTP duration' do + expect(transaction).to receive(:observe) + .with(:gitlab_external_http_duration_seconds, 0.321) + expect(transaction).to receive(:observe) + .with(:gitlab_external_http_duration_seconds, 0.12) + expect(transaction).to receive(:observe) + .with(:gitlab_external_http_duration_seconds, 5.3) + + subscriber.request(event_1) + subscriber.request(event_2) + subscriber.request(event_3) + end + + it 'tracks external HTTP exceptions' do + expect(transaction).to receive(:increment) + .with(:gitlab_external_http_total, 1, { code: 'undefined', method: "POST" }) + expect(transaction).to receive(:increment) + .with(:gitlab_external_http_exception_total, 1) + + subscriber.request(event_3) + end + + it 'stores per-request counters' do + subscriber.request(event_1) + subscriber.request(event_2) + subscriber.request(event_3) + + expect(Gitlab::SafeRequestStore[:external_http_count]).to eq(3) + expect(Gitlab::SafeRequestStore[:external_http_duration_s]).to eq(5.741) # 0.321 + 0.12 + 5.3 + end + + it 'stores a portion of events into the detail store' do + subscriber.request(event_1) + subscriber.request(event_2) + 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( + 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( + 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( + method: 'POST', duration: 5.3, + scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2/issues', + query: 'current=true', + exception_object: be_a(Net::ReadTimeout), + backtrace: be_a(Array) + ) + end + + context 'when the performance bar is not enabled' do + before do + Gitlab::SafeRequestStore.delete(:peek_enabled) + end + + it 'does not capture detail store' do + subscriber.request(event_1) + subscriber.request(event_2) + subscriber.request(event_3) + + expect(Gitlab::SafeRequestStore[:external_http_detail_store]).to be(nil) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb b/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb new file mode 100644 index 00000000000..2d595632772 --- /dev/null +++ b/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb @@ -0,0 +1,203 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do + let(:subscriber) { described_class.new } + + describe '.payload' do + context 'when the request store is empty' do + it 'returns empty data' do + expect(described_class.payload).to eql( + rack_attack_redis_count: 0, + rack_attack_redis_duration_s: 0.0 + ) + end + end + + context 'when the request store already has data' do + before do + Gitlab::SafeRequestStore[:rack_attack_instrumentation] = { + rack_attack_redis_count: 10, + rack_attack_redis_duration_s: 9.0 + } + end + + it 'returns the accumulated data' do + expect(described_class.payload).to eql( + rack_attack_redis_count: 10, + rack_attack_redis_duration_s: 9.0 + ) + end + end + end + + describe '#redis' do + it 'accumulates per-request RackAttack cache usage' do + freeze_time do + subscriber.redis( + ActiveSupport::Notifications::Event.new( + 'redis.rack_attack', Time.current, Time.current + 1.second, '1', { operation: 'fetch' } + ) + ) + subscriber.redis( + ActiveSupport::Notifications::Event.new( + 'redis.rack_attack', Time.current, Time.current + 2.seconds, '1', { operation: 'write' } + ) + ) + subscriber.redis( + ActiveSupport::Notifications::Event.new( + 'redis.rack_attack', Time.current, Time.current + 3.seconds, '1', { operation: 'read' } + ) + ) + end + + expect(Gitlab::SafeRequestStore[:rack_attack_instrumentation]).to eql( + rack_attack_redis_count: 3, + rack_attack_redis_duration_s: 6.0 + ) + end + end + + shared_examples 'log into auth logger' do + context 'when matched throttle does not require user information' do + let(:event) do + ActiveSupport::Notifications::Event.new( + event_name, Time.current, Time.current + 2.seconds, '1', request: double( + :request, + ip: '1.2.3.4', + request_method: 'GET', + fullpath: '/api/v4/internal/authorized_keys', + env: { + 'rack.attack.match_type' => match_type, + 'rack.attack.matched' => 'throttle_unauthenticated' + } + ) + ) + end + + it 'logs request information' do + expect(Gitlab::AuthLogger).to receive(:error).with( + include( + message: 'Rack_Attack', + env: match_type, + remote_ip: '1.2.3.4', + request_method: 'GET', + path: '/api/v4/internal/authorized_keys', + matched: 'throttle_unauthenticated' + ) + ) + subscriber.send(match_type, event) + end + end + + context 'when matched throttle requires user information' do + context 'when user not found' do + let(:event) do + ActiveSupport::Notifications::Event.new( + event_name, Time.current, Time.current + 2.seconds, '1', request: double( + :request, + ip: '1.2.3.4', + request_method: 'GET', + fullpath: '/api/v4/internal/authorized_keys', + env: { + 'rack.attack.match_type' => match_type, + 'rack.attack.matched' => 'throttle_authenticated_api', + 'rack.attack.match_discriminator' => 'not_exist_user_id' + } + ) + ) + end + + it 'logs request information and user id' do + expect(Gitlab::AuthLogger).to receive(:error).with( + include( + message: 'Rack_Attack', + env: match_type, + remote_ip: '1.2.3.4', + request_method: 'GET', + path: '/api/v4/internal/authorized_keys', + matched: 'throttle_authenticated_api', + user_id: 'not_exist_user_id' + ) + ) + subscriber.send(match_type, event) + end + end + + context 'when user found' do + let(:user) { create(:user) } + let(:event) do + ActiveSupport::Notifications::Event.new( + event_name, Time.current, Time.current + 2.seconds, '1', request: double( + :request, + ip: '1.2.3.4', + request_method: 'GET', + fullpath: '/api/v4/internal/authorized_keys', + env: { + 'rack.attack.match_type' => match_type, + 'rack.attack.matched' => 'throttle_authenticated_api', + 'rack.attack.match_discriminator' => user.id + } + ) + ) + end + + it 'logs request information and user meta' do + expect(Gitlab::AuthLogger).to receive(:error).with( + include( + message: 'Rack_Attack', + env: match_type, + remote_ip: '1.2.3.4', + request_method: 'GET', + path: '/api/v4/internal/authorized_keys', + matched: 'throttle_authenticated_api', + user_id: user.id, + 'meta.user' => user.username + ) + ) + subscriber.send(match_type, event) + end + end + end + end + + describe '#throttle' do + let(:match_type) { :throttle } + let(:event_name) { 'throttle.rack_attack' } + + it_behaves_like 'log into auth logger' + end + + describe '#blocklist' do + let(:match_type) { :blocklist } + let(:event_name) { 'blocklist.rack_attack' } + + it_behaves_like 'log into auth logger' + end + + describe '#track' do + let(:match_type) { :track } + let(:event_name) { 'track.rack_attack' } + + it_behaves_like 'log into auth logger' + end + + describe '#safelist' do + let(:event) do + ActiveSupport::Notifications::Event.new( + 'safelist.rack_attack', Time.current, Time.current + 2.seconds, '1', request: double( + :request, + env: { + 'rack.attack.matched' => 'throttle_unauthenticated' + } + ) + ) + end + + it 'adds the matched name to safe request store' do + subscriber.safelist(event) + expect(Gitlab::SafeRequestStore[:instrumentation_throttle_safelist]).to eql('throttle_unauthenticated') + end + end +end diff --git a/spec/lib/gitlab/patch/prependable_spec.rb b/spec/lib/gitlab/patch/prependable_spec.rb index 8feab57a8f3..5b01bb99fc8 100644 --- a/spec/lib/gitlab/patch/prependable_spec.rb +++ b/spec/lib/gitlab/patch/prependable_spec.rb @@ -231,4 +231,22 @@ RSpec.describe Gitlab::Patch::Prependable do .to raise_error(described_class::MultiplePrependedBlocks) end end + + describe 'the extra hack for override verification' do + context 'when ENV["STATIC_VERIFICATION"] is not defined' do + it 'does not extend ClassMethods onto the defining module' do + expect(ee).not_to respond_to(:class_name) + end + end + + context 'when ENV["STATIC_VERIFICATION"] is defined' do + before do + stub_env('STATIC_VERIFICATION', 'true') + end + + it 'does extend ClassMethods onto the defining module' do + expect(ee).to respond_to(:class_name) + end + end + end end diff --git a/spec/lib/gitlab/performance_bar/stats_spec.rb b/spec/lib/gitlab/performance_bar/stats_spec.rb index c34c6f7b31f..ad11eca56d1 100644 --- a/spec/lib/gitlab/performance_bar/stats_spec.rb +++ b/spec/lib/gitlab/performance_bar/stats_spec.rb @@ -22,10 +22,12 @@ RSpec.describe Gitlab::PerformanceBar::Stats do expect(logger).to receive(:info) .with({ duration_ms: 1.096, filename: 'lib/gitlab/pagination/offset_pagination.rb', - filenum: 53, method: 'add_pagination_headers', request_id: 'foo', type: :sql }) + method_path: 'lib/gitlab/pagination/offset_pagination.rb:add_pagination_headers', + count: 1, request_id: 'foo', type: :sql }) expect(logger).to receive(:info) - .with({ duration_ms: 0.817, filename: 'lib/api/helpers.rb', - filenum: 112, method: 'find_project', request_id: 'foo', type: :sql }).twice + .with({ duration_ms: 1.634, filename: 'lib/api/helpers.rb', + method_path: 'lib/api/helpers.rb:find_project', + count: 2, request_id: 'foo', type: :sql }) subject end diff --git a/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb b/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb new file mode 100644 index 00000000000..2cb31b00f39 --- /dev/null +++ b/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::RackAttack::InstrumentedCacheStore do + using RSpec::Parameterized::TableSyntax + + let(:store) { ::ActiveSupport::Cache::NullStore.new } + + subject { described_class.new(upstream_store: store)} + + where(:operation, :params, :test_proc) do + :fetch | [:key] | ->(s) { s.fetch(:key) } + :read | [:key] | ->(s) { s.read(:key) } + :read_multi | [:key_1, :key_2, :key_3] | ->(s) { s.read_multi(:key_1, :key_2, :key_3) } + :write_multi | [{ key_1: 1, key_2: 2, key_3: 3 }] | ->(s) { s.write_multi(key_1: 1, key_2: 2, key_3: 3) } + :fetch_multi | [:key_1, :key_2, :key_3] | ->(s) { s.fetch_multi(:key_1, :key_2, :key_3) {} } + :write | [:key, :value, { option_1: 1 }] | ->(s) { s.write(:key, :value, option_1: 1) } + :delete | [:key] | ->(s) { s.delete(:key) } + :exist? | [:key, { option_1: 1 }] | ->(s) { s.exist?(:key, option_1: 1) } + :delete_matched | [/^key$/, { option_1: 1 }] | ->(s) { s.delete_matched(/^key$/, option_1: 1 ) } + :increment | [:key, 1] | ->(s) { s.increment(:key, 1) } + :decrement | [:key, 1] | ->(s) { s.decrement(:key, 1) } + :cleanup | [] | ->(s) { s.cleanup } + :clear | [] | ->(s) { s.clear } + end + + with_them do + it 'publishes a notification' do + event = nil + + begin + subscriber = ActiveSupport::Notifications.subscribe("redis.rack_attack") do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + end + + test_proc.call(subject) + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + expect(event).not_to be_nil + expect(event.name).to eq("redis.rack_attack") + expect(event.duration).to be_a(Float).and(be > 0.0) + expect(event.payload[:operation]).to eql(operation) + end + + it 'publishes a notification even if the cache store returns an error' do + allow(store).to receive(operation).and_raise('Something went wrong') + + event = nil + exception = nil + + begin + subscriber = ActiveSupport::Notifications.subscribe("redis.rack_attack") do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + end + + begin + test_proc.call(subject) + rescue => e + exception = e + end + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + expect(event).not_to be_nil + expect(event.name).to eq("redis.rack_attack") + expect(event.duration).to be_a(Float).and(be > 0.0) + expect(event.payload[:operation]).to eql(operation) + + expect(exception).not_to be_nil + expect(exception.message).to eql('Something went wrong') + end + + it 'delegates to the upstream store' do + allow(store).to receive(operation).and_call_original + + if params.empty? + expect(store).to receive(operation).with(no_args) + else + expect(store).to receive(operation).with(*params) + end + + test_proc.call(subject) + end + end +end diff --git a/spec/lib/gitlab/rack_attack_spec.rb b/spec/lib/gitlab/rack_attack_spec.rb index 5748e1e49e5..788d2eac61f 100644 --- a/spec/lib/gitlab/rack_attack_spec.rb +++ b/spec/lib/gitlab/rack_attack_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Gitlab::RackAttack, :aggregate_failures do describe '.configure' do let(:fake_rack_attack) { class_double("Rack::Attack") } let(:fake_rack_attack_request) { class_double("Rack::Attack::Request") } + let(:fake_cache) { instance_double("Rack::Attack::Cache") } let(:throttles) do { @@ -27,6 +28,8 @@ RSpec.describe Gitlab::RackAttack, :aggregate_failures do allow(fake_rack_attack).to receive(:track) allow(fake_rack_attack).to receive(:safelist) allow(fake_rack_attack).to receive(:blocklist) + allow(fake_rack_attack).to receive(:cache).and_return(fake_cache) + allow(fake_cache).to receive(:store=) end it 'extends the request class' do diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb index 4c57665b41f..625dcf11546 100644 --- a/spec/lib/gitlab/repository_cache_adapter_spec.rb +++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb @@ -292,12 +292,11 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do describe '#expire_method_caches' do it 'expires the caches of the given methods' do - expect(cache).to receive(:expire).with(:rendered_readme) expect(cache).to receive(:expire).with(:branch_names) - expect(redis_set_cache).to receive(:expire).with(:rendered_readme, :branch_names) - expect(redis_hash_cache).to receive(:delete).with(:rendered_readme, :branch_names) + expect(redis_set_cache).to receive(:expire).with(:branch_names) + expect(redis_hash_cache).to receive(:delete).with(:branch_names) - repository.expire_method_caches(%i(rendered_readme branch_names)) + repository.expire_method_caches(%i(branch_names)) end it 'does not expire caches for non-existent methods' do diff --git a/spec/lib/gitlab/search/query_spec.rb b/spec/lib/gitlab/search/query_spec.rb index dd2f23a7e47..234b683ba1f 100644 --- a/spec/lib/gitlab/search/query_spec.rb +++ b/spec/lib/gitlab/search/query_spec.rb @@ -46,4 +46,22 @@ RSpec.describe Gitlab::Search::Query do expect(subject.filters).to all(include(negated: true)) end end + + context 'with filter value in quotes' do + let(:query) { '"foo bar" name:"my test script.txt"' } + + it 'does not break the filter value in quotes' do + expect(subject.term).to eq('"foo bar"') + expect(subject.filters[0]).to include(name: :name, negated: false, value: "MY TEST SCRIPT.TXT") + end + end + + context 'with extra white spaces between the query words' do + let(:query) { ' foo = bar name:"my test.txt"' } + + it 'removes the extra whitespace between tokens' do + expect(subject.term).to eq('foo = bar') + expect(subject.filters[0]).to include(name: :name, negated: false, value: "MY TEST.TXT") + end + end end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index c437b6bcceb..158d472f7ea 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::SearchResults do include ProjectForksHelper include SearchHelpers + using RSpec::Parameterized::TableSyntax let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, name: 'foo') } @@ -41,8 +42,6 @@ RSpec.describe Gitlab::SearchResults do end describe '#formatted_count' do - using RSpec::Parameterized::TableSyntax - where(:scope, :count_method, :expected) do 'projects' | :limited_projects_count | max_limited_count 'issues' | :limited_issues_count | max_limited_count @@ -61,8 +60,6 @@ RSpec.describe Gitlab::SearchResults do end describe '#highlight_map' do - using RSpec::Parameterized::TableSyntax - where(:scope, :expected) do 'projects' | {} 'issues' | {} @@ -80,8 +77,6 @@ RSpec.describe Gitlab::SearchResults do end describe '#formatted_limited_count' do - using RSpec::Parameterized::TableSyntax - where(:count, :expected) do 23 | '23' 99 | '99' diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index b99a5352717..856ae87c5bf 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -38,7 +38,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do 'pid' => Process.pid, 'created_at' => created_at.to_f, 'enqueued_at' => created_at.to_f, - 'scheduling_latency_s' => scheduling_latency_s + 'scheduling_latency_s' => scheduling_latency_s, + 'job_size_bytes' => be > 0 ) end diff --git a/spec/lib/gitlab/suggestions/commit_message_spec.rb b/spec/lib/gitlab/suggestions/commit_message_spec.rb index 1411f64f8b7..965960f0c3e 100644 --- a/spec/lib/gitlab/suggestions/commit_message_spec.rb +++ b/spec/lib/gitlab/suggestions/commit_message_spec.rb @@ -72,6 +72,17 @@ RSpec.describe Gitlab::Suggestions::CommitMessage do end end + context 'when a custom commit message is specified' do + let(:message) { "i'm a project message. a user's custom message takes precedence over me :(" } + let(:custom_message) { "hello there! i'm a cool custom commit message." } + + it 'shows the custom commit message' do + expect(Gitlab::Suggestions::CommitMessage + .new(user, suggestion_set, custom_message) + .message).to eq(custom_message) + end + end + context 'is specified and includes all placeholders' do let(:message) do '*** %{branch_name} %{files_count} %{file_paths} %{project_name} %{project_path} %{user_full_name} %{username} %{suggestions_count} ***' diff --git a/spec/lib/gitlab/template/finders/global_template_finder_spec.rb b/spec/lib/gitlab/template/finders/global_template_finder_spec.rb index e2751d194d3..38ec28c2b9a 100644 --- a/spec/lib/gitlab/template/finders/global_template_finder_spec.rb +++ b/spec/lib/gitlab/template/finders/global_template_finder_spec.rb @@ -15,9 +15,19 @@ RSpec.describe Gitlab::Template::Finders::GlobalTemplateFinder do FileUtils.rm_rf(base_dir) end - subject(:finder) { described_class.new(base_dir, '', { 'General' => '', 'Bar' => 'Bar' }, excluded_patterns: excluded_patterns) } + subject(:finder) do + described_class.new(base_dir, '', + { 'General' => '', 'Bar' => 'Bar' }, + include_categories_for_file, + excluded_patterns: excluded_patterns) + end let(:excluded_patterns) { [] } + let(:include_categories_for_file) do + { + "SAST" => { "Security" => "Security" } + } + end describe '.find' do context 'with a non-prefixed General template' do @@ -60,6 +70,7 @@ RSpec.describe Gitlab::Template::Finders::GlobalTemplateFinder do context 'with a prefixed template' do before do create_template!('Bar/test-template') + create_template!('Security/SAST') end it 'finds the template with a prefix' do @@ -76,6 +87,16 @@ RSpec.describe Gitlab::Template::Finders::GlobalTemplateFinder do expect { finder.find('../foo') }.to raise_error(/Invalid path/) end + context 'with include_categories_for_file being present' do + it 'finds the template with a prefix' do + expect(finder.find('SAST')).to be_present + end + + it 'does not find any template which is missing in include_categories_for_file' do + expect(finder.find('DAST')).to be_nil + end + end + context 'while listed as an exclusion' do let(:excluded_patterns) { [%r{^Bar/test-template$}] } diff --git a/spec/lib/gitlab/terraform/state_migration_helper_spec.rb b/spec/lib/gitlab/terraform/state_migration_helper_spec.rb new file mode 100644 index 00000000000..36c9c060e98 --- /dev/null +++ b/spec/lib/gitlab/terraform/state_migration_helper_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Terraform::StateMigrationHelper do + before do + stub_terraform_state_object_storage + end + + describe '.migrate_to_remote_storage' do + let!(:local_version) { create(:terraform_state_version, file_store: Terraform::StateUploader::Store::LOCAL) } + + subject { described_class.migrate_to_remote_storage } + + it 'migrates remote files to remote storage' do + subject + + expect(local_version.reload.file_store).to eq(Terraform::StateUploader::Store::REMOTE) + end + end +end diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb index acf7aeb303a..b12a0310dac 100644 --- a/spec/lib/gitlab/tracking/standard_context_spec.rb +++ b/spec/lib/gitlab/tracking/standard_context_spec.rb @@ -9,11 +9,37 @@ RSpec.describe Gitlab::Tracking::StandardContext do let(:snowplow_context) { subject.to_context } describe '#to_context' do - context 'with no arguments' do - it 'creates a Snowplow context with no data' do - snowplow_context.to_json[:data].each do |_, v| - expect(v).to be_nil + context 'default fields' do + context 'environment' do + shared_examples 'contains environment' do |expected_environment| + it 'contains environment' do + expect(snowplow_context.to_json.dig(:data, :environment)).to eq(expected_environment) + end end + + context 'development or test' do + include_examples 'contains environment', 'development' + end + + context 'staging' do + before do + allow(Gitlab).to receive(:staging?).and_return(true) + end + + include_examples 'contains environment', 'staging' + end + + context 'production' do + before do + allow(Gitlab).to receive(:com_and_canary?).and_return(true) + end + + include_examples 'contains environment', 'production' + end + end + + it 'contains source' do + expect(snowplow_context.to_json.dig(:data, :source)).to eq(described_class::GITLAB_RAILS_SOURCE) end end @@ -28,8 +54,8 @@ RSpec.describe Gitlab::Tracking::StandardContext do context 'with namespace' do subject { described_class.new(namespace: namespace) } - it 'creates a Snowplow context using the given data' do - expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(namespace.id) + it 'creates a Snowplow context without namespace and project' do + expect(snowplow_context.to_json.dig(:data, :namespace_id)).to be_nil expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil end end @@ -37,18 +63,18 @@ RSpec.describe Gitlab::Tracking::StandardContext do context 'with project' do subject { described_class.new(project: project) } - it 'creates a Snowplow context using the given data' do - expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(project.namespace.id) - expect(snowplow_context.to_json.dig(:data, :project_id)).to eq(project.id) + it 'creates a Snowplow context without namespace and project' do + expect(snowplow_context.to_json.dig(:data, :namespace_id)).to be_nil + expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil end end context 'with project and namespace' do subject { described_class.new(namespace: namespace, project: project) } - it 'creates a Snowplow context using the given data' do - expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(namespace.id) - expect(snowplow_context.to_json.dig(:data, :project_id)).to eq(project.id) + it 'creates a Snowplow context without namespace and project' do + expect(snowplow_context.to_json.dig(:data, :namespace_id)).to be_nil + expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil end end end diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index 686382dc262..fa01d4e48df 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -302,36 +302,36 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do it 'does not block urls from private networks' do local_ips.each do |ip| stub_domain_resolv(fake_domain, ip) do - expect(described_class).not_to be_blocked_url("http://#{fake_domain}", url_blocker_attributes) + expect(described_class).not_to be_blocked_url("http://#{fake_domain}", **url_blocker_attributes) end - expect(described_class).not_to be_blocked_url("http://#{ip}", url_blocker_attributes) + expect(described_class).not_to be_blocked_url("http://#{ip}", **url_blocker_attributes) end end it 'allows localhost endpoints' do - expect(described_class).not_to be_blocked_url('http://0.0.0.0', url_blocker_attributes) - expect(described_class).not_to be_blocked_url('http://localhost', url_blocker_attributes) - expect(described_class).not_to be_blocked_url('http://127.0.0.1', url_blocker_attributes) + expect(described_class).not_to be_blocked_url('http://0.0.0.0', **url_blocker_attributes) + expect(described_class).not_to be_blocked_url('http://localhost', **url_blocker_attributes) + expect(described_class).not_to be_blocked_url('http://127.0.0.1', **url_blocker_attributes) end it 'allows loopback endpoints' do - expect(described_class).not_to be_blocked_url('http://127.0.0.2', url_blocker_attributes) + expect(described_class).not_to be_blocked_url('http://127.0.0.2', **url_blocker_attributes) end it 'allows IPv4 link-local endpoints' do - expect(described_class).not_to be_blocked_url('http://169.254.169.254', url_blocker_attributes) - expect(described_class).not_to be_blocked_url('http://169.254.168.100', url_blocker_attributes) + expect(described_class).not_to be_blocked_url('http://169.254.169.254', **url_blocker_attributes) + expect(described_class).not_to be_blocked_url('http://169.254.168.100', **url_blocker_attributes) end it 'allows IPv6 link-local endpoints' do - expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]', url_blocker_attributes) - expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.169.254]', url_blocker_attributes) - expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a9fe]', url_blocker_attributes) - expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]', url_blocker_attributes) - expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.168.100]', url_blocker_attributes) - expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a864]', url_blocker_attributes) - expect(described_class).not_to be_blocked_url('http://[fe80::c800:eff:fe74:8]', url_blocker_attributes) + expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]', **url_blocker_attributes) + expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.169.254]', **url_blocker_attributes) + expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a9fe]', **url_blocker_attributes) + expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]', **url_blocker_attributes) + expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.168.100]', **url_blocker_attributes) + expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a864]', **url_blocker_attributes) + expect(described_class).not_to be_blocked_url('http://[fe80::c800:eff:fe74:8]', **url_blocker_attributes) end end @@ -416,11 +416,11 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do attrs = url_blocker_attributes.merge(dns_rebind_protection: false) stub_domain_resolv('example.com', '192.168.1.2') do - expect(described_class).not_to be_blocked_url(url, attrs) + expect(described_class).not_to be_blocked_url(url, **attrs) end stub_domain_resolv('example.com', '192.168.1.3') do - expect(described_class).to be_blocked_url(url, attrs) + expect(described_class).to be_blocked_url(url, **attrs) end end end @@ -442,18 +442,18 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do stub_domain_resolv(domain, '192.168.1.1') do expect(described_class).not_to be_blocked_url("http://#{domain}", - url_blocker_attributes) + **url_blocker_attributes) end stub_domain_resolv(subdomain1, '192.168.1.1') do expect(described_class).not_to be_blocked_url("http://#{subdomain1}", - url_blocker_attributes) + **url_blocker_attributes) end # subdomain2 is not part of the allowlist so it should be blocked stub_domain_resolv(subdomain2, '192.168.1.1') do expect(described_class).to be_blocked_url("http://#{subdomain2}", - url_blocker_attributes) + **url_blocker_attributes) end end @@ -463,12 +463,12 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do stub_domain_resolv(unicode_domain, '192.168.1.1') do expect(described_class).not_to be_blocked_url("http://#{unicode_domain}", - url_blocker_attributes) + **url_blocker_attributes) end stub_domain_resolv(idna_encoded_domain, '192.168.1.1') do expect(described_class).not_to be_blocked_url("http://#{idna_encoded_domain}", - url_blocker_attributes) + **url_blocker_attributes) end end @@ -525,7 +525,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do it 'allows domain with port when resolved ip has port allowed' do stub_domain_resolv("www.resolve-domain.com", '127.0.0.1') do - expect(described_class).not_to be_blocked_url("http://www.resolve-domain.com:2000", url_blocker_attributes) + expect(described_class).not_to be_blocked_url("http://www.resolve-domain.com:2000", **url_blocker_attributes) end end end diff --git a/spec/lib/gitlab/url_blockers/url_allowlist_spec.rb b/spec/lib/gitlab/url_blockers/url_allowlist_spec.rb index d9e44e9b85c..4c4248b143e 100644 --- a/spec/lib/gitlab/url_blockers/url_allowlist_spec.rb +++ b/spec/lib/gitlab/url_blockers/url_allowlist_spec.rb @@ -37,19 +37,19 @@ RSpec.describe Gitlab::UrlBlockers::UrlAllowlist do let(:allowlist) { ['example.io:3000'] } it 'returns true if domain and ports present in allowlist' do - parsed_allowlist = [['example.io', { port: 3000 }]] + parsed_allowlist = [['example.io', 3000]] not_allowed = [ 'example.io', - ['example.io', { port: 3001 }] + ['example.io', 3001] ] aggregate_failures do - parsed_allowlist.each do |domain_and_port| - expect(described_class).to be_domain_allowed(*domain_and_port) + parsed_allowlist.each do |domain, port| + expect(described_class).to be_domain_allowed(domain, port: port) end - not_allowed.each do |domain_and_port| - expect(described_class).not_to be_domain_allowed(*domain_and_port) + not_allowed.each do |domain, port| + expect(described_class).not_to be_domain_allowed(domain, port: port) end end end @@ -139,23 +139,23 @@ RSpec.describe Gitlab::UrlBlockers::UrlAllowlist do it 'returns true if ip and ports present in allowlist' do parsed_allowlist = [ - ['127.0.0.9', { port: 3000 }], - ['[2001:db8:85a3:8d3:1319:8a2e:370:7348]', { port: 443 }] + ['127.0.0.9', 3000], + ['[2001:db8:85a3:8d3:1319:8a2e:370:7348]', 443] ] not_allowed = [ '127.0.0.9', - ['127.0.0.9', { port: 3001 }], + ['127.0.0.9', 3001], '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - ['[2001:db8:85a3:8d3:1319:8a2e:370:7348]', { port: 3001 }] + ['[2001:db8:85a3:8d3:1319:8a2e:370:7348]', 3001] ] aggregate_failures do - parsed_allowlist.each do |ip_and_port| - expect(described_class).to be_ip_allowed(*ip_and_port) + parsed_allowlist.each do |ip, port| + expect(described_class).to be_ip_allowed(ip, port: port) end - not_allowed.each do |ip_and_port| - expect(described_class).not_to be_ip_allowed(*ip_and_port) + not_allowed.each do |ip, port| + expect(described_class).not_to be_ip_allowed(ip, port: port) end end end diff --git a/spec/lib/gitlab/usage/docs/renderer_spec.rb b/spec/lib/gitlab/usage/docs/renderer_spec.rb new file mode 100644 index 00000000000..e62861cd677 --- /dev/null +++ b/spec/lib/gitlab/usage/docs/renderer_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Docs::Renderer do + describe 'contents' do + let(:dictionary_path) { Gitlab::Usage::Docs::Renderer::DICTIONARY_PATH } + let(:items) { Gitlab::Usage::MetricDefinition.definitions } + + it 'generates dictionary for given items' do + generated_dictionary = described_class.new(items).contents + generated_dictionary_keys = RDoc::Markdown + .parse(generated_dictionary) + .table_of_contents + .select { |metric_doc| metric_doc.level == 2 && !metric_doc.text.start_with?('info:') } + .map(&:text) + + expect(generated_dictionary_keys).to match_array(items.keys) + end + end +end diff --git a/spec/lib/gitlab/usage/docs/value_formatter_spec.rb b/spec/lib/gitlab/usage/docs/value_formatter_spec.rb new file mode 100644 index 00000000000..ceb00867c95 --- /dev/null +++ b/spec/lib/gitlab/usage/docs/value_formatter_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Docs::ValueFormatter do + describe '.format' do + using RSpec::Parameterized::TableSyntax + where(:key, :value, :expected_value) do + :group | 'growth::product intelligence' | '`growth::product intelligence`' + :data_source | 'redis' | 'Redis' + :data_source | 'ruby' | 'Ruby' + :introduced_by_url | 'http://test.com' | '[Introduced by](http://test.com)' + :tier | %w(gold premium) | 'gold, premium' + :distribution | %w(ce ee) | 'ce, ee' + :key_path | 'key.path' | '**key.path**' + :milestone | '13.4' | '13.4' + :status | 'data_available' | 'data_available' + end + + with_them do + subject { described_class.format(key, value) } + + it { is_expected.to eq(expected_value) } + end + end +end diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb index e101f837324..bc64bdfbf56 100644 --- a/spec/lib/gitlab/usage/metric_definition_spec.rb +++ b/spec/lib/gitlab/usage/metric_definition_spec.rb @@ -5,17 +5,13 @@ require 'spec_helper' RSpec.describe Gitlab::Usage::MetricDefinition do let(:attributes) do { - name: 'uuid', description: 'GitLab instance unique identifier', value_type: 'string', product_category: 'collection', stage: 'growth', status: 'data_available', default_generation: 'generation_1', - full_path: { - generation_1: 'uuid', - generation_2: 'license.uuid' - }, + key_path: 'uuid', group: 'group::product analytics', time_frame: 'none', data_source: 'database', @@ -44,12 +40,11 @@ RSpec.describe Gitlab::Usage::MetricDefinition do using RSpec::Parameterized::TableSyntax where(:attribute, :value) do - :name | nil :description | nil :value_type | nil :value_type | 'test' :status | nil - :default_generation | nil + :key_path | nil :group | nil :time_frame | nil :time_frame | '29d' @@ -70,6 +65,20 @@ RSpec.describe Gitlab::Usage::MetricDefinition do described_class.new(path, attributes).validate! end + + context 'with skip_validation' do + it 'raise exception if skip_validation: false' do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError)) + + described_class.new(path, attributes.merge( { skip_validation: false } )).validate! + end + + it 'does not raise exception if has skip_validation: true' do + expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception) + + described_class.new(path, attributes.merge( { skip_validation: true } )).validate! + end + end end end diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb index 40671d980d6..d4a789419a4 100644 --- a/spec/lib/gitlab/usage/metric_spec.rb +++ b/spec/lib/gitlab/usage/metric_spec.rb @@ -4,15 +4,15 @@ require 'spec_helper' RSpec.describe Gitlab::Usage::Metric do describe '#definition' do - it 'returns generation_1 metric definiton' do - expect(described_class.new(default_generation_path: 'uuid').definition).to be_an(Gitlab::Usage::MetricDefinition) + it 'returns key_path metric definiton' do + expect(described_class.new(key_path: 'uuid').definition).to be_an(Gitlab::Usage::MetricDefinition) end end describe '#unflatten_default_path' do using RSpec::Parameterized::TableSyntax - where(:default_generation_path, :value, :expected_hash) do + where(:key_path, :value, :expected_hash) do 'uuid' | nil | { uuid: nil } 'uuid' | '1111' | { uuid: '1111' } 'counts.issues' | nil | { counts: { issues: nil } } @@ -21,7 +21,7 @@ RSpec.describe Gitlab::Usage::Metric do end with_them do - subject { described_class.new(default_generation_path: default_generation_path, value: value).unflatten_default_path } + subject { described_class.new(key_path: key_path, value: value).unflatten_key_path } it { is_expected.to eq(expected_hash) } end diff --git a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb new file mode 100644 index 00000000000..a391872c030 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb @@ -0,0 +1,256 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redis_shared_state do + let(:entity1) { 'dfb9d2d2-f56c-4c77-8aeb-6cddc4a1f857' } + let(:entity2) { '1dd9afb2-a3ee-4de1-8ae3-a405579c8584' } + let(:entity3) { '34rfjuuy-ce56-sa35-ds34-dfer567dfrf2' } + let(:entity4) { '8b9a2671-2abf-4bec-a682-22f6a8f7bf31' } + + around do |example| + # We need to freeze to a reference time + # because visits are grouped by the week number in the year + # Without freezing the time, the test may behave inconsistently + # depending on which day of the week test is run. + # Monday 6th of June + reference_time = Time.utc(2020, 6, 1) + travel_to(reference_time) { example.run } + end + + context 'aggregated_metrics_data' do + let(:known_events) do + [ + { name: 'event1_slot', redis_slot: "slot", category: 'category1', aggregation: "weekly" }, + { name: 'event2_slot', redis_slot: "slot", category: 'category2', aggregation: "weekly" }, + { name: 'event3_slot', redis_slot: "slot", category: 'category3', aggregation: "weekly" }, + { name: 'event5_slot', redis_slot: "slot", category: 'category4', aggregation: "weekly" }, + { name: 'event4', category: 'category2', aggregation: "weekly" } + ].map(&:with_indifferent_access) + end + + before do + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:known_events).and_return(known_events) + end + + shared_examples 'aggregated_metrics_data' do + context 'no aggregated metrics is defined' do + it 'returns empty hash' do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics).and_return([]) + end + + expect(aggregated_metrics_data).to eq({}) + end + end + + context 'there are aggregated metrics defined' do + before do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics) + end + end + + context 'with AND operator' do + let(:aggregated_metrics) do + [ + { name: 'gmau_1', events: %w[event1_slot event2_slot], operator: "AND" }, + { name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot], operator: "AND" }, + { name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" }, + { name: 'gmau_4', events: %w[event4], operator: "AND" } + ].map(&:with_indifferent_access) + end + + it 'returns the number of unique events for all known events' do + results = { + 'gmau_1' => 3, + 'gmau_2' => 2, + 'gmau_3' => 1, + 'gmau_4' => 3 + } + + expect(aggregated_metrics_data).to eq(results) + end + end + + context 'with OR operator' do + let(:aggregated_metrics) do + [ + { name: 'gmau_1', events: %w[event3_slot event5_slot], operator: "OR" }, + { name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "OR" }, + { name: 'gmau_3', events: %w[event4], operator: "OR" } + ].map(&:with_indifferent_access) + end + + it 'returns the number of unique events for all known events' do + results = { + 'gmau_1' => 2, + 'gmau_2' => 3, + 'gmau_3' => 3 + } + + expect(aggregated_metrics_data).to eq(results) + end + end + + context 'hidden behind feature flag' do + let(:enabled_feature_flag) { 'test_ff_enabled' } + let(:disabled_feature_flag) { 'test_ff_disabled' } + let(:aggregated_metrics) do + [ + # represents stable aggregated metrics that has been fully released + { name: 'gmau_without_ff', events: %w[event3_slot event5_slot], operator: "OR" }, + # represents new aggregated metric that is under performance testing on gitlab.com + { name: 'gmau_enabled', events: %w[event4], operator: "AND", feature_flag: enabled_feature_flag }, + # represents aggregated metric that is under development and shouldn't be yet collected even on gitlab.com + { name: 'gmau_disabled', events: %w[event4], operator: "AND", feature_flag: disabled_feature_flag } + ].map(&:with_indifferent_access) + end + + it 'returns the number of unique events for all known events' do + skip_feature_flags_yaml_validation + stub_feature_flags(enabled_feature_flag => true, disabled_feature_flag => false) + + expect(aggregated_metrics_data).to eq('gmau_without_ff' => 2, 'gmau_enabled' => 3) + end + end + end + + context 'error handling' do + context 'development and test environment' do + it 'raises error when unknown aggregation operator is used' do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics) + .and_return([{ name: 'gmau_1', events: %w[event1_slot], operator: "SUM" }]) + end + + expect { aggregated_metrics_data }.to raise_error Gitlab::Usage::Metrics::Aggregates::UnknownAggregationOperator + end + + it 're raises Gitlab::UsageDataCounters::HLLRedisCounter::EventError' do + error = Gitlab::UsageDataCounters::HLLRedisCounter::EventError + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union).and_raise(error) + + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics) + .and_return([{ name: 'gmau_1', events: %w[event1_slot], operator: "OR" }]) + end + + expect { aggregated_metrics_data }.to raise_error error + end + end + + context 'production' do + before do + stub_rails_env('production') + end + + it 'rescues unknown aggregation operator error' do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics) + .and_return([{ name: 'gmau_1', events: %w[event1_slot], operator: "SUM" }]) + end + + expect(aggregated_metrics_data).to eq('gmau_1' => -1) + end + + it 'rescues Gitlab::UsageDataCounters::HLLRedisCounter::EventError' do + error = Gitlab::UsageDataCounters::HLLRedisCounter::EventError + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union).and_raise(error) + + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics) + .and_return([{ name: 'gmau_1', events: %w[event1_slot], operator: "OR" }]) + end + + expect(aggregated_metrics_data).to eq('gmau_1' => -1) + end + end + end + end + + describe '.aggregated_metrics_weekly_data' do + subject(:aggregated_metrics_data) { described_class.new.weekly_data } + + before do + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity2, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity3, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity2, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity3, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event3_slot', values: entity1, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event3_slot', values: entity2, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event5_slot', values: entity2, time: 3.days.ago) + + # events out of time scope + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity3, time: 8.days.ago) + + # events in different slots + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity2, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity4, time: 2.days.ago) + end + + it_behaves_like 'aggregated_metrics_data' + end + + describe '.aggregated_metrics_monthly_data' do + subject(:aggregated_metrics_data) { described_class.new.monthly_data } + + it_behaves_like 'aggregated_metrics_data' do + before do + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity2, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity3, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity2, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity3, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event3_slot', values: entity1, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event3_slot', values: entity2, time: 10.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event5_slot', values: entity2, time: 4.weeks.ago.advance(days: 1)) + + # events out of time scope + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event5_slot', values: entity1, time: 4.weeks.ago.advance(days: -1)) + + # events in different slots + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity2, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity4, time: 2.days.ago) + end + end + + context 'Redis calls' do + let(:aggregated_metrics) do + [ + { name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" } + ].map(&:with_indifferent_access) + end + + it 'caches intermediate operations' do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics) + end + + aggregated_metrics[0][:events].each do |event| + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union) + .with(event_names: event, start_date: 4.weeks.ago.to_date, end_date: Date.current) + .once + .and_return(0) + end + + 2.upto(4) do |subset_size| + aggregated_metrics[0][:events].combination(subset_size).each do |events| + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union) + .with(event_names: events, start_date: 4.weeks.ago.to_date, end_date: Date.current) + .once + .and_return(0) + end + end + + aggregated_metrics_data + end + 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 index c0deb2aa00c..6ca7039b3de 100644 --- a/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb @@ -17,7 +17,7 @@ RSpec.describe 'aggregated metrics' do Gitlab::UsageDataCounters::HLLRedisCounter.known_events end - Gitlab::UsageDataCounters::HLLRedisCounter.aggregated_metrics.tap do |aggregated_metrics| + Gitlab::Usage::Metrics::Aggregates::Aggregate.new.send(:aggregated_metrics).tap do |aggregated_metrics| it 'all events has unique name' do event_names = aggregated_metrics&.map { |event| event[:name] } @@ -37,7 +37,7 @@ RSpec.describe 'aggregated metrics' do end it "uses allowed aggregation operators" do - expect(Gitlab::UsageDataCounters::HLLRedisCounter::ALLOWED_METRICS_AGGREGATIONS).to include aggregate[:operator] + expect(Gitlab::Usage::Metrics::Aggregates::ALLOWED_METRICS_AGGREGATIONS).to include aggregate[:operator] end it "uses events from the same Redis slot" do diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index b8eddc0ca7f..f0b8ce6c2fb 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 @@ -39,7 +39,9 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'snippets', 'code_review', 'terraform', - 'ci_templates' + 'ci_templates', + 'quickactions', + 'pipeline_authoring' ) end end @@ -425,182 +427,59 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s end end - context 'aggregated_metrics_data' do + describe '.calculate_events_union' do + let(:time_range) { { start_date: 7.days.ago, end_date: DateTime.current } } let(:known_events) do [ { name: 'event1_slot', redis_slot: "slot", category: 'category1', aggregation: "weekly" }, { name: 'event2_slot', redis_slot: "slot", category: 'category2', aggregation: "weekly" }, { name: 'event3_slot', redis_slot: "slot", category: 'category3', aggregation: "weekly" }, - { name: 'event5_slot', redis_slot: "slot", category: 'category4', aggregation: "weekly" }, + { name: 'event5_slot', redis_slot: "slot", category: 'category4', aggregation: "daily" }, { name: 'event4', category: 'category2', aggregation: "weekly" } ].map(&:with_indifferent_access) end before do allow(described_class).to receive(:known_events).and_return(known_events) - end - - shared_examples 'aggregated_metrics_data' do - context 'no aggregated metrics is defined' do - it 'returns empty hash' do - allow(described_class).to receive(:aggregated_metrics).and_return([]) - expect(aggregated_metrics_data).to eq({}) - end - end - - context 'there are aggregated metrics defined' do - before do - allow(described_class).to receive(:aggregated_metrics).and_return(aggregated_metrics) - end - - context 'with AND operator' do - let(:aggregated_metrics) do - [ - { name: 'gmau_1', events: %w[event1_slot event2_slot], operator: "AND" }, - { name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot], operator: "AND" }, - { name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" }, - { name: 'gmau_4', events: %w[event4], operator: "AND" } - ].map(&:with_indifferent_access) - end - - it 'returns the number of unique events for all known events' do - results = { - 'gmau_1' => 3, - 'gmau_2' => 2, - 'gmau_3' => 1, - 'gmau_4' => 3 - } - - expect(aggregated_metrics_data).to eq(results) - end - end - - context 'with OR operator' do - let(:aggregated_metrics) do - [ - { name: 'gmau_1', events: %w[event3_slot event5_slot], operator: "OR" }, - { name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "OR" }, - { name: 'gmau_3', events: %w[event4], operator: "OR" } - ].map(&:with_indifferent_access) - end - - it 'returns the number of unique events for all known events' do - results = { - 'gmau_1' => 2, - 'gmau_2' => 3, - 'gmau_3' => 3 - } - - expect(aggregated_metrics_data).to eq(results) - end - end - - context 'hidden behind feature flag' do - let(:enabled_feature_flag) { 'test_ff_enabled' } - let(:disabled_feature_flag) { 'test_ff_disabled' } - let(:aggregated_metrics) do - [ - # represents stable aggregated metrics that has been fully released - { name: 'gmau_without_ff', events: %w[event3_slot event5_slot], operator: "OR" }, - # represents new aggregated metric that is under performance testing on gitlab.com - { name: 'gmau_enabled', events: %w[event4], operator: "AND", feature_flag: enabled_feature_flag }, - # represents aggregated metric that is under development and shouldn't be yet collected even on gitlab.com - { name: 'gmau_disabled', events: %w[event4], operator: "AND", feature_flag: disabled_feature_flag } - ].map(&:with_indifferent_access) - end - - it 'returns the number of unique events for all known events' do - skip_feature_flags_yaml_validation - stub_feature_flags(enabled_feature_flag => true, disabled_feature_flag => false) + described_class.track_event('event1_slot', values: entity1, time: 2.days.ago) + described_class.track_event('event1_slot', values: entity2, time: 2.days.ago) + described_class.track_event('event1_slot', values: entity3, time: 2.days.ago) + described_class.track_event('event2_slot', values: entity1, time: 2.days.ago) + described_class.track_event('event2_slot', values: entity2, time: 3.days.ago) + described_class.track_event('event2_slot', values: entity3, time: 3.days.ago) + described_class.track_event('event3_slot', values: entity1, time: 3.days.ago) + described_class.track_event('event3_slot', values: entity2, time: 3.days.ago) + described_class.track_event('event5_slot', values: entity2, time: 3.days.ago) + + # events out of time scope + described_class.track_event('event2_slot', values: entity4, time: 8.days.ago) - expect(aggregated_metrics_data).to eq('gmau_without_ff' => 2, 'gmau_enabled' => 3) - end - end - end + # events in different slots + described_class.track_event('event4', values: entity1, time: 2.days.ago) + described_class.track_event('event4', values: entity2, time: 2.days.ago) end - describe '.aggregated_metrics_weekly_data' do - subject(:aggregated_metrics_data) { described_class.aggregated_metrics_weekly_data } - - before do - described_class.track_event('event1_slot', values: entity1, time: 2.days.ago) - described_class.track_event('event1_slot', values: entity2, time: 2.days.ago) - described_class.track_event('event1_slot', values: entity3, time: 2.days.ago) - described_class.track_event('event2_slot', values: entity1, time: 2.days.ago) - described_class.track_event('event2_slot', values: entity2, time: 3.days.ago) - described_class.track_event('event2_slot', values: entity3, time: 3.days.ago) - described_class.track_event('event3_slot', values: entity1, time: 3.days.ago) - described_class.track_event('event3_slot', values: entity2, time: 3.days.ago) - described_class.track_event('event5_slot', values: entity2, time: 3.days.ago) - - # events out of time scope - described_class.track_event('event2_slot', values: entity3, time: 8.days.ago) - - # events in different slots - described_class.track_event('event4', values: entity1, time: 2.days.ago) - described_class.track_event('event4', values: entity2, time: 2.days.ago) - described_class.track_event('event4', values: entity4, time: 2.days.ago) - end - - it_behaves_like 'aggregated_metrics_data' + it 'calculates union of given events', :aggregate_failure do + expect(described_class.calculate_events_union(**time_range.merge(event_names: %w[event4]))).to eq 2 + expect(described_class.calculate_events_union(**time_range.merge(event_names: %w[event1_slot event2_slot event3_slot]))).to eq 3 end - describe '.aggregated_metrics_monthly_data' do - subject(:aggregated_metrics_data) { described_class.aggregated_metrics_monthly_data } - - it_behaves_like 'aggregated_metrics_data' do - before do - described_class.track_event('event1_slot', values: entity1, time: 2.days.ago) - described_class.track_event('event1_slot', values: entity2, time: 2.days.ago) - described_class.track_event('event1_slot', values: entity3, time: 2.days.ago) - described_class.track_event('event2_slot', values: entity1, time: 2.days.ago) - described_class.track_event('event2_slot', values: entity2, time: 3.days.ago) - described_class.track_event('event2_slot', values: entity3, time: 3.days.ago) - described_class.track_event('event3_slot', values: entity1, time: 3.days.ago) - described_class.track_event('event3_slot', values: entity2, time: 10.days.ago) - described_class.track_event('event5_slot', values: entity2, time: 4.weeks.ago.advance(days: 1)) - - # events out of time scope - described_class.track_event('event5_slot', values: entity1, time: 4.weeks.ago.advance(days: -1)) - - # events in different slots - described_class.track_event('event4', values: entity1, time: 2.days.ago) - described_class.track_event('event4', values: entity2, time: 2.days.ago) - described_class.track_event('event4', values: entity4, time: 2.days.ago) - end - end - - context 'Redis calls' do - let(:aggregated_metrics) do - [ - { name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" } - ].map(&:with_indifferent_access) - end - - let(:known_events) do - [ - { name: 'event1_slot', redis_slot: "slot", category: 'category1', aggregation: "weekly" }, - { name: 'event2_slot', redis_slot: "slot", category: 'category2', aggregation: "weekly" }, - { name: 'event3_slot', redis_slot: "slot", category: 'category3', aggregation: "weekly" }, - { name: 'event5_slot', redis_slot: "slot", category: 'category4', aggregation: "weekly" } - ].map(&:with_indifferent_access) - end - - it 'caches intermediate operations' do - allow(described_class).to receive(:known_events).and_return(known_events) - allow(described_class).to receive(:aggregated_metrics).and_return(aggregated_metrics) + it 'validates and raise exception if events has mismatched slot or aggregation', :aggregate_failure do + 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 + end - 4.downto(1) do |subset_size| - known_events.combination(subset_size).each do |events| - keys = described_class.send(:weekly_redis_keys, events: events, start_date: 4.weeks.ago.to_date, end_date: Date.current) - expect(Gitlab::Redis::HLL).to receive(:count).with(keys: keys).once.and_return(0) - end - end + describe '.weekly_time_range' do + it 'return hash with weekly time range boundaries' do + expect(described_class.weekly_time_range).to eq(start_date: 7.days.ago.to_date, end_date: Date.current) + end + end - subject - end - end + describe '.monthly_time_range' do + it 'return hash with monthly time range boundaries' do + expect(described_class.monthly_time_range).to eq(start_date: 4.weeks.ago.to_date, end_date: Date.current) end end end diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb index c7b208cfb31..509ba43ef32 100644 --- a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb @@ -73,6 +73,22 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl end end + describe '.track_resolve_thread_action' do + subject { described_class.track_resolve_thread_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_RESOLVE_THREAD_ACTION } + end + end + + describe '.track_unresolve_thread_action' do + subject { described_class.track_unresolve_thread_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_UNRESOLVE_THREAD_ACTION } + end + end + describe '.track_create_comment_action' do subject { described_class.track_create_comment_action(note: note) } @@ -148,4 +164,36 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl let(:action) { described_class::MR_PUBLISH_REVIEW_ACTION } end end + + describe '.track_add_suggestion_action' do + subject { described_class.track_add_suggestion_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_ADD_SUGGESTION_ACTION } + end + end + + describe '.track_apply_suggestion_action' do + subject { described_class.track_apply_suggestion_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_APPLY_SUGGESTION_ACTION } + end + end + + describe '.track_users_assigned_to_mr' do + subject { described_class.track_users_assigned_to_mr(users: [user]) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_ASSIGNED_USERS_ACTION } + end + end + + describe '.track_users_review_requested' do + subject { described_class.track_users_review_requested(users: [user]) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_REVIEW_REQUESTED_USERS_ACTION } + end + end end 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 new file mode 100644 index 00000000000..d4c423f57fe --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter, :clean_gitlab_redis_shared_state do + let(:user) { build(:user, id: 1) } + let(:note) { build(:note, author: user) } + let(:args) { nil } + + shared_examples_for 'a tracked quick action unique event' do + specify do + expect { 3.times { subject } } + .to change { + Gitlab::UsageDataCounters::HLLRedisCounter.unique_events( + event_names: action, + start_date: 2.weeks.ago, + end_date: 2.weeks.from_now + ) + } + .by(1) + end + end + + subject { described_class.track_unique_action(quickaction_name, args: args, user: user) } + + describe '.track_unique_action' do + let(:quickaction_name) { 'approve' } + + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_approve' } + end + end + + context 'tracking assigns' do + let(:quickaction_name) { 'assign' } + + context 'single assignee' do + let(:args) { '@one' } + + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_assign_single' } + end + end + + context 'multiple assignees' do + let(:args) { '@one @two' } + + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_assign_multiple' } + end + end + + context 'assigning "me"' do + let(:args) { 'me' } + + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_assign_self' } + end + end + + context 'assigning a reviewer' do + let(:quickaction_name) { 'assign_reviewer' } + + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_assign_reviewer' } + end + end + + context 'assigning a reviewer with request review alias' do + let(:quickaction_name) { 'request_review' } + + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_assign_reviewer' } + end + end + end + + context 'tracking copy_metadata' do + let(:quickaction_name) { 'copy_metadata' } + + context 'for issues' do + let(:args) { '#123' } + + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_copy_metadata_issue' } + end + end + + context 'for merge requests' do + let(:args) { '!123' } + + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_copy_metadata_merge_request' } + end + end + end + + context 'tracking spend' do + let(:quickaction_name) { 'spend' } + + 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' } + + context 'unassigning everyone' do + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_unassign_all' } + end + end + + context 'unassigning specific users' do + let(:args) { '@hello' } + + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_unassign_specific' } + end + end + end + + context 'tracking unlabel' do + context 'called as unlabel' do + let(:quickaction_name) { 'unlabel' } + + context 'removing all labels' do + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_unlabel_all' } + end + end + + context 'removing specific labels' do + let(:args) { '~wow' } + + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_unlabel_specific' } + end + end + end + + context 'called as remove_label' do + let(:quickaction_name) { 'remove_label' } + + it_behaves_like 'a tracked quick action unique event' do + let(:action) { 'i_quickactions_unlabel_all' } + end + end + end +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index fd02521622c..602f6640d72 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -228,11 +228,32 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do ) end - it 'includes imports usage data' do + it 'includes import gmau usage data' do for_defined_days_back do user = create(:user) + group = create(:group) + group.add_owner(user) + + create(:project, import_type: :github, creator_id: user.id) + create(:jira_import_state, :finished, project: create(:project, creator_id: user.id)) + create(:issue_csv_import, user: user) + create(:group_import_state, group: group, user: user) create(:bulk_import, user: user) + end + + expect(described_class.usage_activity_by_stage_manage({})).to include( + unique_users_all_imports: 10 + ) + + expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)).to include( + unique_users_all_imports: 5 + ) + end + + it 'includes imports usage data' do + for_defined_days_back do + user = create(:user) %w(gitlab_project gitlab github bitbucket bitbucket_server gitea git manifest fogbugz phabricator).each do |type| create(:project, import_type: type, creator_id: user.id) @@ -242,72 +263,113 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do create(:jira_import_state, :finished, project: jira_project) create(:issue_csv_import, user: user) + + group = create(:group) + group.add_owner(user) + create(:group_import_state, group: group, user: user) + + bulk_import = create(:bulk_import, user: user) + create(:bulk_import_entity, :group_entity, bulk_import: bulk_import) + create(:bulk_import_entity, :project_entity, bulk_import: bulk_import) end expect(described_class.usage_activity_by_stage_manage({})).to include( { bulk_imports: { - gitlab: 2 + gitlab_v1: 2, + gitlab: Gitlab::UsageData::DEPRECATED_VALUE }, - projects_imported: { - total: 2, - gitlab_project: 2, - gitlab: 2, - github: 2, + project_imports: { bitbucket: 2, bitbucket_server: 2, - gitea: 2, git: 2, + gitea: 2, + github: 2, + gitlab: 2, + gitlab_migration: 2, + gitlab_project: 2, manifest: 2 }, - issues_imported: { + issue_imports: { jira: 2, fogbugz: 2, phabricator: 2, csv: 2 - } + }, + group_imports: { + group_import: 2, + gitlab_migration: 2 + }, + projects_imported: { + total: Gitlab::UsageData::DEPRECATED_VALUE, + gitlab_project: Gitlab::UsageData::DEPRECATED_VALUE, + gitlab: Gitlab::UsageData::DEPRECATED_VALUE, + github: Gitlab::UsageData::DEPRECATED_VALUE, + bitbucket: Gitlab::UsageData::DEPRECATED_VALUE, + bitbucket_server: Gitlab::UsageData::DEPRECATED_VALUE, + gitea: Gitlab::UsageData::DEPRECATED_VALUE, + git: Gitlab::UsageData::DEPRECATED_VALUE, + manifest: Gitlab::UsageData::DEPRECATED_VALUE + }, + issues_imported: { + jira: Gitlab::UsageData::DEPRECATED_VALUE, + fogbugz: Gitlab::UsageData::DEPRECATED_VALUE, + phabricator: Gitlab::UsageData::DEPRECATED_VALUE, + csv: Gitlab::UsageData::DEPRECATED_VALUE + }, + groups_imported: Gitlab::UsageData::DEPRECATED_VALUE } ) expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)).to include( { bulk_imports: { - gitlab: 1 + gitlab_v1: 1, + gitlab: Gitlab::UsageData::DEPRECATED_VALUE }, - projects_imported: { - total: 1, - gitlab_project: 1, - gitlab: 1, - github: 1, + project_imports: { bitbucket: 1, bitbucket_server: 1, - gitea: 1, git: 1, + gitea: 1, + github: 1, + gitlab: 1, + gitlab_migration: 1, + gitlab_project: 1, manifest: 1 }, - issues_imported: { + issue_imports: { jira: 1, fogbugz: 1, phabricator: 1, csv: 1 - } + }, + group_imports: { + group_import: 1, + gitlab_migration: 1 + }, + projects_imported: { + total: Gitlab::UsageData::DEPRECATED_VALUE, + gitlab_project: Gitlab::UsageData::DEPRECATED_VALUE, + gitlab: Gitlab::UsageData::DEPRECATED_VALUE, + github: Gitlab::UsageData::DEPRECATED_VALUE, + bitbucket: Gitlab::UsageData::DEPRECATED_VALUE, + bitbucket_server: Gitlab::UsageData::DEPRECATED_VALUE, + gitea: Gitlab::UsageData::DEPRECATED_VALUE, + git: Gitlab::UsageData::DEPRECATED_VALUE, + manifest: Gitlab::UsageData::DEPRECATED_VALUE + }, + issues_imported: { + jira: Gitlab::UsageData::DEPRECATED_VALUE, + fogbugz: Gitlab::UsageData::DEPRECATED_VALUE, + phabricator: Gitlab::UsageData::DEPRECATED_VALUE, + csv: Gitlab::UsageData::DEPRECATED_VALUE + }, + groups_imported: Gitlab::UsageData::DEPRECATED_VALUE + } ) end - it 'includes group imports usage data' do - for_defined_days_back do - user = create(:user) - group = create(:group) - group.add_owner(user) - create(:group_import_state, group: group, user: user) - end - - expect(described_class.usage_activity_by_stage_manage({})) - .to include(groups_imported: 2) - expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)) - .to include(groups_imported: 1) - end - def omniauth_providers [ OpenStruct.new(name: 'google_oauth2'), @@ -1262,7 +1324,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do subject { described_class.redis_hll_counters } let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories } - let(:ineligible_total_categories) { %w[source_code ci_secrets_management incident_management_alerts snippets terraform] } + let(:ineligible_total_categories) do + %w[source_code ci_secrets_management incident_management_alerts snippets terraform pipeline_authoring] + end it 'has all known_events' do expect(subject).to have_key(:redis_hll_counters) @@ -1286,8 +1350,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do describe '.aggregated_metrics_weekly' do subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_weekly } - it 'uses ::Gitlab::UsageDataCounters::HLLRedisCounter#aggregated_metrics_data', :aggregate_failures do - expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:aggregated_metrics_weekly_data).and_return(global_search_gmau: 123) + it 'uses ::Gitlab::Usage::Metrics::Aggregates::Aggregate#weekly_data', :aggregate_failures do + expect_next_instance_of(::Gitlab::Usage::Metrics::Aggregates::Aggregate) do |instance| + expect(instance).to receive(:weekly_data).and_return(global_search_gmau: 123) + end expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 }) end end @@ -1295,8 +1361,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do describe '.aggregated_metrics_monthly' do subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_monthly } - it 'uses ::Gitlab::UsageDataCounters::HLLRedisCounter#aggregated_metrics_data', :aggregate_failures do - expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:aggregated_metrics_monthly_data).and_return(global_search_gmau: 123) + it 'uses ::Gitlab::Usage::Metrics::Aggregates::Aggregate#monthly_data', :aggregate_failures do + expect_next_instance_of(::Gitlab::Usage::Metrics::Aggregates::Aggregate) do |instance| + expect(instance).to receive(:monthly_data).and_return(global_search_gmau: 123) + end expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 }) end end diff --git a/spec/lib/gitlab/utils/markdown_spec.rb b/spec/lib/gitlab/utils/markdown_spec.rb index 93d91f7ed90..acc5bd47c8c 100644 --- a/spec/lib/gitlab/utils/markdown_spec.rb +++ b/spec/lib/gitlab/utils/markdown_spec.rb @@ -53,33 +53,23 @@ RSpec.describe Gitlab::Utils::Markdown do end context 'when string has a product suffix' do - let(:string) { 'My Header (ULTIMATE)' } - - it 'ignores a product suffix' do - is_expected.to eq 'my-header' - end - - context 'with only modifier' do - let(:string) { 'My Header (STARTER ONLY)' } - - it 'ignores a product suffix' do - is_expected.to eq 'my-header' - end - end - - context 'with "*" around a product suffix' do - let(:string) { 'My Header **(STARTER)**' } - - it 'ignores a product suffix' do - is_expected.to eq 'my-header' - end - end - - context 'with "*" around a product suffix and only modifier' do - let(:string) { 'My Header **(STARTER ONLY)**' } - - it 'ignores a product suffix' do - is_expected.to eq 'my-header' + %w[CORE STARTER PREMIUM ULTIMATE FREE BRONZE SILVER GOLD].each do |tier| + ['', ' ONLY', ' SELF', ' SASS'].each do |modifier| + context "#{tier}#{modifier}" do + let(:string) { "My Header (#{tier}#{modifier})" } + + it 'ignores a product suffix' do + is_expected.to eq 'my-header' + end + + context 'with "*" around a product suffix' do + let(:string) { "My Header **(#{tier}#{modifier})**" } + + it 'ignores a product suffix' do + is_expected.to eq 'my-header' + end + end + end end end end diff --git a/spec/lib/gitlab/utils/override_spec.rb b/spec/lib/gitlab/utils/override_spec.rb index 7ba7392df0f..a5e53c1dfc1 100644 --- a/spec/lib/gitlab/utils/override_spec.rb +++ b/spec/lib/gitlab/utils/override_spec.rb @@ -2,6 +2,9 @@ require 'fast_spec_helper' +# Patching ActiveSupport::Concern +require_relative '../../../../config/initializers/0_as_concern' + RSpec.describe Gitlab::Utils::Override do let(:base) do Struct.new(:good) do @@ -164,6 +167,70 @@ RSpec.describe Gitlab::Utils::Override do it_behaves_like 'checking as intended, nothing was overridden' end + + context 'when ActiveSupport::Concern and class_methods are used' do + # We need to give module names before using Override + let(:base) { stub_const('Base', Module.new) } + let(:extension) { stub_const('Extension', Module.new) } + + def define_base(method_name:) + base.module_eval do + extend ActiveSupport::Concern + + class_methods do + define_method(method_name) do + :f + end + end + end + end + + def define_extension(method_name:) + extension.module_eval do + extend ActiveSupport::Concern + + class_methods do + extend Gitlab::Utils::Override + + override method_name + define_method(method_name) do + :g + end + end + end + end + + context 'when it is defining a overriding method' do + before do + define_base(method_name: :f) + define_extension(method_name: :f) + + base.prepend(extension) + end + + it 'verifies' do + expect(base.f).to eq(:g) + + described_class.verify! + end + end + + context 'when it is not defining a overriding method' do + before do + define_base(method_name: :f) + define_extension(method_name: :g) + + base.prepend(extension) + end + + it 'raises NotImplementedError' do + expect(base.f).to eq(:f) + + expect { described_class.verify! } + .to raise_error(NotImplementedError) + end + end + end end context 'when STATIC_VERIFICATION is not set' do diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb index dfc381d0ef2..27248d1d95a 100644 --- a/spec/lib/gitlab/utils/usage_data_spec.rb +++ b/spec/lib/gitlab/utils/usage_data_spec.rb @@ -58,6 +58,16 @@ RSpec.describe Gitlab::Utils::UsageData do expect(described_class.estimate_batch_distinct_count(relation, 'column')).to eq(5) end + it 'yield provided block with PostgresHll::Buckets' do + buckets = Gitlab::Database::PostgresHll::Buckets.new + + allow_next_instance_of(Gitlab::Database::PostgresHll::BatchDistinctCounter) do |instance| + allow(instance).to receive(:execute).and_return(buckets) + end + + expect { |block| described_class.estimate_batch_distinct_count(relation, 'column', &block) }.to yield_with_args(buckets) + end + context 'quasi integration test for different counting parameters' do # HyperLogLog http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf algorithm # used in estimate_batch_distinct_count produce probabilistic @@ -362,4 +372,97 @@ RSpec.describe Gitlab::Utils::UsageData do end end end + + describe '#save_aggregated_metrics', :clean_gitlab_redis_shared_state do + let(:timestamp) { Time.current.to_i } + let(:time_period) { { created_at: 7.days.ago..Date.current } } + let(:metric_name) { 'test_metric' } + let(:method_params) do + { + metric_name: metric_name, + time_period: time_period, + recorded_at_timestamp: timestamp, + data: data + } + end + + context 'with compatible data argument' do + let(:data) { ::Gitlab::Database::PostgresHll::Buckets.new(141 => 1, 56 => 1) } + + it 'persists serialized data in Redis' do + time_period_name = 'weekly' + + expect(described_class).to receive(:time_period_to_human_name).with(time_period).and_return(time_period_name) + Gitlab::Redis::SharedState.with do |redis| + expect(redis).to receive(:set).with("#{metric_name}_#{time_period_name}-#{timestamp}", '{"141":1,"56":1}', ex: 80.hours) + end + + described_class.save_aggregated_metrics(**method_params) + end + + context 'error handling' do + before do + allow(Gitlab::Redis::SharedState).to receive(:with).and_raise(::Redis::CommandError) + end + + it 'rescues and reraise ::Redis::CommandError for development and test environments' do + expect { described_class.save_aggregated_metrics(**method_params) }.to raise_error ::Redis::CommandError + end + + context 'for environment different than development' do + before do + stub_rails_env('production') + end + + it 'rescues ::Redis::CommandError' do + expect { described_class.save_aggregated_metrics(**method_params) }.not_to raise_error + end + end + end + end + + context 'with incompatible data argument' do + let(:data) { 1 } + + context 'for environment different than development' do + before do + stub_rails_env('production') + end + + it 'does not persist data in Redis' do + Gitlab::Redis::SharedState.with do |redis| + expect(redis).not_to receive(:set) + end + + described_class.save_aggregated_metrics(**method_params) + end + end + + it 'raises error for development environment' do + expect { described_class.save_aggregated_metrics(**method_params) }.to raise_error /Unsupported data type/ + end + end + end + + describe '#time_period_to_human_name' do + it 'translates empty time period as all_time' do + expect(described_class.time_period_to_human_name({})).to eql 'all_time' + end + + it 'translates time period not longer than 7 days as weekly', :aggregate_failures do + days_6_time_period = 6.days.ago..Date.current + days_7_time_period = 7.days.ago..Date.current + + expect(described_class.time_period_to_human_name(column_name: days_6_time_period)).to eql 'weekly' + expect(described_class.time_period_to_human_name(column_name: days_7_time_period)).to eql 'weekly' + end + + it 'translates time period longer than 7 days as monthly', :aggregate_failures do + days_8_time_period = 8.days.ago..Date.current + days_31_time_period = 31.days.ago..Date.current + + expect(described_class.time_period_to_human_name(column_name: days_8_time_period)).to eql 'monthly' + expect(described_class.time_period_to_human_name(column_name: days_31_time_period)).to eql 'monthly' + end + end end diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 1052d4cbacc..665eebdfd9e 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -116,8 +116,6 @@ RSpec.describe Gitlab::Utils do end describe '.ms_to_round_sec' do - using RSpec::Parameterized::TableSyntax - where(:original, :expected) do 1999.8999 | 1.9999 12384 | 12.384 @@ -169,8 +167,6 @@ RSpec.describe Gitlab::Utils do end describe '.remove_line_breaks' do - using RSpec::Parameterized::TableSyntax - where(:original, :expected) do "foo\nbar\nbaz" | "foobarbaz" "foo\r\nbar\r\nbaz" | "foobarbaz" @@ -281,8 +277,6 @@ RSpec.describe Gitlab::Utils do end describe '.append_path' do - using RSpec::Parameterized::TableSyntax - where(:host, :path, :result) do 'http://test/' | '/foo/bar' | 'http://test/foo/bar' 'http://test/' | '//foo/bar' | 'http://test/foo/bar' @@ -393,8 +387,6 @@ RSpec.describe Gitlab::Utils do end describe ".safe_downcase!" do - using RSpec::Parameterized::TableSyntax - where(:str, :result) do "test".freeze | "test" "Test".freeze | "test" diff --git a/spec/lib/gitlab_danger_spec.rb b/spec/lib/gitlab_danger_spec.rb deleted file mode 100644 index ed668c52a0e..00000000000 --- a/spec/lib/gitlab_danger_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe GitlabDanger do - let(:gitlab_danger_helper) { nil } - - subject { described_class.new(gitlab_danger_helper) } - - describe '.local_warning_message' do - it 'returns an informational message with rules that can run' do - expect(described_class.local_warning_message).to eq("==> Only the following Danger rules can be run locally: #{described_class::LOCAL_RULES.join(', ')}") - end - end - - describe '.success_message' do - it 'returns an informational success message' do - expect(described_class.success_message).to eq('==> No Danger rule violations!') - end - end - - describe '#rule_names' do - context 'when running locally' do - it 'returns local only rules' do - expect(subject.rule_names).to eq(described_class::LOCAL_RULES) - end - end - - context 'when running under CI' do - let(:gitlab_danger_helper) { double('danger_gitlab_helper') } - - it 'returns all rules' do - expect(subject.rule_names).to eq(described_class::LOCAL_RULES | described_class::CI_ONLY_RULES) - end - end - end - - describe '#html_link' do - context 'when running locally' do - it 'returns the same string' do - str = 'something' - - expect(subject.html_link(str)).to eq(str) - end - end - - context 'when running under CI' do - let(:gitlab_danger_helper) { double('danger_gitlab_helper') } - - it 'returns a HTML link formatted version of the string' do - str = 'something' - html_formatted_str = %Q{<a href="#{str}">#{str}</a>} - - expect(gitlab_danger_helper).to receive(:html_link).with(str).and_return(html_formatted_str) - - expect(subject.html_link(str)).to eq(html_formatted_str) - end - end - end - - describe '#ci?' do - context 'when gitlab_danger_helper is not available' do - it 'returns false' do - expect(subject.ci?).to be_falsey - end - end - - context 'when gitlab_danger_helper is available' do - let(:gitlab_danger_helper) { double('danger_gitlab_helper') } - - it 'returns true' do - expect(subject.ci?).to be_truthy - end - end - end -end diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index e1b8323eb8e..5f945d5b9fc 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -95,6 +95,26 @@ RSpec.describe Gitlab do end end + describe '.com' do + subject { described_class.com { true } } + + before do + allow(described_class).to receive(:com?).and_return(gl_com) + end + + context 'when on GitLab.com' do + let(:gl_com) { true } + + it { is_expected.to be true } + end + + context 'when not on GitLab.com' do + let(:gl_com) { false } + + it { is_expected.to be_nil } + end + end + describe '.staging?' do subject { described_class.staging? } @@ -332,13 +352,13 @@ RSpec.describe Gitlab do describe '.maintenance_mode?' do it 'returns true when maintenance mode is enabled' do - stub_application_setting(maintenance_mode: true) + stub_maintenance_mode_setting(true) expect(described_class.maintenance_mode?).to eq(true) end it 'returns false when maintenance mode is disabled' do - stub_application_setting(maintenance_mode: false) + stub_maintenance_mode_setting(false) expect(described_class.maintenance_mode?).to eq(false) end diff --git a/spec/lib/peek/views/external_http_spec.rb b/spec/lib/peek/views/external_http_spec.rb new file mode 100644 index 00000000000..cc1813db622 --- /dev/null +++ b/spec/lib/peek/views/external_http_spec.rb @@ -0,0 +1,191 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Peek::Views::ExternalHttp, :request_store do + subject { described_class.new } + + let(:subscriber) { Gitlab::Metrics::Subscribers::ExternalHttp.new } + + before do + allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true) + end + + let(:event_1) do + double(:event, payload: { + method: 'POST', code: "200", duration: 0.03, + scheme: 'https', host: 'gitlab.com', port: 80, path: '/api/v4/projects', + query: 'current=true' + }) + end + + let(:event_2) do + double(:event, payload: { + method: 'POST', duration: 1.3, + scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2/issues', + query: 'current=true', + exception_object: Net::ReadTimeout.new + }) + end + + let(:event_3) do + double(:event, payload: { + method: 'GET', code: "301", duration: 0.005, + scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2', + query: 'current=true', + proxy_host: 'proxy.gitlab.com', proxy_port: 8080 + }) + end + + it 'returns no results' do + expect(subject.results).to eq( + calls: 0, details: [], duration: "0ms", warnings: [] + ) + end + + it 'returns aggregated results' do + subscriber.request(event_1) + subscriber.request(event_2) + subscriber.request(event_3) + + results = subject.results + expect(results[:calls]).to eq(3) + expect(results[:duration]).to eq("1335.00ms") + expect(results[:details].count).to eq(3) + + expected = [ + { + duration: 30.0, + label: "POST https://gitlab.com:80/api/v4/projects?current=true", + code: "Response status: 200", + proxy: nil, + error: nil, + warnings: [] + }, + { + duration: 1300, + label: "POST http://gitlab.com:80/api/v4/projects/2/issues?current=true", + code: nil, + proxy: nil, + error: "Exception: Net::ReadTimeout", + warnings: ["1300.0 over 100"] + }, + { + duration: 5.0, + label: "GET http://gitlab.com:80/api/v4/projects/2?current=true", + code: "Response status: 301", + proxy: nil, + error: nil, + warnings: [] + } + ] + + expect( + results[:details].map { |data| data.slice(:duration, :label, :code, :proxy, :error, :warnings) } + ).to match_array(expected) + end + + context 'when the host is in IPv4 format' do + it 'displays IPv4 in the label' do + subscriber.request( + double(:event, payload: { + method: 'POST', code: "200", duration: 0.03, + scheme: 'https', host: '1.2.3.4', port: 80, path: '/api/v4/projects', + query: 'current=true' + }) + ) + expect( + subject.results[:details].map { |data| data.slice(:duration, :label, :code, :proxy, :error, :warnings) } + ).to match_array( + [ + { + duration: 30.0, + label: "POST https://1.2.3.4:80/api/v4/projects?current=true", + code: "Response status: 200", + proxy: nil, + error: nil, + warnings: [] + } + ] + ) + end + end + + context 'when the host is in IPv6 foramat' do + it 'displays IPv6 in the label' do + subscriber.request( + double(:event, payload: { + method: 'POST', code: "200", duration: 0.03, + scheme: 'https', host: '2606:4700:90:0:f22e:fbec:5bed:a9b9', port: 80, path: '/api/v4/projects', + query: 'current=true' + }) + ) + expect( + subject.results[:details].map { |data| data.slice(:duration, :label, :code, :proxy, :error, :warnings) } + ).to match_array( + [ + { + duration: 30.0, + label: "POST https://[2606:4700:90:0:f22e:fbec:5bed:a9b9]:80/api/v4/projects?current=true", + code: "Response status: 200", + proxy: nil, + error: nil, + warnings: [] + } + ] + ) + end + end + + context 'when the host is invalid' do + it 'displays unknown in the label' do + subscriber.request( + double(:event, payload: { + method: 'POST', code: "200", duration: 0.03, + scheme: 'https', host: '!@#%!@#%!@#%', port: 80, path: '/api/v4/projects', + query: 'current=true' + }) + ) + expect( + subject.results[:details].map { |data| data.slice(:duration, :label, :code, :proxy, :error, :warnings) } + ).to match_array( + [ + { + duration: 30.0, + label: "POST unknown", + code: "Response status: 200", + proxy: nil, + error: nil, + warnings: [] + } + ] + ) + end + end + + context 'when another URI component is invalid' do + it 'displays unknown in the label' do + subscriber.request( + double(:event, payload: { + method: 'POST', code: "200", duration: 0.03, + scheme: 'https', host: 'invalid', port: 'invalid', path: '/api/v4/projects', + query: 'current=true' + }) + ) + expect( + subject.results[:details].map { |data| data.slice(:duration, :label, :code, :proxy, :error, :warnings) } + ).to match_array( + [ + { + duration: 30.0, + label: "POST unknown", + code: "Response status: 200", + proxy: nil, + error: nil, + warnings: [] + } + ] + ) + end + end +end diff --git a/spec/lib/release_highlights/validator/entry_spec.rb b/spec/lib/release_highlights/validator/entry_spec.rb index 648356e63ba..da44938f165 100644 --- a/spec/lib/release_highlights/validator/entry_spec.rb +++ b/spec/lib/release_highlights/validator/entry_spec.rb @@ -80,7 +80,7 @@ RSpec.describe ReleaseHighlights::Validator::Entry do subject.valid? - expect(subject.errors[:packages].first).to include("must be one of", "Core", "Starter", "Premium", "Ultimate") + expect(subject.errors[:packages].first).to include("must be one of", "Free", "Premium", "Ultimate") end end end diff --git a/spec/lib/release_highlights/validator_spec.rb b/spec/lib/release_highlights/validator_spec.rb index e68d9145dcd..a423e8cc5f6 100644 --- a/spec/lib/release_highlights/validator_spec.rb +++ b/spec/lib/release_highlights/validator_spec.rb @@ -70,7 +70,7 @@ RSpec.describe ReleaseHighlights::Validator do --------------------------------------------------------- Validation failed for spec/fixtures/whats_new/invalid.yml --------------------------------------------------------- - * Packages must be one of ["Core", "Starter", "Premium", "Ultimate"] (line 6) + * Packages must be one of ["Free", "Premium", "Ultimate"] (line 6) MESSAGE end diff --git a/spec/lib/security/ci_configuration/sast_build_actions_spec.rb b/spec/lib/security/ci_configuration/sast_build_actions_spec.rb new file mode 100644 index 00000000000..c8f9430eff9 --- /dev/null +++ b/spec/lib/security/ci_configuration/sast_build_actions_spec.rb @@ -0,0 +1,539 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Security::CiConfiguration::SastBuildActions do + let(:default_sast_values) do + { 'global' => + [ + { 'field' => 'SECURE_ANALYZERS_PREFIX', 'defaultValue' => 'registry.gitlab.com/gitlab-org/security-products/analyzers', 'value' => 'registry.gitlab.com/gitlab-org/security-products/analyzers' } + ], + 'pipeline' => + [ + { 'field' => 'stage', 'defaultValue' => 'test', 'value' => 'test' }, + { 'field' => 'SEARCH_MAX_DEPTH', 'defaultValue' => 4, 'value' => 4 }, + { 'field' => 'SAST_ANALYZER_IMAGE_TAG', 'defaultValue' => 2, 'value' => 2 }, + { 'field' => 'SAST_EXCLUDED_PATHS', 'defaultValue' => 'spec, test, tests, tmp', 'value' => 'spec, test, tests, tmp' } + ] } + end + + let(:params) do + { 'global' => + [ + { 'field' => 'SECURE_ANALYZERS_PREFIX', 'defaultValue' => 'registry.gitlab.com/gitlab-org/security-products/analyzers', 'value' => 'new_registry' } + ], + 'pipeline' => + [ + { 'field' => 'stage', 'defaultValue' => 'test', 'value' => 'security' }, + { 'field' => 'SEARCH_MAX_DEPTH', 'defaultValue' => 4, 'value' => 1 }, + { 'field' => 'SAST_ANALYZER_IMAGE_TAG', 'defaultValue' => 2, 'value' => 2 }, + { 'field' => 'SAST_EXCLUDED_PATHS', 'defaultValue' => 'spec, test, tests, tmp', 'value' => 'spec,docs' } + ] } + end + + let(:params_with_analyzer_info) do + params.merge( { 'analyzers' => + [ + { + 'name' => "bandit", + 'enabled' => false + }, + { + 'name' => "brakeman", + 'enabled' => true, + 'variables' => [ + { 'field' => "SAST_BRAKEMAN_LEVEL", + 'defaultValue' => "1", + 'value' => "2" } + ] + }, + { + 'name' => "flawfinder", + 'enabled' => true, + 'variables' => [ + { 'field' => "SAST_FLAWFINDER_LEVEL", + 'defaultValue' => "1", + 'value' => "1" } + ] + } + ] } + ) + end + + let(:params_with_all_analyzers_enabled) do + params.merge( { 'analyzers' => + [ + { + 'name' => "flawfinder", + 'enabled' => true + }, + { + 'name' => "brakeman", + 'enabled' => true + } + ] } + ) + end + + context 'with existing .gitlab-ci.yml' do + let(:auto_devops_enabled) { false } + + context 'sast has not been included' do + context 'template includes are array' do + let(:gitlab_ci_content) { existing_gitlab_ci_and_template_array_without_sast } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') + expect(result.first[:content]).to eq(sast_yaml_two_includes) + end + end + + context 'template include is not an array' do + let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_without_sast } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') + expect(result.first[:content]).to eq(sast_yaml_two_includes) + end + + it 'reports defaults have been overwritten' do + expect(result.first[:default_values_overwritten]).to eq(true) + end + end + end + + context 'sast template include is not an array' do + let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') + expect(result.first[:content]).to eq(sast_yaml_all_params) + end + end + + context 'with default values' do + let(:params) { default_sast_values } + let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set) + end + + it 'reports defaults have not been overwritten' do + expect(result.first[:default_values_overwritten]).to eq(false) + end + + context 'analyzer section' do + let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage } + + subject(:result) { described_class.new(auto_devops_enabled, params_with_analyzer_info, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set_but_analyzers) + end + + context 'analyzers are disabled' do + let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage } + + subject(:result) { described_class.new(auto_devops_enabled, params_with_analyzer_info, gitlab_ci_content).generate } + + it 'writes SAST_EXCLUDED_ANALYZERS' do + stub_const('Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS', 'bandit, brakeman, flawfinder') + + expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set_but_analyzers) + end + end + + context 'all analyzers are enabled' do + let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage } + + subject(:result) { described_class.new(auto_devops_enabled, params_with_all_analyzers_enabled, gitlab_ci_content).generate } + + it 'does not write SAST_DEFAULT_ANALYZERS or SAST_EXCLUDED_ANALYZERS' do + stub_const('Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS', 'brakeman, flawfinder') + + expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set) + end + end + end + end + + context 'with update stage and SEARCH_MAX_DEPTH and set SECURE_ANALYZERS_PREFIX to default' do + let(:params) do + { 'global' => + [ + { 'field' => 'SECURE_ANALYZERS_PREFIX', 'defaultValue' => 'registry.gitlab.com/gitlab-org/security-products/analyzers', 'value' => 'registry.gitlab.com/gitlab-org/security-products/analyzers' } + ], + 'pipeline' => + [ + { 'field' => 'stage', 'defaultValue' => 'test', 'value' => 'brand_new_stage' }, + { 'field' => 'SEARCH_MAX_DEPTH', 'defaultValue' => 4, 'value' => 5 }, + { 'field' => 'SAST_ANALYZER_IMAGE_TAG', 'defaultValue' => 2, 'value' => 2 }, + { 'field' => 'SAST_EXCLUDED_PATHS', 'defaultValue' => 'spec, test, tests, tmp', 'value' => 'spec,docs' } + ] } + end + + let(:gitlab_ci_content) { existing_gitlab_ci } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') + expect(result.first[:content]).to eq(sast_yaml_updated_stage) + end + end + + context 'with no existing variables' do + let(:gitlab_ci_content) { existing_gitlab_ci_with_no_variables } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') + expect(result.first[:content]).to eq(sast_yaml_variable_section_added) + end + end + + context 'with no existing sast config' do + let(:gitlab_ci_content) { existing_gitlab_ci_with_no_sast_section } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') + expect(result.first[:content]).to eq(sast_yaml_sast_section_added) + end + end + + context 'with no existing sast variables' do + let(:gitlab_ci_content) { existing_gitlab_ci_with_no_sast_variables } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') + expect(result.first[:content]).to eq(sast_yaml_sast_variables_section_added) + end + end + + def existing_gitlab_ci_and_template_array_without_sast + { "stages" => %w(test security), + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, + "include" => [{ "template" => "existing.yml" }] } + end + + def existing_gitlab_ci_and_single_template_with_sast_and_default_stage + { "stages" => %w(test), + "variables" => { "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "test" }, + "include" => { "template" => "Security/SAST.gitlab-ci.yml" } } + end + + def existing_gitlab_ci_and_single_template_without_sast + { "stages" => %w(test security), + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, + "include" => { "template" => "existing.yml" } } + end + + def existing_gitlab_ci_with_no_variables + { "stages" => %w(test security), + "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, + "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } + end + + def existing_gitlab_ci_with_no_sast_section + { "stages" => %w(test security), + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } + end + + def existing_gitlab_ci_with_no_sast_variables + { "stages" => %w(test security), + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "sast" => { "stage" => "security" }, + "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } + end + + def existing_gitlab_ci + { "stages" => %w(test security), + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "bad_prefix" }, + "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, + "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } + end + end + + context 'with no .gitlab-ci.yml' do + let(:gitlab_ci_content) { nil } + + context 'autodevops disabled' do + let(:auto_devops_enabled) { false } + + context 'with one empty parameter' do + let(:params) do + { 'global' => + [ + { 'field' => 'SECURE_ANALYZERS_PREFIX', 'defaultValue' => 'registry.gitlab.com/gitlab-org/security-products/analyzers', 'value' => '' } + ] } + end + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set) + end + end + + context 'with all parameters' do + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:content]).to eq(sast_yaml_all_params) + end + end + end + + context 'with autodevops enabled' do + let(:auto_devops_enabled) { true } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + before do + allow_next_instance_of(described_class) do |sast_build_actions| + allow(sast_build_actions).to receive(:auto_devops_stages).and_return(fast_auto_devops_stages) + end + end + + it 'generates the correct YML' do + expect(result.first[:content]).to eq(auto_devops_with_custom_stage) + end + end + end + + describe 'Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS' do + subject(:variable) {Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS} + + it 'is sorted alphabetically' do + sorted_variable = Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS + .split(',') + .map(&:strip) + .sort + .join(', ') + + expect(variable).to eq(sorted_variable) + end + end + + # stubbing this method allows this spec file to use fast_spec_helper + def fast_auto_devops_stages + auto_devops_template = YAML.safe_load( File.read('lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml') ) + auto_devops_template['stages'] + end + + def sast_yaml_with_no_variables_set_but_analyzers + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - test + sast: + variables: + SAST_EXCLUDED_ANALYZERS: bandit + SAST_BRAKEMAN_LEVEL: '2' + stage: test + include: + - template: Security/SAST.gitlab-ci.yml + CI_YML + end + + def sast_yaml_with_no_variables_set + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - test + sast: + stage: test + include: + - template: Security/SAST.gitlab-ci.yml + CI_YML + end + + def sast_yaml_all_params + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - test + - security + variables: + SECURE_ANALYZERS_PREFIX: new_registry + sast: + variables: + SAST_EXCLUDED_PATHS: spec,docs + SEARCH_MAX_DEPTH: 1 + stage: security + include: + - template: Security/SAST.gitlab-ci.yml + CI_YML + end + + def auto_devops_with_custom_stage + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - build + - test + - deploy + - review + - dast + - staging + - canary + - production + - incremental rollout 10% + - incremental rollout 25% + - incremental rollout 50% + - incremental rollout 100% + - performance + - cleanup + - security + variables: + SECURE_ANALYZERS_PREFIX: new_registry + sast: + variables: + SAST_EXCLUDED_PATHS: spec,docs + SEARCH_MAX_DEPTH: 1 + stage: security + include: + - template: Auto-DevOps.gitlab-ci.yml + CI_YML + end + + def sast_yaml_two_includes + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - test + - security + variables: + RANDOM: make sure this persists + SECURE_ANALYZERS_PREFIX: new_registry + sast: + variables: + SAST_EXCLUDED_PATHS: spec,docs + SEARCH_MAX_DEPTH: 1 + stage: security + include: + - template: existing.yml + - template: Security/SAST.gitlab-ci.yml + CI_YML + end + + def sast_yaml_variable_section_added + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - test + - security + sast: + variables: + SAST_EXCLUDED_PATHS: spec,docs + SEARCH_MAX_DEPTH: 1 + stage: security + include: + - template: Security/SAST.gitlab-ci.yml + variables: + SECURE_ANALYZERS_PREFIX: new_registry + CI_YML + end + + def sast_yaml_sast_section_added + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - test + - security + variables: + RANDOM: make sure this persists + SECURE_ANALYZERS_PREFIX: new_registry + include: + - template: Security/SAST.gitlab-ci.yml + sast: + variables: + SAST_EXCLUDED_PATHS: spec,docs + SEARCH_MAX_DEPTH: 1 + stage: security + CI_YML + end + + def sast_yaml_sast_variables_section_added + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - test + - security + variables: + RANDOM: make sure this persists + SECURE_ANALYZERS_PREFIX: new_registry + sast: + stage: security + variables: + SAST_EXCLUDED_PATHS: spec,docs + SEARCH_MAX_DEPTH: 1 + include: + - template: Security/SAST.gitlab-ci.yml + CI_YML + end + + def sast_yaml_updated_stage + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - test + - security + - brand_new_stage + variables: + RANDOM: make sure this persists + sast: + variables: + SAST_EXCLUDED_PATHS: spec,docs + SEARCH_MAX_DEPTH: 5 + stage: brand_new_stage + include: + - template: Security/SAST.gitlab-ci.yml + CI_YML + end +end |