diff options
Diffstat (limited to 'spec/lib')
304 files changed, 11669 insertions, 2318 deletions
diff --git a/spec/lib/api/entities/job_request/image_spec.rb b/spec/lib/api/entities/job_request/image_spec.rb new file mode 100644 index 00000000000..092c181ae9c --- /dev/null +++ b/spec/lib/api/entities/job_request/image_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Entities::JobRequest::Image do + let(:ports) { [{ number: 80, protocol: 'http', name: 'name' }]} + let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports)} + let(:entity) { described_class.new(image) } + + subject { entity.as_json } + + it 'returns the image name' do + expect(subject[:name]).to eq 'image_name' + end + + it 'returns the entrypoint' do + expect(subject[:entrypoint]).to eq ['foo'] + end + + it 'returns the ports' do + expect(subject[:ports]).to eq ports + end + + context 'when the ports param is nil' do + let(:ports) { nil } + + it 'does not return the ports' do + expect(subject[:ports]).to be_nil + end + end +end diff --git a/spec/lib/api/entities/job_request/port_spec.rb b/spec/lib/api/entities/job_request/port_spec.rb new file mode 100644 index 00000000000..40ab4cd6231 --- /dev/null +++ b/spec/lib/api/entities/job_request/port_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::API::Entities::JobRequest::Port do + let(:port) { double(number: 80, protocol: 'http', name: 'name')} + let(:entity) { described_class.new(port) } + + subject { entity.as_json } + + it 'returns the port number' do + expect(subject[:number]).to eq 80 + end + + it 'returns if the port protocol' do + expect(subject[:protocol]).to eq 'http' + end + + it 'returns the port name' do + expect(subject[:name]).to eq 'name' + end +end diff --git a/spec/lib/api/helpers/custom_validators_spec.rb b/spec/lib/api/helpers/custom_validators_spec.rb index 41e6fb47b11..aed86b21cb7 100644 --- a/spec/lib/api/helpers/custom_validators_spec.rb +++ b/spec/lib/api/helpers/custom_validators_spec.rb @@ -21,7 +21,7 @@ describe API::Helpers::CustomValidators do end context 'invalid parameters' do - it 'should raise a validation error' do + it 'raises a validation error' do expect_validation_error({ 'test' => 'some_value' }) end end @@ -44,7 +44,30 @@ describe API::Helpers::CustomValidators do end context 'invalid parameters' do - it 'should raise a validation error' do + it 'raises a validation error' do + expect_validation_error({ 'test' => 'some_other_string' }) + end + end + end + + describe API::Helpers::CustomValidators::ArrayNoneAny do + subject do + described_class.new(['test'], {}, false, scope.new) + end + + context 'valid parameters' do + it 'does not raise a validation error' do + expect_no_validation_error({ 'test' => [] }) + expect_no_validation_error({ 'test' => [1, 2, 3] }) + expect_no_validation_error({ 'test' => 'None' }) + expect_no_validation_error({ 'test' => 'Any' }) + expect_no_validation_error({ 'test' => 'none' }) + expect_no_validation_error({ 'test' => 'any' }) + end + end + + context 'invalid parameters' do + it 'raises a validation error' do expect_validation_error({ 'test' => 'some_other_string' }) end end diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb index 6e215ea1561..c788da55cd2 100644 --- a/spec/lib/api/helpers/pagination_spec.rb +++ b/spec/lib/api/helpers/pagination_spec.rb @@ -2,8 +2,12 @@ require 'spec_helper' describe API::Helpers::Pagination do let(:resource) { Project.all } - let(:incoming_api_projects_url) { "#{Gitlab.config.gitlab.url}:8080/api/v4/projects" } - let(:canonical_api_projects_url) { "#{Gitlab.config.gitlab.url}/api/v4/projects" } + let(:custom_port) { 8080 } + let(:incoming_api_projects_url) { "#{Gitlab.config.gitlab.url}:#{custom_port}/api/v4/projects" } + + before do + stub_config_setting(port: custom_port) + end subject do Class.new.include(described_class).new @@ -48,7 +52,7 @@ describe API::Helpers::Pagination do it 'adds appropriate headers' do expect_header('X-Per-Page', '2') - expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[1].id).to_query}") + expect_header('X-Next-Page', "#{incoming_api_projects_url}?#{query.merge(ks_prev_id: projects[1].id).to_query}") expect_header('Link', anything) do |_key, val| expect(val).to include('rel="next"') @@ -71,7 +75,7 @@ describe API::Helpers::Pagination do it 'adds appropriate headers' do expect_header('X-Per-Page', '2') - expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[2].id).to_query}") + expect_header('X-Next-Page', "#{incoming_api_projects_url}?#{query.merge(ks_prev_id: projects[2].id).to_query}") expect_header('Link', anything) do |_key, val| expect(val).to include('rel="next"') @@ -171,7 +175,7 @@ describe API::Helpers::Pagination do it 'returns the right link to the next page' do expect_header('X-Per-Page', '2') - expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[6].id, ks_prev_name: projects[6].name).to_query}") + expect_header('X-Next-Page', "#{incoming_api_projects_url}?#{query.merge(ks_prev_id: projects[6].id, ks_prev_name: projects[6].name).to_query}") expect_header('Link', anything) do |_key, val| expect(val).to include('rel="next"') end @@ -224,9 +228,9 @@ describe API::Helpers::Pagination do expect_header('X-Prev-Page', '') expect_header('Link', anything) do |_key, val| - expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first")) - expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last")) - expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next")) + expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first")) + expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last")) + expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next")) expect(val).not_to include('rel="prev"') end @@ -290,8 +294,8 @@ describe API::Helpers::Pagination do expect_header('X-Prev-Page', '') expect_header('Link', anything) do |_key, val| - expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first")) - expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next")) + expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first")) + expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next")) expect(val).not_to include('rel="last"') expect(val).not_to include('rel="prev"') end @@ -318,9 +322,9 @@ describe API::Helpers::Pagination do expect_header('X-Prev-Page', '1') expect_header('Link', anything) do |_key, val| - expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first")) - expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last")) - expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="prev")) + expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first")) + expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last")) + expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="prev")) expect(val).not_to include('rel="next"') end @@ -367,8 +371,8 @@ describe API::Helpers::Pagination do expect_header('X-Prev-Page', '') expect_header('Link', anything) do |_key, val| - expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first")) - expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="last")) + expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first")) + expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="last")) expect(val).not_to include('rel="prev"') expect(val).not_to include('rel="next"') expect(val).not_to include('page=0') diff --git a/spec/lib/api/helpers/related_resources_helpers_spec.rb b/spec/lib/api/helpers/related_resources_helpers_spec.rb index 66af7f81535..99fe8795d91 100644 --- a/spec/lib/api/helpers/related_resources_helpers_spec.rb +++ b/spec/lib/api/helpers/related_resources_helpers_spec.rb @@ -5,6 +5,40 @@ describe API::Helpers::RelatedResourcesHelpers do Class.new.include(described_class).new end + describe '#expose_path' do + let(:path) { '/api/v4/awesome_endpoint' } + + context 'empty relative URL root' do + before do + stub_config_setting(relative_url_root: '') + end + + it 'returns the existing path' do + expect(helpers.expose_path(path)).to eq(path) + end + end + + context 'slash relative URL root' do + before do + stub_config_setting(relative_url_root: '/') + end + + it 'returns the existing path' do + expect(helpers.expose_path(path)).to eq(path) + end + end + + context 'with relative URL root' do + before do + stub_config_setting(relative_url_root: '/gitlab/root') + end + + it 'returns the existing path' do + expect(helpers.expose_path(path)).to eq("/gitlab/root" + path) + end + end + end + describe '#expose_url' do let(:path) { '/api/v4/awesome_endpoint' } subject(:url) { helpers.expose_url(path) } diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb index 08165f147bb..00916f80784 100644 --- a/spec/lib/api/helpers_spec.rb +++ b/spec/lib/api/helpers_spec.rb @@ -137,18 +137,6 @@ describe API::Helpers do it_behaves_like 'user namespace finder' end - describe '#user_namespace' do - let(:namespace_finder) do - subject.user_namespace - end - - before do - allow(subject).to receive(:params).and_return({ id: namespace.id }) - end - - it_behaves_like 'user namespace finder' - end - describe '#send_git_blob' do let(:repository) { double } let(:blob) { double(name: 'foobar') } diff --git a/spec/lib/backup/uploads_spec.rb b/spec/lib/backup/uploads_spec.rb new file mode 100644 index 00000000000..544d3754c0f --- /dev/null +++ b/spec/lib/backup/uploads_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Backup::Uploads do + let(:progress) { StringIO.new } + subject(:backup) { described_class.new(progress) } + + describe '#initialize' do + it 'uses the correct upload dir' do + Dir.mktmpdir do |tmpdir| + FileUtils.mkdir_p("#{tmpdir}/uploads") + + allow(Gitlab.config.uploads).to receive(:storage_path) { tmpdir } + + expect(backup.app_files_dir).to eq("#{tmpdir}/uploads") + end + end + end +end diff --git a/spec/lib/banzai/commit_renderer_spec.rb b/spec/lib/banzai/commit_renderer_spec.rb index 1f53657c59c..316dbf052c3 100644 --- a/spec/lib/banzai/commit_renderer_spec.rb +++ b/spec/lib/banzai/commit_renderer_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Banzai::CommitRenderer do - describe '.render' do + describe '.render', :clean_gitlab_redis_cache do it 'renders a commit description and title' do user = build(:user) project = create(:project, :repository) @@ -13,7 +13,7 @@ describe Banzai::CommitRenderer do described_class::ATTRIBUTES.each do |attr| expect_any_instance_of(Banzai::ObjectRenderer).to receive(:render).with([project.commit], attr).once.and_call_original - expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr, {}) + expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr, { skip_project_check: false }).and_call_original end described_class.render([project.commit], project, user) diff --git a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb index b645e49bd43..5b3f679084e 100644 --- a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb +++ b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb @@ -13,6 +13,6 @@ describe Banzai::Filter::BlockquoteFenceFilter do end it 'allows trailing whitespace on blockquote fence lines' do - expect(filter(">>> \ntest\n>>> ")).to eq("> test") + expect(filter(">>> \ntest\n>>> ")).to eq("\n> test\n") end end diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index a0270d93d50..7c94cf37e32 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -121,6 +121,49 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do end end + context "youtrack project" do + let(:project) { create(:youtrack_project) } + + before do + project.update!(issues_enabled: false) + end + + context "with right markdown" do + let(:issue) { ExternalIssue.new("YT-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with underscores in the prefix" do + let(:issue) { ExternalIssue.new("PRJ_1-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with lowercase letters in the prefix" do + let(:issue) { ExternalIssue.new("YTkPrj-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with a single-letter prefix" do + let(:issue) { ExternalIssue.new("T-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with a lowercase prefix" do + let(:issue) { ExternalIssue.new("gl-030", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + end + context "jira project" do let(:project) { create(:jira_project) } let(:reference) { issue.to_reference } 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 55c41e55437..72dfd6ff9ea 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -30,6 +30,23 @@ describe Banzai::Filter::MergeRequestReferenceFilter do end end + describe 'all references' do + let(:doc) { reference_filter(merge.to_reference) } + let(:tag_el) { doc.css('a').first } + + it 'adds merge request iid' do + expect(tag_el["data-iid"]).to eq(merge.iid.to_s) + end + + it 'adds project data attribute with project id' do + expect(tag_el["data-project-path"]).to eq(project.full_path) + end + + it 'does not add `has-tooltip` class' do + expect(tag_el["class"]).not_to include('has-tooltip') + end + end + context 'internal reference' do let(:reference) { merge.to_reference } @@ -57,9 +74,9 @@ describe Banzai::Filter::MergeRequestReferenceFilter do expect(reference_filter(act).to_html).to eq exp end - it 'includes a title attribute' do + it 'has no title' do doc = reference_filter("Merge #{reference}") - expect(doc.css('a').first.attr('title')).to eq merge.title + expect(doc.css('a').first.attr('title')).to eq "" end it 'escapes the title attribute' do @@ -69,9 +86,9 @@ describe Banzai::Filter::MergeRequestReferenceFilter do expect(doc.text).to eq "Merge #{reference}" end - it 'includes default classes' do + it 'includes default classes, without tooltip' do doc = reference_filter("Merge #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request has-tooltip' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index 4c94e4fdae0..f0a5dc8d0d7 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -295,6 +295,25 @@ describe Banzai::Filter::MilestoneReferenceFilter do end end + shared_examples 'references with HTML entities' do + before do + milestone.update!(title: '<html>') + end + + it 'links to a valid reference' do + doc = reference_filter('See %"<html>"') + + expect(doc.css('a').first.attr('href')).to eq urls.milestone_url(milestone) + expect(doc.text).to eq 'See %<html>' + end + + it 'ignores invalid milestone names and escapes entities' do + act = %(Milestone %"<non valid>") + + expect(reference_filter(act).to_html).to eq act + end + end + shared_context 'project milestones' do let(:reference) { milestone.to_reference(format: :iid) } @@ -307,6 +326,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do it_behaves_like 'cross-project / cross-namespace complete reference' it_behaves_like 'cross-project / same-namespace complete reference' it_behaves_like 'cross project shorthand reference' + it_behaves_like 'references with HTML entities' end shared_context 'group milestones' do @@ -317,6 +337,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do it_behaves_like 'String-based single-word references' it_behaves_like 'String-based multi-word references in quotes' it_behaves_like 'referencing a milestone in a link href' + it_behaves_like 'references with HTML entities' it 'does not support references by IID' do doc = reference_filter("See #{Milestone.reference_prefix}#{milestone.iid}") diff --git a/spec/lib/banzai/filter/output_safety_spec.rb b/spec/lib/banzai/filter/output_safety_spec.rb new file mode 100644 index 00000000000..5ffe591c9a4 --- /dev/null +++ b/spec/lib/banzai/filter/output_safety_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::Filter::OutputSafety do + subject do + Class.new do + include Banzai::Filter::OutputSafety + end.new + end + + let(:content) { '<pre><code>foo</code></pre>' } + + context 'when given HTML is safe' do + let(:html) { content.html_safe } + + it 'returns safe HTML' do + expect(subject.escape_once(html)).to eq(html) + end + end + + context 'when given HTML is not safe' do + let(:html) { content } + + it 'returns escaped HTML' do + expect(subject.escape_once(html)).to eq(ERB::Util.html_escape_once(html)) + end + end +end diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb index 8235c411eb7..6f7acfe7072 100644 --- a/spec/lib/banzai/filter/plantuml_filter_spec.rb +++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Banzai::Filter::PlantumlFilter do include FilterSpecHelper - it 'should replace plantuml pre tag with img tag' do + it 'replaces plantuml pre tag with img tag' do stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080") input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>' @@ -12,7 +12,7 @@ describe Banzai::Filter::PlantumlFilter do expect(doc.to_s).to eq output end - it 'should not replace plantuml pre tag with img tag if disabled' do + it 'does not replace plantuml pre tag with img tag if disabled' do stub_application_setting(plantuml_enabled: false) input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' output = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' @@ -21,7 +21,7 @@ describe Banzai::Filter::PlantumlFilter do expect(doc.to_s).to eq output end - it 'should not replace plantuml pre tag with img tag if url is invalid' do + it 'does not replace plantuml pre tag with img tag if url is invalid' do stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid") input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>' diff --git a/spec/lib/banzai/filter/suggestion_filter_spec.rb b/spec/lib/banzai/filter/suggestion_filter_spec.rb index b13c90b54bd..9c4650b73de 100644 --- a/spec/lib/banzai/filter/suggestion_filter_spec.rb +++ b/spec/lib/banzai/filter/suggestion_filter_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe Banzai::Filter::SuggestionFilter do include FilterSpecHelper - let(:input) { "<pre class='code highlight js-syntax-highlight suggestion'><code>foo\n</code></pre>" } + let(:input) { %(<pre class="code highlight js-syntax-highlight suggestion"><code>foo\n</code></pre>) } let(:default_context) do { suggestions_filter_enabled: true } end @@ -23,4 +23,16 @@ describe Banzai::Filter::SuggestionFilter do expect(result[:class]).to be_nil end + + context 'multi-line suggestions' do + let(:data_attr) { Banzai::Filter::SyntaxHighlightFilter::LANG_PARAMS_ATTR } + let(:input) { %(<pre class="code highlight js-syntax-highlight suggestion" #{data_attr}="-3+2"><code>foo\n</code></pre>) } + + it 'element has correct data-lang-params' do + doc = filter(input, default_context) + pre = doc.css('pre').first + + expect(pre[data_attr]).to eq('-3+2') + end + end end diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb index ef52c572898..80ca7a63435 100644 --- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb +++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb @@ -45,7 +45,10 @@ describe Banzai::Filter::SyntaxHighlightFilter do end context "languages that should be passed through" do - %w(math mermaid plantuml).each do |lang| + let(:delimiter) { described_class::PARAMS_DELIMITER } + let(:data_attr) { described_class::LANG_PARAMS_ATTR } + + %w(math mermaid plantuml suggestion).each do |lang| context "when #{lang} is specified" do it "highlights as plaintext but with the correct language attribute and class" do result = filter(%{<pre><code lang="#{lang}">This is a test</code></pre>}) @@ -55,6 +58,33 @@ describe Banzai::Filter::SyntaxHighlightFilter do include_examples "XSS prevention", lang end + + context "when #{lang} has extra params" do + let(:lang_params) { 'foo-bar-kux' } + + it "includes data-lang-params tag with extra information" do + result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}">This is a test</code></pre>}) + + expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{lang}" lang="#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}) + end + + include_examples "XSS prevention", lang + include_examples "XSS prevention", + "#{lang}#{described_class::PARAMS_DELIMITER}<script>alert(1)</script>" + include_examples "XSS prevention", + "#{lang}#{described_class::PARAMS_DELIMITER}<script>alert(1)</script>" + end + end + + context 'when multiple param delimiters are used' do + let(:lang) { 'suggestion' } + let(:lang_params) { '-1+10' } + + it "delimits on the first appearance" do + result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}#{delimiter}more-things">This is a test</code></pre>}) + + expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{lang}" lang="#{lang}" #{data_attr}="#{lang_params}#{delimiter}more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}) + end end end diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb index 7213cd58ea7..4a9880ac85a 100644 --- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb +++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb @@ -58,6 +58,11 @@ describe Banzai::Filter::TableOfContentsFilter do expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-filled-with-punctuation' end + it 'removes any leading or trailing spaces' do + doc = filter(header(1, " \r\n\tTitle with spaces\r\n\t ")) + expect(doc.css('h1 a').first.attr('href')).to eq '#title-with-spaces' + end + it 'appends a unique number to duplicates' do doc = filter(header(1, 'One') + header(2, 'One')) diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb index b9059b85fdc..cce1cd0b284 100644 --- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb +++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb @@ -70,5 +70,47 @@ describe Banzai::Filter::WikiLinkFilter do expect(filtered_link.attribute('href').value).to eq(invalid_link) end end + + context "when the slug is deemed unsafe or invalid" do + let(:link) { "alert(1);" } + + invalid_slugs = [ + "javascript:", + "JaVaScRiPt:", + "\u0001java\u0003script:", + "javascript :", + "javascript: ", + "javascript : ", + ":javascript:", + "javascript:", + "javascript:", + "javascript:", + "javascript:", + "java\0script:", + "  javascript:" + ] + + invalid_slugs.each do |slug| + context "with the slug #{slug}" do + it "doesn't rewrite a (.) relative link" do + filtered_link = filter( + "<a href='.#{link}'>Link</a>", + project_wiki: wiki, + page_slug: slug).children[0] + + expect(filtered_link.attribute('href').value).not_to include(slug) + end + + it "doesn't rewrite a (..) relative link" do + filtered_link = filter( + "<a href='..#{link}'>Link</a>", + project_wiki: wiki, + page_slug: slug).children[0] + + expect(filtered_link.attribute('href').value).not_to include(slug) + end + end + end + end end end diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb index 3b52f6666d0..7b855251a74 100644 --- a/spec/lib/banzai/object_renderer_spec.rb +++ b/spec/lib/banzai/object_renderer_spec.rb @@ -11,7 +11,7 @@ describe Banzai::ObjectRenderer do ) end - let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16) } + let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16) } describe '#render' do context 'with cache' do @@ -60,24 +60,38 @@ describe Banzai::ObjectRenderer do end context 'without cache' do - let(:commit) { project.commit } + let(:cacheless_class) do + Class.new do + attr_accessor :title, :redacted_title_html, :project + + def banzai_render_context(field) + { project: project, pipeline: :single_line } + end + end + end + let(:cacheless_thing) do + cacheless_class.new.tap do |thing| + thing.title = "Merge branch 'branch-merged' into 'master'" + thing.project = project + end + end it 'renders and redacts an Array of objects' do - renderer.render([commit], :title) + renderer.render([cacheless_thing], :title) - expect(commit.redacted_title_html).to eq("Merge branch 'branch-merged' into 'master'") + expect(cacheless_thing.redacted_title_html).to eq("Merge branch 'branch-merged' into 'master'") end it 'calls Banzai::Redactor to perform redaction' do expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original - renderer.render([commit], :title) + renderer.render([cacheless_thing], :title) end it 'retrieves field content using Banzai::Renderer.cacheless_render_field' do - expect(Banzai::Renderer).to receive(:cacheless_render_field).with(commit, :title, {}).and_call_original + expect(Banzai::Renderer).to receive(:cacheless_render_field).with(cacheless_thing, :title, {}).and_call_original - renderer.render([commit], :title) + renderer.render([cacheless_thing], :title) end end end diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb index aaeec953e4b..718649e0e10 100644 --- a/spec/lib/banzai/redactor_spec.rb +++ b/spec/lib/banzai/redactor_spec.rb @@ -13,10 +13,10 @@ describe Banzai::Redactor do it 'redacts an array of documents' do doc1 = Nokogiri::HTML - .fragment('<a class="gfm" data-reference-type="issue">foo</a>') + .fragment('<a class="gfm" href="https://www.gitlab.com" data-reference-type="issue">foo</a>') doc2 = Nokogiri::HTML - .fragment('<a class="gfm" data-reference-type="issue">bar</a>') + .fragment('<a class="gfm" href="https://www.gitlab.com" data-reference-type="issue">bar</a>') redacted_data = redactor.redact([doc1, doc2]) @@ -27,7 +27,7 @@ describe Banzai::Redactor do end it 'replaces redacted reference with inner HTML' do - doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue'>foo</a>") + doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue'>foo</a>") redactor.redact([doc]) expect(doc.to_html).to eq('foo') end @@ -35,20 +35,24 @@ describe Banzai::Redactor do context 'when data-original attribute provided' do let(:original_content) { '<code>foo</code>' } it 'replaces redacted reference with original content' do - doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-original='#{original_content}'>bar</a>") + doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-original='#{original_content}'>bar</a>") redactor.redact([doc]) expect(doc.to_html).to eq(original_content) end - end - - it 'returns <a> tag with original href if it is originally a link reference' do - href = 'http://localhost:3000' - doc = Nokogiri::HTML - .fragment("<a class='gfm' data-reference-type='issue' data-original=#{href} data-link-reference='true'>#{href}</a>") - redactor.redact([doc]) + it 'does not replace redacted reference with original content if href is given' do + html = "<a href='https://www.gitlab.com' data-link-reference='true' class='gfm' data-reference-type='issue' data-reference-type='issue' data-original='Marge'>Marge</a>" + doc = Nokogiri::HTML.fragment(html) + redactor.redact([doc]) + expect(doc.to_html).to eq('<a href="https://www.gitlab.com">Marge</a>') + end - expect(doc.to_html).to eq('<a href="http://localhost:3000">http://localhost:3000</a>') + it 'uses the original content as the link content if given' do + html = "<a href='https://www.gitlab.com' data-link-reference='true' class='gfm' data-reference-type='issue' data-reference-type='issue' data-original='Homer'>Marge</a>" + doc = Nokogiri::HTML.fragment(html) + redactor.redact([doc]) + expect(doc.to_html).to eq('<a href="https://www.gitlab.com">Homer</a>') + end end end @@ -61,7 +65,7 @@ describe Banzai::Redactor do end it 'redacts an issue attached' do - doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-issue='#{issue.id}'>foo</a>") + doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-issue='#{issue.id}'>foo</a>") redactor.redact([doc]) @@ -69,7 +73,7 @@ describe Banzai::Redactor do end it 'redacts an external issue' do - doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-external-issue='#{issue.id}' data-project='#{project.id}'>foo</a>") + doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-external-issue='#{issue.id}' data-project='#{project.id}'>foo</a>") redactor.redact([doc]) diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb index 650cecfc778..aa828e2f0e9 100644 --- a/spec/lib/banzai/renderer_spec.rb +++ b/spec/lib/banzai/renderer_spec.rb @@ -11,16 +11,24 @@ describe Banzai::Renderer do object end + def fake_cacheless_object + object = double('cacheless object') + + allow(object).to receive(:respond_to?).with(:cached_markdown_fields).and_return(false) + + object + end + describe '#render_field' do let(:renderer) { described_class } context 'without cache' do - let(:commit) { create(:project, :repository).commit } + let(:commit) { fake_cacheless_object } it 'returns cacheless render field' do - expect(renderer).to receive(:cacheless_render_field).with(commit, :title, {}) + expect(renderer).to receive(:cacheless_render_field).with(commit, :field, {}) - renderer.render_field(commit, :title) + renderer.render_field(commit, :field) end end diff --git a/spec/lib/banzai/suggestions_parser_spec.rb b/spec/lib/banzai/suggestions_parser_spec.rb deleted file mode 100644 index 79658d710ce..00000000000 --- a/spec/lib/banzai/suggestions_parser_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Banzai::SuggestionsParser do - describe '.parse' do - it 'returns a list of suggestion contents' do - markdown = <<-MARKDOWN.strip_heredoc - ```suggestion - foo - bar - ``` - - ``` - nothing - ``` - - ```suggestion - xpto - baz - ``` - - ```thing - this is not a suggestion, it's a thing - ``` - MARKDOWN - - expect(described_class.parse(markdown)).to eq([" foo\n bar", - " xpto\n baz"]) - end - end -end diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb index c96e7ab8495..3496b01ebcc 100644 --- a/spec/lib/constraints/project_url_constrainer_spec.rb +++ b/spec/lib/constraints/project_url_constrainer_spec.rb @@ -16,6 +16,10 @@ describe Constraints::ProjectUrlConstrainer do let(:request) { build_request('foo', 'bar') } it { expect(subject.matches?(request)).to be_falsey } + + context 'existence_check is false' do + it { expect(subject.matches?(request, existence_check: false)).to be_truthy } + end end context "project id ending with .git" do diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb index 30016da6828..6648e141b7a 100644 --- a/spec/lib/event_filter_spec.rb +++ b/spec/lib/event_filter_spec.rb @@ -26,10 +26,10 @@ describe EventFilter do set(:push_event) { create(:push_event, project: public_project) } set(:merged_event) { create(:event, :merged, project: public_project, target: public_project) } - set(:created_event) { create(:event, :created, project: public_project, target: public_project) } - set(:updated_event) { create(:event, :updated, project: public_project, target: public_project) } - set(:closed_event) { create(:event, :closed, project: public_project, target: public_project) } - set(:reopened_event) { create(:event, :reopened, project: public_project, target: public_project) } + set(:created_event) { create(:event, :created, project: public_project, target: create(:issue, project: public_project)) } + set(:updated_event) { create(:event, :updated, project: public_project, target: create(:issue, project: public_project)) } + set(:closed_event) { create(:event, :closed, project: public_project, target: create(:issue, project: public_project)) } + set(:reopened_event) { create(:event, :reopened, project: public_project, target: create(:issue, project: public_project)) } set(:comments_event) { create(:event, :commented, project: public_project, target: public_project) } set(:joined_event) { create(:event, :joined, project: public_project, target: public_project) } set(:left_event) { create(:event, :left, project: public_project, target: public_project) } diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index e0691aba600..21ba72953fb 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -44,6 +44,36 @@ describe ExtractsPath do end end + context 'ref contains trailing space' do + let(:ref) { 'master ' } + + it 'strips surrounding space' do + assign_ref_vars + + expect(@ref).to eq('master') + end + end + + context 'ref contains leading space' do + let(:ref) { ' master ' } + + it 'strips surrounding space' do + assign_ref_vars + + expect(@ref).to eq('master') + end + end + + context 'ref contains space in the middle' do + let(:ref) { 'master plan ' } + + it 'returns 404' do + expect(self).to receive(:render_404) + + assign_ref_vars + end + end + context 'path contains space' do let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } } diff --git a/spec/lib/forever_spec.rb b/spec/lib/forever_spec.rb index 494c0561975..b9ffe895bf0 100644 --- a/spec/lib/forever_spec.rb +++ b/spec/lib/forever_spec.rb @@ -5,7 +5,7 @@ describe Forever do subject { described_class.date } context 'when using PostgreSQL' do - it 'should return Postgresql future date' do + it 'returns Postgresql future date' do allow(Gitlab::Database).to receive(:postgresql?).and_return(true) expect(subject).to eq(described_class::POSTGRESQL_DATE) @@ -13,7 +13,7 @@ describe Forever do end context 'when using MySQL' do - it 'should return MySQL future date' do + it 'returns MySQL future date' do allow(Gitlab::Database).to receive(:postgresql?).and_return(false) expect(subject).to eq(described_class::MYSQL_DATE) diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb index d3ab599d5a0..b91a09e3137 100644 --- a/spec/lib/gitlab/auth/ldap/config_spec.rb +++ b/spec/lib/gitlab/auth/ldap/config_spec.rb @@ -5,6 +5,65 @@ describe Gitlab::Auth::LDAP::Config do let(:config) { described_class.new('ldapmain') } + def raw_cert + <<-EOS +-----BEGIN CERTIFICATE----- +MIIDZjCCAk4CCQDX+u/9fICksDANBgkqhkiG9w0BAQsFADB1MQswCQYDVQQGEwJV +UzEMMAoGA1UECAwDRm9vMQwwCgYDVQQHDANCYXIxDDAKBgNVBAoMA0JhejEMMAoG +A1UECwwDUXV4MQ0wCwYDVQQDDARsZGFwMR8wHQYJKoZIhvcNAQkBFhBsZGFwQGV4 +YW1wbGUuY29tMB4XDTE5MDIyNzE1NTUxNFoXDTE5MDMyOTE1NTUxNFowdTELMAkG +A1UEBhMCVVMxDDAKBgNVBAgMA0ZvbzEMMAoGA1UEBwwDQmFyMQwwCgYDVQQKDANC +YXoxDDAKBgNVBAsMA1F1eDENMAsGA1UEAwwEbGRhcDEfMB0GCSqGSIb3DQEJARYQ +bGRhcEBleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +APuDB/4/AUmTEmhYzN13no4Kt8hkRbLQuENRHlOeQw05/MVdoB1AWLOPzIXn4kex +GD9tHkoJl8S0QPmAAcPHn5O97e+gd0ze5dRQZl/cSd2/j5zeaMvZ1mCrPN/dOluM +94Oj+wQU4bEcOlrqIMSh0ezJw10R3IHXCQFeGtIZU57WmKcrryQX4kP7KTOgRw/t +CYp+NivQHtLbBEj1MU0l10qMS2+w8Qpqov4MdW4gx4wTgId2j1ZZ56+n6Jsc9qoI +wBWBNL4XU5a3kwhYZDOJoOvI9po33KLdT1dXS81uOFXClp3LGmKDgLTwQ1w+RmQG ++JG4EvTfDIShdcTDXEaOfCECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJM9Btu5g +k8qDiz5TilvpyoGuI4viCwusARFAFmOB/my/cHlVvkuq4bbfV1KJoWWGJg8GcklL +cnIdxc35uYM5icr6xXQyrW0GqAO+LEXyUxVQqYETxrQ/LJ03xhBnuF7hvZJIBiky +GwUy0clJxGfaCeEM8zXwePawLgGjuUawDDQOwigysoWqoMu3VFW8zl8UPa84bow9 +Kn2QmPAkLw4EcqYSCNSSvnyzu5SM64jwLWRXFsmlqD7773oT29vTkqM1EQANFEfT +7gQomLyPqoPBoFph5oSNn6Rf31QX1Sie92EAKVnZ1XmD68hKzjv6ChCtzTv4jABg +XrDwnLkORIAF/Q== +-----END CERTIFICATE----- + EOS + end + + def raw_key + <<-EOS +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQD7gwf+PwFJkxJo +WMzdd56OCrfIZEWy0LhDUR5TnkMNOfzFXaAdQFizj8yF5+JHsRg/bR5KCZfEtED5 +gAHDx5+Tve3voHdM3uXUUGZf3Endv4+c3mjL2dZgqzzf3TpbjPeDo/sEFOGxHDpa +6iDEodHsycNdEdyB1wkBXhrSGVOe1pinK68kF+JD+ykzoEcP7QmKfjYr0B7S2wRI +9TFNJddKjEtvsPEKaqL+DHVuIMeME4CHdo9WWeevp+ibHPaqCMAVgTS+F1OWt5MI +WGQziaDryPaaN9yi3U9XV0vNbjhVwpadyxpig4C08ENcPkZkBviRuBL03wyEoXXE +w1xGjnwhAgMBAAECggEAbw82GVui6uUpjLAhjm3CssAi1TcJ2+L0aq1IMe5Bd3ay +mkg0apY+VNPboQl6zuNxbJh3doPz42UhB8sxfE0Ktwd4KIb4Bxap7+2stwmkCGoN +NVy0c8d2NWuHzuZ2XXTK2vMu5Wd/HWD0l66o14sJEoEpZlB7yU216UevmjSayxjh +aBTSaYyyrf24haTaCuqwph/V73ZlMpFdSALGny0uiP/5inxciMCkMpHfX6BflSb4 +EGKsIYt9BJ0kY4GNG5bCP7971UCxp2eEJhU2fV8HuFGCOD12IqSpUqPxHxjsWpfx +T7FZ3V2kM/58Ca+5LB2y3atcPIdY0/g7/43V4VD+7QKBgQD/PO4/0cmZuuLU1LPT +C/C596kPK0JLlvvRqhbz4byRAkW/n7uQFG7TMtFNle3UmT7rk7pjtbHnByqzEd+9 +jMhBysjHOMg0+DWm7fEtSg/tJ3qLVO3nbdA4qmXYobLcLoG+PCYRLskEHHqTG/Bv +QZLbavOU6rrTqckNr1TMpNBmXwKBgQD8Q0C2YTOpwgjRUe8i6Chnc3o4x8a1i98y +9la6c7y7acWHSbEczMkNfEBrbM73rTb+bBA0Zqw+Z1gkv8bGpvGxX8kbSfJJ2YKW +9koxpLNTVNVapqBa9ImiaozV285dz9Ukx8bnMOJlTELpOl7RRV7iF0smYjfHIl3D +Yxyda/MtfwKBgHb9l/Dmw77IkqE4PFFimqqIHCe3OiP1UpavXh36midcUNoCBLYp +4HTTlyI9iG/5tYysBVQgy7xx6eUrqww6Ss3pVOsTvLp9EL4u5aYAhiZApm+4e2TO +HCmevvZcg/8EK3Zdoj2Wex5QjJBykQe9IVLrrH07ZTfySon3uGfjWkivAoGAGvqS +VC8HGHOw/7n0ilYr5Ax8mM/813OzFj80PVKdb6m7P2HJOFxKcE/Gj/aeF+0FgaZL +AV+tsirZSWzdNGesV5z35Bw/dlh11/FVNAP6TcI34y8I3VFj2uPsVf7hDjVpBTr8 +ccNPoyfJzCm69ESoBiQZnGxKrNhnELtr1wYxhr8CgYApWwf4hVrTWV1zs+pEJenh +AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK +0Ff8afd2Q/OfBeUdq9KA4JO9fNqzEwOWvv8Ryn4ZSYcAuLP7IVJKjjI6R7rYaO/G +3OWJdizbykGOi0BFDu+3dw== +-----END PRIVATE KEY----- + EOS + end + describe '.servers' do it 'returns empty array if no server information is available' do allow(Gitlab.config).to receive(:ldap).and_return('enabled' => false) @@ -89,6 +148,42 @@ describe Gitlab::Auth::LDAP::Config do expect(config.adapter_options[:encryption]).to include({ method: :start_tls }) end + it 'transforms SSL cert and key to OpenSSL objects' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'start_tls', + 'tls_options' => { + 'cert' => raw_cert, + 'key' => raw_key + } + } + ) + + expect(config.adapter_options[:encryption][:tls_options][:cert]).to be_a(OpenSSL::X509::Certificate) + expect(config.adapter_options[:encryption][:tls_options][:key]).to be_a(OpenSSL::PKey::RSA) + end + + it 'logs an error when an invalid key or cert are configured' do + allow(Rails.logger).to receive(:error) + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'start_tls', + 'tls_options' => { + 'cert' => 'invalid cert', + 'key' => 'invalid_key' + } + } + ) + + config.adapter_options + + expect(Rails.logger).to have_received(:error).with(/LDAP TLS Options/).twice + end + context 'when verify_certificates is enabled' do it 'sets tls_options to OpenSSL defaults' do stub_ldap_config( @@ -130,7 +225,9 @@ describe Gitlab::Auth::LDAP::Config do 'host' => 'ldap.example.com', 'port' => 686, 'encryption' => 'simple_tls', - 'ca_file' => '/etc/ca.pem' + 'tls_options' => { + 'ca_file' => '/etc/ca.pem' + } } ) @@ -145,7 +242,9 @@ describe Gitlab::Auth::LDAP::Config do 'host' => 'ldap.example.com', 'port' => 686, 'encryption' => 'simple_tls', - 'ca_file' => ' ' + 'tls_options' => { + 'ca_file' => ' ' + } } ) @@ -160,7 +259,9 @@ describe Gitlab::Auth::LDAP::Config do 'host' => 'ldap.example.com', 'port' => 686, 'encryption' => 'simple_tls', - 'ssl_version' => 'TLSv1_2' + 'tls_options' => { + 'ssl_version' => 'TLSv1_2' + } } ) @@ -175,7 +276,9 @@ describe Gitlab::Auth::LDAP::Config do 'host' => 'ldap.example.com', 'port' => 686, 'encryption' => 'simple_tls', - 'ssl_version' => ' ' + 'tls_options' => { + 'ssl_version' => ' ' + } } ) @@ -223,6 +326,23 @@ describe Gitlab::Auth::LDAP::Config do ) end + it 'transforms SSL cert and key to OpenSSL objects' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'start_tls', + 'tls_options' => { + 'cert' => raw_cert, + 'key' => raw_key + } + } + ) + + expect(config.omniauth_options[:tls_options][:cert]).to be_a(OpenSSL::X509::Certificate) + expect(config.omniauth_options[:tls_options][:key]).to be_a(OpenSSL::PKey::RSA) + end + context 'when verify_certificates is enabled' do it 'specifies disable_verify_certificates as false' do stub_ldap_config( @@ -261,11 +381,13 @@ describe Gitlab::Auth::LDAP::Config do 'port' => 686, 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'ca_file' => '/etc/ca.pem' + 'tls_options' => { + 'ca_file' => '/etc/ca.pem' + } } ) - expect(config.omniauth_options).to include({ ca_file: '/etc/ca.pem' }) + expect(config.omniauth_options[:tls_options]).to include({ ca_file: '/etc/ca.pem' }) end end @@ -277,11 +399,13 @@ describe Gitlab::Auth::LDAP::Config do 'port' => 686, 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'ca_file' => ' ' + 'tls_options' => { + 'ca_file' => ' ' + } } ) - expect(config.omniauth_options).not_to have_key(:ca_file) + expect(config.omniauth_options[:tls_options]).not_to have_key(:ca_file) end end @@ -293,11 +417,13 @@ describe Gitlab::Auth::LDAP::Config do 'port' => 686, 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'ssl_version' => 'TLSv1_2' + 'tls_options' => { + 'ssl_version' => 'TLSv1_2' + } } ) - expect(config.omniauth_options).to include({ ssl_version: 'TLSv1_2' }) + expect(config.omniauth_options[:tls_options]).to include({ ssl_version: 'TLSv1_2' }) end end @@ -309,11 +435,14 @@ describe Gitlab::Auth::LDAP::Config do 'port' => 686, 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'ssl_version' => ' ' + 'tls_options' => { + 'ssl_version' => ' ' + } } ) - expect(config.omniauth_options).not_to have_key(:ssl_version) + # OpenSSL default params includes `ssl_version` so we just check that it's not blank + expect(config.omniauth_options[:tls_options]).not_to include({ ssl_version: ' ' }) end end end diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb index dcbd12fe190..b765c265e69 100644 --- a/spec/lib/gitlab/auth/o_auth/user_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb @@ -207,6 +207,7 @@ describe Gitlab::Auth::OAuth::User do before do allow(ldap_user).to receive(:uid) { uid } allow(ldap_user).to receive(:username) { uid } + allow(ldap_user).to receive(:name) { 'John Doe' } allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] } allow(ldap_user).to receive(:dn) { dn } end @@ -221,6 +222,7 @@ describe Gitlab::Auth::OAuth::User do it "creates a user with dual LDAP and omniauth identities" do expect(gl_user).to be_valid expect(gl_user.username).to eql uid + expect(gl_user.name).to eql 'John Doe' expect(gl_user.email).to eql 'johndoe@example.com' expect(gl_user.identities.length).to be 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } @@ -232,11 +234,13 @@ describe Gitlab::Auth::OAuth::User do ) end - it "has email set as synced" do + it "has name and email set as synced" do + expect(gl_user.user_synced_attributes_metadata.name_synced).to be_truthy expect(gl_user.user_synced_attributes_metadata.email_synced).to be_truthy end - it "has email set as read-only" do + it "has name and email set as read-only" do + expect(gl_user.read_only_attribute?(:name)).to be_truthy expect(gl_user.read_only_attribute?(:email)).to be_truthy end @@ -246,7 +250,7 @@ describe Gitlab::Auth::OAuth::User do end context "and LDAP user has an account already" do - let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') } + let!(:existing_user) { create(:omniauth_user, name: 'John Doe', email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') } it "adds the omniauth identity to the LDAP account" do allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) @@ -254,6 +258,7 @@ describe Gitlab::Auth::OAuth::User do expect(gl_user).to be_valid expect(gl_user.username).to eql 'john' + expect(gl_user.name).to eql 'John Doe' expect(gl_user.email).to eql 'john@example.com' expect(gl_user.identities.length).to be 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index a4a6338961e..3b5ca7c950c 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -5,7 +5,15 @@ describe Gitlab::Auth do describe 'constants' do it 'API_SCOPES contains all scopes for API access' do - expect(subject::API_SCOPES).to eq %i[api read_user sudo read_repository] + expect(subject::API_SCOPES).to eq %i[api read_user] + end + + it 'ADMIN_SCOPES contains all scopes for ADMIN access' do + expect(subject::ADMIN_SCOPES).to eq %i[sudo] + end + + it 'REPOSITORY_SCOPES contains all scopes for REPOSITORY access' do + expect(subject::REPOSITORY_SCOPES).to eq %i[read_repository write_repository] end it 'OPENID_SCOPES contains all scopes for OpenID Connect' do @@ -19,7 +27,29 @@ describe Gitlab::Auth do it 'optional_scopes contains all non-default scopes' do stub_container_registry_config(enabled: true) - expect(subject.optional_scopes).to eq %i[read_user sudo read_repository read_registry openid profile email] + expect(subject.optional_scopes).to eq %i[read_user read_repository write_repository read_registry sudo openid profile email] + end + end + + context 'available_scopes' do + it 'contains all non-default scopes' do + stub_container_registry_config(enabled: true) + + expect(subject.all_available_scopes).to eq %i[api read_user read_repository write_repository read_registry sudo] + end + + it 'contains for non-admin user all non-default scopes without ADMIN access' do + stub_container_registry_config(enabled: true) + user = create(:user, admin: false) + + expect(subject.available_scopes_for(user)).to eq %i[api read_user read_repository write_repository read_registry] + end + + it 'contains for admin user all non-default scopes with ADMIN access' do + stub_container_registry_config(enabled: true) + user = create(:user, admin: true) + + expect(subject.available_scopes_for(user)).to eq %i[api read_user read_repository write_repository read_registry sudo] end context 'registry_scopes' do @@ -122,7 +152,7 @@ describe Gitlab::Auth do token = Gitlab::LfsToken.new(key).token expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") - expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) + expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_only_authentication_abilities)) end it 'does not try password auth before oauth' do @@ -150,7 +180,7 @@ describe Gitlab::Auth do token = Gitlab::LfsToken.new(key).token expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") - expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) + expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_only_authentication_abilities)) end end @@ -182,8 +212,19 @@ describe Gitlab::Auth do it 'succeeds for personal access tokens with the `api` scope' do personal_access_token = create(:personal_access_token, scopes: ['api']) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, full_authentication_abilities)) + expect_results_with_abilities(personal_access_token, full_authentication_abilities) + end + + it 'succeeds for personal access tokens with the `read_repository` scope' do + personal_access_token = create(:personal_access_token, scopes: ['read_repository']) + + expect_results_with_abilities(personal_access_token, [:download_code]) + end + + it 'succeeds for personal access tokens with the `write_repository` scope' do + personal_access_token = create(:personal_access_token, scopes: ['write_repository']) + + expect_results_with_abilities(personal_access_token, [:download_code, :push_code]) end context 'when registry is enabled' do @@ -194,28 +235,24 @@ describe Gitlab::Auth do it 'succeeds for personal access tokens with the `read_registry` scope' do personal_access_token = create(:personal_access_token, scopes: ['read_registry']) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:read_container_image])) + expect_results_with_abilities(personal_access_token, [:read_container_image]) end end it 'succeeds if it is an impersonation token' do impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api']) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_access_token, full_authentication_abilities)) + expect_results_with_abilities(impersonation_token, full_authentication_abilities) end it 'limits abilities based on scope' do personal_access_token = create(:personal_access_token, scopes: %w[read_user sudo]) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [])) + expect_results_with_abilities(personal_access_token, []) end it 'fails if password is nil' do - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', nil, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) + expect_results_with_abilities(nil, nil, false) end end @@ -479,7 +516,7 @@ describe Gitlab::Auth do ] end - def read_authentication_abilities + def read_only_authentication_abilities [ :read_project, :download_code, @@ -488,7 +525,7 @@ describe Gitlab::Auth do end def read_write_authentication_abilities - read_authentication_abilities + [ + read_only_authentication_abilities + [ :push_code, :create_container_image ] @@ -499,4 +536,10 @@ describe Gitlab::Auth do :admin_container_image ] end + + def expect_results_with_abilities(personal_access_token, abilities, success = true) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: success, login: '') + expect(gl_auth.find_for_git_client('', personal_access_token&.token, project: nil, ip: 'ip')) + .to eq(Gitlab::Auth::Result.new(personal_access_token&.user, nil, personal_access_token.nil? ? nil : :personal_access_token, abilities)) + end end diff --git a/spec/lib/gitlab/authorized_keys_spec.rb b/spec/lib/gitlab/authorized_keys_spec.rb new file mode 100644 index 00000000000..42bc509eeef --- /dev/null +++ b/spec/lib/gitlab/authorized_keys_spec.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::AuthorizedKeys do + let(:logger) { double('logger').as_null_object } + + subject { described_class.new(logger) } + + describe '#add_key' do + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture + end + + after do + delete_authorized_keys_file + end + + it "adds a line at the end of the file and strips trailing garbage" do + auth_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaDAxx2E" + + expect(logger).to receive(:info).with('Adding key (key-741): ssh-rsa AAAAB3NzaDAxx2E') + expect(subject.add_key('key-741', 'ssh-rsa AAAAB3NzaDAxx2E trailing garbage')) + .to be_truthy + expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line}\n") + end + end + + context 'authorized_keys file does not exist' do + before do + delete_authorized_keys_file + end + + it 'creates the file' do + expect(subject.add_key('key-741', 'ssh-rsa AAAAB3NzaDAxx2E')).to be_truthy + expect(File.exist?(tmp_authorized_keys_path)).to be_truthy + end + end + end + + describe '#batch_add_keys' do + let(:keys) do + [ + double(shell_id: 'key-12', key: 'ssh-dsa ASDFASGADG trailing garbage'), + double(shell_id: 'key-123', key: 'ssh-rsa GFDGDFSGSDFG') + ] + end + + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture + end + + after do + delete_authorized_keys_file + end + + it "adds lines at the end of the file" do + auth_line1 = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-12\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-dsa ASDFASGADG" + auth_line2 = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-123\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa GFDGDFSGSDFG" + + expect(logger).to receive(:info).with('Adding key (key-12): ssh-dsa ASDFASGADG') + expect(logger).to receive(:info).with('Adding key (key-123): ssh-rsa GFDGDFSGSDFG') + expect(subject.batch_add_keys(keys)).to be_truthy + expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line1}\n#{auth_line2}\n") + end + + context "invalid key" do + let(:keys) { [double(shell_id: 'key-123', key: "ssh-rsa A\tSDFA\nSGADG")] } + + it "doesn't add keys" do + expect(subject.batch_add_keys(keys)).to be_falsey + expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n") + end + end + end + + context 'authorized_keys file does not exist' do + before do + delete_authorized_keys_file + end + + it 'creates the file' do + expect(subject.batch_add_keys(keys)).to be_truthy + expect(File.exist?(tmp_authorized_keys_path)).to be_truthy + end + end + end + + describe '#rm_key' do + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture + end + + after do + delete_authorized_keys_file + end + + it "removes the right line" do + other_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-742\",options ssh-rsa AAAAB3NzaDAxx2E" + delete_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",options ssh-rsa AAAAB3NzaDAxx2E" + erased_line = delete_line.gsub(/./, '#') + File.open(tmp_authorized_keys_path, 'a') do |auth_file| + auth_file.puts delete_line + auth_file.puts other_line + end + + expect(logger).to receive(:info).with('Removing key (key-741)') + expect(subject.rm_key('key-741')).to be_truthy + expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{erased_line}\n#{other_line}\n") + end + end + + context 'authorized_keys file does not exist' do + before do + delete_authorized_keys_file + end + + it 'returns false' do + expect(subject.rm_key('key-741')).to be_falsey + end + end + end + + describe '#clear' do + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture + end + + after do + delete_authorized_keys_file + end + + it "returns true" do + expect(subject.clear).to be_truthy + end + end + + context 'authorized_keys file does not exist' do + before do + delete_authorized_keys_file + end + + it "still returns true" do + expect(subject.clear).to be_truthy + end + end + end + + describe '#list_key_ids' do + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture( + existing_content: + "key-1\tssh-dsa AAA\nkey-2\tssh-rsa BBB\nkey-3\tssh-rsa CCC\nkey-9000\tssh-rsa DDD\n" + ) + end + + after do + delete_authorized_keys_file + end + + it 'returns array of key IDs' do + expect(subject.list_key_ids).to eq([1, 2, 3, 9000]) + end + end + + context 'authorized_keys file does not exist' do + before do + delete_authorized_keys_file + end + + it 'returns an empty array' do + expect(subject.list_key_ids).to be_empty + end + end + end + + def create_authorized_keys_fixture(existing_content: 'existing content') + FileUtils.mkdir_p(File.dirname(tmp_authorized_keys_path)) + File.open(tmp_authorized_keys_path, 'w') { |file| file.puts(existing_content) } + end + + def delete_authorized_keys_file + File.delete(tmp_authorized_keys_path) if File.exist?(tmp_authorized_keys_path) + end + + def tmp_authorized_keys_path + Gitlab.config.gitlab_shell.authorized_keys_file + end +end diff --git a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb index 27281333348..0a5b99d27e7 100644 --- a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb +++ b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb @@ -3,6 +3,12 @@ require 'spec_helper' # rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, schema: 20180619121030 do describe '#perform' do + before do + # This migration was created before we introduced ProjectCiCdSetting#default_git_depth + allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth=).and_return(0) + allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth).and_return(nil) + end + context 'when diff files can be deleted' do let(:merge_request) { create(:merge_request, :merged) } let!(:merge_request_diff) do diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb index bc71a90605a..d3f7f1ded16 100644 --- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -172,14 +172,12 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m let(:exception) { ActiveRecord::RecordNotFound } let(:perform_ignoring_exceptions) do - begin - subject.perform(start_id, stop_id) - rescue described_class::Error - end + subject.perform(start_id, stop_id) + rescue described_class::Error end before do - allow_any_instance_of(described_class::MergeRequestDiff::ActiveRecord_Relation) + allow_any_instance_of(ActiveRecord::Relation) .to receive(:update_all).and_raise(exception) end diff --git a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb index 7c7e58d6bb7..582396275ed 100644 --- a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb @@ -51,7 +51,7 @@ describe Gitlab::BackgroundMigration::MigrateBuildStage, :migration, schema: 201 statuses[:pending]] end - it 'recovers from unique constraint violation only twice' do + it 'recovers from unique constraint violation only twice', :quarantine do allow(described_class::Migratable::Stage) .to receive(:find_by).and_return(nil) diff --git a/spec/lib/gitlab/background_migration/migrate_legacy_uploads_spec.rb b/spec/lib/gitlab/background_migration/migrate_legacy_uploads_spec.rb new file mode 100644 index 00000000000..16b9de6a84e --- /dev/null +++ b/spec/lib/gitlab/background_migration/migrate_legacy_uploads_spec.rb @@ -0,0 +1,253 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::BackgroundMigration::MigrateLegacyUploads, :migration, schema: 20190103140724 do + let(:test_dir) { FileUploader.options['storage_path'] } + + # rubocop: disable RSpec/FactoriesInMigrationSpecs + let(:namespace) { create(:namespace) } + let(:project) { create(:project, :legacy_storage, namespace: namespace) } + let(:issue) { create(:issue, project: project) } + + let(:note1) { create(:note, note: 'some note text awesome', project: project, noteable: issue) } + let(:note2) { create(:note, note: 'some note', project: project, noteable: issue) } + + let(:hashed_project) { create(:project, namespace: namespace) } + let(:issue_hashed_project) { create(:issue, project: hashed_project) } + let(:note_hashed_project) { create(:note, note: 'some note', project: hashed_project, attachment: 'text.pdf', noteable: issue_hashed_project) } + + let(:standard_upload) do + create(:upload, + path: "secretabcde/image.png", + model_id: create(:project).id, model_type: 'Project', uploader: 'FileUploader') + end + + before do + # This migration was created before we introduced ProjectCiCdSetting#default_git_depth + allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth).and_return(nil) + allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth=).and_return(0) + + namespace + project + issue + note1 + note2 + hashed_project + issue_hashed_project + note_hashed_project + standard_upload + end + + def create_remote_upload(model, filename) + create(:upload, :attachment_upload, + path: "note/attachment/#{model.id}/#{filename}", secret: nil, + store: ObjectStorage::Store::REMOTE, model: model) + end + + def create_upload(model, filename, with_file = true) + params = { + path: "uploads/-/system/note/attachment/#{model.id}/#{filename}", + model: model, + store: ObjectStorage::Store::LOCAL + } + + upload = if with_file + create(:upload, :with_file, :attachment_upload, params) + else + create(:upload, :attachment_upload, params) + end + + model.update(attachment: upload.build_uploader) + model.attachment.upload + end + + let(:start_id) { 1 } + let(:end_id) { 10000 } + + def new_upload_legacy + Upload.find_by(model_id: project.id, model_type: 'Project') + end + + def new_upload_hashed + Upload.find_by(model_id: hashed_project.id, model_type: 'Project') + end + + shared_examples 'migrates files correctly' do + before do + described_class.new.perform(start_id, end_id) + end + + it 'removes all the legacy upload records' do + expect(Upload.where(uploader: 'AttachmentUploader')).to be_empty + + expect(standard_upload.reload).to eq(standard_upload) + end + + it 'creates new upload records correctly' do + expect(new_upload_legacy.secret).not_to be_nil + expect(new_upload_legacy.path).to end_with("#{new_upload_legacy.secret}/image.png") + expect(new_upload_legacy.model_id).to eq(project.id) + expect(new_upload_legacy.model_type).to eq('Project') + expect(new_upload_legacy.uploader).to eq('FileUploader') + + expect(new_upload_hashed.secret).not_to be_nil + expect(new_upload_hashed.path).to end_with("#{new_upload_hashed.secret}/text.pdf") + expect(new_upload_hashed.model_id).to eq(hashed_project.id) + expect(new_upload_hashed.model_type).to eq('Project') + expect(new_upload_hashed.uploader).to eq('FileUploader') + end + + it 'updates the legacy upload notes so that they include the file references in the markdown' do + expected_path = File.join('/uploads', new_upload_legacy.secret, 'image.png') + expected_markdown = "some note text awesome \n ![image](#{expected_path})" + expect(note1.reload.note).to eq(expected_markdown) + + expected_path = File.join('/uploads', new_upload_hashed.secret, 'text.pdf') + expected_markdown = "some note \n [text.pdf](#{expected_path})" + expect(note_hashed_project.reload.note).to eq(expected_markdown) + end + + it 'removed the attachments from the note model' do + expect(note1.reload.attachment.file).to be_nil + expect(note2.reload.attachment.file).to be_nil + expect(note_hashed_project.reload.attachment.file).to be_nil + end + end + + context 'when legacy uploads are stored in local storage' do + let!(:legacy_upload1) { create_upload(note1, 'image.png') } + let!(:legacy_upload_not_found) { create_upload(note2, 'image.png', false) } + let!(:legacy_upload_hashed) { create_upload(note_hashed_project, 'text.pdf', with_file: true) } + + shared_examples 'removes legacy local files' do + it 'removes all the legacy upload records' do + expect(File.exist?(legacy_upload1.absolute_path)).to be_truthy + expect(File.exist?(legacy_upload_hashed.absolute_path)).to be_truthy + + described_class.new.perform(start_id, end_id) + + expect(File.exist?(legacy_upload1.absolute_path)).to be_falsey + expect(File.exist?(legacy_upload_hashed.absolute_path)).to be_falsey + end + end + + context 'when object storage is disabled for FileUploader' do + it_behaves_like 'migrates files correctly' + it_behaves_like 'removes legacy local files' + + it 'moves legacy uploads to the correct location' do + described_class.new.perform(start_id, end_id) + + expected_path1 = File.join(test_dir, 'uploads', namespace.path, project.path, new_upload_legacy.secret, 'image.png') + expected_path2 = File.join(test_dir, 'uploads', hashed_project.disk_path, new_upload_hashed.secret, 'text.pdf') + + expect(File.exist?(expected_path1)).to be_truthy + expect(File.exist?(expected_path2)).to be_truthy + end + + context 'when the upload move fails' do + it 'does not remove old uploads' do + expect(FileUploader).to receive(:copy_to).twice.and_raise('failed') + + described_class.new.perform(start_id, end_id) + + expect(legacy_upload1.reload).to eq(legacy_upload1) + expect(legacy_upload_hashed.reload).to eq(legacy_upload_hashed) + expect(standard_upload.reload).to eq(standard_upload) + end + end + end + + context 'when object storage is enabled for FileUploader' do + before do + stub_uploads_object_storage(FileUploader) + end + + it_behaves_like 'migrates files correctly' + it_behaves_like 'removes legacy local files' + + # The process of migrating to object storage is a manual one, + # so it would go against expectations to automatically migrate these files + # to object storage during this migration. + # After this migration, these files should be able to successfully migrate to object storage. + it 'stores files locally' do + described_class.new.perform(start_id, end_id) + + expected_path1 = File.join(test_dir, 'uploads', namespace.path, project.path, new_upload_legacy.secret, 'image.png') + expected_path2 = File.join(test_dir, 'uploads', hashed_project.disk_path, new_upload_hashed.secret, 'text.pdf') + + expect(File.exist?(expected_path1)).to be_truthy + expect(File.exist?(expected_path2)).to be_truthy + end + end + + context 'with legacy_diff_note upload' do + let!(:merge_request) { create(:merge_request, source_project: project) } + let!(:legacy_diff_note) { create(:legacy_diff_note_on_merge_request, note: 'some note', project: project, noteable: merge_request) } + let!(:legacy_upload_diff_note) do + create(:upload, :with_file, :attachment_upload, + path: "uploads/-/system/note/attachment/#{legacy_diff_note.id}/some_legacy.pdf", model: legacy_diff_note) + end + + before do + described_class.new.perform(start_id, end_id) + end + + it 'does not remove legacy diff note file' do + expect(File.exist?(legacy_upload_diff_note.absolute_path)).to be_truthy + end + + it 'removes all the legacy upload records except for the one with legacy_diff_note' do + expect(Upload.where(uploader: 'AttachmentUploader')).to eq([legacy_upload_diff_note]) + end + + it 'adds link to the troubleshooting documentation to the note' do + help_doc_link = 'https://docs.gitlab.com/ee/administration/troubleshooting/migrations.html#legacy-upload-migration' + + expect(legacy_diff_note.reload.note).to include(help_doc_link) + end + end + end + + context 'when legacy uploads are stored in object storage' do + let!(:legacy_upload1) { create_remote_upload(note1, 'image.png') } + let!(:legacy_upload_not_found) { create_remote_upload(note2, 'non-existing.pdf') } + let!(:legacy_upload_hashed) { create_remote_upload(note_hashed_project, 'text.pdf') } + let(:remote_files) do + [ + { key: "#{legacy_upload1.path}" }, + { key: "#{legacy_upload_hashed.path}" } + ] + end + let(:connection) { ::Fog::Storage.new(FileUploader.object_store_credentials) } + let(:bucket) { connection.directories.create(key: 'uploads') } + + def create_remote_files + remote_files.each { |file| bucket.files.create(file) } + end + + before do + stub_uploads_object_storage(FileUploader) + create_remote_files + end + + it_behaves_like 'migrates files correctly' + + it 'moves legacy uploads to the correct remote location' do + described_class.new.perform(start_id, end_id) + + connection = ::Fog::Storage.new(FileUploader.object_store_credentials) + expect(connection.get_object('uploads', new_upload_legacy.path)[:status]).to eq(200) + expect(connection.get_object('uploads', new_upload_hashed.path)[:status]).to eq(200) + end + + it 'removes all the legacy upload records' do + described_class.new.perform(start_id, end_id) + + remote_files.each do |remote_file| + expect(bucket.files.get(remote_file[:key])).to be_nil + end + end + end + # rubocop: enable RSpec/FactoriesInMigrationSpecs +end diff --git a/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb index f8107dd40b9..4db829b1e7b 100644 --- a/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb @@ -30,6 +30,6 @@ describe Gitlab::BackgroundMigration::MigrateStageIndex, :migration, schema: 201 described_class.new.perform(100, 101) - expect(stages.all.pluck(:position)).to eq [2, 3] + expect(stages.all.order(:id).pluck(:position)).to eq [2, 3] end end diff --git a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb index 812e0cc6947..128e118ac17 100644 --- a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb @@ -19,7 +19,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, : end shared_examples 'consistent kubernetes namespace attributes' do - it 'should populate namespace and service account information' do + it 'populates namespace and service account information' do migration.perform clusters_with_namespace.each do |cluster| @@ -41,7 +41,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, : context 'when no Clusters::Project has a Clusters::KubernetesNamespace' do let(:cluster_projects) { cluster_projects_table.all } - it 'should create a Clusters::KubernetesNamespace per Clusters::Project' do + it 'creates a Clusters::KubernetesNamespace per Clusters::Project' do expect do migration.perform end.to change(Clusters::KubernetesNamespace, :count).by(cluster_projects_table.count) @@ -57,7 +57,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, : create_kubernetes_namespace(clusters_table.all) end - it 'should not create any Clusters::KubernetesNamespace' do + it 'does not create any Clusters::KubernetesNamespace' do expect do migration.perform end.not_to change(Clusters::KubernetesNamespace, :count) @@ -78,7 +78,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, : end.to change(Clusters::KubernetesNamespace, :count).by(with_no_kubernetes_namespace.count) end - it 'should not modify clusters with Clusters::KubernetesNamespace' do + it 'does not modify clusters with Clusters::KubernetesNamespace' do migration.perform with_kubernetes_namespace.each do |cluster| diff --git a/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb index 3e009fed0f1..c6bc3db88a3 100644 --- a/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb @@ -9,6 +9,9 @@ describe Gitlab::BackgroundMigration::PopulateExternalPipelineSource, :migration before do # This migration was created before we introduced metadata configs stub_feature_flags(ci_build_metadata_config: false) + # This migration was created before we introduced ProjectCiCdSetting#default_git_depth + allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth).and_return(nil) + allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth=).and_return(0) end let!(:internal_pipeline) { create(:ci_pipeline, source: :web) } diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb index 8582af96199..0e73c8c59c9 100644 --- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb @@ -41,7 +41,7 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch migration.perform(1, 3) end - it 'it creates the fork network' do + it 'creates the fork network' do expect(fork_network1).not_to be_nil expect(fork_network2).not_to be_nil end diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb new file mode 100644 index 00000000000..4a81a37d341 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Gitlab::BackgroundMigration::PopulateMergeRequestAssigneesTable, :migration, schema: 20190315191339 do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:users) { table(:users) } + + let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') } + let(:user_2) { users.create!(email: 'test2@example.com', projects_limit: 100, username: 'test') } + let(:user_3) { users.create!(email: 'test3@example.com', projects_limit: 100, username: 'test') } + + let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') } + let(:merge_requests) { table(:merge_requests) } + let(:merge_request_assignees) { table(:merge_request_assignees) } + + def create_merge_request(id, params = {}) + params.merge!(id: id, + target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: 'mr name', + title: "mr name#{id}") + + merge_requests.create(params) + end + + describe '#perform' do + it 'creates merge_request_assignees rows according to merge_requests' do + create_merge_request(2, assignee_id: user.id) + create_merge_request(3, assignee_id: user_2.id) + create_merge_request(4, assignee_id: user_3.id) + # Test filtering already migrated row + merge_request_assignees.create!(merge_request_id: 2, user_id: user_3.id) + + subject.perform(1, 4) + + rows = merge_request_assignees.order(:id).map { |row| row.attributes.slice('merge_request_id', 'user_id') } + existing_rows = [ + { 'merge_request_id' => 2, 'user_id' => user_3.id } + ] + created_rows = [ + { 'merge_request_id' => 3, 'user_id' => user_2.id }, + { 'merge_request_id' => 4, 'user_id' => user_3.id } + ] + expected_rows = existing_rows + created_rows + + expect(rows.size).to eq(expected_rows.size) + expected_rows.each do |expected_row| + expect(rows).to include(expected_row) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb b/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb new file mode 100644 index 00000000000..740781f1aa5 --- /dev/null +++ b/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Gitlab::BackgroundMigration::ResetMergeStatus, :migration, schema: 20190528180441 do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') } + let(:merge_requests) { table(:merge_requests) } + + def create_merge_request(id, extra_params = {}) + params = { + id: id, + target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: 'mr name', + title: "mr name#{id}" + }.merge(extra_params) + + merge_requests.create!(params) + end + + it 'correctly updates opened mergeable MRs to unchecked' do + create_merge_request(1, state: 'opened', merge_status: 'can_be_merged') + create_merge_request(2, state: 'opened', merge_status: 'can_be_merged') + create_merge_request(3, state: 'opened', merge_status: 'can_be_merged') + create_merge_request(4, state: 'merged', merge_status: 'can_be_merged') + create_merge_request(5, state: 'opened', merge_status: 'cannot_be_merged') + + subject.perform(1, 5) + + expected_rows = [ + { id: 1, state: 'opened', merge_status: 'unchecked' }, + { id: 2, state: 'opened', merge_status: 'unchecked' }, + { id: 3, state: 'opened', merge_status: 'unchecked' }, + { id: 4, state: 'merged', merge_status: 'can_be_merged' }, + { id: 5, state: 'opened', merge_status: 'cannot_be_merged' } + ] + + rows = merge_requests.order(:id).map do |row| + row.attributes.slice('id', 'state', 'merge_status').symbolize_keys + end + + expect(rows).to eq(expected_rows) + end +end diff --git a/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb b/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb new file mode 100644 index 00000000000..d494ce68c5b --- /dev/null +++ b/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20190527194900_schedule_calculate_wiki_sizes.rb') + +describe ScheduleCalculateWikiSizes, :migration, :sidekiq do + let(:migration_class) { Gitlab::BackgroundMigration::CalculateWikiSizes } + let(:migration_name) { migration_class.to_s.demodulize } + + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:project_statistics) { table(:project_statistics) } + + context 'when missing wiki sizes exist' do + before do + namespaces.create!(id: 1, name: 'wiki-migration', path: 'wiki-migration') + projects.create!(id: 1, name: 'wiki-project-1', path: 'wiki-project-1', namespace_id: 1) + projects.create!(id: 2, name: 'wiki-project-2', path: 'wiki-project-2', namespace_id: 1) + projects.create!(id: 3, name: 'wiki-project-3', path: 'wiki-project-3', namespace_id: 1) + project_statistics.create!(id: 1, project_id: 1, namespace_id: 1, wiki_size: 1000) + project_statistics.create!(id: 2, project_id: 2, namespace_id: 1, wiki_size: nil) + project_statistics.create!(id: 3, project_id: 3, namespace_id: 1, wiki_size: nil) + end + + it 'schedules a background migration' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(migration_name).to be_scheduled_delayed_migration(5.minutes, 2, 3) + expect(BackgroundMigrationWorker.jobs.size).to eq 1 + end + end + end + + it 'calculates missing wiki sizes' do + expect(project_statistics.find_by(id: 2).wiki_size).to be_nil + expect(project_statistics.find_by(id: 3).wiki_size).to be_nil + + migrate! + + expect(project_statistics.find_by(id: 2).wiki_size).not_to be_nil + expect(project_statistics.find_by(id: 3).wiki_size).not_to be_nil + end + end + + context 'when missing wiki sizes do not exist' do + before do + namespaces.create!(id: 1, name: 'wiki-migration', path: 'wiki-migration') + projects.create!(id: 1, name: 'wiki-project-1', path: 'wiki-project-1', namespace_id: 1) + project_statistics.create!(id: 1, project_id: 1, namespace_id: 1, wiki_size: 1000) + end + + it 'does not schedule a background migration' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(BackgroundMigrationWorker.jobs.size).to eq 0 + end + end + end + end +end diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb index 7d3d8a949ef..1d0ffb5e9df 100644 --- a/spec/lib/gitlab/background_migration_spec.rb +++ b/spec/lib/gitlab/background_migration_spec.rb @@ -195,4 +195,44 @@ describe Gitlab::BackgroundMigration do end end end + + describe '.dead_jobs?' do + let(:queue) do + [double(args: ['Foo', [10, 20]], queue: described_class.queue)] + end + + context 'when there are dead jobs present' do + before do + allow(Sidekiq::DeadSet).to receive(:new).and_return(queue) + end + + it 'returns true if specific job exists' do + expect(described_class.dead_jobs?('Foo')).to eq(true) + end + + it 'returns false if specific job does not exist' do + expect(described_class.dead_jobs?('Bar')).to eq(false) + end + end + end + + describe '.retrying_jobs?' do + let(:queue) do + [double(args: ['Foo', [10, 20]], queue: described_class.queue)] + end + + context 'when there are dead jobs present' do + before do + allow(Sidekiq::RetrySet).to receive(:new).and_return(queue) + end + + it 'returns true if specific job exists' do + expect(described_class.retrying_jobs?('Foo')).to eq(true) + end + + it 'returns false if specific job does not exist' do + expect(described_class.retrying_jobs?('Bar')).to eq(false) + end + end + end end diff --git a/spec/lib/gitlab/badge/pipeline/template_spec.rb b/spec/lib/gitlab/badge/pipeline/template_spec.rb index 20fa4f879c3..bcef0b7e120 100644 --- a/spec/lib/gitlab/badge/pipeline/template_spec.rb +++ b/spec/lib/gitlab/badge/pipeline/template_spec.rb @@ -59,6 +59,16 @@ describe Gitlab::Badge::Pipeline::Template do end end + context 'when status is preparing' do + before do + allow(badge).to receive(:status).and_return('preparing') + end + + it 'has expected color' do + expect(template.value_color).to eq '#dfb317' + end + end + context 'when status is unknown' do before do allow(badge).to receive(:status).and_return('unknown') diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index c432cc223b9..2e90f6c7f71 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -5,6 +5,7 @@ describe Gitlab::BitbucketImport::Importer do before do stub_omniauth_provider('bitbucket') + stub_feature_flags(stricter_mr_branch_name: false) end let(:statuses) do @@ -95,6 +96,9 @@ describe Gitlab::BitbucketImport::Importer do subject { described_class.new(project) } describe '#import_pull_requests' do + let(:source_branch_sha) { sample.commits.last } + let(:target_branch_sha) { sample.commits.first } + before do allow(subject).to receive(:import_wiki) allow(subject).to receive(:import_issues) @@ -102,9 +106,9 @@ describe Gitlab::BitbucketImport::Importer do pull_request = instance_double( Bitbucket::Representation::PullRequest, iid: 10, - source_branch_sha: sample.commits.last, + source_branch_sha: source_branch_sha, source_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.source_branch, - target_branch_sha: sample.commits.first, + target_branch_sha: target_branch_sha, target_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.target_branch, title: 'This is a title', description: 'This is a test pull request', @@ -162,6 +166,19 @@ describe Gitlab::BitbucketImport::Importer do expect(reply_note).to be_a(DiffNote) expect(reply_note.note).to eq(@reply.note) end + + context "when branches' sha is not found in the repository" do + let(:source_branch_sha) { 'a' * Commit::MIN_SHA_LENGTH } + let(:target_branch_sha) { 'b' * Commit::MIN_SHA_LENGTH } + + it 'uses the pull request sha references' do + expect { subject.execute }.to change { MergeRequest.count }.by(1) + + merge_request_diff = MergeRequest.first.merge_request_diff + expect(merge_request_diff.head_commit_sha).to eq source_branch_sha + expect(merge_request_diff.start_commit_sha).to eq target_branch_sha + end + end end context 'issues statuses' do @@ -206,6 +223,46 @@ describe Gitlab::BitbucketImport::Importer do body: {}.to_json) end + context 'creating labels on project' do + before do + allow(importer).to receive(:import_wiki) + end + + it 'creates labels as expected' do + expect { importer.execute }.to change { Label.count }.from(0).to(Gitlab::BitbucketImport::Importer::LABELS.size) + end + + it 'does not fail if label is already existing' do + label = Gitlab::BitbucketImport::Importer::LABELS.first + ::Labels::CreateService.new(label).execute(project: project) + + expect { importer.execute }.not_to raise_error + end + + it 'does not create new labels' do + Gitlab::BitbucketImport::Importer::LABELS.each do |label| + create(:label, project: project, title: label[:title]) + end + + expect { importer.execute }.not_to change { Label.count } + end + + it 'does not update existing ones' do + label_title = Gitlab::BitbucketImport::Importer::LABELS.first[:title] + existing_label = create(:label, project: project, title: label_title) + # Reload label from database so we avoid timestamp comparison issues related to time precision when comparing + # attributes later. + existing_label.reload + + Timecop.freeze(Time.now + 1.minute) do + importer.execute + + label_after_import = project.labels.find(existing_label.id) + expect(label_after_import.attributes).to eq(existing_label.attributes) + end + end + end + it 'maps statuses to open or closed' do allow(importer).to receive(:import_wiki) diff --git a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb index 1e90a2ef27f..cc09804fd53 100644 --- a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb @@ -105,6 +105,7 @@ describe Gitlab::BitbucketServerImport::Importer do expect(merge_request.metrics.merged_by).to eq(project.owner) expect(merge_request.metrics.merged_at).to eq(@merge_event.merge_timestamp) expect(merge_request.merge_commit_sha).to eq('12345678') + expect(merge_request.state_id).to eq(3) end it 'imports comments' do diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb index 77366e91dca..71b64a3b9df 100644 --- a/spec/lib/gitlab/checks/branch_check_spec.rb +++ b/spec/lib/gitlab/checks/branch_check_spec.rb @@ -48,10 +48,118 @@ describe Gitlab::Checks::BranchCheck do context 'when project repository is empty' do let(:project) { create(:project) } - it 'raises an error if the user is not allowed to push to protected branches' do - expect(user_access).to receive(:can_push_to_branch?).and_return(false) + context 'user is not allowed to push to protected branches' do + before do + allow(user_access) + .to receive(:can_push_to_branch?) + .and_return(false) + end + + it 'raises an error' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /Ask a project Owner or Maintainer to create a default branch/) + end + end + + context 'user is allowed to push to protected branches' do + before do + allow(user_access) + .to receive(:can_push_to_branch?) + .and_return(true) + end + + it 'allows branch creation' do + expect { subject.validate! }.not_to raise_error + end + end + end + + context 'branch creation' do + let(:oldrev) { '0000000000000000000000000000000000000000' } + let(:ref) { 'refs/heads/feature' } + + context 'user can push to branch' do + before do + allow(user_access) + .to receive(:can_push_to_branch?) + .with('feature') + .and_return(true) + end - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /Ask a project Owner or Maintainer to create a default branch/) + it 'does not raise an error' do + expect { subject.validate! }.not_to raise_error + end + end + + context 'user cannot push to branch' do + before do + allow(user_access) + .to receive(:can_push_to_branch?) + .with('feature') + .and_return(false) + end + + context 'user cannot merge to branch' do + before do + allow(user_access) + .to receive(:can_merge_to_branch?) + .with('feature') + .and_return(false) + end + + it 'raises an error' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to create protected branches on this project.') + end + end + + context 'user can merge to branch' do + before do + allow(user_access) + .to receive(:can_merge_to_branch?) + .with('feature') + .and_return(true) + + allow(project.repository) + .to receive(:branch_names_contains_sha) + .with(newrev) + .and_return(['branch']) + end + + context "newrev isn't in any protected branches" do + before do + allow(ProtectedBranch) + .to receive(:any_protected?) + .with(project, ['branch']) + .and_return(false) + end + + it 'raises an error' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only use an existing protected branch ref as the basis of a new protected branch.') + end + end + + context 'newrev is included in a protected branch' do + before do + allow(ProtectedBranch) + .to receive(:any_protected?) + .with(project, ['branch']) + .and_return(true) + end + + context 'via web interface' do + let(:protocol) { 'web' } + + it 'allows branch creation' do + expect { subject.validate! }.not_to raise_error + end + end + + context 'via SSH' do + it 'raises an error' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only create protected branches using the web interface and API.') + end + end + end + end end end diff --git a/spec/lib/gitlab/checks/lfs_check_spec.rb b/spec/lib/gitlab/checks/lfs_check_spec.rb index 35f8069c8a4..dad14e100a7 100644 --- a/spec/lib/gitlab/checks/lfs_check_spec.rb +++ b/spec/lib/gitlab/checks/lfs_check_spec.rb @@ -27,6 +27,18 @@ describe Gitlab::Checks::LfsCheck do allow(project).to receive(:lfs_enabled?).and_return(true) end + context 'with lfs_check feature disabled' do + before do + stub_feature_flags(lfs_check: false) + end + + it 'skips integrity check' do + expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers) + + subject.validate! + end + end + context 'deletion' do let(:changes) { { oldrev: oldrev, ref: ref } } diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb index 773a52cdfbc..6e20e0ef5c3 100644 --- a/spec/lib/gitlab/ci/build/image_spec.rb +++ b/spec/lib/gitlab/ci/build/image_spec.rb @@ -18,11 +18,16 @@ describe Gitlab::Ci::Build::Image do it 'populates fabricated object with the proper name attribute' do expect(subject.name).to eq(image_name) end + + it 'does not populate the ports' do + expect(subject.ports).to be_empty + end end context 'when image is defined as hash' do let(:entrypoint) { '/bin/sh' } - let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint } } ) } + + let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint, ports: [80] } } ) } it 'fabricates an object of the proper class' do is_expected.to be_kind_of(described_class) @@ -32,6 +37,13 @@ describe Gitlab::Ci::Build::Image do expect(subject.name).to eq(image_name) expect(subject.entrypoint).to eq(entrypoint) end + + it 'populates the ports' do + port = subject.ports.first + expect(port.number).to eq 80 + expect(port.protocol).to eq 'http' + expect(port.name).to eq 'default_port' + end end context 'when image name is empty' do @@ -67,6 +79,10 @@ describe Gitlab::Ci::Build::Image do expect(subject.first).to be_kind_of(described_class) expect(subject.first.name).to eq(service_image_name) end + + it 'does not populate the ports' do + expect(subject.first.ports).to be_empty + end end context 'when service is defined as hash' do @@ -75,7 +91,7 @@ describe Gitlab::Ci::Build::Image do let(:service_command) { 'sleep 30' } let(:job) do create(:ci_build, options: { services: [{ name: service_image_name, entrypoint: service_entrypoint, - alias: service_alias, command: service_command }] }) + alias: service_alias, command: service_command, ports: [80] }] }) end it 'fabricates an non-empty array of objects' do @@ -89,6 +105,11 @@ describe Gitlab::Ci::Build::Image do expect(subject.first.entrypoint).to eq(service_entrypoint) expect(subject.first.alias).to eq(service_alias) expect(subject.first.command).to eq(service_command) + + port = subject.first.ports.first + expect(port.number).to eq 80 + expect(port.protocol).to eq 'http' + expect(port.name).to eq 'default_port' end end diff --git a/spec/lib/gitlab/ci/build/policy/changes_spec.rb b/spec/lib/gitlab/ci/build/policy/changes_spec.rb index dc3329061d1..92cf0376c02 100644 --- a/spec/lib/gitlab/ci/build/policy/changes_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/changes_spec.rb @@ -133,7 +133,7 @@ describe Gitlab::Ci::Build::Policy::Changes do let(:seed) { double('build seed', to_resource: ci_build) } context 'when source is merge request' do - let(:source) { :merge_request } + let(:source) { :merge_request_event } let(:merge_request) do create(:merge_request, diff --git a/spec/lib/gitlab/ci/build/policy/refs_spec.rb b/spec/lib/gitlab/ci/build/policy/refs_spec.rb index 553fc0fb9bf..22ca681cfd3 100644 --- a/spec/lib/gitlab/ci/build/policy/refs_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/refs_spec.rb @@ -68,6 +68,20 @@ describe Gitlab::Ci::Build::Policy::Refs do expect(described_class.new(%w[triggers])) .not_to be_satisfied_by(pipeline) end + + context 'when source is merge_request_event' do + let(:pipeline) { build_stubbed(:ci_pipeline, source: :merge_request_event) } + + it 'is satisfied with only: merge_request' do + expect(described_class.new(%w[merge_requests])) + .to be_satisfied_by(pipeline) + end + + it 'is not satisfied with only: merge_request_event' do + expect(described_class.new(%w[merge_request_events])) + .not_to be_satisfied_by(pipeline) + end + end end context 'when matching a ref by a regular expression' do @@ -78,10 +92,49 @@ describe Gitlab::Ci::Build::Policy::Refs do .to be_satisfied_by(pipeline) end + it 'is satisfied when case-insensitive regexp matches pipeline ref' do + expect(described_class.new(['/DOCS-.*/i'])) + .to be_satisfied_by(pipeline) + end + it 'is not satisfied when regexp does not match pipeline ref' do expect(described_class.new(['/fix-.*/'])) .not_to be_satisfied_by(pipeline) end + + context 'when unsafe regexp is used' do + let(:subject) { described_class.new(['/^(?!master).+/']) } + + context 'when allow_unsafe_ruby_regexp is disabled' do + before do + stub_feature_flags(allow_unsafe_ruby_regexp: false) + end + + it 'ignores invalid regexp' do + expect(subject) + .not_to be_satisfied_by(pipeline) + end + end + + context 'when allow_unsafe_ruby_regexp is enabled' do + before do + stub_feature_flags(allow_unsafe_ruby_regexp: true) + end + + it 'is satisfied by regexp' do + expect(subject) + .to be_satisfied_by(pipeline) + end + end + end + end + + context 'malicious regexp' do + let(:pipeline) { build_stubbed(:ci_pipeline, ref: malicious_text) } + + subject { described_class.new([malicious_regexp_ruby]) } + + include_examples 'malicious regexp' end end end diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb index c2c0742efc3..9b016901a20 100644 --- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb @@ -15,6 +15,7 @@ describe Gitlab::Ci::Build::Policy::Variables do before do pipeline.variables.build(key: 'CI_PROJECT_NAME', value: '') + pipeline.variables.build(key: 'MY_VARIABLE', value: 'my-var') end describe '#satisfied_by?' do @@ -24,6 +25,12 @@ describe Gitlab::Ci::Build::Policy::Variables do expect(policy).to be_satisfied_by(pipeline, seed) end + it 'is satisfied by a matching pipeline variable' do + policy = described_class.new(['$MY_VARIABLE']) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + it 'is not satisfied by an overridden empty variable' do policy = described_class.new(['$CI_PROJECT_NAME']) @@ -68,5 +75,19 @@ describe Gitlab::Ci::Build::Policy::Variables do expect(pipeline).not_to be_persisted expect(seed.to_resource).not_to be_persisted end + + context 'when a bridge job is used' do + let(:bridge) do + build(:ci_bridge, pipeline: pipeline, project: project, ref: 'master') + end + + let(:seed) { double('bridge seed', to_resource: bridge) } + + it 'is satisfied by a matching expression for a bridge job' do + policy = described_class.new(['$MY_VARIABLE']) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + end end end diff --git a/spec/lib/gitlab/ci/build/port_spec.rb b/spec/lib/gitlab/ci/build/port_spec.rb new file mode 100644 index 00000000000..1413780dfa6 --- /dev/null +++ b/spec/lib/gitlab/ci/build/port_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Build::Port do + subject { described_class.new(port) } + + context 'when port is defined as an integer' do + let(:port) { 80 } + + it 'populates the object' do + expect(subject.number).to eq 80 + expect(subject.protocol).to eq described_class::DEFAULT_PORT_PROTOCOL + expect(subject.name).to eq described_class::DEFAULT_PORT_NAME + end + end + + context 'when port is defined as hash' do + let(:port) { { number: 80, protocol: 'https', name: 'port_name' } } + + it 'populates the object' do + expect(subject.number).to eq 80 + expect(subject.protocol).to eq 'https' + expect(subject.name).to eq 'port_name' + end + end +end diff --git a/spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb new file mode 100644 index 00000000000..5187f99a441 --- /dev/null +++ b/spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Build::Prerequisite::Factory do + let(:build) { create(:ci_build) } + + describe '.for_build' do + let(:kubernetes_namespace) do + instance_double( + Gitlab::Ci::Build::Prerequisite::KubernetesNamespace, + unmet?: unmet) + end + + subject { described_class.new(build).unmet } + + before do + expect(Gitlab::Ci::Build::Prerequisite::KubernetesNamespace) + .to receive(:new).with(build).and_return(kubernetes_namespace) + end + + context 'prerequisite is unmet' do + let(:unmet) { true } + + it { is_expected.to eq [kubernetes_namespace] } + end + + context 'prerequisite is met' do + let(:unmet) { false } + + it { is_expected.to be_empty } + end + end +end diff --git a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb new file mode 100644 index 00000000000..5387863bd07 --- /dev/null +++ b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do + let(:build) { create(:ci_build) } + + describe '#unmet?' do + subject { described_class.new(build).unmet? } + + context 'build has no deployment' do + before do + expect(build.deployment).to be_nil + end + + it { is_expected.to be_falsey } + end + + context 'build has a deployment' do + let!(:deployment) { create(:deployment, deployable: build) } + + context 'and a cluster to deploy to' do + let(:cluster) { create(:cluster, :group) } + + before do + allow(build.deployment).to receive(:cluster).and_return(cluster) + end + + it { is_expected.to be_truthy } + + context 'and the cluster is not managed' do + let(:cluster) { create(:cluster, :not_managed, projects: [build.project]) } + + it { is_expected.to be_falsey } + end + + context 'and a namespace is already created for this project' do + let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, project: build.project) } + + it { is_expected.to be_falsey } + end + + context 'and cluster is project type' do + let(:cluster) { create(:cluster, :project) } + + it { is_expected.to be_falsey } + end + end + + context 'and no cluster to deploy to' do + before do + expect(deployment.cluster).to be_nil + end + + it { is_expected.to be_falsey } + end + end + end + + describe '#complete!' do + let!(:deployment) { create(:deployment, deployable: build) } + let(:service) { double(execute: true) } + + subject { described_class.new(build).complete! } + + context 'completion is required' do + let(:cluster) { create(:cluster, :group) } + + before do + allow(build.deployment).to receive(:cluster).and_return(cluster) + end + + it 'creates a kubernetes namespace' do + expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService) + .to receive(:new) + .with(cluster: cluster, kubernetes_namespace: instance_of(Clusters::KubernetesNamespace)) + .and_return(service) + + expect(service).to receive(:execute).once + + subject + end + end + + context 'completion is not required' do + before do + expect(deployment.cluster).to be_nil + end + + it 'does not create a namespace' do + expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:new) + + subject + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index 3c0007f4d57..0bc9e8bd3cd 100644 --- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -100,6 +100,26 @@ describe Gitlab::Ci::Config::Entry::Environment do end end + context 'when wrong action type is used' do + let(:config) do + { name: 'production', + action: ['stop'] } + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + + describe '#errors' do + it 'contains error about wrong action type' do + expect(entry.errors) + .to include 'environment action should be a string' + end + end + end + context 'when invalid action is used' do let(:config) do { name: 'production', @@ -151,6 +171,26 @@ describe Gitlab::Ci::Config::Entry::Environment do end end + context 'when wrong url type is used' do + let(:config) do + { name: 'production', + url: ['https://meow.meow'] } + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + + describe '#errors' do + it 'contains error about wrong url type' do + expect(entry.errors) + .to include 'environment url should be a string' + end + end + end + context 'when variables are used for environment' do let(:config) do { name: 'review/$CI_COMMIT_REF_NAME', diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 7651f594a4c..e23efff18d5 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::Ci::Config::Entry::Global do expect(described_class.nodes.keys) .to match_array(%i[before_script image services after_script variables stages - types cache]) + types cache include]) end end end @@ -42,7 +42,7 @@ describe Gitlab::Ci::Config::Entry::Global do end it 'creates node object for each entry' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'creates node object using valid class' do @@ -189,7 +189,7 @@ describe Gitlab::Ci::Config::Entry::Global do describe '#nodes' do it 'instantizes all nodes' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'contains unspecified nodes' do diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index 1a4d9ed5517..1ebdda398b9 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -35,6 +35,12 @@ describe Gitlab::Ci::Config::Entry::Image do expect(entry.entrypoint).to be_nil end end + + describe '#ports' do + it "returns image's ports" do + expect(entry.ports).to be_nil + end + end end context 'when configuration is a hash' do @@ -69,6 +75,38 @@ describe Gitlab::Ci::Config::Entry::Image do expect(entry.entrypoint).to eq %w(/bin/sh run) end end + + context 'when configuration has ports' do + let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + let(:config) { { name: 'ruby:2.2', entrypoint: %w(/bin/sh run), ports: ports } } + let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } + let(:image_ports) { false } + + context 'when with_image_ports metadata is not enabled' do + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors).to include("image config contains disallowed keys: ports") + end + end + end + + context 'when with_image_ports metadata is enabled' do + let(:image_ports) { true } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#ports' do + it "returns image's ports" do + expect(entry.ports).to eq ports + end + end + end + end end context 'when entry value is not correct' do @@ -76,8 +114,8 @@ describe Gitlab::Ci::Config::Entry::Image do describe '#errors' do it 'saves errors' do - expect(entry.errors) - .to include 'image config should be a hash or a string' + expect(entry.errors.first) + .to match /config should be a hash or a string/ end end @@ -93,8 +131,8 @@ describe Gitlab::Ci::Config::Entry::Image do describe '#errors' do it 'saves errors' do - expect(entry.errors) - .to include 'image config contains unknown keys: non_existing' + expect(entry.errors.first) + .to match /config contains unknown keys: non_existing/ 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 0560eb42e4d..e0552ae8c57 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -94,7 +94,7 @@ describe Gitlab::Ci::Config::Entry::Job do it 'returns error about wrong value type' do expect(entry).not_to be_valid - expect(entry.errors).to include "job extends should be a string" + expect(entry.errors).to include "job extends should be an array of strings or a string" end end diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb index 1c987e13a9a..fba5671594d 100644 --- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb @@ -1,4 +1,5 @@ require 'fast_spec_helper' +require 'support/helpers/stub_feature_flags' require_dependency 'active_model' describe Gitlab::Ci::Config::Entry::Policy do @@ -33,6 +34,44 @@ describe Gitlab::Ci::Config::Entry::Policy do end end + context 'when config is an empty regexp' do + let(:config) { ['//'] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when using unsafe regexp' do + include StubFeatureFlags + + let(:config) { ['/^(?!master).+/'] } + + subject { described_class.new([regexp]) } + + context 'when allow_unsafe_ruby_regexp is disabled' do + before do + stub_feature_flags(allow_unsafe_ruby_regexp: false) + end + + it 'is not valid' do + expect(entry).not_to be_valid + end + end + + context 'when allow_unsafe_ruby_regexp is enabled' do + before do + stub_feature_flags(allow_unsafe_ruby_regexp: true) + end + + it 'is valid' do + expect(entry).to be_valid + end + end + end + context 'when config is a special keyword' do let(:config) { %w[tags triggers branches] } @@ -67,6 +106,34 @@ describe Gitlab::Ci::Config::Entry::Policy do end end + context 'when using unsafe regexp' do + include StubFeatureFlags + + let(:config) { { refs: ['/^(?!master).+/'] } } + + subject { described_class.new([regexp]) } + + context 'when allow_unsafe_ruby_regexp is disabled' do + before do + stub_feature_flags(allow_unsafe_ruby_regexp: false) + end + + it 'is not valid' do + expect(entry).not_to be_valid + end + end + + context 'when allow_unsafe_ruby_regexp is enabled' do + before do + stub_feature_flags(allow_unsafe_ruby_regexp: true) + end + + it 'is valid' do + expect(entry).to be_valid + end + end + end + context 'when specifying kubernetes policy' do let(:config) { { kubernetes: 'active' } } diff --git a/spec/lib/gitlab/ci/config/entry/port_spec.rb b/spec/lib/gitlab/ci/config/entry/port_spec.rb new file mode 100644 index 00000000000..5f8f294334e --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/port_spec.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Port do + let(:entry) { described_class.new(config) } + + before do + entry.compose! + end + + context 'when configuration is a string' do + let(:config) { 80 } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid hash' do + expect(entry.value).to eq(number: 80) + end + end + + describe '#number' do + it "returns port number" do + expect(entry.number).to eq 80 + end + end + + describe '#protocol' do + it "is nil" do + expect(entry.protocol).to be_nil + end + end + + describe '#name' do + it "is nil" do + expect(entry.name).to be_nil + end + end + end + + context 'when configuration is a hash' do + context 'with the complete hash' do + let(:config) do + { number: 80, + protocol: 'http', + name: 'foobar' } + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid hash' do + expect(entry.value).to eq config + end + end + + describe '#number' do + it "returns port number" do + expect(entry.number).to eq 80 + end + end + + describe '#protocol' do + it "returns port protocol" do + expect(entry.protocol).to eq 'http' + end + end + + describe '#name' do + it "returns port name" do + expect(entry.name).to eq 'foobar' + end + end + end + + context 'with only the port number' do + let(:config) { { number: 80 } } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid hash' do + expect(entry.value).to eq(number: 80) + end + end + + describe '#number' do + it "returns port number" do + expect(entry.number).to eq 80 + end + end + + describe '#protocol' do + it "is nil" do + expect(entry.protocol).to be_nil + end + end + + describe '#name' do + it "is nil" do + expect(entry.name).to be_nil + end + end + end + + context 'without the number' do + let(:config) { { protocol: 'http' } } + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end + end + + context 'when configuration is invalid' do + let(:config) { '80' } + + describe '#valid?' do + it 'is valid' do + expect(entry).not_to be_valid + end + end + end + + context 'when protocol' do + let(:config) { { number: 80, protocol: protocol, name: 'foobar' } } + + context 'is http' do + let(:protocol) { 'http' } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'is https' do + let(:protocol) { 'https' } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'is neither http nor https' do + let(:protocol) { 'foo' } + + describe '#valid?' do + it 'is invalid' do + expect(entry.errors).to include("port protocol should be http or https") + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/ports_spec.rb b/spec/lib/gitlab/ci/config/entry/ports_spec.rb new file mode 100644 index 00000000000..2063bd1d86c --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/ports_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Ports do + let(:entry) { described_class.new(config) } + + before do + entry.compose! + end + + context 'when configuration is valid' do + let(:config) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid array' do + expect(entry.value).to eq(config) + end + end + end + + context 'when configuration is invalid' do + let(:config) { 'postgresql:9.5' } + + describe '#valid?' do + it 'is invalid' do + expect(entry).not_to be_valid + end + end + + context 'when any of the ports' do + before do + expect(entry).not_to be_valid + expect(entry.errors.count).to eq 1 + end + + context 'have the same name' do + let(:config) do + [{ number: 80, protocol: 'http', name: 'foobar' }, + { number: 81, protocol: 'http', name: 'foobar' }] + end + + describe '#valid?' do + it 'is invalid' do + expect(entry.errors.first).to match /each port name must be different/ + end + end + end + + context 'have the same port' do + let(:config) do + [{ number: 80, protocol: 'http', name: 'foobar' }, + { number: 80, protocol: 'http', name: 'foobar1' }] + end + + describe '#valid?' do + it 'is invalid' do + expect(entry.errors.first).to match /each port number can only be referenced once/ + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 9ebf947a751..d31866a1987 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -39,6 +39,12 @@ describe Gitlab::Ci::Config::Entry::Service do expect(entry.command).to be_nil end end + + describe '#ports' do + it "returns service's ports" do + expect(entry.ports).to be_nil + end + end end context 'when configuration is a hash' do @@ -81,6 +87,50 @@ describe Gitlab::Ci::Config::Entry::Service do expect(entry.entrypoint).to eq %w(/bin/sh run) end end + + context 'when configuration has ports' do + let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + let(:config) do + { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports } + end + let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } + let(:image_ports) { false } + + context 'when with_image_ports metadata is not enabled' do + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors).to include("service config contains disallowed keys: ports") + end + end + end + + context 'when with_image_ports metadata is enabled' do + let(:image_ports) { true } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + + context 'when unknown port keys detected' do + let(:ports) { [{ number: 80, invalid_key: 'foo' }] } + + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors.first) + .to match /port config contains unknown keys: invalid_key/ + end + end + end + + describe '#ports' do + it "returns image's ports" do + expect(entry.ports).to eq ports + end + end + end + end end context 'when entry value is not correct' do @@ -88,8 +138,8 @@ describe Gitlab::Ci::Config::Entry::Service do describe '#errors' do it 'saves errors' do - expect(entry.errors) - .to include 'service config should be a hash or a string' + expect(entry.errors.first) + .to match /config should be a hash or a string/ end end @@ -105,8 +155,8 @@ describe Gitlab::Ci::Config::Entry::Service do describe '#errors' do it 'saves errors' do - expect(entry.errors) - .to include 'service config contains unknown keys: non_existing' + expect(entry.errors.first) + .to match /config contains unknown keys: non_existing/ end end @@ -116,4 +166,26 @@ describe Gitlab::Ci::Config::Entry::Service do end end end + + context 'when service has ports' do + let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + let(:config) do + { name: 'postgresql:9.5', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports } + end + + it 'alias field is mandatory' do + expect(entry).not_to be_valid + expect(entry.errors).to include("service alias can't be blank") + end + end + + context 'when service does not have ports' do + let(:config) do + { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) } + end + + it 'alias field is optional' do + expect(entry).to be_valid + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/services_spec.rb b/spec/lib/gitlab/ci/config/entry/services_spec.rb index 7c4319aee63..d5a1316f665 100644 --- a/spec/lib/gitlab/ci/config/entry/services_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/services_spec.rb @@ -32,4 +32,91 @@ describe Gitlab::Ci::Config::Entry::Services do end end end + + context 'when configuration has ports' do + let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } + let(:config) { ['postgresql:9.5', { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }] } + let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } + let(:image_ports) { false } + + context 'when with_image_ports metadata is not enabled' do + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors).to include("service config contains disallowed keys: ports") + end + end + end + + context 'when with_image_ports metadata is enabled' do + let(:image_ports) { true } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid array' do + expect(entry.value).to eq([{ name: 'postgresql:9.5' }, { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }]) + end + end + + describe 'services alias' do + context 'when they are not unique' do + let(:config) do + ['postgresql:9.5', + { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] }, + { name: 'ruby', alias: 'postgres_old', ports: [81] }] + end + + describe '#valid?' do + it 'is invalid' do + expect(entry).not_to be_valid + expect(entry.errors).to include("services config alias must be unique in services with ports") + end + end + end + + context 'when they are unique' do + let(:config) do + ['postgresql:9.5', + { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] }, + { name: 'ruby', alias: 'ruby', ports: [81] }] + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when one of the duplicated alias is in a service without ports' do + let(:config) do + ['postgresql:9.5', + { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] }, + { name: 'ruby', alias: 'postgres_old' }] + end + + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'when there are not any ports' do + let(:config) do + ['postgresql:9.5', + { name: 'postgresql:9.1', alias: 'postgres_old' }, + { name: 'ruby', alias: 'postgres_old' }] + end + + it 'is valid' do + expect(entry).to be_valid + end + end + end + end + end end diff --git a/spec/lib/gitlab/ci/config/extendable/entry_spec.rb b/spec/lib/gitlab/ci/config/extendable/entry_spec.rb index 0a148375d11..d63612053b6 100644 --- a/spec/lib/gitlab/ci/config/extendable/entry_spec.rb +++ b/spec/lib/gitlab/ci/config/extendable/entry_spec.rb @@ -44,12 +44,12 @@ describe Gitlab::Ci::Config::Extendable::Entry do end end - describe '#extends_key' do + describe '#extends_keys' do context 'when entry is extensible' do it 'returns symbolized extends key value' do entry = described_class.new(:test, test: { extends: 'something' }) - expect(entry.extends_key).to eq :something + expect(entry.extends_keys).to eq [:something] end end @@ -57,7 +57,7 @@ describe Gitlab::Ci::Config::Extendable::Entry do it 'returns nil' do entry = described_class.new(:test, test: 'something') - expect(entry.extends_key).to be_nil + expect(entry.extends_keys).to be_nil end end end @@ -76,7 +76,7 @@ describe Gitlab::Ci::Config::Extendable::Entry do end end - describe '#base_hash!' do + describe '#base_hashes!' do subject { described_class.new(:test, hash) } context 'when base hash is not extensible' do @@ -87,8 +87,8 @@ describe Gitlab::Ci::Config::Extendable::Entry do } end - it 'returns unchanged base hash' do - expect(subject.base_hash!).to eq(script: 'rspec') + it 'returns unchanged base hashes' do + expect(subject.base_hashes!).to eq([{ script: 'rspec' }]) end end @@ -101,12 +101,12 @@ describe Gitlab::Ci::Config::Extendable::Entry do } end - it 'extends the base hash first' do - expect(subject.base_hash!).to eq(extends: 'first', script: 'rspec') + it 'extends the base hashes first' do + expect(subject.base_hashes!).to eq([{ extends: 'first', script: 'rspec' }]) end it 'mutates original context' do - subject.base_hash! + subject.base_hashes! expect(hash.fetch(:second)).to eq(extends: 'first', script: 'rspec') end @@ -171,6 +171,34 @@ describe Gitlab::Ci::Config::Extendable::Entry do end end + context 'when extending multiple hashes correctly' do + let(:hash) do + { + first: { script: 'my value', image: 'ubuntu' }, + second: { image: 'alpine' }, + test: { extends: %w(first second) } + } + end + + let(:result) do + { + first: { script: 'my value', image: 'ubuntu' }, + second: { image: 'alpine' }, + test: { extends: %w(first second), script: 'my value', image: 'alpine' } + } + end + + it 'returns extended part of the hash' do + expect(subject.extend!).to eq result[:test] + end + + it 'mutates original context' do + subject.extend! + + expect(hash).to eq result + end + end + context 'when hash is not extensible' do let(:hash) do { diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb index 1a6b3587599..dd536a241bd 100644 --- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb @@ -3,7 +3,7 @@ require 'fast_spec_helper' describe Gitlab::Ci::Config::External::File::Base do - let(:context) { described_class::Context.new(nil, 'HEAD', nil) } + let(:context) { described_class::Context.new(nil, 'HEAD', nil, Set.new) } let(:test_class) do Class.new(described_class) do @@ -26,7 +26,7 @@ describe Gitlab::Ci::Config::External::File::Base do context 'when a location is present' do let(:location) { 'some-location' } - it 'should return true' do + it 'returns true' do expect(subject).to be_matching end end @@ -34,7 +34,7 @@ describe Gitlab::Ci::Config::External::File::Base do context 'with a location is missing' do let(:location) { nil } - it 'should return false' do + it 'returns false' do expect(subject).not_to be_matching end end @@ -79,4 +79,20 @@ describe Gitlab::Ci::Config::External::File::Base do end end end + + describe '#to_hash' do + context 'with includes' do + let(:location) { 'some/file/config.yml' } + let(:content) { 'include: { template: Bash.gitlab-ci.yml }'} + + before do + allow_any_instance_of(test_class) + .to receive(:content).and_return(content) + end + + it 'does expand hash to include the template' do + expect(subject.to_hash).to include(:before_script) + end + end + end end diff --git a/spec/lib/gitlab/ci/config/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb index ff67a765da0..9451db9522a 100644 --- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb @@ -4,8 +4,10 @@ require 'spec_helper' describe Gitlab::Ci::Config::External::File::Local do set(:project) { create(:project, :repository) } + set(:user) { create(:user) } - let(:context) { described_class::Context.new(project, '12345', nil) } + let(:sha) { '12345' } + let(:context) { described_class::Context.new(project, sha, user, Set.new) } let(:params) { { local: location } } let(:local_file) { described_class.new(params, context) } @@ -13,7 +15,7 @@ describe Gitlab::Ci::Config::External::File::Local do context 'when a local is specified' do let(:params) { { local: 'file' } } - it 'should return true' do + it 'returns true' do expect(local_file).to be_matching end end @@ -21,7 +23,7 @@ describe Gitlab::Ci::Config::External::File::Local do context 'with a missing local' do let(:params) { { local: nil } } - it 'should return false' do + it 'returns false' do expect(local_file).not_to be_matching end end @@ -29,7 +31,7 @@ describe Gitlab::Ci::Config::External::File::Local do context 'with a missing local key' do let(:params) { {} } - it 'should return false' do + it 'returns false' do expect(local_file).not_to be_matching end end @@ -43,7 +45,7 @@ describe Gitlab::Ci::Config::External::File::Local do allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return("image: 'ruby2:2'") end - it 'should return true' do + it 'returns true' do expect(local_file.valid?).to be_truthy end end @@ -51,7 +53,7 @@ describe Gitlab::Ci::Config::External::File::Local do context 'when is not a valid local path' do let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' } - it 'should return false' do + it 'returns false' do expect(local_file.valid?).to be_falsy end end @@ -59,7 +61,7 @@ describe Gitlab::Ci::Config::External::File::Local do context 'when is not a yaml file' do let(:location) { '/config/application.rb' } - it 'should return false' do + it 'returns false' do expect(local_file.valid?).to be_falsy end end @@ -82,7 +84,7 @@ describe Gitlab::Ci::Config::External::File::Local do allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return(local_file_content) end - it 'should return the content of the file' do + it 'returns the content of the file' do expect(local_file.content).to eq(local_file_content) end end @@ -90,7 +92,7 @@ describe Gitlab::Ci::Config::External::File::Local do context 'with an invalid file' do let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' } - it 'should be nil' do + it 'is nil' do expect(local_file.content).to be_nil end end @@ -99,8 +101,40 @@ describe Gitlab::Ci::Config::External::File::Local do describe '#error_message' do let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' } - it 'should return an error message' do + it 'returns an error message' do expect(local_file.error_message).to eq("Local file `#{location}` does not exist!") end end + + describe '#expand_context' do + let(:location) { 'location.yml' } + + subject { local_file.send(:expand_context) } + + it 'inherits project, user and sha' do + is_expected.to include(user: user, project: project, sha: sha) + end + end + + describe '#to_hash' do + context 'properly includes another local file in the same repository' do + let(:location) { 'some/file/config.yml' } + let(:content) { 'include: { local: another-config.yml }'} + + let(:another_location) { 'another-config.yml' } + let(:another_content) { 'rspec: JOB' } + + before do + allow(project.repository).to receive(:blob_data_at).with(sha, location) + .and_return(content) + + allow(project.repository).to receive(:blob_data_at).with(sha, another_location) + .and_return(another_content) + end + + it 'does expand hash to include the template' do + expect(local_file.to_hash).to include(:rspec) + end + end + end end diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb index 11809adcaf6..4acb4f324d3 100644 --- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb @@ -3,12 +3,13 @@ require 'spec_helper' describe Gitlab::Ci::Config::External::File::Project do + set(:context_project) { create(:project) } set(:project) { create(:project, :repository) } set(:user) { create(:user) } let(:context_user) { user } - let(:context) { described_class::Context.new(nil, '12345', context_user) } - let(:subject) { described_class.new(params, context) } + let(:context) { described_class::Context.new(context_project, '12345', context_user, Set.new) } + let(:project_file) { described_class.new(params, context) } before do project.add_developer(user) @@ -18,32 +19,32 @@ describe Gitlab::Ci::Config::External::File::Project do context 'when a file and project is specified' do let(:params) { { file: 'file.yml', project: 'project' } } - it 'should return true' do - expect(subject).to be_matching + it 'returns true' do + expect(project_file).to be_matching end end context 'with only file is specified' do let(:params) { { file: 'file.yml' } } - it 'should return false' do - expect(subject).not_to be_matching + it 'returns false' do + expect(project_file).not_to be_matching end end context 'with only project is specified' do let(:params) { { project: 'project' } } - it 'should return false' do - expect(subject).not_to be_matching + it 'returns false' do + expect(project_file).not_to be_matching end end context 'with a missing local key' do let(:params) { {} } - it 'should return false' do - expect(subject).not_to be_matching + it 'returns false' do + expect(project_file).not_to be_matching end end end @@ -60,16 +61,16 @@ describe Gitlab::Ci::Config::External::File::Project do stub_project_blob(root_ref_sha, '/file.yml') { 'image: ruby:2.1' } end - it 'should return true' do - expect(subject).to be_valid + it 'returns true' do + expect(project_file).to be_valid end context 'when user does not have permission to access file' do let(:context_user) { create(:user) } - it 'should return false' do - expect(subject).not_to be_valid - expect(subject.error_message).to include("Project `#{project.full_path}` not found or access denied!") + it 'returns false' do + expect(project_file).not_to be_valid + expect(project_file.error_message).to include("Project `#{project.full_path}` not found or access denied!") end end end @@ -85,8 +86,8 @@ describe Gitlab::Ci::Config::External::File::Project do stub_project_blob(ref_sha, '/file.yml') { 'image: ruby:2.1' } end - it 'should return true' do - expect(subject).to be_valid + it 'returns true' do + expect(project_file).to be_valid end end @@ -101,9 +102,9 @@ describe Gitlab::Ci::Config::External::File::Project do stub_project_blob(root_ref_sha, '/file.yml') { '' } end - it 'should return false' do - expect(subject).not_to be_valid - expect(subject.error_message).to include("Project `#{project.full_path}` file `/file.yml` is empty!") + it 'returns false' do + expect(project_file).not_to be_valid + expect(project_file.error_message).to include("Project `#{project.full_path}` file `/file.yml` is empty!") end end @@ -112,9 +113,9 @@ describe Gitlab::Ci::Config::External::File::Project do { project: project.full_path, ref: 'I-Do-Not-Exist', file: '/file.yml' } end - it 'should return false' do - expect(subject).not_to be_valid - expect(subject.error_message).to include("Project `#{project.full_path}` reference `I-Do-Not-Exist` does not exist!") + it 'returns false' do + expect(project_file).not_to be_valid + expect(project_file.error_message).to include("Project `#{project.full_path}` reference `I-Do-Not-Exist` does not exist!") end end @@ -123,9 +124,9 @@ describe Gitlab::Ci::Config::External::File::Project do { project: project.full_path, file: '/invalid-file.yml' } end - it 'should return false' do - expect(subject).not_to be_valid - expect(subject.error_message).to include("Project `#{project.full_path}` file `/invalid-file.yml` does not exist!") + it 'returns false' do + expect(project_file).not_to be_valid + expect(project_file.error_message).to include("Project `#{project.full_path}` file `/invalid-file.yml` does not exist!") end end @@ -134,13 +135,23 @@ describe Gitlab::Ci::Config::External::File::Project do { project: project.full_path, file: '/invalid-file' } end - it 'should return false' do - expect(subject).not_to be_valid - expect(subject.error_message).to include('Included file `/invalid-file` does not have YAML extension!') + it 'returns false' do + expect(project_file).not_to be_valid + expect(project_file.error_message).to include('Included file `/invalid-file` does not have YAML extension!') end end end + describe '#expand_context' do + let(:params) { { file: 'file.yml', project: project.full_path, ref: 'master' } } + + subject { project_file.send(:expand_context) } + + it 'inherits user, and target project and sha' do + is_expected.to include(user: user, project: project, sha: project.commit('master').id) + end + end + private def stub_project_blob(ref, path) diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb index 3e0fda9c308..46d68097fff 100644 --- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb @@ -3,7 +3,9 @@ require 'spec_helper' describe Gitlab::Ci::Config::External::File::Remote do - let(:context) { described_class::Context.new(nil, '12345', nil) } + include StubRequests + + let(:context) { described_class::Context.new(nil, '12345', nil, Set.new) } let(:params) { { remote: location } } let(:remote_file) { described_class.new(params, context) } let(:location) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } @@ -21,7 +23,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'when a remote is specified' do let(:params) { { remote: 'http://remote' } } - it 'should return true' do + it 'returns true' do expect(remote_file).to be_matching end end @@ -29,7 +31,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'with a missing remote' do let(:params) { { remote: nil } } - it 'should return false' do + it 'returns false' do expect(remote_file).not_to be_matching end end @@ -37,7 +39,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'with a missing remote key' do let(:params) { {} } - it 'should return false' do + it 'returns false' do expect(remote_file).not_to be_matching end end @@ -46,10 +48,10 @@ describe Gitlab::Ci::Config::External::File::Remote do describe "#valid?" do context 'when is a valid remote url' do before do - WebMock.stub_request(:get, location).to_return(body: remote_file_content) + stub_full_request(location).to_return(body: remote_file_content) end - it 'should return true' do + it 'returns true' do expect(remote_file.valid?).to be_truthy end end @@ -57,7 +59,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'with an irregular url' do let(:location) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } - it 'should return false' do + it 'returns false' do expect(remote_file.valid?).to be_falsy end end @@ -67,7 +69,7 @@ describe Gitlab::Ci::Config::External::File::Remote do allow(Gitlab::HTTP).to receive(:get).and_raise(Timeout::Error) end - it 'should be falsy' do + it 'is falsy' do expect(remote_file.valid?).to be_falsy end end @@ -75,7 +77,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'when is not a yaml file' do let(:location) { 'https://asdasdasdaj48ggerexample.com' } - it 'should be falsy' do + it 'is falsy' do expect(remote_file.valid?).to be_falsy end end @@ -83,7 +85,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'with an internal url' do let(:location) { 'http://localhost:8080' } - it 'should be falsy' do + it 'is falsy' do expect(remote_file.valid?).to be_falsy end end @@ -92,10 +94,10 @@ describe Gitlab::Ci::Config::External::File::Remote do describe "#content" do context 'with a valid remote file' do before do - WebMock.stub_request(:get, location).to_return(body: remote_file_content) + stub_full_request(location).to_return(body: remote_file_content) end - it 'should return the content of the file' do + it 'returns the content of the file' do expect(remote_file.content).to eql(remote_file_content) end end @@ -105,7 +107,7 @@ describe Gitlab::Ci::Config::External::File::Remote do allow(Gitlab::HTTP).to receive(:get).and_raise(Timeout::Error) end - it 'should be falsy' do + it 'is falsy' do expect(remote_file.content).to be_falsy end end @@ -114,10 +116,10 @@ describe Gitlab::Ci::Config::External::File::Remote do let(:location) { 'https://asdasdasdaj48ggerexample.com' } before do - WebMock.stub_request(:get, location).to_raise(SocketError.new('Some HTTP error')) + stub_full_request(location).to_raise(SocketError.new('Some HTTP error')) end - it 'should be nil' do + it 'is nil' do expect(remote_file.content).to be_nil end end @@ -125,7 +127,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'with an internal url' do let(:location) { 'http://localhost:8080' } - it 'should be nil' do + it 'is nil' do expect(remote_file.content).to be_nil end end @@ -144,30 +146,30 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'when timeout error has been raised' do before do - WebMock.stub_request(:get, location).to_timeout + stub_full_request(location).to_timeout end - it 'should returns error message about a timeout' do + it 'returns error message about a timeout' do expect(subject).to match /could not be fetched because of a timeout error!/ end end context 'when HTTP error has been raised' do before do - WebMock.stub_request(:get, location).to_raise(Gitlab::HTTP::Error) + stub_full_request(location).to_raise(Gitlab::HTTP::Error) end - it 'should returns error message about a HTTP error' do + it 'returns error message about a HTTP error' do expect(subject).to match /could not be fetched because of HTTP error!/ end end context 'when response has 404 status' do before do - WebMock.stub_request(:get, location).to_return(body: remote_file_content, status: 404) + stub_full_request(location).to_return(body: remote_file_content, status: 404) end - it 'should returns error message about a timeout' do + it 'returns error message about a timeout' do expect(subject).to match /could not be fetched because of HTTP code `404` error!/ end end @@ -175,10 +177,20 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'when the URL is blocked' do let(:location) { 'http://127.0.0.1/some/path/to/config.yaml' } - it 'should include details about blocked URL' do + it 'includes details about blocked URL' do expect(subject).to eq "Remote file could not be fetched because URL '#{location}' " \ 'is blocked: Requests to localhost are not allowed!' end end end + + describe '#expand_context' do + let(:params) { { remote: 'http://remote' } } + + subject { remote_file.send(:expand_context) } + + it 'drops all parameters' do + is_expected.to include(user: nil, project: nil, sha: nil) + end + end end diff --git a/spec/lib/gitlab/ci/config/external/file/template_spec.rb b/spec/lib/gitlab/ci/config/external/file/template_spec.rb index 1fb5655309a..1609b8fd66b 100644 --- a/spec/lib/gitlab/ci/config/external/file/template_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/template_spec.rb @@ -3,34 +3,37 @@ require 'spec_helper' describe Gitlab::Ci::Config::External::File::Template do - let(:context) { described_class::Context.new(nil, '12345') } + set(:project) { create(:project) } + set(:user) { create(:user) } + + let(:context) { described_class::Context.new(project, '12345', user, Set.new) } let(:template) { 'Auto-DevOps.gitlab-ci.yml' } let(:params) { { template: template } } - subject { described_class.new(params, context) } + let(:template_file) { described_class.new(params, context) } describe '#matching?' do context 'when a template is specified' do let(:params) { { template: 'some-template' } } - it 'should return true' do - expect(subject).to be_matching + it 'returns true' do + expect(template_file).to be_matching end end context 'with a missing template' do let(:params) { { template: nil } } - it 'should return false' do - expect(subject).not_to be_matching + it 'returns false' do + expect(template_file).not_to be_matching end end context 'with a missing template key' do let(:params) { {} } - it 'should return false' do - expect(subject).not_to be_matching + it 'returns false' do + expect(template_file).not_to be_matching end end end @@ -39,32 +42,32 @@ describe Gitlab::Ci::Config::External::File::Template do context 'when is a valid template name' do let(:template) { 'Auto-DevOps.gitlab-ci.yml' } - it 'should return true' do - expect(subject).to be_valid + it 'returns true' do + expect(template_file).to be_valid end end context 'with invalid template name' do let(:template) { 'Template.yml' } - it 'should return false' do - expect(subject).not_to be_valid - expect(subject.error_message).to include('Template file `Template.yml` is not a valid location!') + it 'returns false' do + expect(template_file).not_to be_valid + expect(template_file.error_message).to include('Template file `Template.yml` is not a valid location!') end end context 'with a non-existing template' do let(:template) { 'I-Do-Not-Have-This-Template.gitlab-ci.yml' } - it 'should return false' do - expect(subject).not_to be_valid - expect(subject.error_message).to include('Included file `I-Do-Not-Have-This-Template.gitlab-ci.yml` is empty or does not exist!') + it 'returns false' do + expect(template_file).not_to be_valid + expect(template_file.error_message).to include('Included file `I-Do-Not-Have-This-Template.gitlab-ci.yml` is empty or does not exist!') end end end describe '#template_name' do - let(:template_name) { subject.send(:template_name) } + let(:template_name) { template_file.send(:template_name) } context 'when template does end with .gitlab-ci.yml' do let(:template) { 'my-template.gitlab-ci.yml' } @@ -90,4 +93,14 @@ describe Gitlab::Ci::Config::External::File::Template do end end end + + describe '#expand_context' do + let(:location) { 'location.yml' } + + subject { template_file.send(:expand_context) } + + it 'drops all parameters' do + is_expected.to include(user: nil, project: nil, sha: nil) + end + end end diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index 4cab4961b0f..e068b786b02 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -3,12 +3,15 @@ require 'spec_helper' describe Gitlab::Ci::Config::External::Mapper do + include StubRequests + set(:project) { create(:project, :repository) } set(:user) { create(:user) } let(:local_file) { '/lib/gitlab/ci/templates/non-existent-file.yml' } let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } let(:template_file) { 'Auto-DevOps.gitlab-ci.yml' } + let(:expandset) { Set.new } let(:file_content) do <<~HEREDOC @@ -17,11 +20,11 @@ describe Gitlab::Ci::Config::External::Mapper do end before do - WebMock.stub_request(:get, remote_url).to_return(body: file_content) + stub_full_request(remote_url).to_return(body: file_content) end describe '#process' do - subject { described_class.new(values, project: project, sha: '123456', user: user).process } + subject { described_class.new(values, project: project, sha: '123456', user: user, expandset: expandset).process } context "when single 'include' keyword is defined" do context 'when the string is a local file' do @@ -141,5 +144,37 @@ describe Gitlab::Ci::Config::External::Mapper do expect(subject).to be_empty end end + + context "when duplicate 'include' is defined" do + let(:values) do + { include: [ + { 'local' => local_file }, + { 'local' => local_file } + ], + image: 'ruby:2.2' } + end + + it 'raises an exception' do + expect { subject }.to raise_error(described_class::DuplicateIncludesError) + end + end + + context "when too many 'includes' are defined" do + let(:values) do + { include: [ + { 'local' => local_file }, + { 'remote' => remote_url } + ], + image: 'ruby:2.2' } + end + + before do + stub_const("#{described_class}::MAX_INCLUDES", 1) + end + + it 'raises an exception' do + expect { subject }.to raise_error(described_class::TooManyIncludesError) + end + end end end diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index 1ac58139b25..856187371e1 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -3,20 +3,27 @@ require 'spec_helper' describe Gitlab::Ci::Config::External::Processor do + include StubRequests + set(:project) { create(:project, :repository) } + set(:another_project) { create(:project, :repository) } set(:user) { create(:user) } - let(:processor) { described_class.new(values, project: project, sha: '12345', user: user) } + let(:expandset) { Set.new } + let(:sha) { '12345' } + let(:processor) { described_class.new(values, project: project, sha: '12345', user: user, expandset: expandset) } before do project.add_developer(user) end describe "#perform" do + subject { processor.perform } + context 'when no external files defined' do let(:values) { { image: 'ruby:2.2' } } - it 'should return the same values' do + it 'returns the same values' do expect(processor.perform).to eq(values) end end @@ -24,7 +31,7 @@ describe Gitlab::Ci::Config::External::Processor do context 'when an invalid local file is defined' do let(:values) { { include: '/lib/gitlab/ci/templates/non-existent-file.yml', image: 'ruby:2.2' } } - it 'should raise an error' do + it 'raises an error' do expect { processor.perform }.to raise_error( described_class::IncludeError, "Local file `/lib/gitlab/ci/templates/non-existent-file.yml` does not exist!" @@ -37,10 +44,10 @@ describe Gitlab::Ci::Config::External::Processor do let(:values) { { include: remote_file, image: 'ruby:2.2' } } before do - WebMock.stub_request(:get, remote_file).to_raise(SocketError.new('Some HTTP error')) + stub_full_request(remote_file).and_raise(SocketError.new('Some HTTP error')) end - it 'should raise an error' do + it 'raises an error' do expect { processor.perform }.to raise_error( described_class::IncludeError, "Remote file `#{remote_file}` could not be fetched because of a socket error!" @@ -70,15 +77,15 @@ describe Gitlab::Ci::Config::External::Processor do end before do - WebMock.stub_request(:get, remote_file).to_return(body: external_file_content) + stub_full_request(remote_file).to_return(body: external_file_content) end - it 'should append the file to the values' do + it 'appends the file to the values' do output = processor.perform expect(output.keys).to match_array([:image, :before_script, :rspec, :rubocop]) end - it "should remove the 'include' keyword" do + it "removes the 'include' keyword" do expect(processor.perform[:include]).to be_nil end end @@ -100,12 +107,12 @@ describe Gitlab::Ci::Config::External::Processor do .to receive(:fetch_local_content).and_return(local_file_content) end - it 'should append the file to the values' do + it 'appends the file to the values' do output = processor.perform expect(output.keys).to match_array([:image, :before_script]) end - it "should remove the 'include' keyword" do + it "removes the 'include' keyword" do expect(processor.perform[:include]).to be_nil end end @@ -140,14 +147,14 @@ describe Gitlab::Ci::Config::External::Processor do allow_any_instance_of(Gitlab::Ci::Config::External::File::Local) .to receive(:fetch_local_content).and_return(local_file_content) - WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content) + stub_full_request(remote_file).to_return(body: remote_file_content) end - it 'should append the files to the values' do + it 'appends the files to the values' do expect(processor.perform.keys).to match_array([:image, :stages, :before_script, :rspec]) end - it "should remove the 'include' keyword" do + it "removes the 'include' keyword" do expect(processor.perform[:include]).to be_nil end end @@ -162,7 +169,7 @@ describe Gitlab::Ci::Config::External::Processor do .to receive(:fetch_local_content).and_return(local_file_content) end - it 'should raise an error' do + it 'raises an error' do expect { processor.perform }.to raise_error( described_class::IncludeError, "Included file `/lib/gitlab/ci/templates/template.yml` does not have valid YAML syntax!" @@ -185,10 +192,111 @@ describe Gitlab::Ci::Config::External::Processor do HEREDOC end - it 'should take precedence' do - WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content) + it 'takes precedence' do + stub_full_request(remote_file).to_return(body: remote_file_content) + expect(processor.perform[:image]).to eq('ruby:2.2') end end + + context "when a nested includes are defined" do + let(:values) do + { + include: [ + { local: '/local/file.yml' } + ], + image: 'ruby:2.2' + } + end + + before do + allow(project.repository).to receive(:blob_data_at).with('12345', '/local/file.yml') do + <<~HEREDOC + include: + - template: Ruby.gitlab-ci.yml + - remote: http://my.domain.com/config.yml + - project: #{another_project.full_path} + file: /templates/my-workflow.yml + HEREDOC + end + + allow_any_instance_of(Repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-workflow.yml') do + <<~HEREDOC + include: + - local: /templates/my-build.yml + HEREDOC + end + + allow_any_instance_of(Repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-build.yml') do + <<~HEREDOC + my_build: + script: echo Hello World + HEREDOC + end + + stub_full_request('http://my.domain.com/config.yml') + .to_return(body: 'remote_build: { script: echo Hello World }') + end + + context 'when project is public' do + before do + another_project.update!(visibility: 'public') + end + + it 'properly expands all includes' do + is_expected.to include(:my_build, :remote_build, :rspec) + end + end + + context 'when user is reporter of another project' do + before do + another_project.add_reporter(user) + end + + it 'properly expands all includes' do + is_expected.to include(:my_build, :remote_build, :rspec) + end + end + + context 'when user is not allowed' do + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError, /not found or access denied/) + end + end + + context 'when too many includes is included' do + before do + stub_const('Gitlab::Ci::Config::External::Mapper::MAX_INCLUDES', 1) + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError, /Maximum of 1 nested/) + end + end + end + + context 'when config includes an external configuration file via SSL web request' do + before do + stub_full_request('https://sha256.badssl.com/fake.yml', ip_address: '8.8.8.8') + .to_return(body: 'image: ruby:2.6', status: 200) + + stub_full_request('https://self-signed.badssl.com/fake.yml', ip_address: '8.8.8.9') + .to_raise(OpenSSL::SSL::SSLError.new('SSL_connect returned=1 errno=0 state=error: certificate verify failed (self signed certificate)')) + end + + context 'with an acceptable certificate' do + let(:values) { { include: 'https://sha256.badssl.com/fake.yml' } } + + it { is_expected.to include(image: 'ruby:2.6') } + end + + context 'with a self-signed certificate' do + let(:values) { { include: 'https://self-signed.badssl.com/fake.yml' } } + + it 'returns a reportable configuration error' do + expect { subject }.to raise_error(described_class::IncludeError, /certificate verify failed/) + end + end + end end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 18f255c1ab7..7f336ee853e 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::Ci::Config do + include StubRequests + set(:user) { create(:user) } let(:config) do @@ -123,6 +125,63 @@ describe Gitlab::Ci::Config do ) end end + + context 'when ports have been set' do + context 'in the main image' do + let(:yml) do + <<-EOS + image: + name: ruby:2.2 + ports: + - 80 + EOS + end + + it 'raises an error' do + expect(config.errors).to include("image config contains disallowed keys: ports") + end + end + + context 'in the job image' do + let(:yml) do + <<-EOS + image: ruby:2.2 + + test: + script: rspec + image: + name: ruby:2.2 + ports: + - 80 + EOS + end + + it 'raises an error' do + expect(config.errors).to include("jobs:test:image config contains disallowed keys: ports") + end + end + + context 'in the services' do + let(:yml) do + <<-EOS + image: ruby:2.2 + + test: + script: rspec + image: ruby:2.2 + services: + - name: test + alias: test + ports: + - 80 + EOS + end + + it 'raises an error' do + expect(config.errors).to include("jobs:test:services:service config contains disallowed keys: ports") + end + end + end end context "when using 'include' directive" do @@ -133,7 +192,6 @@ describe Gitlab::Ci::Config do let(:remote_file_content) do <<~HEREDOC variables: - AUTO_DEVOPS_DOMAIN: domain.example.com POSTGRES_USER: user POSTGRES_PASSWORD: testing-password POSTGRES_ENABLED: "true" @@ -160,22 +218,20 @@ describe Gitlab::Ci::Config do end before do - WebMock.stub_request(:get, remote_location) - .to_return(body: remote_file_content) + stub_full_request(remote_location).to_return(body: remote_file_content) allow(project.repository) .to receive(:blob_data_at).and_return(local_file_content) end context "when gitlab_ci_yml has valid 'include' defined" do - it 'should return a composed hash' do + it 'returns a composed hash' do before_script_values = [ "apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs", "ruby -v", "which ruby", "bundle install --jobs $(nproc) \"${FLAGS[@]}\"" ] variables = { - AUTO_DEVOPS_DOMAIN: "domain.example.com", POSTGRES_USER: "user", POSTGRES_PASSWORD: "testing-password", POSTGRES_ENABLED: "true", @@ -259,7 +315,7 @@ describe Gitlab::Ci::Config do HEREDOC end - it 'should take precedence' do + it 'takes precedence' do expect(config.to_hash).to eq({ image: 'ruby:2.2' }) end end @@ -284,7 +340,7 @@ describe Gitlab::Ci::Config do HEREDOC end - it 'should merge the variables dictionaries' do + it 'merges the variables dictionaries' do expect(config.to_hash).to eq({ variables: { A: 'alpha', B: 'beta', C: 'gamma', D: 'delta' } }) end end diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb index 2a3f7807fdb..491e3fba9d9 100644 --- a/spec/lib/gitlab/ci/cron_parser_spec.rb +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -174,6 +174,13 @@ describe Gitlab::Ci::CronParser do it { expect(subject).to be_nil } end + + context 'when cron is scheduled to a non existent day' do + let(:cron) { '0 12 31 2 *' } + let(:cron_timezone) { 'UTC' } + + it { expect(subject).to be_nil } + end end describe '#cron_valid?' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb index fab071405df..3debd42ac65 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb @@ -96,11 +96,13 @@ describe Gitlab::Ci::Pipeline::Chain::Build do context 'when pipeline is running for a merge request' do let(:command) do Gitlab::Ci::Pipeline::Chain::Command.new( - source: :merge_request, + source: :merge_request_event, origin_ref: 'feature', checkout_sha: project.commit.id, after_sha: nil, before_sha: nil, + source_sha: merge_request.diff_head_sha, + target_sha: merge_request.target_branch_sha, trigger_request: nil, schedule: nil, merge_request: merge_request, @@ -115,8 +117,13 @@ describe Gitlab::Ci::Pipeline::Chain::Build do end it 'correctly indicated that this is a merge request pipeline' do - expect(pipeline).to be_merge_request + expect(pipeline).to be_merge_request_event expect(pipeline.merge_request).to eq(merge_request) end + + it 'correctly sets souce sha and target sha to pipeline' do + expect(pipeline.source_sha).to eq(merge_request.diff_head_sha) + expect(pipeline.target_sha).to eq(merge_request.target_branch_sha) + end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb index 6aa802ce6fd..5181e9c1583 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -48,6 +48,24 @@ describe Gitlab::Ci::Pipeline::Chain::Command do end end + describe '#merge_request_ref_exists?' do + subject { command.merge_request_ref_exists? } + + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + + context 'for existing merge request ref' do + let(:origin_ref) { merge_request.ref_path } + + it { is_expected.to eq(true) } + end + + context 'for branch ref' do + let(:origin_ref) { merge_request.source_branch } + + it { is_expected.to eq(false) } + end + end + describe '#ref' do subject { command.ref } @@ -161,6 +179,54 @@ describe Gitlab::Ci::Pipeline::Chain::Command do end end + describe '#source_sha' do + subject { command.source_sha } + + let(:command) do + described_class.new(project: project, + source_sha: source_sha, + merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, target_project: project, source_project: project) + end + + let(:source_sha) { nil } + + context 'when source_sha is specified' do + let(:source_sha) { 'abc' } + + it 'returns the specified value' do + is_expected.to eq('abc') + end + end + end + + describe '#target_sha' do + subject { command.target_sha } + + let(:command) do + described_class.new(project: project, + target_sha: target_sha, + merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, target_project: project, source_project: project) + end + + let(:target_sha) { nil } + + context 'when target_sha is specified' do + let(:target_sha) { 'abc' } + + it 'returns the specified value' do + is_expected.to eq('abc') + end + end + end + describe '#protected_ref?' do let(:command) { described_class.new(project: project, origin_ref: 'my-branch') } diff --git a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb index dc13cae961c..c7f4fc98ca3 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb @@ -23,7 +23,7 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do step.perform! end - it 'should break the chain' do + it 'breaks the chain' do expect(step.break?).to be true end @@ -37,11 +37,11 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do step.perform! end - it 'should not break the chain' do + it 'does not break the chain' do expect(step.break?).to be false end - it 'should not skip a pipeline chain' do + it 'does not skip a pipeline chain' do expect(pipeline.reload).not_to be_skipped end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb index 8ba56d73838..7d750877d09 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb @@ -10,12 +10,33 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do let(:command) do Gitlab::Ci::Pipeline::Chain::Command.new( - project: project, current_user: user, origin_ref: ref) + project: project, current_user: user, origin_ref: origin_ref, merge_request: merge_request) end let(:step) { described_class.new(pipeline, command) } let(:ref) { 'master' } + let(:origin_ref) { ref } + let(:merge_request) { nil } + + shared_context 'detached merge request pipeline' do + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: ref, + target_project: project, + target_branch: 'feature') + end + + let(:pipeline) do + build(:ci_pipeline, + source: :merge_request_event, + merge_request: merge_request, + project: project) + end + + let(:origin_ref) { merge_request.ref_path } + end context 'when users has no ability to run a pipeline' do before do @@ -58,6 +79,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do it { is_expected.to be_truthy } + context 'when pipeline is a detached merge request pipeline' do + include_context 'detached merge request pipeline' + + it { is_expected.to be_truthy } + end + context 'when the branch is protected' do let!(:protected_branch) do create(:protected_branch, project: project, name: ref) @@ -65,6 +92,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do it { is_expected.to be_falsey } + context 'when pipeline is a detached merge request pipeline' do + include_context 'detached merge request pipeline' + + it { is_expected.to be_falsey } + end + context 'when developers are allowed to merge' do let!(:protected_branch) do create(:protected_branch, @@ -74,6 +107,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do end it { is_expected.to be_truthy } + + context 'when pipeline is a detached merge request pipeline' do + include_context 'detached merge request pipeline' + + it { is_expected.to be_truthy } + end end end @@ -112,6 +151,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do end it { is_expected.to be_truthy } + + context 'when pipeline is a detached merge request pipeline' do + include_context 'detached merge request pipeline' + + it { is_expected.to be_truthy } + end end context 'when the tag is protected' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb index 053bc421649..e6c6a82b463 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb @@ -115,7 +115,7 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do let(:pipeline) { build_stubbed(:ci_pipeline, project: project) } let(:merge_request_pipeline) do - build(:ci_pipeline, source: :merge_request, project: project) + build(:ci_pipeline, source: :merge_request_event, project: project) end let(:chain) { described_class.new(merge_request_pipeline, command).tap(&:perform!) } diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb index a7cad423d09..2e8c9d70098 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb @@ -42,6 +42,25 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do end end + context 'when origin ref is a merge request ref' do + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, current_user: user, origin_ref: origin_ref, checkout_sha: checkout_sha) + end + + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:origin_ref) { merge_request.ref_path } + let(:checkout_sha) { project.repository.commit(merge_request.ref_path).id } + + it 'does not break the chain' do + expect(step.break?).to be false + end + + it 'does not append pipeline errors' do + expect(pipeline.errors).to be_empty + end + end + context 'when ref is ambiguous' do let(:project) do create(:project, :repository).tap do |proj| diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb new file mode 100644 index 00000000000..006ce4d8078 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb @@ -0,0 +1,77 @@ +require 'fast_spec_helper' +require 'rspec-parameterized' + +describe Gitlab::Ci::Pipeline::Expression::Lexeme::And do + let(:left) { double('left', evaluate: nil) } + let(:right) { double('right', evaluate: nil) } + + describe '.build' do + it 'creates a new instance of the token' do + expect(described_class.build('&&', left, right)).to be_a(described_class) + end + + context 'with non-evaluable operands' do + let(:left) { double('left') } + let(:right) { double('right') } + + it 'raises an operator error' do + expect { described_class.build('&&', left, right) }.to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError + end + end + end + + describe '.type' do + it 'is an operator' do + expect(described_class.type).to eq :operator + end + end + + describe '.precedence' do + it 'has a precedence' do + expect(described_class.precedence).to be_an Integer + end + end + + describe '#evaluate' do + let(:operator) { described_class.new(left, right) } + + subject { operator.evaluate } + + before do + allow(left).to receive(:evaluate).and_return(left_value) + allow(right).to receive(:evaluate).and_return(right_value) + end + + context 'when left and right are truthy' do + where(:left_value, :right_value) do + [true, 1, 'a'].permutation(2).to_a + end + + with_them do + it { is_expected.to be_truthy } + it { is_expected.to eq(right_value) } + end + end + + context 'when left or right is falsey' do + where(:left_value, :right_value) do + [true, false, nil].permutation(2).to_a + end + + with_them do + it { is_expected.to be_falsey } + end + end + + context 'when left and right are falsey' do + where(:left_value, :right_value) do + [false, nil].permutation(2).to_a + end + + with_them do + it { is_expected.to be_falsey } + it { is_expected.to eq(left_value) } + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb index 019a2ed184d..fcbd2863289 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb @@ -5,9 +5,21 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Equals do let(:right) { double('right') } describe '.build' do - it 'creates a new instance of the token' do - expect(described_class.build('==', left, right)) - .to be_a(described_class) + context 'with non-evaluable operands' do + it 'creates a new instance of the token' do + expect { described_class.build('==', left, right) } + .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError + end + end + + context 'with evaluable operands' do + it 'creates a new instance of the token' do + allow(left).to receive(:evaluate).and_return('my-string') + allow(right).to receive(:evaluate).and_return('my-string') + + expect(described_class.build('==', left, right)) + .to be_a(described_class) + end end end @@ -17,23 +29,40 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Equals do end end + describe '.precedence' do + it 'has a precedence' do + expect(described_class.precedence).to be_an Integer + end + end + describe '#evaluate' do - it 'returns false when left and right are not equal' do - allow(left).to receive(:evaluate).and_return(1) - allow(right).to receive(:evaluate).and_return(2) + let(:operator) { described_class.new(left, right) } - operator = described_class.new(left, right) + subject { operator.evaluate } - expect(operator.evaluate(VARIABLE: 3)).to eq false + before do + allow(left).to receive(:evaluate).and_return(left_value) + allow(right).to receive(:evaluate).and_return(right_value) end - it 'returns true when left and right are equal' do - allow(left).to receive(:evaluate).and_return(1) - allow(right).to receive(:evaluate).and_return(1) + context 'when left and right are equal' do + where(:left_value, :right_value) do + [%w(string string)] + end + + with_them do + it { is_expected.to eq(true) } + end + end - operator = described_class.new(left, right) + context 'when left and right are not equal' do + where(:left_value, :right_value) do + ['one string', 'two string'].permutation(2).to_a + end - expect(operator.evaluate(VARIABLE: 3)).to eq true + with_them do + it { is_expected.to eq(false) } + end end end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb index 49e5af52f4d..97da66d2bcc 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb @@ -6,9 +6,21 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do let(:right) { double('right') } describe '.build' do - it 'creates a new instance of the token' do - expect(described_class.build('=~', left, right)) - .to be_a(described_class) + context 'with non-evaluable operands' do + it 'creates a new instance of the token' do + expect { described_class.build('=~', left, right) } + .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError + end + end + + context 'with evaluable operands' do + it 'creates a new instance of the token' do + allow(left).to receive(:evaluate).and_return('my-string') + allow(right).to receive(:evaluate).and_return('/my-string/') + + expect(described_class.build('=~', left, right)) + .to be_a(described_class) + end end end @@ -18,63 +30,93 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do end end + describe '.precedence' do + it 'has a precedence' do + expect(described_class.precedence).to be_an Integer + end + end + describe '#evaluate' do - it 'returns false when left and right do not match' do - allow(left).to receive(:evaluate).and_return('my-string') - allow(right).to receive(:evaluate) - .and_return(Gitlab::UntrustedRegexp.new('something')) + let(:operator) { described_class.new(left, right) } - operator = described_class.new(left, right) + subject { operator.evaluate } - expect(operator.evaluate).to eq false + before do + allow(left).to receive(:evaluate).and_return(left_value) + allow(right).to receive(:evaluate).and_return(right_value) end - it 'returns true when left and right match' do - allow(left).to receive(:evaluate).and_return('my-awesome-string') - allow(right).to receive(:evaluate) - .and_return(Gitlab::UntrustedRegexp.new('awesome.string$')) + context 'when left and right do not match' do + let(:left_value) { 'my-string' } + let(:right_value) { Gitlab::UntrustedRegexp.new('something') } - operator = described_class.new(left, right) - - expect(operator.evaluate).to eq true + it { is_expected.to eq(nil) } end - it 'supports matching against a nil value' do - allow(left).to receive(:evaluate).and_return(nil) - allow(right).to receive(:evaluate) - .and_return(Gitlab::UntrustedRegexp.new('pattern')) + context 'when left and right match' do + let(:left_value) { 'my-awesome-string' } + let(:right_value) { Gitlab::UntrustedRegexp.new('awesome.string$') } + + it { is_expected.to eq(3) } + end - operator = described_class.new(left, right) + context 'when left is nil' do + let(:left_value) { nil } + let(:right_value) { Gitlab::UntrustedRegexp.new('pattern') } - expect(operator.evaluate).to eq false + it { is_expected.to eq(nil) } end - it 'supports multiline strings' do - allow(left).to receive(:evaluate).and_return <<~TEXT - My awesome contents + context 'when left is a multiline string and matches right' do + let(:left_value) do + <<~TEXT + My awesome contents + + My-text-string! + TEXT + end + + let(:right_value) { Gitlab::UntrustedRegexp.new('text-string') } + + it { is_expected.to eq(24) } + end - My-text-string! - TEXT + context 'when left is a multiline string and does not match right' do + let(:left_value) do + <<~TEXT + My awesome contents - allow(right).to receive(:evaluate) - .and_return(Gitlab::UntrustedRegexp.new('text-string')) + My-terrible-string! + TEXT + end - operator = described_class.new(left, right) + let(:right_value) { Gitlab::UntrustedRegexp.new('text-string') } - expect(operator.evaluate).to eq true + it { is_expected.to eq(nil) } end - it 'supports regexp flags' do - allow(left).to receive(:evaluate).and_return <<~TEXT - My AWESOME content - TEXT + context 'when a matching pattern uses regex flags' do + let(:left_value) do + <<~TEXT + My AWESOME content + TEXT + end + + let(:right_value) { Gitlab::UntrustedRegexp.new('(?i)awesome') } + + it { is_expected.to eq(3) } + end - allow(right).to receive(:evaluate) - .and_return(Gitlab::UntrustedRegexp.new('(?i)awesome')) + context 'when a non-matching pattern uses regex flags' do + let(:left_value) do + <<~TEXT + My AWESOME content + TEXT + end - operator = described_class.new(left, right) + let(:right_value) { Gitlab::UntrustedRegexp.new('(?i)terrible') } - expect(operator.evaluate).to eq true + it { is_expected.to eq(nil) } end end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb new file mode 100644 index 00000000000..38d30c9035a --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotEquals do + let(:left) { double('left') } + let(:right) { double('right') } + + describe '.build' do + context 'with non-evaluable operands' do + it 'creates a new instance of the token' do + expect { described_class.build('!=', left, right) } + .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError + end + end + + context 'with evaluable operands' do + it 'creates a new instance of the token' do + allow(left).to receive(:evaluate).and_return('my-string') + allow(right).to receive(:evaluate).and_return('my-string') + + expect(described_class.build('!=', left, right)) + .to be_a(described_class) + end + end + end + + describe '.type' do + it 'is an operator' do + expect(described_class.type).to eq :operator + end + end + + describe '.precedence' do + it 'has a precedence' do + expect(described_class.precedence).to be_an Integer + end + end + + describe '#evaluate' do + let(:operator) { described_class.new(left, right) } + + subject { operator.evaluate } + + before do + allow(left).to receive(:evaluate).and_return(left_value) + allow(right).to receive(:evaluate).and_return(right_value) + end + + context 'when left and right are equal' do + using RSpec::Parameterized::TableSyntax + + where(:left_value, :right_value) do + 'string' | 'string' + 1 | 1 + '' | '' + nil | nil + end + + with_them do + it { is_expected.to eq(false) } + end + end + + context 'when left and right are not equal' do + where(:left_value, :right_value) do + ['one string', 'two string', 1, 2, '', nil, false, true].permutation(2).to_a + end + + with_them do + it { is_expected.to eq(true) } + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb new file mode 100644 index 00000000000..99110ff8d88 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb @@ -0,0 +1,122 @@ +require 'fast_spec_helper' +require_dependency 're2' + +describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do + let(:left) { double('left') } + let(:right) { double('right') } + + describe '.build' do + context 'with non-evaluable operands' do + it 'creates a new instance of the token' do + expect { described_class.build('!~', left, right) } + .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError + end + end + + context 'with evaluable operands' do + it 'creates a new instance of the token' do + allow(left).to receive(:evaluate).and_return('my-string') + allow(right).to receive(:evaluate).and_return('my-string') + + expect(described_class.build('!~', left, right)) + .to be_a(described_class) + end + end + end + + describe '.type' do + it 'is an operator' do + expect(described_class.type).to eq :operator + end + end + + describe '.precedence' do + it 'has a precedence' do + expect(described_class.precedence).to be_an Integer + end + end + + describe '#evaluate' do + let(:operator) { described_class.new(left, right) } + + subject { operator.evaluate } + + before do + allow(left).to receive(:evaluate).and_return(left_value) + allow(right).to receive(:evaluate).and_return(right_value) + end + + context 'when left and right do not match' do + let(:left_value) { 'my-string' } + let(:right_value) { Gitlab::UntrustedRegexp.new('something') } + + it { is_expected.to eq(true) } + end + + context 'when left and right match' do + let(:left_value) { 'my-awesome-string' } + let(:right_value) { Gitlab::UntrustedRegexp.new('awesome.string$') } + + it { is_expected.to eq(false) } + end + + context 'when left is nil' do + let(:left_value) { nil } + let(:right_value) { Gitlab::UntrustedRegexp.new('pattern') } + + it { is_expected.to eq(true) } + end + + context 'when left is a multiline string and matches right' do + let(:left_value) do + <<~TEXT + My awesome contents + + My-text-string! + TEXT + end + + let(:right_value) { Gitlab::UntrustedRegexp.new('text-string') } + + it { is_expected.to eq(false) } + end + + context 'when left is a multiline string and does not match right' do + let(:left_value) do + <<~TEXT + My awesome contents + + My-terrible-string! + TEXT + end + + let(:right_value) { Gitlab::UntrustedRegexp.new('text-string') } + + it { is_expected.to eq(true) } + end + + context 'when a matching pattern uses regex flags' do + let(:left_value) do + <<~TEXT + My AWESOME content + TEXT + end + + let(:right_value) { Gitlab::UntrustedRegexp.new('(?i)awesome') } + + it { is_expected.to eq(false) } + end + + context 'when a non-matching pattern uses regex flags' do + let(:left_value) do + <<~TEXT + My AWESOME content + TEXT + end + + let(:right_value) { Gitlab::UntrustedRegexp.new('(?i)terrible') } + + it { is_expected.to eq(true) } + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb new file mode 100644 index 00000000000..d542eebc613 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb @@ -0,0 +1,77 @@ +require 'fast_spec_helper' +require 'rspec-parameterized' + +describe Gitlab::Ci::Pipeline::Expression::Lexeme::Or do + let(:left) { double('left', evaluate: nil) } + let(:right) { double('right', evaluate: nil) } + + describe '.build' do + it 'creates a new instance of the token' do + expect(described_class.build('||', left, right)).to be_a(described_class) + end + + context 'with non-evaluable operands' do + let(:left) { double('left') } + let(:right) { double('right') } + + it 'raises an operator error' do + expect { described_class.build('||', left, right) }.to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError + end + end + end + + describe '.type' do + it 'is an operator' do + expect(described_class.type).to eq :operator + end + end + + describe '.precedence' do + it 'has a precedence' do + expect(described_class.precedence).to be_an Integer + end + end + + describe '#evaluate' do + let(:operator) { described_class.new(left, right) } + + subject { operator.evaluate } + + before do + allow(left).to receive(:evaluate).and_return(left_value) + allow(right).to receive(:evaluate).and_return(right_value) + end + + context 'when left and right are truthy' do + where(:left_value, :right_value) do + [true, 1, 'a'].permutation(2).to_a + end + + with_them do + it { is_expected.to be_truthy } + it { is_expected.to eq(left_value) } + end + end + + context 'when left or right is truthy' do + where(:left_value, :right_value) do + [true, false, 'a'].permutation(2).to_a + end + + with_them do + it { is_expected.to be_truthy } + end + end + + context 'when left and right are falsey' do + where(:left_value, :right_value) do + [false, nil].permutation(2).to_a + end + + with_them do + it { is_expected.to be_falsey } + it { is_expected.to eq(right_value) } + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb index 3ebc2e94727..30ea3f3e28e 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb @@ -1,4 +1,4 @@ -require 'fast_spec_helper' +require 'spec_helper' describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do describe '.build' do @@ -30,16 +30,6 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do .to eq Gitlab::UntrustedRegexp.new('pattern') end - it 'is a greedy scanner for regexp boundaries' do - scanner = StringScanner.new('/some .* / pattern/') - - token = described_class.scan(scanner) - - expect(token).not_to be_nil - expect(token.build.evaluate) - .to eq Gitlab::UntrustedRegexp.new('some .* / pattern') - end - it 'does not allow to use an empty pattern' do scanner = StringScanner.new(%(//)) @@ -68,12 +58,90 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do .to eq Gitlab::UntrustedRegexp.new('(?im)pattern') end - it 'does not support arbitrary flags' do + it 'ignores unsupported flags' do scanner = StringScanner.new('/pattern/x') token = described_class.scan(scanner) - expect(token).to be_nil + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('pattern') + end + + it 'is a eager scanner for regexp boundaries' do + scanner = StringScanner.new('/some .* / pattern/') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('some .* ') + end + + it 'does not match on escaped regexp boundaries' do + scanner = StringScanner.new('/some .* \/ pattern/') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('some .* / pattern') + end + + it 'recognizes \ as an escape character for /' do + scanner = StringScanner.new('/some numeric \/$ pattern/') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('some numeric /$ pattern') + end + + it 'does not recognize \ as an escape character for $' do + scanner = StringScanner.new('/some numeric \$ pattern/') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('some numeric \$ pattern') + end + + context 'with the ci_variables_complex_expressions feature flag disabled' do + before do + stub_feature_flags(ci_variables_complex_expressions: false) + end + + it 'is a greedy scanner for regexp boundaries' do + scanner = StringScanner.new('/some .* / pattern/') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('some .* / pattern') + end + + it 'does not recognize the \ escape character for /' do + scanner = StringScanner.new('/some .* \/ pattern/') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('some .* \/ pattern') + end + + it 'does not recognize the \ escape character for $' do + scanner = StringScanner.new('/some numeric \$ pattern/') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('some numeric \$ pattern') + end end end @@ -85,7 +153,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do end it 'raises error if evaluated regexp is not valid' do - allow(Gitlab::UntrustedRegexp).to receive(:valid?).and_return(true) + allow(Gitlab::UntrustedRegexp::RubySyntax).to receive(:valid?).and_return(true) regexp = described_class.new('/invalid ( .*/') diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb index 3f11b3f7673..d8db9c262a1 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb @@ -58,6 +58,56 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do expect { lexer.tokens } .to raise_error described_class::SyntaxError end + + context 'with complex expressions' do + using RSpec::Parameterized::TableSyntax + + subject { described_class.new(expression).tokens.map(&:value) } + + where(:expression, :tokens) do + '$PRESENT_VARIABLE =~ /my var/ && $EMPTY_VARIABLE =~ /nope/' | ['$PRESENT_VARIABLE', '=~', '/my var/', '&&', '$EMPTY_VARIABLE', '=~', '/nope/'] + '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE' | ['$EMPTY_VARIABLE', '==', '""', '&&', '$PRESENT_VARIABLE'] + '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE != "nope"' | ['$EMPTY_VARIABLE', '==', '""', '&&', '$PRESENT_VARIABLE', '!=', '"nope"'] + '$PRESENT_VARIABLE && $EMPTY_VARIABLE' | ['$PRESENT_VARIABLE', '&&', '$EMPTY_VARIABLE'] + '$PRESENT_VARIABLE =~ /my var/ || $EMPTY_VARIABLE =~ /nope/' | ['$PRESENT_VARIABLE', '=~', '/my var/', '||', '$EMPTY_VARIABLE', '=~', '/nope/'] + '$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE' | ['$EMPTY_VARIABLE', '==', '""', '||', '$PRESENT_VARIABLE'] + '$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE != "nope"' | ['$EMPTY_VARIABLE', '==', '""', '||', '$PRESENT_VARIABLE', '!=', '"nope"'] + '$PRESENT_VARIABLE || $EMPTY_VARIABLE' | ['$PRESENT_VARIABLE', '||', '$EMPTY_VARIABLE'] + '$PRESENT_VARIABLE && null || $EMPTY_VARIABLE == ""' | ['$PRESENT_VARIABLE', '&&', 'null', '||', '$EMPTY_VARIABLE', '==', '""'] + end + + with_them do + it { is_expected.to eq(tokens) } + end + end + + context 'with the ci_variables_complex_expressions feature flag turned off' do + before do + stub_feature_flags(ci_variables_complex_expressions: false) + end + + it 'incorrectly tokenizes conjunctive match statements as one match statement' do + tokens = described_class.new('$PRESENT_VARIABLE =~ /my var/ && $EMPTY_VARIABLE =~ /nope/').tokens + + expect(tokens.map(&:value)).to eq(['$PRESENT_VARIABLE', '=~', '/my var/ && $EMPTY_VARIABLE =~ /nope/']) + end + + it 'incorrectly tokenizes disjunctive match statements as one statement' do + tokens = described_class.new('$PRESENT_VARIABLE =~ /my var/ || $EMPTY_VARIABLE =~ /nope/').tokens + + expect(tokens.map(&:value)).to eq(['$PRESENT_VARIABLE', '=~', '/my var/ || $EMPTY_VARIABLE =~ /nope/']) + end + + it 'raises an error about && operators' do + expect { described_class.new('$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE').tokens } + .to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError).with_message('Unknown lexeme found!') + end + + it 'raises an error about || operators' do + expect { described_class.new('$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE').tokens } + .to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError).with_message('Unknown lexeme found!') + end + end end describe '#lexemes' do diff --git a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb index 2b78b1dd4a7..e88ec5561b6 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb @@ -2,25 +2,67 @@ require 'fast_spec_helper' describe Gitlab::Ci::Pipeline::Expression::Parser do describe '#tree' do - context 'when using operators' do + context 'when using two operators' do + it 'returns a reverse descent parse tree' do + expect(described_class.seed('$VAR1 == "123"').tree) + .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals + end + end + + context 'when using three operators' do it 'returns a reverse descent parse tree' do expect(described_class.seed('$VAR1 == "123" == $VAR2').tree) .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals end end - context 'when using a single token' do + context 'when using a single variable token' do it 'returns a single token instance' do expect(described_class.seed('$VAR').tree) .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Variable end end + context 'when using a single string token' do + it 'returns a single token instance' do + expect(described_class.seed('"some value"').tree) + .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::String + end + end + context 'when expression is empty' do it 'returns a null token' do - expect(described_class.seed('').tree) + expect { described_class.seed('').tree } + .to raise_error Gitlab::Ci::Pipeline::Expression::Parser::ParseError + end + end + + context 'when expression is null' do + it 'returns a null token' do + expect(described_class.seed('null').tree) .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Null end end + + context 'when two value tokens have no operator' do + it 'raises a parsing error' do + expect { described_class.seed('$VAR "text"').tree } + .to raise_error Gitlab::Ci::Pipeline::Expression::Parser::ParseError + end + end + + context 'when an operator has no left side' do + it 'raises an OperatorError' do + expect { described_class.seed('== "123"').tree } + .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError + end + end + + context 'when an operator has no right side' do + it 'raises an OperatorError' do + expect { described_class.seed('$VAR ==').tree } + .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError + end + end end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb index 11e73294f18..057e2f3fbe8 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb @@ -1,4 +1,6 @@ -require 'fast_spec_helper' +# TODO switch this back after the "ci_variables_complex_expressions" feature flag is removed +# require 'fast_spec_helper' +require 'spec_helper' require 'rspec-parameterized' describe Gitlab::Ci::Pipeline::Expression::Statement do @@ -7,8 +9,12 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do end let(:variables) do - { 'PRESENT_VARIABLE' => 'my variable', - EMPTY_VARIABLE: '' } + { + 'PRESENT_VARIABLE' => 'my variable', + 'PATH_VARIABLE' => 'a/path/variable/value', + 'FULL_PATH_VARIABLE' => '/a/full/path/variable/value', + 'EMPTY_VARIABLE' => '' + } end describe '.new' do @@ -21,93 +27,158 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do end end - describe '#parse_tree' do - context 'when expression is empty' do - let(:text) { '' } - - it 'raises an error' do - expect { subject.parse_tree } - .to raise_error described_class::StatementError - end - end + describe '#evaluate' do + using RSpec::Parameterized::TableSyntax - context 'when expression grammar is incorrect' do - table = [ - '$VAR "text"', # missing operator - '== "123"', # invalid left side - '"some string"', # only string provided - '$VAR ==', # invalid right side - 'null', # missing lexemes - '' # empty statement - ] - - table.each do |syntax| - context "when expression grammar is #{syntax.inspect}" do - let(:text) { syntax } - - it 'raises a statement error exception' do - expect { subject.parse_tree } - .to raise_error described_class::StatementError - end - - it 'is an invalid statement' do - expect(subject).not_to be_valid - end - end - end + where(:expression, :value) do + '$PRESENT_VARIABLE == "my variable"' | true + '"my variable" == $PRESENT_VARIABLE' | true + '$PRESENT_VARIABLE == null' | false + '$EMPTY_VARIABLE == null' | false + '"" == $EMPTY_VARIABLE' | true + '$EMPTY_VARIABLE' | '' + '$UNDEFINED_VARIABLE == null' | true + 'null == $UNDEFINED_VARIABLE' | true + '$PRESENT_VARIABLE' | 'my variable' + '$UNDEFINED_VARIABLE' | nil + "$PRESENT_VARIABLE =~ /var.*e$/" | 3 + '$PRESENT_VARIABLE =~ /va\r.*e$/' | nil + '$PRESENT_VARIABLE =~ /va\/r.*e$/' | nil + "$PRESENT_VARIABLE =~ /var.*e$/" | 3 + "$PRESENT_VARIABLE =~ /^var.*/" | nil + "$EMPTY_VARIABLE =~ /var.*/" | nil + "$UNDEFINED_VARIABLE =~ /var.*/" | nil + "$PRESENT_VARIABLE =~ /VAR.*/i" | 3 + '$PATH_VARIABLE =~ /path\/variable/' | 2 + '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/' | 0 + '$FULL_PATH_VARIABLE =~ /\\/path\\/variable\\/value$/' | 7 + '$PRESENT_VARIABLE != "my variable"' | false + '"my variable" != $PRESENT_VARIABLE' | false + '$PRESENT_VARIABLE != null' | true + '$EMPTY_VARIABLE != null' | true + '"" != $EMPTY_VARIABLE' | false + '$UNDEFINED_VARIABLE != null' | false + 'null != $UNDEFINED_VARIABLE' | false + "$PRESENT_VARIABLE !~ /var.*e$/" | false + "$PRESENT_VARIABLE !~ /^var.*/" | true + '$PRESENT_VARIABLE !~ /^v\ar.*/' | true + '$PRESENT_VARIABLE !~ /^v\/ar.*/' | true + "$EMPTY_VARIABLE !~ /var.*/" | true + "$UNDEFINED_VARIABLE !~ /var.*/" | true + "$PRESENT_VARIABLE !~ /VAR.*/i" | false + + '$PRESENT_VARIABLE && "string"' | 'string' + '$PRESENT_VARIABLE && $PRESENT_VARIABLE' | 'my variable' + '$PRESENT_VARIABLE && $EMPTY_VARIABLE' | '' + '$PRESENT_VARIABLE && null' | nil + '"string" && $PRESENT_VARIABLE' | 'my variable' + '$EMPTY_VARIABLE && $PRESENT_VARIABLE' | 'my variable' + 'null && $PRESENT_VARIABLE' | nil + '$EMPTY_VARIABLE && "string"' | 'string' + '$EMPTY_VARIABLE && $EMPTY_VARIABLE' | '' + '"string" && $EMPTY_VARIABLE' | '' + '"string" && null' | nil + 'null && "string"' | nil + '"string" && "string"' | 'string' + 'null && null' | nil + + '$PRESENT_VARIABLE =~ /my var/ && $EMPTY_VARIABLE =~ /nope/' | nil + '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE' | 'my variable' + '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE != "nope"' | true + '$PRESENT_VARIABLE && $EMPTY_VARIABLE' | '' + '$PRESENT_VARIABLE && $UNDEFINED_VARIABLE' | nil + '$UNDEFINED_VARIABLE && $EMPTY_VARIABLE' | nil + '$UNDEFINED_VARIABLE && $PRESENT_VARIABLE' | nil + + '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ && $PATH_VARIABLE =~ /path\/variable/' | 2 + '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ && $PATH_VARIABLE =~ /path\/variable/' | nil + '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ && $PATH_VARIABLE =~ /bad\/path\/variable/' | nil + '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ && $PATH_VARIABLE =~ /bad\/path\/variable/' | nil + + '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ || $PATH_VARIABLE =~ /path\/variable/' | 0 + '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ || $PATH_VARIABLE =~ /path\/variable/' | 2 + '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ || $PATH_VARIABLE =~ /bad\/path\/variable/' | 0 + '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ || $PATH_VARIABLE =~ /bad\/path\/variable/' | nil + + '$PRESENT_VARIABLE =~ /my var/ || $EMPTY_VARIABLE =~ /nope/' | 0 + '$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE' | true + '$PRESENT_VARIABLE != "nope" || $EMPTY_VARIABLE == ""' | true + + '$PRESENT_VARIABLE && null || $EMPTY_VARIABLE == ""' | true + '$PRESENT_VARIABLE || $UNDEFINED_VARIABLE' | 'my variable' + '$UNDEFINED_VARIABLE || $PRESENT_VARIABLE' | 'my variable' + '$UNDEFINED_VARIABLE == null || $PRESENT_VARIABLE' | true + '$PRESENT_VARIABLE || $UNDEFINED_VARIABLE == null' | 'my variable' end - context 'when expression grammar is correct' do - context 'when using an operator' do - let(:text) { '$VAR == "value"' } - - it 'returns a reverse descent parse tree' do - expect(subject.parse_tree) - .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals - end + with_them do + let(:text) { expression } - it 'is a valid statement' do - expect(subject).to be_valid - end + it "evaluates to `#{params[:value].inspect}`" do + expect(subject.evaluate).to eq(value) end - context 'when using a single token' do - let(:text) { '$PRESENT_VARIABLE' } + # This test is used to ensure that our parser + # returns exactly the same results as if we + # were evaluating using ruby's `eval` + context 'when using Ruby eval' do + let(:expression_ruby) do + expression + .gsub(/null/, 'nil') + .gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)/) { "variables['#{Regexp.last_match(1)}']" } + end - it 'returns a single token instance' do - expect(subject.parse_tree) - .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Variable + it 'behaves exactly the same' do + expect(instance_eval(expression_ruby)).to eq(subject.evaluate) end end end - end - describe '#evaluate' do - using RSpec::Parameterized::TableSyntax + context 'with the ci_variables_complex_expressions feature flag disabled' do + before do + stub_feature_flags(ci_variables_complex_expressions: false) + end - where(:expression, :value) do - '$PRESENT_VARIABLE == "my variable"' | true - '"my variable" == $PRESENT_VARIABLE' | true - '$PRESENT_VARIABLE == null' | false - '$EMPTY_VARIABLE == null' | false - '"" == $EMPTY_VARIABLE' | true - '$EMPTY_VARIABLE' | '' - '$UNDEFINED_VARIABLE == null' | true - 'null == $UNDEFINED_VARIABLE' | true - '$PRESENT_VARIABLE' | 'my variable' - '$UNDEFINED_VARIABLE' | nil - "$PRESENT_VARIABLE =~ /var.*e$/" | true - "$PRESENT_VARIABLE =~ /^var.*/" | false - "$EMPTY_VARIABLE =~ /var.*/" | false - "$UNDEFINED_VARIABLE =~ /var.*/" | false - "$PRESENT_VARIABLE =~ /VAR.*/i" | true - end + where(:expression, :value) do + '$PRESENT_VARIABLE == "my variable"' | true + '"my variable" == $PRESENT_VARIABLE' | true + '$PRESENT_VARIABLE == null' | false + '$EMPTY_VARIABLE == null' | false + '"" == $EMPTY_VARIABLE' | true + '$EMPTY_VARIABLE' | '' + '$UNDEFINED_VARIABLE == null' | true + 'null == $UNDEFINED_VARIABLE' | true + '$PRESENT_VARIABLE' | 'my variable' + '$UNDEFINED_VARIABLE' | nil + "$PRESENT_VARIABLE =~ /var.*e$/" | true + "$PRESENT_VARIABLE =~ /^var.*/" | false + "$EMPTY_VARIABLE =~ /var.*/" | false + "$UNDEFINED_VARIABLE =~ /var.*/" | false + "$PRESENT_VARIABLE =~ /VAR.*/i" | true + '$PATH_VARIABLE =~ /path/variable/' | true + '$PATH_VARIABLE =~ /path\/variable/' | true + '$FULL_PATH_VARIABLE =~ /^/a/full/path/variable/value$/' | true + '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/' | true + '$PRESENT_VARIABLE != "my variable"' | false + '"my variable" != $PRESENT_VARIABLE' | false + '$PRESENT_VARIABLE != null' | true + '$EMPTY_VARIABLE != null' | true + '"" != $EMPTY_VARIABLE' | false + '$UNDEFINED_VARIABLE != null' | false + 'null != $UNDEFINED_VARIABLE' | false + "$PRESENT_VARIABLE !~ /var.*e$/" | false + "$PRESENT_VARIABLE !~ /^var.*/" | true + "$EMPTY_VARIABLE !~ /var.*/" | true + "$UNDEFINED_VARIABLE !~ /var.*/" | true + "$PRESENT_VARIABLE !~ /VAR.*/i" | false + end - with_them do - let(:text) { expression } + with_them do + let(:text) { expression } - it "evaluates to `#{params[:value].inspect}`" do - expect(subject.evaluate).to eq value + it "evaluates to `#{params[:value].inspect}`" do + expect(subject.evaluate).to eq value + end end end end @@ -125,6 +196,8 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do '$INVALID = 1' | false "$PRESENT_VARIABLE =~ /var.*/" | true "$UNDEFINED_VARIABLE =~ /var.*/" | false + "$PRESENT_VARIABLE !~ /var.*/" | false + "$UNDEFINED_VARIABLE !~ /var.*/" | true end with_them do diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index b379b08ad62..b6231510b91 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -123,6 +123,35 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.action_path).to include 'retry' end end + + context 'when build has unmet prerequisites' do + let(:build) { create(:ci_build, :prerequisite_failure) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Retryable, + Gitlab::Ci::Status::Build::FailedUnmetPrerequisites] + end + + it 'fabricates a failed with unmet prerequisites build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::FailedUnmetPrerequisites + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'failed' + expect(status.icon).to eq 'status_failed' + expect(status.favicon).to eq 'favicon_status_failed' + expect(status.label).to eq 'failed' + expect(status).to have_details + expect(status).to have_action + expect(status.action_title).to include 'Retry' + expect(status.action_path).to include 'retry' + end + end end context 'when build is a canceled' do diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb index af03d5a1308..2a5915d75d0 100644 --- a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb +++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb @@ -31,7 +31,7 @@ describe Gitlab::Ci::Status::Build::FailedAllowed do describe '#group' do it 'returns status failed with warnings status group' do - expect(subject.group).to eq 'failed_with_warnings' + expect(subject.group).to eq 'failed-with-warnings' end end diff --git a/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb b/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb new file mode 100644 index 00000000000..a4854bdc6b9 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Status::Build::FailedUnmetPrerequisites do + describe '#illustration' do + subject { described_class.new(double).illustration } + + it { is_expected.to include(:image, :size, :title, :content) } + end + + describe '.matches?' do + let(:build) { create(:ci_build, :created) } + + subject { described_class.matches?(build, double) } + + context 'when build has not failed' do + it { is_expected.to be_falsey } + end + + context 'when build has failed' do + before do + build.drop!(failure_reason) + end + + context 'with unmet prerequisites' do + let(:failure_reason) { :unmet_prerequisites } + + it { is_expected.to be_truthy } + end + + context 'with a different error' do + let(:failure_reason) { :runner_system_failure } + + it { is_expected.to be_falsey } + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/preparing_spec.rb b/spec/lib/gitlab/ci/status/build/preparing_spec.rb new file mode 100644 index 00000000000..4d8945845ba --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/preparing_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Preparing do + subject do + described_class.new(double('subject')) + end + + describe '#illustration' do + it { expect(subject.illustration).to include(:image, :size, :title, :content) } + end + + describe '.matches?' do + subject { described_class.matches?(build, nil) } + + context 'when build is preparing' do + let(:build) { create(:ci_build, :preparing) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not preparing' do + let(:build) { create(:ci_build, :success) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/preparing_spec.rb b/spec/lib/gitlab/ci/status/preparing_spec.rb new file mode 100644 index 00000000000..7211c0e506d --- /dev/null +++ b/spec/lib/gitlab/ci/status/preparing_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Status::Preparing do + subject do + described_class.new(double('subject'), nil) + end + + describe '#text' do + it { expect(subject.text).to eq 'preparing' } + end + + describe '#label' do + it { expect(subject.label).to eq 'preparing' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'status_created' } + end + + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_created' } + end + + describe '#group' do + it { expect(subject.group).to eq 'preparing' } + end +end diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index dee4f4efd1b..4b299170c87 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -22,7 +22,7 @@ describe Gitlab::Ci::Status::Stage::Factory do end context 'when stage has a core status' do - HasStatus::AVAILABLE_STATUSES.each do |core_status| + (HasStatus::AVAILABLE_STATUSES - %w(manual skipped scheduled)).each do |core_status| context "when core status is #{core_status}" do before do create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status) @@ -64,4 +64,20 @@ describe Gitlab::Ci::Status::Stage::Factory do expect(status.details_path).to include "pipelines/#{pipeline.id}##{stage.name}" end end + + context 'when stage has manual builds' do + (HasStatus::BLOCKED_STATUS + ['skipped']).each do |core_status| + context "when status is #{core_status}" do + before do + create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status) + create(:commit_status, pipeline: pipeline, stage: 'test', status: core_status) + create(:ci_build, pipeline: pipeline, stage: 'build', status: :manual) + end + + it 'fabricates a play manual status' do + expect(status).to be_a(Gitlab::Ci::Status::Stage::PlayManual) + end + end + end + end end diff --git a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb new file mode 100644 index 00000000000..b0113b00ef0 --- /dev/null +++ b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Status::Stage::PlayManual do + let(:stage) { double('stage') } + let(:play_manual) { described_class.new(stage) } + + describe '#action_icon' do + subject { play_manual.action_icon } + + it { is_expected.to eq('play') } + end + + describe '#action_button_title' do + subject { play_manual.action_button_title } + + it { is_expected.to eq('Play all manual') } + end + + describe '#action_title' do + subject { play_manual.action_title } + + it { is_expected.to eq('Play all manual') } + end + + describe '#action_path' do + let(:stage) { create(:ci_stage_entity, status: 'manual') } + let(:pipeline) { stage.pipeline } + let(:play_manual) { stage.detailed_status(create(:user)) } + + subject { play_manual.action_path } + + it { is_expected.to eq("/#{pipeline.project.full_path}/pipelines/#{pipeline.id}/stages/#{stage.name}/play_manual") } + end + + describe '#action_method' do + subject { play_manual.action_method } + + it { is_expected.to eq(:post) } + end + + describe '.matches?' do + let(:user) { double('user') } + + subject { described_class.matches?(stage, user) } + + context 'when stage is skipped' do + let(:stage) { create(:ci_stage_entity, status: :skipped) } + + it { is_expected.to be_truthy } + end + + context 'when stage is manual' do + let(:stage) { create(:ci_stage_entity, status: :manual) } + + it { is_expected.to be_truthy } + end + + context 'when stage is scheduled' do + let(:stage) { create(:ci_stage_entity, status: :scheduled) } + + it { is_expected.to be_truthy } + end + + context 'when stage is success' do + let(:stage) { create(:ci_stage_entity, status: :success) } + + context 'and does not have manual builds' do + it { is_expected.to be_falsy } + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb index 6d05545d1d8..9493b1d89f2 100644 --- a/spec/lib/gitlab/ci/status/success_warning_spec.rb +++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::Ci::Status::SuccessWarning do end describe '#group' do - it { expect(subject.group).to eq 'success_with_warnings' } + it { expect(subject.group).to eq 'success-with-warnings' } end describe '.matches?' do diff --git a/spec/lib/gitlab/ci/templates/templates_spec.rb b/spec/lib/gitlab/ci/templates/templates_spec.rb index 0dd74399a47..b52064b3036 100644 --- a/spec/lib/gitlab/ci/templates/templates_spec.rb +++ b/spec/lib/gitlab/ci/templates/templates_spec.rb @@ -3,9 +3,32 @@ require 'spec_helper' describe "CI YML Templates" do - Gitlab::Template::GitlabCiYmlTemplate.all.each do |template| - it "#{template.name} should be valid" do - expect { Gitlab::Ci::YamlProcessor.new(template.content) }.not_to raise_error + using RSpec::Parameterized::TableSyntax + + subject { Gitlab::Ci::YamlProcessor.new(content) } + + where(:template_name) do + Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name) + end + + with_them do + let(:content) do + <<~EOS + include: + - template: #{template_name} + + concrete_build_implemented_by_a_user: + stage: test + script: do something + EOS + end + + it 'is valid' do + expect { subject }.not_to raise_error + end + + it 'require default stages to be included' do + expect(subject.stages).to include(*Gitlab::Ci::Config::Entry::Stages.default) end end end diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb index 38626f728d7..e45ea1c2528 100644 --- a/spec/lib/gitlab/ci/trace/stream_spec.rb +++ b/spec/lib/gitlab/ci/trace/stream_spec.rb @@ -414,7 +414,7 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do context 'malicious regexp' do let(:data) { malicious_text } - let(:regex) { malicious_regexp } + let(:regex) { malicious_regexp_re2 } include_examples 'malicious regexp' end diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb index 8bf44acb228..613814df23f 100644 --- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Variables::Collection::Item do let(:expected_value) { variable_value } let(:variable) do - { key: variable_key, value: variable_value, public: true } + { key: variable_key, value: variable_value, public: true, masked: false } end describe '.new' do @@ -88,7 +88,7 @@ describe Gitlab::Ci::Variables::Collection::Item do resource = described_class.fabricate(variable) expect(resource).to be_a(described_class) - expect(resource).to eq(key: 'CI_VAR', value: '123', public: false) + expect(resource).to eq(key: 'CI_VAR', value: '123', public: false, masked: false) end it 'supports using another collection item' do @@ -134,7 +134,7 @@ describe Gitlab::Ci::Variables::Collection::Item do .to_runner_variable expect(runner_variable) - .to eq(key: 'VAR', value: 'value', public: true, file: true) + .to eq(key: 'VAR', value: 'value', public: true, file: true, masked: false) end end end diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb index 5c91816a586..8e732d44d5d 100644 --- a/spec/lib/gitlab/ci/variables/collection_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::Ci::Variables::Collection do describe '.new' do it 'can be initialized with an array' do - variable = { key: 'VAR', value: 'value', public: true } + variable = { key: 'VAR', value: 'value', public: true, masked: false } collection = described_class.new([variable]) @@ -66,6 +66,14 @@ describe Gitlab::Ci::Variables::Collection do expect(collection).to include(key: 'VAR_3', value: '3', public: true) end + it 'does not concatenate resource if it undefined' do + collection = described_class.new([{ key: 'VAR_1', value: '1' }]) + + collection.concat(nil) + + expect(collection).to be_one + end + it 'returns self' do expect(subject.concat([key: 'VAR', value: 'test'])) .to eq subject @@ -93,7 +101,7 @@ describe Gitlab::Ci::Variables::Collection do collection = described_class.new([{ key: 'TEST', value: '1' }]) expect(collection.to_runner_variables) - .to eq [{ key: 'TEST', value: '1', public: true }] + .to eq [{ key: 'TEST', value: '1', public: true, masked: false }] end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 91139d421f5..635b4e556e8 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' module Gitlab module Ci describe YamlProcessor do + include StubRequests + subject { described_class.new(config, user: nil) } describe '#build_attributes' do @@ -602,6 +604,100 @@ module Gitlab end end + describe "Include" do + let(:opts) { {} } + + let(:config) do + { + include: include_content, + rspec: { script: "test" } + } + end + + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), opts) } + + context "when validating a ci config file with no project context" do + context "when a single string is provided" do + let(:include_content) { "/local.gitlab-ci.yml" } + + it "returns a validation error" do + expect { subject }.to raise_error /does not have project/ + end + end + + context "when an array is provided" do + let(:include_content) { ["/local.gitlab-ci.yml"] } + + it "returns a validation error" do + expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, /does not have project/) + end + end + + context "when an array of wrong keyed object is provided" do + let(:include_content) { [{ yolo: "/local.gitlab-ci.yml" }] } + + it "returns a validation error" do + expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) + end + end + + context "when an array of mixed typed objects is provided" do + let(:include_content) do + [ + 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml', + { template: 'Auto-DevOps.gitlab-ci.yml' } + ] + end + + before do + stub_full_request('https://gitlab.com/awesome-project/raw/master/.before-script-template.yml') + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: 'prepare: { script: ls -al }') + end + + it "does not return any error" do + expect { subject }.not_to raise_error + end + end + + context "when the include type is incorrect" do + let(:include_content) { { name: "/local.gitlab-ci.yml" } } + + it "returns an invalid configuration error" do + expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) + end + end + end + + context "when validating a ci config file within a project" do + let(:include_content) { "/local.gitlab-ci.yml" } + let(:project) { create(:project, :repository) } + let(:opts) { { project: project, sha: project.commit.sha } } + + context "when the included internal file is present" do + before do + expect(project.repository).to receive(:blob_data_at) + .and_return(YAML.dump({ job1: { script: 'hello' } })) + end + + it "does not return an error" do + expect { subject }.not_to raise_error + end + end + + context "when the included internal file is not present" do + it "returns an error with missing file details" do + expect { subject }.to raise_error( + Gitlab::Ci::YamlProcessor::ValidationError, + "Local file `#{include_content}` does not exist!" + ) + end + end + end + end + describe "When" do %w(on_success on_failure always).each do |when_state| it "returns #{when_state} when defined" do @@ -1154,7 +1250,7 @@ module Gitlab config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) expect do Gitlab::Ci::YamlProcessor.new(config) - end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services:service config should be a hash or a string") end it "returns errors if job services parameter is not an array" do @@ -1168,7 +1264,7 @@ module Gitlab config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) expect do Gitlab::Ci::YamlProcessor.new(config) - end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services:service config should be a hash or a string") end it "returns error if job configuration is invalid" do @@ -1374,7 +1470,7 @@ module Gitlab expect { Gitlab::Ci::YamlProcessor.new(config) } .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, - 'rspec: unknown key in `extends`') + 'rspec: unknown keys in `extends` (something)') end end diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb index b7924302014..51e5bdc6307 100644 --- a/spec/lib/gitlab/contributions_calendar_spec.rb +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -150,13 +150,13 @@ describe Gitlab::ContributionsCalendar do end describe '#starting_year' do - it "should be the start of last year" do + it "is the start of last year" do expect(calendar.starting_year).to eq(last_year.year) end end describe '#starting_month' do - it "should be the start of this month" do + it "is the start of this month" do expect(calendar.starting_month).to eq(today.month) end end diff --git a/spec/lib/gitlab/correlation_id_spec.rb b/spec/lib/gitlab/correlation_id_spec.rb deleted file mode 100644 index 584d1f48386..00000000000 --- a/spec/lib/gitlab/correlation_id_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -describe Gitlab::CorrelationId do - describe '.use_id' do - it 'yields when executed' do - expect { |blk| described_class.use_id('id', &blk) }.to yield_control - end - - it 'stacks correlation ids' do - described_class.use_id('id1') do - described_class.use_id('id2') do |current_id| - expect(current_id).to eq('id2') - end - end - end - - it 'for missing correlation id it generates random one' do - described_class.use_id('id1') do - described_class.use_id(nil) do |current_id| - expect(current_id).not_to be_empty - expect(current_id).not_to eq('id1') - end - end - end - end - - describe '.current_id' do - subject { described_class.current_id } - - it 'returns last correlation id' do - described_class.use_id('id1') do - described_class.use_id('id2') do - is_expected.to eq('id2') - end - end - end - end - - describe '.current_or_new_id' do - subject { described_class.current_or_new_id } - - context 'when correlation id is set' do - it 'returns last correlation id' do - described_class.use_id('id1') do - is_expected.to eq('id1') - end - end - end - - context 'when correlation id is missing' do - it 'returns a new correlation id' do - expect(described_class).to receive(:new_id) - .and_call_original - - is_expected.not_to be_empty - end - end - end - - describe '.ids' do - subject { described_class.send(:ids) } - - it 'returns empty list if not correlation is used' do - is_expected.to be_empty - end - - it 'returns list if correlation ids are used' do - described_class.use_id('id1') do - described_class.use_id('id2') do - is_expected.to eq(%w(id1 id2)) - end - end - end - end -end diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index caf9fc5442c..909dbffa38f 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -115,9 +115,8 @@ describe Gitlab::CurrentSettings do shared_examples 'a non-persisted ApplicationSetting object' do let(:current_settings) { described_class.current_application_settings } - it 'returns a non-persisted ApplicationSetting object' do - expect(current_settings).to be_a(ApplicationSetting) - expect(current_settings).not_to be_persisted + it 'returns a FakeApplicationSettings object' do + expect(current_settings).to be_a(Gitlab::FakeApplicationSettings) end it 'uses the default value from ApplicationSetting.defaults' do @@ -143,9 +142,19 @@ describe Gitlab::CurrentSettings do it_behaves_like 'a non-persisted ApplicationSetting object' - it 'uses the value from the DB attribute if present and not overriden by an accessor' do + it 'uses the value from the DB attribute if present and not overridden by an accessor' do expect(current_settings.home_page_url).to eq(db_settings.home_page_url) end + + context 'when a new column is used before being migrated' do + before do + allow(ApplicationSetting).to receive(:defaults).and_return({ foo: 'bar' }) + end + + it 'uses the default value if present' do + expect(current_settings.foo).to eq('bar') + end + end end end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index 00cb1e6446a..f7642182a17 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -2,7 +2,6 @@ require 'fast_spec_helper' require 'rspec-parameterized' -require 'webmock/rspec' require 'gitlab/danger/helper' @@ -19,39 +18,6 @@ describe Gitlab::Danger::Helper do end end - let(:teammate_json) do - <<~JSON - [ - { - "username": "in-gitlab-ce", - "name": "CE maintainer", - "projects":{ "gitlab-ce": "maintainer backend" } - }, - { - "username": "in-gitlab-ee", - "name": "EE reviewer", - "projects":{ "gitlab-ee": "reviewer frontend" } - } - ] - JSON - end - - let(:ce_teammate_matcher) do - satisfy do |teammate| - teammate.username == 'in-gitlab-ce' && - teammate.name == 'CE maintainer' && - teammate.projects == { 'gitlab-ce' => 'maintainer backend' } - end - end - - let(:ee_teammate_matcher) do - satisfy do |teammate| - teammate.username == 'in-gitlab-ee' && - teammate.name == 'EE reviewer' && - teammate.projects == { 'gitlab-ee' => 'reviewer frontend' } - end - end - let(:fake_git) { double('fake-git') } subject(:helper) { FakeDanger.new(git: fake_git) } @@ -119,69 +85,6 @@ describe Gitlab::Danger::Helper do end end - describe '#team' do - subject(:team) { helper.team } - - context 'HTTP failure' do - before do - WebMock - .stub_request(:get, 'https://about.gitlab.com/roulette.json') - .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, 'https://about.gitlab.com/roulette.json') - .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, 'https://about.gitlab.com/roulette.json') - .to_return(body: teammate_json) - end - - it 'returns an array of teammates' do - is_expected.to contain_exactly(ce_teammate_matcher, ee_teammate_matcher) - end - - it 'memoizes the result' do - expect(team.object_id).to eq(helper.team.object_id) - end - end - end - - describe '#project_team' do - subject { helper.project_team } - - before do - WebMock - .stub_request(:get, 'https://about.gitlab.com/roulette.json') - .to_return(body: teammate_json) - end - - it 'filters team by project_name' do - expect(helper) - .to receive(:project_name) - .at_least(:once) - .and_return('gitlab-ce') - - is_expected.to contain_exactly(ce_teammate_matcher) - 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/foo qa/foo ee/changelogs/foo.yml] } @@ -191,9 +94,8 @@ describe Gitlab::Danger::Helper do expect(helper.changes_by_category).to eq( backend: %w[foo.rb], database: %w[db/foo], - docs: %w[foo.md], frontend: %w[foo.js], - none: %w[ee/changelogs/foo.yml], + none: %w[ee/changelogs/foo.yml foo.md], qa: %w[qa/foo], unknown: %w[foo] ) @@ -202,13 +104,13 @@ describe Gitlab::Danger::Helper do describe '#category_for_file' do where(:path, :expected_category) do - 'doc/foo' | :docs - 'CONTRIBUTING.md' | :docs - 'LICENSE' | :docs - 'MAINTENANCE.md' | :docs - 'PHILOSOPHY.md' | :docs - 'PROCESS.md' | :docs - 'README.md' | :docs + 'doc/foo' | :none + 'CONTRIBUTING.md' | :none + 'LICENSE' | :none + 'MAINTENANCE.md' | :none + 'PHILOSOPHY.md' | :none + 'PROCESS.md' | :none + 'README.md' | :none 'ee/doc/foo' | :unknown 'ee/README' | :unknown @@ -265,14 +167,15 @@ describe Gitlab::Danger::Helper do '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' | :docs - 'foo/bar.md' | :docs + 'foo/bar.txt' | :none + 'foo/bar.md' | :none end with_them do diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb new file mode 100644 index 00000000000..40dce0c5378 --- /dev/null +++ b/spec/lib/gitlab/danger/roulette_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'webmock/rspec' + +require 'gitlab/danger/roulette' + +describe Gitlab::Danger::Roulette do + let(:teammate_json) do + <<~JSON + [ + { + "username": "in-gitlab-ce", + "name": "CE maintainer", + "projects":{ "gitlab-ce": "maintainer backend" } + }, + { + "username": "in-gitlab-ee", + "name": "EE reviewer", + "projects":{ "gitlab-ee": "reviewer frontend" } + } + ] + JSON + end + + let(:ce_teammate_matcher) do + satisfy do |teammate| + teammate.username == 'in-gitlab-ce' && + teammate.name == 'CE maintainer' && + teammate.projects == { 'gitlab-ce' => 'maintainer backend' } + end + end + + let(:ee_teammate_matcher) do + satisfy do |teammate| + teammate.username == 'in-gitlab-ee' && + teammate.name == 'EE reviewer' && + teammate.projects == { 'gitlab-ee' => 'reviewer frontend' } + end + end + + subject(:roulette) { Object.new.extend(described_class) } + + 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 contain_exactly(ce_teammate_matcher, ee_teammate_matcher) + 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-ce') } + + 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 contain_exactly(ce_teammate_matcher) + end + end +end diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb new file mode 100644 index 00000000000..6a6cf1429c8 --- /dev/null +++ b/spec/lib/gitlab/danger/teammate_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +require 'gitlab/danger/teammate' + +describe Gitlab::Danger::Teammate do + subject { described_class.new(options) } + let(:options) { { 'projects' => projects, 'role' => role } } + let(:projects) { { project => capabilities } } + let(:role) { 'Engineer, Manage' } + let(:labels) { [] } + let(:project) { double } + + 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 Create and the category is test' do + let(:labels) { ['Create'] } + + context 'when role is Test Automation Engineer, Create' do + let(:role) { 'Test Automation Engineer, 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 + end + + context 'when role is Test Automation Engineer, Manage' do + let(:role) { 'Test Automation Engineer, Manage' } + + it '#reviewer? returns false' do + expect(subject.reviewer?(project, :test, labels)).to be_falsey + 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 +end diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb new file mode 100644 index 00000000000..0a6e2302b09 --- /dev/null +++ b/spec/lib/gitlab/data_builder/deployment_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::DataBuilder::Deployment do + describe '.build' do + it 'returns the object kind for a deployment' do + deployment = build(:deployment) + + data = described_class.build(deployment) + + expect(data[:object_kind]).to eq('deployment') + end + + it 'returns data for the given build' do + environment = create(:environment, name: "somewhere") + project = create(:project, :repository, name: 'myproj') + commit = project.commit('HEAD') + deployment = create(:deployment, status: :failed, environment: environment, sha: commit.sha, project: project) + deployable = deployment.deployable + expected_deployable_url = Gitlab::Routing.url_helpers.project_job_url(deployable.project, deployable) + expected_user_url = Gitlab::Routing.url_helpers.user_url(deployment.user) + expected_commit_url = Gitlab::UrlBuilder.build(commit) + + data = described_class.build(deployment) + + expect(data[:status]).to eq('failed') + expect(data[:deployable_id]).to eq(deployable.id) + expect(data[:deployable_url]).to eq(expected_deployable_url) + expect(data[:environment]).to eq("somewhere") + expect(data[:project]).to eq(project.hook_attrs) + expect(data[:short_sha]).to eq(deployment.short_sha) + expect(data[:user]).to eq(deployment.user.hook_attrs) + expect(data[:user_url]).to eq(expected_user_url) + expect(data[:commit_url]).to eq(expected_commit_url) + expect(data[:commit_title]).to eq(commit.title) + end + end +end diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb index 9ef987a0826..1f36fd5c6ef 100644 --- a/spec/lib/gitlab/data_builder/pipeline_spec.rb +++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb @@ -50,5 +50,14 @@ describe Gitlab::DataBuilder::Pipeline do it { expect(attributes[:variables]).to be_a(Array) } it { expect(attributes[:variables]).to contain_exactly({ key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1' }) } end + + context 'when pipeline is a detached merge request pipeline' do + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } + let(:pipeline) { merge_request.all_pipelines.first } + + it 'returns a source ref' do + expect(attributes[:ref]).to eq(merge_request.source_branch) + end + end end end diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index 0c4decc6518..46ad674a1eb 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -23,9 +23,12 @@ describe Gitlab::DataBuilder::Push do describe '.build' do let(:data) do - described_class.build(project, user, Gitlab::Git::BLANK_SHA, - '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b', - 'refs/tags/v1.1.0') + described_class.build( + project: project, + user: user, + oldrev: Gitlab::Git::BLANK_SHA, + newrev: '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b', + ref: 'refs/tags/v1.1.0') end it { expect(data).to be_a(Hash) } @@ -47,7 +50,7 @@ describe Gitlab::DataBuilder::Push do include_examples 'deprecated repository hook data' it 'does not raise an error when given nil commits' do - expect { described_class.build(spy, spy, spy, spy, 'refs/tags/v1.1.0', nil) } + expect { described_class.build(project: spy, user: spy, ref: 'refs/tags/v1.1.0', commits: nil) } .not_to raise_error end end diff --git a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb index b44e8c5a110..bd3c66d0548 100644 --- a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb +++ b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb @@ -6,10 +6,11 @@ describe Gitlab::Database::Count::ReltuplesCountStrategy do create(:identity) end - let(:models) { [Project, Identity] } subject { described_class.new(models).count } describe '#count', :postgresql do + let(:models) { [Project, Identity] } + context 'when reltuples is up to date' do before do ActiveRecord::Base.connection.execute('ANALYZE projects') @@ -23,6 +24,22 @@ describe Gitlab::Database::Count::ReltuplesCountStrategy do end end + context 'when models using single-type inheritance are used' do + let(:models) { [Group, CiService, Namespace] } + + before do + models.each do |model| + ActiveRecord::Base.connection.execute("ANALYZE #{model.table_name}") + end + end + + it 'returns nil counts for inherited tables' do + models.each { |model| expect(model).not_to receive(:count) } + + expect(subject).to eq({ Namespace => 3 }) + end + end + context 'insufficient permissions' do it 'returns an empty hash' do allow(ActiveRecord::Base).to receive(:transaction).and_raise(PG::InsufficientPrivilege) diff --git a/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb index 203f9344a41..40d810b195b 100644 --- a/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb +++ b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb @@ -4,15 +4,23 @@ describe Gitlab::Database::Count::TablesampleCountStrategy do before do create_list(:project, 3) create(:identity) + create(:group) end - let(:models) { [Project, Identity] } + let(:models) { [Project, Identity, Group, Namespace] } let(:strategy) { described_class.new(models) } subject { strategy.count } describe '#count', :postgresql do - let(:estimates) { { Project => threshold + 1, Identity => threshold - 1 } } + let(:estimates) do + { + Project => threshold + 1, + Identity => threshold - 1, + Group => threshold + 1, + Namespace => threshold + 1 + } + end let(:threshold) { Gitlab::Database::Count::TablesampleCountStrategy::EXACT_COUNT_THRESHOLD } before do @@ -30,9 +38,13 @@ describe Gitlab::Database::Count::TablesampleCountStrategy do context 'for tables with an estimated large size' do it 'performs a tablesample count' do expect(Project).not_to receive(:count) + expect(Group).not_to receive(:count) + expect(Namespace).not_to receive(:count) result = subject expect(result[Project]).to eq(3) + expect(result[Group]).to eq(1) + expect(result[Namespace]).to eq(4) end end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb index 248cca25a2c..81419e51635 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb @@ -19,7 +19,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete Project.find(project.id) end - describe "#remove_last_ocurrence" do + describe "#remove_last_occurrence" do it "removes only the last occurrence of a string" do input = "this/is/a-word-to-replace/namespace/with/a-word-to-replace" diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb index 1d31f96159c..ddd54a669a3 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb @@ -27,7 +27,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1, :delete do describe '#rename_wildcard_paths' do it_behaves_like 'renames child namespaces' - it 'should rename projects' do + it 'renames projects' do rename_projects = double expect(described_class::RenameProjects) .to receive(:new).with(['the-path'], subject) @@ -40,7 +40,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1, :delete do end describe '#rename_root_paths' do - it 'should rename namespaces' do + it 'renames namespaces' do rename_namespaces = double expect(described_class::RenameNamespaces) .to receive(:new).with(['the-path'], subject) diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 60106ee3c0b..5f57cd6b825 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -17,6 +17,20 @@ describe Gitlab::Database do end end + describe '.human_adapter_name' do + it 'returns PostgreSQL when using PostgreSQL' do + allow(described_class).to receive(:postgresql?).and_return(true) + + expect(described_class.human_adapter_name).to eq('PostgreSQL') + end + + it 'returns MySQL when using MySQL' do + allow(described_class).to receive(:postgresql?).and_return(false) + + expect(described_class.human_adapter_name).to eq('MySQL') + end + end + # These are just simple smoke tests to check if the methods work (regardless # of what they may return). describe '.mysql?' do @@ -87,6 +101,38 @@ describe Gitlab::Database do end end + describe '.postgresql_minimum_supported_version?' do + it 'returns false when not using PostgreSQL' do + allow(described_class).to receive(:postgresql?).and_return(false) + + expect(described_class.postgresql_minimum_supported_version?).to eq(false) + end + + context 'when using PostgreSQL' do + before do + allow(described_class).to receive(:postgresql?).and_return(true) + end + + it 'returns false when using PostgreSQL 9.5' do + allow(described_class).to receive(:version).and_return('9.5') + + expect(described_class.postgresql_minimum_supported_version?).to eq(false) + end + + it 'returns true when using PostgreSQL 9.6' do + allow(described_class).to receive(:version).and_return('9.6') + + expect(described_class.postgresql_minimum_supported_version?).to eq(true) + end + + it 'returns true when using PostgreSQL 10 or newer' do + allow(described_class).to receive(:version).and_return('10') + + expect(described_class.postgresql_minimum_supported_version?).to eq(true) + end + end + end + describe '.join_lateral_supported?' do it 'returns false when using MySQL' do allow(described_class).to receive(:postgresql?).and_return(false) @@ -195,6 +241,12 @@ describe Gitlab::Database do end end + describe '.pg_last_xact_replay_timestamp' do + it 'returns pg_last_xact_replay_timestamp' do + expect(described_class.pg_last_xact_replay_timestamp).to eq('pg_last_xact_replay_timestamp') + end + end + describe '.nulls_last_order' do context 'when using PostgreSQL' do before do diff --git a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb index 4d222564fd0..0ebd8994636 100644 --- a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb @@ -50,8 +50,8 @@ describe Gitlab::DependencyLinker::ComposerJsonLinker do %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} end - it 'links the module name' do - expect(subject).to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel')) + it 'does not link the module name' do + expect(subject).not_to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel')) end it 'links the homepage' do diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb index a97803b119e..f00f6b47b94 100644 --- a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb @@ -41,13 +41,16 @@ describe Gitlab::DependencyLinker::GemfileLinker do end it 'links dependencies' do - expect(subject).to include(link('rails', 'https://rubygems.org/gems/rails')) expect(subject).to include(link('rails-deprecated_sanitizer', 'https://rubygems.org/gems/rails-deprecated_sanitizer')) - expect(subject).to include(link('responders', 'https://rubygems.org/gems/responders')) - expect(subject).to include(link('sprockets', 'https://rubygems.org/gems/sprockets')) expect(subject).to include(link('default_value_for', 'https://rubygems.org/gems/default_value_for')) end + it 'links to external dependencies' do + expect(subject).to include(link('rails', 'https://github.com/rails/rails')) + expect(subject).to include(link('responders', 'https://github.com/rails/responders')) + expect(subject).to include(link('sprockets', 'https://gitlab.example.com/gems/sprockets')) + end + it 'links GitHub repos' do expect(subject).to include(link('rails/rails', 'https://github.com/rails/rails')) expect(subject).to include(link('rails/responders', 'https://github.com/rails/responders')) diff --git a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb index 24ad7d12f4c..6c6a5d70576 100644 --- a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb @@ -43,8 +43,8 @@ describe Gitlab::DependencyLinker::GemspecLinker do %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} end - it 'links the gem name' do - expect(subject).to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git')) + it 'does not link the gem name' do + expect(subject).not_to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git')) end it 'links the license' do diff --git a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb index 1e8b72afb7b..9050127af7f 100644 --- a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb @@ -33,7 +33,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do "express": "4.2.x", "bigpipe": "bigpipe/pagelet", "plates": "https://github.com/flatiron/plates/tarball/master", - "karma": "^1.4.1" + "karma": "^1.4.1", + "random": "git+https://EdOverflow@github.com/example/example.git" }, "devDependencies": { "vows": "^0.7.0", @@ -51,8 +52,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} end - it 'links the module name' do - expect(subject).to include(link('module-name', 'https://npmjs.com/package/module-name')) + it 'does not link the module name' do + expect(subject).not_to include(link('module-name', 'https://npmjs.com/package/module-name')) end it 'links the homepage' do @@ -71,14 +72,21 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do expect(subject).to include(link('primus', 'https://npmjs.com/package/primus')) expect(subject).to include(link('async', 'https://npmjs.com/package/async')) expect(subject).to include(link('express', 'https://npmjs.com/package/express')) - expect(subject).to include(link('bigpipe', 'https://npmjs.com/package/bigpipe')) - expect(subject).to include(link('plates', 'https://npmjs.com/package/plates')) expect(subject).to include(link('karma', 'https://npmjs.com/package/karma')) expect(subject).to include(link('vows', 'https://npmjs.com/package/vows')) expect(subject).to include(link('assume', 'https://npmjs.com/package/assume')) expect(subject).to include(link('pre-commit', 'https://npmjs.com/package/pre-commit')) end + it 'links dependencies to URL detected on value' do + expect(subject).to include(link('bigpipe', 'https://github.com/bigpipe/pagelet')) + expect(subject).to include(link('plates', 'https://github.com/flatiron/plates/tarball/master')) + end + + it 'does not link to NPM when invalid git URL' do + expect(subject).not_to include(link('random', 'https://npmjs.com/package/random')) + end + it 'links GitHub repos' do expect(subject).to include(link('bigpipe/pagelet', 'https://github.com/bigpipe/pagelet')) end diff --git a/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb new file mode 100644 index 00000000000..9bfb1b13a2b --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Gitlab::DependencyLinker::Parser::Gemfile do + describe '#parse' do + let(:file_content) do + <<-CONTENT.strip_heredoc + source 'https://rubygems.org' + + gem "rails", '4.2.6', github: "rails/rails" + gem 'rails-deprecated_sanitizer', '~> 1.0.3' + gem 'responders', '~> 2.0', :github => 'rails/responders' + gem 'sprockets', '~> 3.6.0', git: 'https://gitlab.example.com/gems/sprockets' + gem 'default_value_for', '~> 3.0.0' + CONTENT + end + + subject { described_class.new(file_content).parse(keyword: 'gem') } + + def fetch_package(name) + subject.find { |package| package.name == name } + end + + it 'returns parsed packages' do + expect(subject.size).to eq(5) + expect(subject).to all(be_a(Gitlab::DependencyLinker::Package)) + end + + it 'packages respond to name and external_ref accordingly' do + expect(fetch_package('rails')).to have_attributes(name: 'rails', + github_ref: 'rails/rails', + git_ref: nil) + + expect(fetch_package('sprockets')).to have_attributes(name: 'sprockets', + github_ref: nil, + git_ref: 'https://gitlab.example.com/gems/sprockets') + + expect(fetch_package('default_value_for')).to have_attributes(name: 'default_value_for', + github_ref: nil, + git_ref: nil) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb index cdfd7ad9826..8f1b523653e 100644 --- a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb @@ -43,7 +43,10 @@ describe Gitlab::DependencyLinker::PodfileLinker do it 'links packages' do expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking')) - expect(subject).to include(link('Interstellar/Core', 'https://cocoapods.org/pods/Interstellar')) + end + + it 'links external packages' do + expect(subject).to include(link('Interstellar/Core', 'https://github.com/ashfurrow/Interstellar.git')) end it 'links Git repos' do diff --git a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb index ed60ab45955..bacec830103 100644 --- a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb @@ -42,8 +42,8 @@ describe Gitlab::DependencyLinker::PodspecLinker do %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} end - it 'links the gem name' do - expect(subject).to include(link('Reachability', 'https://cocoapods.org/pods/Reachability')) + it 'does not link the pod name' do + expect(subject).not_to include(link('Reachability', 'https://cocoapods.org/pods/Reachability')) end it 'links the license' do diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb index 256166dbad3..0697594c725 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -27,7 +27,7 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do let(:diffable) { merge_request.merge_request_diff } end - it 'it uses a different cache key if diff line keys change' do + it 'uses a different cache key if diff line keys change' do mr_diff = described_class.new(merge_request.merge_request_diff, diff_options: nil) key = mr_diff.cache_key diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 862590268ca..cc36060f864 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -8,6 +8,47 @@ describe Gitlab::Diff::File do let(:diff) { commit.raw_diffs.first } let(:diff_file) { described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } + def create_file(file_name, content) + Files::CreateService.new( + project, + project.owner, + commit_message: 'Update', + start_branch: branch_name, + branch_name: branch_name, + file_path: file_name, + file_content: content + ).execute + + project.commit(branch_name).diffs.diff_files.first + end + + def update_file(file_name, content) + Files::UpdateService.new( + project, + project.owner, + commit_message: 'Update', + start_branch: branch_name, + branch_name: branch_name, + file_path: file_name, + file_content: content + ).execute + + project.commit(branch_name).diffs.diff_files.first + end + + def delete_file(file_name) + Files::DeleteService.new( + project, + project.owner, + commit_message: 'Update', + start_branch: branch_name, + branch_name: branch_name, + file_path: file_name + ).execute + + project.commit(branch_name).diffs.diff_files.first + end + describe '#diff_lines' do let(:diff_lines) { diff_file.diff_lines } @@ -31,6 +72,13 @@ describe Gitlab::Diff::File do expect(diff_file.diff_lines_for_serializer.last.type).to eq('match') end + context 'when called multiple times' do + it 'only adds bottom match line once' do + expect(diff_file.diff_lines_for_serializer.size).to eq(31) + expect(diff_file.diff_lines_for_serializer.size).to eq(31) + end + end + context 'when deleted' do let(:commit) { project.commit('d59c60028b053793cecfb4022de34602e1a9218e') } let(:diff_file) { commit.diffs.diff_file_with_old_path('files/js/commit.js.coffee') } @@ -675,47 +723,6 @@ describe Gitlab::Diff::File do end let(:branch_name) { 'master' } - def create_file(file_name, content) - Files::CreateService.new( - project, - project.owner, - commit_message: 'Update', - start_branch: branch_name, - branch_name: branch_name, - file_path: file_name, - file_content: content - ).execute - - project.commit(branch_name).diffs.diff_files.first - end - - def update_file(file_name, content) - Files::UpdateService.new( - project, - project.owner, - commit_message: 'Update', - start_branch: branch_name, - branch_name: branch_name, - file_path: file_name, - file_content: content - ).execute - - project.commit(branch_name).diffs.diff_files.first - end - - def delete_file(file_name) - Files::DeleteService.new( - project, - project.owner, - commit_message: 'Update', - start_branch: branch_name, - branch_name: branch_name, - file_path: file_name - ).execute - - project.commit(branch_name).diffs.diff_files.first - end - context 'when empty file is created' do it 'returns true' do diff_file = create_file('empty.md', '') @@ -751,4 +758,123 @@ describe Gitlab::Diff::File do end end end + + describe '#fully_expanded?' do + let(:project) do + create(:project, :custom_repo, files: {}) + end + let(:branch_name) { 'master' } + + context 'when empty file is created' do + it 'returns true' do + diff_file = create_file('empty.md', '') + + expect(diff_file.fully_expanded?).to be_truthy + end + end + + context 'when empty file is deleted' do + it 'returns true' do + create_file('empty.md', '') + diff_file = delete_file('empty.md') + + expect(diff_file.fully_expanded?).to be_truthy + end + end + + context 'when short file with last line removed' do + it 'returns true' do + create_file('with-content.md', (1..3).to_a.join("\n")) + diff_file = update_file('with-content.md', (1..2).to_a.join("\n")) + + expect(diff_file.fully_expanded?).to be_truthy + end + end + + context 'when a single line is added to empty file' do + it 'returns true' do + create_file('empty.md', '') + diff_file = update_file('empty.md', 'new content') + + expect(diff_file.fully_expanded?).to be_truthy + end + end + + context 'when single line file is changed' do + it 'returns true' do + create_file('file.md', 'foo') + diff_file = update_file('file.md', 'bar') + + expect(diff_file.fully_expanded?).to be_truthy + end + end + + context 'when long file is changed' do + before do + create_file('file.md', (1..999).to_a.join("\n")) + end + + context 'when first line is removed' do + it 'returns true' do + diff_file = update_file('file.md', (2..999).to_a.join("\n")) + + expect(diff_file.fully_expanded?).to be_falsey + end + end + + context 'when last line is removed' do + it 'returns true' do + diff_file = update_file('file.md', (1..998).to_a.join("\n")) + + expect(diff_file.fully_expanded?).to be_falsey + end + end + + context 'when first and last lines are removed' do + it 'returns false' do + diff_file = update_file('file.md', (2..998).to_a.join("\n")) + + expect(diff_file.fully_expanded?).to be_falsey + end + end + + context 'when first and last lines are changed' do + it 'returns false' do + content = (2..998).to_a + content.prepend('a') + content.append('z') + content = content.join("\n") + + diff_file = update_file('file.md', content) + + expect(diff_file.fully_expanded?).to be_falsey + end + end + + context 'when every line are changed' do + it 'returns true' do + diff_file = update_file('file.md', "hi\n" * 999) + + expect(diff_file.fully_expanded?).to be_truthy + end + end + + context 'when all contents are cleared' do + it 'returns true' do + diff_file = update_file('file.md', "") + + expect(diff_file.fully_expanded?).to be_truthy + end + end + + context 'when file is binary' do + it 'returns true' do + diff_file = update_file('file.md', (1..998).to_a.join("\n")) + allow(diff_file).to receive(:binary?).and_return(true) + + expect(diff_file.fully_expanded?).to be_truthy + end + end + end + end end diff --git a/spec/lib/gitlab/diff/suggestion_diff_spec.rb b/spec/lib/gitlab/diff/suggestion_diff_spec.rb new file mode 100644 index 00000000000..5a32c2bea37 --- /dev/null +++ b/spec/lib/gitlab/diff/suggestion_diff_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::SuggestionDiff do + describe '#diff_lines' do + let(:from_content) do + <<-BLOB.strip_heredoc + "tags": ["devel", "development", "nightly"], + "desktop-file-name-prefix": "(Development) ", + "finish-args": "foo", + BLOB + end + + let(:to_content) do + <<-BLOB.strip_heredoc + "buildsystem": "meson", + "builddir": true, + "name": "nautilus", + "bar": "bar", + BLOB + end + + let(:suggestion) do + instance_double(Suggestion, from_line: 12, + from_content: from_content, + to_content: to_content) + end + + subject { described_class.new(suggestion).diff_lines } + + let(:expected_diff_lines) do + [ + { old_pos: 12, new_pos: 12, type: "match", text: "@@ -12 +12" }, + { old_pos: 12, new_pos: 12, type: "old", text: "-\"tags\": [\"devel\", \"development\", \"nightly\"]," }, + { old_pos: 13, new_pos: 12, type: "old", text: "-\"desktop-file-name-prefix\": \"(Development) \"," }, + { old_pos: 14, new_pos: 12, type: "old", text: "-\"finish-args\": \"foo\"," }, + { old_pos: 15, new_pos: 12, type: "new", text: "+\"buildsystem\": \"meson\"," }, + { old_pos: 15, new_pos: 13, type: "new", text: "+\"builddir\": true," }, + { old_pos: 15, new_pos: 14, type: "new", text: "+\"name\": \"nautilus\"," }, + { old_pos: 15, new_pos: 15, type: "new", text: "+\"bar\": \"bar\"," } + ] + end + + it 'returns diff lines with correct line numbers' do + diff_lines = subject + + expect(diff_lines).to all(be_a(Gitlab::Diff::Line)) + + expected_diff_lines.each_with_index do |expected_line, index| + expect(diff_lines[index].to_hash).to include(expected_line) + end + end + end +end diff --git a/spec/lib/gitlab/diff/suggestion_spec.rb b/spec/lib/gitlab/diff/suggestion_spec.rb new file mode 100644 index 00000000000..d7ca0e0a522 --- /dev/null +++ b/spec/lib/gitlab/diff/suggestion_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::Suggestion do + shared_examples 'correct suggestion raw content' do + it 'returns correct raw data' do + expect(suggestion.to_hash).to include(from_content: expected_lines.join, + to_content: "#{text}\n", + lines_above: above, + lines_below: below) + end + + it 'returns diff lines with correct line numbers' do + diff_lines = suggestion.diff_lines + + expect(diff_lines).to all(be_a(Gitlab::Diff::Line)) + + expected_diff_lines.each_with_index do |expected_line, index| + expect(diff_lines[index].to_hash).to include(expected_line) + end + end + end + + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let(:position) do + Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 9, + diff_refs: merge_request.diff_refs) + end + let(:diff_file) do + position.diff_file(project.repository) + end + let(:text) { "# parsed suggestion content\n# with comments" } + + def blob_lines_data(from_line, to_line) + diff_file.new_blob_lines_between(from_line, to_line) + end + + def blob_data + blob = diff_file.new_blob + blob.load_all_data! + blob.data + end + + let(:suggestion) do + described_class.new(text, line: line, above: above, below: below, diff_file: diff_file) + end + + describe '#to_hash' do + context 'when changing content surpasses the top limit' do + let(:line) { 4 } + let(:above) { 5 } + let(:below) { 2 } + let(:expected_above) { line - 1 } + let(:expected_below) { below } + let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + let(:expected_diff_lines) do + [ + { old_pos: 1, new_pos: 1, type: 'old', text: "-require 'fileutils'" }, + { old_pos: 2, new_pos: 1, type: 'old', text: "-require 'open3'" }, + { old_pos: 3, new_pos: 1, type: 'old', text: "-" }, + { old_pos: 4, new_pos: 1, type: 'old', text: "-module Popen" }, + { old_pos: 5, new_pos: 1, type: 'old', text: "- extend self" }, + { old_pos: 6, new_pos: 1, type: 'old', text: "-" }, + { old_pos: 7, new_pos: 1, type: 'new', text: "+# parsed suggestion content" }, + { old_pos: 7, new_pos: 2, type: 'new', text: "+# with comments" } + ] + end + + it_behaves_like 'correct suggestion raw content' + end + + context 'when changing content surpasses the amount of lines in the blob (bottom)' do + let(:line) { 5 } + let(:above) { 1 } + let(:below) { blob_data.lines.size + 10 } + let(:expected_below) { below } + let(:expected_above) { above } + let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + let(:expected_diff_lines) do + [ + { old_pos: 4, new_pos: 4, type: "match", text: "@@ -4 +4" }, + { old_pos: 4, new_pos: 4, type: "old", text: "-module Popen" }, + { old_pos: 5, new_pos: 4, type: "old", text: "- extend self" }, + { old_pos: 6, new_pos: 4, type: "old", text: "-" }, + { old_pos: 7, new_pos: 4, type: "old", text: "- def popen(cmd, path=nil)" }, + { old_pos: 8, new_pos: 4, type: "old", text: "- unless cmd.is_a?(Array)" }, + { old_pos: 9, new_pos: 4, type: "old", text: "- raise RuntimeError, \"System commands must be given as an array of strings\"" }, + { old_pos: 10, new_pos: 4, type: "old", text: "- end" }, + { old_pos: 11, new_pos: 4, type: "old", text: "-" }, + { old_pos: 12, new_pos: 4, type: "old", text: "- path ||= Dir.pwd" }, + { old_pos: 13, new_pos: 4, type: "old", text: "-" }, + { old_pos: 14, new_pos: 4, type: "old", text: "- vars = {" }, + { old_pos: 15, new_pos: 4, type: "old", text: "- \"PWD\" => path" }, + { old_pos: 16, new_pos: 4, type: "old", text: "- }" }, + { old_pos: 17, new_pos: 4, type: "old", text: "-" }, + { old_pos: 18, new_pos: 4, type: "old", text: "- options = {" }, + { old_pos: 19, new_pos: 4, type: "old", text: "- chdir: path" }, + { old_pos: 20, new_pos: 4, type: "old", text: "- }" }, + { old_pos: 21, new_pos: 4, type: "old", text: "-" }, + { old_pos: 22, new_pos: 4, type: "old", text: "- unless File.directory?(path)" }, + { old_pos: 23, new_pos: 4, type: "old", text: "- FileUtils.mkdir_p(path)" }, + { old_pos: 24, new_pos: 4, type: "old", text: "- end" }, + { old_pos: 25, new_pos: 4, type: "old", text: "-" }, + { old_pos: 26, new_pos: 4, type: "old", text: "- @cmd_output = \"\"" }, + { old_pos: 27, new_pos: 4, type: "old", text: "- @cmd_status = 0" }, + { old_pos: 28, new_pos: 4, type: "old", text: "-" }, + { old_pos: 29, new_pos: 4, type: "old", text: "- Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|" }, + { old_pos: 30, new_pos: 4, type: "old", text: "- @cmd_output << stdout.read" }, + { old_pos: 31, new_pos: 4, type: "old", text: "- @cmd_output << stderr.read" }, + { old_pos: 32, new_pos: 4, type: "old", text: "- @cmd_status = wait_thr.value.exitstatus" }, + { old_pos: 33, new_pos: 4, type: "old", text: "- end" }, + { old_pos: 34, new_pos: 4, type: "old", text: "-" }, + { old_pos: 35, new_pos: 4, type: "old", text: "- return @cmd_output, @cmd_status" }, + { old_pos: 36, new_pos: 4, type: "old", text: "- end" }, + { old_pos: 37, new_pos: 4, type: "old", text: "-end" }, + { old_pos: 38, new_pos: 4, type: "new", text: "+# parsed suggestion content" }, + { old_pos: 38, new_pos: 5, type: "new", text: "+# with comments" } + ] + end + + it_behaves_like 'correct suggestion raw content' + end + + context 'when lines are within blob lines boundary' do + let(:line) { 5 } + let(:above) { 2 } + let(:below) { 3 } + let(:expected_below) { below } + let(:expected_above) { above } + let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + let(:expected_diff_lines) do + [ + { old_pos: 3, new_pos: 3, type: "match", text: "@@ -3 +3" }, + { old_pos: 3, new_pos: 3, type: "old", text: "-" }, + { old_pos: 4, new_pos: 3, type: "old", text: "-module Popen" }, + { old_pos: 5, new_pos: 3, type: "old", text: "- extend self" }, + { old_pos: 6, new_pos: 3, type: "old", text: "-" }, + { old_pos: 7, new_pos: 3, type: "old", text: "- def popen(cmd, path=nil)" }, + { old_pos: 8, new_pos: 3, type: "old", text: "- unless cmd.is_a?(Array)" }, + { old_pos: 9, new_pos: 3, type: "new", text: "+# parsed suggestion content" }, + { old_pos: 9, new_pos: 4, type: "new", text: "+# with comments" } + ] + end + + it_behaves_like 'correct suggestion raw content' + end + end +end diff --git a/spec/lib/gitlab/diff/suggestions_parser_spec.rb b/spec/lib/gitlab/diff/suggestions_parser_spec.rb new file mode 100644 index 00000000000..1f2af42f6e7 --- /dev/null +++ b/spec/lib/gitlab/diff/suggestions_parser_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::SuggestionsParser do + describe '.parse' do + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let(:position) do + Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 9, + diff_refs: merge_request.diff_refs) + end + + let(:diff_file) do + position.diff_file(project.repository) + end + + subject do + described_class.parse(markdown, project: merge_request.project, + position: position) + end + + def blob_lines_data(from_line, to_line) + diff_file.new_blob_lines_between(from_line, to_line).join + end + + context 'single-line suggestions' do + let(:markdown) do + <<-MARKDOWN.strip_heredoc + ```suggestion + foo + bar + ``` + + ``` + nothing + ``` + + ```suggestion + xpto + baz + ``` + + ```thing + this is not a suggestion, it's a thing + ``` + MARKDOWN + end + + it 'returns a list of Gitlab::Diff::Suggestion' do + expect(subject).to all(be_a(Gitlab::Diff::Suggestion)) + expect(subject.size).to eq(2) + end + + it 'parsed suggestion has correct data' do + from_line, to_line = position.new_line, position.new_line + + expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line), + to_content: " foo\n bar\n", + lines_above: 0, + lines_below: 0) + + expect(subject.second.to_hash).to include(from_content: blob_lines_data(from_line, to_line), + to_content: " xpto\n baz\n", + lines_above: 0, + lines_below: 0) + end + end + + context 'multi-line suggestions' do + let(:markdown) do + <<-MARKDOWN.strip_heredoc + ```suggestion:-2+1 + # above and below + ``` + + ``` + nothing + ``` + + ```suggestion:-3 + # only above + ``` + + ```suggestion:+3 + # only below + ``` + + ```thing + this is not a suggestion, it's a thing + ``` + MARKDOWN + end + + it 'returns a list of Gitlab::Diff::Suggestion' do + expect(subject).to all(be_a(Gitlab::Diff::Suggestion)) + expect(subject.size).to eq(3) + end + + it 'suggestion with above and below param has correct data' do + from_line = position.new_line - 2 + to_line = position.new_line + 1 + + expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line), + to_content: " # above and below\n", + lines_above: 2, + lines_below: 1) + end + + it 'suggestion with above param has correct data' do + from_line = position.new_line - 3 + to_line = position.new_line + + expect(subject.second.to_hash).to eq(from_content: blob_lines_data(from_line, to_line), + to_content: " # only above\n", + lines_above: 3, + lines_below: 0) + end + + it 'suggestion with below param has correct data' do + from_line = position.new_line + to_line = position.new_line + 3 + + expect(subject.third.to_hash).to eq(from_content: blob_lines_data(from_line, to_line), + to_content: " # only below\n", + lines_above: 0, + lines_below: 3) + end + end + end +end diff --git a/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb b/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb index fe26ebb8796..15ee8c40b55 100644 --- a/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb +++ b/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb @@ -3,31 +3,32 @@ require 'spec_helper' describe Gitlab::DiscussionsDiff::HighlightCache, :clean_gitlab_redis_cache do + def fake_file(offset) + { + text: 'foo', + type: 'new', + index: 2 + offset, + old_pos: 10 + offset, + new_pos: 11 + offset, + line_code: 'xpto', + rich_text: '<blips>blops</blips>' + } + end + + let(:mapping) do + { + 3 => [ + fake_file(0), + fake_file(1) + ], + 4 => [ + fake_file(2) + ] + } + end + describe '#write_multiple' do it 'sets multiple keys serializing content as JSON' do - mapping = { - 3 => [ - { - text: 'foo', - type: 'new', - index: 2, - old_pos: 10, - new_pos: 11, - line_code: 'xpto', - rich_text: '<blips>blops</blips>' - }, - { - text: 'foo', - type: 'new', - index: 3, - old_pos: 11, - new_pos: 12, - line_code: 'xpto', - rich_text: '<blops>blips</blops>' - } - ] - } - described_class.write_multiple(mapping) mapping.each do |key, value| @@ -41,53 +42,16 @@ describe Gitlab::DiscussionsDiff::HighlightCache, :clean_gitlab_redis_cache do describe '#read_multiple' do it 'reads multiple keys and serializes content into Gitlab::Diff::Line objects' do - mapping = { - 3 => [ - { - text: 'foo', - type: 'new', - index: 2, - old_pos: 11, - new_pos: 12, - line_code: 'xpto', - rich_text: '<blips>blops</blips>' - }, - { - text: 'foo', - type: 'new', - index: 3, - old_pos: 10, - new_pos: 11, - line_code: 'xpto', - rich_text: '<blips>blops</blips>' - } - ] - } - described_class.write_multiple(mapping) found = described_class.read_multiple(mapping.keys) - expect(found.size).to eq(1) + expect(found.size).to eq(2) expect(found.first.size).to eq(2) expect(found.first).to all(be_a(Gitlab::Diff::Line)) end it 'returns nil when cached key is not found' do - mapping = { - 3 => [ - { - text: 'foo', - type: 'new', - index: 2, - old_pos: 11, - new_pos: 12, - line_code: 'xpto', - rich_text: '<blips>blops</blips>' - } - ] - } - described_class.write_multiple(mapping) found = described_class.read_multiple([2, 3]) @@ -95,8 +59,30 @@ describe Gitlab::DiscussionsDiff::HighlightCache, :clean_gitlab_redis_cache do expect(found.size).to eq(2) expect(found.first).to eq(nil) - expect(found.second.size).to eq(1) + expect(found.second.size).to eq(2) expect(found.second).to all(be_a(Gitlab::Diff::Line)) end end + + describe '#clear_multiple' do + it 'removes all named keys' do + described_class.write_multiple(mapping) + + described_class.clear_multiple(mapping.keys) + + expect(described_class.read_multiple(mapping.keys)).to all(be_nil) + end + + it 'only removed named keys' do + to_clear, to_leave = mapping.keys + + described_class.write_multiple(mapping) + described_class.clear_multiple([to_clear]) + + cleared, left = described_class.read_multiple([to_clear, to_leave]) + + expect(cleared).to be_nil + expect(left).to all(be_a(Gitlab::Diff::Line)) + end + end end diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index 429816efec3..88ea98eb1e1 100644 --- a/spec/lib/gitlab/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -189,14 +189,23 @@ describe Gitlab::EncodingHelper do end end - describe '#binary_stringio' do + describe '#binary_io' do it 'does not mutate the original string encoding' do test = 'my-test' - io_stream = ext_class.binary_stringio(test) + io_stream = ext_class.binary_io(test) expect(io_stream.external_encoding.name).to eq('ASCII-8BIT') expect(test.encoding.name).to eq('UTF-8') end + + it 'returns a copy of the IO with the correct encoding' do + test = fixture_file_upload('spec/fixtures/doc_sample.txt').to_io + + io_stream = ext_class.binary_io(test) + + expect(io_stream.external_encoding.name).to eq('ASCII-8BIT') + expect(test).not_to eq(io_stream) + end end end diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb index f69cb502ca6..a7cb0bb2a87 100644 --- a/spec/lib/gitlab/etag_caching/router_spec.rb +++ b/spec/lib/gitlab/etag_caching/router_spec.rb @@ -19,6 +19,24 @@ describe Gitlab::EtagCaching::Router do expect(result.name).to eq 'issue_title' end + it 'matches with a project name that includes a suffix of create' do + result = described_class.match( + '/group/test-create/issues/123/realtime_changes' + ) + + expect(result).to be_present + expect(result.name).to eq 'issue_title' + end + + it 'matches with a project name that includes a prefix of create' do + result = described_class.match( + '/group/create-test/issues/123/realtime_changes' + ) + + expect(result).to be_present + expect(result.name).to eq 'issue_title' + end + it 'matches project pipelines endpoint' do result = described_class.match( '/my-group/my-project/pipelines.json' diff --git a/spec/lib/gitlab/external_authorization/access_spec.rb b/spec/lib/gitlab/external_authorization/access_spec.rb new file mode 100644 index 00000000000..5dc2521b310 --- /dev/null +++ b/spec/lib/gitlab/external_authorization/access_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do + subject(:access) { described_class.new(build(:user), 'dummy_label') } + + describe '#loaded?' do + it 'is `true` when it was loaded recently' do + Timecop.freeze do + allow(access).to receive(:loaded_at).and_return(5.minutes.ago) + + expect(access).to be_loaded + end + end + + it 'is `false` when there is no loading time' do + expect(access).not_to be_loaded + end + + it 'is `false` when there the result was loaded a long time ago' do + Timecop.freeze do + allow(access).to receive(:loaded_at).and_return(2.weeks.ago) + + expect(access).not_to be_loaded + end + end + end + + describe 'load!' do + let(:fake_client) { double('ExternalAuthorization::Client') } + let(:fake_response) do + double( + 'Response', + 'successful?' => true, + 'valid?' => true, + 'reason' => nil + ) + end + + before do + allow(access).to receive(:load_from_cache) + allow(fake_client).to receive(:request_access).and_return(fake_response) + allow(Gitlab::ExternalAuthorization::Client).to receive(:new) { fake_client } + end + + context 'when loading from the webservice' do + it 'loads from the webservice it the cache was empty' do + expect(access).to receive(:load_from_cache) + expect(access).to receive(:load_from_service).and_call_original + + access.load! + + expect(access).to be_loaded + end + + it 'assigns the accessibility, reason and loaded_at' do + allow(fake_response).to receive(:successful?).and_return(false) + allow(fake_response).to receive(:reason).and_return('Inaccessible label') + + access.load! + + expect(access.reason).to eq('Inaccessible label') + expect(access).not_to have_access + expect(access.loaded_at).not_to be_nil + end + + it 'returns itself' do + expect(access.load!).to eq(access) + end + + it 'stores the result in redis' do + Timecop.freeze do + fake_cache = double + expect(fake_cache).to receive(:store).with(true, nil, Time.now) + expect(access).to receive(:cache).and_return(fake_cache) + + access.load! + end + end + + context 'when the request fails' do + before do + allow(fake_client).to receive(:request_access) do + raise ::Gitlab::ExternalAuthorization::RequestFailed.new('Service unavailable') + end + end + + it 'is loaded' do + access.load! + + expect(access).to be_loaded + end + + it 'assigns the correct accessibility, reason and loaded_at' do + access.load! + + expect(access.reason).to eq('Service unavailable') + expect(access).not_to have_access + expect(access.loaded_at).not_to be_nil + end + + it 'does not store the result in redis' do + fake_cache = double + expect(fake_cache).not_to receive(:store) + allow(access).to receive(:cache).and_return(fake_cache) + + access.load! + end + end + end + + context 'When loading from cache' do + let(:fake_cache) { double('ExternalAuthorization::Cache') } + + before do + allow(access).to receive(:cache).and_return(fake_cache) + end + + it 'does not load from the webservice' do + Timecop.freeze do + expect(fake_cache).to receive(:load).and_return([true, nil, Time.now]) + + expect(access).to receive(:load_from_cache).and_call_original + expect(access).not_to receive(:load_from_service) + + access.load! + end + end + + it 'loads from the webservice when the cached result was too old' do + Timecop.freeze do + expect(fake_cache).to receive(:load).and_return([true, nil, 2.days.ago]) + + expect(access).to receive(:load_from_cache).and_call_original + expect(access).to receive(:load_from_service).and_call_original + allow(fake_cache).to receive(:store) + + access.load! + end + end + end + end +end diff --git a/spec/lib/gitlab/external_authorization/cache_spec.rb b/spec/lib/gitlab/external_authorization/cache_spec.rb new file mode 100644 index 00000000000..58e7d626707 --- /dev/null +++ b/spec/lib/gitlab/external_authorization/cache_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Cache, :clean_gitlab_redis_cache do + let(:user) { build_stubbed(:user) } + let(:cache_key) { "external_authorization:user-#{user.id}:label-dummy_label" } + + subject(:cache) { described_class.new(user, 'dummy_label') } + + def read_from_redis(key) + Gitlab::Redis::Cache.with do |redis| + redis.hget(cache_key, key) + end + end + + def set_in_redis(key, value) + Gitlab::Redis::Cache.with do |redis| + redis.hmset(cache_key, key, value) + end + end + + describe '#load' do + it 'reads stored info from redis' do + Timecop.freeze do + set_in_redis(:access, false) + set_in_redis(:reason, 'Access denied for now') + set_in_redis(:refreshed_at, Time.now) + + access, reason, refreshed_at = cache.load + + expect(access).to eq(false) + expect(reason).to eq('Access denied for now') + expect(refreshed_at).to be_within(1.second).of(Time.now) + end + end + end + + describe '#store' do + it 'sets the values in redis' do + Timecop.freeze do + cache.store(true, 'the reason', Time.now) + + expect(read_from_redis(:access)).to eq('true') + expect(read_from_redis(:reason)).to eq('the reason') + expect(read_from_redis(:refreshed_at)).to eq(Time.now.to_s) + end + end + end +end diff --git a/spec/lib/gitlab/external_authorization/client_spec.rb b/spec/lib/gitlab/external_authorization/client_spec.rb new file mode 100644 index 00000000000..fa18c1e56e8 --- /dev/null +++ b/spec/lib/gitlab/external_authorization/client_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Client do + let(:user) { build(:user, email: 'dummy_user@example.com') } + let(:dummy_url) { 'https://dummy.net/' } + subject(:client) { described_class.new(user, 'dummy_label') } + + before do + stub_application_setting(external_authorization_service_url: dummy_url) + end + + describe '#request_access' do + it 'performs requests to the configured endpoint' do + expect(Excon).to receive(:post).with(dummy_url, any_args) + + client.request_access + end + + it 'adds the correct params for the user to the body of the request' do + expected_body = { + user_identifier: 'dummy_user@example.com', + project_classification_label: 'dummy_label' + }.to_json + expect(Excon).to receive(:post) + .with(dummy_url, hash_including(body: expected_body)) + + client.request_access + end + + it 'respects the the timeout' do + stub_application_setting( + external_authorization_service_timeout: 3 + ) + + expect(Excon).to receive(:post).with(dummy_url, + hash_including( + connect_timeout: 3, + read_timeout: 3, + write_timeout: 3 + )) + + client.request_access + end + + it 'adds the mutual tls params when they are present' do + stub_application_setting( + external_auth_client_cert: 'the certificate data', + external_auth_client_key: 'the key data', + external_auth_client_key_pass: 'open sesame' + ) + expected_params = { + client_cert_data: 'the certificate data', + client_key_data: 'the key data', + client_key_pass: 'open sesame' + } + + expect(Excon).to receive(:post).with(dummy_url, hash_including(expected_params)) + + client.request_access + end + + it 'returns an expected response' do + expect(Excon).to receive(:post) + + expect(client.request_access) + .to be_kind_of(::Gitlab::ExternalAuthorization::Response) + end + + it 'wraps exceptions if the request fails' do + expect(Excon).to receive(:post) { raise Excon::Error.new('the request broke') } + + expect { client.request_access } + .to raise_error(::Gitlab::ExternalAuthorization::RequestFailed) + end + + describe 'for ldap users' do + let(:user) do + create(:omniauth_user, + email: 'dummy_user@example.com', + extern_uid: 'external id', + provider: 'ldapprovider') + end + + it 'includes the ldap dn for ldap users' do + expected_body = { + user_identifier: 'dummy_user@example.com', + project_classification_label: 'dummy_label', + user_ldap_dn: 'external id' + }.to_json + expect(Excon).to receive(:post) + .with(dummy_url, hash_including(body: expected_body)) + + client.request_access + end + end + end +end diff --git a/spec/lib/gitlab/external_authorization/logger_spec.rb b/spec/lib/gitlab/external_authorization/logger_spec.rb new file mode 100644 index 00000000000..81f1b2390e6 --- /dev/null +++ b/spec/lib/gitlab/external_authorization/logger_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Logger do + let(:request_time) { Time.parse('2018-03-26 20:22:15') } + + def fake_access(has_access, user, load_type = :request) + access = double('access') + allow(access).to receive_messages(user: user, + has_access?: has_access, + loaded_at: request_time, + label: 'dummy_label', + load_type: load_type) + + access + end + + describe '.log_access' do + it 'logs a nice message for an access request' do + expected_message = "GRANTED admin@example.com access to 'dummy_label' (the/project/path)" + fake_access = fake_access(true, build(:user, email: 'admin@example.com')) + + expect(described_class).to receive(:info).with(expected_message) + + described_class.log_access(fake_access, 'the/project/path') + end + + it 'does not trip without a project path' do + expected_message = "DENIED admin@example.com access to 'dummy_label'" + fake_access = fake_access(false, build(:user, email: 'admin@example.com')) + + expect(described_class).to receive(:info).with(expected_message) + + described_class.log_access(fake_access, nil) + end + + it 'adds the load time for cached accesses' do + expected_message = "DENIED admin@example.com access to 'dummy_label' - cache #{request_time}" + fake_access = fake_access(false, build(:user, email: 'admin@example.com'), :cache) + + expect(described_class).to receive(:info).with(expected_message) + + described_class.log_access(fake_access, nil) + end + end +end diff --git a/spec/lib/gitlab/external_authorization/response_spec.rb b/spec/lib/gitlab/external_authorization/response_spec.rb new file mode 100644 index 00000000000..43211043eca --- /dev/null +++ b/spec/lib/gitlab/external_authorization/response_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Response do + let(:excon_response) { double } + subject(:response) { described_class.new(excon_response) } + + describe '#valid?' do + it 'is valid for 200, 401, and 403 responses' do + [200, 401, 403].each do |status| + allow(excon_response).to receive(:status).and_return(status) + + expect(response).to be_valid + end + end + + it "is invalid for other statuses" do + expect(excon_response).to receive(:status).and_return(500) + + expect(response).not_to be_valid + end + end + + describe '#reason' do + it 'returns a reason if it was included in the response body' do + expect(excon_response).to receive(:body).and_return({ reason: 'Not authorized' }.to_json) + + expect(response.reason).to eq('Not authorized') + end + + it 'returns nil when there was no body' do + expect(excon_response).to receive(:body).and_return('') + + expect(response.reason).to eq(nil) + end + end + + describe '#successful?' do + it 'is `true` if the status is 200' do + allow(excon_response).to receive(:status).and_return(200) + + expect(response).to be_successful + end + + it 'is `false` if the status is 401 or 403' do + [401, 403].each do |status| + allow(excon_response).to receive(:status).and_return(status) + + expect(response).not_to be_successful + end + end + end +end diff --git a/spec/lib/gitlab/external_authorization_spec.rb b/spec/lib/gitlab/external_authorization_spec.rb new file mode 100644 index 00000000000..7394fbfe0ce --- /dev/null +++ b/spec/lib/gitlab/external_authorization_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization, :request_store do + include ExternalAuthorizationServiceHelpers + + let(:user) { build(:user) } + let(:label) { 'dummy_label' } + + describe '#access_allowed?' do + it 'is always true when the feature is disabled' do + # Not using `stub_application_setting` because the method is prepended in + # `EE::ApplicationSetting` which breaks when using `any_instance` + # https://gitlab.com/gitlab-org/gitlab-ce/issues/33587 + expect(::Gitlab::CurrentSettings.current_application_settings) + .to receive(:external_authorization_service_enabled) { false } + + expect(described_class).not_to receive(:access_for_user_to_label) + + expect(described_class.access_allowed?(user, label)).to be_truthy + end + end + + describe '#rejection_reason' do + it 'is always nil when the feature is disabled' do + expect(::Gitlab::CurrentSettings.current_application_settings) + .to receive(:external_authorization_service_enabled) { false } + + expect(described_class).not_to receive(:access_for_user_to_label) + + expect(described_class.rejection_reason(user, label)).to be_nil + end + end + + describe '#access_for_user_to_label' do + it 'only loads the access once per request' do + enable_external_authorization_service_check + + expect(::Gitlab::ExternalAuthorization::Access) + .to receive(:new).with(user, label).once.and_call_original + + 2.times { described_class.access_for_user_to_label(user, label, nil) } + end + + it 'logs the access request once per request' do + expect(::Gitlab::ExternalAuthorization::Logger) + .to receive(:log_access) + .with(an_instance_of(::Gitlab::ExternalAuthorization::Access), + 'the/project/path') + .once + + 2.times { described_class.access_for_user_to_label(user, label, 'the/project/path') } + end + end +end diff --git a/spec/lib/gitlab/fake_application_settings_spec.rb b/spec/lib/gitlab/fake_application_settings_spec.rb index af12e13d36d..c81cb83d9f4 100644 --- a/spec/lib/gitlab/fake_application_settings_spec.rb +++ b/spec/lib/gitlab/fake_application_settings_spec.rb @@ -1,32 +1,33 @@ require 'spec_helper' describe Gitlab::FakeApplicationSettings do - let(:defaults) { { password_authentication_enabled_for_web: false, foobar: 'asdf', signup_enabled: true, 'test?' => 123 } } + let(:defaults) do + described_class.defaults.merge( + foobar: 'asdf', + 'test?' => 123 + ) + end - subject { described_class.new(defaults) } + let(:setting) { described_class.new(defaults) } it 'wraps OpenStruct variables properly' do - expect(subject.password_authentication_enabled_for_web).to be_falsey - expect(subject.signup_enabled).to be_truthy - expect(subject.foobar).to eq('asdf') + expect(setting.password_authentication_enabled_for_web).to be_truthy + expect(setting.signup_enabled).to be_truthy + expect(setting.foobar).to eq('asdf') end it 'defines predicate methods' do - expect(subject.password_authentication_enabled_for_web?).to be_falsey - expect(subject.signup_enabled?).to be_truthy - end - - it 'predicate method changes when value is updated' do - subject.password_authentication_enabled_for_web = true - - expect(subject.password_authentication_enabled_for_web?).to be_truthy + expect(setting.password_authentication_enabled_for_web?).to be_truthy + expect(setting.signup_enabled?).to be_truthy end it 'does not define a predicate method' do - expect(subject.foobar?).to be_nil + expect(setting.foobar?).to be_nil end it 'does not override an existing predicate method' do - expect(subject.test?).to eq(123) + expect(setting.test?).to eq(123) end + + it_behaves_like 'application settings examples' end diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb index 49a423191bb..dce56bbd2c4 100644 --- a/spec/lib/gitlab/favicon_spec.rb +++ b/spec/lib/gitlab/favicon_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::Favicon, :request_store do expect(described_class.main).to match_asset_path '/assets/favicon.png' end - it 'has blue favicon for development' do + it 'has blue favicon for development', unless: Gitlab.ee? do allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) expect(described_class.main).to match_asset_path '/assets/favicon-blue.png' end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index a1b5cea88c0..1c24244c3a6 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::Git::Blob, :seed_helper do end end - describe '.find' do + shared_examples '.find' do context 'nil path' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, nil) } @@ -128,6 +128,20 @@ describe Gitlab::Git::Blob, :seed_helper do end end + describe '.find with Gitaly enabled' do + it_behaves_like '.find' + end + + describe '.find with Rugged enabled', :enable_rugged do + it 'calls out to the Rugged implementation' do + allow_any_instance_of(Rugged).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original + + described_class.find(repository, SeedRepo::Commit::ID, 'files/images/6049019_460s.jpg') + end + + it_behaves_like '.find' + end + describe '.raw' do let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) } let(:bad_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::BigCommit::ID) } @@ -327,7 +341,7 @@ describe Gitlab::Git::Blob, :seed_helper do it { expect(blob.mode).to eq("100755") } end - context 'file with Chinese text' do + context 'file with Japanese text' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/テスト.txt") } it { expect(blob.name).to eq("テスト.txt") } diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 2611ebed25b..25052a79916 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -112,14 +112,14 @@ describe Gitlab::Git::Commit, :seed_helper do end context 'Class methods' do - describe '.find' do - it "should return first head commit if without params" do + shared_examples '.find' do + it "returns first head commit if without params" do expect(described_class.last(repository).id).to eq( rugged_repo.head.target.oid ) end - it "should return valid commit" do + it "returns valid commit" do expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_valid_commit end @@ -127,21 +127,21 @@ describe Gitlab::Git::Commit, :seed_helper do expect(described_class.find(repository, SeedRepo::Commit::ID).parent_ids).to be_an(Array) end - it "should return valid commit for tag" do + it "returns valid commit for tag" do expect(described_class.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') end - it "should return nil for non-commit ids" do + it "returns nil for non-commit ids" do blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") expect(described_class.find(repository, blob.id)).to be_nil end - it "should return nil for parent of non-commit object" do + it "returns nil for parent of non-commit object" do blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") expect(described_class.find(repository, "#{blob.id}^")).to be_nil end - it "should return nil for nonexisting ids" do + it "returns nil for nonexisting ids" do expect(described_class.find(repository, "+123_4532530XYZ")).to be_nil end @@ -154,6 +154,20 @@ describe Gitlab::Git::Commit, :seed_helper do end end + describe '.find with Gitaly enabled' do + it_should_behave_like '.find' + end + + describe '.find with Rugged enabled', :enable_rugged do + it 'calls out to the Rugged implementation' do + allow_any_instance_of(Rugged).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original + + described_class.find(repository, SeedRepo::Commit::ID) + end + + it_should_behave_like '.find' + end + describe '.last_for_path' do context 'no path' do subject { described_class.last_for_path(repository, 'master') } @@ -314,7 +328,7 @@ describe Gitlab::Git::Commit, :seed_helper do end describe '.find_all' do - it 'should return a return a collection of commits' do + it 'returns a return a collection of commits' do commits = described_class.find_all(repository) expect(commits).to all( be_a_kind_of(described_class) ) @@ -366,7 +380,32 @@ describe Gitlab::Git::Commit, :seed_helper do end end - describe '#batch_by_oid' do + shared_examples '.batch_by_oid' do + context 'with multiple OIDs' do + let(:oids) { [SeedRepo::Commit::ID, SeedRepo::FirstCommit::ID] } + + it 'returns multiple commits' do + commits = described_class.batch_by_oid(repository, oids) + + expect(commits.count).to eq(2) + expect(commits).to all( be_a(Gitlab::Git::Commit) ) + expect(commits.first.sha).to eq(SeedRepo::Commit::ID) + expect(commits.second.sha).to eq(SeedRepo::FirstCommit::ID) + end + end + + context 'when oids is empty' do + it 'returns empty commits' do + commits = described_class.batch_by_oid(repository, []) + + expect(commits.count).to eq(0) + end + end + end + + describe '.batch_by_oid with Gitaly enabled' do + it_should_behave_like '.batch_by_oid' + context 'when oids is empty' do it 'makes no Gitaly request' do expect(Gitlab::GitalyClient).not_to receive(:call) @@ -376,6 +415,16 @@ describe Gitlab::Git::Commit, :seed_helper do end end + describe '.batch_by_oid with Rugged enabled', :enable_rugged do + it_should_behave_like '.batch_by_oid' + + it 'calls out to the Rugged implementation' do + allow_any_instance_of(Rugged).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original + + described_class.batch_by_oid(repository, [SeedRepo::Commit::ID]) + end + end + shared_examples 'extracting commit signature' do context 'when the commit is signed' do let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } @@ -523,6 +572,18 @@ describe Gitlab::Git::Commit, :seed_helper do end end + describe '#gitaly_commit?' do + context 'when the commit data comes from gitaly' do + it { expect(commit.gitaly_commit?).to eq(true) } + end + + context 'when the commit data comes from a Hash' do + let(:commit) { described_class.new(repository, sample_commit_hash) } + + it { expect(commit.gitaly_commit?).to eq(false) } + end + end + describe '#has_zero_stats?' do it { expect(commit.has_zero_stats?).to eq(false) } end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 1d22329b670..9ab669ad488 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -182,7 +182,7 @@ EOT context "without default options" do let(:filtered_options) { described_class.filter_diff_options(options) } - it "should filter invalid options" do + it "filters invalid options" do expect(filtered_options).not_to have_key(:invalid_opt) end end @@ -193,16 +193,16 @@ EOT described_class.filter_diff_options(options, default_options) end - it "should filter invalid options" do + it "filters invalid options" do expect(filtered_options).not_to have_key(:invalid_opt) expect(filtered_options).not_to have_key(:bad_opt) end - it "should merge with default options" do + it "merges with default options" do expect(filtered_options).to have_key(:ignore_whitespace_change) end - it "should override default options" do + it "overrides default options" do expect(filtered_options).to have_key(:max_files) expect(filtered_options[:max_files]).to eq(100) end diff --git a/spec/lib/gitlab/git/gitmodules_parser_spec.rb b/spec/lib/gitlab/git/gitmodules_parser_spec.rb index 6fd2b33486b..de81dcd227d 100644 --- a/spec/lib/gitlab/git/gitmodules_parser_spec.rb +++ b/spec/lib/gitlab/git/gitmodules_parser_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Git::GitmodulesParser do - it 'should parse a .gitmodules file correctly' do + it 'parses a .gitmodules file correctly' do data = <<~GITMODULES [submodule "vendor/libgit2"] path = vendor/libgit2 diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb index 0d5069568e1..ebeb7b7b633 100644 --- a/spec/lib/gitlab/git/object_pool_spec.rb +++ b/spec/lib/gitlab/git/object_pool_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe Gitlab::Git::ObjectPool do + include RepoHelpers + let(:pool_repository) { create(:pool_repository) } let(:source_repository) { pool_repository.source_project.repository } @@ -76,4 +78,43 @@ describe Gitlab::Git::ObjectPool do end end end + + describe '#fetch' do + let(:source_repository_path) { File.join(TestEnv.repos_path, source_repository.relative_path) } + let(:source_repository_rugged) { Rugged::Repository.new(source_repository_path) } + let(:commit_count) { source_repository.commit_count } + + context "when the object's pool repository exists" do + it 'does not raise an error' do + expect { subject.fetch }.not_to raise_error + end + end + + context "when the object's pool repository does not exist" do + before do + subject.delete + end + + it "re-creates the object pool's repository" do + subject.fetch + + expect(subject.repository.exists?).to be true + end + + it 'does not raise an error' do + expect { subject.fetch }.not_to raise_error + end + + it 'fetches objects from the source repository' do + new_commit_id = new_commit_edit_old_file(source_repository_rugged).oid + + expect(subject.repository.exists?).to be false + + subject.fetch + + expect(subject.repository.commit_count('refs/remotes/origin/master')).to eq(commit_count) + expect(subject.repository.commit(new_commit_id).id).to eq(new_commit_id) + end + end + end end diff --git a/spec/lib/gitlab/git/pre_receive_error_spec.rb b/spec/lib/gitlab/git/pre_receive_error_spec.rb index 1b8be62dec6..cb030e38032 100644 --- a/spec/lib/gitlab/git/pre_receive_error_spec.rb +++ b/spec/lib/gitlab/git/pre_receive_error_spec.rb @@ -1,9 +1,19 @@ require 'spec_helper' describe Gitlab::Git::PreReceiveError do - it 'makes its message HTML-friendly' do - ex = described_class.new("hello\nworld\n") + Gitlab::Git::PreReceiveError::SAFE_MESSAGE_PREFIXES.each do |prefix| + context "error messages prefixed with #{prefix}" do + it 'accepts only errors lines with the prefix' do + ex = described_class.new("#{prefix} Hello,\nworld!") - expect(ex.message).to eq('hello<br>world<br>') + expect(ex.message).to eq('Hello,') + end + + it 'makes its message HTML-friendly' do + ex = described_class.new("#{prefix} Hello,\n#{prefix} world!\n") + + expect(ex.message).to eq('Hello,<br>world!') + end + end end end diff --git a/spec/lib/gitlab/git/repository_cleaner_spec.rb b/spec/lib/gitlab/git/repository_cleaner_spec.rb index 7f9cc2bc9ec..7bba0107e58 100644 --- a/spec/lib/gitlab/git/repository_cleaner_spec.rb +++ b/spec/lib/gitlab/git/repository_cleaner_spec.rb @@ -6,57 +6,62 @@ describe Gitlab::Git::RepositoryCleaner do let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:head_sha) { repository.head_commit.id } - let(:object_map_data) { "#{head_sha} #{'0' * 40}" } + let(:object_map_data) { "#{head_sha} #{Gitlab::Git::BLANK_SHA}" } - subject(:cleaner) { described_class.new(repository.raw) } + let(:clean_refs) { %W[refs/environments/1 refs/merge-requests/1 refs/keep-around/#{head_sha}] } + let(:keep_refs) { %w[refs/heads/_keep refs/tags/_keep] } - describe '#apply_bfg_object_map' do - let(:clean_refs) { %W[refs/environments/1 refs/merge-requests/1 refs/keep-around/#{head_sha}] } - let(:keep_refs) { %w[refs/heads/_keep refs/tags/_keep] } + subject(:cleaner) { described_class.new(repository.raw) } + shared_examples_for '#apply_bfg_object_map_stream' do before do (clean_refs + keep_refs).each { |ref| repository.create_ref(head_sha, ref) } end - context 'from StringIO' do - let(:object_map) { StringIO.new(object_map_data) } + it 'removes internal references' do + entries = [] - it 'removes internal references' do - cleaner.apply_bfg_object_map(object_map) + cleaner.apply_bfg_object_map_stream(object_map) do |rsp| + entries.concat(rsp.entries) + end - aggregate_failures do - clean_refs.each { |ref| expect(repository.ref_exists?(ref)).to be_falsy } - keep_refs.each { |ref| expect(repository.ref_exists?(ref)).to be_truthy } - end + aggregate_failures do + clean_refs.each { |ref| expect(repository.ref_exists?(ref)).to be(false) } + keep_refs.each { |ref| expect(repository.ref_exists?(ref)).to be(true) } + + expect(entries).to contain_exactly( + Gitaly::ApplyBfgObjectMapStreamResponse::Entry.new( + type: :COMMIT, + old_oid: head_sha, + new_oid: Gitlab::Git::BLANK_SHA + ) + ) end end + end - context 'from Gitlab::HttpIO' do - let(:url) { 'http://example.com/bfg_object_map.txt' } - let(:tempfile) { Tempfile.new } - let(:object_map) { Gitlab::HttpIO.new(url, object_map_data.size) } + describe '#apply_bfg_object_map_stream (from StringIO)' do + let(:object_map) { StringIO.new(object_map_data) } - around do |example| - begin - tempfile.write(object_map_data) - tempfile.close + include_examples '#apply_bfg_object_map_stream' + end - example.run - ensure - tempfile.unlink - end - end + describe '#apply_bfg_object_map_stream (from Gitlab::HttpIO)' do + let(:url) { 'http://example.com/bfg_object_map.txt' } + let(:tempfile) { Tempfile.new } + let(:object_map) { Gitlab::HttpIO.new(url, object_map_data.size) } - it 'removes internal references' do - stub_remote_url_200(url, tempfile.path) + around do |example| + tempfile.write(object_map_data) + tempfile.close - cleaner.apply_bfg_object_map(object_map) + stub_remote_url_200(url, tempfile.path) - aggregate_failures do - clean_refs.each { |ref| expect(repository.ref_exists?(ref)).to be_falsy } - keep_refs.each { |ref| expect(repository.ref_exists?(ref)).to be_truthy } - end - end + example.run + ensure + tempfile.unlink end + + include_examples '#apply_bfg_object_map_stream' end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 8a9e78ba3c3..e72fb9c6fbc 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -3,6 +3,7 @@ require "spec_helper" describe Gitlab::Git::Repository, :seed_helper do include Gitlab::EncodingHelper + include RepoHelpers using RSpec::Parameterized::TableSyntax shared_examples 'wrapping gRPC errors' do |gitaly_client_class, gitaly_client_method| @@ -28,51 +29,6 @@ describe Gitlab::Git::Repository, :seed_helper do let(:storage_path) { TestEnv.repos_path } let(:user) { build(:user) } - describe '.create_hooks' do - let(:repo_path) { File.join(storage_path, 'hook-test.git') } - let(:hooks_dir) { File.join(repo_path, 'hooks') } - let(:target_hooks_dir) { Gitlab.config.gitlab_shell.hooks_path } - let(:existing_target) { File.join(repo_path, 'foobar') } - - before do - FileUtils.rm_rf(repo_path) - FileUtils.mkdir_p(repo_path) - end - - context 'hooks is a directory' do - let(:existing_file) { File.join(hooks_dir, 'my-file') } - - before do - FileUtils.mkdir_p(hooks_dir) - FileUtils.touch(existing_file) - described_class.create_hooks(repo_path, target_hooks_dir) - end - - it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) } - it { expect(Dir[File.join(repo_path, "hooks.old.*/my-file")].count).to eq(1) } - end - - context 'hooks is a valid symlink' do - before do - FileUtils.mkdir_p existing_target - File.symlink(existing_target, hooks_dir) - described_class.create_hooks(repo_path, target_hooks_dir) - end - - it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) } - end - - context 'hooks is a broken symlink' do - before do - FileUtils.rm_f(existing_target) - File.symlink(existing_target, hooks_dir) - described_class.create_hooks(repo_path, target_hooks_dir) - end - - it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) } - end - end - describe "Respond to" do subject { repository } @@ -95,6 +51,12 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#create_repository' do + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :create_repository do + subject { repository.create_repository } + end + end + describe '#branch_names' do subject { repository.branch_names } @@ -152,13 +114,14 @@ describe Gitlab::Git::Repository, :seed_helper do let(:append_sha) { true } let(:ref) { 'master' } let(:format) { nil } + let(:path) { nil } let(:expected_extension) { 'tar.gz' } let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" } let(:expected_path) { File.join(storage_path, cache_key, expected_filename) } let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" } - subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha) } + subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha, path: path) } it 'sets CommitId to the commit SHA' do expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID) @@ -176,6 +139,14 @@ describe Gitlab::Git::Repository, :seed_helper do expect(metadata['ArchivePath']).to eq(expected_path) end + context 'path is set' do + let(:path) { 'foo/bar' } + + it 'appends the path to the prefix' do + expect(metadata['ArchivePrefix']).to eq("#{expected_prefix}-foo-bar") + end + end + context 'append_sha varies archive path and filename' do where(:append_sha, :ref, :expected_prefix) do sha = SeedRepo::LastCommit::ID @@ -215,6 +186,18 @@ describe Gitlab::Git::Repository, :seed_helper do it { is_expected.to be < 2 } end + describe '#object_directory_size' do + before do + allow(repository.gitaly_repository_client) + .to receive(:get_object_directory_size) + .and_return(2) + end + + subject { repository.object_directory_size } + + it { is_expected.to eq 2048 } + end + describe '#empty?' do it { expect(repository).not_to be_empty } end @@ -441,20 +424,20 @@ describe Gitlab::Git::Repository, :seed_helper do ensure_seeds end - it "should create a new branch" do + it "creates a new branch" do expect(repository.create_branch('new_branch', 'master')).not_to be_nil end - it "should create a new branch with the right name" do + it "creates a new branch with the right name" do expect(repository.create_branch('another_branch', 'master').name).to eq('another_branch') end - it "should fail if we create an existing branch" do + it "fails if we create an existing branch" do repository.create_branch('duplicated_branch', 'master') expect {repository.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") end - it "should fail if we create a branch from a non existing ref" do + it "fails if we create a branch from a non existing ref" do expect {repository.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") end end @@ -513,7 +496,7 @@ describe Gitlab::Git::Repository, :seed_helper do describe "#refs_hash" do subject { repository.refs_hash } - it "should have as many entries as branches and tags" do + it "has as many entries as branches and tags" do expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS # We flatten in case a commit is pointed at by more than one branch and/or tag expect(subject.values.flatten.size).to eq(expected_refs.size) @@ -522,6 +505,13 @@ describe Gitlab::Git::Repository, :seed_helper do it 'has valid commit ids as keys' do expect(subject.keys).to all( match(Commit::COMMIT_SHA_PATTERN) ) end + + it 'does not error when dereferenced_target is nil' do + blob_id = repository.blob_at('master', 'README.md').id + repository_rugged.tags.create("refs/tags/blob-tag", blob_id) + + expect { subject }.not_to raise_error + end end describe '#fetch_repository_as_mirror' do @@ -604,11 +594,11 @@ describe Gitlab::Git::Repository, :seed_helper do end shared_examples 'search files by content' do - it 'should have 2 items' do + it 'has 2 items' do expect(search_results.size).to eq(2) end - it 'should have the correct matching line' do + it 'has the correct matching line' do expect(search_results).to contain_exactly("search-files-by-content-branch:encoding/CHANGELOG\u00001\u0000search-files-by-content change\n", "search-files-by-content-branch:anotherfile\u00001\u0000search-files-by-content change\n") end @@ -619,16 +609,6 @@ describe Gitlab::Git::Repository, :seed_helper do repository.search_files_by_content('search-files-by-content', 'search-files-by-content-branch') end end - - it_should_behave_like 'search files by content' do - let(:search_results) do - repository.gitaly_repository_client.search_files_by_content( - 'search-files-by-content-branch', - 'search-files-by-content', - chunked_response: false - ) - end - end end describe '#find_remote_root_ref' do @@ -851,7 +831,7 @@ describe Gitlab::Git::Repository, :seed_helper do context "where provides 'after' timestamp" do options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') } - it "should returns commits on or after that timestamp" do + it "returns commits on or after that timestamp" do commits = repository.log(options) expect(commits.size).to be > 0 @@ -864,7 +844,7 @@ describe Gitlab::Git::Repository, :seed_helper do context "where provides 'before' timestamp" do options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') } - it "should returns commits on or before that timestamp" do + it "returns commits on or before that timestamp" do commits = repository.log(options) expect(commits.size).to be > 0 @@ -1065,14 +1045,14 @@ describe Gitlab::Git::Repository, :seed_helper do end describe '#find_branch' do - it 'should return a Branch for master' do + it 'returns a Branch for master' do branch = repository.find_branch('master') expect(branch).to be_a_kind_of(Gitlab::Git::Branch) expect(branch.name).to eq('master') end - it 'should handle non-existent branch' do + it 'handles non-existent branch' do branch = repository.find_branch('this-is-garbage') expect(branch).to eq(nil) @@ -1698,12 +1678,48 @@ describe Gitlab::Git::Repository, :seed_helper do expect(repository.delete_config(*%w[does.not.exist test.foo1 test.foo2])).to be_nil + # Workaround for https://github.com/libgit2/rugged/issues/785: If + # Gitaly changes .gitconfig while Rugged has the file loaded + # Rugged::Repository#each_key will report stale values unless a + # lookup is done first. + expect(repository_rugged.config['test.foo1']).to be_nil config_keys = repository_rugged.config.each_key.to_a expect(config_keys).not_to include('test.foo1') expect(config_keys).not_to include('test.foo2') end end + describe '#merge_to_ref' do + let(:repository) { mutable_repository } + let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } + let(:left_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } + let(:right_branch) { 'test-master' } + let(:target_ref) { 'refs/merge-requests/999/merge' } + + before do + repository.create_branch(right_branch, branch_head) unless repository.branch_exists?(right_branch) + end + + def merge_to_ref + repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message') + end + + it 'generates a commit in the target_ref' do + expect(repository.ref_exists?(target_ref)).to be(false) + + commit_sha = merge_to_ref + ref_head = repository.commit(target_ref) + + expect(commit_sha).to be_present + expect(repository.ref_exists?(target_ref)).to be(true) + expect(ref_head.id).to eq(commit_sha) + end + + it 'does not change the right branch HEAD' do + expect { merge_to_ref }.not_to change { repository.find_branch(right_branch).target } + end + end + describe '#merge' do let(:repository) { mutable_repository } let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' } @@ -1910,13 +1926,6 @@ describe Gitlab::Git::Repository, :seed_helper do expect { imported_repo.fsck }.not_to raise_exception end - it 'creates a symlink to the global hooks dir' do - imported_repo.create_from_bundle(valid_bundle_path) - hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') } - - expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) - end - it 'raises an error if the bundle is an attempted malicious payload' do expect do imported_repo.create_from_bundle(malicious_bundle_path) @@ -1924,6 +1933,70 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#compare_source_branch' do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '', 'group/project') } + + context 'within same repository' do + it 'does not create a temp ref' do + expect(repository).not_to receive(:fetch_source_branch!) + expect(repository).not_to receive(:delete_refs) + + compare = repository.compare_source_branch('master', repository, 'feature', straight: false) + expect(compare).to be_a(Gitlab::Git::Compare) + expect(compare.commits.count).to be > 0 + end + + it 'returns empty commits when source ref does not exist' do + compare = repository.compare_source_branch('master', repository, 'non-existent-branch', straight: false) + + expect(compare.commits).to be_empty + end + end + + context 'with different repositories' do + context 'when ref is known by source repo, but not by target' do + before do + mutable_repository.write_ref('another-branch', 'feature') + end + + it 'creates temp ref' do + expect(repository).not_to receive(:fetch_source_branch!) + expect(repository).not_to receive(:delete_refs) + + compare = repository.compare_source_branch('master', mutable_repository, 'another-branch', straight: false) + expect(compare).to be_a(Gitlab::Git::Compare) + expect(compare.commits.count).to be > 0 + end + end + + context 'when ref is known by source and target repos' do + before do + mutable_repository.write_ref('another-branch', 'feature') + repository.write_ref('another-branch', 'feature') + end + + it 'does not create a temp ref' do + expect(repository).not_to receive(:fetch_source_branch!) + expect(repository).not_to receive(:delete_refs) + + compare = repository.compare_source_branch('master', mutable_repository, 'another-branch', straight: false) + expect(compare).to be_a(Gitlab::Git::Compare) + expect(compare.commits.count).to be > 0 + end + end + + context 'when ref is unknown by source repo' do + it 'returns nil when source ref does not exist' do + expect(repository).to receive(:fetch_source_branch!).and_call_original + expect(repository).to receive(:delete_refs).and_call_original + + compare = repository.compare_source_branch('master', mutable_repository, 'non-existent-branch', straight: false) + expect(compare).to be_nil + end + end + end + end + describe '#checksum' do it 'calculates the checksum for non-empty repo' do expect(repository.checksum).to eq '51d0a9662681f93e1fee547a6b7ba2bcaf716059' @@ -2097,86 +2170,48 @@ describe Gitlab::Git::Repository, :seed_helper do repository_rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha) end - # Build the options hash that's passed to Rugged::Commit#create - def commit_options(repo, index, target, ref, message) - options = {} - options[:tree] = index.write_tree(repo) - options[:author] = { - email: "test@example.com", - name: "Test Author", - time: Time.gm(2014, "mar", 3, 20, 15, 1) - } - options[:committer] = { - email: "test@example.com", - name: "Test Author", - time: Time.gm(2014, "mar", 3, 20, 15, 1) - } - options[:message] ||= message - options[:parents] = repo.empty? ? [] : [target].compact - options[:update_ref] = ref - - options - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the - # contents of CHANGELOG with a single new line of text. - def new_commit_edit_old_file(repo) - oid = repo.write("I replaced the changelog with this text", :blob) - index = repo.index - index.read_tree(repo.head.target.tree) - index.add(path: "CHANGELOG", oid: oid, mode: 0100644) - - options = commit_options( - repo, - index, - repo.head.target, - "HEAD", - "Edit CHANGELOG in its original location" - ) - - sha = Rugged::Commit.create(repo, options) - repo.lookup(sha) - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the - # contents of the specified file_path with new text. - def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head) - oid = repo.write(text, :blob) - index = repo.index - index.read_tree(branch.target.tree) - index.add(path: file_path, oid: oid, mode: 0100644) - options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message) - sha = Rugged::Commit.create(repo, options) - repo.lookup(sha) - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the - # contents of encoding/CHANGELOG with new text. - def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text) - branch = repo.branches[branch_name] - new_commit_edit_new_file(repo, file_path, commit_message, text, branch) - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Moves the - # CHANGELOG file to the encoding/ directory. - def new_commit_move_file(repo) - blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid] - file_content = repo.lookup(blob_oid).content - oid = repo.write(file_content, :blob) - index = repo.index - index.read_tree(repo.head.target.tree) - index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644) - index.remove("CHANGELOG") - - options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/") - - sha = Rugged::Commit.create(repo, options) - repo.lookup(sha) - end - def refs(dir) IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line| line.split("\t").last end end + + describe '#disconnect_alternates' do + let(:project) { create(:project, :repository) } + let(:pool_repository) { create(:pool_repository) } + let(:repository) { project.repository } + let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) } + let(:object_pool) { pool_repository.object_pool } + let(:object_pool_path) { File.join(TestEnv.repos_path, object_pool.repository.relative_path) } + let(:object_pool_rugged) { Rugged::Repository.new(object_pool_path) } + + before do + object_pool.create + end + + it 'does not raise an error when disconnecting a non-linked repository' do + expect { repository.disconnect_alternates }.not_to raise_error + end + + it 'removes the alternates file' do + object_pool.link(repository) + + alternates_file = File.join(repository_path, "objects", "info", "alternates") + expect(File.exist?(alternates_file)).to be_truthy + + repository.disconnect_alternates + + expect(File.exist?(alternates_file)).to be_falsey + end + + it 'can still access objects in the object pool' do + object_pool.link(repository) + new_commit = new_commit_edit_old_file(object_pool_rugged) + expect(repository.commit(new_commit.oid).id).to eq(new_commit.oid) + + repository.disconnect_alternates + + expect(repository.commit(new_commit.oid).id).to eq(new_commit.oid) + end + end end diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 4a4d69490a3..7e169cfe270 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe Gitlab::Git::Tree, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } - context :repo do + shared_examples :repo do let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) } it { expect(tree).to be_kind_of Array } @@ -12,6 +12,19 @@ describe Gitlab::Git::Tree, :seed_helper do it { expect(tree.select(&:file?).size).to eq(10) } it { expect(tree.select(&:submodule?).size).to eq(2) } + it 'returns an empty array when called with an invalid ref' do + expect(described_class.where(repository, 'foobar-does-not-exist')).to eq([]) + end + + it 'returns a list of tree objects' do + entries = described_class.where(repository, SeedRepo::Commit::ID, 'files', true) + + expect(entries.map(&:path)).to include('files/html', + 'files/markdown/ruby-style-guide.md') + expect(entries.count).to be >= 10 + expect(entries).to all(be_a(Gitlab::Git::Tree)) + end + describe '#dir?' do let(:dir) { tree.select(&:dir?).first } @@ -20,8 +33,8 @@ describe Gitlab::Git::Tree, :seed_helper do it { expect(dir.commit_id).to eq(SeedRepo::Commit::ID) } it { expect(dir.name).to eq('encoding') } it { expect(dir.path).to eq('encoding') } - it { expect(dir.flat_path).to eq('encoding') } it { expect(dir.mode).to eq('40000') } + it { expect(dir.flat_path).to eq('encoding') } context :subdir do let(:subdir) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files').first } @@ -44,6 +57,51 @@ describe Gitlab::Git::Tree, :seed_helper do it { expect(subdir_file.path).to eq('files/ruby/popen.rb') } it { expect(subdir_file.flat_path).to eq('files/ruby/popen.rb') } end + + context :flat_path do + let(:filename) { 'files/flat/path/correct/content.txt' } + let(:oid) { create_file(filename) } + let(:subdir_file) { Gitlab::Git::Tree.where(repository, oid, 'files/flat').first } + let(:repository_rugged) { Rugged::Repository.new(File.join(SEED_STORAGE_PATH, TEST_REPO_PATH)) } + + it { expect(subdir_file.flat_path).to eq('files/flat/path/correct') } + end + + def create_file(path) + oid = repository_rugged.write('test', :blob) + index = repository_rugged.index + index.add(path: filename, oid: oid, mode: 0100644) + + options = commit_options( + repository_rugged, + index, + repository_rugged.head.target, + 'HEAD', + 'Add new file') + + Rugged::Commit.create(repository_rugged, options) + end + + # Build the options hash that's passed to Rugged::Commit#create + def commit_options(repo, index, target, ref, message) + options = {} + options[:tree] = index.write_tree(repo) + options[:author] = { + email: "test@example.com", + name: "Test Author", + time: Time.gm(2014, "mar", 3, 20, 15, 1) + } + options[:committer] = { + email: "test@example.com", + name: "Test Author", + time: Time.gm(2014, "mar", 3, 20, 15, 1) + } + options[:message] ||= message + options[:parents] = repo.empty? ? [] : [target].compact + options[:update_ref] = ref + + options + end end describe '#file?' do @@ -79,9 +137,17 @@ describe Gitlab::Git::Tree, :seed_helper do end end - describe '#where' do - it 'returns an empty array when called with an invalid ref' do - expect(described_class.where(repository, 'foobar-does-not-exist')).to eq([]) + describe '.where with Gitaly enabled' do + it_behaves_like :repo + end + + describe '.where with Rugged enabled', :enable_rugged do + it 'calls out to the Rugged implementation' do + allow_any_instance_of(Rugged).to receive(:lookup).with(SeedRepo::Commit::ID) + + described_class.where(repository, SeedRepo::Commit::ID, 'files', false) end + + it_behaves_like :repo end end diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb index ded5d7576df..1e577392949 100644 --- a/spec/lib/gitlab/git/wiki_spec.rb +++ b/spec/lib/gitlab/git/wiki_spec.rb @@ -21,13 +21,13 @@ describe Gitlab::Git::Wiki do end it 'returns all the pages' do - expect(subject.pages.count).to eq(2) - expect(subject.pages.first.title).to eq 'page1' - expect(subject.pages.last.title).to eq 'page2' + expect(subject.list_pages.count).to eq(2) + expect(subject.list_pages.first.title).to eq 'page1' + expect(subject.list_pages.last.title).to eq 'page2' end it 'returns only one page' do - pages = subject.pages(limit: 1) + pages = subject.list_pages(limit: 1) expect(pages.count).to eq(1) expect(pages.first.title).to eq 'page1' @@ -62,8 +62,8 @@ describe Gitlab::Git::Wiki do subject.delete_page('*', commit_details('whatever')) - expect(subject.pages.count).to eq 1 - expect(subject.pages.first.title).to eq 'page1' + expect(subject.list_pages.count).to eq 1 + expect(subject.list_pages.first.title).to eq 'page1' end end @@ -87,7 +87,7 @@ describe Gitlab::Git::Wiki do with_them do subject { wiki.preview_slug(title, format) } - let(:gitaly_slug) { wiki.pages.first } + let(:gitaly_slug) { wiki.list_pages.first } it { is_expected.to eq(expected_slug) } @@ -96,7 +96,7 @@ describe Gitlab::Git::Wiki do create_page(title, 'content', format: format) - gitaly_slug = wiki.pages.first.url_path + gitaly_slug = wiki.list_pages.first.url_path is_expected.to eq(gitaly_slug) end diff --git a/spec/lib/gitlab/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb index 3ab04a1c46d..b63389af29f 100644 --- a/spec/lib/gitlab/git_ref_validator_spec.rb +++ b/spec/lib/gitlab/git_ref_validator_spec.rb @@ -1,31 +1,69 @@ require 'spec_helper' describe Gitlab::GitRefValidator do - it { expect(described_class.validate('feature/new')).to be_truthy } - it { expect(described_class.validate('implement_@all')).to be_truthy } - it { expect(described_class.validate('my_new_feature')).to be_truthy } - it { expect(described_class.validate('my-branch')).to be_truthy } - it { expect(described_class.validate('#1')).to be_truthy } - it { expect(described_class.validate('feature/refs/heads/foo')).to be_truthy } - it { expect(described_class.validate('feature/~new/')).to be_falsey } - it { expect(described_class.validate('feature/^new/')).to be_falsey } - it { expect(described_class.validate('feature/:new/')).to be_falsey } - it { expect(described_class.validate('feature/?new/')).to be_falsey } - it { expect(described_class.validate('feature/*new/')).to be_falsey } - it { expect(described_class.validate('feature/[new/')).to be_falsey } - it { expect(described_class.validate('feature/new/')).to be_falsey } - it { expect(described_class.validate('feature/new.')).to be_falsey } - it { expect(described_class.validate('feature\@{')).to be_falsey } - it { expect(described_class.validate('feature\new')).to be_falsey } - it { expect(described_class.validate('feature//new')).to be_falsey } - it { expect(described_class.validate('feature new')).to be_falsey } - it { expect(described_class.validate('refs/heads/')).to be_falsey } - it { expect(described_class.validate('refs/remotes/')).to be_falsey } - it { expect(described_class.validate('refs/heads/feature')).to be_falsey } - it { expect(described_class.validate('refs/remotes/origin')).to be_falsey } - it { expect(described_class.validate('-')).to be_falsey } - it { expect(described_class.validate('-branch')).to be_falsey } - it { expect(described_class.validate('.tag')).to be_falsey } - it { expect(described_class.validate('my branch')).to be_falsey } - it { expect(described_class.validate("\xA0\u0000\xB0")).to be_falsey } + using RSpec::Parameterized::TableSyntax + + context '.validate' do + it { expect(described_class.validate('feature/new')).to be true } + it { expect(described_class.validate('implement_@all')).to be true } + it { expect(described_class.validate('my_new_feature')).to be true } + it { expect(described_class.validate('my-branch')).to be true } + it { expect(described_class.validate('#1')).to be true } + it { expect(described_class.validate('feature/refs/heads/foo')).to be true } + it { expect(described_class.validate('feature/~new/')).to be false } + it { expect(described_class.validate('feature/^new/')).to be false } + it { expect(described_class.validate('feature/:new/')).to be false } + it { expect(described_class.validate('feature/?new/')).to be false } + it { expect(described_class.validate('feature/*new/')).to be false } + it { expect(described_class.validate('feature/[new/')).to be false } + it { expect(described_class.validate('feature/new/')).to be false } + it { expect(described_class.validate('feature/new.')).to be false } + it { expect(described_class.validate('feature\@{')).to be false } + it { expect(described_class.validate('feature\new')).to be false } + it { expect(described_class.validate('feature//new')).to be false } + it { expect(described_class.validate('feature new')).to be false } + it { expect(described_class.validate('refs/heads/')).to be false } + it { expect(described_class.validate('refs/remotes/')).to be false } + it { expect(described_class.validate('refs/heads/feature')).to be false } + it { expect(described_class.validate('refs/remotes/origin')).to be false } + it { expect(described_class.validate('-')).to be false } + it { expect(described_class.validate('-branch')).to be false } + it { expect(described_class.validate('+foo:bar')).to be false } + it { expect(described_class.validate('foo:bar')).to be false } + it { expect(described_class.validate('.tag')).to be false } + it { expect(described_class.validate('my branch')).to be false } + it { expect(described_class.validate("\xA0\u0000\xB0")).to be false } + end + + context '.validate_merge_request_branch' do + it { expect(described_class.validate_merge_request_branch('HEAD')).to be true } + it { expect(described_class.validate_merge_request_branch('feature/new')).to be true } + it { expect(described_class.validate_merge_request_branch('implement_@all')).to be true } + it { expect(described_class.validate_merge_request_branch('my_new_feature')).to be true } + it { expect(described_class.validate_merge_request_branch('my-branch')).to be true } + it { expect(described_class.validate_merge_request_branch('#1')).to be true } + it { expect(described_class.validate_merge_request_branch('feature/refs/heads/foo')).to be true } + it { expect(described_class.validate_merge_request_branch('feature/~new/')).to be false } + it { expect(described_class.validate_merge_request_branch('feature/^new/')).to be false } + it { expect(described_class.validate_merge_request_branch('feature/:new/')).to be false } + it { expect(described_class.validate_merge_request_branch('feature/?new/')).to be false } + it { expect(described_class.validate_merge_request_branch('feature/*new/')).to be false } + it { expect(described_class.validate_merge_request_branch('feature/[new/')).to be false } + it { expect(described_class.validate_merge_request_branch('feature/new/')).to be false } + it { expect(described_class.validate_merge_request_branch('feature/new.')).to be false } + it { expect(described_class.validate_merge_request_branch('feature\@{')).to be false } + it { expect(described_class.validate_merge_request_branch('feature\new')).to be false } + it { expect(described_class.validate_merge_request_branch('feature//new')).to be false } + it { expect(described_class.validate_merge_request_branch('feature new')).to be false } + it { expect(described_class.validate_merge_request_branch('refs/heads/master')).to be true } + it { expect(described_class.validate_merge_request_branch('refs/heads/')).to be false } + it { expect(described_class.validate_merge_request_branch('refs/remotes/')).to be false } + it { expect(described_class.validate_merge_request_branch('-')).to be false } + it { expect(described_class.validate_merge_request_branch('-branch')).to be false } + it { expect(described_class.validate_merge_request_branch('+foo:bar')).to be false } + it { expect(described_class.validate_merge_request_branch('foo:bar')).to be false } + it { expect(described_class.validate_merge_request_branch('.tag')).to be false } + it { expect(described_class.validate_merge_request_branch('my branch')).to be false } + it { expect(described_class.validate_merge_request_branch("\xA0\u0000\xB0")).to be false } + end end diff --git a/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb b/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb index 369deff732a..c42332dc27b 100644 --- a/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb @@ -6,14 +6,14 @@ describe Gitlab::GitalyClient::CleanupService do let(:relative_path) { project.disk_path + '.git' } let(:client) { described_class.new(project.repository) } - describe '#apply_bfg_object_map' do - it 'sends an apply_bfg_object_map message' do + describe '#apply_bfg_object_map_stream' do + it 'sends an apply_bfg_object_map_stream message' do expect_any_instance_of(Gitaly::CleanupService::Stub) - .to receive(:apply_bfg_object_map) + .to receive(:apply_bfg_object_map_stream) .with(kind_of(Enumerator), kind_of(Hash)) - .and_return(double) + .and_return([]) - client.apply_bfg_object_map(StringIO.new) + client.apply_bfg_object_map_stream(StringIO.new) end end end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index d7bd757149d..6d6107ca3e7 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -221,6 +221,21 @@ describe Gitlab::GitalyClient::CommitService do expect(commit).to eq(commit_dbl) end end + + context 'when caching of the ref name is enabled' do + it 'returns a cached commit' do + expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit).once.and_return(double(commit: commit_dbl)) + + commit = nil + 2.times do + ::Gitlab::GitalyClient.allow_ref_name_caching do + commit = described_class.new(repository).find_commit('master') + end + end + + expect(commit).to eq(commit_dbl) + end + end end end diff --git a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb index 149b7ec5bb0..0e0c3d329b5 100644 --- a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb @@ -43,4 +43,24 @@ describe Gitlab::GitalyClient::ObjectPoolService do end end end + + describe '#fetch' do + before do + subject.delete + end + + it 'restores the pool repository objects' do + subject.fetch(project.repository) + + expect(object_pool.repository.exists?).to be(true) + end + + context 'when called twice' do + it "doesn't raise an error" do + subject.delete + + expect { subject.fetch(project.repository) }.not_to raise_error + end + end + end end diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index b37fe2686b6..7579a6577b9 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -39,7 +39,7 @@ describe Gitlab::GitalyClient::OperationService do context "when pre_receive_error is present" do let(:response) do - Gitaly::UserCreateBranchResponse.new(pre_receive_error: "something failed") + Gitaly::UserCreateBranchResponse.new(pre_receive_error: "GitLab: something failed") end it "throws a PreReceive exception" do @@ -80,7 +80,7 @@ describe Gitlab::GitalyClient::OperationService do context "when pre_receive_error is present" do let(:response) do - Gitaly::UserUpdateBranchResponse.new(pre_receive_error: "something failed") + Gitaly::UserUpdateBranchResponse.new(pre_receive_error: "GitLab: something failed") end it "throws a PreReceive exception" do @@ -117,7 +117,7 @@ describe Gitlab::GitalyClient::OperationService do context "when pre_receive_error is present" do let(:response) do - Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "something failed") + Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "GitLab: something failed") end it "throws a PreReceive exception" do @@ -175,7 +175,7 @@ describe Gitlab::GitalyClient::OperationService do shared_examples 'cherry pick and revert errors' do context 'when a pre_receive_error is present' do - let(:response) { response_class.new(pre_receive_error: "something failed") } + let(:response) { response_class.new(pre_receive_error: "GitLab: something failed") } it 'raises a PreReceiveError' do expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "something failed") @@ -313,7 +313,7 @@ describe Gitlab::GitalyClient::OperationService do end context 'when a pre_receive_error is present' do - let(:response) { Gitaly::UserCommitFilesResponse.new(pre_receive_error: "something failed") } + let(:response) { Gitaly::UserCommitFilesResponse.new(pre_receive_error: "GitLab: something failed") } it 'raises a PreReceiveError' do expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "something failed") diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 400d426c949..0bb6e582159 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -89,6 +89,16 @@ describe Gitlab::GitalyClient::RefService do end end + describe '#list_new_blobs' do + it 'raises DeadlineExceeded when timeout is too small' do + newrev = '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' + + expect do + client.list_new_blobs(newrev, dynamic_timeout: 0.001) + end.to raise_error(GRPC::DeadlineExceeded) + end + end + describe '#local_branches' do it 'sends a find_local_branches message' do expect_any_instance_of(Gitaly::RefService::Stub) @@ -155,4 +165,15 @@ describe Gitlab::GitalyClient::RefService do client.delete_refs(except_with_prefixes: prefixes) end end + + describe '#pack_refs' do + it 'sends a pack_refs message' do + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:pack_refs) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(:pack_refs_response)) + + client.pack_refs + end + end end diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index 46ca2340389..a3808adb376 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -73,6 +73,17 @@ describe Gitlab::GitalyClient::RepositoryService do end end + describe '#get_object_directory_size' do + it 'sends a get_object_directory_size message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:get_object_directory_size) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(size: 0) + + client.get_object_directory_size + end + end + describe '#apply_gitattributes' do let(:revision) { 'master' } @@ -231,4 +242,34 @@ describe Gitlab::GitalyClient::RepositoryService do client.raw_changes_between('deadbeef', 'deadpork') end end + + describe '#disconnect_alternates' do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) } + let(:pool_repository) { create(:pool_repository) } + let(:object_pool) { pool_repository.object_pool } + let(:object_pool_service) { Gitlab::GitalyClient::ObjectPoolService.new(object_pool) } + + before do + object_pool_service.create(repository) + object_pool_service.link_repository(repository) + end + + it 'deletes the alternates file' do + repository.disconnect_alternates + + alternates_file = File.join(repository_path, "objects", "info", "alternates") + + expect(File.exist?(alternates_file)).to be_falsey + end + + context 'when called twice' do + it "doesn't raise an error" do + repository.disconnect_alternates + + expect { repository.disconnect_alternates }.not_to raise_error + end + end + end end diff --git a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb index c89913ec8e9..bb10be2a4dc 100644 --- a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb +++ b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb @@ -26,4 +26,14 @@ describe Gitlab::GitalyClient::StorageSettings do end end end + + describe '.disk_access_denied?' do + it 'return false when Rugged is enabled', :enable_rugged do + expect(described_class.disk_access_denied?).to be_falsey + end + + it 'returns true' do + expect(described_class.disk_access_denied?).to be_truthy + end + end end diff --git a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb index d82c9c28da0..4fa8e97aca0 100644 --- a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb @@ -46,7 +46,7 @@ describe Gitlab::GitalyClient::WikiService do end end - describe '#get_all_pages' do + describe '#load_all_pages' do let(:page_2_info) { { title: 'My Page 2', raw_data: 'c', version: page_version } } let(:response) do [ @@ -63,7 +63,7 @@ describe Gitlab::GitalyClient::WikiService do let(:wiki_page_2) { subject[1].first } let(:wiki_page_2_version) { subject[1].last } - subject { client.get_all_pages } + subject { client.load_all_pages } it 'sends a wiki_get_all_pages message' do expect_any_instance_of(Gitaly::WikiService::Stub) @@ -99,7 +99,7 @@ describe Gitlab::GitalyClient::WikiService do end context 'with limits' do - subject { client.get_all_pages(limit: 1) } + subject { client.load_all_pages(limit: 1) } it 'sends a request with the limit' do expect_any_instance_of(Gitaly::WikiService::Stub) diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index cf12baf1a93..da1eb0c2618 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -142,6 +142,48 @@ describe Gitlab::GitalyClient do end end + describe '.request_kwargs' do + context 'when catfile-cache feature is enabled' do + before do + stub_feature_flags('gitaly_catfile-cache': true) + end + + it 'sets the gitaly-session-id in the metadata' do + results = described_class.request_kwargs('default', nil) + 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', nil)[:metadata]['gitaly-session-id'] + + expect(described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']).not_to eq(gitaly_session_id) + 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', nil)[:metadata]['gitaly-session-id'] + + 3.times do + expect(described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']).to eq(gitaly_session_id) + end + end + end + end + + context 'when catfile-cache feature is disabled' do + before do + stub_feature_flags({ 'gitaly_catfile-cache': false }) + end + + it 'does not set the gitaly-session-id in the metadata' do + results = described_class.request_kwargs('default', nil) + expect(results[:metadata]).not_to include('gitaly-session-id') + end + end + end + describe 'enforce_gitaly_request_limits?' do def call_gitaly(count = 1) (1..count).each do @@ -149,11 +191,21 @@ describe Gitlab::GitalyClient do end end - context 'when RequestStore is enabled', :request_store do + context 'when RequestStore is enabled and the maximum number of calls is not enforced by a feature flag', :request_store do + before do + stub_feature_flags(gitaly_enforce_requests_limits: false) + end + it 'allows up the maximum number of allowed calls' do expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) }.not_to raise_error end + it 'allows the maximum number of calls to be exceeded if GITALY_DISABLE_REQUEST_LIMITS is set' do + stub_env('GITALY_DISABLE_REQUEST_LIMITS', 'true') + + expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) }.not_to raise_error + end + context 'when the maximum number of calls has been reached' do before do call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) @@ -189,6 +241,32 @@ describe Gitlab::GitalyClient do end end + context 'in production and when RequestStore is enabled', :request_store do + before do + allow(Rails.env).to receive(:production?).and_return(true) + end + + context 'when the maximum number of calls is enforced by a feature flag' do + before do + stub_feature_flags(gitaly_enforce_requests_limits: true) + end + + it 'does not allow the maximum number of calls to be exceeded' do + expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) }.to raise_error(Gitlab::GitalyClient::TooManyInvocationsError) + end + end + + context 'when the maximum number of calls is not enforced by a feature flag' do + before do + stub_feature_flags(gitaly_enforce_requests_limits: false) + end + + it 'allows the maximum number of calls to be exceeded' do + expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) }.not_to raise_error + end + end + end + context 'when RequestStore is not active' do it 'does not raise errors when the maximum number of allowed calls is exceeded' do expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 2) }.not_to raise_error diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb index 7901ae005d9..dab5767ece1 100644 --- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb @@ -98,6 +98,7 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach description: 'This is my issue', milestone_id: milestone.id, state: :opened, + state_id: 1, created_at: created_at, updated_at: updated_at }, @@ -127,6 +128,7 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach description: "*Created by: alice*\n\nThis is my issue", milestone_id: milestone.id, state: :opened, + state_id: 1, created_at: created_at, updated_at: updated_at }, diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb index b1cac3b6e46..120a07ff2b3 100644 --- a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb @@ -4,6 +4,7 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis let(:project) { create(:project, import_source: 'foo/bar') } let(:client) { double(:client) } let(:importer) { described_class.new(project, client) } + let(:due_on) { Time.new(2017, 2, 1, 12, 00) } let(:created_at) { Time.new(2017, 1, 1, 12, 00) } let(:updated_at) { Time.new(2017, 1, 1, 12, 15) } @@ -14,6 +15,20 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis title: '1.0', description: 'The first release', state: 'open', + due_on: due_on, + created_at: created_at, + updated_at: updated_at + ) + end + + let(:milestone2) do + double( + :milestone, + number: 1, + title: '1.0', + description: 'The first release', + state: 'open', + due_on: nil, created_at: created_at, updated_at: updated_at ) @@ -72,6 +87,7 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis describe '#build' do let(:milestone_hash) { importer.build(milestone) } + let(:milestone_hash2) { importer.build(milestone2) } it 'returns the attributes of the milestone as a Hash' do expect(milestone_hash).to be_an_instance_of(Hash) @@ -98,6 +114,14 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis expect(milestone_hash[:state]).to eq(:active) end + it 'includes the due date' do + expect(milestone_hash[:due_date]).to eq(due_on.to_date) + end + + it 'responds correctly to no due date value' do + expect(milestone_hash2[:due_date]).to be nil + end + it 'includes the created timestamp' do expect(milestone_hash[:created_at]).to eq(created_at) end diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb index 15e59718dce..6d614c6527a 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb @@ -11,6 +11,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi let(:source_commit) { project.repository.commit('feature') } let(:target_commit) { project.repository.commit('master') } let(:milestone) { create(:milestone, project: project) } + let(:state) { :closed } let(:pull_request) do alice = Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice') @@ -26,13 +27,13 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi source_repository_id: 400, target_repository_id: 200, source_repository_owner: 'alice', - state: :closed, + state: state, milestone_number: milestone.iid, author: alice, assignee: alice, created_at: created_at, updated_at: updated_at, - merged_at: merged_at + merged_at: state == :closed && merged_at ) end @@ -92,6 +93,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi source_branch: 'github/fork/alice/feature', target_branch: 'master', state: :merged, + state_id: 3, milestone_id: milestone.id, author_id: user.id, assignee_id: user.id, @@ -137,6 +139,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi source_branch: 'github/fork/alice/feature', target_branch: 'master', state: :merged, + state_id: 3, milestone_id: milestone.id, author_id: project.creator_id, assignee_id: user.id, @@ -183,6 +186,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi source_branch: 'master-42', target_branch: 'master', state: :merged, + state_id: 3, milestone_id: milestone.id, author_id: user.id, assignee_id: user.id, @@ -260,53 +264,63 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi end it 'does not create the source branch if merge request is merged' do - mr, exists = importer.create_merge_request - - importer.insert_git_data(mr, exists) + mr = insert_git_data expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy end - it 'creates the source branch if merge request is open' do - mr, exists = importer.create_merge_request - mr.state = 'opened' - mr.save + context 'when merge request is open' do + let(:state) { :opened } - importer.insert_git_data(mr, exists) + it 'creates the source branch' do + # Ensure the project creator is creating the branches because the + # merge request author may not have access to push to this + # repository. The project owner may also be a group. + allow(project.repository).to receive(:add_branch).with(project.creator, anything, anything).and_call_original - expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy - expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy - end + mr = insert_git_data - it 'ignores Git errors when creating a branch' do - mr, exists = importer.create_merge_request - mr.state = 'opened' - mr.save + expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy + expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + end - expect(project.repository).to receive(:add_branch).and_raise(Gitlab::Git::CommandError) - expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original + it 'is able to retry on pre-receive errors' do + expect(importer).to receive(:insert_or_replace_git_data).twice.and_call_original + expect(project.repository).to receive(:add_branch).and_raise('exception') - importer.insert_git_data(mr, exists) + expect { insert_git_data }.to raise_error('exception') - expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey - expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + expect(project.repository).to receive(:add_branch).with(project.creator, anything, anything).and_call_original + + mr = insert_git_data + + expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy + expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + expect(mr.merge_request_diffs).to be_one + end + + it 'ignores Git command errors when creating a branch' do + expect(project.repository).to receive(:add_branch).and_raise(Gitlab::Git::CommandError) + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original + + mr = insert_git_data + + expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey + expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + end end it 'creates the merge request diffs' do - mr, exists = importer.create_merge_request - - importer.insert_git_data(mr, exists) + mr = insert_git_data expect(mr.merge_request_diffs.exists?).to eq(true) end it 'creates the merge request diff commits' do - mr, exists = importer.create_merge_request - - importer.insert_git_data(mr, exists) + mr = insert_git_data - diff = mr.merge_request_diffs.take + diff = mr.merge_request_diffs.reload.first expect(diff.merge_request_diff_commits.exists?).to eq(true) end @@ -322,5 +336,11 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi expect(mr.merge_request_diffs.exists?).to eq(true) end end + + def insert_git_data + mr, exists = importer.create_merge_request + importer.insert_git_data(mr, exists) + mr + end end end diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb index 47233ea6ee2..705df1f4fe7 100644 --- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb @@ -179,6 +179,17 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do describe '#import_repository' do it 'imports the repository' do + repo = double(:repo, default_branch: 'develop') + + expect(client) + .to receive(:repository) + .with('foo/bar') + .and_return(repo) + + expect(project) + .to receive(:change_head) + .with('develop') + expect(project) .to receive(:ensure_repository) @@ -186,6 +197,11 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do .to receive(:fetch_as_mirror) .with(project.import_url, refmap: Gitlab::GithubImport.refmap, forced: true, remote_name: 'github') + service = double + expect(Projects::HousekeepingService) + .to receive(:new).with(project, :gc).and_return(service) + expect(service).to receive(:execute) + expect(importer.import_repository).to eq(true) end diff --git a/spec/lib/gitlab/github_import/parallel_importer_spec.rb b/spec/lib/gitlab/github_import/parallel_importer_spec.rb index f5df38c9aaf..ecab64a372a 100644 --- a/spec/lib/gitlab/github_import/parallel_importer_spec.rb +++ b/spec/lib/gitlab/github_import/parallel_importer_spec.rb @@ -25,18 +25,9 @@ describe Gitlab::GithubImport::ParallelImporter do end it 'sets the JID in Redis' do - expect(Gitlab::SidekiqStatus) - .to receive(:set) - .with("github-importer/#{project.id}", StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION) - .and_call_original + expect(Gitlab::Import::SetAsyncJid).to receive(:set_jid).with(project).and_call_original importer.execute end - - it 'updates the import JID of the project' do - importer.execute - - expect(project.import_state.reload.jid).to eq("github-importer/#{project.id}") - end end end diff --git a/spec/lib/gitlab/gl_repository/repo_type_spec.rb b/spec/lib/gitlab/gl_repository/repo_type_spec.rb new file mode 100644 index 00000000000..f06a2448ff7 --- /dev/null +++ b/spec/lib/gitlab/gl_repository/repo_type_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::GlRepository::RepoType do + set(:project) { create(:project) } + + shared_examples 'a repo type' do + describe "#identifier_for_subject" do + subject { described_class.identifier_for_subject(project) } + + it { is_expected.to eq(expected_identifier) } + end + + describe "#fetch_id" do + it "finds an id match in the identifier" do + expect(described_class.fetch_id(expected_identifier)).to eq(expected_id) + end + + it 'does not break on other identifiers' do + expect(described_class.fetch_id("wiki-noid")).to eq(nil) + end + end + + describe "#path_suffix" do + subject { described_class.path_suffix } + + it { is_expected.to eq(expected_suffix) } + end + + describe "#repository_for" do + it "finds the repository for the repo type" do + expect(described_class.repository_for(project)).to eq(expected_repository) + end + end + end + + describe Gitlab::GlRepository::PROJECT do + it_behaves_like 'a repo type' do + let(:expected_identifier) { "project-#{project.id}" } + let(:expected_id) { project.id.to_s } + let(:expected_suffix) { "" } + let(:expected_repository) { project.repository } + end + + it "knows its type" do + expect(described_class).not_to be_wiki + expect(described_class).to be_project + end + end + + describe Gitlab::GlRepository::WIKI do + it_behaves_like 'a repo type' do + let(:expected_identifier) { "wiki-#{project.id}" } + let(:expected_id) { project.id.to_s } + let(:expected_suffix) { ".wiki" } + let(:expected_repository) { project.wiki.repository } + end + + it "knows its type" do + expect(described_class).to be_wiki + expect(described_class).not_to be_project + end + end +end diff --git a/spec/lib/gitlab/gl_repository_spec.rb b/spec/lib/gitlab/gl_repository_spec.rb index 4e09020471b..d4b6c629659 100644 --- a/spec/lib/gitlab/gl_repository_spec.rb +++ b/spec/lib/gitlab/gl_repository_spec.rb @@ -5,11 +5,11 @@ describe ::Gitlab::GlRepository do set(:project) { create(:project, :repository) } it 'parses a project gl_repository' do - expect(described_class.parse("project-#{project.id}")).to eq([project, false]) + expect(described_class.parse("project-#{project.id}")).to eq([project, Gitlab::GlRepository::PROJECT]) end it 'parses a wiki gl_repository' do - expect(described_class.parse("wiki-#{project.id}")).to eq([project, true]) + expect(described_class.parse("wiki-#{project.id}")).to eq([project, Gitlab::GlRepository::WIKI]) end it 'throws an argument error on an invalid gl_repository' do diff --git a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb new file mode 100644 index 00000000000..aec9c4baf0a --- /dev/null +++ b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Also see spec/graphql/features/authorization_spec.rb for +# integration tests of AuthorizeFieldService +describe Gitlab::Graphql::Authorize::AuthorizeFieldService do + def type(type_authorizations = []) + Class.new(Types::BaseObject) do + graphql_name "TestType" + + authorize type_authorizations + end + end + + def type_with_field(field_type, field_authorizations = [], resolved_value = "Resolved value") + Class.new(Types::BaseObject) do + graphql_name "TestTypeWithField" + field :test_field, field_type, null: true, authorize: field_authorizations, resolve: -> (_, _, _) { resolved_value} + end + end + + let(:current_user) { double(:current_user) } + subject(:service) { described_class.new(field) } + + describe "#authorized_resolve" do + let(:presented_object) { double("presented object") } + let(:presented_type) { double("parent type", object: presented_object) } + subject(:resolved) { service.authorized_resolve.call(presented_type, {}, { current_user: current_user }) } + + context "scalar types" do + shared_examples "checking permissions on the presented object" do + it "checks the abilities on the object being presented and returns the value" do + expected_permissions.each do |permission| + spy_ability_check_for(permission, presented_object, passed: true) + end + + expect(resolved).to eq("Resolved value") + end + + it "returns nil if the value wasn't authorized" do + allow(Ability).to receive(:allowed?).and_return false + + expect(resolved).to be_nil + end + end + + context "when the field is a built-in scalar type" do + let(:field) { type_with_field(GraphQL::STRING_TYPE, :read_field).fields["testField"].to_graphql } + let(:expected_permissions) { [:read_field] } + + it_behaves_like "checking permissions on the presented object" + end + + context "when the field is a list of scalar types" do + let(:field) { type_with_field([GraphQL::STRING_TYPE], :read_field).fields["testField"].to_graphql } + let(:expected_permissions) { [:read_field] } + + it_behaves_like "checking permissions on the presented object" + end + + context "when the field is sub-classed scalar type" do + let(:field) { type_with_field(Types::TimeType, :read_field).fields["testField"].to_graphql } + let(:expected_permissions) { [:read_field] } + + it_behaves_like "checking permissions on the presented object" + end + + context "when the field is a list of sub-classed scalar types" do + let(:field) { type_with_field([Types::TimeType], :read_field).fields["testField"].to_graphql } + let(:expected_permissions) { [:read_field] } + + it_behaves_like "checking permissions on the presented object" + end + end + + context "when the field is a specific type" do + let(:custom_type) { type(:read_type) } + let(:object_in_field) { double("presented in field") } + let(:field) { type_with_field(custom_type, :read_field, object_in_field).fields["testField"].to_graphql } + + it "checks both field & type permissions" do + spy_ability_check_for(:read_field, object_in_field, passed: true) + spy_ability_check_for(:read_type, object_in_field, passed: true) + + expect(resolved).to eq(object_in_field) + end + + it "returns nil if viewing was not allowed" do + spy_ability_check_for(:read_field, object_in_field, passed: false) + spy_ability_check_for(:read_type, object_in_field, passed: true) + + expect(resolved).to be_nil + end + + context "when the field is a list" do + let(:object_1) { double("presented in field 1") } + let(:object_2) { double("presented in field 2") } + let(:presented_types) { [double(object: object_1), double(object: object_2)] } + let(:field) { type_with_field([custom_type], :read_field, presented_types).fields["testField"].to_graphql } + + it "checks all permissions" do + allow(Ability).to receive(:allowed?) { true } + + spy_ability_check_for(:read_field, object_1, passed: true) + spy_ability_check_for(:read_type, object_1, passed: true) + spy_ability_check_for(:read_field, object_2, passed: true) + spy_ability_check_for(:read_type, object_2, passed: true) + + expect(resolved).to eq(presented_types) + end + + it "filters out objects that the user cannot see" do + allow(Ability).to receive(:allowed?) { true } + + spy_ability_check_for(:read_type, object_1, passed: false) + + expect(resolved.map(&:object)).to contain_exactly(object_2) + end + end + end + end + + private + + def spy_ability_check_for(ability, object, passed: true) + expect(Ability) + .to receive(:allowed?) + .with(current_user, ability, object) + .and_return(passed) + end +end diff --git a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb index 95bf7685ade..13cf52fd795 100644 --- a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb +++ b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb @@ -100,4 +100,22 @@ describe Gitlab::Graphql::Authorize::AuthorizeResource do expect { fake_class.new.find_object }.to raise_error(/Implement #find_object in #{fake_class.name}/) end end + + describe '#authorize' do + it 'adds permissions from subclasses to those of superclasses when used on classes' do + base_class = Class.new do + include Gitlab::Graphql::Authorize::AuthorizeResource + + authorize :base_authorization + end + + sub_class = Class.new(base_class) do + authorize :sub_authorization + end + + expect(base_class.required_permissions).to contain_exactly(:base_authorization) + expect(sub_class.required_permissions) + .to contain_exactly(:base_authorization, :sub_authorization) + end + end end diff --git a/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb b/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb deleted file mode 100644 index cf3a8bcc8b4..00000000000 --- a/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Gitlab::Graphql::Authorize::Instrumentation do - describe '#build_checker' do - let(:current_user) { double(:current_user) } - let(:abilities) { [double(:first_ability), double(:last_ability)] } - - let(:checker) do - described_class.new.__send__(:build_checker, current_user, abilities) - end - - it 'returns a checker which checks for a single object' do - object = double(:object) - - abilities.each do |ability| - spy_ability_check_for(ability, object, passed: true) - end - - expect(checker.call(object)).to eq(object) - end - - it 'returns a checker which checks for all objects' do - objects = [double(:first), double(:last)] - - abilities.each do |ability| - objects.each do |object| - spy_ability_check_for(ability, object, passed: true) - end - end - - expect(checker.call(objects)).to eq(objects) - end - - context 'when some objects would not pass the check' do - it 'returns nil when it is single object' do - disallowed = double(:object) - - spy_ability_check_for(abilities.first, disallowed, passed: false) - - expect(checker.call(disallowed)).to be_nil - end - - it 'returns only objects which passed when there are more than one' do - allowed = double(:allowed) - disallowed = double(:disallowed) - - spy_ability_check_for(abilities.first, disallowed, passed: false) - - abilities.each do |ability| - spy_ability_check_for(ability, allowed, passed: true) - end - - expect(checker.call([disallowed, allowed])) - .to contain_exactly(allowed) - end - end - - def spy_ability_check_for(ability, object, passed: true) - expect(Ability) - .to receive(:allowed?) - .with(current_user, ability, object) - .and_return(passed) - end - end -end diff --git a/spec/lib/gitlab/graphql/authorize_spec.rb b/spec/lib/gitlab/graphql/authorize_spec.rb deleted file mode 100644 index 9c17a3b0e4b..00000000000 --- a/spec/lib/gitlab/graphql/authorize_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Graphql::Authorize do - describe '#authorize' do - it 'adds permissions from subclasses to those of superclasses when used on classes' do - base_class = Class.new do - extend Gitlab::Graphql::Authorize - - authorize :base_authorization - end - sub_class = Class.new(base_class) do - authorize :sub_authorization - end - - expect(base_class.required_permissions).to contain_exactly(:base_authorization) - expect(sub_class.required_permissions) - .to contain_exactly(:base_authorization, :sub_authorization) - end - end -end diff --git a/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb index 9bcc1e78a78..fefa2881b18 100644 --- a/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb +++ b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb @@ -85,6 +85,11 @@ describe Gitlab::Graphql::Connections::KeysetConnection do expect(subject.paged_nodes.size).to eq(3) end + it 'is a loaded memoized array' do + expect(subject.paged_nodes).to be_an(Array) + expect(subject.paged_nodes.object_id).to eq(subject.paged_nodes.object_id) + end + context 'when `first` is passed' do let(:arguments) { { first: 2 } } diff --git a/spec/lib/gitlab/graphql/generic_tracing_spec.rb b/spec/lib/gitlab/graphql/generic_tracing_spec.rb new file mode 100644 index 00000000000..ae92dcc40af --- /dev/null +++ b/spec/lib/gitlab/graphql/generic_tracing_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Graphql::GenericTracing do + let(:graphql_duration_seconds_histogram) { double('Gitlab::Metrics::NullMetric') } + + it 'updates graphql histogram with expected labels' do + query = 'query { users { id } }' + tracer = described_class.new + + allow(tracer) + .to receive(:graphql_duration_seconds) + .and_return(graphql_duration_seconds_histogram) + + expect_metric('graphql.lex', 'lex') + expect_metric('graphql.parse', 'parse') + expect_metric('graphql.validate', 'validate') + expect_metric('graphql.analyze', 'analyze_multiplex') + expect_metric('graphql.execute', 'execute_query_lazy') + expect_metric('graphql.execute', 'execute_multiplex') + + GitlabSchema.execute(query, context: { tracers: [tracer] }) + end + + context "when labkit tracing is enabled" do + before do + expect(Labkit::Tracing).to receive(:enabled?).and_return(true) + end + + it 'yields with labkit tracing' do + expected_tags = { + 'component' => 'web', + 'span.kind' => 'server', + 'platform_key' => 'pkey', + 'key' => 'key' + } + + expect(Labkit::Tracing) + .to receive(:with_tracing) + .with(operation_name: "pkey.key", tags: expected_tags) + .and_yield + + expect { |b| described_class.new.platform_trace('pkey', 'key', nil, &b) }.to yield_control + end + end + + context "when labkit tracing is disabled" do + before do + expect(Labkit::Tracing).to receive(:enabled?).and_return(false) + end + + it 'yields without measurement' do + expect(Labkit::Tracing).not_to receive(:with_tracing) + + expect { |b| described_class.new.platform_trace('pkey', 'key', nil, &b) }.to yield_control + end + end + + private + + def expect_metric(platform_key, key) + expect(graphql_duration_seconds_histogram) + .to receive(:observe) + .with({ platform_key: platform_key, key: key }, be > 0.0) + end +end diff --git a/spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb new file mode 100644 index 00000000000..ec2fcad31e5 --- /dev/null +++ b/spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader do + describe '#find' do + it 'only queries once for project statistics' do + stats = create_list(:project_statistics, 2) + project1 = stats.first.project + project2 = stats.last.project + + expect do + described_class.new(project1.id).find + described_class.new(project2.id).find + end.not_to exceed_query_limit(1) + end + end +end diff --git a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb new file mode 100644 index 00000000000..66033736e01 --- /dev/null +++ b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer do + subject { described_class.new } + + describe '#analyze?' do + context 'feature flag disabled' do + before do + stub_feature_flags(graphql_logging: false) + end + + it 'disables the analyzer' do + expect(subject.analyze?(anything)).to be_falsey + end + end + + context 'feature flag enabled by default' do + it 'enables the analyzer' do + expect(subject.analyze?(anything)).to be_truthy + end + end + end +end diff --git a/spec/lib/gitlab/graphql/representation/tree_entry_spec.rb b/spec/lib/gitlab/graphql/representation/tree_entry_spec.rb new file mode 100644 index 00000000000..d45e690160c --- /dev/null +++ b/spec/lib/gitlab/graphql/representation/tree_entry_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Graphql::Representation::TreeEntry do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + + describe '.decorate' do + it 'returns NilClass when given nil' do + expect(described_class.decorate(nil, repository)).to be_nil + end + + it 'returns array of TreeEntry' do + entries = described_class.decorate(repository.tree.blobs, repository) + + expect(entries.first).to be_a(described_class) + end + end +end diff --git a/spec/lib/gitlab/graphql_logger_spec.rb b/spec/lib/gitlab/graphql_logger_spec.rb new file mode 100644 index 00000000000..4977f98b83e --- /dev/null +++ b/spec/lib/gitlab/graphql_logger_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::GraphqlLogger do + subject { described_class.new('/dev/null') } + + let(:now) { Time.now } + + it 'builds a logger once' do + expect(::Logger).to receive(:new).and_call_original + + subject.info('hello world') + subject.error('hello again') + end + + context 'logging a GraphQL query' do + let(:query) { File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql')) } + + it 'logs a query from JSON' do + analyzer_memo = { + query_string: query, + variables: {}, + complexity: 181, + depth: 0, + duration: 7 + } + + output = subject.format_message('INFO', now, 'test', analyzer_memo) + + data = JSON.parse(output) + expect(data['severity']).to eq('INFO') + expect(data['time']).to eq(now.utc.iso8601(3)) + expect(data['complexity']).to eq(181) + expect(data['variables']).to eq({}) + expect(data['depth']).to eq(0) + expect(data['duration']).to eq(7) + end + end +end diff --git a/spec/lib/gitlab/group_search_results_spec.rb b/spec/lib/gitlab/group_search_results_spec.rb new file mode 100644 index 00000000000..2734fcef0a0 --- /dev/null +++ b/spec/lib/gitlab/group_search_results_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Gitlab::GroupSearchResults do + let(:user) { create(:user) } + + describe 'user search' do + let(:group) { create(:group) } + + it 'returns the users belonging to the group matching the search query' do + user1 = create(:user, username: 'gob_bluth') + create(:group_member, :developer, user: user1, group: group) + + user2 = create(:user, username: 'michael_bluth') + create(:group_member, :developer, user: user2, group: group) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'returns the user belonging to the subgroup matching the search query', :nested_groups do + user1 = create(:user, username: 'gob_bluth') + subgroup = create(:group, parent: group) + create(:group_member, :developer, user: user1, group: subgroup) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'returns the user belonging to the parent group matching the search query', :nested_groups do + user1 = create(:user, username: 'gob_bluth') + parent_group = create(:group, children: [group]) + create(:group_member, :developer, user: user1, group: parent_group) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'does not return the user belonging to the private subgroup', :nested_groups do + user1 = create(:user, username: 'gob_bluth') + subgroup = create(:group, :private, parent: group) + create(:group_member, :developer, user: user1, group: subgroup) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [] + end + + it 'does not return the user belonging to an unrelated group' do + user = create(:user, username: 'gob_bluth') + unrelated_group = create(:group) + create(:group_member, :developer, user: user, group: unrelated_group) + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [] + end + end +end diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb index 3942f168ceb..8e253b51597 100644 --- a/spec/lib/gitlab/hashed_storage/migrator_spec.rb +++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb @@ -1,21 +1,31 @@ +# frozen_string_literal: true + require 'spec_helper' -describe Gitlab::HashedStorage::Migrator do - describe '#bulk_schedule' do - it 'schedules job to StorageMigratorWorker' do +describe Gitlab::HashedStorage::Migrator, :sidekiq, :redis do + describe '#bulk_schedule_migration' do + it 'schedules job to HashedStorage::MigratorWorker' do + Sidekiq::Testing.fake! do + expect { subject.bulk_schedule_migration(start: 1, finish: 5) }.to change(HashedStorage::MigratorWorker.jobs, :size).by(1) + end + end + end + + describe '#bulk_schedule_rollback' do + it 'schedules job to HashedStorage::RollbackerWorker' do Sidekiq::Testing.fake! do - expect { subject.bulk_schedule(start: 1, finish: 5) }.to change(HashedStorage::MigratorWorker.jobs, :size).by(1) + expect { subject.bulk_schedule_rollback(start: 1, finish: 5) }.to change(HashedStorage::RollbackerWorker.jobs, :size).by(1) end end end describe '#bulk_migrate' do - let(:projects) { create_list(:project, 2, :legacy_storage) } + let(:projects) { create_list(:project, 2, :legacy_storage, :empty_repo) } let(:ids) { projects.map(&:id) } - it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do + it 'enqueue jobs to HashedStorage::ProjectMigrateWorker' do Sidekiq::Testing.fake! do - expect { subject.bulk_migrate(start: ids.min, finish: ids.max) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(2) + expect { subject.bulk_migrate(start: ids.min, finish: ids.max) }.to change(HashedStorage::ProjectMigrateWorker.jobs, :size).by(2) end end @@ -32,13 +42,53 @@ describe Gitlab::HashedStorage::Migrator do subject.bulk_migrate(start: ids.min, finish: ids.max) end - it 'has migrated projects set as writable' do + it 'has all projects migrated and set as writable' do perform_enqueued_jobs do subject.bulk_migrate(start: ids.min, finish: ids.max) end projects.each do |project| - expect(project.reload.repository_read_only?).to be_falsey + project.reload + + expect(project.hashed_storage?(:repository)).to be_truthy + expect(project.repository_read_only?).to be_falsey + end + end + end + + describe '#bulk_rollback' do + let(:projects) { create_list(:project, 2, :empty_repo) } + let(:ids) { projects.map(&:id) } + + it 'enqueue jobs to HashedStorage::ProjectRollbackWorker' do + Sidekiq::Testing.fake! do + expect { subject.bulk_rollback(start: ids.min, finish: ids.max) }.to change(HashedStorage::ProjectRollbackWorker.jobs, :size).by(2) + end + end + + it 'rescues and log exceptions' do + allow_any_instance_of(Project).to receive(:rollback_to_legacy_storage!).and_raise(StandardError) + expect { subject.bulk_rollback(start: ids.min, finish: ids.max) }.not_to raise_error + end + + it 'delegates each project in specified range to #rollback' do + projects.each do |project| + expect(subject).to receive(:rollback).with(project) + end + + subject.bulk_rollback(start: ids.min, finish: ids.max) + end + + it 'has all projects rolledback and set as writable' do + perform_enqueued_jobs do + subject.bulk_rollback(start: ids.min, finish: ids.max) + end + + projects.each do |project| + project.reload + + expect(project.legacy_storage?).to be_truthy + expect(project.repository_read_only?).to be_falsey end end end @@ -48,7 +98,7 @@ describe Gitlab::HashedStorage::Migrator do it 'enqueues project migration job' do Sidekiq::Testing.fake! do - expect { subject.migrate(project) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(1) + expect { subject.migrate(project) }.to change(HashedStorage::ProjectMigrateWorker.jobs, :size).by(1) end end @@ -79,7 +129,7 @@ describe Gitlab::HashedStorage::Migrator do it 'doesnt enqueue any migration job' do Sidekiq::Testing.fake! do - expect { subject.migrate(project) }.not_to change(ProjectMigrateHashedStorageWorker.jobs, :size) + expect { subject.migrate(project) }.not_to change(HashedStorage::ProjectMigrateWorker.jobs, :size) end end @@ -88,4 +138,98 @@ describe Gitlab::HashedStorage::Migrator do end end end + + describe '#rollback' do + let(:project) { create(:project, :empty_repo) } + + it 'enqueues project rollback job' do + Sidekiq::Testing.fake! do + expect { subject.rollback(project) }.to change(HashedStorage::ProjectRollbackWorker.jobs, :size).by(1) + end + end + + it 'rescues and log exceptions' do + allow(project).to receive(:rollback_to_hashed_storage!).and_raise(StandardError) + + expect { subject.rollback(project) }.not_to raise_error + end + + it 'rolls-back project storage' do + perform_enqueued_jobs do + subject.rollback(project) + end + + expect(project.reload.legacy_storage?).to be_truthy + end + + it 'has rolled-back project set as writable' do + perform_enqueued_jobs do + subject.rollback(project) + end + + expect(project.reload.repository_read_only?).to be_falsey + end + + context 'when project is already on legacy storage' do + let(:project) { create(:project, :legacy_storage, :empty_repo) } + + it 'doesnt enqueue any rollback job' do + Sidekiq::Testing.fake! do + expect { subject.rollback(project) }.not_to change(HashedStorage::ProjectRollbackWorker.jobs, :size) + end + end + + it 'returns false' do + expect(subject.rollback(project)).to be_falsey + end + end + end + + describe 'migration_pending?' do + set(:project) { create(:project, :empty_repo) } + + it 'returns true when there are MigratorWorker jobs scheduled' do + Sidekiq::Testing.disable! do + ::HashedStorage::MigratorWorker.perform_async(1, 5) + + expect(subject.migration_pending?).to be_truthy + end + end + + it 'returns true when there are ProjectMigrateWorker jobs scheduled' do + Sidekiq::Testing.disable! do + ::HashedStorage::ProjectMigrateWorker.perform_async(1) + + expect(subject.migration_pending?).to be_truthy + end + end + + it 'returns false when queues are empty' do + expect(subject.migration_pending?).to be_falsey + end + end + + describe 'rollback_pending?' do + set(:project) { create(:project, :empty_repo) } + + it 'returns true when there are RollbackerWorker jobs scheduled' do + Sidekiq::Testing.disable! do + ::HashedStorage::RollbackerWorker.perform_async(1, 5) + + expect(subject.rollback_pending?).to be_truthy + end + end + + it 'returns true when there are jobs scheduled' do + Sidekiq::Testing.disable! do + ::HashedStorage::ProjectRollbackWorker.perform_async(1) + + expect(subject.rollback_pending?).to be_truthy + end + end + + it 'returns false when queues are empty' do + expect(subject.rollback_pending?).to be_falsey + end + end end diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb index 26529c4759d..569d5dcc757 100644 --- a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb @@ -97,13 +97,13 @@ describe Gitlab::HookData::IssuableBuilder do end context 'merge_request is assigned' do - let(:merge_request) { create(:merge_request, assignee: user) } + let(:merge_request) { create(:merge_request, assignees: [user]) } let(:data) { described_class.new(merge_request).build(user: user) } it 'returns correct hook data' do expect(data[:object_attributes]['assignee_id']).to eq(user.id) - expect(data[:assignee]).to eq(user.hook_attrs) - expect(data).not_to have_key(:assignees) + expect(data[:assignees].first).to eq(user.hook_attrs) + expect(data).not_to have_key(:assignee) end end end diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index 9ce697adbba..39f80f92fa6 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -10,6 +10,7 @@ describe Gitlab::HookData::MergeRequestBuilder do it 'includes safe attribute' do %w[ assignee_id + assignee_ids author_id created_at description diff --git a/spec/lib/gitlab/http_connection_adapter_spec.rb b/spec/lib/gitlab/http_connection_adapter_spec.rb new file mode 100644 index 00000000000..930d1f62272 --- /dev/null +++ b/spec/lib/gitlab/http_connection_adapter_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::HTTPConnectionAdapter do + describe '#connection' do + context 'when local requests are not allowed' do + it 'sets up the connection' do + uri = URI('https://example.org') + + connection = described_class.new(uri).connection + + expect(connection).to be_a(Net::HTTP) + expect(connection.address).to eq('93.184.216.34') + expect(connection.hostname_override).to eq('example.org') + expect(connection.addr_port).to eq('example.org') + expect(connection.port).to eq(443) + end + + it 'raises error when it is a request to local address' do + uri = URI('http://172.16.0.0/12') + + expect { described_class.new(uri).connection } + .to raise_error(Gitlab::HTTP::BlockedUrlError, + "URL 'http://172.16.0.0/12' is blocked: Requests to the local network are not allowed") + end + + it 'raises error when it is a request to localhost address' do + uri = URI('http://127.0.0.1') + + expect { described_class.new(uri).connection } + .to raise_error(Gitlab::HTTP::BlockedUrlError, + "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed") + end + + context 'when port different from URL scheme is used' do + it 'sets up the addr_port accordingly' do + uri = URI('https://example.org:8080') + + connection = described_class.new(uri).connection + + expect(connection.address).to eq('93.184.216.34') + expect(connection.hostname_override).to eq('example.org') + expect(connection.addr_port).to eq('example.org:8080') + expect(connection.port).to eq(8080) + end + end + end + + context 'when DNS rebinding protection is disabled' do + it 'sets up the connection' do + stub_application_setting(dns_rebinding_protection_enabled: false) + + uri = URI('https://example.org') + + connection = described_class.new(uri).connection + + expect(connection).to be_a(Net::HTTP) + expect(connection.address).to eq('example.org') + expect(connection.hostname_override).to eq(nil) + expect(connection.addr_port).to eq('example.org') + expect(connection.port).to eq(443) + end + end + + context 'when http(s) environment variable is set' do + it 'sets up the connection' do + stub_env('https_proxy' => 'https://my.proxy') + + uri = URI('https://example.org') + + connection = described_class.new(uri).connection + + expect(connection).to be_a(Net::HTTP) + expect(connection.address).to eq('example.org') + expect(connection.hostname_override).to eq(nil) + expect(connection.addr_port).to eq('example.org') + expect(connection.port).to eq(443) + end + end + + context 'when local requests are allowed' do + it 'sets up the connection' do + uri = URI('https://example.org') + + connection = described_class.new(uri, allow_local_requests: true).connection + + expect(connection).to be_a(Net::HTTP) + expect(connection.address).to eq('93.184.216.34') + expect(connection.hostname_override).to eq('example.org') + expect(connection.addr_port).to eq('example.org') + expect(connection.port).to eq(443) + end + + it 'sets up the connection when it is a local network' do + uri = URI('http://172.16.0.0/12') + + connection = described_class.new(uri, allow_local_requests: true).connection + + expect(connection).to be_a(Net::HTTP) + expect(connection.address).to eq('172.16.0.0') + expect(connection.hostname_override).to be(nil) + expect(connection.addr_port).to eq('172.16.0.0') + expect(connection.port).to eq(80) + end + + it 'sets up the connection when it is localhost' do + uri = URI('http://127.0.0.1') + + connection = described_class.new(uri, allow_local_requests: true).connection + + expect(connection).to be_a(Net::HTTP) + expect(connection.address).to eq('127.0.0.1') + expect(connection.hostname_override).to be(nil) + expect(connection.addr_port).to eq('127.0.0.1') + expect(connection.port).to eq(80) + end + end + end +end diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb index 6c37c157f5d..158f77cab2c 100644 --- a/spec/lib/gitlab/http_spec.rb +++ b/spec/lib/gitlab/http_spec.rb @@ -1,6 +1,28 @@ require 'spec_helper' describe Gitlab::HTTP do + include StubRequests + + context 'when allow_local_requests' do + it 'sends the request to the correct URI' do + stub_full_request('https://example.org:8080', ip_address: '8.8.8.8').to_return(status: 200) + + described_class.get('https://example.org:8080', allow_local_requests: false) + + expect(WebMock).to have_requested(:get, 'https://8.8.8.8:8080').once + end + end + + context 'when not allow_local_requests' do + it 'sends the request to the correct URI' do + stub_full_request('https://example.org:8080') + + described_class.get('https://example.org:8080', allow_local_requests: true) + + expect(WebMock).to have_requested(:get, 'https://8.8.8.9:8080').once + end + end + describe 'allow_local_requests_from_hooks_and_services is' do before do WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success') @@ -21,6 +43,8 @@ describe Gitlab::HTTP do context 'if allow_local_requests set to true' do it 'override the global value and allow requests to localhost or private network' do + stub_full_request('http://localhost:3003') + expect { described_class.get('http://localhost:3003', allow_local_requests: true) }.not_to raise_error end end @@ -32,6 +56,8 @@ describe Gitlab::HTTP do end it 'allow requests to localhost' do + stub_full_request('http://localhost:3003') + expect { described_class.get('http://localhost:3003') }.not_to raise_error end @@ -49,7 +75,7 @@ describe Gitlab::HTTP do describe 'handle redirect loops' do before do - WebMock.stub_request(:any, "http://example.org").to_raise(HTTParty::RedirectionTooDeep.new("Redirection Too Deep")) + stub_full_request("http://example.org", method: :any).to_raise(HTTParty::RedirectionTooDeep.new("Redirection Too Deep")) end it 'handles GET requests' do diff --git a/spec/lib/gitlab/import/merge_request_helpers_spec.rb b/spec/lib/gitlab/import/merge_request_helpers_spec.rb new file mode 100644 index 00000000000..cc0f2baf905 --- /dev/null +++ b/spec/lib/gitlab/import/merge_request_helpers_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Import::MergeRequestHelpers, type: :helper do + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + + describe '.create_merge_request_without_hooks' do + let(:iid) { 42 } + + let(:attributes) do + { + iid: iid, + title: 'My Pull Request', + description: 'This is my pull request', + source_project_id: project.id, + target_project_id: project.id, + source_branch: 'master-42', + target_branch: 'master', + state: :merged, + author_id: user.id, + assignee_id: user.id + } + end + + subject { helper.create_merge_request_without_hooks(project, attributes, iid) } + + context 'when merge request does not exist' do + it 'returns a new object' do + expect(subject.first).not_to be_nil + expect(subject.second).to eq(false) + end + + it 'does load all existing objects' do + 5.times do |iid| + MergeRequest.create!( + attributes.merge(iid: iid, source_branch: iid.to_s)) + end + + # does ensure that we only load object twice + # 1. by #insert_and_return_id + # 2. by project.merge_requests.find + expect_any_instance_of(MergeRequest).to receive(:attributes) + .twice.times.and_call_original + + expect(subject.first).not_to be_nil + expect(subject.second).to eq(false) + end + end + + context 'when merge request does exist' do + before do + MergeRequest.create!(attributes) + end + + it 'returns an existing object' do + expect(subject.first).not_to be_nil + expect(subject.second).to eq(true) + end + end + + context 'when project is deleted' do + before do + project.delete + end + + it 'returns an existing object' do + expect(subject.first).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/import/set_async_jid_spec.rb b/spec/lib/gitlab/import/set_async_jid_spec.rb new file mode 100644 index 00000000000..51397280138 --- /dev/null +++ b/spec/lib/gitlab/import/set_async_jid_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::Import::SetAsyncJid do + describe '.set_jid', :clean_gitlab_redis_shared_state do + let(:project) { create(:project, :import_scheduled) } + + it 'sets the JID in Redis' do + expect(Gitlab::SidekiqStatus) + .to receive(:set) + .with("async-import/#{project.id}", StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION) + .and_call_original + + described_class.set_jid(project) + end + + it 'updates the import JID of the project' do + described_class.set_jid(project) + + expect(project.import_state.reload.jid).to eq("async-import/#{project.id}") + end + end +end diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb index ec17ad8541f..21a227335cd 100644 --- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb +++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do + include StubRequests + let(:example_url) { 'http://www.example.com' } let(:strategy) { subject.new(url: example_url, http_method: 'post') } let!(:project) { create(:project, :with_export) } @@ -32,5 +34,17 @@ describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do strategy.execute(user, project) end + + context 'when upload fails' do + it 'stores the export error' do + stub_full_request(example_url, method: :post).to_return(status: [404, 'Page not found']) + + strategy.execute(user, project) + + errors = project.import_export_shared.errors + expect(errors).not_to be_empty + expect(errors.first).to eq "Error uploading the project. Code 404: Page not found" + 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 f2eccec4635..2242543daad 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -99,7 +99,10 @@ merge_requests: - timelogs - head_pipeline - latest_merge_request_diff -- merge_request_pipelines +- pipelines_for_merge_request +- merge_request_assignees +- suggestions +- assignees merge_request_diff: - merge_request - merge_request_diff_commits @@ -127,7 +130,7 @@ ci_pipelines: - scheduled_actions - artifacts - pipeline_schedule -- merge_requests +- merge_requests_as_head_pipeline - merge_request - deployments - environments @@ -220,6 +223,7 @@ project: - packagist_service - pivotaltracker_service - prometheus_service +- hipchat_service - flowdock_service - assembla_service - asana_service @@ -233,6 +237,7 @@ project: - pushover_service - jira_service - redmine_service +- youtrack_service - custom_issue_tracker_service - bugzilla_service - gitlab_issue_tracker_service @@ -317,6 +322,7 @@ project: - pool_repository - kubernetes_namespaces - error_tracking_setting +- metrics_setting award_emoji: - awardable - user @@ -333,6 +339,9 @@ push_event_payload: issue_assignees: - issue - assignee +merge_request_assignees: +- merge_request +- assignee lfs_file_locks: - user project_badges: @@ -350,3 +359,7 @@ resource_label_events: - label error_tracking_setting: - project +suggestions: +- note +metrics_setting: +- project diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb index 536cc359d39..99669285d5b 100644 --- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb +++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb @@ -18,7 +18,11 @@ describe Gitlab::ImportExport::AttributeCleaner do 'notid' => 99, 'import_source' => 'whatever', 'import_type' => 'whatever', - 'non_existent_attr' => 'whatever' + 'non_existent_attr' => 'whatever', + 'some_html' => '<p>dodgy html</p>', + 'legit_html' => '<p>legit html</p>', + '_html' => '<p>perfectly ordinary html</p>', + 'cached_markdown_version' => 12345 } end diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb index 87ab81d8169..ddfbb020a55 100644 --- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb +++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb @@ -29,7 +29,7 @@ describe 'Import/Export attribute configuration' do it 'has no new columns' do relation_names.each do |relation_name| relation_class = relation_class_for_name(relation_name) - relation_attributes = relation_class.new.attributes.keys + relation_attributes = relation_class.new.attributes.keys - relation_class.encrypted_attributes.keys.map(&:to_s) current_attributes = parsed_attributes(relation_name, relation_attributes) safe_attributes = safe_model_attributes[relation_class.to_s].dup || [] diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index 67e4c289906..b95b5dfe791 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -12,7 +12,6 @@ describe Gitlab::ImportExport::MembersMapper do "access_level" => 40, "source_id" => 14, "source_type" => "Project", - "user_id" => 19, "notification_level" => 3, "created_at" => "2016-03-11T10:21:44.822Z", "updated_at" => "2016-03-11T10:21:44.822Z", @@ -25,7 +24,8 @@ describe Gitlab::ImportExport::MembersMapper do "id" => exported_user_id, "email" => user2.email, "username" => 'test' - } + }, + "user_id" => 19 }, { "id" => 3, @@ -73,6 +73,22 @@ describe Gitlab::ImportExport::MembersMapper do expect(user2.authorized_project?(project)).to be true end + it 'maps an owner as a maintainer' do + exported_members.first['access_level'] = ProjectMember::OWNER + + expect(members_mapper.map[exported_user_id]).to eq(user2.id) + expect(ProjectMember.find_by_user_id(user2.id).access_level).to eq(ProjectMember::MAINTAINER) + end + + it 'removes old user_id from member_hash to avoid conflict with user key' do + expect(ProjectMember).to receive(:create) + .twice + .with(hash_excluding('user_id')) + .and_call_original + + members_mapper.map + end + context 'user is not an admin' do let(:user) { create(:user) } diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb index 68eaa70e6b6..4b234411a44 100644 --- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -41,4 +41,20 @@ describe Gitlab::ImportExport::MergeRequestParser do expect(parsed_merge_request).to eq(merge_request) end + + context 'when the merge request has diffs' do + let(:merge_request) do + build(:merge_request, source_project: forked_project, target_project: project) + end + + context 'when the diff is invalid' do + let(:merge_request_diff) { build(:merge_request_diff, merge_request: merge_request, base_commit_sha: 'foobar') } + + it 'sets the diff to nil' do + expect(merge_request_diff).to be_invalid + expect(merge_request_diff.merge_request).to eq merge_request + expect(parsed_merge_request.merge_request_diff).to be_nil + end + end + end end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 1327f414498..fb7bddb386c 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -158,6 +158,8 @@ { "id": 351, "note": "Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi.", + "note_html": "<p>something else entirely</p>", + "cached_markdown_version": 917504, "noteable_type": "Issue", "author_id": 26, "created_at": "2016-06-14T15:02:47.770Z", @@ -2363,6 +2365,8 @@ { "id": 671, "note": "Sit voluptatibus eveniet architecto quidem.", + "note_html": "<p>something else entirely</p>", + "cached_markdown_version": 917504, "noteable_type": "MergeRequest", "author_id": 26, "created_at": "2016-06-14T15:02:56.632Z", @@ -6630,6 +6634,26 @@ "deploy_keys": [], "services": [ { + "id": 101, + "title": "YouTrack", + "project_id": 5, + "created_at": "2016-06-14T15:01:51.327Z", + "updated_at": "2016-06-14T15:01:51.327Z", + "active": false, + "properties": {}, + "template": false, + "push_events": true, + "issues_events": true, + "merge_requests_events": true, + "tag_push_events": true, + "note_events": true, + "job_events": true, + "type": "YoutrackService", + "category": "issue_tracker", + "default": false, + "wiki_page_events": true + }, + { "id": 100, "title": "JetBrains TeamCity CI", "project_id": 5, @@ -6775,6 +6799,28 @@ "wiki_page_events": true }, { + "id": 93, + "title": "HipChat", + "project_id": 5, + "created_at": "2016-06-14T15:01:51.219Z", + "updated_at": "2016-06-14T15:01:51.219Z", + "active": false, + "properties": { + "notify_only_broken_pipelines": true + }, + "template": false, + "push_events": true, + "issues_events": true, + "merge_requests_events": true, + "tag_push_events": true, + "note_events": true, + "pipeline_events": true, + "type": "HipchatService", + "category": "common", + "default": false, + "wiki_page_events": true + }, + { "id": 91, "title": "Flowdock", "project_id": 5, diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 6084dc96410..ca46006ea58 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -58,6 +58,26 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(Milestone.find_by_description('test milestone').issues.count).to eq(2) end + context 'when importing a project with cached_markdown_version and note_html' do + context 'for an Issue' do + it 'does not import note_html' do + note_content = 'Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi' + issue_note = Issue.find_by(description: 'Aliquam enim illo et possimus.').notes.select { |n| n.note.match(/#{note_content}/)}.first + + expect(issue_note.note_html).to match(/#{note_content}/) + end + end + + context 'for a Merge Request' do + it 'does not import note_html' do + note_content = 'Sit voluptatibus eveniet architecto quidem' + merge_request_note = MergeRequest.find_by(title: 'MR1').notes.select { |n| n.note.match(/#{note_content}/)}.first + + expect(merge_request_note.note_html).to match(/#{note_content}/) + end + end + end + it 'creates a valid pipeline note' do expect(Ci::Pipeline.find_by_sha('sha-notes').notes).not_to be_empty end @@ -328,6 +348,19 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end context 'when the project has overridden params in import data' do + it 'handles string versions of visibility_level' do + # Project needs to be in a group for visibility level comparison + # to happen + group = create(:group) + project.group = group + + project.create_import_data(data: { override_params: { visibility_level: Gitlab::VisibilityLevel::INTERNAL.to_s } }) + + restored_project_json + + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + it 'overwrites the params stored in the JSON' do project.create_import_data(data: { override_params: { description: "Overridden" } }) diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index cfc3e0ce926..bc4f867e891 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -91,7 +91,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do end it 'has issue comments' do - expect(saved_project_json['issues'].first['notes']).not_to be_empty + notes = saved_project_json['issues'].first['notes'] + + expect(notes).not_to be_empty + expect(notes.first['type']).to eq('DiscussionNote') end it 'has issue assignees' do @@ -299,7 +302,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do create(:commit_status, project: project, pipeline: ci_build.pipeline) create(:milestone, project: project) - create(:note, noteable: issue, project: project) + create(:discussion_note, noteable: issue, project: project) create(:note, noteable: merge_request, project: project) create(:note, noteable: snippet, project: project) create(:note_on_commit, diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index 8a699eb1461..e2ffb2adb9b 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -34,11 +34,5 @@ describe Gitlab::ImportExport::RepoRestorer do it 'restores the repo successfully' do expect(restorer.restore).to be_truthy end - - it 'has the webhooks' do - restorer.restore - - expect(project_hook_exists?(project)).to be true - end end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index baca8f6d542..9093d21647a 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -11,6 +11,7 @@ Issue: - branch_name - description - state +- state_id - iid - updated_by_id - confidential @@ -158,6 +159,7 @@ MergeRequest: - created_at - updated_at - state +- state_id - merge_status - target_project_id - iid @@ -235,6 +237,8 @@ Ci::Pipeline: - ref - sha - before_sha +- source_sha +- target_sha - push_data - created_at - updated_at @@ -419,6 +423,7 @@ Service: - wiki_page_events - confidential_issues_events - confidential_note_events +- deployment_events ProjectHook: - id - url @@ -492,6 +497,7 @@ Project: - merge_requests_ff_only_enabled - merge_requests_rebase_enabled - jobs_cache_index +- external_authorization_classification_label - pages_https_only Author: - name @@ -601,8 +607,27 @@ ResourceLabelEvent: - user_id - created_at ErrorTracking::ProjectErrorTrackingSetting: -- id - api_url - project_id - project_name - organization_name +Suggestion: +- id +- note_id +- relative_order +- applied +- commit_id +- from_content +- to_content +- outdated +- lines_above +- lines_below +MergeRequestAssignee: +- id +- user_id +- merge_request_id +ProjectMetricsSetting: +- project_id +- external_dashboard_url +- created_at +- updated_at diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb index 94abf9679c4..8060b5d4448 100644 --- a/spec/lib/gitlab/import_sources_spec.rb +++ b/spec/lib/gitlab/import_sources_spec.rb @@ -14,7 +14,8 @@ describe Gitlab::ImportSources do 'Repo by URL' => 'git', 'GitLab export' => 'gitlab_project', 'Gitea' => 'gitea', - 'Manifest file' => 'manifest' + 'Manifest file' => 'manifest', + 'Phabricator' => 'phabricator' } expect(described_class.options).to eq(expected) @@ -35,6 +36,7 @@ describe Gitlab::ImportSources do gitlab_project gitea manifest + phabricator ) expect(described_class.values).to eq(expected) @@ -53,6 +55,7 @@ describe Gitlab::ImportSources do fogbugz gitlab_project gitea + phabricator ) expect(described_class.importer_names).to eq(expected) @@ -70,7 +73,8 @@ describe Gitlab::ImportSources do 'git' => nil, 'gitlab_project' => Gitlab::ImportExport::Importer, 'gitea' => Gitlab::LegacyGithubImport::Importer, - 'manifest' => nil + 'manifest' => nil, + 'phabricator' => Gitlab::PhabricatorImport::Importer } import_sources.each do |name, klass| @@ -91,7 +95,8 @@ describe Gitlab::ImportSources do 'git' => 'Repo by URL', 'gitlab_project' => 'GitLab export', 'gitea' => 'Gitea', - 'manifest' => 'Manifest file' + 'manifest' => 'Manifest file', + 'phabricator' => 'Phabricator' } import_sources.each do |name, title| @@ -102,7 +107,7 @@ describe Gitlab::ImportSources do end describe 'imports_repository? checker' do - let(:allowed_importers) { %w[github gitlab_project bitbucket_server] } + let(:allowed_importers) { %w[github gitlab_project bitbucket_server phabricator] } it 'fails if any importer other than the allowed ones implements this method' do current_importers = described_class.values.select { |kind| described_class.importer(kind).try(:imports_repository?) } diff --git a/spec/lib/gitlab/issuable_metadata_spec.rb b/spec/lib/gitlab/issuable_metadata_spec.rb index 42635a68ee1..916f3876a8e 100644 --- a/spec/lib/gitlab/issuable_metadata_spec.rb +++ b/spec/lib/gitlab/issuable_metadata_spec.rb @@ -19,7 +19,7 @@ describe Gitlab::IssuableMetadata do let!(:closed_issue) { create(:issue, state: :closed, author: user, project: project) } let!(:downvote) { create(:award_emoji, :downvote, awardable: closed_issue) } let!(:upvote) { create(:award_emoji, :upvote, awardable: issue) } - let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") } + let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") } let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) } it 'aggregates stats on issues' do @@ -28,18 +28,18 @@ describe Gitlab::IssuableMetadata do expect(data.count).to eq(2) expect(data[issue.id].upvotes).to eq(1) expect(data[issue.id].downvotes).to eq(0) - expect(data[issue.id].notes_count).to eq(0) + expect(data[issue.id].user_notes_count).to eq(0) expect(data[issue.id].merge_requests_count).to eq(1) expect(data[closed_issue.id].upvotes).to eq(0) expect(data[closed_issue.id].downvotes).to eq(1) - expect(data[closed_issue.id].notes_count).to eq(0) + expect(data[closed_issue.id].user_notes_count).to eq(0) expect(data[closed_issue.id].merge_requests_count).to eq(0) end end context 'merge requests' do - let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") } + let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") } let!(:merge_request_closed) { create(:merge_request, state: "closed", source_project: project, target_project: project, title: "Closed Test") } let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request) } let!(:upvote) { create(:award_emoji, :upvote, awardable: merge_request) } @@ -51,12 +51,12 @@ describe Gitlab::IssuableMetadata do expect(data.count).to eq(2) expect(data[merge_request.id].upvotes).to eq(1) expect(data[merge_request.id].downvotes).to eq(1) - expect(data[merge_request.id].notes_count).to eq(1) + expect(data[merge_request.id].user_notes_count).to eq(1) expect(data[merge_request.id].merge_requests_count).to eq(0) expect(data[merge_request_closed.id].upvotes).to eq(0) expect(data[merge_request_closed.id].downvotes).to eq(0) - expect(data[merge_request_closed.id].notes_count).to eq(0) + expect(data[merge_request_closed.id].user_notes_count).to eq(0) expect(data[merge_request_closed.id].merge_requests_count).to eq(0) end end diff --git a/spec/lib/gitlab/json_cache_spec.rb b/spec/lib/gitlab/json_cache_spec.rb index b52078e8556..b82c09af306 100644 --- a/spec/lib/gitlab/json_cache_spec.rb +++ b/spec/lib/gitlab/json_cache_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::JsonCache do let(:namespace) { 'geo' } let(:key) { 'foo' } let(:expanded_key) { "#{namespace}:#{key}:#{Rails.version}" } - let(:broadcast_message) { create(:broadcast_message) } + set(:broadcast_message) { create(:broadcast_message) } subject(:cache) { described_class.new(namespace: namespace, backend: backend) } @@ -146,6 +146,18 @@ describe Gitlab::JsonCache do expect(cache.read(key, BroadcastMessage)).to be_nil end + + it 'gracefully handles excluded fields from attributes during serialization' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return(broadcast_message.attributes.except("message_html").to_json) + + result = cache.read(key, BroadcastMessage) + + BroadcastMessage.cached_markdown_fields.html_fields.each do |field| + expect(result.public_send(field)).to be_nil + end + end end context 'when the cached value is an array' do @@ -297,13 +309,79 @@ describe Gitlab::JsonCache do expect(result).to eq(broadcast_message) end + context 'when the cached value is an instance of ActiveRecord::Base' do + it 'returns a persisted record when id is set' do + backend.write(expanded_key, broadcast_message.to_json) + + result = cache.fetch(key, as: BroadcastMessage) { 'block result' } + + expect(result).to be_persisted + end + + it 'returns a new record when id is nil' do + backend.write(expanded_key, build(:broadcast_message).to_json) + + result = cache.fetch(key, as: BroadcastMessage) { 'block result' } + + expect(result).to be_new_record + end + + it 'returns a new record when id is missing' do + backend.write(expanded_key, build(:broadcast_message).attributes.except('id').to_json) + + result = cache.fetch(key, as: BroadcastMessage) { 'block result' } + + expect(result).to be_new_record + end + + it 'gracefully handles bad cached entry' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return('{') + + result = cache.fetch(key, as: BroadcastMessage) { 'block result' } + + expect(result).to eq 'block result' + end + + it 'gracefully handles an empty hash' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return('{}') + + expect(cache.fetch(key, as: BroadcastMessage)).to be_a(BroadcastMessage) + end + + it 'gracefully handles unknown attributes' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return(broadcast_message.attributes.merge(unknown_attribute: 1).to_json) + + result = cache.fetch(key, as: BroadcastMessage) { 'block result' } + + expect(result).to eq 'block result' + end + + it 'gracefully handles excluded fields from attributes during serialization' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return(broadcast_message.attributes.except("message_html").to_json) + + result = cache.fetch(key, as: BroadcastMessage) { 'block result' } + + BroadcastMessage.cached_markdown_fields.html_fields.each do |field| + expect(result.public_send(field)).to be_nil + end + end + end + it "returns the result of the block when 'as' option is nil" do result = cache.fetch(key, as: nil) { 'block result' } expect(result).to eq('block result') end - it "returns the result of the block when 'as' option is not informed" do + it "returns the result of the block when 'as' option is missing" do result = cache.fetch(key) { 'block result' } expect(result).to eq('block result') diff --git a/spec/lib/gitlab/json_logger_spec.rb b/spec/lib/gitlab/json_logger_spec.rb index cff7dd58c8c..d3d9fe9948a 100644 --- a/spec/lib/gitlab/json_logger_spec.rb +++ b/spec/lib/gitlab/json_logger_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::JsonLogger do describe '#format_message' do before do - allow(Gitlab::CorrelationId).to receive(:current_id).and_return('new-correlation-id') + allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('new-correlation-id') end it 'formats strings' do diff --git a/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb b/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb index 4a669408025..e1106f7496a 100644 --- a/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb +++ b/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb @@ -28,7 +28,7 @@ describe Gitlab::Kubernetes::ClusterRoleBinding do subject { cluster_role_binding.generate } - it 'should build a Kubeclient Resource' do + it 'builds a Kubeclient Resource' do is_expected.to eq(resource) end end diff --git a/spec/lib/gitlab/kubernetes/config_map_spec.rb b/spec/lib/gitlab/kubernetes/config_map_spec.rb index fe65d03875f..911d6024804 100644 --- a/spec/lib/gitlab/kubernetes/config_map_spec.rb +++ b/spec/lib/gitlab/kubernetes/config_map_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::Kubernetes::ConfigMap do let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: application.files) } subject { config_map.generate } - it 'should build a Kubeclient Resource' do + it 'builds a Kubeclient Resource' do is_expected.to eq(resource) end end diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 8433d40b2ea..0de809833e6 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -33,6 +33,52 @@ describe Gitlab::Kubernetes::Helm::Api do end end + describe '#uninstall' do + before do + allow(client).to receive(:create_pod).and_return(nil) + allow(client).to receive(:get_config_map).and_return(nil) + allow(client).to receive(:create_config_map).and_return(nil) + allow(client).to receive(:delete_pod).and_return(nil) + allow(namespace).to receive(:ensure_exists!).once + end + + it 'ensures the namespace exists before creating the POD' do + expect(namespace).to receive(:ensure_exists!).once.ordered + expect(client).to receive(:create_pod).once.ordered + + subject.uninstall(command) + end + + it 'removes an existing pod before installing' do + expect(client).to receive(:delete_pod).with('install-app-name', 'gitlab-managed-apps').once.ordered + expect(client).to receive(:create_pod).once.ordered + + subject.uninstall(command) + end + + context 'with a ConfigMap' do + let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application_name, files).generate } + + it 'creates a ConfigMap on kubeclient' do + expect(client).to receive(:create_config_map).with(resource).once + + subject.install(command) + end + + context 'config map already exists' do + before do + expect(client).to receive(:get_config_map).with("values-content-configuration-#{application_name}", gitlab_namespace).and_return(resource) + end + + it 'updates the config map' do + expect(client).to receive(:update_config_map).with(resource).once + + subject.install(command) + end + end + end + end + describe '#install' do before do allow(client).to receive(:create_pod).and_return(nil) diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb index aacae78be43..78a4eb44e38 100644 --- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb @@ -41,7 +41,7 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do describe '#pod_resource' do subject { base_command.pod_resource } - it 'should returns a kubeclient resoure with pod content for application' do + it 'returns a kubeclient resoure with pod content for application' do is_expected.to be_an_instance_of ::Kubeclient::Resource end diff --git a/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb b/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb index 167bee22fc3..04649353976 100644 --- a/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Kubernetes::Helm::Certificate do describe '.generate_root' do subject { described_class.generate_root } - it 'should generate a root CA that expires a long way in the future' do + it 'generates a root CA that expires a long way in the future' do expect(subject.cert.not_after).to be > 999.years.from_now end end @@ -13,14 +13,14 @@ describe Gitlab::Kubernetes::Helm::Certificate do describe '#issue' do subject { described_class.generate_root.issue } - it 'should generate a cert that expires soon' do + it 'generates a cert that expires soon' do expect(subject.cert.not_after).to be < 60.minutes.from_now end context 'passing in INFINITE_EXPIRY' do subject { described_class.generate_root.issue(expires_in: described_class::INFINITE_EXPIRY) } - it 'should generate a cert that expires a long way in the future' do + it 'generates a cert that expires a long way in the future' do expect(subject.cert.not_after).to be > 999.years.from_now end end diff --git a/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb new file mode 100644 index 00000000000..cae92305b19 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Kubernetes::Helm::DeleteCommand do + let(:app_name) { 'app-name' } + let(:rbac) { true } + let(:files) { {} } + let(:delete_command) { described_class.new(name: app_name, rbac: rbac, files: files) } + + subject { delete_command } + + it_behaves_like 'helm commands' do + let(:commands) do + <<~EOS + helm init --upgrade + for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done + helm delete --purge app-name + EOS + end + end + + context 'when there is a ca.pem file' do + let(:files) { { 'ca.pem': 'some file content' } } + + it_behaves_like 'helm commands' do + let(:commands) do + <<~EOS + helm init --upgrade + for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done + #{helm_delete_command} + EOS + end + + let(:helm_delete_command) do + <<~EOS.squish + helm delete --purge app-name + --tls + --tls-ca-cert /data/helm/app-name/config/ca.pem + --tls-cert /data/helm/app-name/config/cert.pem + --tls-key /data/helm/app-name/config/key.pem + EOS + end + end + end + + describe '#pod_resource' do + subject { delete_command.pod_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it 'generates a pod that uses the tiller serviceAccountName' do + expect(subject.spec.serviceAccountName).to eq('tiller') + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it 'generates a pod that uses the default serviceAccountName' do + expect(subject.spec.serviceAcccountName).to be_nil + end + end + end + + describe '#pod_name' do + subject { delete_command.pod_name } + + it { is_expected.to eq('uninstall-app-name') } + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index 95b6b3fd953..06c8d127951 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -10,11 +10,11 @@ describe Gitlab::Kubernetes::Helm::Pod do subject { described_class.new(command, namespace, service_account_name: service_account_name) } context 'with a command' do - it 'should generate a Kubeclient::Resource' do + it 'generates a Kubeclient::Resource' do expect(subject.generate).to be_a_kind_of(Kubeclient::Resource) end - it 'should generate the appropriate metadata' do + it 'generates the appropriate metadata' do metadata = subject.generate.metadata expect(metadata.name).to eq("install-#{app.name}") expect(metadata.namespace).to eq('gitlab-managed-apps') @@ -22,12 +22,12 @@ describe Gitlab::Kubernetes::Helm::Pod do expect(metadata.labels['gitlab.org/application']).to eq(app.name) end - it 'should generate a container spec' do + it 'generates a container spec' do spec = subject.generate.spec expect(spec.containers.count).to eq(1) end - it 'should generate the appropriate specifications for the container' do + it 'generates the appropriate specifications for the container' do container = subject.generate.spec.containers.first expect(container.name).to eq('helm') expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.12.3-kube-1.11.7') @@ -37,30 +37,30 @@ describe Gitlab::Kubernetes::Helm::Pod do expect(container.args).to match_array(["-c", "$(COMMAND_SCRIPT)"]) end - it 'should include a never restart policy' do + it 'includes a never restart policy' do spec = subject.generate.spec expect(spec.restartPolicy).to eq('Never') end - it 'should include volumes for the container' do + it 'includes volumes for the container' do container = subject.generate.spec.containers.first expect(container.volumeMounts.first['name']).to eq('configuration-volume') expect(container.volumeMounts.first['mountPath']).to eq("/data/helm/#{app.name}/config") end - it 'should include a volume inside the specification' do + it 'includes a volume inside the specification' do spec = subject.generate.spec expect(spec.volumes.first['name']).to eq('configuration-volume') end - it 'should mount configMap specification in the volume' do + it 'mounts configMap specification in the volume' do volume = subject.generate.spec.volumes.first expect(volume.configMap['name']).to eq("values-content-configuration-#{app.name}") expect(volume.configMap['items'].first['key']).to eq(:'values.yaml') expect(volume.configMap['items'].first['path']).to eq(:'values.yaml') end - it 'should have no serviceAccountName' do + it 'has no serviceAccountName' do spec = subject.generate.spec expect(spec.serviceAccountName).to be_nil end @@ -68,7 +68,7 @@ describe Gitlab::Kubernetes::Helm::Pod do context 'with a service_account_name' do let(:service_account_name) { 'sa' } - it 'should use the serviceAccountName provided' do + it 'uses the serviceAccountName provided' do spec = subject.generate.spec expect(spec.serviceAccountName).to eq(service_account_name) end diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 02364e92149..978e64c4407 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -50,6 +50,36 @@ describe Gitlab::Kubernetes::KubeClient do end end + describe '#initialize' do + shared_examples 'local address' do + it 'blocks local addresses' do + expect { client }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError) + end + + context 'when local requests are allowed' do + before do + stub_application_setting(allow_local_requests_from_hooks_and_services: true) + end + + it 'allows local addresses' do + expect { client }.not_to raise_error + end + end + end + + context 'localhost address' do + let(:api_url) { 'http://localhost:22' } + + it_behaves_like 'local address' + end + + context 'private network address' do + let(:api_url) { 'http://192.168.1.2:3003' } + + it_behaves_like 'local address' + end + end + describe '#core_client' do subject { client.core_client } diff --git a/spec/lib/gitlab/kubernetes/namespace_spec.rb b/spec/lib/gitlab/kubernetes/namespace_spec.rb index e1c35c355f4..e91a755aa03 100644 --- a/spec/lib/gitlab/kubernetes/namespace_spec.rb +++ b/spec/lib/gitlab/kubernetes/namespace_spec.rb @@ -62,5 +62,32 @@ describe Gitlab::Kubernetes::Namespace do subject.ensure_exists! end + + context 'when client errors' do + let(:exception) { Kubeclient::HttpError.new(500, 'system failure', nil) } + + before do + allow(client).to receive(:get_namespace).with(name).once.and_raise(exception) + end + + it 'raises the exception' do + expect { subject.ensure_exists! }.to raise_error(exception) + end + + it 'logs the error' do + expect(subject.send(:logger)).to receive(:error).with( + hash_including( + exception: 'Kubeclient::HttpError', + status_code: 500, + namespace: 'a_namespace', + class_name: 'Gitlab::Kubernetes::Namespace', + event: :failed_to_create_namespace, + message: 'system failure' + ) + ) + + expect { subject.ensure_exists! }.to raise_error(exception) + end + end end end diff --git a/spec/lib/gitlab/kubernetes/role_binding_spec.rb b/spec/lib/gitlab/kubernetes/role_binding_spec.rb index a1a59533bfb..50acee254cb 100644 --- a/spec/lib/gitlab/kubernetes/role_binding_spec.rb +++ b/spec/lib/gitlab/kubernetes/role_binding_spec.rb @@ -42,7 +42,7 @@ describe Gitlab::Kubernetes::RoleBinding, '#generate' do ).generate end - it 'should build a Kubeclient Resource' do + it 'builds a Kubeclient Resource' do is_expected.to eq(resource) end end diff --git a/spec/lib/gitlab/kubernetes/service_account_spec.rb b/spec/lib/gitlab/kubernetes/service_account_spec.rb index 8da9e932dc3..0d525966d18 100644 --- a/spec/lib/gitlab/kubernetes/service_account_spec.rb +++ b/spec/lib/gitlab/kubernetes/service_account_spec.rb @@ -17,7 +17,7 @@ describe Gitlab::Kubernetes::ServiceAccount do subject { service_account.generate } - it 'should build a Kubeclient Resource' do + it 'builds a Kubeclient Resource' do is_expected.to eq(resource) end end diff --git a/spec/lib/gitlab/kubernetes/service_account_token_spec.rb b/spec/lib/gitlab/kubernetes/service_account_token_spec.rb index 0773d3d9aec..0d334bed45f 100644 --- a/spec/lib/gitlab/kubernetes/service_account_token_spec.rb +++ b/spec/lib/gitlab/kubernetes/service_account_token_spec.rb @@ -28,7 +28,7 @@ describe Gitlab::Kubernetes::ServiceAccountToken do subject { service_account_token.generate } - it 'should build a Kubeclient Resource' do + it 'builds a Kubeclient Resource' do is_expected.to eq(resource) end end diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb index f326d57e9c6..57b570a9166 100644 --- a/spec/lib/gitlab/kubernetes_spec.rb +++ b/spec/lib/gitlab/kubernetes_spec.rb @@ -40,10 +40,40 @@ describe Gitlab::Kubernetes do describe '#filter_by_label' do it 'returns matching labels' do - matching_items = [kube_pod(app: 'foo')] + matching_items = [kube_pod(track: 'foo'), kube_deployment(track: 'foo')] + items = matching_items + [kube_pod, kube_deployment] + + expect(filter_by_label(items, 'track' => 'foo')).to eq(matching_items) + end + end + + describe '#filter_by_annotation' do + it 'returns matching labels' do + matching_items = [kube_pod(environment_slug: 'foo'), kube_deployment(environment_slug: 'foo')] + items = matching_items + [kube_pod, kube_deployment] + + expect(filter_by_annotation(items, 'app.gitlab.com/env' => 'foo')).to eq(matching_items) + end + end + + describe '#filter_by_project_environment' do + let(:matching_pod) { kube_pod(environment_slug: 'production', project_slug: 'my-cool-app') } + + it 'returns matching legacy env label' do + matching_pod['metadata']['annotations'].delete('app.gitlab.com/app') + matching_pod['metadata']['annotations'].delete('app.gitlab.com/env') + matching_pod['metadata']['labels']['app'] = 'production' + matching_items = [matching_pod] + items = matching_items + [kube_pod] + + expect(filter_by_project_environment(items, 'my-cool-app', 'production')).to eq(matching_items) + end + + it 'returns matching env label' do + matching_items = [matching_pod] items = matching_items + [kube_pod] - expect(filter_by_label(items, app: 'foo')).to eq(matching_items) + expect(filter_by_project_environment(items, 'my-cool-app', 'production')).to eq(matching_items) end end diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb index 3d4240fa4ba..8675d8691c8 100644 --- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb @@ -47,12 +47,22 @@ describe Gitlab::LegacyGithubImport::ProjectCreator do end context 'when GitHub project is public' do - it 'sets project visibility to public' do + it 'sets project visibility to namespace visibility level' do repo.private = false - project = service.execute - expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + expect(project.visibility_level).to eq(namespace.visibility_level) + end + + context 'when importing into a user namespace' do + subject(:service) { described_class.new(repo, repo.name, user.namespace, user, github_access_token: 'asdffg') } + + it 'sets project visibility to user namespace visibility level' do + repo.private = false + project = service.execute + + expect(project.visibility_level).to eq(user.namespace.visibility_level) + end end end diff --git a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb index 082e3b36dd0..c57b96fb00d 100644 --- a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb @@ -25,6 +25,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do expected = { project: project, tag: 'v1.0.0', + name: 'First release', description: 'Release v1.0.0', created_at: created_at, updated_at: created_at diff --git a/spec/lib/gitlab/lets_encrypt/challenge_spec.rb b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb new file mode 100644 index 00000000000..fcd92586362 --- /dev/null +++ b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::Gitlab::LetsEncrypt::Challenge do + include LetsEncryptHelpers + + let(:challenge) { described_class.new(acme_challenge_double) } + + LetsEncryptHelpers::ACME_CHALLENGE_METHODS.each do |method, value| + describe "##{method}" do + it 'delegates to Acme::Client::Resources::Challenge' do + expect(challenge.public_send(method)).to eq(value) + end + end + end +end diff --git a/spec/lib/gitlab/lets_encrypt/client_spec.rb b/spec/lib/gitlab/lets_encrypt/client_spec.rb new file mode 100644 index 00000000000..5454d9c1af4 --- /dev/null +++ b/spec/lib/gitlab/lets_encrypt/client_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::Gitlab::LetsEncrypt::Client do + include LetsEncryptHelpers + + let(:client) { described_class.new } + + before do + stub_application_setting( + lets_encrypt_notification_email: 'myemail@test.example.com', + lets_encrypt_terms_of_service_accepted: true + ) + end + + let!(:stub_client) { stub_lets_encrypt_client } + + shared_examples 'ensures account registration' do + it 'ensures account registration' do + subject + + expect(stub_client).to have_received(:new_account).with( + contact: 'mailto:myemail@test.example.com', + terms_of_service_agreed: true + ) + end + + it 'generates and stores private key and initialize acme client with it' do + expect(Gitlab::CurrentSettings.lets_encrypt_private_key).to eq(nil) + + subject + + saved_private_key = Gitlab::CurrentSettings.lets_encrypt_private_key + + expect(saved_private_key).to be + expect(Acme::Client).to have_received(:new).with( + hash_including(private_key: eq_pem(saved_private_key)) + ) + end + + context 'when private key is saved in settings' do + let!(:saved_private_key) do + key = OpenSSL::PKey::RSA.new(4096).to_pem + Gitlab::CurrentSettings.current_application_settings.update(lets_encrypt_private_key: key) + key + end + + it 'uses current value of private key' do + subject + + expect(Acme::Client).to have_received(:new).with( + hash_including(private_key: eq_pem(saved_private_key)) + ) + expect(Gitlab::CurrentSettings.lets_encrypt_private_key).to eq(saved_private_key) + end + end + + context 'when acme integration is disabled' do + before do + stub_application_setting(lets_encrypt_terms_of_service_accepted: false) + end + + it 'raises error' do + expect do + subject + end.to raise_error('Acme integration is disabled') + end + end + end + + describe '#new_order' do + subject(:new_order) { client.new_order('example.com') } + + before do + order_double = instance_double('Acme::Order') + allow(stub_client).to receive(:new_order).and_return(order_double) + end + + include_examples 'ensures account registration' + + it 'returns order' do + is_expected.to be_a(::Gitlab::LetsEncrypt::Order) + end + end + + describe '#load_order' do + let(:url) { 'https://example.com/order' } + subject { client.load_order(url) } + + before do + acme_order = instance_double('Acme::Client::Resources::Order') + allow(stub_client).to receive(:order).with(url: url).and_return(acme_order) + end + + include_examples 'ensures account registration' + + it 'loads order' do + is_expected.to be_a(::Gitlab::LetsEncrypt::Order) + end + end + + describe '#load_challenge' do + let(:url) { 'https://example.com/challenge' } + subject { client.load_challenge(url) } + + before do + acme_challenge = instance_double('Acme::Client::Resources::Challenge') + allow(stub_client).to receive(:challenge).with(url: url).and_return(acme_challenge) + end + + include_examples 'ensures account registration' + + it 'loads challenge' do + is_expected.to be_a(::Gitlab::LetsEncrypt::Challenge) + end + end + + describe '#enabled?' do + subject { client.enabled? } + + context 'when terms of service are accepted' do + it { is_expected.to eq(true) } + + context "when private_key isn't present and database is read only" do + before do + allow(::Gitlab::Database).to receive(:read_only?).and_return(true) + end + + it 'returns false' do + expect(::Gitlab::CurrentSettings.lets_encrypt_private_key).to eq(nil) + + is_expected.to eq(false) + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(pages_auto_ssl: false) + end + + it { is_expected.to eq(false) } + end + end + + context 'when terms of service are not accepted' do + before do + stub_application_setting(lets_encrypt_terms_of_service_accepted: false) + end + + it { is_expected.to eq(false) } + end + end + + describe '#terms_of_service_url' do + subject { client.terms_of_service_url } + + it 'returns valid url' do + is_expected.to eq("https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf") + end + end +end diff --git a/spec/lib/gitlab/lets_encrypt/order_spec.rb b/spec/lib/gitlab/lets_encrypt/order_spec.rb new file mode 100644 index 00000000000..1a759103c44 --- /dev/null +++ b/spec/lib/gitlab/lets_encrypt/order_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::Gitlab::LetsEncrypt::Order do + include LetsEncryptHelpers + + let(:acme_order) { acme_order_double } + + let(:order) { described_class.new(acme_order) } + + LetsEncryptHelpers::ACME_ORDER_METHODS.each do |method, value| + describe "##{method}" do + it 'delegates to Acme::Client::Resources::Order' do + expect(order.public_send(method)).to eq(value) + end + end + end + + describe '#new_challenge' do + it 'returns challenge' do + expect(order.new_challenge).to be_a(::Gitlab::LetsEncrypt::Challenge) + end + end + + describe '#request_certificate' do + let(:private_key) do + OpenSSL::PKey::RSA.new(4096).to_pem + end + + it 'generates csr and finalizes order' do + expect(acme_order).to receive(:finalize) do |csr:| + expect do + csr.csr # it's being evaluated lazily + end.not_to raise_error + end + + order.request_certificate(domain: 'example.com', private_key: private_key) + end + end +end diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb index 8961ecc4be0..701ed1f3a1b 100644 --- a/spec/lib/gitlab/lfs_token_spec.rb +++ b/spec/lib/gitlab/lfs_token_spec.rb @@ -77,96 +77,42 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do let(:actor) { create(:user, username: 'test_user_lfs_1') } let(:lfs_token) { described_class.new(actor) } - context 'for an HMAC token' do - before do - # We're not interested in testing LegacyRedisDeviseToken here - allow(Gitlab::LfsToken::LegacyRedisDeviseToken).to receive_message_chain(:new, :token_valid?).and_return(false) - end - - context 'where the token is invalid' do - context "because it's junk" do - it 'returns false' do - expect(lfs_token.token_valid?('junk')).to be_falsey - end - end - - context "because it's been fiddled with" do - it 'returns false' do - fiddled_token = lfs_token.token.tap { |token| token[0] = 'E' } - expect(lfs_token.token_valid?(fiddled_token)).to be_falsey - end - end - - context "because it was generated with a different secret" do - it 'returns false' do - different_actor = create(:user, username: 'test_user_lfs_2') - different_secret_token = described_class.new(different_actor).token - expect(lfs_token.token_valid?(different_secret_token)).to be_falsey - end - end - - context "because it's expired" do - it 'returns false' do - expired_token = lfs_token.token - # Needs to be at least 1860 seconds, because the default expiry is - # 1800 seconds with an additional 60 second leeway. - Timecop.freeze(Time.now + 1865) do - expect(lfs_token.token_valid?(expired_token)).to be_falsey - end - end + context 'where the token is invalid' do + context "because it's junk" do + it 'returns false' do + expect(lfs_token.token_valid?('junk')).to be_falsey end end - context 'where the token is valid' do - it 'returns true' do - expect(lfs_token.token_valid?(lfs_token.token)).to be_truthy + context "because it's been fiddled with" do + it 'returns false' do + fiddled_token = lfs_token.token.tap { |token| token[0] = 'E' } + expect(lfs_token.token_valid?(fiddled_token)).to be_falsey end end - end - - context 'for a LegacyRedisDevise token' do - before do - # We're not interested in testing HMACToken here - allow_any_instance_of(Gitlab::LfsToken::HMACToken).to receive(:token_valid?).and_return(false) - end - - context 'where the token is invalid' do - context "because it's junk" do - it 'returns false' do - expect(lfs_token.token_valid?('junk')).to be_falsey - end - end - context "because it's been fiddled with" do - it 'returns false' do - generated_token = Gitlab::LfsToken::LegacyRedisDeviseToken.new(actor).store_new_token - fiddled_token = generated_token.tap { |token| token[0] = 'E' } - expect(lfs_token.token_valid?(fiddled_token)).to be_falsey - end - end - - context "because it was generated with a different secret" do - it 'returns false' do - different_actor = create(:user, username: 'test_user_lfs_2') - different_secret_token = described_class.new(different_actor).token - expect(lfs_token.token_valid?(different_secret_token)).to be_falsey - end + context "because it was generated with a different secret" do + it 'returns false' do + different_actor = create(:user, username: 'test_user_lfs_2') + different_secret_token = described_class.new(different_actor).token + expect(lfs_token.token_valid?(different_secret_token)).to be_falsey end + end - context "because it's expired" do - it 'returns false' do - generated_token = Gitlab::LfsToken::LegacyRedisDeviseToken.new(actor).store_new_token(1) - # We need a real sleep here because we need to wait for redis to expire the key. - sleep(0.01) - expect(lfs_token.token_valid?(generated_token)).to be_falsey + context "because it's expired" do + it 'returns false' do + expired_token = lfs_token.token + # Needs to be at least 1860 seconds, because the default expiry is + # 1800 seconds with an additional 60 second leeway. + Timecop.freeze(Time.now + 1865) do + expect(lfs_token.token_valid?(expired_token)).to be_falsey end end end context 'where the token is valid' do it 'returns true' do - generated_token = Gitlab::LfsToken::LegacyRedisDeviseToken.new(actor).store_new_token - expect(lfs_token.token_valid?(generated_token)).to be_truthy + expect(lfs_token.token_valid?(lfs_token.token)).to be_truthy end end end diff --git a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb new file mode 100644 index 00000000000..18052b1991c --- /dev/null +++ b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::MarkdownCache::ActiveRecord::Extension do + let(:klass) do + Class.new(ActiveRecord::Base) do + self.table_name = 'issues' + include CacheMarkdownField + cache_markdown_field :title, whitelisted: true + cache_markdown_field :description, pipeline: :single_line + + attr_accessor :author, :project + end + end + + let(:cache_version) { Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 } + let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) } + + let(:markdown) { '`Foo`' } + let(:html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Foo</code></p>' } + + let(:updated_markdown) { '`Bar`' } + let(:updated_html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Bar</code></p>' } + + context 'an unchanged markdown field' do + let(:thing) { klass.new(title: markdown) } + + before do + thing.title = thing.title + thing.save + end + + it { expect(thing.title).to eq(markdown) } + it { expect(thing.title_html).to eq(html) } + it { expect(thing.title_html_changed?).not_to be_truthy } + it { expect(thing.cached_markdown_version).to eq(cache_version) } + end + + context 'a changed markdown field' do + let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) } + + before do + thing.title = updated_markdown + thing.save + end + + it { expect(thing.title_html).to eq(updated_html) } + it { expect(thing.cached_markdown_version).to eq(cache_version) } + end + + context 'when a markdown field is set repeatedly to an empty string' do + it do + expect(thing).to receive(:refresh_markdown_cache).once + thing.title = '' + thing.save + thing.title = '' + thing.save + end + end + + context 'when a markdown field is set repeatedly to a string which renders as empty html' do + it do + expect(thing).to receive(:refresh_markdown_cache).once + thing.title = '[//]: # (This is also a comment.)' + thing.save + thing.title = '[//]: # (This is also a comment.)' + thing.save + end + end + + context 'a non-markdown field changed' do + let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) } + + before do + thing.state = 'closed' + thing.save + end + + it { expect(thing.state).to eq('closed') } + it { expect(thing.title).to eq(markdown) } + it { expect(thing.title_html).to eq(html) } + it { expect(thing.cached_markdown_version).to eq(cache_version) } + end + + context 'version is out of date' do + let(:thing) { klass.new(title: updated_markdown, title_html: html, cached_markdown_version: nil) } + + before do + thing.save + end + + it { expect(thing.title_html).to eq(updated_html) } + it { expect(thing.cached_markdown_version).to eq(cache_version) } + end + + context 'when an invalidating field is changed' do + it 'invalidates the cache when project changes' do + thing.project = :new_project + allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html) + + thing.save + + expect(thing.title_html).to eq(updated_html) + expect(thing.description_html).to eq(updated_html) + expect(thing.cached_markdown_version).to eq(cache_version) + end + + it 'invalidates the cache when author changes' do + thing.author = :new_author + allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html) + + thing.save + + expect(thing.title_html).to eq(updated_html) + expect(thing.description_html).to eq(updated_html) + expect(thing.cached_markdown_version).to eq(cache_version) + end + end + + describe '.attributes' do + it 'excludes cache attributes that is blacklisted by default' do + expect(thing.attributes.keys.sort).not_to include(%w[description_html]) + end + end + + describe '#cached_html_up_to_date?' do + let(:thing) { klass.create(title: updated_markdown, title_html: html, cached_markdown_version: nil) } + subject { thing.cached_html_up_to_date?(:title) } + + it 'returns false if markdown has been changed but html has not' do + thing.title = "changed!" + + is_expected.to be_falsy + end + + it 'returns true if markdown has not been changed but html has' do + thing.title_html = updated_html + + is_expected.to be_truthy + end + + it 'returns true if markdown and html have both been changed' do + thing.title = updated_markdown + thing.title_html = updated_html + + is_expected.to be_truthy + end + + it 'returns false if the markdown field is set but the html is not' do + thing.title_html = nil + + is_expected.to be_falsy + end + end + + describe '#refresh_markdown_cache!' do + before do + thing.title = updated_markdown + end + + it 'skips saving if not persisted' do + expect(thing).to receive(:persisted?).and_return(false) + expect(thing).not_to receive(:update_columns) + + thing.refresh_markdown_cache! + end + + it 'saves the changes' do + expect(thing).to receive(:persisted?).and_return(true) + + expect(thing).to receive(:update_columns) + .with("title_html" => updated_html, + "description_html" => "", + "cached_markdown_version" => cache_version) + + thing.refresh_markdown_cache! + end + end +end diff --git a/spec/lib/gitlab/markdown_cache/field_data_spec.rb b/spec/lib/gitlab/markdown_cache/field_data_spec.rb new file mode 100644 index 00000000000..393bf85aa43 --- /dev/null +++ b/spec/lib/gitlab/markdown_cache/field_data_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::MarkdownCache::FieldData do + subject(:field_data) { described_class.new } + + before do + field_data[:description] = { project: double('project in context') } + end + + it 'translates a markdown field name into a html field name' do + expect(field_data.html_field(:description)).to eq("description_html") + end +end diff --git a/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb new file mode 100644 index 00000000000..b6a781de426 --- /dev/null +++ b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::MarkdownCache::Redis::Extension, :clean_gitlab_redis_cache do + let(:klass) do + Class.new do + include CacheMarkdownField + + def initialize(title: nil, description: nil) + @title, @description = title, description + end + + attr_reader :title, :description + + cache_markdown_field :title, pipeline: :single_line + cache_markdown_field :description + + def cache_key + "cache-key" + end + end + end + + let(:cache_version) { Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 } + let(:thing) { klass.new(title: "`Hello`", description: "`World`") } + let(:expected_cache_key) { "markdown_cache:cache-key" } + + it 'defines the html attributes' do + expect(thing).to respond_to(:title_html, :description_html, :cached_markdown_version) + end + + it 'loads the markdown from the cache only once' do + expect(thing).to receive(:load_cached_markdown).once.and_call_original + + thing.title_html + thing.description_html + end + + it 'correctly loads the markdown if it was stored in redis' do + Gitlab::Redis::Cache.with do |r| + r.mapped_hmset(expected_cache_key, + title_html: 'hello', + description_html: 'world', + cached_markdown_version: cache_version) + end + + expect(thing.title_html).to eq('hello') + expect(thing.description_html).to eq('world') + expect(thing.cached_markdown_version).to eq(cache_version) + end + + describe "#refresh_markdown_cache!" do + it "stores the value in redis" do + expected_results = { "title_html" => "`Hello`", + "description_html" => "<p data-sourcepos=\"1:1-1:7\" dir=\"auto\"><code>World</code></p>", + "cached_markdown_version" => cache_version.to_s } + + thing.refresh_markdown_cache! + + results = Gitlab::Redis::Cache.with do |r| + r.mapped_hmget(expected_cache_key, + "title_html", "description_html", "cached_markdown_version") + end + + expect(results).to eq(expected_results) + end + + it "assigns the values" do + thing.refresh_markdown_cache! + + expect(thing.title_html).to eq('`Hello`') + expect(thing.description_html).to eq("<p data-sourcepos=\"1:1-1:7\" dir=\"auto\"><code>World</code></p>") + expect(thing.cached_markdown_version).to eq(cache_version) + end + end +end diff --git a/spec/lib/gitlab/markdown_cache/redis/store_spec.rb b/spec/lib/gitlab/markdown_cache/redis/store_spec.rb new file mode 100644 index 00000000000..95c68e7d491 --- /dev/null +++ b/spec/lib/gitlab/markdown_cache/redis/store_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::MarkdownCache::Redis::Store, :clean_gitlab_redis_cache do + let(:storable_class) do + Class.new do + cattr_reader :cached_markdown_fields do + Gitlab::MarkdownCache::FieldData.new.tap do |field_data| + field_data[:field_1] = {} + field_data[:field_2] = {} + end + end + + attr_accessor :field_1, :field_2, :field_1_html, :field_2_html, :cached_markdown_version + + def cache_key + "cache-key" + end + end + end + let(:storable) { storable_class.new } + let(:cache_key) { "markdown_cache:#{storable.cache_key}" } + + subject(:store) { described_class.new(storable) } + + def read_values + Gitlab::Redis::Cache.with do |r| + r.mapped_hmget(cache_key, + :field_1_html, :field_2_html, :cached_markdown_version) + end + end + + def store_values(values) + Gitlab::Redis::Cache.with do |r| + r.mapped_hmset(cache_key, + values) + end + end + + describe '#save' do + it 'stores updates to html fields and version' do + values_to_store = { field_1_html: "hello", field_2_html: "world", cached_markdown_version: 1 } + + store.save(values_to_store) + + expect(read_values) + .to eq({ field_1_html: "hello", field_2_html: "world", cached_markdown_version: "1" }) + end + end + + describe '#read' do + it 'reads the html fields and version from redis if they were stored' do + stored_values = { field_1_html: "hello", field_2_html: "world", cached_markdown_version: 1 } + + store_values(stored_values) + + expect(store.read.symbolize_keys).to eq(stored_values) + end + + it 'is mared loaded after reading' do + expect(store).not_to be_loaded + + store.read + + expect(store).to be_loaded + end + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb new file mode 100644 index 00000000000..e88eb140b35 --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store_caching do + include MetricsDashboardHelpers + + set(:project) { build(:project) } + set(:environment) { build(:environment, project: project) } + let(:system_dashboard_path) { Gitlab::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH} + + describe '.find' do + let(:dashboard_path) { '.gitlab/dashboards/test.yml' } + let(:service_call) { described_class.find(project, nil, environment, dashboard_path) } + + it_behaves_like 'misconfigured dashboard service response', :not_found + + context 'when the dashboard exists' do + let(:project) { project_with_dashboard(dashboard_path) } + + it_behaves_like 'valid dashboard service response' + end + + context 'when the dashboard is configured incorrectly' do + let(:project) { project_with_dashboard(dashboard_path, {}) } + + it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity + end + + context 'when the system dashboard is specified' do + let(:dashboard_path) { system_dashboard_path } + + it_behaves_like 'valid dashboard service response' + end + + context 'when no dashboard is specified' do + let(:service_call) { described_class.find(project, nil, environment) } + + it_behaves_like 'valid dashboard service response' + end + end + + describe '.find_all_paths' do + let(:all_dashboard_paths) { described_class.find_all_paths(project) } + let(:system_dashboard) { { path: system_dashboard_path, default: true } } + + it 'includes only the system dashboard by default' do + expect(all_dashboard_paths).to eq([system_dashboard]) + end + + context 'when the project contains dashboards' do + let(:dashboard_path) { '.gitlab/dashboards/test.yml' } + let(:project) { project_with_dashboard(dashboard_path) } + + it 'includes system and project dashboards' do + project_dashboard = { path: dashboard_path, default: false } + + expect(all_dashboard_paths).to contain_exactly(system_dashboard, project_dashboard) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb new file mode 100644 index 00000000000..be3c1095bd7 --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Metrics::Dashboard::Processor do + let(:project) { build(:project) } + let(:environment) { build(:environment, project: project) } + let(:dashboard_yml) { YAML.load_file('spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml') } + + describe 'process' do + let(:process_params) { [project, environment, dashboard_yml] } + let(:dashboard) { described_class.new(*process_params).process(insert_project_metrics: true) } + + context 'when dashboard config corresponds to common metrics' do + let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') } + + it 'inserts metric ids into the config' do + target_metric = all_metrics.find { |metric| metric[:id] == 'metric_a1' } + + expect(target_metric).to include(:metric_id) + expect(target_metric[:metric_id]).to eq(common_metric.id) + end + end + + context 'when the project has associated metrics' do + let!(:project_response_metric) { create(:prometheus_metric, project: project, group: :response) } + let!(:project_system_metric) { create(:prometheus_metric, project: project, group: :system) } + let!(:project_business_metric) { create(:prometheus_metric, project: project, group: :business) } + + it 'includes project-specific metrics' do + expect(all_metrics).to include get_metric_details(project_system_metric) + expect(all_metrics).to include get_metric_details(project_response_metric) + expect(all_metrics).to include get_metric_details(project_business_metric) + end + + it 'orders groups by priority and panels by weight' do + expected_metrics_order = [ + 'metric_b', # group priority 10, panel weight 1 + 'metric_a2', # group priority 1, panel weight 2 + 'metric_a1', # group priority 1, panel weight 1 + project_business_metric.id, # group priority 0, panel weight nil (0) + project_response_metric.id, # group priority -5, panel weight nil (0) + project_system_metric.id, # group priority -10, panel weight nil (0) + ] + actual_metrics_order = all_metrics.map { |m| m[:id] || m[:metric_id] } + + expect(actual_metrics_order).to eq expected_metrics_order + end + + context 'when the dashboard should not include project metrics' do + let(:dashboard) { described_class.new(*process_params).process(insert_project_metrics: false) } + + it 'includes only dashboard metrics' do + metrics = all_metrics.map { |m| m[:id] } + + expect(metrics.length).to be(3) + expect(metrics).to eq %w(metric_b metric_a2 metric_a1) + end + end + end + + shared_examples_for 'errors with message' do |expected_message| + it 'raises a DashboardLayoutError' do + error_class = Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardLayoutError + + expect { dashboard }.to raise_error(error_class, expected_message) + end + end + + context 'when the dashboard is missing panel_groups' do + let(:dashboard_yml) { {} } + + it_behaves_like 'errors with message', 'Top-level key :panel_groups must be an array' + end + + context 'when the dashboard contains a panel_group which is missing panels' do + let(:dashboard_yml) { { panel_groups: [{}] } } + + it_behaves_like 'errors with message', 'Each "panel_group" must define an array :panels' + end + + context 'when the dashboard contains a panel which is missing metrics' do + let(:dashboard_yml) { { panel_groups: [{ panels: [{}] }] } } + + it_behaves_like 'errors with message', 'Each "panel" must define an array :metrics' + end + end + + private + + def all_metrics + dashboard[:panel_groups].map do |group| + group[:panels].map { |panel| panel[:metrics] } + end.flatten + end + + def get_metric_details(metric) + { + query_range: metric.query, + unit: metric.unit, + label: metric.legend, + metric_id: metric.id + } + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb b/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb new file mode 100644 index 00000000000..162beb0268a --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Gitlab::Metrics::Dashboard::ProjectDashboardService, :use_clean_rails_memory_store_caching do + include MetricsDashboardHelpers + + set(:user) { build(:user) } + set(:project) { build(:project) } + set(:environment) { build(:environment, project: project) } + + before do + project.add_maintainer(user) + end + + describe 'get_dashboard' do + let(:dashboard_path) { '.gitlab/dashboards/test.yml' } + let(:service_params) { [project, user, { environment: environment, dashboard_path: dashboard_path }] } + let(:service_call) { described_class.new(*service_params).get_dashboard } + + context 'when the dashboard does not exist' do + it_behaves_like 'misconfigured dashboard service response', :not_found + end + + context 'when the dashboard exists' do + let(:project) { project_with_dashboard(dashboard_path) } + + it_behaves_like 'valid dashboard service response' + + it 'caches the unprocessed dashboard for subsequent calls' do + expect_any_instance_of(described_class) + .to receive(:get_raw_dashboard) + .once + .and_call_original + + described_class.new(*service_params).get_dashboard + described_class.new(*service_params).get_dashboard + end + + context 'and the dashboard is then deleted' do + it 'does not return the previously cached dashboard' do + described_class.new(*service_params).get_dashboard + + delete_project_dashboard(project, user, dashboard_path) + + expect_any_instance_of(described_class) + .to receive(:get_raw_dashboard) + .once + .and_call_original + + described_class.new(*service_params).get_dashboard + end + end + end + + context 'when the dashboard is configured incorrectly' do + let(:project) { project_with_dashboard(dashboard_path, {}) } + + it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity + end + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb b/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb new file mode 100644 index 00000000000..e71ce2481a3 --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Metrics::Dashboard::SystemDashboardService, :use_clean_rails_memory_store_caching do + include MetricsDashboardHelpers + + set(:project) { build(:project) } + set(:environment) { build(:environment, project: project) } + + describe 'get_dashboard' do + let(:dashboard_path) { described_class::SYSTEM_DASHBOARD_PATH } + let(:service_params) { [project, nil, { environment: environment, dashboard_path: dashboard_path }] } + let(:service_call) { described_class.new(*service_params).get_dashboard } + + it_behaves_like 'valid dashboard service response' + + it 'caches the unprocessed dashboard for subsequent calls' do + expect(YAML).to receive(:safe_load).once.and_call_original + + described_class.new(*service_params).get_dashboard + described_class.new(*service_params).get_dashboard + end + + context 'when called with a non-system dashboard' do + let(:dashboard_path) { 'garbage/dashboard/path' } + + # We want to alwaus return the system dashboard. + it_behaves_like 'valid dashboard service response' + end + end +end diff --git a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb new file mode 100644 index 00000000000..f4a6e1fc7d9 --- /dev/null +++ b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Metrics::Samplers::PumaSampler do + subject { described_class.new(5) } + let(:null_metric) { double('null_metric', set: nil, observe: nil) } + + before do + allow(Gitlab::Metrics::NullMetric).to receive(:instance).and_return(null_metric) + end + + describe '#sample' do + before do + expect(subject).to receive(:puma_stats).and_return(puma_stats) + end + + context 'in cluster mode' do + let(:puma_stats) do + <<~EOS + { + "workers": 2, + "phase": 2, + "booted_workers": 2, + "old_workers": 0, + "worker_status": [{ + "pid": 32534, + "index": 0, + "phase": 1, + "booted": true, + "last_checkin": "2019-05-15T07:57:55Z", + "last_status": { + "backlog":0, + "running":1, + "pool_capacity":4, + "max_threads": 4 + } + }] + } + EOS + end + + it 'samples master statistics' do + labels = { worker: 'master' } + + expect(subject.metrics[:puma_workers]).to receive(:set).with(labels, 2) + expect(subject.metrics[:puma_running_workers]).to receive(:set).with(labels, 2) + expect(subject.metrics[:puma_stale_workers]).to receive(:set).with(labels, 0) + expect(subject.metrics[:puma_phase]).to receive(:set).once.with(labels, 2) + expect(subject.metrics[:puma_phase]).to receive(:set).once.with({ worker: 'worker_0' }, 1) + + subject.sample + end + + it 'samples worker statistics' do + labels = { worker: 'worker_0' } + + expect_worker_stats(labels) + + subject.sample + end + end + + context 'with empty worker stats' do + let(:puma_stats) do + <<~EOS + { + "workers": 2, + "phase": 2, + "booted_workers": 2, + "old_workers": 0, + "worker_status": [{ + "pid": 32534, + "index": 0, + "phase": 1, + "booted": true, + "last_checkin": "2019-05-15T07:57:55Z", + "last_status": {} + }] + } + EOS + end + + it 'does not log worker stats' do + expect(subject).not_to receive(:set_worker_metrics) + + subject.sample + end + end + + context 'in single mode' do + let(:puma_stats) do + <<~EOS + { + "backlog":0, + "running":1, + "pool_capacity":4, + "max_threads": 4 + } + EOS + end + + it 'samples worker statistics' do + labels = {} + + expect(subject.metrics[:puma_workers]).to receive(:set).with(labels, 1) + expect(subject.metrics[:puma_running_workers]).to receive(:set).with(labels, 1) + expect_worker_stats(labels) + + subject.sample + end + end + end + + def expect_worker_stats(labels) + expect(subject.metrics[:puma_queued_connections]).to receive(:set).with(labels, 0) + expect(subject.metrics[:puma_active_connections]).to receive(:set).with(labels, 0) + expect(subject.metrics[:puma_running]).to receive(:set).with(labels, 1) + expect(subject.metrics[:puma_pool_capacity]).to receive(:set).with(labels, 4) + expect(subject.metrics[:puma_max_threads]).to receive(:set).with(labels, 4) + expect(subject.metrics[:puma_idle_threads]).to receive(:set).with(labels, 1) + end +end diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb index 7972ff253fe..aaf8c9fa2a0 100644 --- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb @@ -10,17 +10,20 @@ describe Gitlab::Metrics::Samplers::RubySampler do describe '#sample' do it 'samples various statistics' do - expect(Gitlab::Metrics::System).to receive(:memory_usage) + expect(Gitlab::Metrics::System).to receive(:cpu_time) expect(Gitlab::Metrics::System).to receive(:file_descriptor_count) + expect(Gitlab::Metrics::System).to receive(:memory_usage) + expect(Gitlab::Metrics::System).to receive(:process_start_time) + expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors) expect(sampler).to receive(:sample_gc) sampler.sample end - it 'adds a metric containing the memory usage' do + it 'adds a metric containing the process resident memory bytes' do expect(Gitlab::Metrics::System).to receive(:memory_usage).and_return(9000) - expect(sampler.metrics[:memory_usage]).to receive(:set).with({}, 9000) + expect(sampler.metrics[:process_resident_memory_bytes]).to receive(:set).with({}, 9000) sampler.sample end @@ -34,6 +37,27 @@ describe Gitlab::Metrics::Samplers::RubySampler do sampler.sample end + it 'adds a metric containing the process total cpu time' do + expect(Gitlab::Metrics::System).to receive(:cpu_time).and_return(0.51) + expect(sampler.metrics[:process_cpu_seconds_total]).to receive(:set).with({}, 0.51) + + sampler.sample + end + + it 'adds a metric containing the process start time' do + expect(Gitlab::Metrics::System).to receive(:process_start_time).and_return(12345) + expect(sampler.metrics[:process_start_time_seconds]).to receive(:set).with({}, 12345) + + sampler.sample + end + + it 'adds a metric containing the process max file descriptors' do + expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors).and_return(1024) + expect(sampler.metrics[:process_max_fds]).to receive(:set).with({}, 1024) + + sampler.sample + end + it 'clears any GC profiles' do expect(GC::Profiler).to receive(:clear) diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb index 4b03f3c2532..090e456644f 100644 --- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb @@ -39,8 +39,8 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do it 'updates metrics type unix and with addr' do labels = { socket_type: 'unix', socket_address: socket_address } - expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active') - expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued') + expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active') + expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued') subject.sample end @@ -50,7 +50,6 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do context 'unicorn listens on tcp sockets' do let(:tcp_socket_address) { '0.0.0.0:8080' } let(:tcp_sockets) { [tcp_socket_address] } - before do allow(unicorn).to receive(:listener_names).and_return(tcp_sockets) end @@ -71,13 +70,29 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do it 'updates metrics type unix and with addr' do labels = { socket_type: 'tcp', socket_address: tcp_socket_address } - expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active') - expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued') + expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active') + expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued') subject.sample end end end + + context 'additional metrics' do + let(:unicorn_workers) { 2 } + + before do + allow(unicorn).to receive(:listener_names).and_return([""]) + allow(::Gitlab::Metrics::System).to receive(:cpu_time).and_return(3.14) + allow(subject).to receive(:unicorn_workers_count).and_return(unicorn_workers) + end + + it "sets additional metrics" do + expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, unicorn_workers) + + subject.sample + end + end end describe '#start' do diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb index 14afcdf5daa..b0603d96eb2 100644 --- a/spec/lib/gitlab/metrics/system_spec.rb +++ b/spec/lib/gitlab/metrics/system_spec.rb @@ -13,6 +13,18 @@ describe Gitlab::Metrics::System do expect(described_class.file_descriptor_count).to be > 0 end end + + describe '.max_open_file_descriptors' do + it 'returns the max allowed open file descriptors' do + expect(described_class.max_open_file_descriptors).to be > 0 + end + end + + describe '.process_start_time' do + it 'returns the process start time' do + expect(described_class.process_start_time).to be > 0 + end + end else describe '.memory_usage' do it 'returns 0.0' do @@ -25,6 +37,18 @@ describe Gitlab::Metrics::System do expect(described_class.file_descriptor_count).to eq(0) end end + + describe '.max_open_file_descriptors' do + it 'returns 0' do + expect(described_class.max_open_file_descriptors).to eq(0) + end + end + + describe 'process_start_time' do + it 'returns 0' do + expect(described_class.process_start_time).to eq(0) + end + end end describe '.cpu_time' do diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb new file mode 100644 index 00000000000..e70fde09edd --- /dev/null +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -0,0 +1,229 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Metrics::Transaction do + let(:transaction) { described_class.new } + let(:metric) { transaction.metrics[0] } + + let(:sensitive_tags) do + { + path: 'private', + branch: 'sensitive' + } + end + + shared_examples 'tag filter' do |sane_tags| + it 'filters potentially sensitive tags' do + expect(metric.tags).to eq(sane_tags) + end + end + + describe '#duration' do + it 'returns the duration of a transaction in seconds' do + transaction.run { } + + expect(transaction.duration).to be > 0 + end + end + + describe '#allocated_memory' do + it 'returns the allocated memory in bytes' do + transaction.run { 'a' * 32 } + + expect(transaction.allocated_memory).to be_a_kind_of(Numeric) + end + end + + describe '#run' do + it 'yields the supplied block' do + expect { |b| transaction.run(&b) }.to yield_control + end + + it 'stores the transaction in the current thread' do + transaction.run do + expect(described_class.current).to eq(transaction) + end + end + + it 'removes the transaction from the current thread upon completion' do + transaction.run { } + + expect(described_class.current).to be_nil + end + end + + describe '#add_metric' do + it 'adds a metric to the transaction' do + transaction.add_metric('foo', value: 1) + + expect(metric.series).to eq('rails_foo') + expect(metric.tags).to eq({}) + expect(metric.values).to eq(value: 1) + end + + context 'with sensitive tags' do + before do + transaction + .add_metric('foo', { value: 1 }, **sensitive_tags.merge(sane: 'yes')) + end + + it_behaves_like 'tag filter', sane: 'yes' + end + end + + describe '#method_call_for' do + it 'returns a MethodCall' do + method = transaction.method_call_for('Foo#bar', :Foo, '#bar') + + expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall) + end + end + + describe '#increment' do + it 'increments a counter' do + transaction.increment(:time, 1) + transaction.increment(:time, 2) + + values = metric_values(time: 3) + + expect(transaction).to receive(:add_metric) + .with('transactions', values, {}) + + transaction.track_self + end + end + + describe '#set' do + it 'sets a value' do + transaction.set(:number, 10) + + values = metric_values(number: 10) + + expect(transaction).to receive(:add_metric) + .with('transactions', values, {}) + + transaction.track_self + end + end + + describe '#finish' do + it 'tracks the transaction details and submits them to Sidekiq' do + expect(transaction).to receive(:track_self) + expect(transaction).to receive(:submit) + + transaction.finish + end + end + + describe '#track_self' do + it 'adds a metric for the transaction itself' do + values = metric_values + + expect(transaction).to receive(:add_metric) + .with('transactions', values, {}) + + transaction.track_self + end + end + + describe '#submit' do + it 'submits the metrics to Sidekiq' do + transaction.track_self + + expect(Gitlab::Metrics).to receive(:submit_metrics) + .with([an_instance_of(Hash)]) + + transaction.submit + end + + it 'adds the action as a tag for every metric' do + allow(transaction) + .to receive(:labels) + .and_return(controller: 'Foo', action: 'bar') + + transaction.track_self + + hash = { + series: 'rails_transactions', + tags: { action: 'Foo#bar' }, + values: metric_values, + timestamp: a_kind_of(Integer) + } + + expect(Gitlab::Metrics).to receive(:submit_metrics) + .with([hash]) + + transaction.submit + end + + it 'does not add an action tag for events' do + allow(transaction) + .to receive(:labels) + .and_return(controller: 'Foo', action: 'bar') + + transaction.add_event(:meow) + + hash = { + series: 'events', + tags: { event: :meow }, + values: { count: 1 }, + timestamp: a_kind_of(Integer) + } + + expect(Gitlab::Metrics).to receive(:submit_metrics) + .with([hash]) + + transaction.submit + end + end + + describe '#add_event' do + it 'adds a metric' do + transaction.add_event(:meow) + + expect(metric).to be_an_instance_of(Gitlab::Metrics::Metric) + end + + it "does not prefix the metric's series name" do + transaction.add_event(:meow) + + expect(metric.series).to eq(described_class::EVENT_SERIES) + end + + it 'tracks a counter for every event' do + transaction.add_event(:meow) + + expect(metric.values).to eq(count: 1) + end + + it 'tracks the event name' do + transaction.add_event(:meow) + + expect(metric.tags).to eq(event: :meow) + end + + it 'allows tracking of custom tags' do + transaction.add_event(:meow, animal: 'cat') + + expect(metric.tags).to eq(event: :meow, animal: 'cat') + end + + context 'with sensitive tags' do + before do + transaction.add_event(:meow, **sensitive_tags.merge(sane: 'yes')) + end + + it_behaves_like 'tag filter', event: :meow, sane: 'yes' + end + end + + private + + def metric_values(opts = {}) + { + duration: 0.0, + allocated_memory: a_kind_of(Numeric) + }.merge(opts) + end +end diff --git a/spec/lib/gitlab/middleware/basic_health_check_spec.rb b/spec/lib/gitlab/middleware/basic_health_check_spec.rb index 187d903a5e1..86bdc479b66 100644 --- a/spec/lib/gitlab/middleware/basic_health_check_spec.rb +++ b/spec/lib/gitlab/middleware/basic_health_check_spec.rb @@ -28,6 +28,35 @@ describe Gitlab::Middleware::BasicHealthCheck do end end + context 'with X-Forwarded-For headers' do + let(:load_balancer_ip) { '1.2.3.4' } + + before do + env['HTTP_X_FORWARDED_FOR'] = "#{load_balancer_ip}, 127.0.0.1" + env['REMOTE_ADDR'] = '127.0.0.1' + env['PATH_INFO'] = described_class::HEALTH_PATH + end + + it 'returns 200 response when endpoint is allowed' do + allow(Settings.monitoring).to receive(:ip_whitelist).and_return([load_balancer_ip]) + expect(app).not_to receive(:call) + + response = middleware.call(env) + + expect(response[0]).to eq(200) + expect(response[1]).to eq({ 'Content-Type' => 'text/plain' }) + expect(response[2]).to eq(['GitLab OK']) + end + + it 'returns 404 when whitelist is not configured' do + allow(Settings.monitoring).to receive(:ip_whitelist).and_return([]) + + response = middleware.call(env) + + expect(response[0]).to eq(404) + end + end + context 'whitelisted IP' do before do env['REMOTE_ADDR'] = '127.0.0.1' diff --git a/spec/lib/gitlab/namespaced_session_store_spec.rb b/spec/lib/gitlab/namespaced_session_store_spec.rb new file mode 100644 index 00000000000..c0af2ede32a --- /dev/null +++ b/spec/lib/gitlab/namespaced_session_store_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::NamespacedSessionStore do + let(:key) { :some_key } + subject { described_class.new(key) } + + it 'stores data under the specified key' do + Gitlab::Session.with_session({}) do + subject[:new_data] = 123 + + expect(Thread.current[:session_storage][key]).to eq(new_data: 123) + end + end + + it 'retrieves data from the given key' do + Thread.current[:session_storage] = { key => { existing_data: 123 } } + + expect(subject[:existing_data]).to eq 123 + end +end diff --git a/spec/lib/gitlab/object_hierarchy_spec.rb b/spec/lib/gitlab/object_hierarchy_spec.rb index 4700a7ad2e1..e6e9ae3223e 100644 --- a/spec/lib/gitlab/object_hierarchy_spec.rb +++ b/spec/lib/gitlab/object_hierarchy_spec.rb @@ -81,6 +81,24 @@ describe Gitlab::ObjectHierarchy, :postgresql do expect { relation.update_all(share_with_group_lock: false) } .to raise_error(ActiveRecord::ReadOnlyRecord) end + + context 'when with_depth is true' do + let(:relation) do + described_class.new(Group.where(id: parent.id)).base_and_descendants(with_depth: true) + end + + it 'includes depth in the results' do + object_depths = { + parent.id => 1, + child1.id => 2, + child2.id => 3 + } + + relation.each do |object| + expect(object.depth).to eq(object_depths[object.id]) + end + end + end end describe '#descendants' do @@ -91,6 +109,28 @@ describe Gitlab::ObjectHierarchy, :postgresql do end end + describe '#max_descendants_depth' do + subject { described_class.new(base_relation).max_descendants_depth } + + context 'when base relation is empty' do + let(:base_relation) { Group.where(id: nil) } + + it { expect(subject).to be_nil } + end + + context 'when base has no children' do + let(:base_relation) { Group.where(id: child2) } + + it { expect(subject).to eq(1) } + end + + context 'when base has grandchildren' do + let(:base_relation) { Group.where(id: parent) } + + it { expect(subject).to eq(3) } + end + end + describe '#ancestors' do it 'includes only the ancestors' do relation = described_class.new(Group.where(id: child2)).ancestors diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb index d808b4d49e0..32296caf819 100644 --- a/spec/lib/gitlab/omniauth_initializer_spec.rb +++ b/spec/lib/gitlab/omniauth_initializer_spec.rb @@ -38,6 +38,28 @@ describe Gitlab::OmniauthInitializer do subject.execute([hash_config]) end + it 'normalizes a String strategy_class' do + hash_config = { 'name' => 'hash', 'args' => { strategy_class: 'OmniAuth::Strategies::OAuth2Generic' } } + + expect(devise_config).to receive(:omniauth).with(:hash, strategy_class: OmniAuth::Strategies::OAuth2Generic) + + subject.execute([hash_config]) + end + + it 'allows a class to be specified in strategy_class' do + hash_config = { 'name' => 'hash', 'args' => { strategy_class: OmniAuth::Strategies::OAuth2Generic } } + + expect(devise_config).to receive(:omniauth).with(:hash, strategy_class: OmniAuth::Strategies::OAuth2Generic) + + subject.execute([hash_config]) + end + + it 'throws an error for an invalid strategy_class' do + hash_config = { 'name' => 'hash', 'args' => { strategy_class: 'OmniAuth::Strategies::Bogus' } } + + expect { subject.execute([hash_config]) }.to raise_error(NameError) + end + it 'configures fail_with_empty_uid for shibboleth' do shibboleth_config = { 'name' => 'shibboleth', 'args' => {} } @@ -61,5 +83,13 @@ describe Gitlab::OmniauthInitializer do subject.execute([cas3_config]) end + + it 'configures name for openid_connect' do + openid_connect_config = { 'name' => 'openid_connect', 'args' => {} } + + expect(devise_config).to receive(:omniauth).with(:openid_connect, name: 'openid_connect') + + subject.execute([openid_connect_config]) + end end end diff --git a/spec/lib/gitlab/optimistic_locking_spec.rb b/spec/lib/gitlab/optimistic_locking_spec.rb index 81f81d4f963..6fdf61ee0a7 100644 --- a/spec/lib/gitlab/optimistic_locking_spec.rb +++ b/spec/lib/gitlab/optimistic_locking_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::OptimisticLocking do describe '#retry_lock' do it 'does not reload object if state changes' do - expect(pipeline).not_to receive(:reload) + expect(pipeline).not_to receive(:reset) expect(pipeline).to receive(:succeed).and_call_original described_class.retry_lock(pipeline) do |subject| @@ -17,7 +17,7 @@ describe Gitlab::OptimisticLocking do it 'retries action if exception is raised' do pipeline.succeed - expect(pipeline2).to receive(:reload).and_call_original + expect(pipeline2).to receive(:reset).and_call_original expect(pipeline2).to receive(:drop).twice.and_call_original described_class.retry_lock(pipeline2) do |subject| diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index e90e0aba0a4..84b2e2dc823 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -100,7 +100,7 @@ describe Gitlab::PathRegex do end let(:ee_top_level_words) do - ['unsubscribes'] + %w(unsubscribes v2) end let(:files_in_public) do @@ -120,10 +120,10 @@ describe Gitlab::PathRegex do # - Followed by one or more path-parts not starting with `:` or `*` # - Followed by a path-part that includes a wildcard parameter `*` # At the time of writing these routes match: http://rubular.com/r/Rv2pDE5Dvw - STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id} - NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*} - ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*} - WILDCARD_SEGMENT = /\*/ + STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}.freeze + NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}.freeze + ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}.freeze + WILDCARD_SEGMENT = /\*/.freeze let(:namespaced_wildcard_routes) do routes_without_format.select do |p| p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}} @@ -144,7 +144,7 @@ describe Gitlab::PathRegex do end.uniq end - STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/} + STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/}.freeze let(:group_routes) do routes_without_format.select do |path| path =~ STARTING_WITH_GROUP diff --git a/spec/lib/gitlab/phabricator_import/base_worker_spec.rb b/spec/lib/gitlab/phabricator_import/base_worker_spec.rb new file mode 100644 index 00000000000..d46d908a3e3 --- /dev/null +++ b/spec/lib/gitlab/phabricator_import/base_worker_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::PhabricatorImport::BaseWorker do + let(:subclass) do + # Creating an anonymous class for a worker is complicated, as we generate the + # queue name from the class name. + Gitlab::PhabricatorImport::ImportTasksWorker + end + + describe '.schedule' do + let(:arguments) { %w[project_id the_next_page] } + + it 'schedules the job' do + expect(subclass).to receive(:perform_async).with(*arguments) + + subclass.schedule(*arguments) + end + + it 'counts the scheduled job', :clean_gitlab_redis_shared_state do + state = Gitlab::PhabricatorImport::WorkerState.new('project_id') + + allow(subclass).to receive(:remove_job) # otherwise the job is removed before we saw it + + expect { subclass.schedule(*arguments) }.to change { state.running_count }.by(1) + end + end + + describe '#perform' do + let(:project) { create(:project, :import_started, import_url: "https://a.phab.instance") } + let(:worker) { subclass.new } + let(:state) { Gitlab::PhabricatorImport::WorkerState.new(project.id) } + + before do + allow(worker).to receive(:import) + end + + it 'does not break for a non-existing project' do + expect { worker.perform('not a thing') }.not_to raise_error + end + + it 'does not do anything when the import is not in progress' do + project = create(:project, :import_failed) + + expect(worker).not_to receive(:import) + + worker.perform(project.id) + end + + it 'calls import for the project' do + expect(worker).to receive(:import).with(project, 'other_arg') + + worker.perform(project.id, 'other_arg') + end + + it 'marks the project as imported if there was only one job running' do + worker.perform(project.id) + + expect(project.import_state.reload).to be_finished + end + + it 'does not mark the job as finished when there are more scheduled jobs' do + 2.times { state.add_job } + + worker.perform(project.id) + + expect(project.import_state.reload).to be_in_progress + end + + it 'decrements the job counter' do + expect { worker.perform(project.id) }.to change { state.running_count }.by(-1) + end + end +end diff --git a/spec/lib/gitlab/phabricator_import/cache/map_spec.rb b/spec/lib/gitlab/phabricator_import/cache/map_spec.rb new file mode 100644 index 00000000000..52c7a02219f --- /dev/null +++ b/spec/lib/gitlab/phabricator_import/cache/map_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Gitlab::PhabricatorImport::Cache::Map, :clean_gitlab_redis_cache do + set(:project) { create(:project) } + let(:redis) { Gitlab::Redis::Cache } + subject(:map) { described_class.new(project) } + + describe '#get_gitlab_model' do + it 'returns nil if there was nothing cached for the phabricator id' do + expect(map.get_gitlab_model('does not exist')).to be_nil + end + + it 'returns the object if it was set in redis' do + issue = create(:issue, project: project) + set_in_redis('exists', issue) + + expect(map.get_gitlab_model('exists')).to eq(issue) + end + + it 'extends the TTL for the cache key' do + set_in_redis('extend', create(:issue, project: project)) do |redis| + redis.expire(cache_key('extend'), 10.seconds.to_i) + end + + map.get_gitlab_model('extend') + + ttl = redis.with { |redis| redis.ttl(cache_key('extend')) } + + expect(ttl).to be > 10.seconds + end + end + + describe '#set_gitlab_model' do + around do |example| + Timecop.freeze { example.run } + end + + it 'sets the class and id in redis with a ttl' do + issue = create(:issue, project: project) + + map.set_gitlab_model(issue, 'it is set') + + set_data, ttl = redis.with do |redis| + redis.pipelined do |p| + p.mapped_hmget(cache_key('it is set'), :classname, :database_id) + p.ttl(cache_key('it is set')) + end + end + + expect(set_data).to eq({ classname: 'Issue', database_id: issue.id.to_s }) + expect(ttl).to be_within(1.second).of(StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION) + end + end + + def set_in_redis(key, object) + redis.with do |redis| + redis.mapped_hmset(cache_key(key), + { classname: object.class, database_id: object.id }) + yield(redis) if block_given? + end + end + + def cache_key(phabricator_id) + subject.__send__(:cache_key_for_phabricator_id, phabricator_id) + end +end diff --git a/spec/lib/gitlab/phabricator_import/conduit/client_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/client_spec.rb new file mode 100644 index 00000000000..542b3cd060f --- /dev/null +++ b/spec/lib/gitlab/phabricator_import/conduit/client_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::PhabricatorImport::Conduit::Client do + let(:client) do + described_class.new('https://see-ya-later.phabricator', 'api-token') + end + + describe '#get' do + it 'performs and parses a request' do + params = { some: 'extra', values: %w[are passed] } + stub_valid_request(params) + + response = client.get('test', params: params) + + expect(response).to be_a(Gitlab::PhabricatorImport::Conduit::Response) + expect(response).to be_success + end + + it 'wraps request errors in an `ApiError`' do + stub_timeout + + expect { client.get('test') }.to raise_error(Gitlab::PhabricatorImport::Conduit::ApiError) + end + + it 'raises response error' do + stub_error_response + + expect { client.get('test') } + .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /has the wrong length/) + end + end + + def stub_valid_request(params = {}) + WebMock.stub_request( + :get, 'https://see-ya-later.phabricator/api/test' + ).with( + body: CGI.unescape(params.reverse_merge('api.token' => 'api-token').to_query) + ).and_return( + status: 200, + body: fixture_file('phabricator_responses/maniphest.search.json') + ) + end + + def stub_timeout + WebMock.stub_request( + :get, 'https://see-ya-later.phabricator/api/test' + ).to_timeout + end + + def stub_error_response + WebMock.stub_request( + :get, 'https://see-ya-later.phabricator/api/test' + ).and_return( + status: 200, + body: fixture_file('phabricator_responses/auth_failed.json') + ) + end +end diff --git a/spec/lib/gitlab/phabricator_import/conduit/maniphest_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/maniphest_spec.rb new file mode 100644 index 00000000000..0d7714649b9 --- /dev/null +++ b/spec/lib/gitlab/phabricator_import/conduit/maniphest_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::PhabricatorImport::Conduit::Maniphest do + let(:maniphest) do + described_class.new(phabricator_url: 'https://see-ya-later.phabricator', api_token: 'api-token') + end + + describe '#tasks' do + let(:fake_client) { double('Phabricator client') } + + before do + allow(maniphest).to receive(:client).and_return(fake_client) + end + + it 'calls the api with the correct params' do + expected_params = { + after: '123', + attachments: { + projects: 1, subscribers: 1, columns: 1 + } + } + + expect(fake_client).to receive(:get).with('maniphest.search', + params: expected_params) + + maniphest.tasks(after: '123') + end + + it 'returns a parsed response' do + response = Gitlab::PhabricatorImport::Conduit::Response + .new(fixture_file('phabricator_responses/maniphest.search.json')) + + allow(fake_client).to receive(:get).and_return(response) + + expect(maniphest.tasks).to be_a(Gitlab::PhabricatorImport::Conduit::TasksResponse) + end + end +end diff --git a/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb new file mode 100644 index 00000000000..a8596968f14 --- /dev/null +++ b/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::PhabricatorImport::Conduit::Response do + let(:response) { described_class.new(JSON.parse(fixture_file('phabricator_responses/maniphest.search.json')))} + let(:error_response) { described_class.new(JSON.parse(fixture_file('phabricator_responses/auth_failed.json'))) } + + describe '.parse!' do + it 'raises a ResponseError if the http response was not successfull' do + fake_response = double(:http_response, success?: false, status: 401) + + expect { described_class.parse!(fake_response) } + .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /responded with 401/) + end + + it 'raises a ResponseError if the response contained a Phabricator error' do + fake_response = double(:http_response, + success?: true, + status: 200, + body: fixture_file('phabricator_responses/auth_failed.json')) + + expect { described_class.parse!(fake_response) } + .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /ERR-INVALID-AUTH: API token/) + end + + it 'raises a ResponseError if JSON parsing failed' do + fake_response = double(:http_response, + success?: true, + status: 200, + body: 'This is no JSON') + + expect { described_class.parse!(fake_response) } + .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /unexpected token at/) + end + + it 'returns a parsed response for valid input' do + fake_response = double(:http_response, + success?: true, + status: 200, + body: fixture_file('phabricator_responses/maniphest.search.json')) + + expect(described_class.parse!(fake_response)).to be_a(described_class) + end + end + + describe '#success?' do + it { expect(response).to be_success } + it { expect(error_response).not_to be_success } + end + + describe '#error_code' do + it { expect(error_response.error_code).to eq('ERR-INVALID-AUTH') } + it { expect(response.error_code).to be_nil } + end + + describe '#error_info' do + it 'returns the correct error info' do + expected_message = 'API token "api-token" has the wrong length. API tokens should be 32 characters long.' + + expect(error_response.error_info).to eq(expected_message) + end + + it { expect(response.error_info).to be_nil } + end + + describe '#data' do + it { expect(error_response.data).to be_nil } + it { expect(response.data).to be_an(Array) } + end + + describe '#pagination' do + it { expect(error_response.pagination).to be_nil } + + it 'builds the pagination correctly' do + expect(response.pagination).to be_a(Gitlab::PhabricatorImport::Conduit::Pagination) + expect(response.pagination.next_page).to eq('284') + end + end +end diff --git a/spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb new file mode 100644 index 00000000000..4b4c2a6276e --- /dev/null +++ b/spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::PhabricatorImport::Conduit::TasksResponse do + let(:conduit_response) do + Gitlab::PhabricatorImport::Conduit::Response + .new(JSON.parse(fixture_file('phabricator_responses/maniphest.search.json'))) + end + + subject(:response) { described_class.new(conduit_response) } + + describe '#pagination' do + it 'delegates to the conduit reponse' do + expect(response.pagination).to eq(conduit_response.pagination) + end + end + + describe '#tasks' do + it 'builds the correct tasks representation' do + tasks = response.tasks + + titles = tasks.map(&:issue_attributes).map { |attrs| attrs[:title] } + + expect(titles).to contain_exactly('Things are slow', 'Things are broken') + end + end +end diff --git a/spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb b/spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb new file mode 100644 index 00000000000..1e38ef8aaa5 --- /dev/null +++ b/spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::PhabricatorImport::ImportTasksWorker do + describe '#perform' do + it 'calls the correct importer' do + project = create(:project, :import_started, import_url: "https://the.phab.ulr") + fake_importer = instance_double(Gitlab::PhabricatorImport::Issues::Importer) + + expect(Gitlab::PhabricatorImport::Issues::Importer).to receive(:new).with(project).and_return(fake_importer) + expect(fake_importer).to receive(:execute) + + described_class.new.perform(project.id) + end + end +end diff --git a/spec/lib/gitlab/phabricator_import/importer_spec.rb b/spec/lib/gitlab/phabricator_import/importer_spec.rb new file mode 100644 index 00000000000..bf14010a187 --- /dev/null +++ b/spec/lib/gitlab/phabricator_import/importer_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Gitlab::PhabricatorImport::Importer do + it { expect(described_class).to be_async } + + it "acts like it's importing repositories" do + expect(described_class).to be_imports_repository + end + + describe '#execute' do + let(:project) { create(:project, :import_scheduled) } + subject(:importer) { described_class.new(project) } + + it 'sets a custom jid that will be kept up to date' do + expect { importer.execute }.to change { project.import_state.reload.jid } + end + + it 'starts importing tasks' do + expect(Gitlab::PhabricatorImport::ImportTasksWorker).to receive(:schedule).with(project.id) + + importer.execute + end + + it 'marks the import as failed when something goes wrong' do + allow(importer).to receive(:schedule_first_tasks_page).and_raise('Stuff is broken') + + importer.execute + + expect(project.import_state).to be_failed + end + end +end diff --git a/spec/lib/gitlab/phabricator_import/issues/importer_spec.rb b/spec/lib/gitlab/phabricator_import/issues/importer_spec.rb new file mode 100644 index 00000000000..2412cf76f79 --- /dev/null +++ b/spec/lib/gitlab/phabricator_import/issues/importer_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::PhabricatorImport::Issues::Importer do + set(:project) { create(:project) } + + let(:response) do + Gitlab::PhabricatorImport::Conduit::TasksResponse.new( + Gitlab::PhabricatorImport::Conduit::Response + .new(JSON.parse(fixture_file('phabricator_responses/maniphest.search.json'))) + ) + end + + subject(:importer) { described_class.new(project, nil) } + + before do + client = instance_double(Gitlab::PhabricatorImport::Conduit::Maniphest) + + allow(client).to receive(:tasks).and_return(response) + allow(importer).to receive(:client).and_return(client) + end + + describe '#execute' do + it 'imports each task in the response' do + response.tasks.each do |task| + task_importer = instance_double(Gitlab::PhabricatorImport::Issues::TaskImporter) + + expect(task_importer).to receive(:execute) + expect(Gitlab::PhabricatorImport::Issues::TaskImporter) + .to receive(:new).with(project, task) + .and_return(task_importer) + end + + importer.execute + end + + it 'schedules the next batch if there is one' do + expect(Gitlab::PhabricatorImport::ImportTasksWorker) + .to receive(:schedule).with(project.id, response.pagination.next_page) + + importer.execute + end + + it 'does not reschedule when there is no next page' do + allow(response.pagination).to receive(:has_next_page?).and_return(false) + + expect(Gitlab::PhabricatorImport::ImportTasksWorker) + .not_to receive(:schedule) + + importer.execute + end + end +end diff --git a/spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb b/spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb new file mode 100644 index 00000000000..1625604e754 --- /dev/null +++ b/spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::PhabricatorImport::Issues::TaskImporter do + set(:project) { create(:project) } + let(:task) do + Gitlab::PhabricatorImport::Representation::Task.new( + { + 'phid' => 'the-phid', + 'fields' => { + 'name' => 'Title', + 'description' => { + 'raw' => '# This is markdown\n it can contain more text.' + }, + 'dateCreated' => '1518688921', + 'dateClosed' => '1518789995' + } + } + ) + end + + describe '#execute' do + it 'creates the issue with the expected attributes' do + issue = described_class.new(project, task).execute + + expect(issue.project).to eq(project) + expect(issue).to be_persisted + expect(issue.author).to eq(User.ghost) + expect(issue.title).to eq('Title') + expect(issue.description).to eq('# This is markdown\n it can contain more text.') + expect(issue).to be_closed + expect(issue.created_at).to eq(Time.at(1518688921)) + expect(issue.closed_at).to eq(Time.at(1518789995)) + end + + it 'does not recreate the issue when called multiple times' do + expect { described_class.new(project, task).execute } + .to change { project.issues.reload.size }.from(0).to(1) + expect { described_class.new(project, task).execute } + .not_to change { project.issues.reload.size } + end + + it 'does not trigger a save when the object did not change' do + existing_issue = create(:issue, + task.issue_attributes.merge(author: User.ghost)) + importer = described_class.new(project, task) + allow(importer).to receive(:issue).and_return(existing_issue) + + expect(existing_issue).not_to receive(:save!) + + importer.execute + end + end +end diff --git a/spec/lib/gitlab/phabricator_import/project_creator_spec.rb b/spec/lib/gitlab/phabricator_import/project_creator_spec.rb new file mode 100644 index 00000000000..e9455b866ac --- /dev/null +++ b/spec/lib/gitlab/phabricator_import/project_creator_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::PhabricatorImport::ProjectCreator do + let(:user) { create(:user) } + let(:params) do + { path: 'new-phab-import', + phabricator_server_url: 'http://phab.example.com', + api_token: 'the-token' } + end + subject(:creator) { described_class.new(user, params) } + + describe '#execute' do + it 'creates a project correctly and schedule an import' do + expect_next_instance_of(Gitlab::PhabricatorImport::Importer) do |importer| + expect(importer).to receive(:execute) + end + + project = creator.execute + + expect(project).to be_persisted + expect(project).to be_import + expect(project.import_type).to eq('phabricator') + expect(project.import_data.credentials).to match(a_hash_including(api_token: 'the-token')) + expect(project.import_data.data).to match(a_hash_including('phabricator_url' => 'http://phab.example.com')) + expect(project.import_url).to eq(Project::UNKNOWN_IMPORT_URL) + expect(project.namespace).to eq(user.namespace) + end + + context 'when import params are missing' do + let(:params) do + { path: 'new-phab-import', + phabricator_server_url: 'http://phab.example.com', + api_token: '' } + end + + it 'returns nil' do + expect(creator.execute).to be_nil + end + end + + context 'when import params are invalid' do + let(:params) do + { path: 'new-phab-import', + namespace_id: '-1', + phabricator_server_url: 'http://phab.example.com', + api_token: 'the-token' } + end + + it 'returns an unpersisted project' do + project = creator.execute + + expect(project).not_to be_persisted + expect(project).not_to be_valid + end + end + end +end diff --git a/spec/lib/gitlab/phabricator_import/representation/task_spec.rb b/spec/lib/gitlab/phabricator_import/representation/task_spec.rb new file mode 100644 index 00000000000..dfbd8c546eb --- /dev/null +++ b/spec/lib/gitlab/phabricator_import/representation/task_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Gitlab::PhabricatorImport::Representation::Task do + subject(:task) do + described_class.new( + { + 'phid' => 'the-phid', + 'fields' => { + 'name' => 'Title'.ljust(257, '.'), # A string padded to 257 chars + 'description' => { + 'raw' => '# This is markdown\n it can contain more text.' + }, + 'dateCreated' => '1518688921', + 'dateClosed' => '1518789995' + } + } + ) + end + + describe '#issue_attributes' do + it 'contains the expected values' do + expected_attributes = { + title: 'Title'.ljust(255, '.'), + description: '# This is markdown\n it can contain more text.', + state: :closed, + created_at: Time.at(1518688921), + closed_at: Time.at(1518789995) + } + + expect(task.issue_attributes).to eq(expected_attributes) + end + end +end diff --git a/spec/lib/gitlab/phabricator_import/worker_state_spec.rb b/spec/lib/gitlab/phabricator_import/worker_state_spec.rb new file mode 100644 index 00000000000..a44947445c9 --- /dev/null +++ b/spec/lib/gitlab/phabricator_import/worker_state_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::PhabricatorImport::WorkerState, :clean_gitlab_redis_shared_state do + subject(:state) { described_class.new('weird-project-id') } + let(:key) { 'phabricator-import/jobs/project-weird-project-id/job-count' } + + describe '#add_job' do + it 'increments the counter for jobs' do + set_value(3) + + expect { state.add_job }.to change { get_value }.from('3').to('4') + end + end + + describe '#remove_job' do + it 'decrements the counter for jobs' do + set_value(3) + + expect { state.remove_job }.to change { get_value }.from('3').to('2') + end + end + + describe '#running_count' do + it 'reads the value' do + set_value(9) + + expect(state.running_count).to eq(9) + end + + it 'returns 0 when nothing was set' do + expect(state.running_count).to eq(0) + end + end + + def set_value(value) + redis.with { |r| r.set(key, value) } + end + + def get_value + redis.with { |r| r.get(key) } + end + + def redis + Gitlab::Redis::SharedState + end +end diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb index 8bb0c1a0b8a..5af52db7a1f 100644 --- a/spec/lib/gitlab/profiler_spec.rb +++ b/spec/lib/gitlab/profiler_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe Gitlab::Profiler do - RSpec::Matchers.define_negated_matcher :not_change, :change - let(:null_logger) { Logger.new('/dev/null') } let(:private_token) { 'private' } @@ -29,13 +27,13 @@ describe Gitlab::Profiler do it 'sends a POST request when data is passed' do post_data = '{"a":1}' - expect(app).to receive(:post).with(anything, post_data, anything) + expect(app).to receive(:post).with(anything, params: post_data, headers: anything) described_class.profile('/', post_data: post_data) end it 'uses the private_token for auth if given' do - expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token) + expect(app).to receive(:get).with('/', params: nil, headers: { 'Private-Token' => private_token }) expect(app).to receive(:get).with('/api/v4/users') described_class.profile('/', private_token: private_token) @@ -53,7 +51,7 @@ describe Gitlab::Profiler do user = double(:user) expect(described_class).to receive(:with_user).with(nil).and_call_original - expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token) + expect(app).to receive(:get).with('/', params: nil, headers: { 'Private-Token' => private_token }) expect(app).to receive(:get).with('/api/v4/users') described_class.profile('/', user: user, private_token: private_token) @@ -187,7 +185,7 @@ describe Gitlab::Profiler do end it 'does not modify the standard Rails loggers' do - expect { described_class.with_custom_logger(nil) { } } + expect { described_class.with_custom_logger(nil) {} } .to not_change { ActiveRecord::Base.logger } .and not_change { ActionController::Base.logger } .and not_change { ActiveSupport::LogSubscriber.colorize_logging } @@ -204,7 +202,7 @@ describe Gitlab::Profiler do end it 'cleans up ApplicationController afterwards' do - expect { described_class.with_user(user) { } } + expect { described_class.with_user(user) {} } .to not_change { ActionController.instance_methods(false) } end end @@ -213,7 +211,7 @@ describe Gitlab::Profiler do it 'does not define methods on ApplicationController' do expect(ApplicationController).not_to receive(:define_method) - described_class.with_user(nil) { } + described_class.with_user(nil) {} end end end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 6831274d37c..4a41d5cf51e 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -412,4 +412,36 @@ describe Gitlab::ProjectSearchResults do end end end + + describe 'user search' do + it 'returns the user belonging to the project matching the search query' do + project = create(:project) + + user1 = create(:user, username: 'gob_bluth') + create(:project_member, :developer, user: user1, project: project) + + user2 = create(:user, username: 'michael_bluth') + create(:project_member, :developer, user: user2, project: project) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, project, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'returns the user belonging to the group matching the search query' do + group = create(:group) + project = create(:project, namespace: group) + + user1 = create(:user, username: 'gob_bluth') + create(:group_member, :developer, user: user1, group: group) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, project, 'gob').objects('users') + + expect(result).to eq [user1] + end + end end diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb index 0cee100b64e..8c2fc048a54 100644 --- a/spec/lib/gitlab/project_template_spec.rb +++ b/spec/lib/gitlab/project_template_spec.rb @@ -7,7 +7,10 @@ describe Gitlab::ProjectTemplate do described_class.new('rails', 'Ruby on Rails', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/rails'), described_class.new('spring', 'Spring', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/spring'), described_class.new('express', 'NodeJS Express', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/express'), - described_class.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore'), + described_class.new('iosswift', 'iOS (Swift)', 'A ready-to-go template for use with iOS Swift apps.', 'https://gitlab.com/gitlab-org/project-templates/iosswift'), + described_class.new('dotnetcore', '.NET Core', 'A .NET Core console application template, customizable for any .NET Core project', 'https://gitlab.com/gitlab-org/project-templates/dotnetcore'), + described_class.new('android', 'Android', 'A ready-to-go template for use with Android apps.', 'https://gitlab.com/gitlab-org/project-templates/android'), + described_class.new('gomicro', 'Go Micro', 'Go Micro is a framework for micro service development.', 'https://gitlab.com/gitlab-org/project-templates/go-micro'), described_class.new('hugo', 'Pages/Hugo', 'Everything you need to get started using a Hugo Pages site.', 'https://gitlab.com/pages/hugo'), described_class.new('jekyll', 'Pages/Jekyll', 'Everything you need to get started using a Jekyll Pages site.', 'https://gitlab.com/pages/jekyll'), described_class.new('plainhtml', 'Pages/Plain HTML', 'Everything you need to get started using a plain HTML Pages site.', 'https://gitlab.com/pages/plain-html'), diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb index 5a88b23aa82..a6589f0c0a3 100644 --- a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb @@ -9,9 +9,35 @@ describe Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery do let(:query_params) { [environment.id] } it 'queries using specific time' do - expect(client).to receive(:query_range).with(anything, start: 8.hours.ago.to_f, stop: Time.now.to_f) - + expect(client).to receive(:query_range) + .with(anything, start: 8.hours.ago.to_f, stop: Time.now.to_f) expect(query_result).not_to be_nil end + + context 'when start and end time parameters are provided' do + let(:query_params) { [environment.id, start_time, end_time] } + + context 'as unix timestamps' do + let(:start_time) { 4.hours.ago.to_f } + let(:end_time) { 2.hours.ago.to_f } + + it 'queries using the provided times' do + expect(client).to receive(:query_range) + .with(anything, start: start_time, stop: end_time) + expect(query_result).not_to be_nil + end + end + + context 'as Date/Time objects' do + let(:start_time) { 4.hours.ago } + let(:end_time) { 2.hours.ago } + + it 'queries using the provided times converted to unix' do + expect(client).to receive(:query_range) + .with(anything, start: start_time.to_f, stop: end_time.to_f) + expect(query_result).not_to be_nil + end + end + end end end diff --git a/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb new file mode 100644 index 00000000000..7f6283715f2 --- /dev/null +++ b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Prometheus::Queries::KnativeInvocationQuery do + include PrometheusHelpers + + let(:project) { create(:project) } + let(:serverless_func) { Serverless::Function.new(project, 'test-name', 'test-ns') } + + let(:client) { double('prometheus_client') } + subject { described_class.new(client) } + + context 'verify queries' do + before do + allow(PrometheusMetric).to receive(:find_by_identifier).and_return(create(:prometheus_metric, query: prometheus_istio_query('test-name', 'test-ns'))) + allow(client).to receive(:query_range) + end + + it 'has the query, but no data' do + results = subject.query(serverless_func.id) + + expect(results.queries[0][:query_range]).to eql('floor(sum(rate(istio_revision_request_count{destination_configuration="test-name", destination_namespace="test-ns"}[1m])*30))') + end + end +end diff --git a/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb b/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb index 420218a695a..936447b8474 100644 --- a/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::Prometheus::Queries::MatchedMetricQuery do [{ '__name__' => 'metric_a' }, { '__name__' => 'metric_b' }] end - let(:partialy_empty_series_info) { [{ '__name__' => 'metric_a', 'environment' => '' }] } + let(:partially_empty_series_info) { [{ '__name__' => 'metric_a', 'environment' => '' }] } let(:empty_series_info) { [] } let(:client) { double('prometheus_client') } @@ -60,7 +60,7 @@ describe Gitlab::Prometheus::Queries::MatchedMetricQuery do context 'one of the series info was not found' do before do - allow(client).to receive(:series).and_return(partialy_empty_series_info) + allow(client).to receive(:series).and_return(partially_empty_series_info) end it 'responds with one active and one missing metric' do expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 1, metrics_missing_requirements: 1 }]) diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb index 78c74266c61..6dc99ef26ec 100644 --- a/spec/lib/gitlab/prometheus/query_variables_spec.rb +++ b/spec/lib/gitlab/prometheus/query_variables_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe Gitlab::Prometheus::QueryVariables do describe '.call' do + let(:project) { environment.project } let(:environment) { create(:environment) } let(:slug) { environment.slug } @@ -21,13 +22,32 @@ describe Gitlab::Prometheus::QueryVariables do end context 'with deployment platform' do - let(:kube_namespace) { environment.deployment_platform.actual_namespace } + context 'with project cluster' do + let(:kube_namespace) { environment.deployment_platform.cluster.kubernetes_namespace_for(project) } - before do - create(:cluster, :provided_by_user, projects: [environment.project]) + before do + create(:cluster, :project, :provided_by_user, projects: [project]) + end + + it { is_expected.to include(kube_namespace: kube_namespace) } end - it { is_expected.to include(kube_namespace: kube_namespace) } + context 'with group cluster' do + let(:cluster) { create(:cluster, :group, :provided_by_user, groups: [group]) } + let(:group) { create(:group) } + let(:project2) { create(:project) } + let(:kube_namespace) { k8s_ns.namespace } + + let!(:k8s_ns) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project) } + let!(:k8s_ns2) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project2) } + + before do + group.projects << project + group.projects << project2 + end + + it { is_expected.to include(kube_namespace: kube_namespace) } + end end end end diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb index 4c3b8deefb9..f15ae83a02c 100644 --- a/spec/lib/gitlab/prometheus_client_spec.rb +++ b/spec/lib/gitlab/prometheus_client_spec.rb @@ -60,15 +60,13 @@ describe Gitlab::PrometheusClient do end describe 'failure to reach a provided prometheus url' do - let(:prometheus_url) {"https://prometheus.invalid.example.com"} + let(:prometheus_url) {"https://prometheus.invalid.example.com/api/v1/query?query=1"} - subject { described_class.new(RestClient::Resource.new(prometheus_url)) } - - context 'exceptions are raised' do + shared_examples 'exceptions are raised' do it 'raises a Gitlab::PrometheusClient::Error error when a SocketError is rescued' do req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError) - expect { subject.send(:get, '/', {}) } + expect { subject } .to raise_error(Gitlab::PrometheusClient::Error, "Can't connect to #{prometheus_url}") expect(req_stub).to have_been_requested end @@ -76,7 +74,7 @@ describe Gitlab::PrometheusClient do it 'raises a Gitlab::PrometheusClient::Error error when a SSLError is rescued' do req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError) - expect { subject.send(:get, '/', {}) } + expect { subject } .to raise_error(Gitlab::PrometheusClient::Error, "#{prometheus_url} contains invalid SSL data") expect(req_stub).to have_been_requested end @@ -84,11 +82,23 @@ describe Gitlab::PrometheusClient do it 'raises a Gitlab::PrometheusClient::Error error when a RestClient::Exception is rescued' do req_stub = stub_prometheus_request_with_exception(prometheus_url, RestClient::Exception) - expect { subject.send(:get, '/', {}) } + expect { subject } .to raise_error(Gitlab::PrometheusClient::Error, "Network connection error") expect(req_stub).to have_been_requested end end + + context 'ping' do + subject { described_class.new(RestClient::Resource.new(prometheus_url)).ping } + + it_behaves_like 'exceptions are raised' + end + + context 'proxy' do + subject { described_class.new(RestClient::Resource.new(prometheus_url)).proxy('query', { query: '1' }) } + + it_behaves_like 'exceptions are raised' + end end describe '#query' do @@ -230,4 +240,87 @@ describe Gitlab::PrometheusClient do let(:execute_query) { subject.query_range(prometheus_query) } end end + + describe '.compute_step' do + using RSpec::Parameterized::TableSyntax + + let(:now) { Time.now.utc } + + subject { described_class.compute_step(start, stop) } + + where(:time_interval_in_seconds, :step) do + 0 | 60 + 10.hours | 60 + 10.hours + 1 | 61 + # frontend options + 30.minutes | 60 + 3.hours | 60 + 8.hours | 60 + 1.day | 144 + 3.days | 432 + 1.week | 1008 + end + + with_them do + let(:start) { now - time_interval_in_seconds } + let(:stop) { now } + + it { is_expected.to eq(step) } + end + end + + describe 'proxy' do + context 'get API' do + let(:prometheus_query) { prometheus_cpu_query('env-slug') } + let(:query_url) { prometheus_query_url(prometheus_query) } + + around do |example| + Timecop.freeze { example.run } + end + + context 'when response status code is 200' do + it 'returns response object' do + req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('vector')) + + response = subject.proxy('query', { query: prometheus_query }) + json_response = JSON.parse(response.body) + + expect(response.code).to eq(200) + expect(json_response).to eq({ + 'status' => 'success', + 'data' => { + 'resultType' => 'vector', + 'result' => [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }] + } + }) + expect(req_stub).to have_been_requested + end + end + + context 'when response status code is not 200' do + it 'returns response object' do + req_stub = stub_prometheus_request(query_url, status: 400, body: { error: 'error' }) + + response = subject.proxy('query', { query: prometheus_query }) + json_response = JSON.parse(response.body) + + expect(req_stub).to have_been_requested + expect(response.code).to eq(400) + expect(json_response).to eq('error' => 'error') + end + end + + context 'when RestClient::Exception is raised' do + before do + stub_prometheus_request_with_exception(query_url, RestClient::Exception) + end + + it 'raises PrometheusClient::Error' do + expect { subject.proxy('query', { query: prometheus_query }) }.to( + raise_error(Gitlab::PrometheusClient::Error, 'Network connection error') + ) + end + end + end + end end diff --git a/spec/lib/gitlab/push_options_spec.rb b/spec/lib/gitlab/push_options_spec.rb new file mode 100644 index 00000000000..fc9e421bea6 --- /dev/null +++ b/spec/lib/gitlab/push_options_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::PushOptions do + describe 'namespace and key validation' do + it 'ignores unrecognised namespaces' do + options = described_class.new(['invalid.key=value']) + + expect(options.get(:invalid)).to eq(nil) + end + + it 'ignores unrecognised keys' do + options = described_class.new(['merge_request.key=value']) + + expect(options.get(:merge_request)).to eq(nil) + end + + it 'ignores blank keys' do + options = described_class.new(['merge_request']) + + expect(options.get(:merge_request)).to eq(nil) + end + + it 'parses recognised namespace and key pairs' do + options = described_class.new(['merge_request.target=value']) + + expect(options.get(:merge_request)).to include({ + target: 'value' + }) + end + end + + describe '#get' do + it 'can emulate Hash#dig' do + options = described_class.new(['merge_request.target=value']) + + expect(options.get(:merge_request, :target)).to eq('value') + end + end + + describe '#as_json' do + it 'returns all options' do + options = described_class.new(['merge_request.target=value']) + + expect(options.as_json).to include( + merge_request: { + target: 'value' + } + ) + end + end + + it 'can parse multiple push options' do + options = described_class.new([ + 'merge_request.create', + 'merge_request.target=value' + ]) + + expect(options.get(:merge_request)).to include({ + create: true, + target: 'value' + }) + expect(options.get(:merge_request, :create)).to eq(true) + expect(options.get(:merge_request, :target)).to eq('value') + end + + it 'stores options internally as a HashWithIndifferentAccess' do + options = described_class.new([ + 'merge_request.create' + ]) + + expect(options.get('merge_request', 'create')).to eq(true) + expect(options.get(:merge_request, :create)).to eq(true) + end + + it 'selects the last option when options contain duplicate namespace and key pairs' do + options = described_class.new([ + 'merge_request.target=value1', + 'merge_request.target=value2' + ]) + + expect(options.get(:merge_request, :target)).to eq('value2') + end + + it 'defaults values to true' do + options = described_class.new(['merge_request.create']) + + expect(options.get(:merge_request, :create)).to eq(true) + end + + it 'expands aliases' do + options = described_class.new(['mr.target=value']) + + expect(options.get(:merge_request, :target)).to eq('value') + end + + it 'forgives broken push options' do + options = described_class.new(['merge_request . target = value']) + + expect(options.get(:merge_request, :target)).to eq('value') + end +end diff --git a/spec/lib/gitlab/quick_actions/command_definition_spec.rb b/spec/lib/gitlab/quick_actions/command_definition_spec.rb index 5dae82a63b4..b6e0adbc1c2 100644 --- a/spec/lib/gitlab/quick_actions/command_definition_spec.rb +++ b/spec/lib/gitlab/quick_actions/command_definition_spec.rb @@ -69,10 +69,40 @@ describe Gitlab::QuickActions::CommandDefinition do expect(subject.available?(opts)).to be true end end + + context "when the command has types" do + before do + subject.types = [Issue, Commit] + end + + context "when the command target type is allowed" do + it "returns true" do + opts[:quick_action_target] = Issue.new + expect(subject.available?(opts)).to be true + end + end + + context "when the command target type is not allowed" do + it "returns true" do + opts[:quick_action_target] = MergeRequest.new + expect(subject.available?(opts)).to be false + end + end + end + + context "when the command has no types" do + it "any target type is allowed" do + opts[:quick_action_target] = Issue.new + expect(subject.available?(opts)).to be true + + opts[:quick_action_target] = MergeRequest.new + expect(subject.available?(opts)).to be true + end + end end describe "#execute" do - let(:context) { OpenStruct.new(run: false) } + let(:context) { OpenStruct.new(run: false, commands_executed_count: nil) } context "when the command is a noop" do it "doesn't execute the command" do @@ -80,6 +110,7 @@ describe Gitlab::QuickActions::CommandDefinition do subject.execute(context, nil) + expect(context.commands_executed_count).to be_nil expect(context.run).to be false end end @@ -97,6 +128,7 @@ describe Gitlab::QuickActions::CommandDefinition do it "doesn't execute the command" do subject.execute(context, nil) + expect(context.commands_executed_count).to be_nil expect(context.run).to be false end end @@ -112,6 +144,7 @@ describe Gitlab::QuickActions::CommandDefinition do subject.execute(context, true) expect(context.run).to be true + expect(context.commands_executed_count).to eq(1) end end @@ -120,6 +153,7 @@ describe Gitlab::QuickActions::CommandDefinition do subject.execute(context, nil) expect(context.run).to be true + expect(context.commands_executed_count).to eq(1) end end end @@ -134,6 +168,7 @@ describe Gitlab::QuickActions::CommandDefinition do subject.execute(context, true) expect(context.run).to be true + expect(context.commands_executed_count).to eq(1) end end diff --git a/spec/lib/gitlab/quick_actions/dsl_spec.rb b/spec/lib/gitlab/quick_actions/dsl_spec.rb index fd4df8694ba..185adab1ff6 100644 --- a/spec/lib/gitlab/quick_actions/dsl_spec.rb +++ b/spec/lib/gitlab/quick_actions/dsl_spec.rb @@ -48,13 +48,19 @@ describe Gitlab::QuickActions::Dsl do substitution :something do |text| "#{text} Some complicated thing you want in here" end + + desc 'A command with types' + types Issue, Commit + command :has_types do + "Has Issue and Commit types" + end end end describe '.command_definitions' do it 'returns an array with commands definitions' do no_args_def, explanation_with_aliases_def, dynamic_description_def, - cc_def, cond_action_def, with_params_parsing_def, substitution_def = + cc_def, cond_action_def, with_params_parsing_def, substitution_def, has_types = DummyClass.command_definitions expect(no_args_def.name).to eq(:no_args) @@ -63,6 +69,7 @@ describe Gitlab::QuickActions::Dsl do expect(no_args_def.explanation).to eq('') expect(no_args_def.params).to eq([]) expect(no_args_def.condition_block).to be_nil + expect(no_args_def.types).to eq([]) expect(no_args_def.action_block).to be_a_kind_of(Proc) expect(no_args_def.parse_params_block).to be_nil expect(no_args_def.warning).to eq('') @@ -73,6 +80,7 @@ describe Gitlab::QuickActions::Dsl do expect(explanation_with_aliases_def.explanation).to eq('Static explanation') expect(explanation_with_aliases_def.params).to eq(['The first argument']) expect(explanation_with_aliases_def.condition_block).to be_nil + expect(explanation_with_aliases_def.types).to eq([]) expect(explanation_with_aliases_def.action_block).to be_a_kind_of(Proc) expect(explanation_with_aliases_def.parse_params_block).to be_nil expect(explanation_with_aliases_def.warning).to eq('Possible problem!') @@ -83,6 +91,7 @@ describe Gitlab::QuickActions::Dsl do expect(dynamic_description_def.explanation).to eq('') expect(dynamic_description_def.params).to eq(['The first argument', 'The second argument']) expect(dynamic_description_def.condition_block).to be_nil + expect(dynamic_description_def.types).to eq([]) expect(dynamic_description_def.action_block).to be_a_kind_of(Proc) expect(dynamic_description_def.parse_params_block).to be_nil expect(dynamic_description_def.warning).to eq('') @@ -93,6 +102,7 @@ describe Gitlab::QuickActions::Dsl do expect(cc_def.explanation).to eq('') expect(cc_def.params).to eq([]) expect(cc_def.condition_block).to be_nil + expect(cc_def.types).to eq([]) expect(cc_def.action_block).to be_nil expect(cc_def.parse_params_block).to be_nil expect(cc_def.warning).to eq('') @@ -103,6 +113,7 @@ describe Gitlab::QuickActions::Dsl do expect(cond_action_def.explanation).to be_a_kind_of(Proc) expect(cond_action_def.params).to eq([]) expect(cond_action_def.condition_block).to be_a_kind_of(Proc) + expect(cond_action_def.types).to eq([]) expect(cond_action_def.action_block).to be_a_kind_of(Proc) expect(cond_action_def.parse_params_block).to be_nil expect(cond_action_def.warning).to eq('') @@ -113,6 +124,7 @@ describe Gitlab::QuickActions::Dsl do expect(with_params_parsing_def.explanation).to eq('') expect(with_params_parsing_def.params).to eq([]) expect(with_params_parsing_def.condition_block).to be_nil + expect(with_params_parsing_def.types).to eq([]) expect(with_params_parsing_def.action_block).to be_a_kind_of(Proc) expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc) expect(with_params_parsing_def.warning).to eq('') @@ -123,9 +135,21 @@ describe Gitlab::QuickActions::Dsl do expect(substitution_def.explanation).to eq('') expect(substitution_def.params).to eq(['<Comment>']) expect(substitution_def.condition_block).to be_nil + expect(substitution_def.types).to eq([]) expect(substitution_def.action_block.call('text')).to eq('text Some complicated thing you want in here') expect(substitution_def.parse_params_block).to be_nil expect(substitution_def.warning).to eq('') + + expect(has_types.name).to eq(:has_types) + expect(has_types.aliases).to eq([]) + expect(has_types.description).to eq('A command with types') + expect(has_types.explanation).to eq('') + expect(has_types.params).to eq([]) + expect(has_types.condition_block).to be_nil + expect(has_types.types).to eq([Issue, Commit]) + expect(has_types.action_block).to be_a_kind_of(Proc) + expect(has_types.parse_params_block).to be_nil + expect(has_types.warning).to eq('') end end end diff --git a/spec/lib/gitlab/rack_timeout_observer_spec.rb b/spec/lib/gitlab/rack_timeout_observer_spec.rb new file mode 100644 index 00000000000..3dc1a8b68fb --- /dev/null +++ b/spec/lib/gitlab/rack_timeout_observer_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::RackTimeoutObserver do + let(:counter) { Gitlab::Metrics::NullMetric.instance } + + before do + allow(Gitlab::Metrics).to receive(:counter) + .with(any_args) + .and_return(counter) + end + + describe '#callback' do + context 'when request times out' do + let(:env) do + { + ::Rack::Timeout::ENV_INFO_KEY => double(state: :timed_out), + 'action_dispatch.request.parameters' => { + 'controller' => 'foo', + 'action' => 'bar' + } + } + end + + subject { described_class.new } + + it 'increments timeout counter' do + expect(counter) + .to receive(:increment) + .with({ controller: 'foo', action: 'bar', route: nil, state: :timed_out }) + + subject.callback.call(env) + end + end + + context 'when request expires' do + let(:endpoint) { double } + let(:env) do + { + ::Rack::Timeout::ENV_INFO_KEY => double(state: :expired), + Grape::Env::API_ENDPOINT => endpoint + } + end + + subject { described_class.new } + + it 'increments timeout counter' do + allow(endpoint).to receive_message_chain('route.pattern.origin') { 'foobar' } + expect(counter) + .to receive(:increment) + .with({ controller: nil, action: nil, route: 'foobar', state: :expired }) + + subject.callback.call(env) + end + end + end +end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 4139d1c650c..d982053d92e 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ReferenceExtractor do diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb index 13940713dfc..8fbda929064 100644 --- a/spec/lib/gitlab/repo_path_spec.rb +++ b/spec/lib/gitlab/repo_path_spec.rb @@ -6,43 +6,49 @@ describe ::Gitlab::RepoPath do context 'a repository storage path' do it 'parses a full repository path' do - expect(described_class.parse(project.repository.full_path)).to eq([project, false, nil]) + expect(described_class.parse(project.repository.full_path)).to eq([project, Gitlab::GlRepository::PROJECT, nil]) end it 'parses a full wiki path' do - expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, true, nil]) + expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, Gitlab::GlRepository::WIKI, nil]) end end context 'a relative path' do it 'parses a relative repository path' do - expect(described_class.parse(project.full_path + '.git')).to eq([project, false, nil]) + expect(described_class.parse(project.full_path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, nil]) end it 'parses a relative wiki path' do - expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, true, nil]) + expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, Gitlab::GlRepository::WIKI, nil]) end it 'parses a relative path starting with /' do - expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, false, nil]) + expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, nil]) end context 'of a redirected project' do let(:redirect) { project.route.create_redirect('foo/bar') } it 'parses a relative repository path' do - expect(described_class.parse(redirect.path + '.git')).to eq([project, false, 'foo/bar']) + expect(described_class.parse(redirect.path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, 'foo/bar']) end it 'parses a relative wiki path' do - expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, true, 'foo/bar.wiki']) + expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, Gitlab::GlRepository::WIKI, 'foo/bar.wiki']) end it 'parses a relative path starting with /' do - expect(described_class.parse('/' + redirect.path + '.git')).to eq([project, false, 'foo/bar']) + expect(described_class.parse('/' + redirect.path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, 'foo/bar']) end end end + + it "returns the default type for non existent paths" do + _project, type, _redirected = described_class.parse("path/non-existent.git") + + expect(type).to eq(Gitlab::GlRepository.default_type) + end end describe '.find_project' do diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb index fd443cc1f71..23e45aff1c5 100644 --- a/spec/lib/gitlab/request_context_spec.rb +++ b/spec/lib/gitlab/request_context_spec.rb @@ -6,6 +6,31 @@ describe Gitlab::RequestContext do let(:app) { -> (env) {} } let(:env) { Hash.new } + context 'with X-Forwarded-For headers', :request_store do + let(:load_balancer_ip) { '1.2.3.4' } + let(:headers) do + { + 'HTTP_X_FORWARDED_FOR' => "#{load_balancer_ip}, 127.0.0.1", + 'REMOTE_ADDR' => '127.0.0.1' + } + end + + let(:env) { Rack::MockRequest.env_for("/").merge(headers) } + + it 'returns the load balancer IP' do + client_ip = nil + + endpoint = proc do + client_ip = Gitlab::SafeRequestStore[:client_ip] + [200, {}, ["Hello"]] + end + + described_class.new(endpoint).call(env) + + expect(client_ip).to eq(load_balancer_ip) + end + end + context 'when RequestStore::Middleware is used' do around do |example| RequestStore::Middleware.new(-> (env) { example.run }).call({}) @@ -15,7 +40,7 @@ describe Gitlab::RequestContext do let(:ip) { '192.168.1.11' } before do - allow_any_instance_of(ActionDispatch::Request).to receive(:ip).and_return(ip) + allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip) described_class.new(app).call(env) end diff --git a/spec/lib/gitlab/route_map_spec.rb b/spec/lib/gitlab/route_map_spec.rb index d672f7b5675..a39c774429e 100644 --- a/spec/lib/gitlab/route_map_spec.rb +++ b/spec/lib/gitlab/route_map_spec.rb @@ -60,7 +60,7 @@ describe Gitlab::RouteMap do subject do map = described_class.new(<<-"MAP".strip_heredoc) - - source: '#{malicious_regexp}' + - source: '#{malicious_regexp_re2}' public: '/' MAP diff --git a/spec/lib/gitlab/sanitizers/exif_spec.rb b/spec/lib/gitlab/sanitizers/exif_spec.rb new file mode 100644 index 00000000000..bd5f330c7a1 --- /dev/null +++ b/spec/lib/gitlab/sanitizers/exif_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe Gitlab::Sanitizers::Exif do + let(:sanitizer) { described_class.new } + + describe '#batch_clean' do + context 'with image uploads' do + let!(:uploads) { create_list(:upload, 3, :with_file, :issuable_upload) } + + it 'processes all uploads if range ID is not set' do + expect(sanitizer).to receive(:clean).exactly(3).times + + sanitizer.batch_clean + end + + it 'processes only uploads in the selected range' do + expect(sanitizer).to receive(:clean).once + + sanitizer.batch_clean(start_id: uploads[1].id, stop_id: uploads[1].id) + end + + it 'pauses if sleep_time is set' do + expect(sanitizer).to receive(:sleep).exactly(3).times.with(1.second) + expect(sanitizer).to receive(:clean).exactly(3).times + + sanitizer.batch_clean(sleep_time: 1) + end + end + + it 'filters only jpg/tiff images' do + create(:upload, path: 'filename.jpg') + create(:upload, path: 'filename.jpeg') + create(:upload, path: 'filename.JPG') + create(:upload, path: 'filename.tiff') + create(:upload, path: 'filename.TIFF') + create(:upload, path: 'filename.png') + create(:upload, path: 'filename.txt') + + expect(sanitizer).to receive(:clean).exactly(5).times + sanitizer.batch_clean + end + end + + describe '#clean' do + let(:uploader) { create(:upload, :with_file, :issuable_upload).build_uploader } + + context "no dry run" do + it "removes exif from the image" do + uploader.store!(fixture_file_upload('spec/fixtures/rails_sample.jpg')) + + original_upload = uploader.upload + expected_args = ["exiftool", "-all=", "-tagsFromFile", "@", *Gitlab::Sanitizers::Exif::EXCLUDE_PARAMS, "--IPTC:all", "--XMP-iptcExt:all", kind_of(String)] + + expect(sanitizer).to receive(:extra_tags).and_return(["", 0]) + expect(sanitizer).to receive(:exec_remove_exif!).once.and_call_original + expect(uploader).to receive(:store!).and_call_original + expect(Gitlab::Popen).to receive(:popen).with(expected_args) do |args| + File.write("#{args.last}_original", "foo") if args.last.start_with?(Dir.tmpdir) + + [expected_args, 0] + end + + sanitizer.clean(uploader, dry_run: false) + + expect(uploader.upload.id).not_to eq(original_upload.id) + expect(uploader.upload.path).to eq(original_upload.path) + end + + it "ignores image without exif" do + expected_args = ["exiftool", "-all", "-j", "-sort", "--IPTC:all", "--XMP-iptcExt:all", kind_of(String)] + + expect(Gitlab::Popen).to receive(:popen).with(expected_args).and_return(["[{}]", 0]) + expect(sanitizer).not_to receive(:exec_remove_exif!) + expect(uploader).not_to receive(:store!) + + sanitizer.clean(uploader, dry_run: false) + end + + it "raises an error if the exiftool fails with an error" do + expect(Gitlab::Popen).to receive(:popen).and_return(["error", 1]) + + expect { sanitizer.clean(uploader, dry_run: false) }.to raise_exception(RuntimeError, "failed to get exif tags: error") + end + end + + context "dry run" do + it "doesn't change the image" do + expect(sanitizer).to receive(:extra_tags).and_return({ 'foo' => 'bar' }) + expect(sanitizer).not_to receive(:exec_remove_exif!) + expect(uploader).not_to receive(:store!) + + sanitizer.clean(uploader, dry_run: true) + end + end + end + + describe "#extra_tags" do + it "returns a list of keys for exif file" do + tags = '[{ + "DigitalSourceType": "some source", + "ImageHeight": 654 + }]' + + expect(Gitlab::Popen).to receive(:popen).and_return([tags, 0]) + + expect(sanitizer.extra_tags('filename')).not_to be_empty + end + + it "returns an empty list for file with only whitelisted and ignored tags" do + tags = '[{ + "ImageHeight": 654, + "Megapixels": 0.641 + }]' + + expect(Gitlab::Popen).to receive(:popen).and_return([tags, 0]) + + expect(sanitizer.extra_tags('some file')).to be_empty + end + end +end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 87288baedb0..3d27156b356 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -97,7 +97,7 @@ describe Gitlab::SearchResults do results.objects('merge_requests') end - it 'it skips project filter if default project context is used' do + it 'skips project filter if default project context is used' do allow(results).to receive(:default_project_filter).and_return(true) expect(results).not_to receive(:project_ids_relation) @@ -113,7 +113,7 @@ describe Gitlab::SearchResults do results.objects('issues') end - it 'it skips project filter if default project context is used' do + it 'skips project filter if default project context is used' do allow(results).to receive(:default_project_filter).and_return(true) expect(results).not_to receive(:project_ids_relation) @@ -121,6 +121,22 @@ describe Gitlab::SearchResults do results.objects('issues') end end + + describe '#users' do + it 'does not call the UsersFinder when the current_user is not allowed to read users list' do + allow(Ability).to receive(:allowed?).and_return(false) + + expect(UsersFinder).not_to receive(:new).with(user, search: 'foo').and_call_original + + results.objects('users') + end + + it 'calls the UsersFinder' do + expect(UsersFinder).to receive(:new).with(user, search: 'foo').and_call_original + + results.objects('users') + end + end end it 'does not list issues on private projects' do @@ -240,4 +256,28 @@ describe Gitlab::SearchResults do expect(results.objects('merge_requests')).not_to include merge_request end + + context 'milestones' do + it 'returns correct set of milestones' do + private_project_1 = create(:project, :private) + private_project_2 = create(:project, :private) + internal_project = create(:project, :internal) + public_project_1 = create(:project, :public) + public_project_2 = create(:project, :public, :issues_disabled, :merge_requests_disabled) + private_project_1.add_developer(user) + # milestones that should not be visible + create(:milestone, project: private_project_2, title: 'Private project without access milestone') + create(:milestone, project: public_project_2, title: 'Public project with milestones disabled milestone') + # milestones that should be visible + milestone_1 = create(:milestone, project: private_project_1, title: 'Private project with access milestone', state: 'closed') + milestone_2 = create(:milestone, project: internal_project, title: 'Internal project milestone') + milestone_3 = create(:milestone, project: public_project_1, title: 'Public project with milestones enabled milestone') + # Global search scope takes user authorized projects, internal projects and public projects. + limit_projects = ProjectsFinder.new(current_user: user).execute + + milestones = described_class.new(user, limit_projects, 'milestone').objects('milestones') + + expect(milestones).to match_array([milestone_1, milestone_2, milestone_3]) + end + end end diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb index 1128eaf8560..af8b059b984 100644 --- a/spec/lib/gitlab/sentry_spec.rb +++ b/spec/lib/gitlab/sentry_spec.rb @@ -2,12 +2,15 @@ require 'spec_helper' describe Gitlab::Sentry do describe '.context' do - it 'adds the locale to the tags' do + it 'adds the expected tags' do expect(described_class).to receive(:enabled?).and_return(true) + allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid') described_class.context(nil) expect(Raven.tags_context[:locale].to_s).to eq(I18n.locale.to_s) + expect(Raven.tags_context[Labkit::Correlation::CorrelationId::LOG_KEY.to_sym].to_s) + .to eq('cid') end end @@ -27,7 +30,7 @@ describe Gitlab::Sentry do context 'when exceptions should not be raised' do before do allow(described_class).to receive(:should_raise_for_dev?).and_return(false) - allow(Gitlab::CorrelationId).to receive(:current_id).and_return('cid') + allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid') end it 'logs the exception with all attributes passed' do @@ -65,7 +68,7 @@ describe Gitlab::Sentry do before do allow(described_class).to receive(:enabled?).and_return(true) - allow(Gitlab::CorrelationId).to receive(:current_id).and_return('cid') + allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid') end it 'calls Raven.capture_exception' do diff --git a/spec/lib/gitlab/session_spec.rb b/spec/lib/gitlab/session_spec.rb new file mode 100644 index 00000000000..8db73f0ec7b --- /dev/null +++ b/spec/lib/gitlab/session_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Session do + it 'uses the current thread as a data store' do + Thread.current[:session_storage] = { a: :b } + + expect(described_class.current).to eq(a: :b) + ensure + Thread.current[:session_storage] = nil + end + + describe '#with_session' do + it 'sets session hash' do + described_class.with_session(one: 1) do + expect(described_class.current).to eq(one: 1) + end + end + + it 'restores current store after' do + described_class.with_session(two: 2) { } + + expect(described_class.current).to eq nil + end + end +end diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 033e1bf81a1..bce2e754176 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -8,6 +8,7 @@ describe Gitlab::Shell do let(:gitlab_shell) { described_class.new } let(:popen_vars) { { 'GIT_TERMINAL_PROMPT' => ENV['GIT_TERMINAL_PROMPT'] } } let(:timeout) { Gitlab.config.gitlab_shell.git_timeout } + let(:gitlab_authorized_keys) { double } before do allow(Project).to receive(:find).and_return(project) @@ -49,13 +50,38 @@ describe Gitlab::Shell do describe '#add_key' do context 'when authorized_keys_enabled is true' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + it 'calls #gitlab_shell_fast_execute with add-key command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([ + :gitlab_shell_keys_path, + 'add-key', + 'key-123', + 'ssh-rsa foobar' + ]) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + + expect(gitlab_authorized_keys) + .to receive(:add_key) + .with('key-123', 'ssh-rsa foobar') + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar') + end end end @@ -64,10 +90,24 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'authorized_keys_file set' do + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end end end @@ -76,24 +116,89 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls #gitlab_shell_fast_execute with add-key command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([ + :gitlab_shell_keys_path, + 'add-key', + 'key-123', + 'ssh-rsa foobar' + ]) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + expect(gitlab_authorized_keys) + .to receive(:add_key) + .with('key-123', 'ssh-rsa foobar') + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar') + end end end end describe '#batch_add_keys' do + let(:keys) { [double(shell_id: 'key-123', key: 'ssh-rsa foobar')] } + context 'when authorized_keys_enabled is true' do - it 'instantiates KeyAdder' do - expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + context 'authorized_keys_file not set' do + let(:io) { double } - gitlab_shell.batch_add_keys do |adder| - adder.add_key('key-123', 'ssh-rsa foobar') + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end + + context 'valid keys' do + before do + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls gitlab-keys with batch-add-keys command' do + expect(IO) + .to receive(:popen) + .with("gitlab_shell_keys_path batch-add-keys", 'w') + .and_yield(io) + + expect(io).to receive(:puts).with("key-123\tssh-rsa foobar") + expect(gitlab_shell.batch_add_keys(keys)).to be_truthy + end + end + + context 'invalid keys' do + let(:keys) { [double(shell_id: 'key-123', key: "ssh-rsa A\tSDFA\nSGADG")] } + + it 'catches failure and returns false' do + expect(gitlab_shell.batch_add_keys(keys)).to be_falsey + end + end + end + + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + + expect(gitlab_authorized_keys) + .to receive(:batch_add_keys) + .with(keys) + + gitlab_shell.batch_add_keys(keys) end end end @@ -103,11 +208,23 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - it 'does nothing' do - expect_any_instance_of(Gitlab::Shell::KeyAdder).not_to receive(:add_key) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end + + it 'does nothing' do + expect(IO).not_to receive(:popen) + + gitlab_shell.batch_add_keys(keys) + end + end + + context 'authorized_keys_file set' do + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - gitlab_shell.batch_add_keys do |adder| - adder.add_key('key-123', 'ssh-rsa foobar') + gitlab_shell.batch_add_keys(keys) end end end @@ -117,11 +234,37 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - it 'instantiates KeyAdder' do - expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + context 'authorized_keys_file not set' do + let(:io) { double } - gitlab_shell.batch_add_keys do |adder| - adder.add_key('key-123', 'ssh-rsa foobar') + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls gitlab-keys with batch-add-keys command' do + expect(IO) + .to receive(:popen) + .with("gitlab_shell_keys_path batch-add-keys", 'w') + .and_yield(io) + + expect(io).to receive(:puts).with("key-123\tssh-rsa foobar") + + gitlab_shell.batch_add_keys(keys) + end + end + + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + + expect(gitlab_authorized_keys) + .to receive(:batch_add_keys) + .with(keys) + + gitlab_shell.batch_add_keys(keys) end end end @@ -129,13 +272,34 @@ describe Gitlab::Shell do describe '#remove_key' do context 'when authorized_keys_enabled is true' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls #gitlab_shell_fast_execute with rm-key command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([ + :gitlab_shell_keys_path, + 'rm-key', + 'key-123' + ]) - gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + gitlab_shell.remove_key('key-123') + end + end + + context 'authorized_keys_file not set' do + it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') + + gitlab_shell.remove_key('key-123') + end end end @@ -144,10 +308,24 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + + gitlab_shell.remove_key('key-123') + end + end + + context 'authorized_keys_file set' do + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + gitlab_shell.remove_key('key-123') + end end end @@ -156,232 +334,256 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls #gitlab_shell_fast_execute with rm-key command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([ + :gitlab_shell_keys_path, + 'rm-key', + 'key-123' + ]) - gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + gitlab_shell.remove_key('key-123') + end end - end - context 'when key content is not given' do - it 'calls rm-key with only one argument' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'rm-key', 'key-123'] - ) + context 'authorized_keys_file not set' do + it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') - gitlab_shell.remove_key('key-123') + gitlab_shell.remove_key('key-123') + end end end end describe '#remove_all_keys' do context 'when authorized_keys_enabled is true' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with([:gitlab_shell_keys_path, 'clear']) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end - gitlab_shell.remove_all_keys - end - end + it 'calls #gitlab_shell_fast_execute with clear command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([:gitlab_shell_keys_path, 'clear']) - context 'when authorized_keys_enabled is false' do - before do - stub_application_setting(authorized_keys_enabled: false) + gitlab_shell.remove_all_keys + end end - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#clear' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:clear) - gitlab_shell.remove_all_keys + gitlab_shell.remove_all_keys + end end end - context 'when authorized_keys_enabled is nil' do + context 'when authorized_keys_enabled is false' do before do - stub_application_setting(authorized_keys_enabled: nil) + stub_application_setting(authorized_keys_enabled: false) end - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'clear'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end - gitlab_shell.remove_all_keys - end - end - end + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) - describe '#remove_keys_not_found_in_db' do - context 'when keys are in the file that are not in the DB' do - before do - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') - @another_key = create(:key) # this one IS in the DB + gitlab_shell.remove_all_keys + end end - it 'removes the keys' do - expect(find_in_authorized_keys_file(1234)).to be_truthy - expect(find_in_authorized_keys_file(9876)).to be_truthy - expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy - gitlab_shell.remove_keys_not_found_in_db - expect(find_in_authorized_keys_file(1234)).to be_falsey - expect(find_in_authorized_keys_file(9876)).to be_falsey - expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy + context 'authorized_keys_file set' do + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) + + gitlab_shell.remove_all_keys + end end end - context 'when keys there are duplicate keys in the file that are not in the DB' do + context 'when authorized_keys_enabled is nil' do before do - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + stub_application_setting(authorized_keys_enabled: nil) end - it 'removes the keys' do - expect(find_in_authorized_keys_file(1234)).to be_truthy - gitlab_shell.remove_keys_not_found_in_db - expect(find_in_authorized_keys_file(1234)).to be_falsey - end + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end - it 'does not run remove more than once per key (in a batch)' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234').once - gitlab_shell.remove_keys_not_found_in_db - end - end + it 'calls #gitlab_shell_fast_execute with clear command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([:gitlab_shell_keys_path, 'clear']) - context 'when keys there are duplicate keys in the file that ARE in the DB' do - before do - gitlab_shell.remove_all_keys - @key = create(:key) - gitlab_shell.add_key(@key.shell_id, @key.key) + gitlab_shell.remove_all_keys + end end - it 'does not remove the key' do - gitlab_shell.remove_keys_not_found_in_db - expect(find_in_authorized_keys_file(@key.id)).to be_truthy - end + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#clear' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:clear) - it 'does not need to run a SELECT query for that batch, on account of that key' do - expect_any_instance_of(ActiveRecord::Relation).not_to receive(:pluck) - gitlab_shell.remove_keys_not_found_in_db + gitlab_shell.remove_all_keys + end end end + end - unless ENV['CI'] # Skip in CI, it takes 1 minute - context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do + describe '#remove_keys_not_found_in_db' do + context 'when keys are in the file that are not in the DB' do + context 'authorized_keys_file not set' do before do + stub_gitlab_shell_setting(authorized_keys_file: nil) gitlab_shell.remove_all_keys - 100.times { |i| create(:key) } # first batch is all in the DB gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') + @another_key = create(:key) # this one IS in the DB end - it 'removes the keys not in the DB' do - expect(find_in_authorized_keys_file(1234)).to be_truthy + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') + expect(gitlab_shell).to receive(:remove_key).with('key-9876') + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}") + gitlab_shell.remove_keys_not_found_in_db - expect(find_in_authorized_keys_file(1234)).to be_falsey end end - end - end - describe '#batch_read_key_ids' do - context 'when there are keys in the authorized_keys file' do - before do - gitlab_shell.remove_all_keys - (1..4).each do |i| - gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + context 'authorized_keys_file set' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') + @another_key = create(:key) # this one IS in the DB end - end - it 'iterates over the key IDs in the file, in batches' do - loop_count = 0 - first_batch = [1, 2] - second_batch = [3, 4] + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') + expect(gitlab_shell).to receive(:remove_key).with('key-9876') + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}") - gitlab_shell.batch_read_key_ids(batch_size: 2) do |batch| - expected = (loop_count == 0 ? first_batch : second_batch) - expect(batch).to eq(expected) - loop_count += 1 + gitlab_shell.remove_keys_not_found_in_db end end end - end - describe '#list_key_ids' do - context 'when there are keys in the authorized_keys file' do - before do - gitlab_shell.remove_all_keys - (1..4).each do |i| - gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + context 'when keys there are duplicate keys in the file that are not in the DB' do + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') + + gitlab_shell.remove_keys_not_found_in_db end end - it 'outputs the key IDs in the file, separated by newlines' do - ids = [] - gitlab_shell.list_key_ids do |io| - io.each do |line| - ids << line - end + context 'authorized_keys_file set' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') end - expect(ids).to eq(%W{1\n 2\n 3\n 4\n}) - end - end + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') - context 'when there are no keys in the authorized_keys file' do - before do - gitlab_shell.remove_all_keys + gitlab_shell.remove_keys_not_found_in_db + end end + end - it 'outputs nothing, not even an empty string' do - ids = [] - gitlab_shell.list_key_ids do |io| - io.each do |line| - ids << line - end + context 'when keys there are duplicate keys in the file that ARE in the DB' do + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + gitlab_shell.remove_all_keys + @key = create(:key) + gitlab_shell.add_key(@key.shell_id, @key.key) end - expect(ids).to eq([]) + it 'does not remove the key' do + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}") + + gitlab_shell.remove_keys_not_found_in_db + end end - end - end - describe Gitlab::Shell::KeyAdder do - describe '#add_key' do - it 'removes trailing garbage' do - io = spy(:io) - adder = described_class.new(io) + context 'authorized_keys_file set' do + before do + gitlab_shell.remove_all_keys + @key = create(:key) + gitlab_shell.add_key(@key.shell_id, @key.key) + end - adder.add_key('key-42', "ssh-rsa foo bar\tbaz") + it 'does not remove the key' do + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}") - expect(io).to have_received(:puts).with("key-42\tssh-rsa foo") + gitlab_shell.remove_keys_not_found_in_db + end end + end - it 'handles multiple spaces in the key' do - io = spy(:io) - adder = described_class.new(io) + unless ENV['CI'] # Skip in CI, it takes 1 minute + context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + gitlab_shell.remove_all_keys + 100.times { |i| create(:key) } # first batch is all in the DB + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end - adder.add_key('key-42', "ssh-rsa foo") + it 'removes the keys not in the DB' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') - expect(io).to have_received(:puts).with("key-42\tssh-rsa foo") - end + gitlab_shell.remove_keys_not_found_in_db + end + end - it 'raises an exception if the key contains a tab' do - expect do - described_class.new(StringIO.new).add_key('key-42', "ssh-rsa\tfoobar") - end.to raise_error(Gitlab::Shell::Error) - end + context 'authorized_keys_file set' do + before do + gitlab_shell.remove_all_keys + 100.times { |i| create(:key) } # first batch is all in the DB + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys not in the DB' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') - it 'raises an exception if the key contains a newline' do - expect do - described_class.new(StringIO.new).add_key('key-42', "ssh-rsa foobar\nssh-rsa pawned") - end.to raise_error(Gitlab::Shell::Error) + gitlab_shell.remove_keys_not_found_in_db + end + end end end end @@ -393,7 +595,6 @@ describe Gitlab::Shell do before do allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(gitlab_shell_path) - allow(Gitlab.config.gitlab_shell).to receive(:hooks_path).and_return(gitlab_shell_hooks_path) allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) end @@ -411,16 +612,6 @@ describe Gitlab::Shell do FileUtils.rm_rf(created_path) end - it 'creates a repository' do - expect(gitlab_shell.create_repository(repository_storage, repo_name, repo_name)).to be_truthy - - expect(File.stat(created_path).mode & 0o777).to eq(0o770) - - hooks_path = File.join(created_path, 'hooks') - expect(File.lstat(hooks_path)).to be_symlink - expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path) - end - it 'returns false when the command fails' do FileUtils.mkdir_p(File.dirname(created_path)) # This file will block the creation of the repo's .git directory. That @@ -567,12 +758,4 @@ describe Gitlab::Shell do end end end - - def find_in_authorized_keys_file(key_id) - gitlab_shell.batch_read_key_ids do |ids| - return true if ids.include?(key_id) # rubocop:disable Cop/AvoidReturnFromBlocks - end - - false - end end diff --git a/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb b/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb index a138ad7c910..0ff694d409b 100644 --- a/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb @@ -30,7 +30,7 @@ describe Gitlab::SidekiqMiddleware::CorrelationInjector do it 'injects into payload the correlation id' do expect_any_instance_of(described_class).to receive(:call).and_call_original - Gitlab::CorrelationId.use_id('new-correlation-id') do + Labkit::Correlation::CorrelationId.use_id('new-correlation-id') do TestWorker.perform_async(1234) end diff --git a/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb b/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb index 94ae4ffa184..8410467ef1f 100644 --- a/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb @@ -23,7 +23,7 @@ describe Gitlab::SidekiqMiddleware::CorrelationLogger do expect_any_instance_of(described_class).to receive(:call).and_call_original expect_any_instance_of(TestWorker).to receive(:perform).with(1234) do - expect(Gitlab::CorrelationId.current_id).to eq('new-correlation-id') + expect(Labkit::Correlation::CorrelationId.current_id).to eq('new-correlation-id') end Sidekiq::Client.push( diff --git a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb new file mode 100644 index 00000000000..1a5a38b5d99 --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe Gitlab::SidekiqMiddleware::MemoryKiller do + subject { described_class.new } + let(:pid) { 999 } + + let(:worker) { double(:worker, class: 'TestWorker') } + let(:job) { { 'jid' => 123 } } + let(:queue) { 'test_queue' } + + def run + thread = subject.call(worker, job, queue) { nil } + thread&.join + end + + before do + allow(subject).to receive(:get_rss).and_return(10.kilobytes) + allow(subject).to receive(:pid).and_return(pid) + end + + context 'when MAX_RSS is set to 0' do + before do + stub_const("#{described_class}::MAX_RSS", 0) + end + + it 'does nothing' do + expect(subject).not_to receive(:sleep) + + run + end + end + + context 'when MAX_RSS is exceeded' do + before do + stub_const("#{described_class}::MAX_RSS", 5.kilobytes) + end + + it 'sends the TSTP, TERM and KILL signals at expected times' do + expect(subject).to receive(:sleep).with(15 * 60).ordered + expect(Process).to receive(:kill).with('SIGTSTP', pid).ordered + + expect(subject).to receive(:sleep).with(30).ordered + expect(Process).to receive(:kill).with('SIGTERM', pid).ordered + + expect(subject).to receive(:sleep).with(10).ordered + expect(Process).to receive(:kill).with('SIGKILL', pid).ordered + + run + end + + it 'sends TSTP and TERM to the pid, but KILL to the pgroup, when running as process leader' do + allow(Process).to receive(:getpgrp) { pid } + allow(subject).to receive(:sleep) + + expect(Process).to receive(:kill).with('SIGTSTP', pid).ordered + expect(Process).to receive(:kill).with('SIGTERM', pid).ordered + expect(Process).to receive(:kill).with('SIGKILL', 0).ordered + + run + end + end + + context 'when MAX_RSS is not exceeded' do + before do + stub_const("#{described_class}::MAX_RSS", 15.kilobytes) + end + + it 'does nothing' do + expect(subject).not_to receive(:sleep) + + run + end + end +end diff --git a/spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb b/spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb deleted file mode 100644 index 0001795c3f0..00000000000 --- a/spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'spec_helper' - -describe Gitlab::SidekiqMiddleware::Shutdown do - subject { described_class.new } - - let(:pid) { Process.pid } - let(:worker) { double(:worker, class: 'TestWorker') } - let(:job) { { 'jid' => 123 } } - let(:queue) { 'test_queue' } - let(:block) { proc { nil } } - - def run - subject.call(worker, job, queue) { block.call } - described_class.shutdown_thread&.join - end - - def pop_trace - subject.trace.pop(true) - end - - before do - allow(subject).to receive(:get_rss).and_return(10.kilobytes) - described_class.clear_shutdown_thread - end - - context 'when MAX_RSS is set to 0' do - before do - stub_const("#{described_class}::MAX_RSS", 0) - end - - it 'does nothing' do - expect(subject).not_to receive(:sleep) - - run - end - end - - def expect_shutdown_sequence - expect(pop_trace).to eq([:sleep, 15 * 60]) - expect(pop_trace).to eq([:kill, 'SIGTSTP', pid]) - - expect(pop_trace).to eq([:sleep, 30]) - expect(pop_trace).to eq([:kill, 'SIGTERM', pid]) - - expect(pop_trace).to eq([:sleep, 10]) - expect(pop_trace).to eq([:kill, 'SIGKILL', pid]) - end - - context 'when MAX_RSS is exceeded' do - before do - stub_const("#{described_class}::MAX_RSS", 5.kilobytes) - end - - it 'sends the TSTP, TERM and KILL signals at expected times' do - run - - expect_shutdown_sequence - end - end - - context 'when MAX_RSS is not exceeded' do - before do - stub_const("#{described_class}::MAX_RSS", 15.kilobytes) - end - - it 'does nothing' do - expect(subject).not_to receive(:sleep) - - run - end - end - - context 'when WantShutdown is raised' do - let(:block) { proc { raise described_class::WantShutdown } } - - it 'starts the shutdown sequence and re-raises the exception' do - expect { run }.to raise_exception(described_class::WantShutdown) - - # We can't expect 'run' to have joined on the shutdown thread, because - # it hit an exception. - shutdown_thread = described_class.shutdown_thread - expect(shutdown_thread).not_to be_nil - shutdown_thread.join - - expect_shutdown_sequence - end - end -end diff --git a/spec/lib/gitlab/sidekiq_signals_spec.rb b/spec/lib/gitlab/sidekiq_signals_spec.rb new file mode 100644 index 00000000000..77ecd1840d2 --- /dev/null +++ b/spec/lib/gitlab/sidekiq_signals_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Gitlab::SidekiqSignals do + describe '.install' do + let(:result) { Hash.new { |h, k| h[k] = 0 } } + let(:int_handler) { -> (_) { result['INT'] += 1 } } + let(:term_handler) { -> (_) { result['TERM'] += 1 } } + let(:other_handler) { -> (_) { result['OTHER'] += 1 } } + let(:signals) { { 'INT' => int_handler, 'TERM' => term_handler, 'OTHER' => other_handler } } + + context 'not a process group leader' do + before do + allow(Process).to receive(:getpgrp) { Process.pid * 2 } + end + + it 'does nothing' do + expect { described_class.install!(signals) } + .not_to change { signals } + end + end + + context 'as a process group leader' do + before do + allow(Process).to receive(:getpgrp) { Process.pid } + end + + it 'installs its own signal handlers for TERM and INT only' do + described_class.install!(signals) + + expect(signals['INT']).not_to eq(int_handler) + expect(signals['TERM']).not_to eq(term_handler) + expect(signals['OTHER']).to eq(other_handler) + end + + %w[INT TERM].each do |signal| + it "installs a forwarding signal handler for #{signal}" do + described_class.install!(signals) + + expect(described_class) + .to receive(:trap) + .with(signal, 'IGNORE') + .and_return(:original_trap) + .ordered + + expect(Process) + .to receive(:kill) + .with(signal, 0) + .ordered + + expect(described_class) + .to receive(:trap) + .with(signal, :original_trap) + .ordered + + signals[signal].call(:cli) + + expect(result[signal]).to eq(1) + end + + it "raises if sidekiq no longer traps SIG#{signal}" do + signals.delete(signal) + + expect { described_class.install!(signals) } + .to raise_error(/Sidekiq should have registered/) + end + end + end + end +end diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb index fe46c67a920..5f0a7e925ca 100644 --- a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb +++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb @@ -15,6 +15,13 @@ describe Gitlab::Template::GitlabCiYmlTemplate do expect(all).to include('Docker') expect(all).to include('Ruby') end + + it 'ensure that the template name is used exactly once' do + all = subject.all.group_by(&:name) + duplicates = all.select { |_, templates| templates.length > 1 } + + expect(duplicates).to be_empty + end end describe '.find' do diff --git a/spec/lib/gitlab/tracing/factory_spec.rb b/spec/lib/gitlab/tracing/factory_spec.rb deleted file mode 100644 index 945490f0988..00000000000 --- a/spec/lib/gitlab/tracing/factory_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -describe Gitlab::Tracing::Factory do - describe '.create_tracer' do - let(:service_name) { 'rspec' } - - context "when tracing is not configured" do - it 'ignores null connection strings' do - expect(described_class.create_tracer(service_name, nil)).to be_nil - end - - it 'ignores empty connection strings' do - expect(described_class.create_tracer(service_name, '')).to be_nil - end - - it 'ignores unknown implementations' do - expect(described_class.create_tracer(service_name, 'opentracing://invalid_driver')).to be_nil - end - - it 'ignores invalid connection strings' do - expect(described_class.create_tracer(service_name, 'open?tracing')).to be_nil - end - end - - context "when tracing is configured with jaeger" do - let(:mock_tracer) { double('tracer') } - - it 'processes default connections' do - expect(Gitlab::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, {}).and_return(mock_tracer) - - expect(described_class.create_tracer(service_name, 'opentracing://jaeger')).to be(mock_tracer) - end - - it 'processes connections with parameters' do - expect(Gitlab::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, { a: '1', b: '2', c: '3' }).and_return(mock_tracer) - - expect(described_class.create_tracer(service_name, 'opentracing://jaeger?a=1&b=2&c=3')).to be(mock_tracer) - end - end - end -end diff --git a/spec/lib/gitlab/tracing/grpc_interceptor_spec.rb b/spec/lib/gitlab/tracing/grpc_interceptor_spec.rb deleted file mode 100644 index 7f5aecb7baa..00000000000 --- a/spec/lib/gitlab/tracing/grpc_interceptor_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -describe Gitlab::Tracing::GRPCInterceptor do - subject { described_class.instance } - - shared_examples_for "a grpc interceptor method" do - let(:custom_error) { Class.new(StandardError) } - - it 'yields' do - expect { |b| method.call(kwargs, &b) }.to yield_control - end - - it 'propagates exceptions' do - expect { method.call(kwargs) { raise custom_error } }.to raise_error(custom_error) - end - end - - describe '#request_response' do - let(:method) { subject.method(:request_response) } - let(:kwargs) { { request: {}, call: {}, method: 'grc_method', metadata: {} } } - - it_behaves_like 'a grpc interceptor method' - end - - describe '#client_streamer' do - let(:method) { subject.method(:client_streamer) } - let(:kwargs) { { requests: [], call: {}, method: 'grc_method', metadata: {} } } - - it_behaves_like 'a grpc interceptor method' - end - - describe '#server_streamer' do - let(:method) { subject.method(:server_streamer) } - let(:kwargs) { { request: {}, call: {}, method: 'grc_method', metadata: {} } } - - it_behaves_like 'a grpc interceptor method' - end - - describe '#bidi_streamer' do - let(:method) { subject.method(:bidi_streamer) } - let(:kwargs) { { requests: [], call: {}, method: 'grc_method', metadata: {} } } - - it_behaves_like 'a grpc interceptor method' - end -end diff --git a/spec/lib/gitlab/tracing/jaeger_factory_spec.rb b/spec/lib/gitlab/tracing/jaeger_factory_spec.rb deleted file mode 100644 index 3d6a007cfd9..00000000000 --- a/spec/lib/gitlab/tracing/jaeger_factory_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -describe Gitlab::Tracing::JaegerFactory do - describe '.create_tracer' do - let(:service_name) { 'rspec' } - - shared_examples_for 'a jaeger tracer' do - it 'responds to active_span methods' do - expect(tracer).to respond_to(:active_span) - end - - it 'yields control' do - expect { |b| tracer.start_active_span('operation_name', &b) }.to yield_control - end - end - - context 'processes default connections' do - it_behaves_like 'a jaeger tracer' do - let(:tracer) { described_class.create_tracer(service_name, {}) } - end - end - - context 'handles debug options' do - it_behaves_like 'a jaeger tracer' do - let(:tracer) { described_class.create_tracer(service_name, { debug: "1" }) } - end - end - - context 'handles const sampler' do - it_behaves_like 'a jaeger tracer' do - let(:tracer) { described_class.create_tracer(service_name, { sampler: "const", sampler_param: "1" }) } - end - end - - context 'handles probabilistic sampler' do - it_behaves_like 'a jaeger tracer' do - let(:tracer) { described_class.create_tracer(service_name, { sampler: "probabilistic", sampler_param: "0.5" }) } - end - end - - context 'handles http_endpoint configurations' do - it_behaves_like 'a jaeger tracer' do - let(:tracer) { described_class.create_tracer(service_name, { http_endpoint: "http://localhost:1234" }) } - end - end - - context 'handles udp_endpoint configurations' do - it_behaves_like 'a jaeger tracer' do - let(:tracer) { described_class.create_tracer(service_name, { udp_endpoint: "localhost:4321" }) } - end - end - - context 'ignores invalid parameters' do - it_behaves_like 'a jaeger tracer' do - let(:tracer) { described_class.create_tracer(service_name, { invalid: "true" }) } - end - end - - context 'accepts the debug parameter when strict_parser is set' do - it_behaves_like 'a jaeger tracer' do - let(:tracer) { described_class.create_tracer(service_name, { debug: "1", strict_parsing: "1" }) } - end - end - - it 'rejects invalid parameters when strict_parser is set' do - expect { described_class.create_tracer(service_name, { invalid: "true", strict_parsing: "1" }) }.to raise_error(StandardError) - end - end -end diff --git a/spec/lib/gitlab/tracing/rack_middleware_spec.rb b/spec/lib/gitlab/tracing/rack_middleware_spec.rb deleted file mode 100644 index 13d4d8a89f7..00000000000 --- a/spec/lib/gitlab/tracing/rack_middleware_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Gitlab::Tracing::RackMiddleware do - using RSpec::Parameterized::TableSyntax - - describe '#call' do - context 'for normal middleware flow' do - let(:fake_app) { -> (env) { fake_app_response } } - subject { described_class.new(fake_app) } - let(:request) { } - - context 'for 200 responses' do - let(:fake_app_response) { [200, { 'Content-Type': 'text/plain' }, ['OK']] } - - it 'delegates correctly' do - expect(subject.call(Rack::MockRequest.env_for("/"))).to eq(fake_app_response) - end - end - - context 'for 500 responses' do - let(:fake_app_response) { [500, { 'Content-Type': 'text/plain' }, ['Error']] } - - it 'delegates correctly' do - expect(subject.call(Rack::MockRequest.env_for("/"))).to eq(fake_app_response) - end - end - end - - context 'when an application is raising an exception' do - let(:custom_error) { Class.new(StandardError) } - let(:fake_app) { ->(env) { raise custom_error } } - - subject { described_class.new(fake_app) } - - it 'delegates propagates exceptions correctly' do - expect { subject.call(Rack::MockRequest.env_for("/")) }.to raise_error(custom_error) - end - end - end - - describe '.build_sanitized_url_from_env' do - def env_for_url(url) - env = Rack::MockRequest.env_for(input_url) - env['action_dispatch.parameter_filter'] = [/token/] - - env - end - - where(:input_url, :output_url) do - '/gitlab-org/gitlab-ce' | 'http://example.org/gitlab-org/gitlab-ce' - '/gitlab-org/gitlab-ce?safe=1' | 'http://example.org/gitlab-org/gitlab-ce?safe=1' - '/gitlab-org/gitlab-ce?private_token=secret' | 'http://example.org/gitlab-org/gitlab-ce?private_token=%5BFILTERED%5D' - '/gitlab-org/gitlab-ce?mixed=1&private_token=secret' | 'http://example.org/gitlab-org/gitlab-ce?mixed=1&private_token=%5BFILTERED%5D' - end - - with_them do - it { expect(described_class.build_sanitized_url_from_env(env_for_url(input_url))).to eq(output_url) } - end - end -end diff --git a/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb deleted file mode 100644 index c9d1a06b3e6..00000000000 --- a/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb +++ /dev/null @@ -1,147 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' -require 'rspec-parameterized' - -describe Gitlab::Tracing::Rails::ActionViewSubscriber do - using RSpec::Parameterized::TableSyntax - - shared_examples 'an actionview notification' do - it 'should notify the tracer when the hash contains null values' do - expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception) - - subject.public_send(notify_method, start, finish, payload) - end - - it 'should notify the tracer when the payload is missing values' do - expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception) - - subject.public_send(notify_method, start, finish, payload.compact) - end - - it 'should not throw exceptions when with the default tracer' do - expect { subject.public_send(notify_method, start, finish, payload) }.not_to raise_error - end - end - - describe '.instrument' do - it 'is unsubscribeable' do - unsubscribe = described_class.instrument - - expect(unsubscribe).not_to be_nil - expect { unsubscribe.call }.not_to raise_error - end - end - - describe '#notify_render_template' do - subject { described_class.new } - let(:start) { Time.now } - let(:finish) { Time.now } - let(:notification_name) { 'render_template' } - let(:notify_method) { :notify_render_template } - - where(:identifier, :layout, :exception) do - nil | nil | nil - "" | nil | nil - "show.haml" | nil | nil - nil | "" | nil - nil | "layout.haml" | nil - nil | nil | StandardError.new - end - - with_them do - let(:payload) do - { - exception: exception, - identifier: identifier, - layout: layout - } - end - - let(:expected_tags) do - { - 'component' => 'ActionView', - 'template.id' => identifier, - 'template.layout' => layout - } - end - - it_behaves_like 'an actionview notification' - end - end - - describe '#notify_render_collection' do - subject { described_class.new } - let(:start) { Time.now } - let(:finish) { Time.now } - let(:notification_name) { 'render_collection' } - let(:notify_method) { :notify_render_collection } - - where( - :identifier, :count, :expected_count, :cache_hits, :expected_cache_hits, :exception) do - nil | nil | 0 | nil | 0 | nil - "" | nil | 0 | nil | 0 | nil - "show.haml" | nil | 0 | nil | 0 | nil - nil | 0 | 0 | nil | 0 | nil - nil | 1 | 1 | nil | 0 | nil - nil | nil | 0 | 0 | 0 | nil - nil | nil | 0 | 1 | 1 | nil - nil | nil | 0 | nil | 0 | StandardError.new - end - - with_them do - let(:payload) do - { - exception: exception, - identifier: identifier, - count: count, - cache_hits: cache_hits - } - end - - let(:expected_tags) do - { - 'component' => 'ActionView', - 'template.id' => identifier, - 'template.count' => expected_count, - 'template.cache.hits' => expected_cache_hits - } - end - - it_behaves_like 'an actionview notification' - end - end - - describe '#notify_render_partial' do - subject { described_class.new } - let(:start) { Time.now } - let(:finish) { Time.now } - let(:notification_name) { 'render_partial' } - let(:notify_method) { :notify_render_partial } - - where(:identifier, :exception) do - nil | nil - "" | nil - "show.haml" | nil - nil | StandardError.new - end - - with_them do - let(:payload) do - { - exception: exception, - identifier: identifier - } - end - - let(:expected_tags) do - { - 'component' => 'ActionView', - 'template.id' => identifier - } - end - - it_behaves_like 'an actionview notification' - end - end -end diff --git a/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb deleted file mode 100644 index 3d066843148..00000000000 --- a/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' -require 'rspec-parameterized' - -describe Gitlab::Tracing::Rails::ActiveRecordSubscriber do - using RSpec::Parameterized::TableSyntax - - describe '.instrument' do - it 'is unsubscribeable' do - unsubscribe = described_class.instrument - - expect(unsubscribe).not_to be_nil - expect { unsubscribe.call }.not_to raise_error - end - end - - describe '#notify' do - subject { described_class.new } - let(:start) { Time.now } - let(:finish) { Time.now } - - where(:name, :operation_name, :exception, :connection_id, :cached, :cached_response, :sql) do - nil | "active_record:sqlquery" | nil | nil | nil | false | nil - "" | "active_record:sqlquery" | nil | nil | nil | false | nil - "User Load" | "active_record:User Load" | nil | nil | nil | false | nil - "Repo Load" | "active_record:Repo Load" | StandardError.new | nil | nil | false | nil - nil | "active_record:sqlquery" | nil | 123 | nil | false | nil - nil | "active_record:sqlquery" | nil | nil | false | false | nil - nil | "active_record:sqlquery" | nil | nil | true | true | nil - nil | "active_record:sqlquery" | nil | nil | true | true | "SELECT * FROM users" - end - - with_them do - def payload - { - name: name, - exception: exception, - connection_id: connection_id, - cached: cached, - sql: sql - } - end - - def expected_tags - { - "component" => "ActiveRecord", - "span.kind" => "client", - "db.type" => "sql", - "db.connection_id" => connection_id, - "db.cached" => cached_response, - "db.statement" => sql - } - end - - it 'should notify the tracer when the hash contains null values' do - expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception) - - subject.notify(start, finish, payload) - end - - it 'should notify the tracer when the payload is missing values' do - expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception) - - subject.notify(start, finish, payload.compact) - end - - it 'should not throw exceptions when with the default tracer' do - expect { subject.notify(start, finish, payload) }.not_to raise_error - end - end - end -end diff --git a/spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb b/spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb deleted file mode 100644 index 3755860b5ba..00000000000 --- a/spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -describe Gitlab::Tracing::Sidekiq::ClientMiddleware do - describe '#call' do - let(:worker_class) { 'test_worker_class' } - let(:job) do - { - 'class' => "jobclass", - 'queue' => "jobqueue", - 'retry' => 0, - 'args' => %w{1 2 3} - } - end - let(:queue) { 'test_queue' } - let(:redis_pool) { double("redis_pool") } - let(:custom_error) { Class.new(StandardError) } - let(:span) { OpenTracing.start_span('test', ignore_active_scope: true) } - - subject { described_class.new } - - it 'yields' do - expect(subject).to receive(:in_tracing_span).with( - operation_name: "sidekiq:jobclass", - tags: { - "component" => "sidekiq", - "span.kind" => "client", - "sidekiq.queue" => "jobqueue", - "sidekiq.jid" => nil, - "sidekiq.retry" => "0", - "sidekiq.args" => "1, 2, 3" - } - ).and_yield(span) - - expect { |b| subject.call(worker_class, job, queue, redis_pool, &b) }.to yield_control - end - - it 'propagates exceptions' do - expect { subject.call(worker_class, job, queue, redis_pool) { raise custom_error } }.to raise_error(custom_error) - end - end -end diff --git a/spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb b/spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb deleted file mode 100644 index c3087de785a..00000000000 --- a/spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -describe Gitlab::Tracing::Sidekiq::ServerMiddleware do - describe '#call' do - let(:worker_class) { 'test_worker_class' } - let(:job) do - { - 'class' => "jobclass", - 'queue' => "jobqueue", - 'retry' => 0, - 'args' => %w{1 2 3} - } - end - let(:queue) { 'test_queue' } - let(:custom_error) { Class.new(StandardError) } - let(:span) { OpenTracing.start_span('test', ignore_active_scope: true) } - subject { described_class.new } - - it 'yields' do - expect(subject).to receive(:in_tracing_span).with( - hash_including( - operation_name: "sidekiq:jobclass", - tags: { - "component" => "sidekiq", - "span.kind" => "server", - "sidekiq.queue" => "jobqueue", - "sidekiq.jid" => nil, - "sidekiq.retry" => "0", - "sidekiq.args" => "1, 2, 3" - } - ) - ).and_yield(span) - - expect { |b| subject.call(worker_class, job, queue, &b) }.to yield_control - end - - it 'propagates exceptions' do - expect { subject.call(worker_class, job, queue) { raise custom_error } }.to raise_error(custom_error) - end - end -end diff --git a/spec/lib/gitlab/tracing_spec.rb b/spec/lib/gitlab/tracing_spec.rb index 566b5050e47..db75ce2a998 100644 --- a/spec/lib/gitlab/tracing_spec.rb +++ b/spec/lib/gitlab/tracing_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Tracing do end with_them do - it 'should return the correct state for .enabled?' do + it 'returns the correct state for .enabled?' do expect(described_class).to receive(:connection_string).and_return(connection_string) expect(described_class.enabled?).to eq(enabled_state) @@ -33,7 +33,7 @@ describe Gitlab::Tracing do end with_them do - it 'should return the correct state for .tracing_url_enabled?' do + it 'returns the correct state for .tracing_url_enabled?' do expect(described_class).to receive(:enabled?).and_return(enabled?) allow(described_class).to receive(:tracing_url_template).and_return(tracing_url_template) @@ -56,7 +56,7 @@ describe Gitlab::Tracing do end with_them do - it 'should return the correct state for .tracing_url' do + it 'returns the correct state for .tracing_url' do expect(described_class).to receive(:tracing_url_enabled?).and_return(tracing_url_enabled?) allow(described_class).to receive(:tracing_url_template).and_return(tracing_url_template) allow(Gitlab::CorrelationId).to receive(:current_id).and_return(correlation_id) diff --git a/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb b/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb new file mode 100644 index 00000000000..f1882e03581 --- /dev/null +++ b/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb @@ -0,0 +1,118 @@ +require 'fast_spec_helper' +require 'support/shared_examples/malicious_regexp_shared_examples' +require 'support/helpers/stub_feature_flags' + +describe Gitlab::UntrustedRegexp::RubySyntax do + describe '.matches_syntax?' do + it 'returns true if regexp is valid' do + expect(described_class.matches_syntax?('/some .* thing/')) + .to be true + end + + it 'returns true if regexp is invalid, but resembles regexp' do + expect(described_class.matches_syntax?('/some ( thing/')) + .to be true + end + end + + describe '.valid?' do + it 'returns true if regexp is valid' do + expect(described_class.valid?('/some .* thing/')) + .to be true + end + + it 'returns false if regexp is invalid' do + expect(described_class.valid?('/some ( thing/')) + .to be false + end + end + + describe '.fabricate' do + context 'when regexp is valid' do + it 'fabricates regexp without flags' do + expect(described_class.fabricate('/some .* thing/')).not_to be_nil + end + end + + context 'when regexp is empty' do + it 'fabricates regexp correctly' do + expect(described_class.fabricate('//')).not_to be_nil + end + end + + context 'when regexp is a raw pattern' do + it 'returns error' do + expect(described_class.fabricate('some .* thing')).to be_nil + end + end + end + + describe '.fabricate!' do + context 'safe regexp is used' do + context 'when regexp is using /regexp/ scheme with flags' do + it 'fabricates regexp with a single flag' do + regexp = described_class.fabricate!('/something/i') + + expect(regexp).to eq Gitlab::UntrustedRegexp.new('(?i)something') + expect(regexp.scan('SOMETHING')).to be_one + end + + it 'fabricates regexp with multiple flags' do + regexp = described_class.fabricate!('/something/im') + + expect(regexp).to eq Gitlab::UntrustedRegexp.new('(?im)something') + end + + it 'fabricates regexp without flags' do + regexp = described_class.fabricate!('/something/') + + expect(regexp).to eq Gitlab::UntrustedRegexp.new('something') + end + end + end + + context 'when unsafe regexp is used' do + include StubFeatureFlags + + before do + stub_feature_flags(allow_unsafe_ruby_regexp: true) + + allow(Gitlab::UntrustedRegexp).to receive(:new).and_raise(RegexpError) + end + + context 'when no fallback is enabled' do + it 'raises an exception' do + expect { described_class.fabricate!('/something/') } + .to raise_error(RegexpError) + end + end + + context 'when fallback is used' do + it 'fabricates regexp with a single flag' do + regexp = described_class.fabricate!('/something/i', fallback: true) + + expect(regexp).to eq Regexp.new('something', Regexp::IGNORECASE) + end + + it 'fabricates regexp with multiple flags' do + regexp = described_class.fabricate!('/something/im', fallback: true) + + expect(regexp).to eq Regexp.new('something', Regexp::IGNORECASE | Regexp::MULTILINE) + end + + it 'fabricates regexp without flags' do + regexp = described_class.fabricate!('/something/', fallback: true) + + expect(regexp).to eq Regexp.new('something') + end + end + end + + context 'when regexp is a raw pattern' do + it 'raises an error' do + expect { described_class.fabricate!('some .* thing') } + .to raise_error(RegexpError) + end + end + end +end diff --git a/spec/lib/gitlab/untrusted_regexp_spec.rb b/spec/lib/gitlab/untrusted_regexp_spec.rb index 0a6ac0aa294..9d483f13a5e 100644 --- a/spec/lib/gitlab/untrusted_regexp_spec.rb +++ b/spec/lib/gitlab/untrusted_regexp_spec.rb @@ -2,48 +2,6 @@ require 'fast_spec_helper' require 'support/shared_examples/malicious_regexp_shared_examples' describe Gitlab::UntrustedRegexp do - describe '.valid?' do - it 'returns true if regexp is valid' do - expect(described_class.valid?('/some ( thing/')) - .to be false - end - - it 'returns true if regexp is invalid' do - expect(described_class.valid?('/some .* thing/')) - .to be true - end - end - - describe '.fabricate' do - context 'when regexp is using /regexp/ scheme with flags' do - it 'fabricates regexp with a single flag' do - regexp = described_class.fabricate('/something/i') - - expect(regexp).to eq described_class.new('(?i)something') - expect(regexp.scan('SOMETHING')).to be_one - end - - it 'fabricates regexp with multiple flags' do - regexp = described_class.fabricate('/something/im') - - expect(regexp).to eq described_class.new('(?im)something') - end - - it 'fabricates regexp without flags' do - regexp = described_class.fabricate('/something/') - - expect(regexp).to eq described_class.new('something') - end - end - - context 'when regexp is a raw pattern' do - it 'raises an error' do - expect { described_class.fabricate('some .* thing') } - .to raise_error(RegexpError) - end - end - end - describe '#initialize' do subject { described_class.new(pattern) } @@ -92,11 +50,41 @@ describe Gitlab::UntrustedRegexp do end end + describe '#match?' do + subject { described_class.new(regexp).match?(text) } + + context 'malicious regexp' do + let(:text) { malicious_text } + let(:regexp) { malicious_regexp_re2 } + + include_examples 'malicious regexp' + end + + context 'matching regexp' do + let(:regexp) { 'foo' } + let(:text) { 'foo' } + + it 'returns an array of nil matches' do + is_expected.to eq(true) + end + end + + context 'non-matching regexp' do + let(:regexp) { 'boo' } + let(:text) { 'foo' } + + it 'returns an array of nil matches' do + is_expected.to eq(false) + end + end + end + describe '#scan' do subject { described_class.new(regexp).scan(text) } + context 'malicious regexp' do let(:text) { malicious_text } - let(:regexp) { malicious_regexp } + let(:regexp) { malicious_regexp_re2 } include_examples 'malicious regexp' end diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index 62970bd8cb6..253366e0789 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -2,6 +2,87 @@ require 'spec_helper' describe Gitlab::UrlBlocker do + describe '#validate!' do + context 'when URI is nil' do + let(:import_url) { nil } + + it 'returns no URI and hostname' do + uri, hostname = described_class.validate!(import_url) + + expect(uri).to be(nil) + expect(hostname).to be(nil) + end + end + + context 'when URI is internal' do + let(:import_url) { 'http://localhost' } + + it 'returns URI and no hostname' do + uri, hostname = described_class.validate!(import_url) + + expect(uri).to eq(Addressable::URI.parse('http://[::1]')) + expect(hostname).to eq('localhost') + end + end + + context 'when the URL hostname is a domain' do + let(:import_url) { 'https://example.org' } + + it 'returns URI and hostname' do + uri, hostname = described_class.validate!(import_url) + + expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34')) + expect(hostname).to eq('example.org') + end + end + + context 'when the URL hostname is an IP address' do + let(:import_url) { 'https://93.184.216.34' } + + it 'returns URI and no hostname' do + uri, hostname = described_class.validate!(import_url) + + expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34')) + expect(hostname).to be(nil) + end + end + + context 'disabled DNS rebinding protection' do + context 'when URI is internal' do + let(:import_url) { 'http://localhost' } + + it 'returns URI and no hostname' do + uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false) + + expect(uri).to eq(Addressable::URI.parse('http://localhost')) + expect(hostname).to be(nil) + end + end + + context 'when the URL hostname is a domain' do + let(:import_url) { 'https://example.org' } + + it 'returns URI and no hostname' do + uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false) + + expect(uri).to eq(Addressable::URI.parse('https://example.org')) + expect(hostname).to eq(nil) + end + end + + context 'when the URL hostname is an IP address' do + let(:import_url) { 'https://93.184.216.34' } + + it 'returns URI and no hostname' do + uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false) + + expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34')) + expect(hostname).to be(nil) + end + end + end + end + describe '#blocked_url?' do let(:ports) { Project::VALID_IMPORT_PORTS } @@ -23,10 +104,10 @@ describe Gitlab::UrlBlocker do expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git', ports: ports)).to be true end - it 'returns true for bad protocol' do - expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['https'])).to be false + it 'returns true for bad scheme' do + expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', schemes: ['https'])).to be false expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false - expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true + expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', schemes: ['http'])).to be true end it 'returns true for bad protocol on configured web/SSH host and ports' do @@ -208,7 +289,7 @@ describe Gitlab::UrlBlocker do end def stub_domain_resolv(domain, ip) - address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false) + address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false, ipv4?: false) allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([address]) allow(address).to receive(:ipv6_v4mapped?).and_return(false) end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 9f495a5d50b..bbcb92608d8 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -32,7 +32,7 @@ describe Gitlab::UrlBuilder do url = described_class.build(milestone) - expect(url).to eq "#{Settings.gitlab['url']}/#{milestone.project.full_path}/milestones/#{milestone.iid}" + expect(url).to eq "#{Settings.gitlab['url']}/#{milestone.project.full_path}/-/milestones/#{milestone.iid}" end end diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index 6e98a999766..7242255d535 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -115,6 +115,40 @@ describe Gitlab::UrlSanitizer do end end + describe '#user' do + context 'credentials in hash' do + it 'overrides URL-provided user' do + sanitizer = described_class.new('http://a:b@example.com', credentials: { user: 'c', password: 'd' }) + + expect(sanitizer.user).to eq('c') + end + end + + context 'credentials in URL' do + where(:url, :user) do + 'http://foo:bar@example.com' | 'foo' + 'http://foo:bar:baz@example.com' | 'foo' + 'http://:bar@example.com' | nil + 'http://foo:@example.com' | 'foo' + 'http://foo@example.com' | 'foo' + 'http://:@example.com' | nil + 'http://@example.com' | nil + 'http://example.com' | nil + + # Other invalid URLs + nil | nil + '' | nil + 'no' | nil + end + + with_them do + subject { described_class.new(url).user } + + it { is_expected.to eq(user) } + end + end + end + describe '#full_url' do context 'credentials in hash' do where(:credentials, :userinfo) do @@ -161,7 +195,7 @@ describe Gitlab::UrlSanitizer do end context 'when credentials contains special chars' do - it 'should parse the URL without errors' do + it 'parses the URL without errors' do url_sanitizer = described_class.new("https://foo:b?r@github.com/me/project.git") expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index d3eae80cc56..e44463dd767 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -13,6 +13,8 @@ describe Gitlab::UsageData do create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true) create(:service, project: projects[1], type: 'SlackService', active: true) create(:service, project: projects[2], type: 'SlackService', active: true) + create(:project_error_tracking_setting, project: projects[0]) + create(:project_error_tracking_setting, project: projects[1], enabled: false) gcp_cluster = create(:cluster, :provided_by_gcp) create(:cluster, :provided_by_user) @@ -33,7 +35,7 @@ describe Gitlab::UsageData do subject { described_class.data } it "gathers usage data" do - expect(subject.keys).to match_array(%i( + expect(subject.keys).to include(*%i( active_user_count counts recorded_at @@ -55,16 +57,13 @@ describe Gitlab::UsageData do database avg_cycle_analytics web_ide_commits + influxdb_metrics_enabled + prometheus_metrics_enabled )) end it "gathers usage counts" do - count_data = subject[:counts] - - expect(count_data[:boards]).to eq(1) - expect(count_data[:projects]).to eq(3) - - expect(count_data.keys).to match_array(%i( + expected_keys = %i( assignee_lists boards ci_builds @@ -79,6 +78,8 @@ describe Gitlab::UsageData do auto_devops_disabled deploy_keys deployments + successful_deployments + failed_deployments environments clusters clusters_enabled @@ -106,6 +107,7 @@ describe Gitlab::UsageData do milestone_lists milestones notes + pool_repositories projects projects_imported_from_github projects_jira_active @@ -115,6 +117,7 @@ describe Gitlab::UsageData do projects_slack_slash_active projects_prometheus_active projects_with_repositories_enabled + projects_with_error_tracking_enabled pages_domains protected_branches releases @@ -125,7 +128,14 @@ describe Gitlab::UsageData do uploads web_hooks user_preferences - )) + ) + + count_data = subject[:counts] + + expect(count_data[:boards]).to eq(1) + expect(count_data[:projects]).to eq(3) + expect(count_data.keys).to include(*expected_keys) + expect(expected_keys - count_data.keys).to be_empty end it 'does not gather user preferences usage data when the feature is disabled' do @@ -144,6 +154,7 @@ describe Gitlab::UsageData do expect(count_data[:projects_slack_notifications_active]).to eq(2) expect(count_data[:projects_slack_slash_active]).to eq(1) expect(count_data[:projects_with_repositories_enabled]).to eq(2) + expect(count_data[:projects_with_error_tracking_enabled]).to eq(1) expect(count_data[:clusters_enabled]).to eq(7) expect(count_data[:project_clusters_enabled]).to eq(6) @@ -203,7 +214,7 @@ describe Gitlab::UsageData do it "gathers license data" do expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid) expect(subject[:version]).to eq(Gitlab::VERSION) - expect(subject[:installation_type]).to eq(Gitlab::INSTALLATION_TYPE) + expect(subject[:installation_type]).to eq('gitlab-development-kit') expect(subject[:active_user_count]).to eq(User.active.count) expect(subject[:recorded_at]).to be_a(Time) end diff --git a/spec/lib/gitlab/user_extractor_spec.rb b/spec/lib/gitlab/user_extractor_spec.rb index fcc05ab3a0c..b86ec5445b8 100644 --- a/spec/lib/gitlab/user_extractor_spec.rb +++ b/spec/lib/gitlab/user_extractor_spec.rb @@ -38,6 +38,18 @@ describe Gitlab::UserExtractor do expect(extractor.users).to include(user) end + + context 'input as array of strings' do + it 'is treated as one string' do + extractor = described_class.new(text.lines) + + user_1 = create(:user, username: "USER-1") + user_4 = create(:user, username: "USER-4") + user_email = create(:user, email: 'user@gitlab.org') + + expect(extractor.users).to contain_exactly(user_1, user_4, user_email) + end + end end describe '#matches' do @@ -48,6 +60,14 @@ describe Gitlab::UserExtractor do it 'includes all mentioned usernames' do expect(extractor.matches[:usernames]).to contain_exactly('user-1', 'user-2', 'user-4') end + + context 'input has no matching e-mail or usernames' do + it 'returns an empty list of users' do + extractor = described_class.new('My test') + + expect(extractor.users).to be_empty + end + end end describe '#references' do diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 8f5029b3565..4645339f439 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -213,4 +213,22 @@ describe Gitlab::Utils do expect(subject[:variables].first[:key]).to eq('VAR1') end end + + describe '.try_megabytes_to_bytes' do + context 'when the size can be converted to megabytes' do + it 'returns the size in megabytes' do + size = described_class.try_megabytes_to_bytes(1) + + expect(size).to eq(1.megabytes) + end + end + + context 'when the size can not be converted to megabytes' do + it 'returns the input size' do + size = described_class.try_megabytes_to_bytes('foo') + + expect(size).to eq('foo') + end + end + end end diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb index 2c1146ceff5..0a170a157fe 100644 --- a/spec/lib/gitlab/visibility_level_spec.rb +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -85,4 +85,12 @@ describe Gitlab::VisibilityLevel do .to eq(described_class::PRIVATE) end end + + describe '.valid_level?' do + it 'returns true when visibility is valid' do + expect(described_class.valid_level?(described_class::PRIVATE)).to be_truthy + expect(described_class.valid_level?(described_class::INTERNAL)).to be_truthy + expect(described_class.valid_level?(described_class::PUBLIC)).to be_truthy + end + end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 7213eee5675..f8332757fcd 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -16,40 +16,80 @@ describe Gitlab::Workhorse do let(:ref) { 'master' } let(:format) { 'zip' } let(:storage_path) { Gitlab.config.gitlab.repository_downloads_path } - let(:base_params) { repository.archive_metadata(ref, storage_path, format, append_sha: nil) } - let(:gitaly_params) do - base_params.merge( - 'GitalyServer' => { - 'address' => Gitlab::GitalyClient.address(project.repository_storage), - 'token' => Gitlab::GitalyClient.token(project.repository_storage) - }, - 'GitalyRepository' => repository.gitaly_repository.to_h.deep_stringify_keys - ) - end + let(:path) { 'some/path' if Feature.enabled?(:git_archive_path, default_enabled: true) } + let(:metadata) { repository.archive_metadata(ref, storage_path, format, append_sha: nil, path: path) } let(:cache_disabled) { false } subject do - described_class.send_git_archive(repository, ref: ref, format: format, append_sha: nil) + described_class.send_git_archive(repository, ref: ref, format: format, append_sha: nil, path: path) end before do allow(described_class).to receive(:git_archive_cache_disabled?).and_return(cache_disabled) end - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) + context 'feature flag disabled' do + before do + stub_feature_flags(git_archive_path: false) + end - expect(key).to eq('Gitlab-Workhorse-Send-Data') - expect(command).to eq('git-archive') - expect(params).to include(gitaly_params) + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) + + expected_params = metadata.merge( + 'GitalyRepository' => repository.gitaly_repository.to_h, + 'GitalyServer' => { + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + } + ) + + expect(key).to eq('Gitlab-Workhorse-Send-Data') + expect(command).to eq('git-archive') + expect(params).to eq(expected_params.deep_stringify_keys) + end + + context 'when archive caching is disabled' do + let(:cache_disabled) { true } + + it 'tells workhorse not to use the cache' do + _, _, params = decode_workhorse_header(subject) + expect(params).to include({ 'DisableCache' => true }) + end + end end - context 'when archive caching is disabled' do - let(:cache_disabled) { true } + context 'feature flag enabled' do + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) + + expect(key).to eq('Gitlab-Workhorse-Send-Data') + expect(command).to eq('git-archive') + expect(params).to eq({ + 'GitalyServer' => { + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + }, + 'ArchivePath' => metadata['ArchivePath'], + 'GetArchiveRequest' => Base64.encode64( + Gitaly::GetArchiveRequest.new( + repository: repository.gitaly_repository, + commit_id: metadata['CommitId'], + prefix: metadata['ArchivePrefix'], + format: Gitaly::GetArchiveRequest::Format::ZIP, + path: path + ).to_proto + ) + }.deep_stringify_keys) + end - it 'tells workhorse not to use the cache' do - _, _, params = decode_workhorse_header(subject) - expect(params).to include({ 'DisableCache' => true }) + context 'when archive caching is disabled' do + let(:cache_disabled) { true } + + it 'tells workhorse not to use the cache' do + _, _, params = decode_workhorse_header(subject) + expect(params).to include({ 'DisableCache' => true }) + end end end @@ -87,7 +127,7 @@ describe Gitlab::Workhorse do end end - describe '.terminal_websocket' do + describe '.channel_websocket' do def terminal(ca_pem: nil) out = { subprotocols: ['foo'], @@ -101,25 +141,25 @@ describe Gitlab::Workhorse do def workhorse(ca_pem: nil) out = { - 'Terminal' => { + 'Channel' => { 'Subprotocols' => ['foo'], 'Url' => 'wss://example.com/terminal.ws', 'Header' => { 'Authorization' => ['Token x'] }, 'MaxSessionTime' => 600 } } - out['Terminal']['CAPem'] = ca_pem if ca_pem + out['Channel']['CAPem'] = ca_pem if ca_pem out end context 'without ca_pem' do - subject { described_class.terminal_websocket(terminal) } + subject { described_class.channel_websocket(terminal) } it { is_expected.to eq(workhorse) } end context 'with ca_pem' do - subject { described_class.terminal_websocket(terminal(ca_pem: "foo")) } + subject { described_class.channel_websocket(terminal(ca_pem: "foo")) } it { is_expected.to eq(workhorse(ca_pem: "foo")) } end @@ -250,11 +290,11 @@ describe Gitlab::Workhorse do } end - subject { described_class.git_http_ok(repository, false, user, action) } + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action) } it { expect(subject).to include(params) } - context 'when is_wiki' do + context 'when the repo_type is a wiki' do let(:params) do { GL_ID: "user-#{user.id}", @@ -264,7 +304,7 @@ describe Gitlab::Workhorse do } end - subject { described_class.git_http_ok(repository, true, user, action) } + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::WIKI, user, action) } it { expect(subject).to include(params) } end @@ -304,7 +344,7 @@ describe Gitlab::Workhorse do end context 'show_all_refs enabled' do - subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) } + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) } it { is_expected.to include(ShowAllRefs: true) } end @@ -322,7 +362,7 @@ describe Gitlab::Workhorse do it { expect(subject).to include(gitaly_params) } context 'show_all_refs enabled' do - subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) } + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) } it { is_expected.to include(ShowAllRefs: true) } end diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index 5f7a0cca351..e075904b0cc 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -10,7 +10,7 @@ describe Gitlab do end describe '.revision' do - let(:cmd) { %W[#{described_class.config.git.bin_path} log --pretty=format:%h -n 1] } + let(:cmd) { %W[#{described_class.config.git.bin_path} log --pretty=format:%h --abbrev=11 -n 1] } around do |example| described_class.instance_variable_set(:@_revision, nil) @@ -95,4 +95,48 @@ describe Gitlab do expect(described_class.com?).to eq false end end + + describe '.ee?' do + it 'returns true when using Enterprise Edition' do + stub_const('License', Class.new) + + expect(described_class.ee?).to eq(true) + end + + it 'returns false when using Community Edition' do + hide_const('License') + + expect(described_class.ee?).to eq(false) + end + end + + describe '.http_proxy_env?' do + it 'returns true when lower case https' do + stub_env('https_proxy', 'https://my.proxy') + + expect(described_class.http_proxy_env?).to eq(true) + end + + it 'returns true when upper case https' do + stub_env('HTTPS_PROXY', 'https://my.proxy') + + expect(described_class.http_proxy_env?).to eq(true) + end + + it 'returns true when lower case http' do + stub_env('http_proxy', 'http://my.proxy') + + expect(described_class.http_proxy_env?).to eq(true) + end + + it 'returns true when upper case http' do + stub_env('HTTP_PROXY', 'http://my.proxy') + + expect(described_class.http_proxy_env?).to eq(true) + end + + it 'returns false when not set' do + expect(described_class.http_proxy_env?).to eq(false) + end + end end diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb index e2134dc279c..1fefc947636 100644 --- a/spec/lib/google_api/cloud_platform/client_spec.rb +++ b/spec/lib/google_api/cloud_platform/client_spec.rb @@ -97,6 +97,12 @@ describe GoogleApi::CloudPlatform::Client do "node_config": { "machine_type": machine_type }, + "master_auth": { + "username": "admin", + "client_certificate_config": { + issue_client_certificate: true + } + }, "legacy_abac": { "enabled": true } @@ -122,6 +128,12 @@ describe GoogleApi::CloudPlatform::Client do "node_config": { "machine_type": machine_type }, + "master_auth": { + "username": "admin", + "client_certificate_config": { + issue_client_certificate: true + } + }, "legacy_abac": { "enabled": false } diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index 77fea5b2d24..346455067a7 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Mattermost::Session, type: :request do include ExclusiveLeaseHelpers + include StubRequests let(:user) { create(:user) } @@ -24,7 +25,7 @@ describe Mattermost::Session, type: :request do let(:location) { 'http://location.tld' } let(:cookie_header) {'MMOAUTH=taskik8az7rq8k6rkpuas7htia; Path=/;'} let!(:stub) do - WebMock.stub_request(:get, "#{mattermost_url}/oauth/gitlab/login") + stub_full_request("#{mattermost_url}/oauth/gitlab/login") .to_return(headers: { 'location' => location, 'Set-Cookie' => cookie_header }, status: 302) end @@ -63,7 +64,7 @@ describe Mattermost::Session, type: :request do end before do - WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete") + stub_full_request("#{mattermost_url}/signup/gitlab/complete") .with(query: hash_including({ 'state' => state })) .to_return do |request| post "/oauth/token", @@ -80,7 +81,7 @@ describe Mattermost::Session, type: :request do end end - WebMock.stub_request(:post, "#{mattermost_url}/api/v4/users/logout") + stub_full_request("#{mattermost_url}/api/v4/users/logout", method: :post) .to_return(headers: { Authorization: 'token thisworksnow' }, status: 200) end diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb index 1024e1a25ea..8ccbd90ddb8 100644 --- a/spec/lib/object_storage/direct_upload_spec.rb +++ b/spec/lib/object_storage/direct_upload_spec.rb @@ -121,7 +121,7 @@ describe ObjectStorage::DirectUpload do expect(subject[:MultipartUpload][:PartURLs].length).to eq(2) end - it 'part size is mimimum, 5MB' do + it 'part size is minimum, 5MB' do expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) end end diff --git a/spec/lib/quality/kubernetes_client_spec.rb b/spec/lib/quality/kubernetes_client_spec.rb index f35d9464d48..4e77dcc97e6 100644 --- a/spec/lib/quality/kubernetes_client_spec.rb +++ b/spec/lib/quality/kubernetes_client_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Quality::KubernetesClient do expect(Gitlab::Popen).to receive(:popen_with_detail) .with([%(kubectl --namespace "#{namespace}" delete ) \ 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \ - "--now -l release=\"#{release_name}\""]) + "--now --ignore-not-found --include-uninitialized -l release=\"#{release_name}\""]) .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false))) expect { subject.cleanup(release_name: release_name) }.to raise_error(described_class::CommandFailedError) @@ -23,7 +23,7 @@ RSpec.describe Quality::KubernetesClient do expect(Gitlab::Popen).to receive(:popen_with_detail) .with([%(kubectl --namespace "#{namespace}" delete ) \ 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \ - "--now -l release=\"#{release_name}\""]) + "--now --ignore-not-found --include-uninitialized -l release=\"#{release_name}\""]) .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true))) # We're not verifying the output here, just silencing it diff --git a/spec/lib/quality/seeders/issues_spec.rb b/spec/lib/quality/seeders/issues_spec.rb new file mode 100644 index 00000000000..e17414a541a --- /dev/null +++ b/spec/lib/quality/seeders/issues_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Quality::Seeders::Issues do + let(:project) { create(:project) } + + subject { described_class.new(project: project) } + + describe '#seed' do + it 'seeds issues' do + issues_created = subject.seed(backfill_weeks: 1, average_issues_per_week: 1) + + expect(issues_created).to be_between(0, 2) + expect(project.issues.count).to eq(issues_created) + end + end +end diff --git a/spec/lib/quality/test_level_spec.rb b/spec/lib/quality/test_level_spec.rb new file mode 100644 index 00000000000..3465c3a050b --- /dev/null +++ b/spec/lib/quality/test_level_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Quality::TestLevel do + describe '#pattern' do + context 'when level is unit' do + it 'returns a pattern' do + expect(subject.pattern(:unit)) + .to eq("spec/{bin,config,db,dependencies,factories,finders,frontend,graphql,helpers,initializers,javascripts,lib,migrations,models,policies,presenters,rack_servers,routing,rubocop,serializers,services,sidekiq,tasks,uploaders,validators,views,workers,elastic_integration}{,/**/}*_spec.rb") + end + end + + context 'when level is integration' do + it 'returns a pattern' do + expect(subject.pattern(:integration)) + .to eq("spec/{controllers,mailers,requests}{,/**/}*_spec.rb") + end + end + + context 'when level is system' do + it 'returns a pattern' do + expect(subject.pattern(:system)) + .to eq("spec/{features}{,/**/}*_spec.rb") + end + end + + context 'with a prefix' do + it 'returns a pattern' do + expect(described_class.new('ee/').pattern(:system)) + .to eq("ee/spec/{features}{,/**/}*_spec.rb") + end + end + + describe 'performance' do + it 'memoizes the pattern for a given level' do + expect(subject.pattern(:system).object_id).to eq(subject.pattern(:system).object_id) + end + + it 'freezes the pattern for a given level' do + expect(subject.pattern(:system)).to be_frozen + end + end + end + + describe '#regexp' do + context 'when level is unit' do + it 'returns a regexp' do + expect(subject.regexp(:unit)) + .to eq(%r{spec/(bin|config|db|dependencies|factories|finders|frontend|graphql|helpers|initializers|javascripts|lib|migrations|models|policies|presenters|rack_servers|routing|rubocop|serializers|services|sidekiq|tasks|uploaders|validators|views|workers|elastic_integration)}) + end + end + + context 'when level is integration' do + it 'returns a regexp' do + expect(subject.regexp(:integration)) + .to eq(%r{spec/(controllers|mailers|requests)}) + end + end + + context 'when level is system' do + it 'returns a regexp' do + expect(subject.regexp(:system)) + .to eq(%r{spec/(features)}) + end + end + + context 'with a prefix' do + it 'returns a regexp' do + expect(described_class.new('ee/').regexp(:system)) + .to eq(%r{ee/spec/(features)}) + end + end + + describe 'performance' do + it 'memoizes the regexp for a given level' do + expect(subject.regexp(:system).object_id).to eq(subject.regexp(:system).object_id) + end + + it 'freezes the regexp for a given level' do + expect(subject.regexp(:system)).to be_frozen + end + end + end + + describe '#level_for' do + it 'returns the correct level for a unit test' do + expect(subject.level_for('spec/models/abuse_report_spec.rb')).to eq(:unit) + end + + it 'returns the correct level for an integration test' do + expect(subject.level_for('spec/mailers/abuse_report_mailer_spec.rb')).to eq(:integration) + end + + it 'returns the correct level for a system test' do + expect(subject.level_for('spec/features/abuse_report_spec.rb')).to eq(:system) + end + + it 'raises an error for an unknown level' do + expect { subject.level_for('spec/unknown/foo_spec.rb') } + .to raise_error(described_class::UnknownTestLevelError, + %r{Test level for spec/unknown/foo_spec.rb couldn't be set. Please rename the file properly or change the test level detection regexes in .+/lib/quality/test_level.rb.}) + end + end +end diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb index 88e7e2e5ebb..cb14204b99a 100644 --- a/spec/lib/sentry/client_spec.rb +++ b/spec/lib/sentry/client_spec.rb @@ -61,11 +61,37 @@ describe Sentry::Client do end end + shared_examples 'maps exceptions' do + exceptions = { + HTTParty::Error => 'Error when connecting to Sentry', + Net::OpenTimeout => 'Connection to Sentry timed out', + SocketError => 'Received SocketError when trying to connect to Sentry', + OpenSSL::SSL::SSLError => 'Sentry returned invalid SSL data', + Errno::ECONNREFUSED => 'Connection refused', + StandardError => 'Sentry request failed due to StandardError' + } + + exceptions.each do |exception, message| + context "#{exception}" do + before do + stub_request(:get, sentry_request_url).to_raise(exception) + end + + it do + expect { subject } + .to raise_exception(Sentry::Client::Error, message) + end + end + end + end + describe '#list_issues' do let(:issue_status) { 'unresolved' } let(:limit) { 20 } + let(:sentry_api_response) { issues_sample_response } + let(:sentry_request_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' } - let!(:sentry_api_request) { stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: issues_sample_response) } + let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) } subject { client.list_issues(issue_status: issue_status, limit: limit) } @@ -74,6 +100,14 @@ describe Sentry::Client do it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error it_behaves_like 'has correct length', 1 + shared_examples 'has correct external_url' do + context 'external_url' do + it 'is constructed correctly' do + expect(subject[0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11') + end + end + end + context 'error object created from sentry response' do using RSpec::Parameterized::TableSyntax @@ -96,14 +130,10 @@ describe Sentry::Client do end with_them do - it { expect(subject[0].public_send(error_object)).to eq(issues_sample_response[0].dig(*sentry_response)) } + it { expect(subject[0].public_send(error_object)).to eq(sentry_api_response[0].dig(*sentry_response)) } end - context 'external_url' do - it 'is constructed correctly' do - expect(subject[0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11') - end - end + it_behaves_like 'has correct external_url' end context 'redirects' do @@ -115,16 +145,14 @@ describe Sentry::Client do # Sentry API returns 404 if there are extra slashes in the URL! context 'extra slashes in URL' do let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects//sentry-org/sentry-project/' } - let(:client) { described_class.new(sentry_url, token) } - let!(:valid_req_stub) do - stub_sentry_request( - 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' \ + let(:sentry_request_url) do + 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' \ 'issues/?limit=20&query=is:unresolved' - ) end it 'removes extra slashes in api url' do + expect(client.url).to eq(sentry_url) expect(Gitlab::HTTP).to receive(:get).with( URI('https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/issues/'), anything @@ -132,15 +160,47 @@ describe Sentry::Client do subject - expect(valid_req_stub).to have_been_requested + expect(sentry_api_request).to have_been_requested end end + + context 'Older sentry versions where keys are not present' do + let(:sentry_api_response) do + issues_sample_response[0...1].map do |issue| + issue[:project].delete(:id) + issue + end + end + + it_behaves_like 'calls sentry api' + + it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error + it_behaves_like 'has correct length', 1 + + it_behaves_like 'has correct external_url' + end + + context 'essential keys missing in API response' do + let(:sentry_api_response) do + issues_sample_response[0...1].map do |issue| + issue.except(:id) + end + end + + it 'raises exception' do + expect { subject }.to raise_error(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"') + end + end + + it_behaves_like 'maps exceptions' end describe '#list_projects' do let(:sentry_list_projects_url) { 'https://sentrytest.gitlab.com/api/0/projects/' } - let!(:sentry_api_request) { stub_sentry_request(sentry_list_projects_url, body: projects_sample_response) } + let(:sentry_api_response) { projects_sample_response } + + let!(:sentry_api_request) { stub_sentry_request(sentry_list_projects_url, body: sentry_api_response) } subject { client.list_projects } @@ -149,14 +209,31 @@ describe Sentry::Client do it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project it_behaves_like 'has correct length', 2 - context 'keys missing in API response' do - it 'raises exception' do - projects_sample_response[0].delete(:slug) + context 'essential keys missing in API response' do + let(:sentry_api_response) do + projects_sample_response[0...1].map do |project| + project.except(:slug) + end + end - stub_sentry_request(sentry_list_projects_url, body: projects_sample_response) + it 'raises exception' do + expect { subject }.to raise_error(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "slug"') + end + end - expect { subject }.to raise_error(Sentry::Client::SentryError, 'Sentry API response is missing keys. key not found: "slug"') + context 'optional keys missing in sentry response' do + let(:sentry_api_response) do + projects_sample_response[0...1].map do |project| + project[:organization].delete(:id) + project.delete(:id) + project.except(:status) + end end + + it_behaves_like 'calls sentry api' + + it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project + it_behaves_like 'has correct length', 1 end context 'error object created from sentry response' do @@ -173,7 +250,11 @@ describe Sentry::Client do end with_them do - it { expect(subject[0].public_send(sentry_project_object)).to eq(projects_sample_response[0].dig(*sentry_response)) } + it do + expect(subject[0].public_send(sentry_project_object)).to( + eq(sentry_api_response[0].dig(*sentry_response)) + ) + end end end @@ -203,12 +284,18 @@ describe Sentry::Client do expect(valid_req_stub).to have_been_requested end end + + context 'when exception is raised' do + let(:sentry_request_url) { sentry_list_projects_url } + + it_behaves_like 'maps exceptions' + end end private def stub_sentry_request(url, body: {}, status: 200, headers: {}) - WebMock.stub_request(:get, url) + stub_request(:get, url) .to_return( status: status, headers: { 'Content-Type' => 'application/json' }.merge(headers), |