diff options
Diffstat (limited to 'spec/lib')
216 files changed, 8372 insertions, 4060 deletions
diff --git a/spec/lib/api/entities/merge_request_basic_spec.rb b/spec/lib/api/entities/merge_request_basic_spec.rb index fe4c27b70ae..8572b067984 100644 --- a/spec/lib/api/entities/merge_request_basic_spec.rb +++ b/spec/lib/api/entities/merge_request_basic_spec.rb @@ -7,7 +7,7 @@ RSpec.describe ::API::Entities::MergeRequestBasic do let_it_be(:project) { create(:project, :public) } let_it_be(:merge_request) { create(:merge_request) } let_it_be(:labels) { create_list(:label, 3) } - let_it_be(:merge_requests) { create_list(:labeled_merge_request, 10, :unique_branches, :with_diffs, labels: labels) } + let_it_be(:merge_requests) { create_list(:labeled_merge_request, 10, :unique_branches, labels: labels) } # This mimics the behavior of the `Grape::Entity` serializer def present(obj) @@ -42,29 +42,14 @@ RSpec.describe ::API::Entities::MergeRequestBasic do end context 'reviewers' do - context "when merge_request_reviewers FF is enabled" do - before do - stub_feature_flags(merge_request_reviewers: true) - merge_request.reviewers = [user] - end - - it 'includes assigned reviewers' do - result = Gitlab::Json.parse(present(merge_request).to_json) - - expect(result['reviewers'][0]['username']).to eq user.username - end + before do + merge_request.reviewers = [user] end - context "when merge_request_reviewers FF is disabled" do - before do - stub_feature_flags(merge_request_reviewers: false) - end - - it 'does not include reviewers' do - result = Gitlab::Json.parse(present(merge_request).to_json) + it 'includes assigned reviewers' do + result = Gitlab::Json.parse(present(merge_request).to_json) - expect(result.keys).not_to include('reviewers') - end + expect(result['reviewers'][0]['username']).to eq user.username end end end 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/api/support/git_access_actor_spec.rb b/spec/lib/api/support/git_access_actor_spec.rb index 143cc6e56ee..a09cabf4cd7 100644 --- a/spec/lib/api/support/git_access_actor_spec.rb +++ b/spec/lib/api/support/git_access_actor_spec.rb @@ -152,6 +152,10 @@ RSpec.describe API::Support::GitAccessActor do end describe '#update_last_used_at!' do + before do + stub_feature_flags(disable_ssh_key_used_tracking: false) + end + context 'when initialized with a User' do let(:user) { build(:user) } @@ -170,6 +174,14 @@ RSpec.describe API::Support::GitAccessActor do subject.update_last_used_at! end + + it 'does not update `last_used_at` when the functionality is disabled' do + stub_feature_flags(disable_ssh_key_used_tracking: true) + + expect(key).not_to receive(:update_last_used_at) + + subject.update_last_used_at! + end 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..92de191da2d 100644 --- a/spec/lib/backup/files_spec.rb +++ b/spec/lib/backup/files_spec.rb @@ -21,10 +21,6 @@ RSpec.describe Backup::Files do allow(File).to receive(:realpath).with("/var/gitlab-pages").and_return("/var/gitlab-pages") allow(File).to receive(:realpath).with("/var/gitlab-pages/..").and_return("/var") - allow_any_instance_of(String).to receive(:color) do |string, _color| - string - end - allow_any_instance_of(described_class).to receive(:progress).and_return(progress) end @@ -150,7 +146,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 +154,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 +164,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 +182,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/custom_emoji_filter_spec.rb b/spec/lib/banzai/filter/custom_emoji_filter_spec.rb new file mode 100644 index 00000000000..ca8c9750e7f --- /dev/null +++ b/spec/lib/banzai/filter/custom_emoji_filter_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::CustomEmojiFilter do + include FilterSpecHelper + + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:custom_emoji) { create(:custom_emoji, name: 'tanuki', group: group) } + let_it_be(:custom_emoji2) { create(:custom_emoji, name: 'happy_tanuki', group: group, file: 'https://foo.bar/happy.png') } + + it 'replaces supported name custom emoji' do + doc = filter('<p>:tanuki:</p>', project: project) + + expect(doc.css('gl-emoji').first.attributes['title'].value).to eq('tanuki') + expect(doc.css('gl-emoji img').size).to eq 1 + end + + it 'ignores non existent custom emoji' do + exp = act = '<p>:foo:</p>' + doc = filter(act) + + expect(doc.to_html).to match Regexp.escape(exp) + end + + it 'correctly uses the custom emoji URL' do + doc = filter('<p>:tanuki:</p>') + + expect(doc.css('img').first.attributes['src'].value).to eq(custom_emoji.file) + end + + it 'matches with adjacent text' do + doc = filter('tanuki (:tanuki:)') + + expect(doc.css('img').size).to eq 1 + end + + it 'matches multiple same custom emoji' do + doc = filter(':tanuki: :tanuki:') + + expect(doc.css('img').size).to eq 2 + end + + it 'matches multiple custom emoji' do + doc = filter(':tanuki: (:happy_tanuki:)') + + expect(doc.css('img').size).to eq 2 + end + + it 'does not match enclosed colons' do + doc = filter('tanuki:tanuki:') + + expect(doc.css('img').size).to be 0 + end + + it 'keeps whitespace intact' do + doc = filter('This deserves a :tanuki:, big time.') + + expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/) + end + + it 'does not match emoji in a string' do + doc = filter("'2a00:tanuki:100::1'") + + expect(doc.css('gl-emoji').size).to eq 0 + end + + it 'does not do N+1 query' do + create(:custom_emoji, name: 'party-parrot', group: group) + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + filter('<p>:tanuki:</p>') + end + + expect do + filter('<p>:tanuki: :party-parrot:</p>') + end.not_to exceed_all_query_limit(control_count.count) + end +end diff --git a/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb b/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb new file mode 100644 index 00000000000..2d7089853cf --- /dev/null +++ b/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::FeatureFlagReferenceFilter do + include FilterSpecHelper + + let_it_be(:project) { create(:project, :public) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) } + let_it_be(:reference) { feature_flag.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Feature Flag #{reference}</#{elem}>" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'with internal reference' do + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project, feature_flag) + end + + it 'links with adjacent text' do + doc = reference_filter("Feature Flag (#{reference}.)") + + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(reference)}</a>\.\)}) + end + + it 'ignores invalid feature flag IIDs' do + exp = act = "Check [feature_flag:#{non_existing_record_id}]" + + expect(reference_filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = reference_filter("Feature Flag #{reference}") + + expect(doc.css('a').first.attr('title')).to eq feature_flag.name + end + + it 'escapes the title attribute' do + allow(feature_flag).to receive(:name).and_return(%{"></a>whatever<a title="}) + doc = reference_filter("Feature Flag #{reference}") + + expect(doc.text).to eq "Feature Flag #{reference}" + end + + it 'includes default classes' do + doc = reference_filter("Feature Flag #{reference}") + + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-feature_flag has-tooltip' + end + + it 'includes a data-project attribute' do + doc = reference_filter("Feature Flag #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-feature-flag attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-feature-flag') + expect(link.attr('data-feature-flag')).to eq feature_flag.id.to_s + end + + it 'supports an :only_path context' do + doc = reference_filter("Feature Flag #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.edit_project_feature_flag_url(project, feature_flag.iid, only_path: true) + end + end + + context 'with cross-project / cross-namespace complete reference' do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project2) { create(:project, :public, namespace: namespace) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) } + let_it_be(:reference) { "[feature_flag:#{project2.full_path}/#{feature_flag.iid}]" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag) + end + + it 'produces a valid text in a link' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text).to eql(reference) + end + + it 'produces a valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{reference}.)") + end + + it 'ignores invalid feature flag IIDs on the referenced project' do + exp = act = "Check [feature_flag:#{non_existing_record_id}]" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'with cross-project / same-namespace complete reference' do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project) { create(:project, :public, namespace: namespace) } + let_it_be(:project2) { create(:project, :public, namespace: namespace) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) } + let_it_be(:reference) { "[feature_flag:#{project2.full_path}/#{feature_flag.iid}]" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag) + end + + it 'produces a valid text in a link' do + doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)") + + expect(doc.css('a').first.text).to eql("[feature_flag:#{project2.path}/#{feature_flag.iid}]") + end + + it 'produces a valid text' do + doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)") + + expect(doc.text).to eql("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)") + end + + it 'ignores invalid feature flag IIDs on the referenced project' do + exp = act = "Check [feature_flag:#{non_existing_record_id}]" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'with cross-project shorthand reference' do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project) { create(:project, :public, namespace: namespace) } + let_it_be(:project2) { create(:project, :public, namespace: namespace) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) } + let_it_be(:reference) { "[feature_flag:#{project2.path}/#{feature_flag.iid}]" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag) + end + + it 'produces a valid text in a link' do + doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)") + + expect(doc.css('a').first.text).to eql("[feature_flag:#{project2.path}/#{feature_flag.iid}]") + end + + it 'produces a valid text' do + doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)") + + expect(doc.text).to eql("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)") + end + + it 'ignores invalid feature flag IDs on the referenced project' do + exp = act = "Check [feature_flag:#{non_existing_record_id}]" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'with cross-project URL reference' do + let_it_be(:namespace) { create(:namespace, name: 'cross-reference') } + let_it_be(:project2) { create(:project, :public, namespace: namespace) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) } + let_it_be(:reference) { urls.edit_project_feature_flag_url(project2, feature_flag) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag) + end + + it 'links with adjacent text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(feature_flag.to_reference(project))}</a>\.\)}) + end + + it 'ignores invalid feature flag IIDs on the referenced project' do + act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>}) + end + end + + context 'with group context' do + let_it_be(:group) { create(:group) } + + it 'links to a valid reference' do + reference = "[feature_flag:#{project.full_path}/#{feature_flag.iid}]" + result = reference_filter("See #{reference}", { project: nil, group: group } ) + + expect(result.css('a').first.attr('href')).to eq(urls.edit_project_feature_flag_url(project, feature_flag)) + end + + it 'ignores internal references' do + exp = act = "See [feature_flag:#{feature_flag.iid}]" + + expect(reference_filter(act, project: nil, group: group).to_html).to eq exp + end + end +end diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index df78a3321ba..811c2aca342 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -216,7 +216,7 @@ RSpec.describe Banzai::Filter::MergeRequestReferenceFilter do end context 'URL reference for a commit' do - let(:mr) { create(:merge_request, :with_diffs) } + let(:mr) { create(:merge_request) } let(:reference) do urls.project_merge_request_url(mr.project, mr) + "/diffs?commit_id=#{mr.diff_head_sha}" end 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/broadcast_message_pipeline_spec.rb b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb index 41a91c56f3b..ad4256c2045 100644 --- a/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb @@ -3,11 +3,14 @@ require 'spec_helper' RSpec.describe Banzai::Pipeline::BroadcastMessagePipeline do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + before do stub_commonmark_sourcepos_disabled end - subject { described_class.to_html(exp, project: spy) } + subject { described_class.to_html(exp, project: project) } context "allows `a` elements" do let(:exp) { "<a>Link</a>" } 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/banzai/reference_parser/feature_flag_parser_spec.rb b/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb new file mode 100644 index 00000000000..288eb9ae360 --- /dev/null +++ b/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::ReferenceParser::FeatureFlagParser do + include ReferenceParserHelpers + + subject { described_class.new(Banzai::RenderContext.new(project, user)) } + + let(:link) { empty_html_link } + + describe '#nodes_visible_to_user' do + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + let(:feature_flag) { create(:operations_feature_flag, project: project) } + + context 'when the link has a data-issue attribute' do + before do + link['data-feature-flag'] = feature_flag.id.to_s + end + + it_behaves_like "referenced feature visibility", "issues", "merge_requests" do + before do + project.add_developer(user) if enable_user? + end + end + end + end + + describe '#referenced_by' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) } + + describe 'when the link has a data-feature-flag attribute' do + context 'using an existing feature flag ID' do + it 'returns an Array of feature flags' do + link['data-feature-flag'] = feature_flag.id.to_s + + expect(subject.referenced_by([link])).to eq([feature_flag]) + end + end + + context 'using a non-existing feature flag ID' do + it 'returns an empty Array' do + link['data-feature-flag'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + 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/award_emoji_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/award_emoji_transformer_spec.rb new file mode 100644 index 00000000000..5b560a30bf5 --- /dev/null +++ b/spec/lib/bulk_imports/common/transformers/award_emoji_transformer_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Common::Transformers::AwardEmojiTransformer do + describe '#transform' do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:bulk_import) { create(:bulk_import) } + let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) } + + let(:hash) do + { + 'name' => 'thumbs up', + 'user' => { + 'public_email' => email + } + } + end + + before do + group.add_developer(user) + end + + shared_examples 'sets user_id and removes user key' do + it 'sets found user_id and removes user key' do + transformed_hash = subject.transform(context, hash) + + expect(transformed_hash['user']).to be_nil + expect(transformed_hash['user_id']).to eq(user.id) + end + end + + context 'when user can be found by email' do + let(:email) { user.email } + + include_examples 'sets user_id and removes user key' + end + + context 'when user cannot be found by email' do + let(:user) { bulk_import.user } + let(:email) { nil } + + include_examples 'sets user_id and removes user key' + end + end +end 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..ef46da7062b --- /dev/null +++ b/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Graphql::GetGroupQuery do + describe '#variables' do + let(:entity) { double(source_full_path: 'test', bulk_import: nil) } + let(:context) { BulkImports::Pipeline::Context.new(entity) } + + it 'returns query variables based on entity information' do + expected = { full_path: entity.source_full_path } + + expect(described_class.variables(context)).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..247da200d68 --- /dev/null +++ b/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb @@ -0,0 +1,32 @@ +# 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', bulk_import: nil) } + let(:context) { BulkImports::Pipeline::Context.new(entity) } + + 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(context)).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/graphql/get_members_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb new file mode 100644 index 00000000000..5d05f5a2d30 --- /dev/null +++ b/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Graphql::GetMembersQuery do + it 'has a valid query' do + entity = create(:bulk_import_entity) + context = BulkImports::Pipeline::Context.new(entity) + + query = GraphQL::Query.new( + GitlabSchema, + described_class.to_s, + variables: described_class.variables(context) + ) + result = GitlabSchema.static_validator.validate(query) + + expect(result[:errors]).to be_empty + end + + describe '#data_path' do + it 'returns data path' do + expected = %w[data group group_members 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 group_members 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/loaders/members_loader_spec.rb b/spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb new file mode 100644 index 00000000000..d552578e7be --- /dev/null +++ b/spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Loaders::MembersLoader do + describe '#load' do + let_it_be(:user_importer) { create(:user) } + let_it_be(:user_member) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:bulk_import) { create(:bulk_import, user: user_importer) } + let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) } + + let_it_be(:data) do + { + 'user_id' => user_member.id, + 'created_by_id' => user_importer.id, + 'access_level' => 30, + 'created_at' => '2020-01-01T00:00:00Z', + 'updated_at' => '2020-01-01T00:00:00Z', + 'expires_at' => nil + } + end + + it 'does nothing when there is no data' do + expect { subject.load(context, nil) }.not_to change(GroupMember, :count) + end + + it 'creates the member' do + expect { subject.load(context, data) }.to change(GroupMember, :count).by(1) + + member = group.members.last + + expect(member.user).to eq(user_member) + expect(member.created_by).to eq(user_importer) + expect(member.access_level).to eq(30) + expect(member.created_at).to eq('2020-01-01T00:00:00Z') + expect(member.updated_at).to eq('2020-01-01T00:00:00Z') + expect(member.expires_at).to eq(nil) + 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..61950cdd9b0 100644 --- a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb @@ -6,41 +6,34 @@ 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 } + subject { described_class.new(context) } before do allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| @@ -53,20 +46,20 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do it 'imports new group into destination group' do group_path = 'my-destination-group' - subject.run(context) + subject.run imported_group = Group.find_by_path(group_path) 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 +80,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..63f28916d9a --- /dev/null +++ b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb @@ -0,0 +1,118 @@ +# 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) } + + subject { described_class.new(context) } + + 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 }.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) + + subject.after_run(data) + + tracker = entity.trackers.find_by(relation: :labels) + + expect(tracker.has_next_page).to eq(true) + expect(tracker.next_page).to eq(cursor) + end + end + + context 'when extracted data has no next page' do + it 'updates tracker information and does not run pipeline' do + data = extractor_data(title: 'label', has_next_page: false) + + expect(subject).not_to receive(:run) + + subject.after_run(data) + + tracker = entity.trackers.find_by(relation: :labels) + + expect(tracker.has_next_page).to eq(false) + expect(tracker.next_page).to be_nil + end + end + end + + describe '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/members_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb new file mode 100644 index 00000000000..9f498f8154f --- /dev/null +++ b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do + let_it_be(:member_user1) { create(:user, email: 'email1@email.com') } + let_it_be(:member_user2) { create(:user, email: 'email2@email.com') } + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:cursor) { 'cursor' } + let_it_be(:bulk_import) { create(:bulk_import, user: user) } + let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) } + + subject { described_class.new(context) } + + describe '#run' do + it 'maps existing users to the imported group' do + first_page = member_data(email: member_user1.email, has_next_page: true, cursor: cursor) + last_page = member_data(email: member_user2.email, has_next_page: false) + + 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 }.to change(GroupMember, :count).by(2) + + members = group.members.map { |m| m.slice(:user_id, :access_level) } + + expect(members).to contain_exactly( + { user_id: member_user1.id, access_level: 30 }, + { user_id: member_user2.id, access_level: 30 } + ) + 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::GetMembersQuery + } + ) + end + + it 'has transformers' do + expect(described_class.transformers) + .to contain_exactly( + { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil }, + { klass: BulkImports::Groups::Transformers::MemberAttributesTransformer, options: nil } + ) + end + + it 'has loaders' do + expect(described_class.get_loader).to eq(klass: BulkImports::Groups::Loaders::MembersLoader, options: nil) + end + end + + def member_data(email:, has_next_page:, cursor: nil) + data = { + 'created_at' => '2020-01-01T00:00:00Z', + 'updated_at' => '2020-01-01T00:00:00Z', + 'expires_at' => nil, + 'access_level' => { + 'integer_value' => 30 + }, + 'user' => { + 'public_email' => email + } + } + + page_info = { + 'end_cursor' => cursor, + 'has_next_page' => has_next_page + } + + BulkImports::Pipeline::ExtractedData.new(data: data, page_info: page_info) + end +end diff --git a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb index e5a8ed7f47d..0404c52b895 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 [ @@ -31,7 +25,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do ] end - subject { described_class.new } + subject { described_class.new(context) } before do allow_next_instance_of(BulkImports::Groups::Extractors::SubgroupsExtractor) do |extractor| @@ -42,7 +36,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do end it 'creates entities for the subgroups' do - expect { subject.run(context) }.to change(BulkImports::Entity, :count).by(1) + expect { subject.run }.to change(BulkImports::Entity, :count).by(1) subgroup_entity = BulkImports::Entity.last 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/groups/transformers/member_attributes_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb new file mode 100644 index 00000000000..f66c67fc6a2 --- /dev/null +++ b/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Transformers::MemberAttributesTransformer do + let_it_be(:user) { create(:user) } + let_it_be(:secondary_email) { 'secondary@email.com' } + let_it_be(:group) { create(:group) } + let_it_be(:bulk_import) { create(:bulk_import, user: user) } + let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) } + + it 'returns nil when receives no data' do + expect(subject.transform(context, nil)).to eq(nil) + end + + it 'returns nil when no user is found' do + expect(subject.transform(context, member_data)).to eq(nil) + expect(subject.transform(context, member_data(email: 'inexistent@email.com'))).to eq(nil) + end + + context 'when the user is not confirmed' do + before do + user.update!(confirmed_at: nil) + end + + it 'returns nil even when the primary email match' do + data = member_data(email: user.email) + + expect(subject.transform(context, data)).to eq(nil) + end + + it 'returns nil even when a secondary email match' do + user.emails << Email.new(email: secondary_email) + data = member_data(email: secondary_email) + + expect(subject.transform(context, data)).to eq(nil) + end + end + + context 'when the user is confirmed' do + before do + user.update!(confirmed_at: Time.now.utc) + end + + it 'finds the user by the primary email' do + data = member_data(email: user.email) + + expect(subject.transform(context, data)).to eq( + 'access_level' => 30, + 'user_id' => user.id, + 'created_by_id' => user.id, + 'created_at' => '2020-01-01T00:00:00Z', + 'updated_at' => '2020-01-01T00:00:00Z', + 'expires_at' => nil + ) + end + + it 'finds the user by the secondary email' do + user.emails << Email.new(email: secondary_email, confirmed_at: Time.now.utc) + data = member_data(email: secondary_email) + + expect(subject.transform(context, data)).to eq( + 'access_level' => 30, + 'user_id' => user.id, + 'created_by_id' => user.id, + 'created_at' => '2020-01-01T00:00:00Z', + 'updated_at' => '2020-01-01T00:00:00Z', + 'expires_at' => nil + ) + end + + context 'format access level' do + it 'ignores record if no access level is given' do + data = member_data(email: user.email, access_level: nil) + + expect(subject.transform(context, data)).to be_nil + end + + it 'ignores record if is not a valid access level' do + data = member_data(email: user.email, access_level: 999) + + expect(subject.transform(context, data)).to be_nil + end + end + end + + def member_data(email: '', access_level: 30) + { + 'created_at' => '2020-01-01T00:00:00Z', + 'updated_at' => '2020-01-01T00:00:00Z', + 'expires_at' => nil, + 'access_level' => { + 'integer_value' => access_level + }, + 'user' => { + 'public_email' => email + } + } + end +end diff --git a/spec/lib/bulk_imports/importers/group_importer_spec.rb b/spec/lib/bulk_imports/importers/group_importer_spec.rb index 87baf1b8026..b4fdb7b5e5b 100644 --- a/spec/lib/bulk_imports/importers/group_importer_spec.rb +++ b/spec/lib/bulk_imports/importers/group_importer_spec.rb @@ -4,28 +4,29 @@ require 'spec_helper' RSpec.describe BulkImports::Importers::GroupImporter do let(:user) { create(:user) } + let(:group) { create(:group) } let(:bulk_import) { create(:bulk_import) } - let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import) } + let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import, group: group) } let(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) } - let(:context) 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('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) if Gitlab.ee? expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context + expect_to_run_pipeline BulkImports::Groups::Pipelines::MembersPipeline, context: context + expect_to_run_pipeline BulkImports::Groups::Pipelines::LabelsPipeline, context: context + + if Gitlab.ee? + expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) + expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline'.constantize, context: context) + end subject.execute @@ -33,7 +34,7 @@ RSpec.describe BulkImports::Importers::GroupImporter do end context 'when failed' do - let(:bulk_import_entity) { create(:bulk_import_entity, :failed, bulk_import: bulk_import) } + let(:bulk_import_entity) { create(:bulk_import_entity, :failed, bulk_import: bulk_import, group: group) } it 'does not transition entity to finished state' do allow(bulk_import_entity).to receive(:start!) @@ -46,8 +47,8 @@ RSpec.describe BulkImports::Importers::GroupImporter do end def expect_to_run_pipeline(klass, context:) - expect_next_instance_of(klass) do |pipeline| - expect(pipeline).to receive(:run).with(context) + expect_next_instance_of(klass, context) do |pipeline| + expect(pipeline).to receive(:run) end end end diff --git a/spec/lib/bulk_imports/pipeline/context_spec.rb b/spec/lib/bulk_imports/pipeline/context_spec.rb index 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..76e4e64a7d6 100644 --- a/spec/lib/bulk_imports/pipeline/runner_spec.rb +++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb @@ -39,56 +39,94 @@ RSpec.describe BulkImports::Pipeline::Runner do extractor BulkImports::Extractor transformer BulkImports::Transformer loader BulkImports::Loader + + def after_run(_); end end stub_const('BulkImports::MyPipeline', pipeline) end 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: entity.id, + bulk_import_entity_type: 'group_entity', + pipeline_class: 'BulkImports::MyPipeline', + pipeline_step: :after_run + ) 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) + BulkImports::MyPipeline.new(context).run end 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| @@ -97,12 +135,13 @@ RSpec.describe BulkImports::Pipeline::Runner do end it 'logs import failure' do - BulkImports::MyPipeline.new.run(context) + BulkImports::MyPipeline.new(context).run failure = entity.failures.first 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 @@ -113,7 +152,7 @@ RSpec.describe BulkImports::Pipeline::Runner do end it 'marks entity as failed' do - BulkImports::MyPipeline.new.run(context) + BulkImports::MyPipeline.new(context).run expect(entity.failed?).to eq(true) end @@ -129,13 +168,13 @@ RSpec.describe BulkImports::Pipeline::Runner do ) end - BulkImports::MyPipeline.new.run(context) + BulkImports::MyPipeline.new(context).run end end context 'when pipeline is not marked to abort on failure' do it 'marks entity as failed' do - BulkImports::MyPipeline.new.run(context) + BulkImports::MyPipeline.new(context).run expect(entity.failed?).to eq(false) end @@ -144,25 +183,23 @@ 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 - BulkImports::MyPipeline.new.run(context) + BulkImports::MyPipeline.new(context).run end end end diff --git a/spec/lib/feature/gitaly_spec.rb b/spec/lib/feature/gitaly_spec.rb index a2181a63335..696427bb8b6 100644 --- a/spec/lib/feature/gitaly_spec.rb +++ b/spec/lib/feature/gitaly_spec.rb @@ -3,35 +3,78 @@ require 'spec_helper' RSpec.describe Feature::Gitaly do - let(:feature_flag) { "mep_mep" } + let_it_be(:project) { create(:project) } + let_it_be(:project_2) { create(:project) } + + before do + skip_feature_flags_yaml_validation + end describe ".enabled?" do - context 'when the gate is closed' do - before do - stub_feature_flags(gitaly_mep_mep: false) + context 'when the flag is set globally' do + let(:feature_flag) { 'global_flag' } + + context 'when the gate is closed' do + before do + stub_feature_flags(gitaly_global_flag: false) + end + + it 'returns false' do + expect(described_class.enabled?(feature_flag)).to be(false) + end end - it 'returns false' do - expect(described_class.enabled?(feature_flag)).to be(false) + context 'when the flag defaults to on' do + it 'returns true' do + expect(described_class.enabled?(feature_flag)).to be(true) + end end end - context 'when the flag defaults to on' do - it 'returns true' do - expect(described_class.enabled?(feature_flag)).to be(true) + context 'when the flag is enabled for a particular project' do + let(:feature_flag) { 'project_flag' } + + before do + stub_feature_flags(gitaly_project_flag: project) + end + + it 'returns true for that project' do + expect(described_class.enabled?(feature_flag, project)).to be(true) + end + + it 'returns false for any other project' do + expect(described_class.enabled?(feature_flag, project_2)).to be(false) + end + + it 'returns false when no project is passed' do + expect(described_class.enabled?(feature_flag)).to be(false) end end end describe ".server_feature_flags" do before do - stub_feature_flags(gitaly_mep_mep: true, foo: true) + stub_feature_flags(gitaly_global_flag: true, gitaly_project_flag: project, non_gitaly_flag: false) end subject { described_class.server_feature_flags } - it { is_expected.to be_a(Hash) } - it { is_expected.to eq("gitaly-feature-mep-mep" => "true") } + it 'returns a hash of flags starting with the prefix, with dashes instead of underscores' do + expect(subject).to eq('gitaly-feature-global-flag' => 'true', + 'gitaly-feature-project-flag' => 'false') + end + + context 'when a project is passed' do + it 'returns the value for the flag on the given project' do + expect(described_class.server_feature_flags(project)) + .to eq('gitaly-feature-global-flag' => 'true', + 'gitaly-feature-project-flag' => 'true') + + expect(described_class.server_feature_flags(project_2)) + .to eq('gitaly-feature-global-flag' => 'true', + 'gitaly-feature-project-flag' => 'false') + end + end context 'when table does not exist' do before do 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/alert_management/payload/generic_spec.rb b/spec/lib/gitlab/alert_management/payload/generic_spec.rb index b7660462b0d..d022c629458 100644 --- a/spec/lib/gitlab/alert_management/payload/generic_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/generic_spec.rb @@ -19,7 +19,34 @@ RSpec.describe Gitlab::AlertManagement::Payload::Generic do describe '#severity' do subject { parsed_payload.severity } - it_behaves_like 'parsable alert payload field with fallback', 'critical', 'severity' + context 'when set' do + using RSpec::Parameterized::TableSyntax + + let(:raw_payload) { { 'severity' => payload_severity } } + + where(:payload_severity, :expected_severity) do + 'critical' | :critical + 'high' | :high + 'medium' | :medium + 'low' | :low + 'info' | :info + + 'CRITICAL' | :critical + 'cRiTiCaL' | :critical + + 'unmapped' | nil + 1 | nil + nil | nil + end + + with_them do + it { is_expected.to eq(expected_severity) } + end + end + + context 'without key' do + it { is_expected.to be_nil } + end end describe '#monitoring_tool' do diff --git a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb index 457db58a28b..f574f5ba6a3 100644 --- a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb @@ -156,8 +156,6 @@ RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do end describe '#gitlab_fingerprint' do - subject { parsed_payload.gitlab_fingerprint } - let(:raw_payload) do { 'startsAt' => Time.current.to_s, @@ -166,6 +164,8 @@ RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do } end + subject { parsed_payload.gitlab_fingerprint } + it 'returns a fingerprint' do plain_fingerprint = [ parsed_payload.send(:starts_at_raw), @@ -237,4 +237,63 @@ RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do it { is_expected.to be_falsey } end end + + describe '#severity' do + subject { parsed_payload.severity } + + context 'when set' do + using RSpec::Parameterized::TableSyntax + + let(:raw_payload) { { 'labels' => { 'severity' => payload_severity } } } + + where(:payload_severity, :expected_severity) do + 'critical' | :critical + 'high' | :high + 'medium' | :medium + 'low' | :low + 'info' | :info + + 's1' | :critical + 's2' | :high + 's3' | :medium + 's4' | :low + 's5' | :info + 'p1' | :critical + 'p2' | :high + 'p3' | :medium + 'p4' | :low + 'p5' | :info + + 'CRITICAL' | :critical + 'cRiTiCaL' | :critical + 'S1' | :critical + + 'unmapped' | nil + 1 | nil + nil | nil + + 'debug' | :info + 'information' | :info + 'notice' | :info + 'warn' | :low + 'warning' | :low + 'minor' | :low + 'error' | :medium + 'major' | :high + 'emergency' | :critical + 'fatal' | :critical + + 'alert' | :medium + 'page' | :high + end + + with_them do + it { is_expected.to eq(expected_severity) } + end + end + + context 'without key' do + it { is_expected.to be_nil } + end + end end diff --git a/spec/lib/gitlab/alert_management/payload_spec.rb b/spec/lib/gitlab/alert_management/payload_spec.rb index 44b55e228c5..7c129a8a48e 100644 --- a/spec/lib/gitlab/alert_management/payload_spec.rb +++ b/spec/lib/gitlab/alert_management/payload_spec.rb @@ -56,5 +56,20 @@ RSpec.describe Gitlab::AlertManagement::Payload do it { is_expected.to be_a Gitlab::AlertManagement::Payload::Generic } end end + + context 'with integration specified by caller' do + let(:integration) { instance_double(AlertManagement::HttpIntegration) } + + subject { described_class.parse(project, payload, integration: integration) } + + it 'passes an integration to a specific payload' do + expect(::Gitlab::AlertManagement::Payload::Generic) + .to receive(:new) + .with(project: project, payload: payload, integration: integration) + .and_call_original + + subject + end + end end end diff --git a/spec/lib/gitlab/api_authentication/token_locator_spec.rb b/spec/lib/gitlab/api_authentication/token_locator_spec.rb index 68ce48a70ea..e933fd8352e 100644 --- a/spec/lib/gitlab/api_authentication/token_locator_spec.rb +++ b/spec/lib/gitlab/api_authentication/token_locator_spec.rb @@ -51,5 +51,26 @@ RSpec.describe Gitlab::APIAuthentication::TokenLocator do end end end + + context 'with :http_token' do + let(:type) { :http_token } + + context 'without credentials' do + let(:request) { double(headers: {}) } + + it 'returns nil' do + expect(subject).to be(nil) + end + end + + context 'with credentials' do + let(:password) { 'bar' } + let(:request) { double(headers: { "Authorization" => password }) } + + it 'returns the credentials' do + expect(subject.password).to eq(password) + end + end + end end end diff --git a/spec/lib/gitlab/api_authentication/token_resolver_spec.rb b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb index 0028fb080ac..97a7c8ba7cf 100644 --- a/spec/lib/gitlab/api_authentication/token_resolver_spec.rb +++ b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb @@ -47,8 +47,8 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do subject { resolver.resolve(raw) } - context 'with :personal_access_token' do - let(:type) { :personal_access_token } + context 'with :personal_access_token_with_username' do + let(:type) { :personal_access_token_with_username } let(:token) { personal_access_token } context 'with valid credentials' do @@ -62,10 +62,16 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do it_behaves_like 'an unauthorized request' end + + context 'with no username' do + let(:raw) { username_and_password(nil, token.token) } + + it_behaves_like 'an unauthorized request' + end end - context 'with :job_token' do - let(:type) { :job_token } + context 'with :job_token_with_username' do + let(:type) { :job_token_with_username } let(:token) { ci_job } context 'with valid credentials' do @@ -93,8 +99,8 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do end end - context 'with :deploy_token' do - let(:type) { :deploy_token } + context 'with :deploy_token_with_username' do + let(:type) { :deploy_token_with_username } let(:token) { deploy_token } context 'with a valid deploy token' do @@ -109,6 +115,51 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do it_behaves_like 'an unauthorized request' end end + + context 'with :personal_access_token' do + let(:type) { :personal_access_token } + let(:token) { personal_access_token } + + context 'with valid credentials' do + let(:raw) { username_and_password(nil, token.token) } + + it_behaves_like 'an authorized request' + end + end + + context 'with :job_token' do + let(:type) { :job_token } + let(:token) { ci_job } + + context 'with valid credentials' do + let(:raw) { username_and_password(nil, token.token) } + + it_behaves_like 'an authorized request' + end + + context 'when the job is not running' do + let(:raw) { username_and_password(nil, ci_job_done.token) } + + it_behaves_like 'an unauthorized request' + end + + context 'with an invalid job token' do + let(:raw) { username_and_password(nil, "not a valid CI job token") } + + it_behaves_like 'an unauthorized request' + end + end + + context 'with :deploy_token' do + let(:type) { :deploy_token } + let(:token) { deploy_token } + + context 'with a valid deploy token' do + let(:raw) { username_and_password(nil, token.token) } + + it_behaves_like 'an authorized request' + end + end end def username_and_password(username, password) diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 36e4decdead..08510d4652b 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -510,6 +510,73 @@ module Gitlab expect(render(input, context)).to include(output.strip) end + + it 'does not convert a blockdiag diagram to image' do + input = <<~ADOC + [blockdiag] + .... + blockdiag { + Kroki -> generates -> "Block diagrams"; + Kroki -> is -> "very easy!"; + + Kroki [color = "greenyellow"]; + "Block diagrams" [color = "pink"]; + "very easy!" [color = "orange"]; + } + .... + ADOC + + output = <<~HTML + <div> + <div> + <pre>blockdiag { + Kroki -> generates -> "Block diagrams"; + Kroki -> is -> "very easy!"; + + Kroki [color = "greenyellow"]; + "Block diagrams" [color = "pink"]; + "very easy!" [color = "orange"]; + }</pre> + </div> + </div> + HTML + + expect(render(input, context)).to include(output.strip) + end + end + + context 'with Kroki and BlockDiag (additional format) enabled' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true) + allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io') + allow_any_instance_of(ApplicationSetting).to receive(:kroki_formats_blockdiag).and_return(true) + end + + it 'converts a blockdiag diagram to image' do + input = <<~ADOC + [blockdiag] + .... + blockdiag { + Kroki -> generates -> "Block diagrams"; + Kroki -> is -> "very easy!"; + + Kroki [color = "greenyellow"]; + "Block diagrams" [color = "pink"]; + "very easy!" [color = "orange"]; + } + .... + ADOC + + output = <<~HTML + <div> + <div> + <a class="no-attachment-icon" href="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="></a> + </div> + </div> + HTML + + expect(render(input, context)).to include(output.strip) + end end end diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb index 775f8f056b5..cddcaf09b74 100644 --- a/spec/lib/gitlab/auth/auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/auth_finders_spec.rb @@ -7,7 +7,18 @@ RSpec.describe Gitlab::Auth::AuthFinders do include HttpBasicAuthHelpers # Create the feed_token and static_object_token for the user - let_it_be(:user) { create(:user).tap(&:feed_token).tap(&:static_object_token) } + let_it_be(:user, freeze: true) { create(:user).tap(&:feed_token).tap(&:static_object_token) } + let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user) } + + let_it_be(:project, freeze: true) { create(:project, :private) } + let_it_be(:pipeline, freeze: true) { create(:ci_pipeline, project: project) } + let_it_be(:job, freeze: true) { create(:ci_build, :running, pipeline: pipeline, user: user) } + let_it_be(:failed_job, freeze: true) { create(:ci_build, :failed, pipeline: pipeline, user: user) } + + let_it_be(:project2, freeze: true) { create(:project, :private) } + let_it_be(:pipeline2, freeze: true) { create(:ci_pipeline, project: project2) } + let_it_be(:job2, freeze: true) { create(:ci_build, :running, pipeline: pipeline2, user: user) } + let(:env) do { 'rack.input' => '' @@ -15,6 +26,12 @@ RSpec.describe Gitlab::Auth::AuthFinders do end let(:request) { ActionDispatch::Request.new(env) } + let(:params) { {} } + + before_all do + project.add_developer(user) + project2.add_developer(user) + end def set_param(key, value) request.update_param(key, value) @@ -28,75 +45,93 @@ RSpec.describe Gitlab::Auth::AuthFinders do env.merge!(basic_auth_header(username, password)) end - shared_examples 'find user from job token' do + def set_bearer_token(token) + env['HTTP_AUTHORIZATION'] = "Bearer #{token}" + end + + shared_examples 'find user from job token' do |without_job_token_allowed| context 'when route is allowed to be authenticated' do let(:route_authentication_setting) { { job_token_allowed: true } } - it "returns an Unauthorized exception for an invalid token" do - set_token('invalid token') + context 'for an invalid token' do + let(:token) { 'invalid token' } - expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError) + it "returns an Unauthorized exception" do + expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError) + expect(@current_authenticated_job).to be_nil + end end context 'with a running job' do - before do - job.update!(status: :running) - end - - it 'return user if token is valid' do - set_token(job.token) + let(:token) { job.token } + it 'return user' do expect(subject).to eq(user) expect(@current_authenticated_job).to eq job end end context 'with a job that is not running' do - before do - job.update!(status: :failed) - end + let(:token) { failed_job.token } it 'returns an Unauthorized exception' do - set_token(job.token) - expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError) + expect(@current_authenticated_job).to be_nil + end + end + end + + context 'when route is not allowed to be authenticated' do + let(:route_authentication_setting) { { job_token_allowed: false } } + + context 'with a running job' do + let(:token) { job.token } + + if without_job_token_allowed == :error + it 'returns an Unauthorized exception' do + expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError) + expect(@current_authenticated_job).to be_nil + end + elsif without_job_token_allowed == :user + it 'returns the user' do + expect(subject).to eq(user) + expect(@current_authenticated_job).to eq job + end + else + it 'returns nil' do + is_expected.to be_nil + expect(@current_authenticated_job).to be_nil + end end end end end describe '#find_user_from_bearer_token' do - let_it_be_with_reload(:job) { create(:ci_build, user: user) } - subject { find_user_from_bearer_token } context 'when the token is passed as an oauth token' do - def set_token(token) - env['HTTP_AUTHORIZATION'] = "Bearer #{token}" + before do + set_bearer_token(token) end - context 'with a job token' do - it_behaves_like 'find user from job token' - end + it_behaves_like 'find user from job token', :error + end - context 'with oauth token' do - let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) } - let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api').token } + context 'with oauth token' do + let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) } + let(:doorkeeper_access_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') } - before do - set_token(token) - end - - it { is_expected.to eq user } + before do + set_bearer_token(doorkeeper_access_token.token) end + + it { is_expected.to eq user } end context 'with a personal access token' do - let_it_be(:pat) { create(:personal_access_token, user: user) } - let(:token) { pat.token } - before do - env[described_class::PRIVATE_TOKEN_HEADER] = pat.token + env[described_class::PRIVATE_TOKEN_HEADER] = personal_access_token.token end it { is_expected.to eq user } @@ -277,7 +312,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end describe '#deploy_token_from_request' do - let_it_be(:deploy_token) { create(:deploy_token) } + let_it_be(:deploy_token, freeze: true) { create(:deploy_token) } let_it_be(:route_authentication_setting) { { deploy_token_allowed: true } } subject { deploy_token_from_request } @@ -293,11 +328,13 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'with deploy token headers' do - before do - set_header(described_class::DEPLOY_TOKEN_HEADER, deploy_token.token) - end + context 'with valid deploy token' do + before do + set_header(described_class::DEPLOY_TOKEN_HEADER, deploy_token.token) + end - it { is_expected.to eq deploy_token } + it { is_expected.to eq deploy_token } + end it_behaves_like 'an unauthenticated route' @@ -311,17 +348,19 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'with oauth headers' do - before do - set_header('HTTP_AUTHORIZATION', "Bearer #{deploy_token.token}") - end + context 'with valid token' do + before do + set_bearer_token(deploy_token.token) + end - it { is_expected.to eq deploy_token } + it { is_expected.to eq deploy_token } - it_behaves_like 'an unauthenticated route' + it_behaves_like 'an unauthenticated route' + end context 'with invalid token' do before do - set_header('HTTP_AUTHORIZATION', "Bearer invalid_token") + set_bearer_token('invalid_token') end it { is_expected.to be_nil } @@ -348,8 +387,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do end describe '#find_user_from_access_token' do - let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } - before do set_header('SCRIPT_NAME', 'url.atom') end @@ -374,24 +411,34 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'with OAuth headers' do - it 'returns user' do - set_header('HTTP_AUTHORIZATION', "Bearer #{personal_access_token.token}") + context 'with valid personal access token' do + before do + set_bearer_token(personal_access_token.token) + end - expect(find_user_from_access_token).to eq user + it 'returns user' do + expect(find_user_from_access_token).to eq user + end end - it 'returns exception if invalid personal_access_token' do - env['HTTP_AUTHORIZATION'] = 'Bearer invalid_20byte_token' + context 'with invalid personal_access_token' do + before do + set_bearer_token('invalid_20byte_token') + end - expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + it 'returns exception' do + expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end end context 'when using a non-prefixed access token' do - let_it_be(:personal_access_token) { create(:personal_access_token, :no_prefix, user: user) } + let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, :no_prefix, user: user) } - it 'returns user' do - set_header('HTTP_AUTHORIZATION', "Bearer #{personal_access_token.token}") + before do + set_bearer_token(personal_access_token.token) + end + it 'returns user' do expect(find_user_from_access_token).to eq user end end @@ -399,8 +446,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do end describe '#find_user_from_web_access_token' do - let_it_be_with_reload(:personal_access_token) { create(:personal_access_token, user: user) } - before do set_header(described_class::PRIVATE_TOKEN_HEADER, personal_access_token.token) end @@ -451,9 +496,9 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'when the token has read_api scope' do - before do - personal_access_token.update!(scopes: ['read_api']) + let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user, scopes: ['read_api']) } + before do set_header('SCRIPT_NAME', '/api/endpoint') end @@ -481,8 +526,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do end describe '#find_personal_access_token' do - let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } - before do set_header('SCRIPT_NAME', 'url.atom') end @@ -516,21 +559,23 @@ RSpec.describe Gitlab::Auth::AuthFinders do describe '#find_oauth_access_token' do let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) } - let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') } + let(:doorkeeper_access_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') } context 'passed as header' do - it 'returns token if valid oauth_access_token' do - set_header('HTTP_AUTHORIZATION', "Bearer #{token.token}") + before do + set_bearer_token(doorkeeper_access_token.token) + end - expect(find_oauth_access_token.token).to eq token.token + it 'returns token if valid oauth_access_token' do + expect(find_oauth_access_token.token).to eq doorkeeper_access_token.token end end context 'passed as param' do it 'returns user if valid oauth_access_token' do - set_param(:access_token, token.token) + set_param(:access_token, doorkeeper_access_token.token) - expect(find_oauth_access_token.token).to eq token.token + expect(find_oauth_access_token.token).to eq doorkeeper_access_token.token end end @@ -538,10 +583,14 @@ RSpec.describe Gitlab::Auth::AuthFinders do expect(find_oauth_access_token).to be_nil end - it 'returns exception if invalid oauth_access_token' do - set_header('HTTP_AUTHORIZATION', "Bearer invalid_token") + context 'with invalid token' do + before do + set_bearer_token('invalid_token') + end - expect { find_oauth_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + it 'returns exception if invalid oauth_access_token' do + expect { find_oauth_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end end end @@ -551,7 +600,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'access token is valid' do - let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let(:route_authentication_setting) { { basic_auth_personal_access_token: true } } it 'finds the token from basic auth' do @@ -572,8 +620,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'route_setting is not set' do - let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } - it 'returns nil' do auth_header_with(personal_access_token.token) @@ -582,7 +628,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'route_setting is not correct' do - let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let(:route_authentication_setting) { { basic_auth_personal_access_token: false } } it 'returns nil' do @@ -629,44 +674,18 @@ RSpec.describe Gitlab::Auth::AuthFinders do context 'with CI username' do let(:username) { ::Gitlab::Auth::CI_JOB_USER } - let_it_be(:user) { create(:user) } - let_it_be(:build) { create(:ci_build, user: user, status: :running) } - - it 'returns nil without password' do - set_basic_auth_header(username, nil) - - is_expected.to be_nil - end - - it 'returns user with valid token' do - set_basic_auth_header(username, build.token) - - is_expected.to eq user - expect(@current_authenticated_job).to eq build - end - - it 'raises error with invalid token' do - set_basic_auth_header(username, 'token') - - expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError) + before do + set_basic_auth_header(username, token) end - it 'returns exception if the job is not running' do - set_basic_auth_header(username, build.token) - build.success! - - expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError) - end + it_behaves_like 'find user from job token', :user end end describe '#validate_access_token!' do subject { validate_access_token! } - let_it_be_with_reload(:personal_access_token) { create(:personal_access_token, user: user) } - context 'with a job token' do - let_it_be(:job) { create(:ci_build, user: user, status: :running) } let(:route_authentication_setting) { { job_token_allowed: true } } before do @@ -684,6 +703,8 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'token is not valid' do + let_it_be_with_reload(:personal_access_token) { create(:personal_access_token, user: user) } + before do allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token) end @@ -706,7 +727,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end context 'with impersonation token' do - let_it_be(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) } + let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, :impersonation, user: user) } context 'when impersonation is disabled' do before do @@ -722,96 +743,30 @@ RSpec.describe Gitlab::Auth::AuthFinders do end describe '#find_user_from_job_token' do - let_it_be(:job) { create(:ci_build, user: user, status: :running) } - let(:route_authentication_setting) { { job_token_allowed: true } } - subject { find_user_from_job_token } - context 'when the job token is in the headers' do - it 'returns the user if valid job token' do - set_header(described_class::JOB_TOKEN_HEADER, job.token) - - is_expected.to eq(user) - expect(@current_authenticated_job).to eq(job) - end - - it 'returns nil without job token' do - set_header(described_class::JOB_TOKEN_HEADER, '') - - is_expected.to be_nil - end - - it 'returns exception if invalid job token' do - set_header(described_class::JOB_TOKEN_HEADER, 'invalid token') - - expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError) + context 'when the token is in the headers' do + before do + set_header(described_class::JOB_TOKEN_HEADER, token) end - it 'returns exception if the job is not running' do - set_header(described_class::JOB_TOKEN_HEADER, job.token) - job.success! + it_behaves_like 'find user from job token' + end - expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError) + context 'when the token is in the job_token param' do + before do + set_param(described_class::JOB_TOKEN_PARAM, token) end - context 'when route is not allowed to be authenticated' do - let(:route_authentication_setting) { { job_token_allowed: false } } - - it 'sets current_user to nil' do - set_header(described_class::JOB_TOKEN_HEADER, job.token) - - allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(true) - - is_expected.to be_nil - end - end + it_behaves_like 'find user from job token' end - context 'when the job token is in the params' do - shared_examples 'job token params' do |token_key_name| - before do - set_param(token_key_name, token) - end - - context 'with valid job token' do - let(:token) { job.token } - - it 'returns the user' do - is_expected.to eq(user) - expect(@current_authenticated_job).to eq(job) - end - end - - context 'with empty job token' do - let(:token) { '' } - - it 'returns nil' do - is_expected.to be_nil - end - end - - context 'with invalid job token' do - let(:token) { 'invalid token' } - - it 'returns exception' do - expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError) - end - end - - context 'when route is not allowed to be authenticated' do - let(:route_authentication_setting) { { job_token_allowed: false } } - let(:token) { job.token } - - it 'sets current_user to nil' do - allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(true) - - is_expected.to be_nil - end - end + context 'when the token is in the token param' do + before do + set_param(described_class::RUNNER_JOB_TOKEN_PARAM, token) end - it_behaves_like 'job token params', described_class::JOB_TOKEN_PARAM - it_behaves_like 'job token params', described_class::RUNNER_JOB_TOKEN_PARAM + it_behaves_like 'find user from job token' end context 'when the job token is provided via basic auth' do @@ -834,7 +789,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end describe '#cluster_agent_token_from_authorization_token' do - let_it_be(:agent_token) { create(:cluster_agent_token) } + let_it_be(:agent_token, freeze: true) { create(:cluster_agent_token) } context 'when route_setting is empty' do it 'returns nil' do @@ -884,7 +839,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do end describe '#find_runner_from_token' do - let_it_be(:runner) { create(:ci_runner) } + let_it_be(:runner, freeze: true) { create(:ci_runner) } context 'with API requests' do before do 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/otp/session_enforcer_spec.rb b/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb deleted file mode 100644 index 928aade4008..00000000000 --- a/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Auth::Otp::SessionEnforcer, :clean_gitlab_redis_shared_state do - let_it_be(:key) { create(:key)} - - describe '#update_session' do - it 'registers a session in Redis' do - redis = double(:redis) - expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis) - - expect(redis).to( - receive(:setex) - .with("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}", - described_class::DEFAULT_EXPIRATION, - true) - .once) - - described_class.new(key).update_session - end - end - - describe '#access_restricted?' do - subject { described_class.new(key).access_restricted? } - - context 'with existing session' do - before do - Gitlab::Redis::SharedState.with do |redis| - redis.set("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}", true ) - end - end - - it { is_expected.to be_falsey } - end - - context 'without an existing session' do - it { is_expected.to be_truthy } - end - end -end 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/backfill_project_updated_at_after_repository_storage_move_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb new file mode 100644 index 00000000000..708e5e21dbe --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillProjectUpdatedAtAfterRepositoryStorageMove, :migration, schema: 20210210093901 do + let(:projects) { table(:projects) } + let(:project_repository_storage_moves) { table(:project_repository_storage_moves) } + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + + subject { described_class.new } + + describe '#perform' do + it 'updates project updated_at column if they were moved to a different repository storage' do + freeze_time do + project_1 = projects.create!(id: 1, namespace_id: namespace.id, updated_at: 1.day.ago) + project_2 = projects.create!(id: 2, namespace_id: namespace.id, updated_at: Time.current) + original_project_3_updated_at = 2.minutes.from_now + project_3 = projects.create!(id: 3, namespace_id: namespace.id, updated_at: original_project_3_updated_at) + original_project_4_updated_at = 10.days.ago + project_4 = projects.create!(id: 4, namespace_id: namespace.id, updated_at: original_project_4_updated_at) + + repository_storage_move_1 = project_repository_storage_moves.create!(project_id: project_1.id, updated_at: 2.hours.ago, source_storage_name: 'default', destination_storage_name: 'default') + repository_storage_move_2 = project_repository_storage_moves.create!(project_id: project_2.id, updated_at: Time.current, source_storage_name: 'default', destination_storage_name: 'default') + project_repository_storage_moves.create!(project_id: project_3.id, updated_at: Time.current, source_storage_name: 'default', destination_storage_name: 'default') + + subject.perform([1, 2, 3, 4, non_existing_record_id]) + + expect(project_1.reload.updated_at).to eq(repository_storage_move_1.updated_at + 1.second) + expect(project_2.reload.updated_at).to eq(repository_storage_move_2.updated_at + 1.second) + expect(project_3.reload.updated_at).to eq(original_project_3_updated_at) + expect(project_4.reload.updated_at).to eq(original_project_4_updated_at) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb b/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb new file mode 100644 index 00000000000..f724b007e01 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::PopulateIssueEmailParticipants, schema: 20201128210234 do + let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') } + let!(:project) { table(:projects).create!(id: 1, namespace_id: namespace.id) } + let!(:issue1) { table(:issues).create!(id: 1, project_id: project.id, service_desk_reply_to: "a@gitlab.com") } + let!(:issue2) { table(:issues).create!(id: 2, project_id: project.id, service_desk_reply_to: "b@gitlab.com") } + let(:issue_email_participants) { table(:issue_email_participants) } + + describe '#perform' do + it 'migrates email addresses from service desk issues', :aggregate_failures do + expect { subject.perform(1, 2) }.to change { issue_email_participants.count }.by(2) + + expect(issue_email_participants.find_by(issue_id: 1).email).to eq("a@gitlab.com") + expect(issue_email_participants.find_by(issue_id: 2).email).to eq("b@gitlab.com") + end + 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/ast_spec.rb b/spec/lib/gitlab/changelog/ast_spec.rb new file mode 100644 index 00000000000..fa15ac979fe --- /dev/null +++ b/spec/lib/gitlab/changelog/ast_spec.rb @@ -0,0 +1,246 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Changelog::AST::Identifier do + let(:state) { Gitlab::Changelog::EvalState.new } + + describe '#evaluate' do + it 'evaluates a selector' do + data = { 'number' => 10 } + + expect(described_class.new('number').evaluate(state, data)).to eq(10) + end + + it 'returns nil if the key is not set' do + expect(described_class.new('number').evaluate(state, {})).to be_nil + end + + it 'returns nil if the input is not a Hash' do + expect(described_class.new('number').evaluate(state, 45)).to be_nil + end + + it 'returns the current data when using the special identifier "it"' do + expect(described_class.new('it').evaluate(state, 45)).to eq(45) + end + end +end + +RSpec.describe Gitlab::Changelog::AST::Integer do + let(:state) { Gitlab::Changelog::EvalState.new } + + describe '#evaluate' do + it 'evaluates a selector' do + expect(described_class.new(0).evaluate(state, [10])).to eq(10) + end + + it 'returns nil if the index is not set' do + expect(described_class.new(1).evaluate(state, [10])).to be_nil + end + + it 'returns nil if the input is not an Array' do + expect(described_class.new(0).evaluate(state, {})).to be_nil + end + end +end + +RSpec.describe Gitlab::Changelog::AST::Selector do + let(:state) { Gitlab::Changelog::EvalState.new } + let(:data) { { 'numbers' => [10] } } + + describe '#evaluate' do + it 'evaluates a selector' do + ident = Gitlab::Changelog::AST::Identifier.new('numbers') + int = Gitlab::Changelog::AST::Integer.new(0) + + expect(described_class.new([ident, int]).evaluate(state, data)).to eq(10) + end + + it 'evaluates a selector that returns nil' do + int = Gitlab::Changelog::AST::Integer.new(0) + + expect(described_class.new([int]).evaluate(state, data)).to be_nil + end + end +end + +RSpec.describe Gitlab::Changelog::AST::Variable do + let(:state) { Gitlab::Changelog::EvalState.new } + let(:data) { { 'numbers' => [10] } } + + describe '#evaluate' do + it 'evaluates a variable' do + node = Gitlab::Changelog::Parser + .new + .parse_and_transform('{{numbers.0}}') + .nodes[0] + + expect(node.evaluate(state, data)).to eq('10') + end + + it 'evaluates an undefined variable' do + node = + Gitlab::Changelog::Parser.new.parse_and_transform('{{foobar}}').nodes[0] + + expect(node.evaluate(state, data)).to eq('') + end + + it 'evaluates the special variable "it"' do + node = + Gitlab::Changelog::Parser.new.parse_and_transform('{{it}}').nodes[0] + + expect(node.evaluate(state, data)).to eq(data.to_s) + end + end +end + +RSpec.describe Gitlab::Changelog::AST::Expressions do + let(:state) { Gitlab::Changelog::EvalState.new } + + describe '#evaluate' do + it 'evaluates all expressions' do + node = Gitlab::Changelog::Parser + .new + .parse_and_transform('{{number}}foo') + + expect(node.evaluate(state, { 'number' => 10 })).to eq('10foo') + end + end +end + +RSpec.describe Gitlab::Changelog::AST::Text do + let(:state) { Gitlab::Changelog::EvalState.new } + + describe '#evaluate' do + it 'returns the text' do + expect(described_class.new('foo').evaluate(state, {})).to eq('foo') + end + end +end + +RSpec.describe Gitlab::Changelog::AST::If do + let(:state) { Gitlab::Changelog::EvalState.new } + + describe '#evaluate' do + it 'evaluates a truthy if expression without an else clause' do + node = Gitlab::Changelog::Parser + .new + .parse_and_transform('{% if thing %}foo{% end %}') + .nodes[0] + + expect(node.evaluate(state, { 'thing' => true })).to eq('foo') + end + + it 'evaluates a falsy if expression without an else clause' do + node = Gitlab::Changelog::Parser + .new + .parse_and_transform('{% if thing %}foo{% end %}') + .nodes[0] + + expect(node.evaluate(state, { 'thing' => false })).to eq('') + end + + it 'evaluates a falsy if expression with an else clause' do + node = Gitlab::Changelog::Parser + .new + .parse_and_transform('{% if thing %}foo{% else %}bar{% end %}') + .nodes[0] + + expect(node.evaluate(state, { 'thing' => false })).to eq('bar') + end + end + + describe '#truthy?' do + it 'returns true for a non-empty String' do + expect(described_class.new.truthy?('foo')).to eq(true) + end + + it 'returns true for a non-empty Array' do + expect(described_class.new.truthy?([10])).to eq(true) + end + + it 'returns true for a Boolean true' do + expect(described_class.new.truthy?(true)).to eq(true) + end + + it 'returns false for an empty String' do + expect(described_class.new.truthy?('')).to eq(false) + end + + it 'returns true for an empty Array' do + expect(described_class.new.truthy?([])).to eq(false) + end + + it 'returns false for a Boolean false' do + expect(described_class.new.truthy?(false)).to eq(false) + end + end +end + +RSpec.describe Gitlab::Changelog::AST::Each do + let(:state) { Gitlab::Changelog::EvalState.new } + + describe '#evaluate' do + it 'evaluates the expression' do + data = { 'animals' => [{ 'name' => 'Cat' }, { 'name' => 'Dog' }] } + node = Gitlab::Changelog::Parser + .new + .parse_and_transform('{% each animals %}{{name}}{% end %}') + .nodes[0] + + expect(node.evaluate(state, data)).to eq('CatDog') + end + + it 'returns an empty string when the input is not a collection' do + data = { 'animals' => 10 } + node = Gitlab::Changelog::Parser + .new + .parse_and_transform('{% each animals %}{{name}}{% end %}') + .nodes[0] + + expect(node.evaluate(state, data)).to eq('') + end + + it 'disallows too many nested loops' do + data = { + 'foo' => [ + { + 'bar' => [ + { + 'baz' => [ + { + 'quix' => [ + { + 'foo' => [{ 'name' => 'Alice' }] + } + ] + } + ] + } + ] + } + ] + } + + template = <<~TPL + {% each foo %} + {% each bar %} + {% each baz %} + {% each quix %} + {% each foo %} + {{name}} + {% end %} + {% end %} + {% end %} + {% end %} + {% end %} + TPL + + node = + Gitlab::Changelog::Parser.new.parse_and_transform(template).nodes[0] + + expect { node.evaluate(state, data) } + .to raise_error(Gitlab::Changelog::Error) + end + end +end diff --git a/spec/lib/gitlab/changelog/committer_spec.rb b/spec/lib/gitlab/changelog/committer_spec.rb new file mode 100644 index 00000000000..1e04fe346cb --- /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 Error' 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(Gitlab::Changelog::Error) + 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..51988acf3d1 --- /dev/null +++ b/spec/lib/gitlab/changelog/config_spec.rb @@ -0,0 +1,98 @@ +# 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::AST::Expressions) + + expect(config.categories).to eq({ 'foo' => 'bar' }) + end + + it 'raises Error when the categories are not a Hash' do + expect { described_class.from_hash(project, 'categories' => 10) } + .to raise_error(Gitlab::Changelog::Error) + 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/parser_spec.rb b/spec/lib/gitlab/changelog/parser_spec.rb new file mode 100644 index 00000000000..1d353f5eb35 --- /dev/null +++ b/spec/lib/gitlab/changelog/parser_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Changelog::Parser do + let(:parser) { described_class.new } + + describe '#root' do + it 'parses an empty template' do + expect(parser.root).to parse('') + end + + it 'parses a variable with a single identifier step' do + expect(parser.root).to parse('{{foo}}') + end + + it 'parses a variable with a single integer step' do + expect(parser.root).to parse('{{0}}') + end + + it 'parses a variable with multiple selector steps' do + expect(parser.root).to parse('{{foo.bar}}') + end + + it 'parses a variable with an integer selector step' do + expect(parser.root).to parse('{{foo.bar.0}}') + end + + it 'parses the special "it" variable' do + expect(parser.root).to parse('{{it}}') + end + + it 'parses a text node' do + expect(parser.root).to parse('foo') + end + + it 'parses an if expression' do + expect(parser.root).to parse('{% if foo %}bar{% end %}') + end + + it 'parses an if-else expression' do + expect(parser.root).to parse('{% if foo %}bar{% else %}baz{% end %}') + end + + it 'parses an each expression' do + expect(parser.root).to parse('{% each foo %}foo{% end %}') + end + + it 'parses an escaped newline' do + expect(parser.root).to parse("foo\\\nbar") + end + + it 'parses a regular newline' do + expect(parser.root).to parse("foo\nbar") + end + + it 'parses the default changelog template' do + expect(parser.root).to parse(Gitlab::Changelog::Config::DEFAULT_TEMPLATE) + end + + it 'raises an error when parsing an integer selector that is too large' do + expect(parser.root).not_to parse('{{100000000000}}') + end + end + + describe '#parse_and_transform' do + it 'parses and transforms a template' do + node = parser.parse_and_transform('foo') + + expect(node).to be_instance_of(Gitlab::Changelog::AST::Expressions) + end + + it 'raises parsing errors using a custom error class' do + expect { parser.parse_and_transform('{% each') } + .to raise_error(Gitlab::Changelog::Error) + 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..f95244d6750 --- /dev/null +++ b/spec/lib/gitlab/changelog/release_spec.rb @@ -0,0 +1,129 @@ +# 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 + + context 'when a category has no entries' do + it "isn't included in the output" do + config.categories['kittens'] = 'Kittens' + config.categories['fixed'] = 'Bug fixes' + + release.add_entry( + title: 'Entry title', + commit: commit, + category: 'fixed' + ) + + expect(release.to_markdown).to eq(<<~OUT) + ## 1.0.0 (2021-01-05) + + ### Bug fixes (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/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/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb index 439799fe973..1b8dfae692a 100644 --- a/spec/lib/gitlab/ci/config/entry/commands_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb @@ -87,18 +87,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Commands do describe '#errors' do it 'saves errors' do expect(entry.errors) - .to include 'commands config should be a string or an array containing strings and arrays of strings' + .to include 'commands config should be a string or a nested array of strings up to 10 levels deep' end end end context 'when entry value is multi-level nested array' do - let(:config) { [['ls', ['echo 1']], 'pwd'] } + let(:config) do + ['ls 0', ['ls 1', ['ls 2', ['ls 3', ['ls 4', ['ls 5', ['ls 6', ['ls 7', ['ls 8', ['ls 9', ['ls 10']]]]]]]]]]] + end describe '#errors' do it 'saves errors' do expect(entry.errors) - .to include 'commands config should be a string or an array containing strings and arrays of strings' + .to include 'commands config should be a string or a nested array of strings up to 10 levels deep' end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 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/config/yaml/tags/reference_spec.rb b/spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb new file mode 100644 index 00000000000..c68dccd3455 --- /dev/null +++ b/spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Yaml::Tags::Reference do + let(:config) do + Gitlab::Ci::Config::Yaml.load!(yaml) + end + + describe '.tag' do + it 'implements the tag method' do + expect(described_class.tag).to eq('!reference') + end + end + + describe '#resolve' do + subject { Gitlab::Ci::Config::Yaml::Tags::Resolver.new(config).to_hash } + + context 'with circular references' do + let(:yaml) do + <<~YML + a: !reference [b] + b: !reference [a] + YML + end + + it 'raises CircularReferenceError' do + expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, '!reference ["b"] is part of a circular chain' + end + end + + context 'with nested circular references' do + let(:yaml) do + <<~YML + a: !reference [b, c] + b: { c: !reference [d, e, f] } + d: { e: { f: !reference [a] } } + YML + end + + it 'raises CircularReferenceError' do + expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, '!reference ["b", "c"] is part of a circular chain' + end + end + + context 'with missing references' do + let(:yaml) { 'a: !reference [b]' } + + it 'raises MissingReferenceError' do + expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, '!reference ["b"] could not be found' + end + end + + context 'with invalid references' do + using RSpec::Parameterized::TableSyntax + + where(:yaml, :error_message) do + 'a: !reference' | '!reference [] is not valid' + 'a: !reference str' | '!reference "str" is not valid' + 'a: !reference 1' | '!reference "1" is not valid' + 'a: !reference [1]' | '!reference [1] is not valid' + 'a: !reference { b: c }' | '!reference {"b"=>"c"} is not valid' + end + + with_them do + it 'raises an error' do + expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, error_message + end + end + end + + context 'with arrays' do + let(:yaml) do + <<~YML + a: { b: [1, 2] } + c: { d: { e: [3, 4] } } + f: { g: [ !reference [a, b], 5, !reference [c, d, e]] } + YML + end + + it { is_expected.to match(a_hash_including({ f: { g: [[1, 2], 5, [3, 4]] } })) } + end + + context 'with hashes' do + context 'when referencing an entire hash' do + let(:yaml) do + <<~YML + a: { b: { c: 'c', d: 'd' } } + e: { f: !reference [a, b] } + YML + end + + it { is_expected.to match(a_hash_including({ e: { f: { c: 'c', d: 'd' } } })) } + end + + context 'when referencing only a hash value' do + let(:yaml) do + <<~YML + a: { b: { c: 'c', d: 'd' } } + e: { f: { g: !reference [a, b, c], h: 'h' } } + i: !reference [e, f] + YML + end + + it { is_expected.to match(a_hash_including({ i: { g: 'c', h: 'h' } })) } + end + + context 'when referencing a value before its definition' do + let(:yaml) do + <<~YML + a: { b: !reference [c, d] } + g: { h: { i: 'i', j: 1 } } + c: { d: { e: !reference [g, h, j], f: 'f' } } + YML + end + + it { is_expected.to match(a_hash_including({ a: { b: { e: 1, f: 'f' } } })) } + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb b/spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb new file mode 100644 index 00000000000..594242c33cc --- /dev/null +++ b/spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Yaml::Tags::Resolver do + let(:config) do + Gitlab::Ci::Config::Yaml.load!(yaml) + end + + describe '#to_hash' do + subject { described_class.new(config).to_hash } + + context 'when referencing deeply nested arrays' do + let(:yaml_templates) do + <<~YML + .job-1: + script: + - echo doing step 1 of job 1 + - echo doing step 2 of job 1 + + .job-2: + script: + - echo doing step 1 of job 2 + - !reference [.job-1, script] + - echo doing step 2 of job 2 + + .job-3: + script: + - echo doing step 1 of job 3 + - !reference [.job-2, script] + - echo doing step 2 of job 3 + YML + end + + let(:job_yaml) do + <<~YML + test: + script: + - echo preparing to test + - !reference [.job-3, script] + - echo test finished + YML + end + + shared_examples 'expands references' do + it 'expands the references' do + is_expected.to match({ + '.job-1': { + script: [ + 'echo doing step 1 of job 1', + 'echo doing step 2 of job 1' + ] + }, + '.job-2': { + script: [ + 'echo doing step 1 of job 2', + [ + 'echo doing step 1 of job 1', + 'echo doing step 2 of job 1' + ], + 'echo doing step 2 of job 2' + ] + }, + '.job-3': { + script: [ + 'echo doing step 1 of job 3', + [ + 'echo doing step 1 of job 2', + [ + 'echo doing step 1 of job 1', + 'echo doing step 2 of job 1' + ], + 'echo doing step 2 of job 2' + ], + 'echo doing step 2 of job 3' + ] + }, + test: { + script: [ + 'echo preparing to test', + [ + 'echo doing step 1 of job 3', + [ + 'echo doing step 1 of job 2', + [ + 'echo doing step 1 of job 1', + 'echo doing step 2 of job 1' + ], + 'echo doing step 2 of job 2' + ], + 'echo doing step 2 of job 3' + ], + 'echo test finished' + ] + } + }) + end + end + + context 'when templates are defined before the job' do + let(:yaml) do + <<~YML + #{yaml_templates} + #{job_yaml} + YML + end + + it_behaves_like 'expands references' + end + + context 'when templates are defined after the job' do + let(:yaml) do + <<~YML + #{job_yaml} + #{yaml_templates} + YML + end + + it_behaves_like 'expands references' + end + end + end +end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index dc03d2f80fe..45ce4cac6c4 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -263,6 +263,26 @@ RSpec.describe Gitlab::Ci::Config do end end end + + context 'when yaml uses circular !reference' do + let(:yml) do + <<~YAML + job-1: + script: + - !reference [job-2, before_script] + + job-2: + before_script: !reference [job-1, script] + YAML + end + + it 'raises error' do + expect { config }.to raise_error( + described_class::ConfigError, + /\!reference \["job-2", "before_script"\] is part of a circular chain/ + ) + end + end end context "when using 'include' directive" do 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/jwt_spec.rb b/spec/lib/gitlab/ci/jwt_spec.rb index 3130c0c0c41..342ca6b8b75 100644 --- a/spec/lib/gitlab/ci/jwt_spec.rb +++ b/spec/lib/gitlab/ci/jwt_spec.rb @@ -44,6 +44,9 @@ RSpec.describe Gitlab::Ci::Jwt do expect(payload[:pipeline_id]).to eq(pipeline.id.to_s) expect(payload[:job_id]).to eq(build.id.to_s) expect(payload[:ref]).to eq(pipeline.source_ref) + expect(payload[:ref_protected]).to eq(build.protected.to_s) + expect(payload[:environment]).to be_nil + expect(payload[:environment_protected]).to be_nil end end @@ -90,6 +93,39 @@ RSpec.describe Gitlab::Ci::Jwt do expect(payload[:ref_protected]).to eq('true') end end + + describe 'environment' do + let(:environment) { build_stubbed(:environment, project: project, name: 'production') } + let(:build) do + build_stubbed( + :ci_build, + project: project, + user: user, + pipeline: pipeline, + environment: environment.name + ) + end + + before do + allow(build).to receive(:persisted_environment).and_return(environment) + end + + it 'has correct values for environment attributes' do + expect(payload[:environment]).to eq('production') + expect(payload[:environment_protected]).to eq('false') + end + + context ':ci_jwt_include_environment feature flag is disabled' do + before do + stub_feature_flags(ci_jwt_include_environment: false) + end + + it 'does not include environment attributes' do + expect(payload).not_to have_key(:environment) + expect(payload).not_to have_key(:environment_protected) + end + end + end end describe '.for_build' do 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/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb index 20406acb658..53dea1d0d19 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb @@ -235,7 +235,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do with_them do before do - project.update!(ci_keep_latest_artifact: keep_latest_artifact) + project.update!(keep_latest_artifact: keep_latest_artifact) end it 'builds a pipeline with appropriate locked value' do 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/pipeline/chain/template_usage_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb index 3616461d94f..cd868a57bbc 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::TemplateUsage do %w(Template-1 Template-2).each do |expected_template| expect(Gitlab::UsageDataCounters::CiTemplateUniqueCounter).to( receive(:track_unique_project_event) - .with(project_id: project.id, template: expected_template) + .with(project_id: project.id, template: expected_template, config_source: pipeline.config_source) ) end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index cf020fc343c..0efc7484699 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -383,14 +383,25 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do end context 'when job is a bridge' do - let(:attributes) do + let(:base_attributes) do { name: 'rspec', ref: 'master', options: { trigger: 'my/project' }, scheduling_type: :stage } end + let(:attributes) { base_attributes } + it { is_expected.to be_a(::Ci::Bridge) } it { is_expected.to be_valid } + + context 'when job belongs to a resource group' do + let(:attributes) { base_attributes.merge(resource_group_key: 'iOS') } + + it 'returns a job with resource group' do + expect(subject.resource_group).not_to be_nil + expect(subject.resource_group.key).to eq('iOS') + end + end end it 'memoizes a resource object' do diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/resource_group_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/processable/resource_group_spec.rb index 8fcc242ba5f..b7260599de2 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build/resource_group_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/processable/resource_group_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::ResourceGroup do +RSpec.describe Gitlab::Ci::Pipeline::Seed::Processable::ResourceGroup do let_it_be(:project) { create(:project) } let(:job) { build(:ci_build, project: project) } let(:seed) { described_class.new(job, resource_group_key) } 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/status/bridge/factory_spec.rb b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb index d27bb98ba9a..6081f104e42 100644 --- a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb @@ -117,14 +117,31 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do end end + context 'when bridge is waiting for resource' do + let(:bridge) { create_bridge(:waiting_for_resource, :resource_group) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::WaitingForResource + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'waiting' + expect(status.group).to eq 'waiting-for-resource' + expect(status.icon).to eq 'status_pending' + expect(status.favicon).to eq 'favicon_pending' + expect(status.illustration).to include(:image, :size, :title) + expect(status).not_to have_details + end + end + private - def create_bridge(trait) + def create_bridge(*traits) upstream_project = create(:project, :repository) downstream_project = create(:project, :repository) upstream_pipeline = create(:ci_pipeline, :running, project: upstream_project) trigger = { trigger: { project: downstream_project.full_path, branch: 'feature' } } - create(:ci_bridge, trait, options: trigger, pipeline: upstream_pipeline) + create(:ci_bridge, *traits, options: trigger, pipeline: upstream_pipeline) end end diff --git a/spec/lib/gitlab/ci/status/bridge/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/bridge/waiting_for_resource_spec.rb new file mode 100644 index 00000000000..3e19df28d83 --- /dev/null +++ b/spec/lib/gitlab/ci/status/bridge/waiting_for_resource_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Status::Bridge::WaitingForResource do + it { expect(described_class).to be < Gitlab::Ci::Status::Processable::WaitingForResource } +end diff --git a/spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb new file mode 100644 index 00000000000..44bd5a8611a --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Status::Build::WaitingForResource do + it { expect(described_class).to be < Gitlab::Ci::Status::Processable::WaitingForResource } +end diff --git a/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb new file mode 100644 index 00000000000..91a9724d043 --- /dev/null +++ b/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Status::Processable::WaitingForResource do + let(:user) { create(:user) } + + subject do + processable = create(:ci_build, :waiting_for_resource, :resource_group) + described_class.new(Gitlab::Ci::Status::Core.new(processable, user)) + end + + describe '#illustration' do + it { expect(subject.illustration).to include(:image, :size, :title) } + end + + describe '.matches?' do + subject {described_class.matches?(processable, user) } + + context 'when processable is waiting for resource' do + let(:processable) { create(:ci_build, :waiting_for_resource) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when processable is not waiting for resource' do + let(:processable) { create(:ci_build) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end 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/collection/sorted_spec.rb b/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb index d85bf29f77f..954273fd41e 100644 --- a/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb @@ -5,8 +5,11 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do describe '#errors' do context 'when FF :variable_inside_variable is disabled' do + let_it_be(:project_with_flag_disabled) { create(:project) } + let_it_be(:project_with_flag_enabled) { create(:project) } + before do - stub_feature_flags(variable_inside_variable: false) + stub_feature_flags(variable_inside_variable: [project_with_flag_enabled]) end context 'table tests' do @@ -53,7 +56,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do end with_them do - subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) } + subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project_with_flag_disabled) } it 'does not report error' do expect(subject.errors).to eq(nil) @@ -67,8 +70,11 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do end context 'when FF :variable_inside_variable is enabled' do + let_it_be(:project_with_flag_disabled) { create(:project) } + let_it_be(:project_with_flag_enabled) { create(:project) } + before do - stub_feature_flags(variable_inside_variable: true) + stub_feature_flags(variable_inside_variable: [project_with_flag_enabled]) end context 'table tests' do @@ -100,7 +106,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do end with_them do - subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) } + subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project_with_flag_enabled) } it 'errors matches expected validation result' do expect(subject.errors).to eq(validation_result) @@ -164,7 +170,8 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do end with_them do - subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) } + let_it_be(:project) { create(:project) } + subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project) } it 'does not expand variables' do expect(subject.sort).to eq(variables) @@ -239,7 +246,8 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do end with_them do - subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) } + let_it_be(:project) { create(:project) } + subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project) } it 'sort returns correctly sorted variables' do expect(subject.sort.map { |var| var[:key] }).to eq(result) 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/config/entry/validators/nested_array_helpers_spec.rb b/spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb new file mode 100644 index 00000000000..cd68307e71f --- /dev/null +++ b/spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Config::Entry::Validators::NestedArrayHelpers do + let(:config_struct) do + Struct.new(:value, keyword_init: true) do + include ActiveModel::Validations + extend Gitlab::Config::Entry::Validators::NestedArrayHelpers + + validates_each :value do |record, attr, value| + unless validate_nested_array(value, 2) { |v| v.is_a?(Integer) } + record.errors.add(attr, "is invalid") + end + end + end + end + + describe '#validate_nested_array' do + let(:config) { config_struct.new(value: value) } + + subject(:errors) { config.errors } + + before do + config.valid? + end + + context 'with valid values' do + context 'with arrays of integers' do + let(:value) { [10, 11] } + + it { is_expected.to be_empty } + end + + context 'with nested arrays of integers' do + let(:value) { [10, [11, 12]] } + + it { is_expected.to be_empty } + end + end + + context 'with invalid values' do + subject(:error_messages) { errors.messages } + + context 'with single integers' do + let(:value) { 10 } + + it { is_expected.to eq({ value: ['is invalid'] }) } + end + + context 'when it is nested over the limit' do + let(:value) { [10, [11, [12]]] } + + it { is_expected.to eq({ value: ['is invalid'] }) } + end + + context 'when a value in the array is not valid' do + let(:value) { [10, 11.5] } + + it { is_expected.to eq({ value: ['is invalid'] }) } + end + + context 'when a value in the nested array is not valid' do + let(:value) { [10, [11, 12.5]] } + + it { is_expected.to eq({ value: ['is invalid'] }) } + end + end + end +end 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/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb index 21503dc1501..76578340f7b 100644 --- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb @@ -218,7 +218,7 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do context 'when `to` is given' do before do - Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) } + Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project, finished_at: Time.zone.now) } end it 'finds records created between `from` and `to` range' do @@ -230,12 +230,34 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do end context 'when `from` and `to` are within a day' do - it 'returns the number of deployments made on that day' do - freeze_time do - create(:deployment, :success, project: project) - options[:from] = options[:to] = Time.now + context 'when query_deploymenys_via_finished_at_in_vsa feature flag is off' do + before do + stub_feature_flags(query_deploymenys_via_finished_at_in_vsa: false) + end + + it 'returns the number of deployments made on that day' do + freeze_time do + create(:deployment, :success, project: project) + options[:from] = options[:to] = Time.zone.now + + expect(subject).to eq('1') + end + end + end + + context 'when query_deploymenys_via_finished_at_in_vsa feature flag is off' do + before do + stub_feature_flags(query_deploymenys_via_finished_at_in_vsa: true) + end + + it 'returns the number of deployments made on that day' do + freeze_time do + create(:deployment, :success, project: project, finished_at: Time.zone.now) + options[:from] = Time.zone.now.at_beginning_of_day + options[:to] = Time.zone.now.at_end_of_day - expect(subject).to eq('1') + expect(subject).to eq('1') + 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..fd7cadeb89e 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 @@ -102,5 +104,16 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do expect(merge_request_attrs[:url]).to eq("http://localhost/#{merge_request.target_project.full_path}/-/merge_requests/#{merge_request.iid}") end end + + context 'when pipeline has retried builds' do + before do + create(:ci_build, :retried, pipeline: pipeline) + end + + it 'does not contain retried builds in payload' do + expect(data[:builds].count).to eq(1) + expect(build_data[:id]).to eq(build.id) + end + end 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/migrations/instrumentation_spec.rb b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb new file mode 100644 index 00000000000..3804dc52a77 --- /dev/null +++ b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Gitlab::Database::Migrations::Instrumentation do + describe '#observe' do + subject { described_class.new } + + let(:migration) { 1234 } + + it 'executes the given block' do + expect { |b| subject.observe(migration, &b) }.to yield_control + end + + context 'behavior with observers' do + subject { described_class.new(observers).observe(migration) {} } + + let(:observers) { [observer] } + let(:observer) { instance_double('Gitlab::Database::Migrations::Observers::MigrationObserver', before: nil, after: nil, record: nil) } + + it 'calls #before, #after, #record on given observers' do + expect(observer).to receive(:before).ordered + expect(observer).to receive(:after).ordered + expect(observer).to receive(:record).ordered do |observation| + expect(observation.migration).to eq(migration) + end + + subject + end + + it 'ignores errors coming from observers #before' do + expect(observer).to receive(:before).and_raise('some error') + + subject + end + + it 'ignores errors coming from observers #after' do + expect(observer).to receive(:after).and_raise('some error') + + subject + end + + it 'ignores errors coming from observers #record' do + expect(observer).to receive(:record).and_raise('some error') + + subject + end + end + + context 'on successful execution' do + subject { described_class.new.observe(migration) {} } + + it 'records walltime' do + expect(subject.walltime).not_to be_nil + end + + it 'records success' do + expect(subject.success).to be_truthy + end + + it 'records the migration version' do + expect(subject.migration).to eq(migration) + end + end + + context 'upon failure' do + subject { described_class.new.observe(migration) { raise 'something went wrong' } } + + it 'raises the exception' do + expect { subject }.to raise_error(/something went wrong/) + end + + context 'retrieving observations' do + subject { instance.observations.first } + + before do + instance.observe(migration) { raise 'something went wrong' } + rescue + # ignore + end + + let(:instance) { described_class.new } + + it 'records walltime' do + expect(subject.walltime).not_to be_nil + end + + it 'records failure' do + expect(subject.success).to be_falsey + end + + it 'records the migration version' do + expect(subject.migration).to eq(migration) + end + end + end + + context 'sequence of migrations with failures' do + subject { described_class.new } + + let(:migration1) { double('migration1', call: nil) } + let(:migration2) { double('migration2', call: nil) } + + it 'records observations for all migrations' do + subject.observe('migration1') {} + subject.observe('migration2') { raise 'something went wrong' } rescue nil + + expect(subject.observations.size).to eq(2) + end + end + end +end diff --git a/spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb b/spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb new file mode 100644 index 00000000000..73466471944 --- /dev/null +++ b/spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Gitlab::Database::Migrations::Observers::TotalDatabaseSizeChange do + subject { described_class.new } + + let(:observation) { Gitlab::Database::Migrations::Observation.new } + let(:connection) { ActiveRecord::Base.connection } + let(:query) { 'select pg_database_size(current_database())' } + + it 'records the size change' do + expect(connection).to receive(:execute).with(query).once.and_return([{ 'pg_database_size' => 1024 }]) + expect(connection).to receive(:execute).with(query).once.and_return([{ 'pg_database_size' => 256 }]) + + subject.before + subject.after + subject.record(observation) + + expect(observation.total_database_size_change).to eq(256 - 1024) + end + + context 'out of order calls' do + before do + allow(connection).to receive(:execute).with(query).and_return([{ 'pg_database_size' => 1024 }]) + end + + it 'does not record anything if before size is unknown' do + subject.after + + expect { subject.record(observation) }.not_to change { observation.total_database_size_change } + end + + it 'does not record anything if after size is unknown' do + subject.before + + expect { subject.record(observation) }.not_to change { observation.total_database_size_change } + end + end +end diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb index 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/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb index b1ffbedc7bf..eb11c051adc 100644 --- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb @@ -40,6 +40,13 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do expect(new_issue.description).to eq(expected_description.strip) end + it 'creates an issue_email_participant' do + receiver.execute + new_issue = Issue.last + + expect(new_issue.issue_email_participants.first.email).to eq("jake@adventuretime.ooo") + end + it 'sends thank you email' do expect { receiver.execute }.to have_enqueued_job.on_queue('mailers') end 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..7eeae3f3f33 100644 --- a/spec/lib/gitlab/experimentation_spec.rb +++ b/spec/lib/gitlab/experimentation_spec.rb @@ -7,7 +7,6 @@ require 'spec_helper' RSpec.describe Gitlab::Experimentation::EXPERIMENTS do it 'temporarily ensures we know what experiments exist for backwards compatibility' do expected_experiment_keys = [ - :onboarding_issues, :ci_notification_dot, :upgrade_link_in_user_menu_a, :invite_members_version_a, @@ -15,8 +14,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 +25,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 +35,10 @@ RSpec.describe Gitlab::Experimentation do }, test_experiment: { tracking_category: 'Team' + }, + tabular_experiment: { + tracking_category: 'Team', + rollout_strategy: rollout_strategy } }) @@ -46,6 +50,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 +180,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/push_spec.rb b/spec/lib/gitlab/git/push_spec.rb index 0f52f10c0a6..8ba43b2967c 100644 --- a/spec/lib/gitlab/git/push_spec.rb +++ b/spec/lib/gitlab/git/push_spec.rb @@ -86,6 +86,16 @@ RSpec.describe Gitlab::Git::Push do it { is_expected.to be_force_push } end + + context 'when called muiltiple times' do + it 'does not make make multiple calls to the force push check' do + expect(Gitlab::Checks::ForcePush).to receive(:force_push?).once + + 2.times do + subject.force_push? + end + end + end end describe '#branch_added?' 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/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index a0cafe3d763..9a1ecfe6459 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -388,108 +388,6 @@ RSpec.describe Gitlab::GitAccess do end end - describe '#check_otp_session!' do - let_it_be(:user) { create(:user, :two_factor_via_otp)} - let_it_be(:key) { create(:key, user: user) } - let_it_be(:actor) { key } - - before do - project.add_developer(user) - stub_feature_flags(two_factor_for_cli: true) - end - - context 'with an OTP session', :clean_gitlab_redis_shared_state do - before do - Gitlab::Redis::SharedState.with do |redis| - redis.set("#{Gitlab::Auth::Otp::SessionEnforcer::OTP_SESSIONS_NAMESPACE}:#{key.id}", true) - end - end - - it 'allows push and pull access' do - aggregate_failures do - expect { push_access_check }.not_to raise_error - expect { pull_access_check }.not_to raise_error - end - end - end - - context 'without OTP session' do - it 'does not allow push or pull access' do - user = 'jane.doe' - host = 'fridge.ssh' - port = 42 - - stub_config( - gitlab_shell: { - ssh_user: user, - ssh_host: host, - ssh_port: port - } - ) - - error_message = "OTP verification is required to access the repository.\n\n"\ - " Use: ssh #{user}@#{host} -p #{port} 2fa_verify" - - aggregate_failures do - expect { push_access_check }.to raise_forbidden(error_message) - expect { pull_access_check }.to raise_forbidden(error_message) - end - end - - context 'when protocol is HTTP' do - let(:protocol) { 'http' } - - it 'allows push and pull access' do - aggregate_failures do - expect { push_access_check }.not_to raise_error - expect { pull_access_check }.not_to raise_error - end - end - end - - context 'when actor is not an SSH key' do - let(:deploy_key) { create(:deploy_key, user: user) } - let(:actor) { deploy_key } - - before do - deploy_key.deploy_keys_projects.create(project: project, can_push: true) - end - - it 'allows push and pull access' do - aggregate_failures do - expect { push_access_check }.not_to raise_error - expect { pull_access_check }.not_to raise_error - end - end - end - - context 'when 2FA is not enabled for the user' do - let(:user) { create(:user)} - let(:actor) { create(:key, user: user) } - - it 'allows push and pull access' do - aggregate_failures do - expect { push_access_check }.not_to raise_error - expect { pull_access_check }.not_to raise_error - end - end - end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(two_factor_for_cli: false) - end - - it 'allows push and pull access' do - aggregate_failures do - expect { push_access_check }.not_to raise_error - expect { pull_access_check }.not_to raise_error - end - end - end - end - end - describe '#check_db_accessibility!' do context 'when in a read-only GitLab instance' do before do diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index ce01566b870..22707c9a36b 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -299,6 +299,11 @@ RSpec.describe Gitlab::GitalyClient::OperationService do let(:start_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } let(:end_sha) { '54cec5282aa9f21856362fe321c800c236a61615' } let(:commit_message) { 'Squash message' } + + let(:time) do + Time.now.utc + end + let(:request) do Gitaly::UserSquashRequest.new( repository: repository.gitaly_repository, @@ -307,7 +312,8 @@ RSpec.describe Gitlab::GitalyClient::OperationService do start_sha: start_sha, end_sha: end_sha, author: gitaly_user, - commit_message: commit_message + commit_message: commit_message, + timestamp: Google::Protobuf::Timestamp.new(seconds: time.to_i) ) end @@ -315,7 +321,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do let(:response) { Gitaly::UserSquashResponse.new(squash_sha: squash_sha) } subject do - client.user_squash(user, squash_id, start_sha, end_sha, user, commit_message) + client.user_squash(user, squash_id, start_sha, end_sha, user, commit_message, time) end it 'sends a user_squash message and returns the squash sha' do diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index 7fcb11c4dfd..a8d42f4bccf 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -267,31 +267,63 @@ RSpec.describe Gitlab::GitalyClient do end describe '.request_kwargs' do - context 'when catfile-cache feature is enabled' do - before do - stub_feature_flags('gitaly_catfile-cache': true) + it 'sets the gitaly-session-id in the metadata' do + results = described_class.request_kwargs('default', timeout: 1) + expect(results[:metadata]).to include('gitaly-session-id') + end + + context 'when RequestStore is not enabled' do + it 'sets a different gitaly-session-id per request' do + gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id'] + + expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).not_to eq(gitaly_session_id) end + end - it 'sets the gitaly-session-id in the metadata' do - results = described_class.request_kwargs('default', timeout: 1) - expect(results[:metadata]).to include('gitaly-session-id') + context 'when RequestStore is enabled', :request_store do + it 'sets the same gitaly-session-id on every outgoing request metadata' do + gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id'] + + 3.times do + expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).to eq(gitaly_session_id) + end end + end - context 'when RequestStore is not enabled' do - it 'sets a different gitaly-session-id per request' do - gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id'] + context 'gitlab_git_env' do + let(:policy) { 'gitaly-route-repository-accessor-policy' } - expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).not_to eq(gitaly_session_id) + context 'when RequestStore is disabled' do + it 'does not force-route to primary' do + expect(described_class.request_kwargs('default', timeout: 1)[:metadata][policy]).to be_nil end end - context 'when RequestStore is enabled', :request_store do - it 'sets the same gitaly-session-id on every outgoing request metadata' do - gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id'] + context 'when RequestStore is enabled without git_env', :request_store do + it 'does not force-orute to primary' do + expect(described_class.request_kwargs('default', timeout: 1)[:metadata][policy]).to be_nil + end + end - 3.times do - expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).to eq(gitaly_session_id) - end + context 'when RequestStore is enabled with empty git_env', :request_store do + before do + Gitlab::SafeRequestStore[:gitlab_git_env] = {} + end + + it 'disables force-routing to primary' do + expect(described_class.request_kwargs('default', timeout: 1)[:metadata][policy]).to be_nil + end + end + + context 'when RequestStore is enabled with populated git_env', :request_store do + before do + Gitlab::SafeRequestStore[:gitlab_git_env] = { + "GIT_OBJECT_DIRECTORY_RELATIVE" => "foo/bar" + } + end + + it 'enables force-routing to primary' do + expect(described_class.request_kwargs('default', timeout: 1)[:metadata][policy]).to eq('primary-only') end 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/graphql/queries_spec.rb b/spec/lib/gitlab/graphql/queries_spec.rb index 6e08a87523f..a140a283c1b 100644 --- a/spec/lib/gitlab/graphql/queries_spec.rb +++ b/spec/lib/gitlab/graphql/queries_spec.rb @@ -151,6 +151,10 @@ RSpec.describe Gitlab::Graphql::Queries do let(:path) { 'post_by_slug.graphql' } it_behaves_like 'a valid GraphQL query for the blog schema' + + it 'has a complexity' do + expect(subject.complexity(schema)).to be < 10 + end end context 'a query with an import' do diff --git a/spec/lib/gitlab/health_checks/master_check_spec.rb b/spec/lib/gitlab/health_checks/master_check_spec.rb index 1c1efe178e2..287ebcec207 100644 --- a/spec/lib/gitlab/health_checks/master_check_spec.rb +++ b/spec/lib/gitlab/health_checks/master_check_spec.rb @@ -4,47 +4,67 @@ require 'spec_helper' require_relative './simple_check_shared' RSpec.describe Gitlab::HealthChecks::MasterCheck do - let(:result_class) { Gitlab::HealthChecks::Result } - before do stub_const('SUCCESS_CODE', 100) stub_const('FAILURE_CODE', 101) - described_class.register_master end - after do - described_class.finish_master - end + context 'when Puma runs in Clustered mode' do + before do + allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(true) - describe '#readiness' do - context 'when master is running' do - it 'worker does return success' do - _, child_status = run_worker + described_class.register_master + end - expect(child_status.exitstatus).to eq(SUCCESS_CODE) - end + after do + described_class.finish_master end - context 'when master finishes early' do - before do - described_class.send(:close_write) + describe '.available?' do + specify { expect(described_class.available?).to be true } + end + + describe '.readiness' do + context 'when master is running' do + it 'worker does return success' do + _, child_status = run_worker + + expect(child_status.exitstatus).to eq(SUCCESS_CODE) + end end - it 'worker does return failure' do - _, child_status = run_worker + context 'when master finishes early' do + before do + described_class.send(:close_write) + end - expect(child_status.exitstatus).to eq(FAILURE_CODE) + it 'worker does return failure' do + _, child_status = run_worker + + expect(child_status.exitstatus).to eq(FAILURE_CODE) + end end - end - def run_worker - pid = fork do - described_class.register_worker + def run_worker + pid = fork do + described_class.register_worker - exit(described_class.readiness.success ? SUCCESS_CODE : FAILURE_CODE) + exit(described_class.readiness.success ? SUCCESS_CODE : FAILURE_CODE) + end + + Process.wait2(pid) end + end + end + + # '.readiness' check is not invoked if '.available?' returns false + context 'when Puma runs in Single mode' do + before do + allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(false) + end - Process.wait2(pid) + describe '.available?' do + specify { expect(described_class.available?).to be false } end end end diff --git a/spec/lib/gitlab/health_checks/probes/collection_spec.rb b/spec/lib/gitlab/health_checks/probes/collection_spec.rb index 03138e936aa..69828c143db 100644 --- a/spec/lib/gitlab/health_checks/probes/collection_spec.rb +++ b/spec/lib/gitlab/health_checks/probes/collection_spec.rb @@ -61,6 +61,35 @@ RSpec.describe Gitlab::HealthChecks::Probes::Collection do expect(subject.json[:message]).to eq('Redis::CannotConnectError : Redis down') end end + + context 'when some checks are not available' do + before do + allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(false) + end + + let(:checks) do + [ + Gitlab::HealthChecks::MasterCheck + ] + end + + it 'asks for check availability' do + expect(Gitlab::HealthChecks::MasterCheck).to receive(:available?) + + subject + end + + it 'does not call `readiness` on checks that are not available' do + expect(Gitlab::HealthChecks::MasterCheck).not_to receive(:readiness) + + subject + end + + it 'does not fail collection check' do + expect(subject.http_status).to eq(200) + expect(subject.json[:status]).to eq('ok') + end + end end context 'without checks' do 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..d0282e14d5f 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 @@ -560,7 +561,10 @@ project: - alert_management_http_integrations - exported_protected_branches - incident_management_oncall_schedules +- incident_management_oncall_rotations - debian_distributions +- merge_request_metrics +- security_orchestration_policy_configuration award_emoji: - awardable - user @@ -589,6 +593,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/decompressed_archive_size_validator_spec.rb b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb index efb271086a0..96c467e78d6 100644 --- a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb +++ b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb @@ -27,25 +27,55 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do end context 'when file exceeds allowed decompressed size' do - it 'returns false' do + it 'logs error message returns false' do + expect(Gitlab::Import::Logger) + .to receive(:info) + .with( + import_upload_archive_path: filepath, + import_upload_archive_size: File.size(filepath), + message: 'Decompressed archive size limit reached' + ) expect(subject.valid?).to eq(false) end end - context 'when something goes wrong during decompression' do - before do - allow(subject.archive_file).to receive(:eof?).and_raise(StandardError) + context 'when exception occurs during decompression' do + shared_examples 'logs raised exception and terminates validator process group' do + let(:std) { double(:std, close: nil, value: nil) } + let(:wait_thr) { double } + + before do + allow(Process).to receive(:getpgid).and_return(2) + allow(Open3).to receive(:popen3).and_return([std, std, std, wait_thr]) + allow(wait_thr).to receive(:[]).with(:pid).and_return(1) + allow(wait_thr).to receive(:value).and_raise(exception) + end + + it 'logs raised exception and terminates validator process group' do + expect(Gitlab::Import::Logger) + .to receive(:info) + .with( + import_upload_archive_path: filepath, + import_upload_archive_size: File.size(filepath), + message: error_message + ) + expect(Process).to receive(:kill).with(-1, 2) + expect(subject.valid?).to eq(false) + end end - it 'logs and tracks raised exception' do - expect(Gitlab::ErrorTracking).to receive(:track_exception).with(instance_of(StandardError)) - expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(message: 'Decompressed archive size validation failed.')) + context 'when timeout occurs' do + let(:error_message) { 'Timeout reached during archive decompression' } + let(:exception) { Timeout::Error } - subject.valid? + include_examples 'logs raised exception and terminates validator process group' end - it 'returns false' do - expect(subject.valid?).to eq(false) + context 'when exception occurs' do + let(:error_message) { 'Error!' } + let(:exception) { StandardError.new(error_message) } + + include_examples 'logs raised exception and terminates validator process group' end end end 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..a5c9cde4c37 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -9,12 +9,17 @@ RSpec.describe Gitlab::InstrumentationHelper do describe '.keys' do it 'returns all available payload keys' do expected_keys = [ + :cpu_s, :gitaly_calls, :gitaly_duration_s, :rugged_calls, :rugged_duration_s, :elasticsearch_calls, :elasticsearch_duration_s, + :elasticsearch_timed_out_count, + :mem_objects, + :mem_bytes, + :mem_mallocs, :redis_calls, :redis_duration_s, :redis_read_bytes, @@ -37,7 +42,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) @@ -49,10 +58,14 @@ RSpec.describe Gitlab::InstrumentationHelper do subject { described_class.add_instrumentation_data(payload) } - it 'adds only DB counts by default' do + before do + described_class.init_instrumentation_data + end + + it 'includes DB counts' do subject - expect(payload).to eq(db_count: 0, db_cached_count: 0, db_write_count: 0) + expect(payload).to include(db_count: 0, db_cached_count: 0, db_write_count: 0) end context 'when Gitaly calls are made' do @@ -110,6 +123,47 @@ RSpec.describe Gitlab::InstrumentationHelper do expect(payload[:throttle_safelist]).to eq('foobar') end end + + it 'logs cpu_s duration' do + subject + + expect(payload).to include(:cpu_s) + end + + context 'when logging memory allocations' do + include MemoryInstrumentationHelper + + before do + skip_memory_instrumentation! + end + + it 'logs memory usage metrics' do + subject + + expect(payload).to include( + :mem_objects, + :mem_bytes, + :mem_mallocs + ) + end + + context 'when trace_memory_allocations is disabled' do + before do + stub_feature_flags(trace_memory_allocations: false) + Gitlab::Memory::Instrumentation.ensure_feature_flag! + end + + it 'does not log memory usage metrics' do + subject + + expect(payload).not_to include( + :mem_objects, + :mem_bytes, + :mem_mallocs + ) + end + end + end end describe '.queue_duration_for_job' do 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/memory/instrumentation_spec.rb b/spec/lib/gitlab/memory/instrumentation_spec.rb new file mode 100644 index 00000000000..6b53550a3d0 --- /dev/null +++ b/spec/lib/gitlab/memory/instrumentation_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Memory::Instrumentation do + include MemoryInstrumentationHelper + + before do + skip_memory_instrumentation! + end + + describe '.available?' do + it 'returns true' do + expect(described_class).to be_available + end + end + + describe '.start_thread_memory_allocations' do + subject { described_class.start_thread_memory_allocations } + + context 'when feature flag trace_memory_allocations is enabled' do + before do + stub_feature_flags(trace_memory_allocations: true) + end + + it 'a hash is returned' do + is_expected.not_to be_empty + end + end + + context 'when feature flag trace_memory_allocations is disabled' do + before do + stub_feature_flags(trace_memory_allocations: false) + end + + it 'a nil is returned' do + is_expected.to be_nil + end + end + + context 'when feature is unavailable' do + before do + allow(described_class).to receive(:available?) { false } + end + + it 'a nil is returned' do + is_expected.to be_nil + end + end + end + + describe '.with_memory_allocations' do + let(:ntimes) { 100 } + + subject do + described_class.with_memory_allocations do + Array.new(1000).map { '0' * 100 } + end + end + + before do + expect(described_class).to receive(:start_thread_memory_allocations).and_call_original + expect(described_class).to receive(:measure_thread_memory_allocations).and_call_original + end + + context 'when feature flag trace_memory_allocations is enabled' do + before do + stub_feature_flags(trace_memory_allocations: true) + end + + it 'a hash is returned' do + is_expected.to include( + mem_objects: be > 1000, + mem_mallocs: be > 1000, + mem_bytes: be > 100_000 # 100 items * 100 bytes each + ) + end + end + + context 'when feature flag trace_memory_allocations is disabled' do + before do + stub_feature_flags(trace_memory_allocations: false) + end + + it 'a nil is returned' do + is_expected.to be_nil + end + end + + context 'when feature is unavailable' do + before do + allow(described_class).to receive(:available?) { false } + end + + it 'a nil is returned' do + is_expected.to be_nil + 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/middleware/request_context_spec.rb b/spec/lib/gitlab/middleware/request_context_spec.rb index 431f4453e37..6d5b581feaa 100644 --- a/spec/lib/gitlab/middleware/request_context_spec.rb +++ b/spec/lib/gitlab/middleware/request_context_spec.rb @@ -18,9 +18,11 @@ RSpec.describe Gitlab::Middleware::RequestContext do end describe '#call' do - context 'setting the client ip' do - subject { Gitlab::RequestContext.instance.client_ip } + let(:instance) { Gitlab::RequestContext.instance } + + subject { described_class.new(app).call(env) } + context 'setting the client ip' do context 'with X-Forwarded-For headers' do let(:load_balancer_ip) { '1.2.3.4' } let(:headers) do @@ -33,13 +35,7 @@ RSpec.describe Gitlab::Middleware::RequestContext do let(:env) { Rack::MockRequest.env_for("/").merge(headers) } it 'returns the load balancer IP' do - endpoint = proc do - [200, {}, ["Hello"]] - end - - described_class.new(endpoint).call(env) - - expect(subject).to eq(load_balancer_ip) + expect { subject }.to change { instance.client_ip }.from(nil).to(load_balancer_ip) end end @@ -47,32 +43,19 @@ RSpec.describe Gitlab::Middleware::RequestContext do let(:ip) { '192.168.1.11' } before do - allow_next_instance_of(Rack::Request) do |instance| - allow(instance).to receive(:ip).and_return(ip) + allow_next_instance_of(Rack::Request) do |request| + allow(request).to receive(:ip).and_return(ip) end - described_class.new(app).call(env) end - it { is_expected.to eq(ip) } - end + it 'sets the `client_ip`' do + expect { subject }.to change { instance.client_ip }.from(nil).to(ip) + end - context 'before RequestContext middleware run' do - it { is_expected.to be_nil } + it 'sets the `request_start_time`' do + expect { subject }.to change { instance.request_start_time }.from(nil).to(Float) + end end end end - - context 'setting the thread cpu time' do - it 'sets the `start_thread_cpu_time`' do - expect { described_class.new(app).call(env) } - .to change { Gitlab::RequestContext.instance.start_thread_cpu_time }.from(nil).to(Float) - end - end - - context 'setting the request start time' do - it 'sets the `request_start_time`' do - expect { described_class.new(app).call(env) } - .to change { Gitlab::RequestContext.instance.request_start_time }.from(nil).to(Float) - end - end end diff --git a/spec/lib/gitlab/pages_transfer_spec.rb b/spec/lib/gitlab/pages_transfer_spec.rb index 4f0ee76b244..552a2e0701c 100644 --- a/spec/lib/gitlab/pages_transfer_spec.rb +++ b/spec/lib/gitlab/pages_transfer_spec.rb @@ -8,13 +8,24 @@ RSpec.describe Gitlab::PagesTransfer do context 'when receiving an allowed method' do it 'schedules a PagesTransferWorker', :aggregate_failures do - described_class::Async::METHODS.each do |meth| + described_class::METHODS.each do |meth| expect(PagesTransferWorker) .to receive(:perform_async).with(meth, %w[foo bar]) async.public_send(meth, 'foo', 'bar') end end + + it 'does nothing if legacy storage is disabled' do + stub_feature_flags(pages_update_legacy_storage: false) + + described_class::METHODS.each do |meth| + expect(PagesTransferWorker) + .not_to receive(:perform_async) + + async.public_send(meth, 'foo', 'bar') + end + end end context 'when receiving a private method' do @@ -59,6 +70,15 @@ RSpec.describe Gitlab::PagesTransfer do expect(subject.public_send(meth, *args)).to be(false) end + + it 'does nothing if legacy storage is disabled' do + stub_feature_flags(pages_update_legacy_storage: false) + + subject.public_send(meth, *args) + + expect(File.exist?(config_path_before)).to be(true) + expect(File.exist?(config_path_after)).to be(false) + end end describe '#move_namespace' do 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/request_forgery_protection_spec.rb b/spec/lib/gitlab/request_forgery_protection_spec.rb index 20996dd44b8..a7b777cf4f2 100644 --- a/spec/lib/gitlab/request_forgery_protection_spec.rb +++ b/spec/lib/gitlab/request_forgery_protection_spec.rb @@ -52,6 +52,11 @@ RSpec.describe Gitlab::RequestForgeryProtection, :allow_forgery_protection do end describe '.verified?' do + it 'does not modify the env' do + env['REQUEST_METHOD'] = "GET" + expect { described_class.verified?(env) }.not_to change { env } + end + context 'when the request method is GET' do before do env['REQUEST_METHOD'] = 'GET' diff --git a/spec/lib/gitlab/runtime_spec.rb b/spec/lib/gitlab/runtime_spec.rb index 8ed7cc141cd..1ec14092c63 100644 --- a/spec/lib/gitlab/runtime_spec.rb +++ b/spec/lib/gitlab/runtime_spec.rb @@ -44,10 +44,11 @@ RSpec.describe Gitlab::Runtime do context "puma" do let(:puma_type) { double('::Puma') } + let(:max_workers) { 2 } before do stub_const('::Puma', puma_type) - allow(puma_type).to receive_message_chain(:cli_config, :options).and_return(max_threads: 2) + allow(puma_type).to receive_message_chain(:cli_config, :options).and_return(max_threads: 2, workers: max_workers) stub_env('ACTION_CABLE_IN_APP', 'false') end @@ -70,6 +71,20 @@ RSpec.describe Gitlab::Runtime do it_behaves_like "valid runtime", :puma, 11 end + + describe ".puma_in_clustered_mode?" do + context 'when Puma is set up with workers > 0' do + let(:max_workers) { 4 } + + specify { expect(described_class.puma_in_clustered_mode?).to be true } + end + + context 'when Puma is set up with workers = 0' do + let(:max_workers) { 0 } + + specify { expect(described_class.puma_in_clustered_mode?).to be false } + end + end end context "unicorn" 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..a1b18172a31 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' @@ -183,12 +178,18 @@ RSpec.describe Gitlab::SearchResults do end context 'ordering' do - let(:query) { 'sorted' } let!(:old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'old-1', title: 'sorted old', created_at: 1.month.ago) } let!(:new_result) { create(:merge_request, :opened, source_project: project, source_branch: 'new-1', title: 'sorted recent', created_at: 1.day.ago) } let!(:very_old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'very-old-1', title: 'sorted very old', created_at: 1.year.ago) } - include_examples 'search results sorted' + let!(:old_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-old-1', title: 'updated old', updated_at: 1.month.ago) } + let!(:new_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-new-1', title: 'updated recent', updated_at: 1.day.ago) } + let!(:very_old_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-very-old-1', title: 'updated very old', updated_at: 1.year.ago) } + + include_examples 'search results sorted' do + let(:results_created) { described_class.new(user, 'sorted', Project.order(:id), sort: sort, filters: filters) } + let(:results_updated) { described_class.new(user, 'updated', Project.order(:id), sort: sort, filters: filters) } + end end end @@ -219,12 +220,18 @@ RSpec.describe Gitlab::SearchResults do end context 'ordering' do - let(:query) { 'sorted' } let!(:old_result) { create(:issue, project: project, title: 'sorted old', created_at: 1.month.ago) } let!(:new_result) { create(:issue, project: project, title: 'sorted recent', created_at: 1.day.ago) } let!(:very_old_result) { create(:issue, project: project, title: 'sorted very old', created_at: 1.year.ago) } - include_examples 'search results sorted' + let!(:old_updated) { create(:issue, project: project, title: 'updated old', updated_at: 1.month.ago) } + let!(:new_updated) { create(:issue, project: project, title: 'updated recent', updated_at: 1.day.ago) } + let!(:very_old_updated) { create(:issue, project: project, title: 'updated very old', updated_at: 1.year.ago) } + + include_examples 'search results sorted' do + let(:results_created) { described_class.new(user, 'sorted', Project.order(:id), sort: sort, filters: filters) } + let(:results_updated) { described_class.new(user, 'updated', Project.order(:id), sort: sort, filters: filters) } + end end end diff --git a/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb b/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb deleted file mode 100644 index 94dcf6f9b9a..00000000000 --- a/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::SidekiqLogging::ExceptionHandler do - describe '#call' do - let(:job) do - { - "class" => "TestWorker", - "args" => [1234, 'hello'], - "retry" => false, - "queue" => "cronjob:test_queue", - "queue_namespace" => "cronjob", - "jid" => "da883554ee4fe414012f5f42", - "correlation_id" => 'cid' - } - end - - let(:exception_message) { 'An error was thrown' } - let(:backtrace) { caller } - let(:exception) { RuntimeError.new(exception_message) } - let(:logger) { double } - - before do - allow(Sidekiq).to receive(:logger).and_return(logger) - allow(exception).to receive(:backtrace).and_return(backtrace) - end - - subject { described_class.new.call(exception, { context: 'Test', job: job }) } - - it 'logs job data into root tree' do - expected_data = job.merge( - error_class: 'RuntimeError', - error_message: exception_message, - context: 'Test', - error_backtrace: Rails.backtrace_cleaner.clean(backtrace) - ) - - expect(logger).to receive(:warn).with(expected_data) - - subject - end - end -end diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index b99a5352717..3e8e117ec71 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -3,7 +3,13 @@ require 'spec_helper' RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do - describe '#call' do + before do + # We disable a memory instrumentation feature + # as this requires a special patched Ruby + allow(Gitlab::Memory::Instrumentation).to receive(:available?) { false } + end + + describe '#call', :request_store do let(:timestamp) { Time.iso8601('2018-01-01T12:00:00.000Z') } let(:created_at) { timestamp - 1.second } let(:scheduling_latency_s) { 1.0 } @@ -21,14 +27,13 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do "correlation_id" => 'cid', "error_message" => "wrong number of arguments (2 for 3)", "error_class" => "ArgumentError", - "error_backtrace" => [], - "db_count" => 1, - "db_write_count" => 0, - "db_cached_count" => 0 + "error_backtrace" => [] } end let(:logger) { double } + let(:clock_realtime_start) { 0.222222299 } + let(:clock_realtime_end) { 1.333333799 } let(:clock_thread_cputime_start) { 0.222222299 } let(:clock_thread_cputime_end) { 1.333333799 } let(:start_payload) do @@ -38,7 +43,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 @@ -49,7 +55,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do 'duration_s' => 0.0, 'completed_at' => timestamp.to_f, 'cpu_s' => 1.111112, - 'db_duration_s' => 0.0 + 'db_duration_s' => 0.0, + 'db_cached_count' => 0, + 'db_count' => 0, + 'db_write_count' => 0 ) end @@ -58,7 +67,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec', 'job_status' => 'fail', 'error_class' => 'ArgumentError', - 'error_message' => 'some exception' + 'error_message' => 'Something went wrong', + 'error_backtrace' => be_a(Array).and(be_present) ) end @@ -67,7 +77,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do allow(subject).to receive(:current_time).and_return(timestamp.to_f) - allow(Process).to receive(:clock_gettime).with(Process::CLOCK_THREAD_CPUTIME_ID).and_return(clock_thread_cputime_start, clock_thread_cputime_end) + allow(Process).to receive(:clock_gettime).with(Process::CLOCK_REALTIME, :float_second) + .and_return(clock_realtime_start, clock_realtime_end) + allow(Process).to receive(:clock_gettime).with(Process::CLOCK_THREAD_CPUTIME_ID, :float_second) + .and_return(clock_thread_cputime_start, clock_thread_cputime_end) end subject { described_class.new } @@ -84,25 +97,97 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original - subject.call(job, 'test_queue') { } + call_subject(job, 'test_queue') { } + end + end + + it 'logs real job wrapped by active job worker' do + wrapped_job = job.merge( + "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", + "wrapped" => "TestWorker" + ) + + Timecop.freeze(timestamp) do + expect(logger).to receive(:info).with(start_payload).ordered + expect(logger).to receive(:info).with(end_payload).ordered + expect(subject).to receive(:log_job_start).and_call_original + expect(subject).to receive(:log_job_done).and_call_original + + call_subject(wrapped_job, 'test_queue') { } end end it 'logs an exception in job' do Timecop.freeze(timestamp) do expect(logger).to receive(:info).with(start_payload) - expect(logger).to receive(:warn).with(hash_including(exception_payload)) + expect(logger).to receive(:warn).with(include(exception_payload)) expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original expect do - subject.call(job, 'test_queue') do - raise ArgumentError, 'some exception' + call_subject(job, 'test_queue') do + raise ArgumentError, 'Something went wrong' end end.to raise_error(ArgumentError) end end + it 'logs the root cause of an Sidekiq::JobRetry::Skip exception in the job' do + Timecop.freeze(timestamp) do + expect(logger).to receive(:info).with(start_payload) + expect(logger).to receive(:warn).with(include(exception_payload)) + expect(subject).to receive(:log_job_start).and_call_original + expect(subject).to receive(:log_job_done).and_call_original + + expect do + call_subject(job, 'test_queue') do + raise ArgumentError, 'Something went wrong' + rescue + raise Sidekiq::JobRetry::Skip + end + end.to raise_error(Sidekiq::JobRetry::Skip) + end + end + + it 'logs the root cause of an Sidekiq::JobRetry::Handled exception in the job' do + Timecop.freeze(timestamp) do + expect(logger).to receive(:info).with(start_payload) + expect(logger).to receive(:warn).with(include(exception_payload)) + expect(subject).to receive(:log_job_start).and_call_original + expect(subject).to receive(:log_job_done).and_call_original + + expect do + call_subject(job, 'test_queue') do + raise ArgumentError, 'Something went wrong' + rescue + raise Sidekiq::JobRetry::Handled + end + end.to raise_error(Sidekiq::JobRetry::Handled) + end + end + + it 'keeps Sidekiq::JobRetry::Handled exception if the cause does not exist' do + Timecop.freeze(timestamp) do + expect(logger).to receive(:info).with(start_payload) + expect(logger).to receive(:warn).with( + include( + 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec', + 'job_status' => 'fail', + 'error_class' => 'Sidekiq::JobRetry::Skip', + 'error_message' => 'Sidekiq::JobRetry::Skip' + ) + ) + expect(subject).to receive(:log_job_start).and_call_original + expect(subject).to receive(:log_job_done).and_call_original + + expect do + call_subject(job, 'test_queue') do + raise Sidekiq::JobRetry::Skip + end + end.to raise_error(Sidekiq::JobRetry::Skip) + end + end + it 'does not modify the job' do Timecop.freeze(timestamp) do job_copy = job.deep_dup @@ -111,11 +196,29 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do allow(subject).to receive(:log_job_start).and_call_original allow(subject).to receive(:log_job_done).and_call_original - subject.call(job, 'test_queue') do + call_subject(job, 'test_queue') do expect(job).to eq(job_copy) end end end + + it 'does not modify the wrapped job' do + Timecop.freeze(timestamp) do + wrapped_job = job.merge( + "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", + "wrapped" => "TestWorker" + ) + job_copy = wrapped_job.deep_dup + + allow(logger).to receive(:info) + allow(subject).to receive(:log_job_start).and_call_original + allow(subject).to receive(:log_job_done).and_call_original + + call_subject(wrapped_job, 'test_queue') do + expect(wrapped_job).to eq(job_copy) + end + end + end end context 'with SIDEKIQ_LOG_ARGUMENTS disabled' do @@ -130,7 +233,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original - subject.call(job, 'test_queue') { } + call_subject(job, 'test_queue') { } end end @@ -143,7 +246,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original - subject.call(job.except("created_at", "enqueued_at"), 'test_queue') { } + call_subject(job.except("created_at", "enqueued_at"), 'test_queue') { } end end end @@ -159,7 +262,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original - subject.call(job, 'test_queue') { } + call_subject(job, 'test_queue') { } end end end @@ -177,7 +280,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do end let(:expected_end_payload) do - end_payload.merge(timing_data) + end_payload.merge(timing_data.stringify_keys) end it 'logs with Gitaly and Rugged timing data' do @@ -185,7 +288,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(logger).to receive(:info).with(start_payload).ordered expect(logger).to receive(:info).with(expected_end_payload).ordered - subject.call(job, 'test_queue') do + call_subject(job, 'test_queue') do job.merge!(timing_data) end end @@ -207,7 +310,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do let(:expected_end_payload_with_db) do expected_end_payload.merge( 'db_duration_s' => a_value >= 0.1, - 'db_count' => 1, + 'db_count' => a_value >= 1, 'db_cached_count' => 0, 'db_write_count' => 0 ) @@ -217,7 +320,9 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(logger).to receive(:info).with(expected_start_payload).ordered expect(logger).to receive(:info).with(expected_end_payload_with_db).ordered - subject.call(job, 'test_queue') { ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);') } + call_subject(job, 'test_queue') do + ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);') + end end it 'prevents database time from leaking to the next job' do @@ -226,8 +331,13 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(logger).to receive(:info).with(expected_start_payload).ordered expect(logger).to receive(:info).with(expected_end_payload).ordered - subject.call(job, 'test_queue') { ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);') } - subject.call(job, 'test_queue') { } + call_subject(job.dup, 'test_queue') do + ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);') + end + + Gitlab::SafeRequestStore.clear! + + call_subject(job.dup, 'test_queue') { } end end @@ -243,7 +353,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(logger).to receive(:info).with(expected_start_payload).ordered expect(logger).to receive(:info).with(expected_end_payload).ordered - subject.call(job, 'test_queue') do + call_subject(job, 'test_queue') do job["#{ApplicationWorker::LOGGING_EXTRA_KEY}.key1"] = 15 job["#{ApplicationWorker::LOGGING_EXTRA_KEY}.key2"] = 16 job['key that will be ignored because it does not start with extra.'] = 17 @@ -251,13 +361,29 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do end end end + + def call_subject(job, queue) + # This structured logger strongly depends on execution of `InstrumentationLogger` + subject.call(job, queue) do + ::Gitlab::SidekiqMiddleware::InstrumentationLogger.new.call('worker', job, queue) do + yield + end + end + end end describe '#add_time_keys!' do - let(:time) { { duration: 0.1231234, cputime: 1.2342345 } } + let(:time) { { duration: 0.1231234 } } let(:payload) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status' } } let(:current_utc_time) { Time.now.utc } - let(:payload_with_time_keys) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status', 'duration_s' => 0.123123, 'cpu_s' => 1.234235, 'completed_at' => current_utc_time.to_f } } + + let(:payload_with_time_keys) do + { 'class' => 'my-class', + 'message' => 'my-message', + 'job_status' => 'my-job-status', + 'duration_s' => 0.123123, + 'completed_at' => current_utc_time.to_f } + end subject { described_class.new } diff --git a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb index f7010b2001a..e2b36125b4e 100644 --- a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb @@ -60,6 +60,27 @@ RSpec.describe Gitlab::SidekiqMiddleware::ClientMetrics do end end + context "when a worker is wrapped into ActiveJob" do + before do + stub_const('TestWrappedWorker', Class.new) + TestWrappedWorker.class_eval do + include Sidekiq::Worker + end + end + + it_behaves_like "a metrics client middleware" do + let(:job) do + { + "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper, + "wrapped" => TestWrappedWorker + } + end + + let(:worker) { TestWrappedWorker.new } + let(:labels) { default_labels.merge(urgency: "") } + end + end + context "when workers are attributed" do def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category) klass = Class.new do diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb index 44bfaf4cc3c..e58e41d3e4f 100644 --- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb @@ -198,6 +198,28 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do it_behaves_like "a metrics middleware" end + context "when a worker is wrapped into ActiveJob" do + before do + stub_const('TestWrappedWorker', Class.new) + TestWrappedWorker.class_eval do + include Sidekiq::Worker + end + end + + let(:job) do + { + "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper, + "wrapped" => TestWrappedWorker + } + end + + let(:worker) { TestWrappedWorker.new } + let(:worker_class) { TestWrappedWorker } + let(:labels) { default_labels.merge(urgency: "") } + + it_behaves_like "a metrics middleware" + end + context "when workers are attributed" do def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category) Class.new do 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..7a0a4f0cc46 100644 --- a/spec/lib/gitlab/tracking/standard_context_spec.rb +++ b/spec/lib/gitlab/tracking/standard_context_spec.rb @@ -9,47 +9,48 @@ 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 '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 - end - context 'with extra data' do - subject { described_class.new(foo: 'bar') } - - it 'creates a Snowplow context with the given data' do - expect(snowplow_context.to_json.dig(:data, :foo)).to eq('bar') + context 'development or test' do + include_examples 'contains environment', 'development' end - end - context 'with namespace' do - subject { described_class.new(namespace: namespace) } + context 'staging' do + before do + allow(Gitlab).to receive(:staging?).and_return(true) + end - 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 be_nil + include_examples 'contains environment', 'staging' end - end - context 'with project' do - subject { described_class.new(project: project) } + context 'production' do + before do + allow(Gitlab).to receive(:com_and_canary?).and_return(true) + end - 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) + include_examples 'contains environment', 'production' end end - context 'with project and namespace' do - subject { described_class.new(namespace: namespace, project: project) } + it 'contains source' do + expect(snowplow_context.to_json.dig(:data, :source)).to eq(described_class::GITLAB_RAILS_SOURCE) + end + + context 'with extra data' do + subject { described_class.new(foo: 'bar') } - 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 with the given data' do + expect(snowplow_context.to_json.dig(:data, :foo)).to eq('bar') end end + + it 'does not contain any ids' do + expect(snowplow_context.to_json[:data].keys).not_to include(:user_id, :project_id, :namespace_id) + end end end diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb index 8f1fd49f4c5..80740c8112e 100644 --- a/spec/lib/gitlab/tracking_spec.rb +++ b/spec/lib/gitlab/tracking_spec.rb @@ -42,36 +42,31 @@ RSpec.describe Gitlab::Tracking do end shared_examples 'delegates to destination' do |klass| - context 'with standard context' do - it "delegates to #{klass} destination" do - expect_any_instance_of(klass).to receive(:event) do |_, category, action, args| - expect(category).to eq('category') - expect(action).to eq('action') - expect(args[:label]).to eq('label') - expect(args[:property]).to eq('property') - expect(args[:value]).to eq(1.5) - expect(args[:context].length).to eq(1) - expect(args[:context].first.to_json[:schema]).to eq(Gitlab::Tracking::StandardContext::GITLAB_STANDARD_SCHEMA_URL) - expect(args[:context].first.to_json[:data]).to include(foo: 'bar') - end + it "delegates to #{klass} destination" do + other_context = double(:context) - described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5, - standard_context: Gitlab::Tracking::StandardContext.new(foo: 'bar')) - end - end + project = double(:project) + user = double(:user) + namespace = double(:namespace) - context 'without standard context' do - it "delegates to #{klass} destination" do - expect_any_instance_of(klass).to receive(:event) do |_, category, action, args| - expect(category).to eq('category') - expect(action).to eq('action') - expect(args[:label]).to eq('label') - expect(args[:property]).to eq('property') - expect(args[:value]).to eq(1.5) - end + expect(Gitlab::Tracking::StandardContext) + .to receive(:new) + .with(project: project, user: user, namespace: namespace) + .and_call_original - described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5) + expect_any_instance_of(klass).to receive(:event) do |_, category, action, args| + expect(category).to eq('category') + expect(action).to eq('action') + expect(args[:label]).to eq('label') + expect(args[:property]).to eq('property') + expect(args[:value]).to eq(1.5) + expect(args[:context].length).to eq(2) + expect(args[:context].first).to eq(other_context) + expect(args[:context].last.to_json[:schema]).to eq(Gitlab::Tracking::StandardContext::GITLAB_STANDARD_SCHEMA_URL) end + + described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5, + context: [other_context], project: project, user: user, namespace: namespace) 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..0677aa2d9d7 --- /dev/null +++ b/spec/lib/gitlab/usage/docs/renderer_spec.rb @@ -0,0 +1,22 @@ +# 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) + .map { |text| text.sub('<code>', '').sub('</code>', '') } + + 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..7002c76a7cf --- /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 + :product_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..8b592838f5d 100644 --- a/spec/lib/gitlab/usage/metric_definition_spec.rb +++ b/spec/lib/gitlab/usage/metric_definition_spec.rb @@ -5,18 +5,14 @@ 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', + product_stage: 'growth', status: 'data_available', default_generation: 'generation_1', - full_path: { - generation_1: 'uuid', - generation_2: 'license.uuid' - }, - group: 'group::product analytics', + key_path: 'uuid', + product_group: 'group::product analytics', time_frame: 'none', data_source: 'database', distribution: %w(ee ce), @@ -44,13 +40,12 @@ 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 - :group | nil + :key_path | nil + :product_group | nil :time_frame | nil :time_frame | '29d' :data_source | 'other' @@ -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..5469ded18f9 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb @@ -0,0 +1,281 @@ +# 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' } + let(:end_date) { Date.current } + let(:sources) { Gitlab::Usage::Metrics::Aggregates::Sources } + + let_it_be(:recorded_at) { Time.current.to_i } + + context 'aggregated_metrics_data' do + shared_examples 'aggregated_metrics_data' do + context 'no aggregated metric 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 disabled database_sourced_aggregated_metrics feature flag' do + before do + stub_feature_flags(database_sourced_aggregated_metrics: false) + end + + let(:aggregated_metrics) do + [ + { name: 'gmau_1', source: 'redis', events: %w[event3 event5], operator: "OR" }, + { name: 'gmau_2', source: 'database', events: %w[event1 event2 event3], operator: "OR" } + ].map(&:with_indifferent_access) + end + + it 'skips database sourced metrics', :aggregate_failures do + results = { + 'gmau_1' => 5 + } + + params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at } + + expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event3 event5])).and_return(5) + expect(sources::PostgresHll).not_to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3])) + expect(aggregated_metrics_data).to eq(results) + end + end + + context 'with AND operator' do + let(:aggregated_metrics) do + [ + { name: 'gmau_1', source: 'redis', events: %w[event3 event5], operator: "AND" }, + { name: 'gmau_2', source: 'database', events: %w[event1 event2 event3], operator: "AND" } + ].map(&:with_indifferent_access) + end + + it 'returns the number of unique events recorded for every metric in aggregate', :aggregate_failures do + results = { + 'gmau_1' => 2, + 'gmau_2' => 1 + } + params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at } + + # gmau_1 data is as follow + # |A| => 4 + expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event3')).and_return(4) + # |B| => 6 + expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event5')).and_return(6) + # |A + B| => 8 + expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event3 event5])).and_return(8) + # Exclusion inclusion principle formula to calculate intersection of 2 sets + # |A & B| = (|A| + |B|) - |A + B| => (4 + 6) - 8 => 2 + + # gmau_2 data is as follow: + # |A| => 2 + expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event1')).and_return(2) + # |B| => 3 + expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event2')).and_return(3) + # |C| => 5 + expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event3')).and_return(5) + + # |A + B| => 4 therefore |A & B| = (|A| + |B|) - |A + B| => 2 + 3 - 4 => 1 + expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2])).and_return(4) + # |A + C| => 6 therefore |A & C| = (|A| + |C|) - |A + C| => 2 + 5 - 6 => 1 + expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event3])).and_return(6) + # |B + C| => 7 therefore |B & C| = (|B| + |C|) - |B + C| => 3 + 5 - 7 => 1 + expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event2 event3])).and_return(7) + # |A + B + C| => 8 + expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3])).and_return(8) + # Exclusion inclusion principle formula to calculate intersection of 3 sets + # |A & B & C| = (|A & B| + |A & C| + |B & C|) - (|A| + |B| + |C|) + |A + B + C| + # (1 + 1 + 1) - (2 + 3 + 5) + 8 => 1 + + expect(aggregated_metrics_data).to eq(results) + end + end + + context 'with OR operator' do + let(:aggregated_metrics) do + [ + { name: 'gmau_1', source: 'redis', events: %w[event3 event5], operator: "OR" }, + { name: 'gmau_2', source: 'database', events: %w[event1 event2 event3], operator: "OR" } + ].map(&:with_indifferent_access) + end + + it 'returns the number of unique events occurred for any metric in aggregate', :aggregate_failures do + results = { + 'gmau_1' => 5, + 'gmau_2' => 3 + } + params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at } + + expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event3 event5])).and_return(5) + expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3])).and_return(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', source: 'redis', events: %w[event3_slot event5_slot], operator: "OR" }, + # represents new aggregated metric that is under performance testing on gitlab.com + { name: 'gmau_enabled', source: 'redis', events: %w[event4], operator: "OR", 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', source: 'redis', events: %w[event4], operator: "OR", feature_flag: disabled_feature_flag } + ].map(&:with_indifferent_access) + end + + it 'does not calculate data for aggregates with ff turned off' do + skip_feature_flags_yaml_validation + skip_default_enabled_yaml_check + stub_feature_flags(enabled_feature_flag => true, disabled_feature_flag => false) + allow(sources::RedisHll).to receive(:calculate_metrics_union).and_return(6) + + expect(aggregated_metrics_data).to eq('gmau_without_ff' => 6, 'gmau_enabled' => 6) + 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', source: 'redis', events: %w[event1_slot], operator: "SUM" }]) + end + + expect { aggregated_metrics_data }.to raise_error Gitlab::Usage::Metrics::Aggregates::UnknownAggregationOperator + end + + it 'raises error when unknown aggregation source is used' do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics) + .and_return([{ name: 'gmau_1', source: 'whoami', events: %w[event1_slot], operator: "AND" }]) + end + + expect { aggregated_metrics_data }.to raise_error Gitlab::Usage::Metrics::Aggregates::UnknownAggregationSource + 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', source: 'redis', 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', source: 'redis', events: %w[event1_slot], operator: "SUM" }]) + end + + expect(aggregated_metrics_data).to eq('gmau_1' => -1) + end + + it 'rescues unknown aggregation source error' do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics) + .and_return([{ name: 'gmau_1', source: 'whoami', events: %w[event1_slot], operator: "AND" }]) + 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', source: 'redis', events: %w[event1_slot], operator: "OR" }]) + end + + expect(aggregated_metrics_data).to eq('gmau_1' => -1) + end + end + end + end + + it 'allows for YAML aliases in aggregated metrics configs' do + expect(YAML).to receive(:safe_load).with(kind_of(String), aliases: true) + + described_class.new(recorded_at) + end + + describe '.aggregated_metrics_weekly_data' do + subject(:aggregated_metrics_data) { described_class.new(recorded_at).weekly_data } + + let(:start_date) { 7.days.ago.to_date } + + it_behaves_like 'aggregated_metrics_data' + end + + describe '.aggregated_metrics_monthly_data' do + subject(:aggregated_metrics_data) { described_class.new(recorded_at).monthly_data } + + let(:start_date) { 4.weeks.ago.to_date } + + it_behaves_like 'aggregated_metrics_data' + + context 'metrics union calls' do + let(:aggregated_metrics) do + [ + { name: 'gmau_3', source: 'redis', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" } + ].map(&:with_indifferent_access) + end + + it 'caches intermediate operations', :aggregate_failures do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics) + end + + params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at } + + aggregated_metrics[0][:events].each do |event| + expect(sources::RedisHll).to receive(:calculate_metrics_union) + .with(params.merge(metric_names: event)) + .once + .and_return(0) + end + + 2.upto(4) do |subset_size| + aggregated_metrics[0][:events].combination(subset_size).each do |events| + expect(sources::RedisHll).to receive(:calculate_metrics_union) + .with(params.merge(metric_names: events)) + .once + .and_return(0) + end + end + + aggregated_metrics_data + end + end + end + end +end diff --git a/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb new file mode 100644 index 00000000000..7b8be8e8bc6 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::PostgresHll, :clean_gitlab_redis_shared_state do + let_it_be(:start_date) { 7.days.ago } + let_it_be(:end_date) { Date.current } + let_it_be(:recorded_at) { Time.current } + let_it_be(:time_period) { { created_at: (start_date..end_date) } } + let(:metric_1) { 'metric_1' } + let(:metric_2) { 'metric_2' } + let(:metric_names) { [metric_1, metric_2] } + + describe '.calculate_events_union' do + subject(:calculate_metrics_union) do + described_class.calculate_metrics_union(metric_names: metric_names, start_date: start_date, end_date: end_date, recorded_at: recorded_at) + end + + before do + [ + { + metric_name: metric_1, + time_period: time_period, + recorded_at_timestamp: recorded_at, + data: ::Gitlab::Database::PostgresHll::Buckets.new(141 => 1, 56 => 1) + }, + { + metric_name: metric_2, + time_period: time_period, + recorded_at_timestamp: recorded_at, + data: ::Gitlab::Database::PostgresHll::Buckets.new(10 => 1, 56 => 1) + } + ].each do |params| + described_class.save_aggregated_metrics(**params) + end + end + + it 'returns the number of unique events in the union of all metrics' do + expect(calculate_metrics_union.round(2)).to eq(3.12) + end + + context 'when there is no aggregated data saved' do + let(:metric_names) { [metric_1, 'i do not have any records'] } + + it 'raises error when union data is missing' do + expect { calculate_metrics_union }.to raise_error Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable + end + end + + context 'when there is only one metric defined as aggregated' do + let(:metric_names) { [metric_1] } + + it 'returns the number of unique events for that metric' do + expect(calculate_metrics_union.round(2)).to eq(2.08) + end + end + end + + describe '.save_aggregated_metrics' do + subject(:save_aggregated_metrics) do + described_class.save_aggregated_metrics(metric_name: metric_1, + time_period: time_period, + recorded_at_timestamp: recorded_at, + data: data) + 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 + Gitlab::Redis::SharedState.with do |redis| + expect(redis).to receive(:set).with("#{metric_1}_weekly-#{recorded_at.to_i}", '{"141":1,"56":1}', ex: 120.hours) + end + + save_aggregated_metrics + end + + context 'with monthly key' do + let_it_be(:start_date) { 4.weeks.ago } + let_it_be(:time_period) { { created_at: (start_date..end_date) } } + + it 'persists serialized data in Redis' do + Gitlab::Redis::SharedState.with do |redis| + expect(redis).to receive(:set).with("#{metric_1}_monthly-#{recorded_at.to_i}", '{"141":1,"56":1}', ex: 120.hours) + end + + save_aggregated_metrics + end + end + + context 'with all_time key' do + let_it_be(:time_period) { nil } + + it 'persists serialized data in Redis' do + Gitlab::Redis::SharedState.with do |redis| + expect(redis).to receive(:set).with("#{metric_1}_all_time-#{recorded_at.to_i}", '{"141":1,"56":1}', ex: 120.hours) + end + + save_aggregated_metrics + end + 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 { save_aggregated_metrics }.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 { save_aggregated_metrics }.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 + + save_aggregated_metrics + end + end + + it 'raises error for development environment' do + expect { save_aggregated_metrics }.to raise_error /Unsupported data type/ + end + end + end +end diff --git a/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb new file mode 100644 index 00000000000..af2de5ea343 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::RedisHll do + describe '.calculate_events_union' do + let(:event_names) { %w[event_a event_b] } + let(:start_date) { 7.days.ago } + let(:end_date) { Date.current } + + subject(:calculate_metrics_union) do + described_class.calculate_metrics_union(metric_names: event_names, start_date: start_date, end_date: end_date, recorded_at: nil) + end + + it 'calls Gitlab::UsageDataCounters::HLLRedisCounter.calculate_events_union' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union) + .with(event_names: event_names, start_date: start_date, end_date: end_date) + .and_return(5) + + calculate_metrics_union + end + + it 'prevents from using fallback value as valid union result' do + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union).and_return(-1) + + expect { calculate_metrics_union }.to raise_error Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable + 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..58f974fbe12 100644 --- a/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb @@ -13,18 +13,32 @@ RSpec.describe 'aggregated metrics' do end end + RSpec::Matchers.define :has_known_source do + match do |aggregate| + Gitlab::Usage::Metrics::Aggregates::SOURCES.include?(aggregate[:source]) + end + + failure_message do |aggregate| + "Aggregate with name: `#{aggregate[:name]}` uses not allowed source `#{aggregate[:source]}`" + end + end + let_it_be(:known_events) do Gitlab::UsageDataCounters::HLLRedisCounter.known_events end - Gitlab::UsageDataCounters::HLLRedisCounter.aggregated_metrics.tap do |aggregated_metrics| + Gitlab::Usage::Metrics::Aggregates::Aggregate.new(Time.current).send(:aggregated_metrics).tap do |aggregated_metrics| it 'all events has unique name' do event_names = aggregated_metrics&.map { |event| event[:name] } expect(event_names).to eq(event_names&.uniq) end - aggregated_metrics&.each do |aggregate| + it 'all aggregated metrics has known source' do + expect(aggregated_metrics).to all has_known_source + end + + aggregated_metrics&.select { |agg| agg[:source] == Gitlab::Usage::Metrics::Aggregates::REDIS_SOURCE }&.each do |aggregate| context "for #{aggregate[:name]} aggregate of #{aggregate[:events].join(' ')}" do let_it_be(:events_records) { known_events.select { |event| aggregate[:events].include?(event[:name]) } } @@ -37,7 +51,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/ci_template_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb index ba7bfe47bc9..b1d5d106082 100644 --- a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb @@ -3,28 +3,88 @@ require 'spec_helper' RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter do - let(:project_id) { 1 } - describe '.track_unique_project_event' do - described_class::TEMPLATE_TO_EVENT.keys.each do |template| - context "when given template #{template}" do - it_behaves_like 'tracking unique hll events', :usage_data_track_ci_templates_unique_projects do - subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template) } + using RSpec::Parameterized::TableSyntax + + where(:template, :config_source, :expected_event) do + # Implicit Auto DevOps usage + 'Auto-DevOps.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops' + 'Jobs/Build.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_build' + 'Jobs/Deploy.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_deploy' + 'Security/SAST.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_sast' + 'Security/Secret-Detection.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_secret_detection' + # Explicit include:template usage + '5-Minute-Production-App.gitlab-ci.yml' | :repository_source | 'p_ci_templates_5_min_production_app' + 'Auto-DevOps.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops' + 'AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_cf_deploy_ec2' + 'AWS/Deploy-ECS.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_deploy_ecs' + 'Jobs/Build.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_build' + 'Jobs/Deploy.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy' + 'Jobs/Deploy.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy_latest' + 'Security/SAST.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_sast' + 'Security/Secret-Detection.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_secret_detection' + 'Terraform/Base.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_terraform_base_latest' + end + + with_them do + it_behaves_like 'tracking unique hll events' do + subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source) } + + let(:project_id) { 1 } + let(:target_id) { expected_event } + let(:expected_type) { instance_of(Integer) } + end + end + + context 'known_events coverage tests' do + let(:project_id) { 1 } + let(:config_source) { :repository_source } - let(:target_id) { "p_ci_templates_#{described_class::TEMPLATE_TO_EVENT[template]}" } - let(:expected_type) { instance_of(Integer) } + # These tests help guard against missing "explicit" events in known_events/ci_templates.yml + context 'explicit include:template events' do + described_class::TEMPLATE_TO_EVENT.keys.each do |template| + it "does not raise error for #{template}" do + expect do + described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source) + end.not_to raise_error + end + end + end + + # This test is to help guard against missing "implicit" events in known_events/ci_templates.yml + it 'does not raise error for any template in an implicit Auto DevOps pipeline' do + project = create(:project, :auto_devops) + pipeline = double(project: project) + command = double + result = Gitlab::Ci::YamlProcessor.new( + Gitlab::Ci::Pipeline::Chain::Config::Content::AutoDevops.new(pipeline, command).content, + project: project, + user: double, + sha: double + ).execute + + config_source = :auto_devops_source + + result.included_templates.each do |template| + expect do + described_class.track_unique_project_event(project_id: project.id, template: template, config_source: config_source) + end.not_to raise_error end end end - it 'does not track templates outside of TEMPLATE_TO_EVENT' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to( - receive(:track_event) - ) + context 'templates outside of TEMPLATE_TO_EVENT' do + let(:project_id) { 1 } + let(:config_source) { :repository_source } + Dir.glob(File.join('lib', 'gitlab', 'ci', 'templates', '**'), base: Rails.root) do |template| next if described_class::TEMPLATE_TO_EVENT.key?(template) - described_class.track_unique_project_event(project_id: 1, template: template) + it "does not track #{template}" do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to(receive(:track_event)) + + described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source) + end end end end 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..b4894ec049f 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 @@ -27,6 +27,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'deploy_token_packages', 'user_packages', 'compliance', + 'ecosystem', 'analytics', 'ide_edit', 'search', @@ -39,12 +40,16 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'snippets', 'code_review', 'terraform', - 'ci_templates' + 'ci_templates', + 'quickactions', + 'pipeline_authoring' ) end end describe 'known_events' do + let(:feature) { 'test_hll_redis_counter_ff_check' } + let(:weekly_event) { 'g_analytics_contribution' } let(:daily_event) { 'g_analytics_search' } let(:analytics_slot_event) { 'g_analytics_contribution' } @@ -64,7 +69,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s let(:known_events) do [ - { name: weekly_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "weekly" }, + { name: weekly_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "weekly", feature_flag: feature }, { name: daily_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "daily" }, { name: category_productivity_event, redis_slot: "analytics", category: productivity_category, aggregation: "weekly" }, { name: compliance_slot_event, redis_slot: "compliance", category: compliance_category, aggregation: "weekly" }, @@ -75,6 +80,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s end before do + skip_feature_flags_yaml_validation + skip_default_enabled_yaml_check allow(described_class).to receive(:known_events).and_return(known_events) end @@ -85,6 +92,32 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s end describe '.track_event' do + context 'with feature flag set' do + it 'tracks the event when feature enabled' do + stub_feature_flags(feature => true) + + expect(Gitlab::Redis::HLL).to receive(:add) + + described_class.track_event(weekly_event, values: 1) + end + + it 'does not track the event with feature flag disabled' do + stub_feature_flags(feature => false) + + expect(Gitlab::Redis::HLL).not_to receive(:add) + + described_class.track_event(weekly_event, values: 1) + end + end + + context 'with no feature flag set' do + it 'tracks the event' do + expect(Gitlab::Redis::HLL).to receive(:add) + + described_class.track_event(daily_event, values: 1) + end + end + context 'when usage_ping is disabled' do it 'does not track the event' do stub_application_setting(usage_ping_enabled: false) @@ -425,182 +458,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..a604de4a61f 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,54 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl end end + describe '.track_approve_mr_action' do + subject { described_class.track_approve_mr_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_APPROVE_ACTION } + end + end + + describe '.track_unapprove_mr_action' do + subject { described_class.track_unapprove_mr_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_UNAPPROVE_ACTION } + 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_title_edit_action' do + subject { described_class.track_title_edit_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_EDIT_MR_TITLE_ACTION } + end + end + + describe '.track_description_edit_action' do + subject { described_class.track_description_edit_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_EDIT_MR_DESC_ACTION } + end + end + describe '.track_create_comment_action' do subject { described_class.track_create_comment_action(note: note) } @@ -148,4 +196,92 @@ 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_marked_as_draft_action' do + subject { described_class.track_marked_as_draft_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_MARKED_AS_DRAFT_ACTION } + end + end + + describe '.track_unmarked_as_draft_action' do + subject { described_class.track_unmarked_as_draft_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_UNMARKED_AS_DRAFT_ACTION } + end + end + + describe '.track_task_item_status_changed' do + subject { described_class.track_task_item_status_changed(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_TASK_ITEM_STATUS_CHANGED_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 + + describe '.track_approval_rule_added_action' do + subject { described_class.track_approval_rule_added_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_APPROVAL_RULE_ADDED_USERS_ACTION } + end + end + + describe '.track_approval_rule_edited_action' do + subject { described_class.track_approval_rule_edited_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_APPROVAL_RULE_EDITED_USERS_ACTION } + end + end + + describe '.track_approval_rule_deleted_action' do + subject { described_class.track_approval_rule_deleted_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_APPROVAL_RULE_DELETED_USERS_ACTION } + end + end + + describe '.track_mr_create_from_issue' do + subject { described_class.track_mr_create_from_issue(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_CREATE_FROM_ISSUE_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_counters/vs_code_extenion_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/vs_code_extenion_activity_unique_counter_spec.rb new file mode 100644 index 00000000000..7593d51fe76 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/vs_code_extenion_activity_unique_counter_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'a tracked vs code unique action' do |event| + before do + stub_application_setting(usage_ping_enabled: true) + end + + def count_unique(date_from:, date_to:) + Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to) + end + + it 'tracks when the user agent is from vs code' do + aggregate_failures do + user_agent = { user_agent: 'vs-code-gitlab-workflow/3.11.1 VSCode/1.52.1 Node.js/12.14.1 (darwin; x64)' } + + expect(track_action(user: user1, **user_agent)).to be_truthy + expect(track_action(user: user1, **user_agent)).to be_truthy + expect(track_action(user: user2, **user_agent)).to be_truthy + + expect(count_unique(date_from: time - 1.week, date_to: time + 1.week)).to eq(2) + end + end + + it 'does not track when the user agent is not from vs code' do + aggregate_failures do + user_agent = { user_agent: 'normal_user_agent' } + + expect(track_action(user: user1, **user_agent)).to be_falsey + expect(track_action(user: user1, **user_agent)).to be_falsey + expect(track_action(user: user2, **user_agent)).to be_falsey + + expect(count_unique(date_from: time - 1.week, date_to: time + 1.week)).to eq(0) + end + end + + it 'does not track if user agent is not present' do + expect(track_action(user: nil, user_agent: nil)).to be_nil + end + + it 'does not track if user is not present' do + user_agent = { user_agent: 'vs-code-gitlab-workflow/3.11.1 VSCode/1.52.1 Node.js/12.14.1 (darwin; x64)' } + + expect(track_action(user: nil, **user_agent)).to be_nil + end +end + +RSpec.describe Gitlab::UsageDataCounters::VSCodeExtensionActivityUniqueCounter, :clean_gitlab_redis_shared_state do + let(:user1) { build(:user, id: 1) } + let(:user2) { build(:user, id: 2) } + let(:time) { Time.current } + + context 'when tracking a vs code api request' do + it_behaves_like 'a tracked vs code unique action' do + let(:action) { described_class::VS_CODE_API_REQUEST_ACTION } + + def track_action(params) + described_class.track_api_request_when_trackable(**params) + 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..e964e695828 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 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/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 9662ad13631..c22df5dd063 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -15,9 +15,7 @@ RSpec.describe Gitlab::Workhorse do end before do - allow(Feature::Gitaly).to receive(:server_feature_flags).and_return({ - 'gitaly-feature-foobar' => 'true' - }) + stub_feature_flags(gitaly_enforce_requests_limits: true) end describe ".send_git_archive" do @@ -43,7 +41,7 @@ RSpec.describe Gitlab::Workhorse do expect(command).to eq('git-archive') expect(params).to eq({ 'GitalyServer' => { - features: { 'gitaly-feature-foobar' => 'true' }, + features: { 'gitaly-feature-enforce-requests-limits' => 'true' }, address: Gitlab::GitalyClient.address(project.repository_storage), token: Gitlab::GitalyClient.token(project.repository_storage) }, @@ -73,7 +71,7 @@ RSpec.describe Gitlab::Workhorse do expect(command).to eq('git-archive') expect(params).to eq({ 'GitalyServer' => { - features: { 'gitaly-feature-foobar' => 'true' }, + features: { 'gitaly-feature-enforce-requests-limits' => 'true' }, address: Gitlab::GitalyClient.address(project.repository_storage), token: Gitlab::GitalyClient.token(project.repository_storage) }, @@ -124,7 +122,7 @@ RSpec.describe Gitlab::Workhorse do expect(command).to eq("git-format-patch") expect(params).to eq({ 'GitalyServer' => { - features: { 'gitaly-feature-foobar' => 'true' }, + features: { 'gitaly-feature-enforce-requests-limits' => 'true' }, address: Gitlab::GitalyClient.address(project.repository_storage), token: Gitlab::GitalyClient.token(project.repository_storage) }, @@ -187,7 +185,7 @@ RSpec.describe Gitlab::Workhorse do expect(command).to eq("git-diff") expect(params).to eq({ 'GitalyServer' => { - features: { 'gitaly-feature-foobar' => 'true' }, + features: { 'gitaly-feature-enforce-requests-limits' => 'true' }, address: Gitlab::GitalyClient.address(project.repository_storage), token: Gitlab::GitalyClient.token(project.repository_storage) }, @@ -274,7 +272,7 @@ RSpec.describe Gitlab::Workhorse do let(:gitaly_params) do { GitalyServer: { - features: { 'gitaly-feature-foobar' => 'true' }, + features: { 'gitaly-feature-enforce-requests-limits' => 'true' }, address: Gitlab::GitalyClient.address('default'), token: Gitlab::GitalyClient.token('default') } @@ -310,6 +308,35 @@ RSpec.describe Gitlab::Workhorse do it { is_expected.to include(ShowAllRefs: true) } end + + context 'when a feature flag is set for a single project' do + before do + stub_feature_flags(gitaly_mep_mep: project) + end + + it 'sets the flag to true for that project' do + response = described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action) + + expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true', + 'gitaly-feature-mep-mep' => 'true') + end + + it 'sets the flag to false for other projects' do + other_project = create(:project, :public, :repository) + response = described_class.git_http_ok(other_project.repository, Gitlab::GlRepository::PROJECT, user, action) + + expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true', + 'gitaly-feature-mep-mep' => 'false') + end + + it 'sets the flag to false when there is no project' do + snippet = create(:personal_snippet, :repository) + response = described_class.git_http_ok(snippet.repository, Gitlab::GlRepository::SNIPPET, user, action) + + expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true', + 'gitaly-feature-mep-mep' => 'false') + end + end end context "when git_receive_pack action is passed" do @@ -423,7 +450,7 @@ RSpec.describe Gitlab::Workhorse do expect(command).to eq('git-blob') expect(params).to eq({ 'GitalyServer' => { - features: { 'gitaly-feature-foobar' => 'true' }, + features: { 'gitaly-feature-enforce-requests-limits' => 'true' }, address: Gitlab::GitalyClient.address(project.repository_storage), token: Gitlab::GitalyClient.token(project.repository_storage) }, @@ -485,7 +512,7 @@ RSpec.describe Gitlab::Workhorse do expect(command).to eq('git-snapshot') expect(params).to eq( 'GitalyServer' => { - 'features' => { 'gitaly-feature-foobar' => 'true' }, + 'features' => { 'gitaly-feature-enforce-requests-limits' => 'true' }, 'address' => Gitlab::GitalyClient.address(project.repository_storage), 'token' => Gitlab::GitalyClient.token(project.repository_storage) }, 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/object_storage/config_spec.rb b/spec/lib/object_storage/config_spec.rb index 0ead2a1d269..1361d80fe75 100644 --- a/spec/lib/object_storage/config_spec.rb +++ b/spec/lib/object_storage/config_spec.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' require 'rspec-parameterized' -require 'fog/core' RSpec.describe ObjectStorage::Config do using RSpec::Parameterized::TableSyntax @@ -34,7 +33,9 @@ RSpec.describe ObjectStorage::Config do } end - subject { described_class.new(raw_config.as_json) } + subject do + described_class.new(raw_config.as_json) + end describe '#load_provider' do before do @@ -45,6 +46,10 @@ RSpec.describe ObjectStorage::Config do it 'registers AWS as a provider' do expect(Fog.providers.keys).to include(:aws) end + + describe '#fog_connection' do + it { expect(subject.fog_connection).to be_a_kind_of(Fog::AWS::Storage::Real) } + end end context 'with Google' do @@ -59,6 +64,10 @@ RSpec.describe ObjectStorage::Config do it 'registers Google as a provider' do expect(Fog.providers.keys).to include(:google) end + + describe '#fog_connection' do + it { expect(subject.fog_connection).to be_a_kind_of(Fog::Storage::GoogleXML::Real) } + end end context 'with Azure' do @@ -73,6 +82,10 @@ RSpec.describe ObjectStorage::Config do it 'registers AzureRM as a provider' do expect(Fog.providers.keys).to include(:azurerm) end + + describe '#fog_connection' do + it { expect(subject.fog_connection).to be_a_kind_of(Fog::Storage::AzureRM::Real) } + end end end @@ -170,6 +183,50 @@ RSpec.describe ObjectStorage::Config do it { expect(subject.provider).to eq('AWS') } it { expect(subject.aws?).to be true } it { expect(subject.google?).to be false } + + it 'returns the default S3 endpoint' do + subject.load_provider + + expect(subject.s3_endpoint).to eq("https://test-bucket.s3.amazonaws.com") + end + + describe 'with a custom endpoint' do + let(:endpoint) { 'https://my.example.com' } + + before do + credentials[:endpoint] = endpoint + end + + it 'returns the custom endpoint' do + subject.load_provider + + expect(subject.s3_endpoint).to eq(endpoint) + end + end + + context 'with custom S3 host and port' do + where(:host, :port, :scheme, :expected) do + 's3.example.com' | 8080 | nil | 'https://test-bucket.s3.example.com:8080' + 's3.example.com' | 443 | nil | 'https://test-bucket.s3.example.com' + 's3.example.com' | 443 | "https" | 'https://test-bucket.s3.example.com' + 's3.example.com' | nil | nil | 'https://test-bucket.s3.example.com' + 's3.example.com' | 80 | "http" | 'http://test-bucket.s3.example.com' + 's3.example.com' | "bogus" | nil | nil + end + + with_them do + before do + credentials[:host] = host + credentials[:port] = port + credentials[:scheme] = scheme + subject.load_provider + end + + it 'returns expected host' do + expect(subject.s3_endpoint).to eq(expected) + end + end + end end context 'with Google credentials' do 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..98c4f771f33 --- /dev/null +++ b/spec/lib/peek/views/external_http_spec.rb @@ -0,0 +1,215 @@ +# 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 + { + 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 + { + 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 + { + 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(double(:event, payload: event_1)) + subscriber.request(double(:event, payload: event_2)) + subscriber.request(double(:event, payload: 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 + before do + event_1[:host] = '1.2.3.4' + end + + it 'displays IPv4 in the label' do + subscriber.request(double(:event, payload: event_1)) + + expect(subject.results[:details]).to contain_exactly( + a_hash_including( + 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 + before do + event_1[:host] = '2606:4700:90:0:f22e:fbec:5bed:a9b9' + end + + it 'displays IPv6 in the label' do + subscriber.request(double(:event, payload: event_1)) + + expect(subject.results[:details]).to contain_exactly( + a_hash_including( + 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 query is a hash' do + before do + event_1[:query] = { current: true, 'item1' => 'string', 'item2' => [1, 2] } + end + + it 'converts query hash into a query string' do + subscriber.request(double(:event, payload: event_1)) + + expect(subject.results[:details]).to contain_exactly( + a_hash_including( + duration: 30.0, + label: "POST https://gitlab.com:80/api/v4/projects?current=true&item1=string&item2%5B%5D=1&item2%5B%5D=2", + code: "Response status: 200", + proxy: nil, + error: nil, + warnings: [] + ) + ) + end + end + + context 'when the host is invalid' do + before do + event_1[:host] = '!@#%!@#%!@#%' + end + + it 'displays unknown in the label' do + subscriber.request(double(:event, payload: event_1)) + + expect(subject.results[:details]).to contain_exactly( + a_hash_including( + duration: 30.0, + label: "POST unknown", + code: "Response status: 200", + proxy: nil, + error: nil, + warnings: [] + ) + ) + end + end + + context 'when URI creation raises an URI::Error' do + before do + # This raises an URI::Error exception + event_1[:port] = 'invalid' + end + + it 'displays unknown in the label' do + subscriber.request(double(:event, payload: event_1)) + + expect(subject.results[:details]).to contain_exactly( + a_hash_including( + duration: 30.0, + label: "POST unknown", + code: "Response status: 200", + proxy: nil, + error: nil, + warnings: [] + ) + ) + end + end + + context 'when URI creation raises a StandardError exception' do + before do + # This raises a TypeError exception + event_1[:scheme] = 1234 + end + + it 'displays unknown in the label' do + subscriber.request(double(:event, payload: event_1)) + + expect(subject.results[:details]).to contain_exactly( + a_hash_including( + 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 |