diff options
Diffstat (limited to 'spec/lib')
154 files changed, 6125 insertions, 1064 deletions
diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb index 707212e07fd..086a006c45f 100644 --- a/spec/lib/banzai/filter/emoji_filter_spec.rb +++ b/spec/lib/banzai/filter/emoji_filter_spec.rb @@ -68,9 +68,9 @@ describe Banzai::Filter::EmojiFilter, lib: true do expect(doc.css('gl-emoji').size).to eq 1 end - it 'matches multiple emoji in a row' do + it 'does not match multiple emoji in a row' do doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:') - expect(doc.css('gl-emoji').size).to eq 3 + expect(doc.css('gl-emoji').size).to eq 0 end it 'unicode matches multiple emoji in a row' do @@ -83,6 +83,12 @@ describe Banzai::Filter::EmojiFilter, lib: true do expect(doc.css('gl-emoji').size).to eq 6 end + it 'does not match emoji in a string' do + doc = filter("'2a00:a4c0:100::1'") + + expect(doc.css('gl-emoji').size).to eq 0 + end + it 'has a data-name attribute' do doc = filter(':-1:') expect(doc.css('gl-emoji').first.attr('data-name')).to eq 'thumbsdown' diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb index d9e4525cb28..0f8ec8de7a0 100644 --- a/spec/lib/banzai/filter/external_link_filter_spec.rb +++ b/spec/lib/banzai/filter/external_link_filter_spec.rb @@ -1,5 +1,22 @@ require 'spec_helper' +shared_examples 'an external link with rel attribute' do + it 'adds rel="nofollow" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'nofollow' + end + + it 'adds rel="noreferrer" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'noreferrer' + end + + it 'adds rel="noopener" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'noopener' + end +end + describe Banzai::Filter::ExternalLinkFilter, lib: true do include FilterSpecHelper @@ -22,49 +39,58 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do context 'for root links on document' do let(:doc) { filter %q(<a href="https://google.com/">Google</a>) } - it 'adds rel="nofollow" to external links' do - expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to include 'nofollow' + it_behaves_like 'an external link with rel attribute' + end + + context 'for nested links on document' do + let(:doc) { filter %q(<p><a href="https://google.com/">Google</a></p>) } + + it_behaves_like 'an external link with rel attribute' + end + + context 'for invalid urls' do + it 'skips broken hrefs' do + doc = filter %q(<p><a href="don't crash on broken urls">Google</a></p>) + expected = %q(<p><a href="don't%20crash%20on%20broken%20urls">Google</a></p>) + + expect(doc.to_html).to eq(expected) end - it 'adds rel="noreferrer" to external links' do - expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to include 'noreferrer' + it 'skips improperly formatted mailtos' do + doc = filter %q(<p><a href="mailto://jblogs@example.com">Email</a></p>) + expected = %q(<p><a href="mailto://jblogs@example.com">Email</a></p>) + + expect(doc.to_html).to eq(expected) end end - context 'for nested links on document' do - let(:doc) { filter %q(<p><a href="https://google.com/">Google</a></p>) } + context 'for links with a username' do + context 'with a valid username' do + let(:doc) { filter %q(<a href="https://user@google.com/">Google</a>) } - it 'adds rel="nofollow" to external links' do - expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to include 'nofollow' + it_behaves_like 'an external link with rel attribute' end - it 'adds rel="noreferrer" to external links' do - expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to include 'noreferrer' + context 'with an impersonated username' do + let(:internal) { Gitlab.config.gitlab.url } + + let(:doc) { filter %Q(<a href="https://#{internal}@example.com" target="_blank">Reverse Tabnabbing</a>) } + + it_behaves_like 'an external link with rel attribute' end end context 'for non-lowercase scheme links' do - let(:doc_with_http) { filter %q(<p><a href="httP://google.com/">Google</a></p>) } - let(:doc_with_https) { filter %q(<p><a href="hTTpS://google.com/">Google</a></p>) } - - it 'adds rel="nofollow" to external links' do - expect(doc_with_http.at_css('a')).to have_attribute('rel') - expect(doc_with_https.at_css('a')).to have_attribute('rel') + context 'with http' do + let(:doc) { filter %q(<p><a href="httP://google.com/">Google</a></p>) } - expect(doc_with_http.at_css('a')['rel']).to include 'nofollow' - expect(doc_with_https.at_css('a')['rel']).to include 'nofollow' + it_behaves_like 'an external link with rel attribute' end - it 'adds rel="noreferrer" to external links' do - expect(doc_with_http.at_css('a')).to have_attribute('rel') - expect(doc_with_https.at_css('a')).to have_attribute('rel') + context 'with https' do + let(:doc) { filter %q(<p><a href="hTTpS://google.com/">Google</a></p>) } - expect(doc_with_http.at_css('a')['rel']).to include 'noreferrer' - expect(doc_with_https.at_css('a')['rel']).to include 'noreferrer' + it_behaves_like 'an external link with rel attribute' end it 'skips internal links' do @@ -84,14 +110,6 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do context 'for protocol-relative links' do let(:doc) { filter %q(<p><a href="//google.com/">Google</a></p>) } - it 'adds rel="nofollow" to external links' do - expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to include 'nofollow' - end - - it 'adds rel="noreferrer" to external links' do - expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to include 'noreferrer' - end + it_behaves_like 'an external link with rel attribute' end end diff --git a/spec/lib/banzai/filter/issuable_state_filter_spec.rb b/spec/lib/banzai/filter/issuable_state_filter_spec.rb new file mode 100644 index 00000000000..9c2399815b9 --- /dev/null +++ b/spec/lib/banzai/filter/issuable_state_filter_spec.rb @@ -0,0 +1,197 @@ +require 'spec_helper' + +describe Banzai::Filter::IssuableStateFilter, lib: true do + include ActionView::Helpers::UrlHelper + include FilterSpecHelper + + let(:user) { create(:user) } + let(:context) { { current_user: user, issuable_state_filter_enabled: true } } + let(:closed_issue) { create_issue(:closed) } + let(:project) { create(:empty_project, :public) } + let(:other_project) { create(:empty_project, :public) } + + def create_link(text, data) + link_to(text, '', class: 'gfm has-tooltip', data: data) + end + + def create_issue(state) + create(:issue, state, project: project) + end + + def create_merge_request(state) + create(:merge_request, state, + source_project: project, target_project: project) + end + + it 'ignores non-GFM links' do + html = %(See <a href="https://google.com/">Google</a>) + doc = filter(html, current_user: user) + + expect(doc.css('a').last.text).to eq('Google') + end + + it 'ignores non-issuable links' do + link = create_link('text', project: project, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq('text') + end + + it 'ignores issuable links with empty content' do + link = create_link('', issue: closed_issue.id, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq('') + end + + it 'ignores issuable links with custom anchor' do + link = create_link('something', issue: closed_issue.id, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq('something') + end + + it 'ignores issuable links to specific comments' do + link = create_link("#{closed_issue.to_reference} (comment 1)", issue: closed_issue.id, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference} (comment 1)") + end + + it 'ignores merge request links to diffs tab' do + merge_request = create(:merge_request, :closed) + link = create_link( + "#{merge_request.to_reference} (diffs)", + merge_request: merge_request.id, + reference_type: 'merge_request' + ) + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (diffs)") + end + + it 'handles cross project references' do + link = create_link(closed_issue.to_reference(other_project), issue: closed_issue.id, reference_type: 'issue') + doc = filter(link, context.merge(project: other_project)) + + expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference(other_project)} (closed)") + end + + it 'does not append state when filter is not enabled' do + link = create_link('text', issue: closed_issue.id, reference_type: 'issue') + context = { current_user: user } + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq('text') + end + + context 'when project is in pending delete' do + before do + project.update!(pending_delete: true) + end + + it 'does not append issue state' do + link = create_link('text', issue: closed_issue.id, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq('text') + end + end + + context 'for issue references' do + it 'ignores open issue references' do + issue = create_issue(:opened) + link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq(issue.to_reference) + end + + it 'ignores reopened issue references' do + issue = create_issue(:reopened) + link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq(issue.to_reference) + end + + it 'appends state to closed issue references' do + link = create_link(closed_issue.to_reference, issue: closed_issue.id, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference} (closed)") + end + end + + context 'for merge request references' do + it 'ignores open merge request references' do + merge_request = create_merge_request(:opened) + + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) + + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq(merge_request.to_reference) + end + + it 'ignores reopened merge request references' do + merge_request = create_merge_request(:reopened) + + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) + + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq(merge_request.to_reference) + end + + it 'ignores locked merge request references' do + merge_request = create_merge_request(:locked) + + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) + + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq(merge_request.to_reference) + end + + it 'appends state to closed merge request references' do + merge_request = create_merge_request(:closed) + + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) + + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (closed)") + end + + it 'appends state to merged merge request references' do + merge_request = create_merge_request(:merged) + + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) + + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (merged)") + end + end +end diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb new file mode 100644 index 00000000000..897288b8ad5 --- /dev/null +++ b/spec/lib/banzai/filter/markdown_filter_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Banzai::Filter::MarkdownFilter, lib: true do + include FilterSpecHelper + + context 'code block' do + it 'adds language to lang attribute when specified' do + result = filter("```html\nsome code\n```") + + expect(result).to start_with("\n<pre><code lang=\"html\">") + end + + it 'does not add language to lang attribute when not specified' do + result = filter("```\nsome code\n```") + + expect(result).to start_with("\n<pre><code>") + end + end +end diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb index f85a5dcbd8b..9b8ecb201f3 100644 --- a/spec/lib/banzai/filter/plantuml_filter_spec.rb +++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb @@ -5,7 +5,7 @@ describe Banzai::Filter::PlantumlFilter, lib: true do it 'should replace plantuml pre tag with img tag' do stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080") - input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + 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>' doc = filter(input) @@ -14,8 +14,8 @@ describe Banzai::Filter::PlantumlFilter, lib: true do it 'should not replace plantuml pre tag with img tag if disabled' do stub_application_setting(plantuml_enabled: false) - input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' - output = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre></pre></pre>' + input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' + output = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' doc = filter(input) expect(doc.to_s).to eq output @@ -23,7 +23,7 @@ describe Banzai::Filter::PlantumlFilter, lib: true do it 'should not replace plantuml pre tag with img tag if url is invalid' do stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid") - input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + 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>' doc = filter(input) diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb index 0140a91c7ba..7c4a0f32c7b 100644 --- a/spec/lib/banzai/filter/redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/redactor_filter_spec.rb @@ -15,6 +15,16 @@ describe Banzai::Filter::RedactorFilter, lib: true do link_to('text', '', class: 'gfm', data: data) end + it 'skips when the skip_redaction flag is set' do + user = create(:user) + project = create(:empty_project) + + link = reference_link(project: project.id, reference_type: 'test') + doc = filter(link, current_user: user, skip_redaction: true) + + expect(doc.css('a').length).to eq 1 + end + context 'with data-project' do let(:parser_class) do Class.new(Banzai::ReferenceParser::BaseParser) do @@ -103,7 +113,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do it 'allows references for assignee' do assignee = create(:user) project = create(:empty_project, :public) - issue = create(:issue, :confidential, project: project, assignee: assignee) + issue = create(:issue, :confidential, project: project, assignees: [assignee]) link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue') doc = filter(link, current_user: assignee) diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index b4cd5f63a15..fdbc65b5e00 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -49,11 +49,12 @@ describe Banzai::Filter::SanitizationFilter, lib: true do instance = described_class.new('Foo') 3.times { instance.whitelist } - expect(instance.whitelist[:transformers].size).to eq 5 + expect(instance.whitelist[:transformers].size).to eq 4 end - it 'allows syntax highlighting' do - exp = act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>} + it 'sanitizes `class` attribute from all elements' do + act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>} + exp = %q{<pre><code><span class="k">def</span></code></pre>} expect(filter(act).to_html).to eq exp end diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb index 63fb1bb25c4..f61fc8ceb9e 100644 --- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb +++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb @@ -12,14 +12,14 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do context "when a valid language is specified" do it "highlights as that language" do - result = filter('<pre><code class="ruby">def fun end</code></pre>') + result = filter('<pre><code lang="ruby">def fun end</code></pre>') expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>') end end context "when an invalid language is specified" do it "highlights as plaintext" do - result = filter('<pre><code class="gnuplot">This is a test</code></pre>') + result = filter('<pre><code lang="gnuplot">This is a test</code></pre>') expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>') end end @@ -30,7 +30,7 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do end it "highlights as plaintext" do - result = filter('<pre><code class="ruby">This is a test</code></pre>') + result = filter('<pre><code lang="ruby">This is a test</code></pre>') expect(result.to_html).to eq('<pre class="code highlight" lang="" v-pre="true"><code>This is a test</code></pre>') end end diff --git a/spec/lib/banzai/issuable_extractor_spec.rb b/spec/lib/banzai/issuable_extractor_spec.rb new file mode 100644 index 00000000000..e5d332efb08 --- /dev/null +++ b/spec/lib/banzai/issuable_extractor_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Banzai::IssuableExtractor, lib: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:extractor) { described_class.new(project, user) } + let(:issue) { create(:issue, project: project) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:issue_link) do + html_to_node( + "<a href='' data-issue='#{issue.id}' data-reference-type='issue' class='gfm'>text</a>" + ) + end + let(:merge_request_link) do + html_to_node( + "<a href='' data-merge-request='#{merge_request.id}' data-reference-type='merge_request' class='gfm'>text</a>" + ) + end + + def html_to_node(html) + Nokogiri::HTML.fragment( + html + ).children[0] + end + + it 'returns instances of issuables for nodes with references' do + result = extractor.extract([issue_link, merge_request_link]) + + expect(result).to eq(issue_link => issue, merge_request_link => merge_request) + end + + describe 'caching' do + before do + RequestStore.begin! + end + + after do + RequestStore.end! + RequestStore.clear! + end + + it 'saves records to cache' do + extractor.extract([issue_link, merge_request_link]) + + second_call_queries = ActiveRecord::QueryRecorder.new do + extractor.extract([issue_link, merge_request_link]) + end.count + + expect(second_call_queries).to eq 0 + end + end +end diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb index 6bcda87c999..dd2674f9f20 100644 --- a/spec/lib/banzai/object_renderer_spec.rb +++ b/spec/lib/banzai/object_renderer_spec.rb @@ -3,128 +3,51 @@ require 'spec_helper' describe Banzai::ObjectRenderer do let(:project) { create(:empty_project) } let(:user) { project.owner } - - def fake_object(attrs = {}) - object = double(attrs.merge("new_record?" => true, "destroyed?" => true)) - allow(object).to receive(:markdown_cache_field_for).with(:note).and_return(:note_html) - allow(object).to receive(:banzai_render_context).with(:note).and_return(project: nil, author: nil) - allow(object).to receive(:update_column).with(:note_html, anything).and_return(true) - object - end + let(:renderer) { described_class.new(project, user, custom_value: 'value') } + let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_VERSION) } describe '#render' do it 'renders and redacts an Array of objects' do - renderer = described_class.new(project, user) - object = fake_object(note: 'hello', note_html: nil) - - expect(renderer).to receive(:render_objects).with([object], :note). - and_call_original - - expect(renderer).to receive(:redact_documents). - with(an_instance_of(Array)). - and_call_original - - expect(object).to receive(:redacted_note_html=).with('<p dir="auto">hello</p>') - expect(object).to receive(:user_visible_reference_count=).with(0) - renderer.render([object], :note) - end - end - - describe '#render_objects' do - it 'renders an Array of objects' do - object = fake_object(note: 'hello', note_html: nil) - - renderer = described_class.new(project, user) - expect(renderer).to receive(:render_attributes).with([object], :note). - and_call_original - - rendered = renderer.render_objects([object], :note) - - expect(rendered).to be_an_instance_of(Array) - expect(rendered[0]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment) - end - end - - describe '#redact_documents' do - it 'redacts a set of documents and returns them as an Array of Hashes' do - doc = Nokogiri::HTML.fragment('<p>hello</p>') - renderer = described_class.new(project, user) - - expect_any_instance_of(Banzai::Redactor).to receive(:redact). - with([doc]). - and_call_original - - redacted = renderer.redact_documents([doc]) - - expect(redacted.count).to eq(1) - expect(redacted.first[:visible_reference_count]).to eq(0) - expect(redacted.first[:document].to_html).to eq('<p>hello</p>') + expect(object.redacted_note_html).to eq '<p dir="auto">hello</p>' + expect(object.user_visible_reference_count).to eq 0 end - end - describe '#context_for' do - let(:object) { fake_object(note: 'hello') } - let(:renderer) { described_class.new(project, user) } + it 'calls Banzai::Redactor to perform redaction' do + expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original - it 'returns a Hash' do - expect(renderer.context_for(object, :note)).to be_an_instance_of(Hash) - end - - it 'includes the banzai render context for the object' do - expect(object).to receive(:banzai_render_context).with(:note).and_return(foo: :bar) - context = renderer.context_for(object, :note) - expect(context).to have_key(:foo) - expect(context[:foo]).to eq(:bar) - end - end - - describe '#render_attributes' do - it 'renders the attribute of a list of objects' do - objects = [fake_object(note: 'hello', note_html: nil), fake_object(note: 'bye', note_html: nil)] - renderer = described_class.new(project, user) - - objects.each do |object| - expect(Banzai).to receive(:render_field).with(object, :note).and_call_original - end - - docs = renderer.render_attributes(objects, :note) - - expect(docs[0]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment) - expect(docs[0].to_html).to eq('<p dir="auto">hello</p>') - - expect(docs[1]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment) - expect(docs[1].to_html).to eq('<p dir="auto">bye</p>') - end - - it 'returns when no objects to render' do - objects = [] - renderer = described_class.new(project, user, pipeline: :note) - - expect(renderer.render_attributes(objects, :note)).to eq([]) + renderer.render([object], :note) end - end - describe '#base_context' do - let(:context) do - described_class.new(project, user, foo: :bar).base_context - end + it 'retrieves field content using Banzai.render_field' do + expect(Banzai).to receive(:render_field).with(object, :note).and_call_original - it 'returns a Hash' do - expect(context).to be_an_instance_of(Hash) - end - - it 'includes the custom attributes' do - expect(context[:foo]).to eq(:bar) + renderer.render([object], :note) end - it 'includes the current user' do - expect(context[:current_user]).to eq(user) - end + it 'passes context to PostProcessPipeline' do + another_user = create(:user) + another_project = create(:empty_project) + object = Note.new( + note: 'hello', + note_html: 'hello', + author: another_user, + project: another_project + ) + + expect(Banzai::Pipeline::PostProcessPipeline).to receive(:to_document).with( + anything, + hash_including( + skip_redaction: true, + current_user: user, + project: another_project, + author: another_user, + custom_value: 'value' + ) + ).and_call_original - it 'includes the current project' do - expect(context[:project]).to eq(project) + renderer.render([object], :note) end end end diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb index 6d2c141e18b..e6f2963193c 100644 --- a/spec/lib/banzai/redactor_spec.rb +++ b/spec/lib/banzai/redactor_spec.rb @@ -42,6 +42,31 @@ describe Banzai::Redactor do end end + context 'when project is in pending delete' do + let!(:issue) { create(:issue, project: project) } + let(:redactor) { described_class.new(project, user) } + + before do + project.update(pending_delete: true) + end + + it 'redacts an issue attached' do + doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-issue='#{issue.id}'>foo</a>") + + redactor.redact([doc]) + + expect(doc.to_html).to eq('foo') + 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>") + + redactor.redact([doc]) + + expect(doc.to_html).to eq('foo') + end + end + context 'when reference visible to user' do it 'does not redact an array of documents' do doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>' diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb index aa127f0179d..d5746107ee1 100644 --- a/spec/lib/banzai/reference_parser/base_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -92,20 +92,49 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do end describe '#grouped_objects_for_nodes' do - it 'returns a Hash grouping objects per ID' do - nodes = [double(:node)] + it 'returns a Hash grouping objects per node' do + link = double(:link) + + expect(link).to receive(:has_attribute?). + with('data-user'). + and_return(true) + + expect(link).to receive(:attr). + with('data-user'). + and_return(user.id.to_s) + + nodes = [link] expect(subject).to receive(:unique_attribute_values). with(nodes, 'data-user'). - and_return([user.id]) + and_return([user.id.to_s]) hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user') - expect(hash).to eq({ user.id => user }) + expect(hash).to eq({ link => user }) end - it 'returns an empty Hash when the list of nodes is empty' do - expect(subject.grouped_objects_for_nodes([], User, 'data-user')).to eq({}) + it 'returns an empty Hash when entry does not exist in the database' do + link = double(:link) + + expect(link).to receive(:has_attribute?). + with('data-user'). + and_return(true) + + expect(link).to receive(:attr). + with('data-user'). + and_return('1') + + nodes = [link] + bad_id = user.id + 100 + + expect(subject).to receive(:unique_attribute_values). + with(nodes, 'data-user'). + and_return([bad_id.to_s]) + + hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user') + + expect(hash).to eq({}) end end diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 6873b7b85f9..7031c47231c 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -67,6 +67,16 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do expect(subject.referenced_by([])).to eq([]) end end + + context 'when issue with given ID does not exist' do + before do + link['data-issue'] = '-1' + end + + it 'returns an empty Array' do + expect(subject.referenced_by([link])).to eq([]) + end + end end end @@ -75,7 +85,7 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do link['data-issue'] = issue.id.to_s nodes = [link] - expect(subject.issues_for_nodes(nodes)).to eq({ issue.id => issue }) + expect(subject.issues_for_nodes(nodes)).to eq({ link => issue }) end end end diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb index 31ca9d27b0b..4ec998efe53 100644 --- a/spec/lib/banzai/reference_parser/user_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb @@ -180,6 +180,15 @@ describe Banzai::ReferenceParser::UserParser, lib: true do expect(subject.nodes_user_can_reference(user, [link])).to eq([]) end + + it 'returns the nodes if the project attribute value equals the current project ID' do + other_user = create(:user) + + link['data-project'] = project.id.to_s + link['data-author'] = other_user.id.to_s + + expect(subject.nodes_user_can_reference(user, [link])).to eq([link]) + end end context 'when the link does not have a data-author attribute' do diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb index aaa6b12e67e..0e094405e33 100644 --- a/spec/lib/banzai/renderer_spec.rb +++ b/spec/lib/banzai/renderer_spec.rb @@ -1,73 +1,36 @@ require 'spec_helper' describe Banzai::Renderer do - def expect_render(project = :project) - expected_context = { project: project } - expect(renderer).to receive(:cacheless_render) { :html }.with(:markdown, expected_context) - end - - def expect_cache_update - expect(object).to receive(:update_column).with("field_html", :html) - end - - def fake_object(*features) - markdown = :markdown if features.include?(:markdown) - html = :html if features.include?(:html) - - object = double( - "object", - banzai_render_context: { project: :project }, - field: markdown, - field_html: html - ) + def fake_object(fresh:) + object = double('object') - allow(object).to receive(:markdown_cache_field_for).with(:field).and_return("field_html") - allow(object).to receive(:new_record?).and_return(features.include?(:new)) - allow(object).to receive(:destroyed?).and_return(features.include?(:destroyed)) + allow(object).to receive(:cached_html_up_to_date?).with(:field).and_return(fresh) + allow(object).to receive(:cached_html_for).with(:field).and_return('field_html') object end - describe "#render_field" do - let(:renderer) { Banzai::Renderer } - let(:subject) { renderer.render_field(object, :field) } + describe '#render_field' do + let(:renderer) { described_class } + subject { renderer.render_field(object, :field) } - context "with an empty cache" do - let(:object) { fake_object(:markdown) } - it "caches and returns the result" do - expect_render - expect_cache_update - expect(subject).to eq(:html) - end - end + context 'with a stale cache' do + let(:object) { fake_object(fresh: false) } - context "with a filled cache" do - let(:object) { fake_object(:markdown, :html) } + it 'caches and returns the result' do + expect(object).to receive(:refresh_markdown_cache!).with(do_update: true) - it "uses the cache" do - expect_render.never - expect_cache_update.never - should eq(:html) + is_expected.to eq('field_html') end end - context "new object" do - let(:object) { fake_object(:new, :markdown) } - - it "doesn't cache the result" do - expect_render - expect_cache_update.never - expect(subject).to eq(:html) - end - end + context 'with an up-to-date cache' do + let(:object) { fake_object(fresh: true) } - context "destroyed object" do - let(:object) { fake_object(:destroyed, :markdown) } + it 'uses the cache' do + expect(object).to receive(:refresh_markdown_cache!).never - it "doesn't cache the result" do - expect_render - expect_cache_update.never - expect(subject).to eq(:html) + is_expected.to eq('field_html') end end end diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb index 0762fd7e56a..a5dfb49478a 100644 --- a/spec/lib/ci/ansi2html_spec.rb +++ b/spec/lib/ci/ansi2html_spec.rb @@ -1,159 +1,160 @@ require 'spec_helper' describe Ci::Ansi2html, lib: true do - subject { Ci::Ansi2html } + subject { described_class } it "prints non-ansi as-is" do - expect(subject.convert("Hello")[:html]).to eq('Hello') + expect(convert_html("Hello")).to eq('Hello') end it "strips non-color-changing controll sequences" do - expect(subject.convert("Hello \e[2Kworld")[:html]).to eq('Hello world') + expect(convert_html("Hello \e[2Kworld")).to eq('Hello world') end it "prints simply red" do - expect(subject.convert("\e[31mHello\e[0m")[:html]).to eq('<span class="term-fg-red">Hello</span>') + expect(convert_html("\e[31mHello\e[0m")).to eq('<span class="term-fg-red">Hello</span>') end it "prints simply red without trailing reset" do - expect(subject.convert("\e[31mHello")[:html]).to eq('<span class="term-fg-red">Hello</span>') + expect(convert_html("\e[31mHello")).to eq('<span class="term-fg-red">Hello</span>') end it "prints simply yellow" do - expect(subject.convert("\e[33mHello\e[0m")[:html]).to eq('<span class="term-fg-yellow">Hello</span>') + expect(convert_html("\e[33mHello\e[0m")).to eq('<span class="term-fg-yellow">Hello</span>') end it "prints default on blue" do - expect(subject.convert("\e[39;44mHello")[:html]).to eq('<span class="term-bg-blue">Hello</span>') + expect(convert_html("\e[39;44mHello")).to eq('<span class="term-bg-blue">Hello</span>') end it "prints red on blue" do - expect(subject.convert("\e[31;44mHello")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello</span>') + expect(convert_html("\e[31;44mHello")).to eq('<span class="term-fg-red term-bg-blue">Hello</span>') end it "resets colors after red on blue" do - expect(subject.convert("\e[31;44mHello\e[0m world")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world') + expect(convert_html("\e[31;44mHello\e[0m world")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world') end it "performs color change from red/blue to yellow/blue" do - expect(subject.convert("\e[31;44mHello \e[33mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-blue">world</span>') + expect(convert_html("\e[31;44mHello \e[33mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-blue">world</span>') end it "performs color change from red/blue to yellow/green" do - expect(subject.convert("\e[31;44mHello \e[33;42mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-green">world</span>') + expect(convert_html("\e[31;44mHello \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-green">world</span>') end it "performs color change from red/blue to reset to yellow/green" do - expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>') + expect(convert_html("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>') end it "ignores unsupported codes" do - expect(subject.convert("\e[51mHello\e[0m")[:html]).to eq('Hello') + expect(convert_html("\e[51mHello\e[0m")).to eq('Hello') end it "prints light red" do - expect(subject.convert("\e[91mHello\e[0m")[:html]).to eq('<span class="term-fg-l-red">Hello</span>') + expect(convert_html("\e[91mHello\e[0m")).to eq('<span class="term-fg-l-red">Hello</span>') end it "prints default on light red" do - expect(subject.convert("\e[101mHello\e[0m")[:html]).to eq('<span class="term-bg-l-red">Hello</span>') + expect(convert_html("\e[101mHello\e[0m")).to eq('<span class="term-bg-l-red">Hello</span>') end it "performs color change from red/blue to default/blue" do - expect(subject.convert("\e[31;44mHello \e[39mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>') + expect(convert_html("\e[31;44mHello \e[39mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>') end it "performs color change from light red/blue to default/blue" do - expect(subject.convert("\e[91;44mHello \e[39mworld")[:html]).to eq('<span class="term-fg-l-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>') + expect(convert_html("\e[91;44mHello \e[39mworld")).to eq('<span class="term-fg-l-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>') end it "prints bold text" do - expect(subject.convert("\e[1mHello")[:html]).to eq('<span class="term-bold">Hello</span>') + expect(convert_html("\e[1mHello")).to eq('<span class="term-bold">Hello</span>') end it "resets bold text" do - expect(subject.convert("\e[1mHello\e[21m world")[:html]).to eq('<span class="term-bold">Hello</span> world') - expect(subject.convert("\e[1mHello\e[22m world")[:html]).to eq('<span class="term-bold">Hello</span> world') + expect(convert_html("\e[1mHello\e[21m world")).to eq('<span class="term-bold">Hello</span> world') + expect(convert_html("\e[1mHello\e[22m world")).to eq('<span class="term-bold">Hello</span> world') end it "prints italic text" do - expect(subject.convert("\e[3mHello")[:html]).to eq('<span class="term-italic">Hello</span>') + expect(convert_html("\e[3mHello")).to eq('<span class="term-italic">Hello</span>') end it "resets italic text" do - expect(subject.convert("\e[3mHello\e[23m world")[:html]).to eq('<span class="term-italic">Hello</span> world') + expect(convert_html("\e[3mHello\e[23m world")).to eq('<span class="term-italic">Hello</span> world') end it "prints underlined text" do - expect(subject.convert("\e[4mHello")[:html]).to eq('<span class="term-underline">Hello</span>') + expect(convert_html("\e[4mHello")).to eq('<span class="term-underline">Hello</span>') end it "resets underlined text" do - expect(subject.convert("\e[4mHello\e[24m world")[:html]).to eq('<span class="term-underline">Hello</span> world') + expect(convert_html("\e[4mHello\e[24m world")).to eq('<span class="term-underline">Hello</span> world') end it "prints concealed text" do - expect(subject.convert("\e[8mHello")[:html]).to eq('<span class="term-conceal">Hello</span>') + expect(convert_html("\e[8mHello")).to eq('<span class="term-conceal">Hello</span>') end it "resets concealed text" do - expect(subject.convert("\e[8mHello\e[28m world")[:html]).to eq('<span class="term-conceal">Hello</span> world') + expect(convert_html("\e[8mHello\e[28m world")).to eq('<span class="term-conceal">Hello</span> world') end it "prints crossed-out text" do - expect(subject.convert("\e[9mHello")[:html]).to eq('<span class="term-cross">Hello</span>') + expect(convert_html("\e[9mHello")).to eq('<span class="term-cross">Hello</span>') end it "resets crossed-out text" do - expect(subject.convert("\e[9mHello\e[29m world")[:html]).to eq('<span class="term-cross">Hello</span> world') + expect(convert_html("\e[9mHello\e[29m world")).to eq('<span class="term-cross">Hello</span> world') end it "can print 256 xterm fg colors" do - expect(subject.convert("\e[38;5;16mHello")[:html]).to eq('<span class="xterm-fg-16">Hello</span>') + expect(convert_html("\e[38;5;16mHello")).to eq('<span class="xterm-fg-16">Hello</span>') end it "can print 256 xterm fg colors on normal magenta background" do - expect(subject.convert("\e[38;5;16;45mHello")[:html]).to eq('<span class="xterm-fg-16 term-bg-magenta">Hello</span>') + expect(convert_html("\e[38;5;16;45mHello")).to eq('<span class="xterm-fg-16 term-bg-magenta">Hello</span>') end it "can print 256 xterm bg colors" do - expect(subject.convert("\e[48;5;240mHello")[:html]).to eq('<span class="xterm-bg-240">Hello</span>') + expect(convert_html("\e[48;5;240mHello")).to eq('<span class="xterm-bg-240">Hello</span>') end it "can print 256 xterm bg colors on normal magenta foreground" do - expect(subject.convert("\e[48;5;16;35mHello")[:html]).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>') + expect(convert_html("\e[48;5;16;35mHello")).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>') end it "prints bold colored text vividly" do - expect(subject.convert("\e[1;31mHello\e[0m")[:html]).to eq('<span class="term-fg-l-red term-bold">Hello</span>') + expect(convert_html("\e[1;31mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>') end it "prints bold light colored text correctly" do - expect(subject.convert("\e[1;91mHello\e[0m")[:html]).to eq('<span class="term-fg-l-red term-bold">Hello</span>') + expect(convert_html("\e[1;91mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>') end it "prints <" do - expect(subject.convert("<")[:html]).to eq('<') + expect(convert_html("<")).to eq('<') end it "replaces newlines with line break tags" do - expect(subject.convert("\n")[:html]).to eq('<br>') + expect(convert_html("\n")).to eq('<br>') end it "groups carriage returns with newlines" do - expect(subject.convert("\r\n")[:html]).to eq('<br>') + expect(convert_html("\r\n")).to eq('<br>') end describe "incremental update" do shared_examples 'stateable converter' do - let(:pass1) { subject.convert(pre_text) } - let(:pass2) { subject.convert(pre_text + text, pass1[:state]) } + let(:pass1_stream) { StringIO.new(pre_text) } + let(:pass2_stream) { StringIO.new(pre_text + text) } + let(:pass1) { subject.convert(pass1_stream) } + let(:pass2) { subject.convert(pass2_stream, pass1.state) } it "to returns html to append" do - expect(pass2[:append]).to be_truthy - expect(pass2[:html]).to eq(html) - expect(pass1[:text] + pass2[:text]).to eq(pre_text + text) - expect(pass1[:html] + pass2[:html]).to eq(pre_html + html) + expect(pass2.append).to be_truthy + expect(pass2.html).to eq(html) + expect(pass1.html + pass2.html).to eq(pre_html + html) end end @@ -193,4 +194,27 @@ describe Ci::Ansi2html, lib: true do it_behaves_like 'stateable converter' end end + + describe "truncates" do + let(:text) { "Hello World" } + let(:stream) { StringIO.new(text) } + let(:subject) { described_class.convert(stream) } + + before do + stream.seek(3, IO::SEEK_SET) + end + + it "returns truncated output" do + expect(subject.truncated).to be_truthy + end + + it "does not append output" do + expect(subject.append).to be_falsey + end + end + + def convert_html(data) + stream = StringIO.new(data) + subject.convert(stream).html + end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 53abc056602..fe2c00bb2ca 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -225,7 +225,7 @@ module Ci before_script: ["pwd"], rspec: { script: "rspec", type: "test", only: %w(master deploy) }, staging: { script: "deploy", type: "deploy", only: %w(master deploy) }, - production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }, + production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] } }) config_processor = GitlabCiYamlProcessor.new(config, 'fork') @@ -381,7 +381,7 @@ module Ci before_script: ["pwd"], rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] }, staging: { script: "deploy", type: "deploy", except: ["master"] }, - production: { script: "deploy", type: "deploy", except: ["master@fork"] }, + production: { script: "deploy", type: "deploy", except: ["master@fork"] } }) config_processor = GitlabCiYamlProcessor.new(config, 'fork') @@ -716,7 +716,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( paths: ["logs/", "binaries/"], untracked: true, - key: 'key', + key: 'key' ) end @@ -734,7 +734,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( paths: ["logs/", "binaries/"], untracked: true, - key: 'key', + key: 'key' ) end @@ -743,7 +743,7 @@ module Ci cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, rspec: { script: "rspec", - cache: { paths: ["test/"], untracked: false, key: 'local' }, + cache: { paths: ["test/"], untracked: false, key: 'local' } } }) @@ -753,7 +753,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( paths: ["test/"], untracked: false, - key: 'local', + key: 'local' ) end end diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb index 96dacdc5cd2..db680489a8d 100644 --- a/spec/lib/constraints/group_url_constrainer_spec.rb +++ b/spec/lib/constraints/group_url_constrainer_spec.rb @@ -17,14 +17,49 @@ describe GroupUrlConstrainer, lib: true do it { expect(subject.matches?(request)).to be_truthy } end + context 'valid request for nested group with reserved top level name' do + let!(:nested_group) { create(:group, path: 'api', parent: group) } + let!(:request) { build_request('gitlab/api') } + + it { expect(subject.matches?(request)).to be_truthy } + end + context 'invalid request' do let(:request) { build_request('foo') } it { expect(subject.matches?(request)).to be_falsey } end + + context 'when the request matches a redirect route' do + context 'for a root group' do + let!(:redirect_route) { group.redirect_routes.create!(path: 'gitlabb') } + + context 'and is a GET request' do + let(:request) { build_request(redirect_route.path) } + + it { expect(subject.matches?(request)).to be_truthy } + end + + context 'and is NOT a GET request' do + let(:request) { build_request(redirect_route.path, 'POST') } + + it { expect(subject.matches?(request)).to be_falsey } + end + end + + context 'for a nested group' do + let!(:nested_group) { create(:group, path: 'nested', parent: group) } + let!(:redirect_route) { nested_group.redirect_routes.create!(path: 'gitlabb/nested') } + let(:request) { build_request(redirect_route.path) } + + it { expect(subject.matches?(request)).to be_truthy } + end + end end - def build_request(path) - double(:request, params: { id: path }) + def build_request(path, method = 'GET') + double(:request, + 'get?': (method == 'GET'), + params: { id: path }) end end diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb index 4f25ad88960..b6884e37aa3 100644 --- a/spec/lib/constraints/project_url_constrainer_spec.rb +++ b/spec/lib/constraints/project_url_constrainer_spec.rb @@ -24,9 +24,26 @@ describe ProjectUrlConstrainer, lib: true do it { expect(subject.matches?(request)).to be_falsey } end end + + context 'when the request matches a redirect route' do + let(:old_project_path) { 'old_project_path' } + let!(:redirect_route) { project.redirect_routes.create!(path: "#{namespace.full_path}/#{old_project_path}") } + + context 'and is a GET request' do + let(:request) { build_request(namespace.full_path, old_project_path) } + it { expect(subject.matches?(request)).to be_truthy } + end + + context 'and is NOT a GET request' do + let(:request) { build_request(namespace.full_path, old_project_path, 'POST') } + it { expect(subject.matches?(request)).to be_falsey } + end + end end - def build_request(namespace, project) - double(:request, params: { namespace_id: namespace, id: project }) + def build_request(namespace, project, method = 'GET') + double(:request, + 'get?': (method == 'GET'), + params: { namespace_id: namespace, id: project }) end end diff --git a/spec/lib/constraints/user_url_constrainer_spec.rb b/spec/lib/constraints/user_url_constrainer_spec.rb index 207b6fe6c9e..ed69b830979 100644 --- a/spec/lib/constraints/user_url_constrainer_spec.rb +++ b/spec/lib/constraints/user_url_constrainer_spec.rb @@ -15,9 +15,26 @@ describe UserUrlConstrainer, lib: true do it { expect(subject.matches?(request)).to be_falsey } end + + context 'when the request matches a redirect route' do + let(:old_project_path) { 'old_project_path' } + let!(:redirect_route) { user.namespace.redirect_routes.create!(path: 'foo') } + + context 'and is a GET request' do + let(:request) { build_request(redirect_route.path) } + it { expect(subject.matches?(request)).to be_truthy } + end + + context 'and is NOT a GET request' do + let(:request) { build_request(redirect_route.path, 'POST') } + it { expect(subject.matches?(request)).to be_falsey } + end + end end - def build_request(username) - double(:request, params: { username: username }) + def build_request(username, method = 'GET') + double(:request, + 'get?': (method == 'GET'), + params: { username: username }) end end diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb index bbacdc67ebd..ab010c6dfeb 100644 --- a/spec/lib/container_registry/blob_spec.rb +++ b/spec/lib/container_registry/blob_spec.rb @@ -1,110 +1,121 @@ require 'spec_helper' describe ContainerRegistry::Blob do - let(:digest) { 'sha256:0123456789012345' } + let(:group) { create(:group, name: 'group') } + let(:project) { create(:empty_project, path: 'test', group: group) } + + let(:repository) do + create(:container_repository, name: 'image', + tags: %w[latest rc1], + project: project) + end + let(:config) do - { - 'digest' => digest, + { 'digest' => 'sha256:0123456789012345', 'mediaType' => 'binary', - 'size' => 1000 - } + 'size' => 1000 } + end + + let(:blob) { described_class.new(repository, config) } + + before do + stub_container_registry_config(enabled: true, + api_url: 'http://registry.gitlab', + host_port: 'registry.gitlab') end - let(:token) { 'authorization-token' } - - let(:registry) { ContainerRegistry::Registry.new('http://example.com', token: token) } - let(:repository) { registry.repository('group/test') } - let(:blob) { repository.blob(config) } it { expect(blob).to respond_to(:repository) } it { expect(blob).to delegate_method(:registry).to(:repository) } it { expect(blob).to delegate_method(:client).to(:repository) } - context '#path' do - subject { blob.path } - - it { is_expected.to eq('example.com/group/test@sha256:0123456789012345') } + describe '#path' do + it 'returns a valid path to the blob' do + expect(blob.path).to eq('group/test/image@sha256:0123456789012345') + end end - context '#digest' do - subject { blob.digest } - - it { is_expected.to eq(digest) } + describe '#digest' do + it 'return correct digest value' do + expect(blob.digest).to eq 'sha256:0123456789012345' + end end - context '#type' do - subject { blob.type } - - it { is_expected.to eq('binary') } + describe '#type' do + it 'returns a correct type' do + expect(blob.type).to eq 'binary' + end end - context '#revision' do - subject { blob.revision } - - it { is_expected.to eq('0123456789012345') } + describe '#revision' do + it 'returns a correct blob SHA' do + expect(blob.revision).to eq '0123456789012345' + end end - context '#short_revision' do - subject { blob.short_revision } - - it { is_expected.to eq('012345678') } + describe '#short_revision' do + it 'return a short SHA' do + expect(blob.short_revision).to eq '012345678' + end end - context '#delete' do + describe '#delete' do before do - stub_request(:delete, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345'). - to_return(status: 200) + stub_request(:delete, 'http://registry.gitlab/v2/group/test/image/blobs/sha256:0123456789012345') + .to_return(status: 200) end - subject { blob.delete } - - it { is_expected.to be_truthy } + it 'returns true when blob has been successfuly deleted' do + expect(blob.delete).to be_truthy + end end - context '#data' do - let(:data) { '{"key":"value"}' } - - subject { blob.data } - + describe '#data' do context 'when locally stored' do before do - stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345'). + stub_request(:get, 'http://registry.gitlab/v2/group/test/image/blobs/sha256:0123456789012345'). to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, - body: data) + body: '{"key":"value"}') end - it { is_expected.to eq(data) } + it 'returns a correct blob data' do + expect(blob.data).to eq '{"key":"value"}' + end end context 'when externally stored' do + let(:location) { 'http://external.com/blob/file' } + before do - stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345'). - with(headers: { 'Authorization' => "bearer #{token}" }). - to_return( + stub_request(:get, 'http://registry.gitlab/v2/group/test/image/blobs/sha256:0123456789012345') + .with(headers: { 'Authorization' => 'bearer token' }) + .to_return( status: 307, headers: { 'Location' => location }) end context 'for a valid address' do - let(:location) { 'http://external.com/blob/file' } - before do stub_request(:get, location). - with(headers: { 'Authorization' => nil }). + with { |request| !request.headers.include?('Authorization') }. to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, - body: data) + body: '{"key":"value"}') end - it { is_expected.to eq(data) } + it 'returns correct data' do + expect(blob.data).to eq '{"key":"value"}' + end end context 'for invalid file' do let(:location) { 'file:///etc/passwd' } - it { expect{ subject }.to raise_error(ArgumentError, 'invalid address') } + it 'raises an error' do + expect { blob.data }.to raise_error(ArgumentError, 'invalid address') + end end end end diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb new file mode 100644 index 00000000000..ec03b533383 --- /dev/null +++ b/spec/lib/container_registry/client_spec.rb @@ -0,0 +1,39 @@ +# coding: utf-8 +require 'spec_helper' + +describe ContainerRegistry::Client do + let(:token) { '12345' } + let(:options) { { token: token } } + let(:client) { described_class.new("http://container-registry", options) } + + describe '#blob' do + it 'GET /v2/:name/blobs/:digest' do + stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345"). + with(headers: { + 'Accept' => 'application/octet-stream', + 'Authorization' => "bearer #{token}" + }). + to_return(status: 200, body: "Blob") + + expect(client.blob('group/test', 'sha256:0123456789012345')).to eq('Blob') + end + + it 'follows 307 redirect for GET /v2/:name/blobs/:digest' do + stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345"). + with(headers: { + 'Accept' => 'application/octet-stream', + 'Authorization' => "bearer #{token}" + }). + to_return(status: 307, body: "", headers: { Location: 'http://redirected' }) + # We should probably use hash_excluding here, but that requires an update to WebMock: + # https://github.com/bblimke/webmock/blob/master/lib/webmock/matchers/hash_excluding_matcher.rb + stub_request(:get, "http://redirected/"). + with { |request| !request.headers.include?('Authorization') }. + to_return(status: 200, body: "Successfully redirected") + + response = client.blob('group/test', 'sha256:0123456789012345') + + expect(response).to eq('Successfully redirected') + end + end +end diff --git a/spec/lib/container_registry/path_spec.rb b/spec/lib/container_registry/path_spec.rb new file mode 100644 index 00000000000..c2bcb54210b --- /dev/null +++ b/spec/lib/container_registry/path_spec.rb @@ -0,0 +1,246 @@ +require 'spec_helper' + +describe ContainerRegistry::Path do + subject { described_class.new(path) } + + describe '#components' do + let(:path) { 'path/to/some/project' } + + it 'splits components by a forward slash' do + expect(subject.components).to eq %w[path to some project] + end + end + + describe '#nodes' do + context 'when repository path is valid' do + let(:path) { 'path/to/some/project' } + + it 'return all project path like node in reverse order' do + expect(subject.nodes).to eq %w[path/to/some/project + path/to/some + path/to] + end + end + + context 'when repository path is invalid' do + let(:path) { '' } + + it 'rasises en error' do + expect { subject.nodes } + .to raise_error described_class::InvalidRegistryPathError + end + end + end + + describe '#to_s' do + context 'when path does not have uppercase characters' do + let(:path) { 'some/image' } + + it 'return a string with a repository path' do + expect(subject.to_s).to eq 'some/image' + end + end + + context 'when path has uppercase characters' do + let(:path) { 'SoMe/ImAgE' } + + it 'return a string with a repository path' do + expect(subject.to_s).to eq 'some/image' + end + end + end + + describe '#valid?' do + context 'when path has less than two components' do + let(:path) { 'something/' } + + it { is_expected.not_to be_valid } + end + + context 'when path has more than allowed number of components' do + let(:path) { 'a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/r/s/t/u/w/y/z' } + + it { is_expected.not_to be_valid } + end + + context 'when path has invalid characters' do + let(:path) { 'some\path' } + + it { is_expected.not_to be_valid } + end + + context 'when path has two or more components' do + let(:path) { 'some/path' } + + it { is_expected.to be_valid } + end + + context 'when path is related to multi-level image' do + let(:path) { 'some/path/my/image' } + + it { is_expected.to be_valid } + end + + context 'when path contains uppercase letters' do + let(:path) { 'Some/Registry' } + + it { is_expected.to be_valid } + end + end + + describe '#has_repository?' do + context 'when project exists' do + let(:project) { create(:empty_project) } + let(:path) { "#{project.full_path}/my/image" } + + context 'when path already has matching repository' do + before do + create(:container_repository, project: project, name: 'my/image') + end + + it { is_expected.to have_repository } + it { is_expected.to have_project } + end + + context 'when path does not have matching repository' do + it { is_expected.not_to have_repository } + it { is_expected.to have_project } + end + end + + context 'when project does not exist' do + let(:path) { 'some/project/my/image' } + + it { is_expected.not_to have_repository } + it { is_expected.not_to have_project } + end + end + + describe '#repository_project' do + let(:group) { create(:group, path: 'some_group') } + + context 'when project for given path exists' do + let(:path) { 'some_group/some_project' } + + before do + create(:empty_project, group: group, name: 'some_project') + create(:empty_project, name: 'some_project') + end + + it 'returns a correct project' do + expect(subject.repository_project.group).to eq group + end + end + + context 'when project for given path does not exist' do + let(:path) { 'not/matching' } + + it 'returns nil' do + expect(subject.repository_project).to be_nil + end + end + + context 'when matching multi-level path' do + let(:project) do + create(:empty_project, group: group, name: 'some_project') + end + + context 'when using the zero-level path' do + let(:path) { project.full_path } + + it 'supports zero-level path' do + expect(subject.repository_project).to eq project + end + end + + context 'when using first-level path' do + let(:path) { "#{project.full_path}/repository" } + + it 'supports first-level path' do + expect(subject.repository_project).to eq project + end + end + + context 'when using second-level path' do + let(:path) { "#{project.full_path}/repository/name" } + + it 'supports second-level path' do + expect(subject.repository_project).to eq project + end + end + + context 'when using too deep nesting in the path' do + let(:path) { "#{project.full_path}/repository/name/invalid" } + + it 'does not support three-levels of nesting' do + expect(subject.repository_project).to be_nil + end + end + end + end + + describe '#repository_name' do + context 'when project does not exist' do + let(:path) { 'some/name' } + + it 'returns nil' do + expect(subject.repository_name).to be_nil + end + end + + context 'when project exists' do + let(:group) { create(:group, path: 'Some_Group') } + + before do + create(:empty_project, group: group, name: 'some_project') + end + + context 'when project path equal repository path' do + let(:path) { 'some_group/some_project' } + + it 'returns an empty string' do + expect(subject.repository_name).to eq '' + end + end + + context 'when repository path has one additional level' do + let(:path) { 'some_group/some_project/repository' } + + it 'returns a correct repository name' do + expect(subject.repository_name).to eq 'repository' + end + end + + context 'when repository path has two additional levels' do + let(:path) { 'some_group/some_project/repository/image' } + + it 'returns a correct repository name' do + expect(subject.repository_name).to eq 'repository/image' + end + end + end + end + + describe '#project_path' do + context 'when project does not exist' do + let(:path) { 'some/name' } + + it 'returns nil' do + expect(subject.project_path).to be_nil + end + end + + context 'when project with uppercase characters in path exists' do + let(:path) { 'somegroup/myproject/my/image' } + let(:group) { create(:group, path: 'SomeGroup') } + + before do + create(:empty_project, group: group, name: 'MyProject') + end + + it 'returns downcased project path' do + expect(subject.project_path).to eq 'somegroup/myproject' + end + end + end +end diff --git a/spec/lib/container_registry/registry_spec.rb b/spec/lib/container_registry/registry_spec.rb index 4f3f8b24fc4..4d6eea94bf0 100644 --- a/spec/lib/container_registry/registry_spec.rb +++ b/spec/lib/container_registry/registry_spec.rb @@ -10,7 +10,7 @@ describe ContainerRegistry::Registry do it { is_expected.to respond_to(:uri) } it { is_expected.to respond_to(:path) } - it { expect(subject.repository('test')).not_to be_nil } + it { expect(subject).not_to be_nil } context '#path' do subject { registry.path } diff --git a/spec/lib/container_registry/repository_spec.rb b/spec/lib/container_registry/repository_spec.rb deleted file mode 100644 index c364e759108..00000000000 --- a/spec/lib/container_registry/repository_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'spec_helper' - -describe ContainerRegistry::Repository do - let(:registry) { ContainerRegistry::Registry.new('http://example.com') } - let(:repository) { registry.repository('group/test') } - - it { expect(repository).to respond_to(:registry) } - it { expect(repository).to delegate_method(:client).to(:registry) } - it { expect(repository.tag('test')).not_to be_nil } - - context '#path' do - subject { repository.path } - - it { is_expected.to eq('example.com/group/test') } - end - - context 'manifest processing' do - before do - stub_request(:get, 'http://example.com/v2/group/test/tags/list'). - with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' }). - to_return( - status: 200, - body: JSON.dump(tags: ['test']), - headers: { 'Content-Type' => 'application/json' }) - end - - context '#manifest' do - subject { repository.manifest } - - it { is_expected.not_to be_nil } - end - - context '#valid?' do - subject { repository.valid? } - - it { is_expected.to be_truthy } - end - - context '#tags' do - subject { repository.tags } - - it { is_expected.not_to be_empty } - end - end - - context '#delete_tags' do - let(:tag) { ContainerRegistry::Tag.new(repository, 'tag') } - - before { expect(repository).to receive(:tags).twice.and_return([tag]) } - - subject { repository.delete_tags } - - context 'succeeds' do - before { expect(tag).to receive(:delete).and_return(true) } - - it { is_expected.to be_truthy } - end - - context 'any fails' do - before { expect(tag).to receive(:delete).and_return(false) } - - it { is_expected.to be_falsey } - end - end -end diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb index c5e31ae82b6..f8fffbdca41 100644 --- a/spec/lib/container_registry/tag_spec.rb +++ b/spec/lib/container_registry/tag_spec.rb @@ -1,25 +1,66 @@ require 'spec_helper' describe ContainerRegistry::Tag do - let(:registry) { ContainerRegistry::Registry.new('http://example.com') } - let(:repository) { registry.repository('group/test') } - let(:tag) { repository.tag('tag') } - let(:headers) { { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' } } + let(:group) { create(:group, name: 'group') } + let(:project) { create(:project, path: 'test', group: group) } + + let(:repository) do + create(:container_repository, name: '', project: project) + end + + let(:headers) do + { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' } + end + + let(:tag) { described_class.new(repository, 'tag') } + + before do + stub_container_registry_config(enabled: true, + api_url: 'http://registry.gitlab', + host_port: 'registry.gitlab') + end it { expect(tag).to respond_to(:repository) } it { expect(tag).to delegate_method(:registry).to(:repository) } it { expect(tag).to delegate_method(:client).to(:repository) } - context '#path' do - subject { tag.path } + describe '#path' do + context 'when tag belongs to zero-level repository' do + let(:repository) do + create(:container_repository, name: '', + tags: %w[rc1], + project: project) + end + + it 'returns path to the image' do + expect(tag.path).to eq('group/test:tag') + end + end - it { is_expected.to eq('example.com/group/test:tag') } + context 'when tag belongs to first-level repository' do + let(:repository) do + create(:container_repository, name: 'my_image', + tags: %w[tag], + project: project) + end + + it 'returns path to the image' do + expect(tag.path).to eq('group/test/my_image:tag') + end + end + end + + describe '#location' do + it 'returns a full location of the tag' do + expect(tag.location) + .to eq 'registry.gitlab/group/test:tag' + end end context 'manifest processing' do context 'schema v1' do before do - stub_request(:get, 'http://example.com/v2/group/test/manifests/tag'). + stub_request(:get, 'http://registry.gitlab/v2/group/test/manifests/tag'). with(headers: headers). to_return( status: 200, @@ -56,7 +97,7 @@ describe ContainerRegistry::Tag do context 'schema v2' do before do - stub_request(:get, 'http://example.com/v2/group/test/manifests/tag'). + stub_request(:get, 'http://registry.gitlab/v2/group/test/manifests/tag'). with(headers: headers). to_return( status: 200, @@ -93,7 +134,7 @@ describe ContainerRegistry::Tag do context 'when locally stored' do before do - stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac'). + stub_request(:get, 'http://registry.gitlab/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac'). with(headers: { 'Accept' => 'application/octet-stream' }). to_return( status: 200, @@ -105,7 +146,7 @@ describe ContainerRegistry::Tag do context 'when externally stored' do before do - stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac'). + stub_request(:get, 'http://registry.gitlab/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac'). with(headers: { 'Accept' => 'application/octet-stream' }). to_return( status: 307, @@ -123,29 +164,29 @@ describe ContainerRegistry::Tag do end end - context 'manifest digest' do + context 'with stubbed digest' do before do - stub_request(:head, 'http://example.com/v2/group/test/manifests/tag'). - with(headers: headers). - to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' }) + stub_request(:head, 'http://registry.gitlab/v2/group/test/manifests/tag') + .with(headers: headers) + .to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' }) end - context '#digest' do - subject { tag.digest } - - it { is_expected.to eq('sha256:digest') } + describe '#digest' do + it 'returns a correct tag digest' do + expect(tag.digest).to eq 'sha256:digest' + end end - context '#delete' do + describe '#delete' do before do - stub_request(:delete, 'http://example.com/v2/group/test/manifests/sha256:digest'). - with(headers: headers). - to_return(status: 200) + stub_request(:delete, 'http://registry.gitlab/v2/group/test/manifests/sha256:digest') + .with(headers: headers) + .to_return(status: 200) end - subject { tag.delete } - - it { is_expected.to be_truthy } + it 'correctly deletes the tag' do + expect(tag.delete).to be_truthy + end end end end diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb index 90628917943..7faa0f31b68 100644 --- a/spec/lib/expand_variables_spec.rb +++ b/spec/lib/expand_variables_spec.rb @@ -25,7 +25,7 @@ describe ExpandVariables do result: 'keyvalueresult', variables: [ { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' }, + { key: 'variable2', value: 'result' } ] }, { value: 'key${variable}${variable2}', result: 'keyvalueresult', @@ -37,7 +37,7 @@ describe ExpandVariables do result: 'keyresultvalue', variables: [ { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' }, + { key: 'variable2', value: 'result' } ] }, { value: 'key${variable2}${variable}', result: 'keyresultvalue', @@ -49,7 +49,7 @@ describe ExpandVariables do result: 'review/feature/add-review-apps', variables: [ { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' } - ] }, + ] } ] tests.each do |test| diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index bca57105d1d..2c7ebb15fd7 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -22,26 +22,24 @@ module Gitlab expect(Asciidoctor).to receive(:convert) .with(input, expected_asciidoc_opts).and_return(html) - expect( render(input, context) ).to eql html + expect(render(input, context)).to eq(html) end context "with asciidoc_opts" do - let(:asciidoc_opts) { { safe: :safe, attributes: ['foo'] } } - it "merges the options with default ones" do expected_asciidoc_opts = { - safe: :safe, + safe: :secure, backend: :gitlab_html5, - attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo'] + attributes: described_class::DEFAULT_ADOC_ATTRS } expect(Asciidoctor).to receive(:convert) .with(input, expected_asciidoc_opts).and_return(html) - render(input, context, asciidoc_opts) + render(input, context) end end - + context "XSS" do links = { 'links' => { @@ -50,7 +48,7 @@ module Gitlab }, 'images' => { input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]', - output: "<div>\n<p><span><img src=\"https://localhost.com/image.png\" alt=\"Alt text\"></span></p>\n</div>" + output: "<img src=\"https://localhost.com/image.png\" alt=\"Alt text\">" }, 'pre' => { input: '```mypre"><script>alert(3)</script>', @@ -60,10 +58,18 @@ module Gitlab links.each do |name, data| it "does not convert dangerous #{name} into HTML" do - expect(render(data[:input], context)).to eql data[:output] + expect(render(data[:input], context)).to include(data[:output]) end end end + + context 'external links' do + it 'adds the `rel` attribute to the link' do + output = render('link:https://google.com[Google]', context) + + expect(output).to include('rel="nofollow noreferrer noopener"') + end + end end def render(*args) diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 03c4879ed6f..50bc3ef1b7c 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -118,7 +118,7 @@ describe Gitlab::Auth, lib: true do it 'succeeds for OAuth tokens with the `api` scope' do expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2') - expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) + expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities)) end it 'fails for OAuth tokens with other scopes' do @@ -175,7 +175,7 @@ describe Gitlab::Auth, lib: true do user = create( :user, username: 'normal_user', - password: 'my-secret', + password: 'my-secret' ) expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')) @@ -186,7 +186,7 @@ describe Gitlab::Auth, lib: true do user = create( :user, username: 'oauth2', - password: 'my-secret', + password: 'my-secret' ) expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')) diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb index f84782ab440..c59ff7fb290 100644 --- a/spec/lib/gitlab/backup/manager_spec.rb +++ b/spec/lib/gitlab/backup/manager_spec.rb @@ -151,7 +151,7 @@ describe Backup::Manager, lib: true do allow(Dir).to receive(:glob).and_return( [ '1451606400_2016_01_01_gitlab_backup.tar', - '1451520000_2015_12_31_gitlab_backup.tar', + '1451520000_2015_12_31_gitlab_backup.tar' ] ) end diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb new file mode 100644 index 00000000000..b386852b196 --- /dev/null +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -0,0 +1,304 @@ +require 'spec_helper' + +describe Gitlab::Cache::Ci::ProjectPipelineStatus, :redis do + let(:project) { create(:project) } + let(:pipeline_status) { described_class.new(project) } + let(:cache_key) { "projects/#{project.id}/pipeline_status" } + + describe '.load_for_project' do + it "loads the status" do + expect_any_instance_of(described_class).to receive(:load_status) + + described_class.load_for_project(project) + end + end + + describe 'loading in batches' do + let(:status) { 'success' } + let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' } + let(:ref) { 'master' } + let(:pipeline_info) { { sha: sha, status: status, ref: ref } } + let(:project_without_status) { create(:project) } + + describe '.load_in_batch_for_projects' do + it 'preloads pipeline_status on projects' do + described_class.load_in_batch_for_projects([project]) + + # Don't call the accessor that would lazy load the variable + expect(project.instance_variable_get('@pipeline_status')).to be_a(described_class) + end + + describe 'without a status in redis' do + it 'loads the status from a commit when it was not in redis' do + empty_status = { sha: nil, status: nil, ref: nil } + fake_pipeline = described_class.new( + project_without_status, + pipeline_info: empty_status, + loaded_from_cache: false + ) + + expect(described_class).to receive(:new). + with(project_without_status, + pipeline_info: empty_status, + loaded_from_cache: false). + and_return(fake_pipeline) + expect(fake_pipeline).to receive(:load_from_project) + expect(fake_pipeline).to receive(:store_in_cache) + + described_class.load_in_batch_for_projects([project_without_status]) + end + + it 'only connects to redis twice' do + # Once to load, once to store in the cache + expect(Gitlab::Redis).to receive(:with).exactly(2).and_call_original + + described_class.load_in_batch_for_projects([project_without_status]) + + expect(project_without_status.pipeline_status).not_to be_nil + end + end + + describe 'when a status was cached in redis' do + before do + Gitlab::Redis.with do |redis| + redis.mapped_hmset(cache_key, + { sha: sha, status: status, ref: ref }) + end + end + + it 'loads the correct status' do + described_class.load_in_batch_for_projects([project]) + + pipeline_status = project.instance_variable_get('@pipeline_status') + + expect(pipeline_status.sha).to eq(sha) + expect(pipeline_status.status).to eq(status) + expect(pipeline_status.ref).to eq(ref) + end + + it 'only connects to redis once' do + expect(Gitlab::Redis).to receive(:with).exactly(1).and_call_original + + described_class.load_in_batch_for_projects([project]) + + expect(project.pipeline_status).not_to be_nil + end + + it "doesn't load the status separatly" do + expect_any_instance_of(described_class).not_to receive(:load_from_project) + expect_any_instance_of(described_class).not_to receive(:load_from_cache) + + described_class.load_in_batch_for_projects([project]) + end + end + end + + describe '.cached_results_for_projects' do + it 'loads a status from redis for all projects' do + Gitlab::Redis.with do |redis| + redis.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref }) + end + + result = [{ loaded_from_cache: false, pipeline_info: { sha: nil, status: nil, ref: nil } }, + { loaded_from_cache: true, pipeline_info: pipeline_info }] + + expect(described_class.cached_results_for_projects([project_without_status, project])).to eq(result) + end + end + end + + describe '.update_for_pipeline' do + it 'refreshes the cache if nescessary' do + pipeline = build_stubbed(:ci_pipeline, + sha: '123456', status: 'success', ref: 'master') + fake_status = double + expect(described_class).to receive(:new). + with(pipeline.project, + pipeline_info: { + sha: '123456', status: 'success', ref: 'master' + }). + and_return(fake_status) + + expect(fake_status).to receive(:store_in_cache_if_needed) + + described_class.update_for_pipeline(pipeline) + end + end + + describe '#has_status?' do + it "is false when the status wasn't loaded yet" do + expect(pipeline_status.has_status?).to be_falsy + end + + it 'is true when all status information was loaded' do + fake_commit = double + allow(fake_commit).to receive(:status).and_return('failed') + allow(fake_commit).to receive(:sha).and_return('failed424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6') + allow(pipeline_status).to receive(:commit).and_return(fake_commit) + allow(pipeline_status).to receive(:has_cache?).and_return(false) + + pipeline_status.load_status + + expect(pipeline_status.has_status?).to be_truthy + end + end + + describe '#load_status' do + it 'loads the status from the cache when there is one' do + expect(pipeline_status).to receive(:has_cache?).and_return(true) + expect(pipeline_status).to receive(:load_from_cache) + + pipeline_status.load_status + end + + it 'loads the status from the project commit when there is no cache' do + allow(pipeline_status).to receive(:has_cache?).and_return(false) + + expect(pipeline_status).to receive(:load_from_project) + + pipeline_status.load_status + end + + it 'stores the status in the cache when it loading it from the project' do + allow(pipeline_status).to receive(:has_cache?).and_return(false) + allow(pipeline_status).to receive(:load_from_project) + + expect(pipeline_status).to receive(:store_in_cache) + + pipeline_status.load_status + end + + it 'sets the state to loaded' do + pipeline_status.load_status + + expect(pipeline_status).to be_loaded + end + + it 'only loads the status once' do + expect(pipeline_status).to receive(:has_cache?).and_return(true).exactly(1) + expect(pipeline_status).to receive(:load_from_cache).exactly(1) + + pipeline_status.load_status + pipeline_status.load_status + end + end + + describe "#load_from_project" do + let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) } + + it 'reads the status from the pipeline for the commit' do + pipeline_status.load_from_project + + expect(pipeline_status.status).to eq('success') + expect(pipeline_status.sha).to eq(project.commit.sha) + expect(pipeline_status.ref).to eq(project.default_branch) + end + + it "doesn't fail for an empty project" do + status_for_empty_commit = described_class.new(create(:empty_project)) + + status_for_empty_commit.load_status + + expect(status_for_empty_commit).to be_loaded + end + end + + describe "#store_in_cache", :redis do + it "sets the object in redis" do + pipeline_status.sha = '123456' + pipeline_status.status = 'failed' + + pipeline_status.store_in_cache + read_sha, read_status = Gitlab::Redis.with { |redis| redis.hmget(cache_key, :sha, :status) } + + expect(read_sha).to eq('123456') + expect(read_status).to eq('failed') + end + end + + describe '#store_in_cache_if_needed', :redis do + it 'stores the state in the cache when the sha is the HEAD of the project' do + create(:ci_pipeline, :success, project: project, sha: project.commit.sha) + pipeline_status = described_class.load_for_project(project) + + pipeline_status.store_in_cache_if_needed + sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget(cache_key, :sha, :status, :ref) } + + expect(sha).not_to be_nil + expect(status).not_to be_nil + expect(ref).not_to be_nil + end + + it "doesn't store the status in redis when the sha is not the head of the project" do + other_status = described_class.new( + project, + pipeline_info: { sha: "123456", status: "failed" } + ) + + other_status.store_in_cache_if_needed + sha, status = Gitlab::Redis.with { |redis| redis.hmget(cache_key, :sha, :status) } + + expect(sha).to be_nil + expect(status).to be_nil + end + + it "deletes the cache if the repository doesn't have a head commit" do + empty_project = create(:empty_project) + Gitlab::Redis.with do |redis| + redis.mapped_hmset(cache_key, + { sha: 'sha', status: 'pending', ref: 'master' }) + end + + other_status = described_class.new(empty_project, + pipeline_info: { + sha: "123456", status: "failed" + }) + + other_status.store_in_cache_if_needed + sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/pipeline_status", :sha, :status, :ref) } + + expect(sha).to be_nil + expect(status).to be_nil + expect(ref).to be_nil + end + end + + describe "with a status in redis", :redis do + let(:status) { 'success' } + let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' } + let(:ref) { 'master' } + + before do + Gitlab::Redis.with do |redis| + redis.mapped_hmset(cache_key, + { sha: sha, status: status, ref: ref }) + end + end + + describe '#load_from_cache' do + it 'reads the status from redis' do + pipeline_status.load_from_cache + + expect(pipeline_status.sha).to eq(sha) + expect(pipeline_status.status).to eq(status) + expect(pipeline_status.ref).to eq(ref) + end + end + + describe '#has_cache?' do + it 'knows the status is cached' do + expect(pipeline_status.has_cache?).to be_truthy + end + end + + describe '#delete_from_cache' do + it 'deletes values from redis' do + pipeline_status.delete_from_cache + + key_exists = Gitlab::Redis.with { |redis| redis.exists(cache_key) } + + expect(key_exists).to be_falsy + end + end + end +end diff --git a/spec/lib/gitlab/changes_list_spec.rb b/spec/lib/gitlab/changes_list_spec.rb index 69d86144e32..464508fcd73 100644 --- a/spec/lib/gitlab/changes_list_spec.rb +++ b/spec/lib/gitlab/changes_list_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::ChangesList do let(:invalid_changes) { 1 } context 'when changes is a valid string' do - let(:changes_list) { Gitlab::ChangesList.new(valid_changes_string) } + let(:changes_list) { described_class.new(valid_changes_string) } it 'splits elements by newline character' do expect(changes_list).to contain_exactly({ diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index b6e924d67be..eb4f06b371c 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -40,11 +40,15 @@ describe Gitlab::ChatCommands::Command, service: true do context 'when trying to do deployment' do let(:params) { { text: 'deploy staging to production' } } - let!(:build) { create(:ci_build, project: project) } + let!(:build) { create(:ci_build, pipeline: pipeline) } + let!(:pipeline) { create(:ci_pipeline, project: project) } let!(:staging) { create(:environment, name: 'staging', project: project) } let!(:deployment) { create(:deployment, environment: staging, deployable: build) } + let!(:manual) do - create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production') + create(:ci_build, :manual, pipeline: pipeline, + name: 'first', + environment: 'production') end context 'and user can not create deployment' do @@ -56,7 +60,7 @@ describe Gitlab::ChatCommands::Command, service: true do context 'and user does have deployment permission' do before do - project.team << [user, :developer] + build.project.add_master(user) end it 'returns action' do @@ -66,7 +70,9 @@ describe Gitlab::ChatCommands::Command, service: true do context 'when duplicate action exists' do let!(:manual2) do - create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production') + create(:ci_build, :manual, pipeline: pipeline, + name: 'second', + environment: 'production') end it 'returns error' do diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb index b3358a32161..b33389d959e 100644 --- a/spec/lib/gitlab/chat_commands/deploy_spec.rb +++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::ChatCommands::Deploy, service: true do let(:regex_match) { described_class.match('deploy staging to production') } before do - project.team << [user, :master] + project.add_master(user) end subject do @@ -23,7 +23,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do context 'with environment' do let!(:staging) { create(:environment, name: 'staging', project: project) } - let!(:build) { create(:ci_build, project: project) } + let!(:pipeline) { create(:ci_pipeline, project: project) } + let!(:build) { create(:ci_build, pipeline: pipeline) } let!(:deployment) { create(:deployment, environment: staging, deployable: build) } context 'without actions' do @@ -35,7 +36,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do context 'with action' do let!(:manual1) do - create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production') + create(:ci_build, :manual, pipeline: pipeline, + name: 'first', + environment: 'production') end it 'returns success result' do @@ -45,7 +48,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do context 'when duplicate action exists' do let!(:manual2) do - create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production') + create(:ci_build, :manual, pipeline: pipeline, + name: 'second', + environment: 'production') end it 'returns error' do @@ -57,8 +62,7 @@ describe Gitlab::ChatCommands::Deploy, service: true do context 'when teardown action exists' do let!(:teardown) do create(:ci_build, :manual, :teardown_environment, - project: project, pipeline: build.pipeline, - name: 'teardown', environment: 'production') + pipeline: pipeline, name: 'teardown', environment: 'production') end it 'returns the success message' do diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index e22f88b7a32..8d81ed5856e 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -5,13 +5,10 @@ describe Gitlab::Checks::ChangeAccess, lib: true do let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:user_access) { Gitlab::UserAccess.new(user, project: project) } - let(:changes) do - { - oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c', - newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51', - ref: 'refs/heads/master' - } - end + let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } + let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + let(:ref) { 'refs/heads/master' } + let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } } let(:protocol) { 'ssh' } subject do @@ -23,7 +20,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do ).exec end - before { allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) } + before { project.add_developer(user) } context 'without failed checks' do it "doesn't return any error" do @@ -41,62 +38,135 @@ describe Gitlab::Checks::ChangeAccess, lib: true do end context 'tags check' do - let(:changes) do - { - oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c', - newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51', - ref: 'refs/tags/v1.0.0' - } - end + let(:ref) { 'refs/tags/v1.0.0' } it 'returns an error if the user is not allowed to update tags' do + allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false) expect(subject.status).to be(false) expect(subject.message).to eq('You are not allowed to change existing tags on this project.') end - end - context 'protected branches check' do - before do - allow(project).to receive(:protected_branch?).with('master').and_return(true) - end + context 'with protected tag' do + let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') } - it 'returns an error if the user is not allowed to do forced pushes to protected branches' do - expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) + context 'as master' do + before { project.add_master(user) } - expect(subject.status).to be(false) - expect(subject.message).to eq('You are not allowed to force push code to a protected branch on this project.') - end + context 'deletion' do + let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } + let(:newrev) { '0000000000000000000000000000000000000000' } - it 'returns an error if the user is not allowed to merge to protected branches' do - expect_any_instance_of(Gitlab::Checks::MatchingMergeRequest).to receive(:match?).and_return(true) - expect(user_access).to receive(:can_merge_to_branch?).and_return(false) - expect(user_access).to receive(:can_push_to_branch?).and_return(false) + it 'is prevented' do + expect(subject.status).to be(false) + expect(subject.message).to include('cannot be deleted') + end + end - expect(subject.status).to be(false) - expect(subject.message).to eq('You are not allowed to merge code into protected branches on this project.') + context 'update' do + let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } + let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + + it 'is prevented' do + expect(subject.status).to be(false) + expect(subject.message).to include('cannot be updated') + end + end + end + + context 'creation' do + let(:oldrev) { '0000000000000000000000000000000000000000' } + let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + let(:ref) { 'refs/tags/v9.1.0' } + + it 'prevents creation below access level' do + expect(subject.status).to be(false) + expect(subject.message).to include('allowed to create this tag as it is protected') + end + + context 'when user has access' do + let!(:protected_tag) { create(:protected_tag, :developers_can_create, project: project, name: 'v*') } + + it 'allows tag creation' do + expect(subject.status).to be(true) + end + end + end end + end - it 'returns 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 'branches check' do + context 'trying to delete the default branch' do + let(:newrev) { '0000000000000000000000000000000000000000' } + let(:ref) { 'refs/heads/master' } - expect(subject.status).to be(false) - expect(subject.message).to eq('You are not allowed to push code to protected branches on this project.') + it 'returns an error' do + expect(subject.status).to be(false) + expect(subject.message).to eq('The default branch of a project cannot be deleted.') + end end - context 'branch deletion' do - let(:changes) do - { - oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c', - newrev: '0000000000000000000000000000000000000000', - ref: 'refs/heads/master' - } + context 'protected branches check' do + before do + allow(ProtectedBranch).to receive(:protected?).with(project, 'master').and_return(true) + allow(ProtectedBranch).to receive(:protected?).with(project, 'feature').and_return(true) + end + + it 'returns an error if the user is not allowed to do forced pushes to protected branches' do + expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) + + expect(subject.status).to be(false) + expect(subject.message).to eq('You are not allowed to force push code to a protected branch on this project.') + end + + it 'returns an error if the user is not allowed to merge to protected branches' do + expect_any_instance_of(Gitlab::Checks::MatchingMergeRequest).to receive(:match?).and_return(true) + expect(user_access).to receive(:can_merge_to_branch?).and_return(false) + expect(user_access).to receive(:can_push_to_branch?).and_return(false) + + expect(subject.status).to be(false) + expect(subject.message).to eq('You are not allowed to merge code into protected branches on this project.') end - it 'returns an error if the user is not allowed to delete protected branches' do + it 'returns 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) + expect(subject.status).to be(false) - expect(subject.message).to eq('You are not allowed to delete protected branches from this project.') + expect(subject.message).to eq('You are not allowed to push code to protected branches on this project.') + end + + context 'branch deletion' do + let(:newrev) { '0000000000000000000000000000000000000000' } + let(:ref) { 'refs/heads/feature' } + + context 'if the user is not allowed to delete protected branches' do + it 'returns an error' do + expect(subject.status).to be(false) + expect(subject.message).to eq('You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.') + end + end + + context 'if the user is allowed to delete protected branches' do + before do + project.add_master(user) + end + + context 'through the web interface' do + let(:protocol) { 'web' } + + it 'allows branch deletion' do + expect(subject.status).to be(true) + end + end + + context 'over SSH or HTTP' do + it 'returns an error' do + expect(subject.status).to be(false) + expect(subject.message).to eq('You can only delete protected branches using the web interface.') + end + end + end end end end diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb index 7a84bbebd02..bc66ce83d4a 100644 --- a/spec/lib/gitlab/checks/force_push_spec.rb +++ b/spec/lib/gitlab/checks/force_push_spec.rb @@ -1,19 +1,19 @@ require 'spec_helper' -describe Gitlab::Checks::ChangeAccess, lib: true do +describe Gitlab::Checks::ForcePush, lib: true do let(:project) { create(:project, :repository) } context "exit code checking" do it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0]) - expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error + expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error end it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) - expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError) + expect { described_class.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError) end end end diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb index abc93e1b44a..3b905611467 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb @@ -135,6 +135,17 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do subject { |example| path(example).nodes } it { is_expected.to eq 4 } end + + describe '#blob' do + let(:file_entry) { |example| path(example) } + subject { file_entry.blob } + + it 'returns a blob representing the entry data' do + expect(subject).to be_a(Blob) + expect(subject.path).to eq(file_entry.path) + expect(subject.size).to eq(file_entry.metadata[:size]) + end + end end describe 'non-existent/', path: 'non-existent/' do diff --git a/spec/lib/gitlab/ci/build/credentials/factory_spec.rb b/spec/lib/gitlab/ci/build/credentials/factory_spec.rb index 10b4b7a8826..d53db05e5e6 100644 --- a/spec/lib/gitlab/ci/build/credentials/factory_spec.rb +++ b/spec/lib/gitlab/ci/build/credentials/factory_spec.rb @@ -3,14 +3,14 @@ require 'spec_helper' describe Gitlab::Ci::Build::Credentials::Factory do let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) } - subject { Gitlab::Ci::Build::Credentials::Factory.new(build).create! } + subject { described_class.new(build).create! } class TestProvider def initialize(build); end end before do - allow_any_instance_of(Gitlab::Ci::Build::Credentials::Factory).to receive(:providers).and_return([TestProvider]) + allow_any_instance_of(described_class).to receive(:providers).and_return([TestProvider]) end context 'when provider is valid' do diff --git a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry_spec.rb index 84e44dd53e2..c6054138cde 100644 --- a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb +++ b/spec/lib/gitlab/ci/build/credentials/registry_spec.rb @@ -4,14 +4,14 @@ describe Gitlab::Ci::Build::Credentials::Registry do let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) } let(:registry_url) { 'registry.example.com:5005' } - subject { Gitlab::Ci::Build::Credentials::Registry.new(build) } + subject { described_class.new(build) } before do stub_container_registry_config(host_port: registry_url) end it 'contains valid DockerRegistry credentials' do - expect(subject).to be_kind_of(Gitlab::Ci::Build::Credentials::Registry) + expect(subject).to be_kind_of(described_class) expect(subject.username).to eq 'gitlab-ci-token' expect(subject.password).to eq build.token @@ -20,7 +20,7 @@ describe Gitlab::Ci::Build::Credentials::Registry do end describe '.valid?' do - subject { Gitlab::Ci::Build::Credentials::Registry.new(build).valid? } + subject { described_class.new(build).valid? } context 'when registry is enabled' do before do diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 684d01e9056..23270ad5053 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -113,7 +113,7 @@ describe Gitlab::Ci::Config::Entry::Global do describe '#variables_value' do it 'returns variables' do - expect(global.variables_value).to eq(VAR: 'value') + expect(global.variables_value).to eq('VAR' => 'value') end end @@ -154,7 +154,7 @@ describe Gitlab::Ci::Config::Entry::Global do services: ['postgres:9.1', 'mysql:5.5'], stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'] }, - variables: { VAR: 'value' }, + variables: { 'VAR' => 'value' }, ignore: false, after_script: ['make clean'] }, spinach: { name: :spinach, @@ -167,7 +167,7 @@ describe Gitlab::Ci::Config::Entry::Global do cache: { key: 'k', untracked: true, paths: ['public/'] }, variables: {}, ignore: false, - after_script: ['make clean'] }, + after_script: ['make clean'] } ) end end diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb index f15f02f403e..84bfef9e8ad 100644 --- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb @@ -13,6 +13,14 @@ describe Gitlab::Ci::Config::Entry::Variables do it 'returns hash with key value strings' do expect(entry.value).to eq config end + + context 'with numeric keys and values in the config' do + let(:config) { { 10 => 20 } } + + it 'converts numeric key and numeric value into strings' do + expect(entry.value).to eq('10' => '20') + end + end end describe '#errors' do diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb new file mode 100644 index 00000000000..809fda11879 --- /dev/null +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -0,0 +1,186 @@ +require 'spec_helper' + +describe Gitlab::Ci::CronParser do + shared_examples_for "returns time in the future" do + it { is_expected.to be > Time.now } + end + + describe '#next_time_from' do + subject { described_class.new(cron, cron_timezone).next_time_from(Time.now) } + + context 'when cron and cron_timezone are valid' do + context 'when specific time' do + let(:cron) { '3 4 5 6 *' } + let(:cron_timezone) { 'UTC' } + + it_behaves_like "returns time in the future" + + it 'returns exact time' do + expect(subject.min).to eq(3) + expect(subject.hour).to eq(4) + expect(subject.day).to eq(5) + expect(subject.month).to eq(6) + end + end + + context 'when specific day of week' do + let(:cron) { '* * * * 0' } + let(:cron_timezone) { 'UTC' } + + it_behaves_like "returns time in the future" + + it 'returns exact day of week' do + expect(subject.wday).to eq(0) + end + end + + context 'when slash used' do + let(:cron) { '*/10 */6 */10 */10 *' } + let(:cron_timezone) { 'UTC' } + + it_behaves_like "returns time in the future" + + it 'returns specific time' do + expect(subject.min).to be_in([0, 10, 20, 30, 40, 50]) + expect(subject.hour).to be_in([0, 6, 12, 18]) + expect(subject.day).to be_in([1, 11, 21, 31]) + expect(subject.month).to be_in([1, 11]) + end + end + + context 'when range used' do + let(:cron) { '0,20,40 * 1-5 * *' } + let(:cron_timezone) { 'UTC' } + + it_behaves_like "returns time in the future" + + it 'returns specific time' do + expect(subject.min).to be_in([0, 20, 40]) + expect(subject.day).to be_in((1..5).to_a) + end + end + + context 'when cron_timezone is TZInfo format' do + before do + allow(Time).to receive(:zone) + .and_return(ActiveSupport::TimeZone['UTC']) + end + + let(:hour_in_utc) do + ActiveSupport::TimeZone[cron_timezone] + .now.change(hour: 0).in_time_zone('UTC').hour + end + + context 'when cron_timezone is US/Pacific' do + let(:cron) { '* 0 * * *' } + let(:cron_timezone) { 'US/Pacific' } + + it_behaves_like "returns time in the future" + + it 'converts time in server time zone' do + expect(subject.hour).to eq(hour_in_utc) + end + end + end + + context 'when cron_timezone is ActiveSupport::TimeZone format' do + before do + allow(Time).to receive(:zone) + .and_return(ActiveSupport::TimeZone['UTC']) + end + + let(:hour_in_utc) do + ActiveSupport::TimeZone[cron_timezone] + .now.change(hour: 0).in_time_zone('UTC').hour + end + + context 'when cron_timezone is Berlin' do + let(:cron) { '* 0 * * *' } + let(:cron_timezone) { 'Berlin' } + + it_behaves_like "returns time in the future" + + it 'converts time in server time zone' do + expect(subject.hour).to eq(hour_in_utc) + end + end + + context 'when cron_timezone is Eastern Time (US & Canada)' do + let(:cron) { '* 0 * * *' } + let(:cron_timezone) { 'Eastern Time (US & Canada)' } + + it_behaves_like "returns time in the future" + + it 'converts time in server time zone' do + expect(subject.hour).to eq(hour_in_utc) + end + end + end + end + + context 'when cron and cron_timezone are invalid' do + let(:cron) { 'invalid_cron' } + let(:cron_timezone) { 'invalid_cron_timezone' } + + it { is_expected.to be_nil } + end + + context 'when cron syntax is quoted' do + let(:cron) { "'0 * * * *'" } + let(:cron_timezone) { 'UTC' } + + it { expect(subject).to be_nil } + end + + context 'when cron syntax is rufus-scheduler syntax' do + let(:cron) { 'every 3h' } + let(:cron_timezone) { 'UTC' } + + it { expect(subject).to be_nil } + end + end + + describe '#cron_valid?' do + subject { described_class.new(cron, Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE).cron_valid? } + + context 'when cron is valid' do + let(:cron) { '* * * * *' } + + it { is_expected.to eq(true) } + end + + context 'when cron is invalid' do + let(:cron) { '*********' } + + it { is_expected.to eq(false) } + end + + context 'when cron syntax is quoted' do + let(:cron) { "'0 * * * *'" } + + it { is_expected.to eq(false) } + end + end + + describe '#cron_timezone_valid?' do + subject { described_class.new(Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_CRON, cron_timezone).cron_timezone_valid? } + + context 'when cron is valid' do + let(:cron_timezone) { 'Europe/Istanbul' } + + it { is_expected.to eq(true) } + end + + context 'when cron is invalid' do + let(:cron_timezone) { 'Invalid-zone' } + + it { is_expected.to eq(false) } + end + + context 'when cron_timezone is ActiveSupport::TimeZone format' do + let(:cron_timezone) { 'Eastern Time (US & Canada)' } + + it { is_expected.to eq(true) } + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/action_spec.rb b/spec/lib/gitlab/ci/status/build/action_spec.rb new file mode 100644 index 00000000000..8c25f72804b --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/action_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Action do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#label' do + before do + allow(status).to receive(:label).and_return('label') + end + + context 'when status has action' do + before do + allow(status).to receive(:has_action?).and_return(true) + end + + it 'does not append text' do + expect(subject.label).to eq 'label' + end + end + + context 'when status does not have action' do + before do + allow(status).to receive(:has_action?).and_return(false) + end + + it 'appends text about action not allowed' do + expect(subject.label).to eq 'label (not allowed)' + end + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is an action' do + let(:build) { create(:ci_build, :manual) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not manual' do + let(:build) { create(:ci_build) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index e648a3ac3a2..185bb9098da 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -204,11 +204,12 @@ describe Gitlab::Ci::Status::Build::Factory do it 'matches correct extended statuses' do expect(factory.extended_statuses) - .to eq [Gitlab::Ci::Status::Build::Play] + .to eq [Gitlab::Ci::Status::Build::Play, + Gitlab::Ci::Status::Build::Action] end - it 'fabricates a play detailed status' do - expect(status).to be_a Gitlab::Ci::Status::Build::Play + it 'fabricates action detailed status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Action end it 'fabricates status with correct details' do @@ -216,11 +217,26 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.group).to eq 'manual' expect(status.icon).to eq 'icon_status_manual' expect(status.favicon).to eq 'favicon_status_manual' - expect(status.label).to eq 'manual play action' + expect(status.label).to include 'manual play action' expect(status).to have_details - expect(status).to have_action expect(status.action_path).to include 'play' end + + context 'when user has ability to play action' do + before do + build.project.add_master(user) + end + + it 'fabricates status that has action' do + expect(status).to have_action + end + end + + context 'when user does not have ability to play action' do + it 'fabricates status that has no action' do + expect(status).not_to have_action + end + end end context 'when build is an environment stop action' do @@ -232,21 +248,24 @@ describe Gitlab::Ci::Status::Build::Factory do it 'matches correct extended statuses' do expect(factory.extended_statuses) - .to eq [Gitlab::Ci::Status::Build::Stop] + .to eq [Gitlab::Ci::Status::Build::Stop, + Gitlab::Ci::Status::Build::Action] end - it 'fabricates a stop detailed status' do - expect(status).to be_a Gitlab::Ci::Status::Build::Stop + it 'fabricates action detailed status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Action end - it 'fabricates status with correct details' do - expect(status.text).to eq 'manual' - expect(status.group).to eq 'manual' - expect(status.icon).to eq 'icon_status_manual' - expect(status.favicon).to eq 'favicon_status_manual' - expect(status.label).to eq 'manual stop action' - expect(status).to have_details - expect(status).to have_action + context 'when user is not allowed to execute manual action' do + it 'fabricates status with correct details' do + expect(status.text).to eq 'manual' + expect(status.group).to eq 'manual' + expect(status.icon).to eq 'icon_status_manual' + expect(status.favicon).to eq 'favicon_status_manual' + expect(status.label).to eq 'manual stop action (not allowed)' + expect(status).to have_details + expect(status).not_to have_action + end end end end diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index 6c97a4fe5ca..f5d0f977768 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -1,43 +1,48 @@ require 'spec_helper' describe Gitlab::Ci::Status::Build::Play do - let(:status) { double('core') } - let(:user) { double('user') } + let(:user) { create(:user) } + let(:build) { create(:ci_build, :manual) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } subject { described_class.new(status) } describe '#label' do - it { expect(subject.label).to eq 'manual play action' } + it 'has a label that says it is a manual action' do + expect(subject.label).to eq 'manual play action' + end end - describe 'action details' do - let(:user) { create(:user) } - let(:build) { create(:ci_build) } - let(:status) { Gitlab::Ci::Status::Core.new(build, user) } - - describe '#has_action?' do - context 'when user is allowed to update build' do - before { build.project.team << [user, :developer] } + describe '#has_action?' do + context 'when user is allowed to update build' do + context 'when user can push to branch' do + before { build.project.add_master(user) } it { is_expected.to have_action } end - context 'when user is not allowed to update build' do + context 'when user can not push to the branch' do + before { build.project.add_developer(user) } + it { is_expected.not_to have_action } end end - describe '#action_path' do - it { expect(subject.action_path).to include "#{build.id}/play" } + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } end + end - describe '#action_icon' do - it { expect(subject.action_icon).to eq 'icon_action_play' } - end + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/play" } + end - describe '#action_title' do - it { expect(subject.action_title).to eq 'Play' } - end + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'icon_action_play' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Play' } end describe '.matches?' do diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb index c2d74ca5cde..6eacb07078b 100644 --- a/spec/lib/gitlab/ci/status/extended_spec.rb +++ b/spec/lib/gitlab/ci/status/extended_spec.rb @@ -1,12 +1,8 @@ require 'spec_helper' describe Gitlab::Ci::Status::Extended do - subject do - Class.new.include(described_class) - end - it 'requires subclass to implement matcher' do - expect { subject.matches?(double, double) } + expect { described_class.matches?(double, double) } .to raise_error(NotImplementedError) end end diff --git a/spec/lib/gitlab/ci/status/group/common_spec.rb b/spec/lib/gitlab/ci/status/group/common_spec.rb new file mode 100644 index 00000000000..c0ca05881f5 --- /dev/null +++ b/spec/lib/gitlab/ci/status/group/common_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Group::Common do + subject do + Gitlab::Ci::Status::Core.new(double, double) + .extend(described_class) + end + + it 'does not have action' do + expect(subject).not_to have_action + end + + it 'has details' do + expect(subject).not_to have_details + end + + it 'has no details_path' do + expect(subject.details_path).to be_falsy + end +end diff --git a/spec/lib/gitlab/ci/status/group/factory_spec.rb b/spec/lib/gitlab/ci/status/group/factory_spec.rb new file mode 100644 index 00000000000..0cd83123938 --- /dev/null +++ b/spec/lib/gitlab/ci/status/group/factory_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Group::Factory do + it 'inherits from the core factory' do + expect(described_class) + .to be < Gitlab::Ci::Status::Factory + end + + it 'exposes group helpers' do + expect(described_class.common_helpers) + .to eq Gitlab::Ci::Status::Group::Common + end +end diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb new file mode 100644 index 00000000000..40ac5a3ed37 --- /dev/null +++ b/spec/lib/gitlab/ci/trace/stream_spec.rb @@ -0,0 +1,256 @@ +require 'spec_helper' + +describe Gitlab::Ci::Trace::Stream do + describe 'delegates' do + subject { described_class.new { nil } } + + it { is_expected.to delegate_method(:close).to(:stream) } + it { is_expected.to delegate_method(:tell).to(:stream) } + it { is_expected.to delegate_method(:seek).to(:stream) } + it { is_expected.to delegate_method(:size).to(:stream) } + it { is_expected.to delegate_method(:path).to(:stream) } + it { is_expected.to delegate_method(:truncate).to(:stream) } + it { is_expected.to delegate_method(:valid?).to(:stream).as(:present?) } + it { is_expected.to delegate_method(:file?).to(:path).as(:present?) } + end + + describe '#limit' do + let(:stream) do + described_class.new do + StringIO.new((1..8).to_a.join("\n")) + end + end + + it 'if size is larger we start from beginning' do + stream.limit(20) + + expect(stream.tell).to eq(0) + end + + it 'if size is smaller we start from the end' do + stream.limit(2) + + expect(stream.raw).to eq("8") + end + + context 'when the trace contains ANSI sequence and Unicode' do + let(:stream) do + described_class.new do + File.open(expand_fixture_path('trace/ansi-sequence-and-unicode')) + end + end + + it 'forwards to the next linefeed, case 1' do + stream.limit(7) + + result = stream.raw + + expect(result).to eq('') + expect(result.encoding).to eq(Encoding.default_external) + end + + it 'forwards to the next linefeed, case 2' do + stream.limit(29) + + result = stream.raw + + expect(result).to eq("\e[01;32m許功蓋\e[0m\n") + expect(result.encoding).to eq(Encoding.default_external) + end + + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/30796 + it 'reads in binary, output as Encoding.default_external' do + stream.limit(52) + + result = stream.html + + expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ<br><span class=\"term-fg-green\">許功蓋</span><br>") + expect(result.encoding).to eq(Encoding.default_external) + end + end + end + + describe '#append' do + let(:tempfile) { Tempfile.new } + + let(:stream) do + described_class.new do + tempfile.write("12345678") + tempfile.rewind + tempfile + end + end + + after do + tempfile.unlink + end + + it "truncates and append content" do + stream.append("89", 4) + stream.seek(0) + + expect(stream.size).to eq(6) + expect(stream.raw).to eq("123489") + end + + it 'appends in binary mode' do + '😺'.force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset| + stream.append(byte, offset) + end + + stream.seek(0) + + expect(stream.size).to eq(4) + expect(stream.raw).to eq('😺') + end + end + + describe '#set' do + let(:stream) do + described_class.new do + StringIO.new("12345678") + end + end + + before do + stream.set("8901") + end + + it "overwrite content" do + stream.seek(0) + + expect(stream.size).to eq(4) + expect(stream.raw).to eq("8901") + end + end + + describe '#raw' do + let(:path) { __FILE__ } + let(:lines) { File.readlines(path) } + let(:stream) do + described_class.new do + File.open(path) + end + end + + it 'returns all contents if last_lines is not specified' do + result = stream.raw + + expect(result).to eq(lines.join) + expect(result.encoding).to eq(Encoding.default_external) + end + + context 'limit max lines' do + before do + # specifying BUFFER_SIZE forces to seek backwards + allow(described_class).to receive(:BUFFER_SIZE) + .and_return(2) + end + + it 'returns last few lines' do + result = stream.raw(last_lines: 2) + + expect(result).to eq(lines.last(2).join) + expect(result.encoding).to eq(Encoding.default_external) + end + + it 'returns everything if trying to get too many lines' do + result = stream.raw(last_lines: lines.size * 2) + + expect(result).to eq(lines.join) + expect(result.encoding).to eq(Encoding.default_external) + end + end + end + + describe '#html_with_state' do + let(:stream) do + described_class.new do + StringIO.new("1234") + end + end + + it 'returns html content with state' do + result = stream.html_with_state + + expect(result.html).to eq("1234") + end + + context 'follow-up state' do + let!(:last_result) { stream.html_with_state } + + before do + stream.append("5678", 4) + stream.seek(0) + end + + it "returns appended trace" do + result = stream.html_with_state(last_result.state) + + expect(result.append).to be_truthy + expect(result.html).to eq("5678") + end + end + end + + describe '#html' do + let(:stream) do + described_class.new do + StringIO.new("12\n34\n56") + end + end + + it "returns html" do + expect(stream.html).to eq("12<br>34<br>56") + end + + it "returns html for last line only" do + expect(stream.html(last_lines: 1)).to eq("56") + end + end + + describe '#extract_coverage' do + let(:stream) do + described_class.new do + StringIO.new(data) + end + end + + subject { stream.extract_coverage(regex) } + + context 'valid content & regex' do + let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered' } + let(:regex) { '\(\d+.\d+\%\) covered' } + + it { is_expected.to eq("98.29") } + end + + context 'valid content & bad regex' do + let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' } + let(:regex) { 'very covered' } + + it { is_expected.to be_nil } + end + + context 'no coverage content & regex' do + let(:data) { 'No coverage for today :sad:' } + let(:regex) { '\(\d+.\d+\%\) covered' } + + it { is_expected.to be_nil } + end + + context 'multiple results in content & regex' do + let(:data) { ' (98.39%) covered. (98.29%) covered' } + let(:regex) { '\(\d+.\d+\%\) covered' } + + it { is_expected.to eq("98.29") } + end + + context 'using a regex capture' do + let(:data) { 'TOTAL 9926 3489 65%' } + let(:regex) { 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)' } + + it { is_expected.to eq("65") } + end + end +end diff --git a/spec/lib/gitlab/ci/trace_reader_spec.rb b/spec/lib/gitlab/ci/trace_reader_spec.rb deleted file mode 100644 index ff5551bf703..00000000000 --- a/spec/lib/gitlab/ci/trace_reader_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::TraceReader do - let(:path) { __FILE__ } - let(:lines) { File.readlines(path) } - let(:bytesize) { lines.sum(&:bytesize) } - - it 'returns last few lines' do - 10.times do - subject = build_subject - last_lines = random_lines - - expected = lines.last(last_lines).join - result = subject.read(last_lines: last_lines) - - expect(result).to eq(expected) - expect(result.encoding).to eq(Encoding.default_external) - end - end - - it 'returns everything if trying to get too many lines' do - result = build_subject.read(last_lines: lines.size * 2) - - expect(result).to eq(lines.join) - expect(result.encoding).to eq(Encoding.default_external) - end - - it 'returns all contents if last_lines is not specified' do - result = build_subject.read - - expect(result).to eq(lines.join) - expect(result.encoding).to eq(Encoding.default_external) - end - - it 'raises an error if not passing an integer for last_lines' do - expect do - build_subject.read(last_lines: lines) - end.to raise_error(ArgumentError) - end - - def random_lines - Random.rand(lines.size) + 1 - end - - def random_buffer - Random.rand(bytesize) + 1 - end - - def build_subject - described_class.new(__FILE__, buffer_size: random_buffer) - end -end diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb new file mode 100644 index 00000000000..9cb0b62590a --- /dev/null +++ b/spec/lib/gitlab/ci/trace_spec.rb @@ -0,0 +1,228 @@ +require 'spec_helper' + +describe Gitlab::Ci::Trace do + let(:build) { create(:ci_build) } + let(:trace) { described_class.new(build) } + + describe "associations" do + it { expect(trace).to respond_to(:job) } + it { expect(trace).to delegate_method(:old_trace).to(:job) } + end + + describe '#html' do + before do + trace.set("12\n34") + end + + it "returns formatted html" do + expect(trace.html).to eq("12<br>34") + end + + it "returns last line of formatted html" do + expect(trace.html(last_lines: 1)).to eq("34") + end + end + + describe '#raw' do + before do + trace.set("12\n34") + end + + it "returns raw output" do + expect(trace.raw).to eq("12\n34") + end + + it "returns last line of raw output" do + expect(trace.raw(last_lines: 1)).to eq("34") + end + end + + describe '#extract_coverage' do + let(:regex) { '\(\d+.\d+\%\) covered' } + + context 'matching coverage' do + before do + trace.set('Coverage 1033 / 1051 LOC (98.29%) covered') + end + + it "returns valid coverage" do + expect(trace.extract_coverage(regex)).to eq("98.29") + end + end + + context 'no coverage' do + before do + trace.set('No coverage') + end + + it 'returs nil' do + expect(trace.extract_coverage(regex)).to be_nil + end + end + end + + describe '#set' do + before do + trace.set("12") + end + + it "returns trace" do + expect(trace.raw).to eq("12") + end + + context 'overwrite trace' do + before do + trace.set("34") + end + + it "returns new trace" do + expect(trace.raw).to eq("34") + end + end + + context 'runners token' do + let(:token) { 'my_secret_token' } + + before do + build.project.update(runners_token: token) + trace.set(token) + end + + it "hides token" do + expect(trace.raw).not_to include(token) + end + end + + context 'hides build token' do + let(:token) { 'my_secret_token' } + + before do + build.update(token: token) + trace.set(token) + end + + it "hides token" do + expect(trace.raw).not_to include(token) + end + end + end + + describe '#append' do + before do + trace.set("1234") + end + + it "returns correct trace" do + expect(trace.append("56", 4)).to eq(6) + expect(trace.raw).to eq("123456") + end + + context 'tries to append trace at different offset' do + it "fails with append" do + expect(trace.append("56", 2)).to eq(-4) + expect(trace.raw).to eq("1234") + end + end + + context 'runners token' do + let(:token) { 'my_secret_token' } + + before do + build.project.update(runners_token: token) + trace.append(token, 0) + end + + it "hides token" do + expect(trace.raw).not_to include(token) + end + end + + context 'build token' do + let(:token) { 'my_secret_token' } + + before do + build.update(token: token) + trace.append(token, 0) + end + + it "hides token" do + expect(trace.raw).not_to include(token) + end + end + end + + describe 'trace handling' do + context 'trace does not exist' do + it { expect(trace.exist?).to be(false) } + end + + context 'new trace path is used' do + before do + trace.send(:ensure_directory) + + File.open(trace.send(:default_path), "w") do |file| + file.write("data") + end + end + + it "trace exist" do + expect(trace.exist?).to be(true) + end + + it "can be erased" do + trace.erase! + expect(trace.exist?).to be(false) + end + end + + context 'deprecated path' do + let(:path) { trace.send(:deprecated_path) } + + context 'with valid ci_id' do + before do + build.project.update(ci_id: 1000) + + FileUtils.mkdir_p(File.dirname(path)) + + File.open(path, "w") do |file| + file.write("data") + end + end + + it "trace exist" do + expect(trace.exist?).to be(true) + end + + it "can be erased" do + trace.erase! + expect(trace.exist?).to be(false) + end + end + + context 'without valid ci_id' do + it "does not return deprecated path" do + expect(path).to be_nil + end + end + end + + context 'stored in database' do + before do + build.send(:write_attribute, :trace, "data") + end + + it "trace exist" do + expect(trace.exist?).to be(true) + end + + it "can be erased" do + trace.erase! + expect(trace.exist?).to be(false) + end + + it "returns database data" do + expect(trace.raw).to eq("data") + end + end + end +end diff --git a/spec/lib/gitlab/conflict/file_collection_spec.rb b/spec/lib/gitlab/conflict/file_collection_spec.rb index 39d892c18c0..27f23ea70dc 100644 --- a/spec/lib/gitlab/conflict/file_collection_spec.rb +++ b/spec/lib/gitlab/conflict/file_collection_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Conflict::FileCollection, lib: true do let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start') } - let(:file_collection) { Gitlab::Conflict::FileCollection.new(merge_request) } + let(:file_collection) { described_class.read_only(merge_request) } describe '#files' do it 'returns an array of Conflict::Files' do diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb index e18a219ef36..79632e2b6a3 100644 --- a/spec/lib/gitlab/contributions_calendar_spec.rb +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -47,7 +47,7 @@ describe Gitlab::ContributionsCalendar do action: Event::CREATED, target: @targets[project], author: contributor, - created_at: day, + created_at: day ) end diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index b01c4805a34..c796c98ec9f 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -10,7 +10,7 @@ describe Gitlab::CurrentSettings do describe '#current_application_settings' do context 'with DB available' do before do - allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true) + allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(true) end it 'attempts to use cached values first' do @@ -36,7 +36,7 @@ describe Gitlab::CurrentSettings do context 'with DB unavailable' do before do - allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false) + allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(false) end it 'returns an in-memory ApplicationSetting object' do diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb index c455cd9b942..d8757c601ab 100644 --- a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do before do allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return(Issue.all) - allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:serialize) do |event| + allow_any_instance_of(described_class).to receive(:serialize) do |event| event end diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 9d2ba481919..3610a0354e8 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -11,8 +11,6 @@ describe 'cycle analytics events' do end before do - allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context]) - setup(context) end @@ -132,6 +130,8 @@ describe 'cycle analytics events' do end before do + merge_request.update(head_pipeline: pipeline) + create(:ci_build, pipeline: pipeline, status: :success, author: user) create(:ci_build, pipeline: pipeline, status: :success, author: user) @@ -228,6 +228,8 @@ describe 'cycle analytics events' do end before do + merge_request.update(head_pipeline: pipeline) + create(:ci_build, pipeline: pipeline, status: :success, author: user) create(:ci_build, pipeline: pipeline, status: :success, author: user) @@ -332,7 +334,7 @@ describe 'cycle analytics events' do def setup(context) milestone = create(:milestone, project: project) context.update(milestone: milestone) - mr = create_merge_request_closing_issue(context) + mr = create_merge_request_closing_issue(context, commit_message: "References #{context.to_reference}") ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) end diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index dbcfb9b7400..e59cba35b2f 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -35,6 +35,7 @@ describe Gitlab::DataBuilder::Push, lib: true do it { expect(data[:ref]).to eq('refs/tags/v1.1.0') } it { expect(data[:user_id]).to eq(user.id) } it { expect(data[:user_name]).to eq(user.name) } + it { expect(data[:user_username]).to eq(user.username) } it { expect(data[:user_email]).to eq(user.email) } it { expect(data[:user_avatar]).to eq(user.avatar_url) } it { expect(data[:project_id]).to eq(project.id) } diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index e007044868c..dfa3ae9142e 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -58,6 +58,48 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end end + describe '#remove_concurrent_index' do + context 'outside a transaction' do + before do + allow(model).to receive(:transaction_open?).and_return(false) + end + + context 'using PostgreSQL' do + before do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + allow(model).to receive(:disable_statement_timeout) + end + + it 'removes the index concurrently' do + expect(model).to receive(:remove_index). + with(:users, { algorithm: :concurrently, column: :foo }) + + model.remove_concurrent_index(:users, :foo) + end + end + + context 'using MySQL' do + it 'removes an index' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + + expect(model).to receive(:remove_index). + with(:users, { column: :foo }) + + model.remove_concurrent_index(:users, :foo) + end + end + end + + context 'inside a transaction' do + it 'raises RuntimeError' do + expect(model).to receive(:transaction_open?).and_return(true) + + expect { model.remove_concurrent_index(:users, :foo) }. + to raise_error(RuntimeError) + end + end + end + describe '#add_concurrent_foreign_key' do context 'inside a transaction' do it 'raises an error' do @@ -133,6 +175,50 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end end + describe '#true_value' do + context 'using PostgreSQL' do + before do + expect(Gitlab::Database).to receive(:postgresql?).and_return(true) + end + + it 'returns the appropriate value' do + expect(model.true_value).to eq("'t'") + end + end + + context 'using MySQL' do + before do + expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + end + + it 'returns the appropriate value' do + expect(model.true_value).to eq(1) + end + end + end + + describe '#false_value' do + context 'using PostgreSQL' do + before do + expect(Gitlab::Database).to receive(:postgresql?).and_return(true) + end + + it 'returns the appropriate value' do + expect(model.false_value).to eq("'f'") + end + end + + context 'using MySQL' do + before do + expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + end + + it 'returns the appropriate value' do + expect(model.false_value).to eq(0) + end + end + end + describe '#update_column_in_batches' do before do create_list(:empty_project, 5) @@ -252,4 +338,431 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end end end + + describe '#rename_column_concurrently' do + context 'in a transaction' do + it 'raises RuntimeError' do + allow(model).to receive(:transaction_open?).and_return(true) + + expect { model.rename_column_concurrently(:users, :old, :new) }. + to raise_error(RuntimeError) + end + end + + context 'outside a transaction' do + let(:old_column) do + double(:column, + type: :integer, + limit: 8, + default: 0, + null: false, + precision: 5, + scale: 1) + end + + let(:trigger_name) { model.rename_trigger_name(:users, :old, :new) } + + before do + allow(model).to receive(:transaction_open?).and_return(false) + allow(model).to receive(:column_for).and_return(old_column) + + # Since MySQL and PostgreSQL use different quoting styles we'll just + # stub the methods used for this to make testing easier. + allow(model).to receive(:quote_column_name) { |name| name.to_s } + allow(model).to receive(:quote_table_name) { |name| name.to_s } + end + + context 'using MySQL' do + it 'renames a column concurrently' do + allow(Gitlab::Database).to receive(:postgresql?).and_return(false) + + expect(model).to receive(:install_rename_triggers_for_mysql). + with(trigger_name, 'users', 'old', 'new') + + expect(model).to receive(:add_column). + with(:users, :new, :integer, + limit: old_column.limit, + precision: old_column.precision, + scale: old_column.scale) + + expect(model).to receive(:change_column_default). + with(:users, :new, old_column.default) + + expect(model).to receive(:update_column_in_batches) + + expect(model).to receive(:change_column_null).with(:users, :new, false) + + expect(model).to receive(:copy_indexes).with(:users, :old, :new) + expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new) + + model.rename_column_concurrently(:users, :old, :new) + end + end + + context 'using PostgreSQL' do + it 'renames a column concurrently' do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + + expect(model).to receive(:install_rename_triggers_for_postgresql). + with(trigger_name, 'users', 'old', 'new') + + expect(model).to receive(:add_column). + with(:users, :new, :integer, + limit: old_column.limit, + precision: old_column.precision, + scale: old_column.scale) + + expect(model).to receive(:change_column_default). + with(:users, :new, old_column.default) + + expect(model).to receive(:update_column_in_batches) + + expect(model).to receive(:change_column_null).with(:users, :new, false) + + expect(model).to receive(:copy_indexes).with(:users, :old, :new) + expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new) + + model.rename_column_concurrently(:users, :old, :new) + end + end + end + end + + describe '#cleanup_concurrent_column_rename' do + it 'cleans up the renaming procedure for PostgreSQL' do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + + expect(model).to receive(:remove_rename_triggers_for_postgresql). + with(:users, /trigger_.{12}/) + + expect(model).to receive(:remove_column).with(:users, :old) + + model.cleanup_concurrent_column_rename(:users, :old, :new) + end + + it 'cleans up the renaming procedure for MySQL' do + allow(Gitlab::Database).to receive(:postgresql?).and_return(false) + + expect(model).to receive(:remove_rename_triggers_for_mysql). + with(/trigger_.{12}/) + + expect(model).to receive(:remove_column).with(:users, :old) + + model.cleanup_concurrent_column_rename(:users, :old, :new) + end + end + + describe '#change_column_type_concurrently' do + it 'changes the column type' do + expect(model).to receive(:rename_column_concurrently). + with('users', 'username', 'username_for_type_change', type: :text) + + model.change_column_type_concurrently('users', 'username', :text) + end + end + + describe '#cleanup_concurrent_column_type_change' do + it 'cleans up the type changing procedure' do + expect(model).to receive(:cleanup_concurrent_column_rename). + with('users', 'username', 'username_for_type_change') + + expect(model).to receive(:rename_column). + with('users', 'username_for_type_change', 'username') + + model.cleanup_concurrent_column_type_change('users', 'username') + end + end + + describe '#install_rename_triggers_for_postgresql' do + it 'installs the triggers for PostgreSQL' do + expect(model).to receive(:execute). + with(/CREATE OR REPLACE FUNCTION foo()/m) + + expect(model).to receive(:execute). + with(/CREATE TRIGGER foo/m) + + model.install_rename_triggers_for_postgresql('foo', :users, :old, :new) + end + end + + describe '#install_rename_triggers_for_mysql' do + it 'installs the triggers for MySQL' do + expect(model).to receive(:execute). + with(/CREATE TRIGGER foo_insert.+ON users/m) + + expect(model).to receive(:execute). + with(/CREATE TRIGGER foo_update.+ON users/m) + + model.install_rename_triggers_for_mysql('foo', :users, :old, :new) + end + end + + describe '#remove_rename_triggers_for_postgresql' do + it 'removes the function and trigger' do + expect(model).to receive(:execute).with('DROP TRIGGER foo ON bar') + expect(model).to receive(:execute).with('DROP FUNCTION foo()') + + model.remove_rename_triggers_for_postgresql('bar', 'foo') + end + end + + describe '#remove_rename_triggers_for_mysql' do + it 'removes the triggers' do + expect(model).to receive(:execute).with('DROP TRIGGER foo_insert') + expect(model).to receive(:execute).with('DROP TRIGGER foo_update') + + model.remove_rename_triggers_for_mysql('foo') + end + end + + describe '#rename_trigger_name' do + it 'returns a String' do + expect(model.rename_trigger_name(:users, :foo, :bar)). + to match(/trigger_.{12}/) + end + end + + describe '#indexes_for' do + it 'returns the indexes for a column' do + idx1 = double(:idx, columns: %w(project_id)) + idx2 = double(:idx, columns: %w(user_id)) + + allow(model).to receive(:indexes).with('table').and_return([idx1, idx2]) + + expect(model.indexes_for('table', :user_id)).to eq([idx2]) + end + end + + describe '#foreign_keys_for' do + it 'returns the foreign keys for a column' do + fk1 = double(:fk, column: 'project_id') + fk2 = double(:fk, column: 'user_id') + + allow(model).to receive(:foreign_keys).with('table').and_return([fk1, fk2]) + + expect(model.foreign_keys_for('table', :user_id)).to eq([fk2]) + end + end + + describe '#copy_indexes' do + context 'using a regular index using a single column' do + it 'copies the index' do + index = double(:index, + columns: %w(project_id), + name: 'index_on_issues_project_id', + using: nil, + where: nil, + opclasses: {}, + unique: false, + lengths: [], + orders: []) + + allow(model).to receive(:indexes_for).with(:issues, 'project_id'). + and_return([index]) + + expect(model).to receive(:add_concurrent_index). + with(:issues, + %w(gl_project_id), + unique: false, + name: 'index_on_issues_gl_project_id', + length: [], + order: []) + + model.copy_indexes(:issues, :project_id, :gl_project_id) + end + end + + context 'using a regular index with multiple columns' do + it 'copies the index' do + index = double(:index, + columns: %w(project_id foobar), + name: 'index_on_issues_project_id_foobar', + using: nil, + where: nil, + opclasses: {}, + unique: false, + lengths: [], + orders: []) + + allow(model).to receive(:indexes_for).with(:issues, 'project_id'). + and_return([index]) + + expect(model).to receive(:add_concurrent_index). + with(:issues, + %w(gl_project_id foobar), + unique: false, + name: 'index_on_issues_gl_project_id_foobar', + length: [], + order: []) + + model.copy_indexes(:issues, :project_id, :gl_project_id) + end + end + + context 'using an index with a WHERE clause' do + it 'copies the index' do + index = double(:index, + columns: %w(project_id), + name: 'index_on_issues_project_id', + using: nil, + where: 'foo', + opclasses: {}, + unique: false, + lengths: [], + orders: []) + + allow(model).to receive(:indexes_for).with(:issues, 'project_id'). + and_return([index]) + + expect(model).to receive(:add_concurrent_index). + with(:issues, + %w(gl_project_id), + unique: false, + name: 'index_on_issues_gl_project_id', + length: [], + order: [], + where: 'foo') + + model.copy_indexes(:issues, :project_id, :gl_project_id) + end + end + + context 'using an index with a USING clause' do + it 'copies the index' do + index = double(:index, + columns: %w(project_id), + name: 'index_on_issues_project_id', + where: nil, + using: 'foo', + opclasses: {}, + unique: false, + lengths: [], + orders: []) + + allow(model).to receive(:indexes_for).with(:issues, 'project_id'). + and_return([index]) + + expect(model).to receive(:add_concurrent_index). + with(:issues, + %w(gl_project_id), + unique: false, + name: 'index_on_issues_gl_project_id', + length: [], + order: [], + using: 'foo') + + model.copy_indexes(:issues, :project_id, :gl_project_id) + end + end + + context 'using an index with custom operator classes' do + it 'copies the index' do + index = double(:index, + columns: %w(project_id), + name: 'index_on_issues_project_id', + using: nil, + where: nil, + opclasses: { 'project_id' => 'bar' }, + unique: false, + lengths: [], + orders: []) + + allow(model).to receive(:indexes_for).with(:issues, 'project_id'). + and_return([index]) + + expect(model).to receive(:add_concurrent_index). + with(:issues, + %w(gl_project_id), + unique: false, + name: 'index_on_issues_gl_project_id', + length: [], + order: [], + opclasses: { 'gl_project_id' => 'bar' }) + + model.copy_indexes(:issues, :project_id, :gl_project_id) + end + end + + describe 'using an index of which the name does not contain the source column' do + it 'raises RuntimeError' do + index = double(:index, + columns: %w(project_id), + name: 'index_foobar_index', + using: nil, + where: nil, + opclasses: {}, + unique: false, + lengths: [], + orders: []) + + allow(model).to receive(:indexes_for).with(:issues, 'project_id'). + and_return([index]) + + expect { model.copy_indexes(:issues, :project_id, :gl_project_id) }. + to raise_error(RuntimeError) + end + end + end + + describe '#copy_foreign_keys' do + it 'copies foreign keys from one column to another' do + fk = double(:fk, + from_table: 'issues', + to_table: 'projects', + on_delete: :cascade) + + allow(model).to receive(:foreign_keys_for).with(:issues, :project_id). + and_return([fk]) + + expect(model).to receive(:add_concurrent_foreign_key). + with('issues', 'projects', column: :gl_project_id, on_delete: :cascade) + + model.copy_foreign_keys(:issues, :project_id, :gl_project_id) + end + end + + describe '#column_for' do + it 'returns a column object for an existing column' do + column = model.column_for(:users, :id) + + expect(column.name).to eq('id') + end + + it 'returns nil when a column does not exist' do + expect(model.column_for(:users, :kittens)).to be_nil + end + end + + describe '#replace_sql' do + context 'using postgres' do + before do + allow(Gitlab::Database).to receive(:mysql?).and_return(false) + end + + it 'builds the sql with correct functions' do + expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s). + to include('regexp_replace') + end + end + + context 'using mysql' do + before do + allow(Gitlab::Database).to receive(:mysql?).and_return(true) + end + + it 'builds the sql with the correct functions' do + expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s). + to include('locate', 'insert') + end + end + + describe 'results' do + let!(:user) { create(:user, name: 'Kathy Alice Aliceson') } + + it 'replaces the correct part of the string' do + model.update_column_in_batches(:users, :name, model.replace_sql(Arel::Table.new(:users)[:name], 'Alice', 'Eve')) + expect(user.reload.name).to eq('Kathy Eve Aliceson') + end + end + end end diff --git a/spec/lib/gitlab/database/multi_threaded_migration_spec.rb b/spec/lib/gitlab/database/multi_threaded_migration_spec.rb new file mode 100644 index 00000000000..6c45f13bb5a --- /dev/null +++ b/spec/lib/gitlab/database/multi_threaded_migration_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Gitlab::Database::MultiThreadedMigration do + let(:migration) do + Class.new { include Gitlab::Database::MultiThreadedMigration }.new + end + + describe '#connection' do + after do + Thread.current[described_class::MULTI_THREAD_AR_CONNECTION] = nil + end + + it 'returns the thread-local connection if present' do + Thread.current[described_class::MULTI_THREAD_AR_CONNECTION] = 10 + + expect(migration.connection).to eq(10) + end + + it 'returns the global connection if no thread-local connection was set' do + expect(migration.connection).to eq(ActiveRecord::Base.connection) + end + end + + describe '#with_multiple_threads' do + it 'starts multiple threads and yields the supplied block in every thread' do + output = Queue.new + + migration.with_multiple_threads(2) do + output << migration.connection.execute('SELECT 1') + end + + expect(output.size).to eq(2) + end + + it 'joins the threads when the join parameter is set' do + expect_any_instance_of(Thread).to receive(:join).and_call_original + + migration.with_multiple_threads(1) { } + end + end +end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb new file mode 100644 index 00000000000..a3ab4e3dd9e --- /dev/null +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb @@ -0,0 +1,206 @@ +require 'spec_helper' + +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase do + let(:migration) { FakeRenameReservedPathMigrationV1.new } + let(:subject) { described_class.new(['the-path'], migration) } + + before do + allow(migration).to receive(:say) + end + + def migration_namespace(namespace) + Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses:: + Namespace.find(namespace.id) + end + + def migration_project(project) + Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses:: + Project.find(project.id) + end + + describe "#remove_last_ocurrence" do + it "removes only the last occurance of a string" do + input = "this/is/a-word-to-replace/namespace/with/a-word-to-replace" + + expect(subject.remove_last_occurrence(input, "a-word-to-replace")) + .to eq("this/is/a-word-to-replace/namespace/with/") + end + end + + describe '#remove_cached_html_for_projects' do + let(:project) { create(:empty_project, description_html: 'Project description') } + + it 'removes description_html from projects' do + subject.remove_cached_html_for_projects([project.id]) + + expect(project.reload.description_html).to be_nil + end + + it 'removes issue descriptions' do + issue = create(:issue, project: project, description_html: 'Issue description') + + subject.remove_cached_html_for_projects([project.id]) + + expect(issue.reload.description_html).to be_nil + end + + it 'removes merge request descriptions' do + merge_request = create(:merge_request, + source_project: project, + target_project: project, + description_html: 'MergeRequest description') + + subject.remove_cached_html_for_projects([project.id]) + + expect(merge_request.reload.description_html).to be_nil + end + + it 'removes note html' do + note = create(:note, + project: project, + noteable: create(:issue, project: project), + note_html: 'note description') + + subject.remove_cached_html_for_projects([project.id]) + + expect(note.reload.note_html).to be_nil + end + + it 'removes milestone description' do + milestone = create(:milestone, + project: project, + description_html: 'milestone description') + + subject.remove_cached_html_for_projects([project.id]) + + expect(milestone.reload.description_html).to be_nil + end + end + + describe '#rename_path_for_routable' do + context 'for namespaces' do + let(:namespace) { create(:namespace, path: 'the-path') } + it "renames namespaces called the-path" do + subject.rename_path_for_routable(migration_namespace(namespace)) + + expect(namespace.reload.path).to eq("the-path0") + end + + it "renames the route to the namespace" do + subject.rename_path_for_routable(migration_namespace(namespace)) + + expect(Namespace.find(namespace.id).full_path).to eq("the-path0") + end + + it "renames the route for projects of the namespace" do + project = create(:project, path: "project-path", namespace: namespace) + + subject.rename_path_for_routable(migration_namespace(namespace)) + + expect(project.route.reload.path).to eq("the-path0/project-path") + end + + it 'returns the old & the new path' do + old_path, new_path = subject.rename_path_for_routable(migration_namespace(namespace)) + + expect(old_path).to eq('the-path') + expect(new_path).to eq('the-path0') + end + + it "doesn't rename routes that start with a similar name" do + other_namespace = create(:namespace, path: 'the-path-but-not-really') + project = create(:empty_project, path: 'the-project', namespace: other_namespace) + + subject.rename_path_for_routable(migration_namespace(namespace)) + + expect(project.route.reload.path).to eq('the-path-but-not-really/the-project') + end + + context "the-path namespace -> subgroup -> the-path0 project" do + it "updates the route of the project correctly" do + subgroup = create(:group, path: "subgroup", parent: namespace) + project = create(:project, path: "the-path0", namespace: subgroup) + + subject.rename_path_for_routable(migration_namespace(namespace)) + + expect(project.route.reload.path).to eq("the-path0/subgroup/the-path0") + end + end + end + + context 'for projects' do + let(:parent) { create(:namespace, path: 'the-parent') } + let(:project) { create(:empty_project, path: 'the-path', namespace: parent) } + + it 'renames the project called `the-path`' do + subject.rename_path_for_routable(migration_project(project)) + + expect(project.reload.path).to eq('the-path0') + end + + it 'renames the route for the project' do + subject.rename_path_for_routable(project) + + expect(project.reload.route.path).to eq('the-parent/the-path0') + end + + it 'returns the old & new path' do + old_path, new_path = subject.rename_path_for_routable(migration_project(project)) + + expect(old_path).to eq('the-parent/the-path') + expect(new_path).to eq('the-parent/the-path0') + end + end + end + + describe '#move_pages' do + it 'moves the pages directory' do + expect(subject).to receive(:move_folders) + .with(TestEnv.pages_path, 'old-path', 'new-path') + + subject.move_pages('old-path', 'new-path') + end + end + + describe "#move_uploads" do + let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') } + let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') } + + it 'moves subdirectories in the uploads folder' do + expect(subject).to receive(:uploads_dir).and_return(uploads_dir) + expect(subject).to receive(:move_folders).with(uploads_dir, 'old_path', 'new_path') + + subject.move_uploads('old_path', 'new_path') + end + + it "doesn't move uploads when they are stored in object storage" do + expect(subject).to receive(:file_storage?).and_return(false) + expect(subject).not_to receive(:move_folders) + + subject.move_uploads('old_path', 'new_path') + end + end + + describe '#move_folders' do + let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') } + let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') } + + before do + FileUtils.remove_dir(test_dir) if File.directory?(test_dir) + FileUtils.mkdir_p(uploads_dir) + allow(subject).to receive(:uploads_dir).and_return(uploads_dir) + end + + it 'moves a folder with files' do + source = File.join(uploads_dir, 'parent-group', 'sub-group') + FileUtils.mkdir_p(source) + destination = File.join(uploads_dir, 'parent-group', 'moved-group') + FileUtils.touch(File.join(source, 'test.txt')) + expected_file = File.join(destination, 'test.txt') + + subject.move_folders(uploads_dir, File.join('parent-group', 'sub-group'), File.join('parent-group', 'moved-group')) + + expect(File.exist?(expected_file)).to be(true) + end + end +end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb new file mode 100644 index 00000000000..c56fded7516 --- /dev/null +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb @@ -0,0 +1,227 @@ +require 'spec_helper' + +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do + let(:migration) { FakeRenameReservedPathMigrationV1.new } + let(:subject) { described_class.new(['the-path'], migration) } + + before do + allow(migration).to receive(:say) + end + + def migration_namespace(namespace) + Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses:: + Namespace.find(namespace.id) + end + + describe '#namespaces_for_paths' do + context 'nested namespaces' do + let(:subject) { described_class.new(['parent/the-Path'], migration) } + + it 'includes the namespace' do + parent = create(:namespace, path: 'parent') + child = create(:namespace, path: 'the-path', parent: parent) + + found_ids = subject.namespaces_for_paths(type: :child). + map(&:id) + + expect(found_ids).to contain_exactly(child.id) + end + end + + context 'for child namespaces' do + it 'only returns child namespaces with the correct path' do + _root_namespace = create(:namespace, path: 'THE-path') + _other_path = create(:namespace, + path: 'other', + parent: create(:namespace)) + namespace = create(:namespace, + path: 'the-path', + parent: create(:namespace)) + + found_ids = subject.namespaces_for_paths(type: :child). + map(&:id) + + expect(found_ids).to contain_exactly(namespace.id) + end + + it 'has no namespaces that look the same' do + _root_namespace = create(:namespace, path: 'THE-path') + _similar_path = create(:namespace, + path: 'not-really-the-path', + parent: create(:namespace)) + namespace = create(:namespace, + path: 'the-path', + parent: create(:namespace)) + + found_ids = subject.namespaces_for_paths(type: :child). + map(&:id) + + expect(found_ids).to contain_exactly(namespace.id) + end + end + + context 'for top levelnamespaces' do + it 'only returns child namespaces with the correct path' do + root_namespace = create(:namespace, path: 'the-path') + _other_path = create(:namespace, path: 'other') + _child_namespace = create(:namespace, + path: 'the-path', + parent: create(:namespace)) + + found_ids = subject.namespaces_for_paths(type: :top_level). + map(&:id) + + expect(found_ids).to contain_exactly(root_namespace.id) + end + + it 'has no namespaces that just look the same' do + root_namespace = create(:namespace, path: 'the-path') + _similar_path = create(:namespace, path: 'not-really-the-path') + _child_namespace = create(:namespace, + path: 'the-path', + parent: create(:namespace)) + + found_ids = subject.namespaces_for_paths(type: :top_level). + map(&:id) + + expect(found_ids).to contain_exactly(root_namespace.id) + end + end + end + + describe '#move_repositories' do + let(:namespace) { create(:group, name: 'hello-group') } + it 'moves a project for a namespace' do + create(:project, namespace: namespace, path: 'hello-project') + expected_path = File.join(TestEnv.repos_path, 'bye-group', 'hello-project.git') + + subject.move_repositories(namespace, 'hello-group', 'bye-group') + + expect(File.directory?(expected_path)).to be(true) + end + + it 'moves a namespace in a subdirectory correctly' do + child_namespace = create(:group, name: 'sub-group', parent: namespace) + create(:project, namespace: child_namespace, path: 'hello-project') + + expected_path = File.join(TestEnv.repos_path, 'hello-group', 'renamed-sub-group', 'hello-project.git') + + subject.move_repositories(child_namespace, 'hello-group/sub-group', 'hello-group/renamed-sub-group') + + expect(File.directory?(expected_path)).to be(true) + end + + it 'moves a parent namespace with subdirectories' do + child_namespace = create(:group, name: 'sub-group', parent: namespace) + create(:project, namespace: child_namespace, path: 'hello-project') + expected_path = File.join(TestEnv.repos_path, 'renamed-group', 'sub-group', 'hello-project.git') + + subject.move_repositories(child_namespace, 'hello-group', 'renamed-group') + + expect(File.directory?(expected_path)).to be(true) + end + end + + describe "#child_ids_for_parent" do + it "collects child ids for all levels" do + parent = create(:namespace) + first_child = create(:namespace, parent: parent) + second_child = create(:namespace, parent: parent) + third_child = create(:namespace, parent: second_child) + all_ids = [parent.id, first_child.id, second_child.id, third_child.id] + + collected_ids = subject.child_ids_for_parent(parent, ids: [parent.id]) + + expect(collected_ids).to contain_exactly(*all_ids) + end + end + + describe "#rename_namespace" do + let(:namespace) { create(:group, name: 'the-path') } + + it 'renames paths & routes for the namespace' do + expect(subject).to receive(:rename_path_for_routable). + with(namespace). + and_call_original + + subject.rename_namespace(namespace) + + expect(namespace.reload.path).to eq('the-path0') + end + + it "moves the the repository for a project in the namespace" do + create(:project, namespace: namespace, path: "the-path-project") + expected_repo = File.join(TestEnv.repos_path, "the-path0", "the-path-project.git") + + subject.rename_namespace(namespace) + + expect(File.directory?(expected_repo)).to be(true) + end + + it "moves the uploads for the namespace" do + expect(subject).to receive(:move_uploads).with("the-path", "the-path0") + + subject.rename_namespace(namespace) + end + + it "moves the pages for the namespace" do + expect(subject).to receive(:move_pages).with("the-path", "the-path0") + + subject.rename_namespace(namespace) + end + + it 'invalidates the markdown cache of related projects' do + project = create(:empty_project, namespace: namespace, path: "the-path-project") + + expect(subject).to receive(:remove_cached_html_for_projects).with([project.id]) + + subject.rename_namespace(namespace) + end + + it "doesn't rename users for other namespaces" do + expect(subject).not_to receive(:rename_user) + + subject.rename_namespace(namespace) + end + + it 'renames the username of a namespace for a user' do + user = create(:user, username: 'the-path') + + expect(subject).to receive(:rename_user).with('the-path', 'the-path0') + + subject.rename_namespace(user.namespace) + end + end + + describe '#rename_user' do + it 'renames a username' do + subject = described_class.new([], migration) + user = create(:user, username: 'broken') + + subject.rename_user('broken', 'broken0') + + expect(user.reload.username).to eq('broken0') + end + end + + describe '#rename_namespaces' do + let!(:top_level_namespace) { create(:namespace, path: 'the-path') } + let!(:child_namespace) do + create(:namespace, path: 'the-path', parent: create(:namespace)) + end + + it 'renames top level namespaces the namespace' do + expect(subject).to receive(:rename_namespace). + with(migration_namespace(top_level_namespace)) + + subject.rename_namespaces(type: :top_level) + end + + it 'renames child namespaces' do + expect(subject).to receive(:rename_namespace). + with(migration_namespace(child_namespace)) + + subject.rename_namespaces(type: :child) + end + end +end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb new file mode 100644 index 00000000000..59e8de2712d --- /dev/null +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' + +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects do + let(:migration) { FakeRenameReservedPathMigrationV1.new } + let(:subject) { described_class.new(['the-path'], migration) } + + before do + allow(migration).to receive(:say) + end + + describe '#projects_for_paths' do + it 'searches using nested paths' do + namespace = create(:namespace, path: 'hello') + project = create(:empty_project, path: 'THE-path', namespace: namespace) + + result_ids = described_class.new(['Hello/the-path'], migration). + projects_for_paths.map(&:id) + + expect(result_ids).to contain_exactly(project.id) + end + + it 'includes the correct projects' do + project = create(:empty_project, path: 'THE-path') + _other_project = create(:empty_project) + + result_ids = subject.projects_for_paths.map(&:id) + + expect(result_ids).to contain_exactly(project.id) + end + end + + describe '#rename_projects' do + let!(:projects) { create_list(:empty_project, 2, path: 'the-path') } + + it 'renames each project' do + expect(subject).to receive(:rename_project).twice + + subject.rename_projects + end + + it 'invalidates the markdown cache of related projects' do + expect(subject).to receive(:remove_cached_html_for_projects). + with(projects.map(&:id)) + + subject.rename_projects + end + end + + describe '#rename_project' do + let(:project) do + create(:empty_project, + path: 'the-path', + namespace: create(:namespace, path: 'known-parent' )) + end + + it 'renames path & route for the project' do + expect(subject).to receive(:rename_path_for_routable). + with(project). + and_call_original + + subject.rename_project(project) + + expect(project.reload.path).to eq('the-path0') + end + + it 'moves the wiki & the repo' do + expect(subject).to receive(:move_repository). + with(project, 'known-parent/the-path.wiki', 'known-parent/the-path0.wiki') + expect(subject).to receive(:move_repository). + with(project, 'known-parent/the-path', 'known-parent/the-path0') + + subject.rename_project(project) + end + + it 'moves uploads' do + expect(subject).to receive(:move_uploads). + with('known-parent/the-path', 'known-parent/the-path0') + + subject.rename_project(project) + end + + it 'moves pages' do + expect(subject).to receive(:move_pages). + with('known-parent/the-path', 'known-parent/the-path0') + + subject.rename_project(project) + end + end + + describe '#move_repository' do + let(:known_parent) { create(:namespace, path: 'known-parent') } + let(:project) { create(:project, path: 'the-path', namespace: known_parent) } + + it 'moves the repository for a project' do + expected_path = File.join(TestEnv.repos_path, 'known-parent', 'new-repo.git') + + subject.move_repository(project, 'known-parent/the-path', 'known-parent/new-repo') + + expect(File.directory?(expected_path)).to be(true) + end + end +end 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 new file mode 100644 index 00000000000..f8cc1eb91ec --- /dev/null +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +shared_examples 'renames child namespaces' do |type| + it 'renames namespaces' do + rename_namespaces = double + expect(described_class::RenameNamespaces). + to receive(:new).with(['first-path', 'second-path'], subject). + and_return(rename_namespaces) + expect(rename_namespaces).to receive(:rename_namespaces). + with(type: :child) + + subject.rename_wildcard_paths(['first-path', 'second-path']) + end +end + +describe Gitlab::Database::RenameReservedPathsMigration::V1 do + let(:subject) { FakeRenameReservedPathMigrationV1.new } + + before do + allow(subject).to receive(:say) + end + + describe '#rename_child_paths' do + it_behaves_like 'renames child namespaces' + end + + describe '#rename_wildcard_paths' do + it_behaves_like 'renames child namespaces' + + it 'should rename projects' do + rename_projects = double + expect(described_class::RenameProjects). + to receive(:new).with(['the-path'], subject). + and_return(rename_projects) + + expect(rename_projects).to receive(:rename_projects) + + subject.rename_wildcard_paths(['the-path']) + end + end + + describe '#rename_root_paths' do + it 'should rename namespaces' do + rename_namespaces = double + expect(described_class::RenameNamespaces). + to receive(:new).with(['the-path'], subject). + and_return(rename_namespaces) + expect(rename_namespaces).to receive(:rename_namespaces). + with(type: :top_level) + + subject.rename_root_paths('the-path') + end + end +end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 4ce4e6e1034..9b1d66a1b1c 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -150,13 +150,13 @@ describe Gitlab::Database, lib: true do it 'returns correct value for PostgreSQL' do expect(described_class).to receive(:postgresql?).and_return(true) - expect(MigrationTest.new.true_value).to eq "'t'" + expect(described_class.true_value).to eq "'t'" end it 'returns correct value for MySQL' do expect(described_class).to receive(:postgresql?).and_return(false) - expect(MigrationTest.new.true_value).to eq 1 + expect(described_class.true_value).to eq 1 end end @@ -164,13 +164,13 @@ describe Gitlab::Database, lib: true do it 'returns correct value for PostgreSQL' do expect(described_class).to receive(:postgresql?).and_return(true) - expect(MigrationTest.new.false_value).to eq "'f'" + expect(described_class.false_value).to eq "'f'" end it 'returns correct value for MySQL' do expect(described_class).to receive(:postgresql?).and_return(false) - expect(MigrationTest.new.false_value).to eq 0 + expect(described_class.false_value).to eq 0 end end end diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb new file mode 100644 index 00000000000..2e52097a946 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb @@ -0,0 +1,60 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::GemfileLinker, lib: true do + describe '.support?' do + it 'supports Gemfile' do + expect(described_class.support?('Gemfile')).to be_truthy + end + + it 'supports gems.rb' do + expect(described_class.support?('gems.rb')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('Gemfile.lock')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { 'Gemfile' } + + 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 { Gitlab::Highlight.highlight(file_name, file_content) } + + def link(name, url) + %{<a href="#{url}" rel="noopener noreferrer" target="_blank">#{name}</a>} + end + + it 'links sources' do + expect(subject).to include(link('https://rubygems.org', 'https://rubygems.org')) + 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 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')) + end + + it 'links Git repos' do + expect(subject).to include(link('https://gitlab.example.com/gems/sprockets', 'https://gitlab.example.com/gems/sprockets')) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb new file mode 100644 index 00000000000..03d5b61d70c --- /dev/null +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker, lib: true do + describe '.link' do + it 'links using GemfileLinker' do + blob_name = 'Gemfile' + + expect(described_class::GemfileLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + end +end diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index c6bd4e81f4f..7d7d4a55e63 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -34,7 +34,7 @@ describe Gitlab::Diff::Highlight, lib: true do end it 'highlights and marks added lines' do - code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n} + code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left">RuntimeError</span></span><span class="p"><span class="idiff">,</span></span><span class="idiff right"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n} expect(subject[5].text).to eq(code) end @@ -67,7 +67,7 @@ describe Gitlab::Diff::Highlight, lib: true do end it 'marks added lines' do - code = %q{+ raise <span class='idiff left right'>RuntimeError, </span>"System commands must be given as an array of strings"} + code = %q{+ raise <span class="idiff left right">RuntimeError, </span>"System commands must be given as an array of strings"} expect(subject[5].text).to eq(code) expect(subject[5].text).to be_html_safe diff --git a/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb new file mode 100644 index 00000000000..d6e8b8ac4b2 --- /dev/null +++ b/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe Gitlab::Diff::InlineDiffMarkdownMarker, lib: true do + describe '#mark' do + let(:raw) { "abc 'def'" } + let(:inline_diffs) { [2..5] } + let(:subject) { described_class.new(raw).mark(inline_diffs, mode: :deletion) } + + it 'marks the range' do + expect(subject).to eq("ab{-c 'd-}ef'") + expect(subject).to be_html_safe + end + end +end diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb index 198ff977f24..95da344802d 100644 --- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb @@ -1,26 +1,26 @@ require 'spec_helper' describe Gitlab::Diff::InlineDiffMarker, lib: true do - describe '#inline_diffs' do + describe '#mark' do context "when the rich text is html safe" do - let(:raw) { "abc 'def'" } + let(:raw) { "abc 'def'" } let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">'def'</span>}.html_safe } let(:inline_diffs) { [2..5] } - let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw, rich).mark(inline_diffs) } + let(:subject) { described_class.new(raw, rich).mark(inline_diffs) } - it 'marks the inline diffs' do - expect(subject).to eq(%{<span class="abc">ab<span class='idiff left'>c</span></span><span class="space"><span class='idiff'> </span></span><span class="def"><span class='idiff right'>'d</span>ef'</span>}) + it 'marks the range' do + expect(subject).to eq(%{<span class="abc">ab<span class="idiff left">c</span></span><span class="space"><span class="idiff"> </span></span><span class="def"><span class="idiff right">'d</span>ef'</span>}) expect(subject).to be_html_safe end end context "when the text text is not html safe" do - let(:raw) { "abc 'def'" } + let(:raw) { "abc 'def'" } let(:inline_diffs) { [2..5] } - let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw).mark(inline_diffs) } + let(:subject) { described_class.new(raw).mark(inline_diffs) } - it 'marks the inline diffs' do - expect(subject).to eq(%{ab<span class='idiff left right'>c 'd</span>ef'}) + it 'marks the range' do + expect(subject).to eq(%{ab<span class="idiff left right">c 'd</span>ef'}) expect(subject).to be_html_safe end end diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 994995b57b8..4d202a76e1b 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -100,7 +100,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do project, current_user, start_branch: branch_name, - target_branch: branch_name, + branch_name: branch_name, commit_message: "Create file", file_path: file_name, file_content: content @@ -113,7 +113,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do project, current_user, start_branch: branch_name, - target_branch: branch_name, + branch_name: branch_name, commit_message: "Update file", file_path: file_name, file_content: content @@ -122,11 +122,11 @@ describe Gitlab::Diff::PositionTracer, lib: true do end def delete_file(branch_name, file_name) - Files::DestroyService.new( + Files::DeleteService.new( project, current_user, start_branch: branch_name, - target_branch: branch_name, + branch_name: branch_name, commit_message: "Delete file", file_path: file_name ).execute @@ -569,13 +569,8 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 1 1 BB # 2 2 A - it "returns the new position" do - expect_new_position( - old_path: file_name, - new_path: new_file_name, - old_line: old_position.new_line, - new_line: old_position.new_line - ) + it "returns nil since the line doesn't exist in the new diffs anymore" do + expect(subject).to be_nil end end @@ -1377,7 +1372,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do nil, { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, { old_path: file_name, old_line: 6 }, - { new_path: file_name, new_line: 7 }, + { new_path: file_name, new_line: 7 } ] expect_positions(old_position_attrs, new_position_attrs) @@ -1449,7 +1444,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do nil, { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, { old_path: file_name, old_line: 6 }, - { new_path: file_name, new_line: 7 }, + { new_path: file_name, new_line: 7 } ] expect_positions(old_position_attrs, new_position_attrs) @@ -1503,7 +1498,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, nil, - { new_path: file_name, new_line: 6 }, + { new_path: file_name, new_line: 6 } ] expect_positions(old_position_attrs, new_position_attrs) @@ -1751,7 +1746,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do { old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 }, { old_path: file_name, old_line: 5 }, { new_path: file_name, new_line: 6 }, - { new_path: file_name, new_line: 7 }, + { new_path: file_name, new_line: 7 } ] expect_positions(old_position_attrs, new_position_attrs) diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index b300feaabe1..3f79eaf7afb 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -143,6 +143,7 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do expect(new_note.author).to eq(sent_notification.recipient) expect(new_note.position).to eq(note.position) expect(new_note.note).to include("I could not disagree more.") + expect(new_note.in_reply_to?(note)).to be_truthy end it "adds all attachments" do diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index 2a86b427806..c6e3524f743 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -4,12 +4,38 @@ require_relative 'email_shared_blocks' describe Gitlab::Email::Receiver, lib: true do include_context :email_shared_context + context "when the email contains a valid email address in a Delivered-To header" do + let(:email_raw) { fixture_file('emails/forwarded_new_issue.eml') } + let(:handler) { double(:handler) } + + before do + stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo") + + allow(handler).to receive(:execute) + allow(handler).to receive(:metrics_params) + end + + it "finds the mail key" do + expect(Gitlab::Email::Handler).to receive(:for).with(an_instance_of(Mail::Message), 'gitlabhq/gitlabhq+auth_token').and_return(handler) + + receiver.execute + end + end + context "when we cannot find a capable handler" do let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "!!!") } - it "raises a UnknownIncomingEmail" do + it "raises an UnknownIncomingEmail error" do expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) end + + context "and the email contains no references header" do + let(:email_raw) { fixture_file("emails/auto_reply.eml").gsub(mail_key, "!!!") } + + it "raises an UnknownIncomingEmail error" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) + end + end end context "when the email is blank" do diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb index 8b5bfc4dbb0..24df04e985a 100644 --- a/spec/lib/gitlab/etag_caching/middleware_spec.rb +++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb @@ -47,9 +47,9 @@ describe Gitlab::EtagCaching::Middleware do it 'tracks "etag_caching_key_not_found" event' do expect(Gitlab::Metrics).to receive(:add_event) - .with(:etag_caching_middleware_used) + .with(:etag_caching_middleware_used, endpoint: 'issue_notes') expect(Gitlab::Metrics).to receive(:add_event) - .with(:etag_caching_key_not_found) + .with(:etag_caching_key_not_found, endpoint: 'issue_notes') middleware.call(build_env(path, if_none_match)) end @@ -91,14 +91,33 @@ describe Gitlab::EtagCaching::Middleware do expect(status).to eq 304 end + it 'returns empty body' do + _, _, body = middleware.call(build_env(path, if_none_match)) + + expect(body).to be_empty + end + it 'tracks "etag_caching_cache_hit" event' do expect(Gitlab::Metrics).to receive(:add_event) - .with(:etag_caching_middleware_used) + .with(:etag_caching_middleware_used, endpoint: 'issue_notes') expect(Gitlab::Metrics).to receive(:add_event) - .with(:etag_caching_cache_hit) + .with(:etag_caching_cache_hit, endpoint: 'issue_notes') middleware.call(build_env(path, if_none_match)) end + + context 'when polling is disabled' do + before do + allow(Gitlab::PollingInterval).to receive(:polling_enabled?). + and_return(false) + end + + it 'returns status code 429' do + status, _, _ = middleware.call(build_env(path, if_none_match)) + + expect(status).to eq 429 + end + end end context 'when If-None-Match header does not match ETag in store' do @@ -119,9 +138,9 @@ describe Gitlab::EtagCaching::Middleware do mock_app_response expect(Gitlab::Metrics).to receive(:add_event) - .with(:etag_caching_middleware_used) + .with(:etag_caching_middleware_used, endpoint: 'issue_notes') expect(Gitlab::Metrics).to receive(:add_event) - .with(:etag_caching_resource_changed) + .with(:etag_caching_resource_changed, endpoint: 'issue_notes') middleware.call(build_env(path, if_none_match)) end @@ -137,9 +156,9 @@ describe Gitlab::EtagCaching::Middleware do it 'tracks "etag_caching_header_missing" event' do expect(Gitlab::Metrics).to receive(:add_event) - .with(:etag_caching_middleware_used) + .with(:etag_caching_middleware_used, endpoint: 'issue_notes') expect(Gitlab::Metrics).to receive(:add_event) - .with(:etag_caching_header_missing) + .with(:etag_caching_header_missing, endpoint: 'issue_notes') middleware.call(build_env(path, if_none_match)) end diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb new file mode 100644 index 00000000000..5ae4a19263c --- /dev/null +++ b/spec/lib/gitlab/etag_caching/router_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe Gitlab::EtagCaching::Router do + it 'matches issue notes endpoint' do + env = build_env( + '/my-group/and-subgroup/here-comes-the-project/noteable/issue/1/notes' + ) + + result = described_class.match(env) + + expect(result).to be_present + expect(result.name).to eq 'issue_notes' + end + + it 'matches issue title endpoint' do + env = build_env( + '/my-group/my-project/issues/123/realtime_changes' + ) + + result = described_class.match(env) + + expect(result).to be_present + expect(result.name).to eq 'issue_title' + end + + it 'matches project pipelines endpoint' do + env = build_env( + '/my-group/my-project/pipelines.json' + ) + + result = described_class.match(env) + + expect(result).to be_present + expect(result.name).to eq 'project_pipelines' + end + + it 'matches commit pipelines endpoint' do + env = build_env( + '/my-group/my-project/commit/aa8260d253a53f73f6c26c734c72fdd600f6e6d4/pipelines.json' + ) + + result = described_class.match(env) + + expect(result).to be_present + expect(result.name).to eq 'commit_pipelines' + end + + it 'matches new merge request pipelines endpoint' do + env = build_env( + '/my-group/my-project/merge_requests/new.json' + ) + + result = described_class.match(env) + + expect(result).to be_present + expect(result.name).to eq 'new_merge_request_pipelines' + end + + it 'matches merge request pipelines endpoint' do + env = build_env( + '/my-group/my-project/merge_requests/234/pipelines.json' + ) + + result = described_class.match(env) + + expect(result).to be_present + expect(result.name).to eq 'merge_request_pipelines' + end + + it 'does not match blob with confusing name' do + env = build_env( + '/my-group/my-project/blob/master/pipelines.json' + ) + + result = described_class.match(env) + + expect(result).to be_blank + end + + def build_env(path) + { 'PATH_INFO' => path } + end +end diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb new file mode 100644 index 00000000000..5a32ffd462c --- /dev/null +++ b/spec/lib/gitlab/file_finder_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::FileFinder, lib: true do + describe '#find' do + let(:project) { create(:project, :public, :repository) } + let(:finder) { described_class.new(project, project.default_branch) } + + it 'finds by name' do + results = finder.find('files') + expect(results.map(&:first)).to include('files/images/wm.svg') + end + + it 'finds by content' do + results = finder.find('files') + + blob = results.select { |result| result.first == "CHANGELOG" }.flatten.last + + expect(blob.filename).to eq("CHANGELOG") + end + end +end diff --git a/spec/lib/gitlab/git/attributes_spec.rb b/spec/lib/gitlab/git/attributes_spec.rb index 9c011e34c11..1cfd8db09a5 100644 --- a/spec/lib/gitlab/git/attributes_spec.rb +++ b/spec/lib/gitlab/git/attributes_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Git::Attributes, seed_helper: true do let(:path) do - File.join(SEED_REPOSITORY_PATH, 'with-git-attributes.git') + File.join(SEED_STORAGE_PATH, 'with-git-attributes.git') end subject { described_class.new(path) } @@ -141,7 +141,7 @@ describe Gitlab::Git::Attributes, seed_helper: true do end it 'does not yield when the attributes file has an unsupported encoding' do - path = File.join(SEED_REPOSITORY_PATH, 'with-invalid-git-attributes.git') + path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git') attrs = described_class.new(path) expect { |b| attrs.each_line(&b) }.not_to yield_control diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index e169f5af6b6..8b041ac69b1 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" describe Gitlab::Git::Blame, seed_helper: true do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } let(:blame) do Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md") end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index b883526151e..e6a07a58d73 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe Gitlab::Git::Blob, seed_helper: true do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } describe 'initialize' do let(:blob) { Gitlab::Git::Blob.new(name: 'test') } @@ -234,7 +234,7 @@ describe Gitlab::Git::Blob, seed_helper: true do it { expect(blob.lfs_pointer?).to eq(true) } it { expect(blob.lfs_oid).to eq("4206f951d2691c78aac4c0ce9f2b23580b2c92cdcc4336e1028742c0274938e0") } - it { expect(blob.lfs_size).to eq("19548") } + it { expect(blob.lfs_size).to eq(19548) } it { expect(blob.id).to eq("f4d76af13003d1106be7ac8c5a2a3d37ddf32c2a") } it { expect(blob.name).to eq("image.jpg") } it { expect(blob.path).to eq("files/lfs/image.jpg") } @@ -273,7 +273,7 @@ describe Gitlab::Git::Blob, seed_helper: true do it { expect(blob.lfs_pointer?).to eq(false) } it { expect(blob.lfs_oid).to eq(nil) } - it { expect(blob.lfs_size).to eq("1575078") } + it { expect(blob.lfs_size).to eq(1575078) } it { expect(blob.id).to eq("5ae35296e1f95c1ef9feda1241477ed29a448572") } it { expect(blob.name).to eq("picture-invalid.png") } it { expect(blob.path).to eq("files/lfs/picture-invalid.png") } diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index 78234b396c5..9eac7660cd1 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -1,12 +1,57 @@ require "spec_helper" describe Gitlab::Git::Branch, seed_helper: true do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } subject { repository.branches } it { is_expected.to be_kind_of Array } + describe 'initialize' do + let(:commit_id) { 'f00' } + let(:commit_subject) { "My commit".force_encoding('ASCII-8BIT') } + let(:committer) do + Gitaly::FindLocalBranchCommitAuthor.new( + name: generate(:name), + email: generate(:email), + date: Google::Protobuf::Timestamp.new(seconds: 123) + ) + end + let(:author) do + Gitaly::FindLocalBranchCommitAuthor.new( + name: generate(:name), + email: generate(:email), + date: Google::Protobuf::Timestamp.new(seconds: 456) + ) + end + let(:gitaly_branch) do + Gitaly::FindLocalBranchResponse.new( + name: 'foo', commit_id: commit_id, commit_subject: commit_subject, + commit_author: author, commit_committer: committer + ) + end + let(:attributes) do + { + id: commit_id, + message: commit_subject, + authored_date: Time.at(author.date.seconds), + author_name: author.name, + author_email: author.email, + committed_date: Time.at(committer.date.seconds), + committer_name: committer.name, + committer_email: committer.email + } + end + let(:branch) { described_class.new(repository, 'foo', gitaly_branch) } + + it 'parses Gitaly::FindLocalBranchResponse correctly' do + expect(Gitlab::Git::Commit).to receive(:decorate). + with(hash_including(attributes)).and_call_original + + expect(branch.dereferenced_target.message.encoding).to be(Encoding::UTF_8) + end + end + describe '#size' do subject { super().size } it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) } diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 5cf4631fbfc..3e44c577643 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" describe Gitlab::Git::Commit, seed_helper: true do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) } let(:rugged_commit) do repository.rugged.lookup(SeedRepo::Commit::ID) @@ -9,7 +9,7 @@ describe Gitlab::Git::Commit, seed_helper: true do describe "Commit info" do before do - repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH).rugged @committer = { email: 'mike@smith.com', @@ -59,7 +59,7 @@ describe Gitlab::Git::Commit, seed_helper: true do after do # Erase the new commit so other tests get the original repo - repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH).rugged repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) end end @@ -95,7 +95,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end context 'with broken repo' do - let(:repository) { Gitlab::Git::Repository.new(TEST_BROKEN_REPO_PATH) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH) } it 'returns nil' do expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_nil diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb index e28debe1494..7c45071ec45 100644 --- a/spec/lib/gitlab/git/compare_spec.rb +++ b/spec/lib/gitlab/git/compare_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" describe Gitlab::Git::Compare, seed_helper: true do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, false) } let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, true) } diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 992126ef153..4189aaef643 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" describe Gitlab::Git::Diff, seed_helper: true do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } before do @raw_diff_hash = { @@ -120,7 +120,7 @@ EOT new_mode: 0100644, from_id: '357406f3075a57708d0163752905cc1576fceacc', to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0', - raw_chunks: raw_chunks, + raw_chunks: raw_chunks ) ) end diff --git a/spec/lib/gitlab/git/encoding_helper_spec.rb b/spec/lib/gitlab/git/encoding_helper_spec.rb index 83311536893..1a3bf802a07 100644 --- a/spec/lib/gitlab/git/encoding_helper_spec.rb +++ b/spec/lib/gitlab/git/encoding_helper_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" describe Gitlab::Git::EncodingHelper do let(:ext_class) { Class.new { extend Gitlab::Git::EncodingHelper } } - let(:binary_string) { File.join(SEED_REPOSITORY_PATH, 'gitlab_logo.png') } + let(:binary_string) { File.join(SEED_STORAGE_PATH, 'gitlab_logo.png') } describe '#encode!' do [ @@ -19,8 +19,8 @@ describe Gitlab::Git::EncodingHelper do [ 'removes invalid bytes from ASCII-8bit encoded multibyte string. This can occur when a git diff match line truncates in the middle of a multibyte character. This occurs after the second word in this example. The test string is as short as we can get while still triggering the error condition when not looking at `detect[:confidence]`.', "mu ns\xC3\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ".force_encoding('ASCII-8BIT'), - "mu ns\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ", - ], + "mu ns\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi " + ] ].each do |description, test_string, xpect| it description do expect(ext_class.encode!(test_string)).to eq(xpect) @@ -37,18 +37,18 @@ describe Gitlab::Git::EncodingHelper do [ "encodes valid utf8 encoded string to utf8", "λ, λ, λ".encode("UTF-8"), - "λ, λ, λ".encode("UTF-8"), + "λ, λ, λ".encode("UTF-8") ], [ "encodes valid ASCII-8BIT encoded string to utf8", "ascii only".encode("ASCII-8BIT"), - "ascii only".encode("UTF-8"), + "ascii only".encode("UTF-8") ], [ "encodes valid ISO-8859-1 encoded string to utf8", "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("ISO-8859-1", "UTF-8"), - "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("UTF-8"), - ], + "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("UTF-8") + ] ].each do |description, test_string, xpect| it description do r = ext_class.encode_utf8(test_string.force_encoding('UTF-8')) @@ -56,6 +56,10 @@ describe Gitlab::Git::EncodingHelper do expect(r.encoding.name).to eq('UTF-8') end end + + it 'returns empty string on conversion errors' do + expect { ext_class.encode_utf8('') }.not_to raise_error(ArgumentError) + end end describe '#clean' do @@ -73,8 +77,8 @@ describe Gitlab::Git::EncodingHelper do [ 'removes invalid bytes from ASCII-8bit encoded multibyte string.', "Lorem ipsum\xC3\n dolor sit amet, xy\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg".force_encoding('ASCII-8BIT'), - "Lorem ipsum\n dolor sit amet, xyà yùabcdùefg", - ], + "Lorem ipsum\n dolor sit amet, xyà yùabcdùefg" + ] ].each do |description, test_string, xpect| it description do expect(ext_class.encode!(test_string)).to eq(xpect) diff --git a/spec/lib/gitlab/git/env_spec.rb b/spec/lib/gitlab/git/env_spec.rb new file mode 100644 index 00000000000..d9df99bfe05 --- /dev/null +++ b/spec/lib/gitlab/git/env_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' + +describe Gitlab::Git::Env do + describe "#set" do + context 'with RequestStore.store disabled' do + before do + allow(RequestStore).to receive(:active?).and_return(false) + end + + it 'does not store anything' do + described_class.set(GIT_OBJECT_DIRECTORY: 'foo') + + expect(described_class.all).to be_empty + end + end + + context 'with RequestStore.store enabled' do + before do + allow(RequestStore).to receive(:active?).and_return(true) + end + + it 'whitelist some `GIT_*` variables and stores them using RequestStore' do + described_class.set( + GIT_OBJECT_DIRECTORY: 'foo', + GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar', + GIT_EXEC_PATH: 'baz', + PATH: '~/.bin:/bin') + + expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo') + expect(described_class[:GIT_ALTERNATE_OBJECT_DIRECTORIES]).to eq('bar') + expect(described_class[:GIT_EXEC_PATH]).to be_nil + expect(described_class[:bar]).to be_nil + end + end + end + + describe "#all" do + context 'with RequestStore.store enabled' do + before do + allow(RequestStore).to receive(:active?).and_return(true) + described_class.set( + GIT_OBJECT_DIRECTORY: 'foo', + GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar') + end + + it 'returns an env hash' do + expect(described_class.all).to eq({ + 'GIT_OBJECT_DIRECTORY' => 'foo', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' + }) + end + end + end + + describe "#[]" do + context 'with RequestStore.store enabled' do + before do + allow(RequestStore).to receive(:active?).and_return(true) + end + + before do + described_class.set( + GIT_OBJECT_DIRECTORY: 'foo', + GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar') + end + + it 'returns a stored value for an existing key' do + expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo') + end + + it 'returns nil for an non-existing key' do + expect(described_class[:foo]).to be_nil + end + end + end + + describe 'thread-safety' do + context 'with RequestStore.store enabled' do + before do + allow(RequestStore).to receive(:active?).and_return(true) + described_class.set(GIT_OBJECT_DIRECTORY: 'foo') + end + + it 'is thread-safe' do + another_thread = Thread.new do + described_class.set(GIT_OBJECT_DIRECTORY: 'bar') + + Thread.stop + described_class[:GIT_OBJECT_DIRECTORY] + end + + # Ensure another_thread runs first + sleep 0.1 until another_thread.stop? + + expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo') + + another_thread.run + expect(another_thread.value).to eq('bar') + end + end + end +end diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb index d0c7ca60ddc..21b71654251 100644 --- a/spec/lib/gitlab/git/index_spec.rb +++ b/spec/lib/gitlab/git/index_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Git::Index, seed_helper: true do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } let(:index) { described_class.new(repository) } before do @@ -33,7 +33,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.create(options) }.to raise_error('Filename already exists') + expect { index.create(options) }.to raise_error('A file with this name already exists') end end @@ -89,7 +89,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.create_dir(options) }.to raise_error('Directory already exists as a file') + expect { index.create_dir(options) }.to raise_error('A file with this name already exists') end end @@ -99,7 +99,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.create_dir(options) }.to raise_error('Directory already exists') + expect { index.create_dir(options) }.to raise_error('A directory with this name already exists') end end end @@ -118,7 +118,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.update(options) }.to raise_error("File doesn't exist") + expect { index.update(options) }.to raise_error("A file with this name doesn't exist") end end @@ -156,7 +156,15 @@ describe Gitlab::Git::Index, seed_helper: true do it 'raises an error' do options[:previous_path] = 'documents/story.txt' - expect { index.move(options) }.to raise_error("File doesn't exist") + expect { index.move(options) }.to raise_error("A file with this name doesn't exist") + end + end + + context 'when a file at the new path already exists' do + it 'raises an error' do + options[:file_path] = 'CHANGELOG' + + expect { index.move(options) }.to raise_error("A file with this name already exists") end end @@ -203,7 +211,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.delete(options) }.to raise_error("File doesn't exist") + expect { index.delete(options) }.to raise_error("A file with this name doesn't exist") end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index d4b7684adfd..cb107c6d1f9 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe Gitlab::Git::Repository, seed_helper: true do include Gitlab::Git::EncodingHelper - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } describe "Respond to" do subject { repository } @@ -14,6 +14,69 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.to respond_to(:tags) } end + describe '#root_ref' do + context 'with gitaly disabled' do + before { allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) } + + it 'calls #discover_default_branch' do + expect(repository).to receive(:discover_default_branch) + repository.root_ref + end + end + + context 'with gitaly enabled' do + before { stub_gitaly } + after { Gitlab::GitalyClient.clear_stubs! } + + it 'gets the branch name from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) + repository.root_ref + end + + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name). + and_raise(GRPC::NotFound) + expect { repository.root_ref }.to raise_error(Gitlab::Git::Repository::NoRepository) + end + + it 'wraps GRPC exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name). + and_raise(GRPC::Unknown) + expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError) + end + end + end + + describe "#rugged" do + context 'with no Git env stored' do + before do + expect(Gitlab::Git::Env).to receive(:all).and_return({}) + end + + it "whitelist some variables and pass them via the alternates keyword argument" do + expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: []) + + repository.rugged + end + end + + context 'with some Git env stored' do + before do + expect(Gitlab::Git::Env).to receive(:all).and_return({ + 'GIT_OBJECT_DIRECTORY' => 'foo', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar', + 'GIT_OTHER' => 'another_env' + }) + end + + it "whitelist some variables and pass them via the alternates keyword argument" do + expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: %w[foo bar]) + + repository.rugged + end + end + end + describe "#discover_default_branch" do let(:master) { 'master' } let(:feature) { 'feature' } @@ -55,6 +118,28 @@ describe Gitlab::Git::Repository, seed_helper: true do end it { is_expected.to include("master") } it { is_expected.not_to include("branch-from-space") } + + context 'with gitaly enabled' do + before { stub_gitaly } + after { Gitlab::GitalyClient.clear_stubs! } + + it 'gets the branch names from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) + subject + end + + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names). + and_raise(GRPC::NotFound) + expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) + end + + it 'wraps GRPC other exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names). + and_raise(GRPC::Unknown) + expect { subject }.to raise_error(Gitlab::Git::CommandError) + end + end end describe '#tag_names' do @@ -71,6 +156,28 @@ describe Gitlab::Git::Repository, seed_helper: true do end it { is_expected.to include("v1.0.0") } it { is_expected.not_to include("v5.0.0") } + + context 'with gitaly enabled' do + before { stub_gitaly } + after { Gitlab::GitalyClient.clear_stubs! } + + it 'gets the tag names from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) + subject + end + + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names). + and_raise(GRPC::NotFound) + expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) + end + + it 'wraps GRPC exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names). + and_raise(GRPC::Unknown) + expect { subject }.to raise_error(Gitlab::Git::CommandError) + end + end end shared_examples 'archive check' do |extenstion| @@ -221,7 +328,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end context '#submodules' do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } context 'where repo has submodules' do let(:submodules) { repository.submodules('master') } @@ -290,9 +397,9 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#reset" do - change_path = File.join(TEST_NORMAL_REPO_PATH, "CHANGELOG") - untracked_path = File.join(TEST_NORMAL_REPO_PATH, "UNTRACKED") - tracked_path = File.join(TEST_NORMAL_REPO_PATH, "files", "ruby", "popen.rb") + change_path = File.join(SEED_STORAGE_PATH, TEST_NORMAL_REPO_PATH, "CHANGELOG") + untracked_path = File.join(SEED_STORAGE_PATH, TEST_NORMAL_REPO_PATH, "UNTRACKED") + tracked_path = File.join(SEED_STORAGE_PATH, TEST_NORMAL_REPO_PATH, "files", "ruby", "popen.rb") change_text = "New changelog text" untracked_text = "This file is untracked" @@ -311,7 +418,7 @@ describe Gitlab::Git::Repository, seed_helper: true do f.write(untracked_text) end - @normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) + @normal_repo = Gitlab::Git::Repository.new('default', TEST_NORMAL_REPO_PATH) @normal_repo.reset("HEAD", :hard) end @@ -354,7 +461,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context "-b" do before(:all) do - @normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) + @normal_repo = Gitlab::Git::Repository.new('default', TEST_NORMAL_REPO_PATH) @normal_repo.checkout(new_branch, { b: true }, "origin/feature") end @@ -382,7 +489,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context "without -b" do context "and specifying a nonexistent branch" do it "should not do anything" do - normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) + normal_repo = Gitlab::Git::Repository.new('default', TEST_NORMAL_REPO_PATH) expect { normal_repo.checkout(new_branch) }.to raise_error(Rugged::ReferenceError) expect(normal_repo.rugged.branches[new_branch]).to be_nil @@ -402,7 +509,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context "and with a valid branch" do before(:all) do - @normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) + @normal_repo = Gitlab::Git::Repository.new('default', TEST_NORMAL_REPO_PATH) @normal_repo.rugged.branches.create("feature", "origin/feature") @normal_repo.checkout("feature") end @@ -414,13 +521,13 @@ describe Gitlab::Git::Repository, seed_helper: true do end it "should update the working directory" do - File.open(File.join(TEST_NORMAL_REPO_PATH, ".gitignore"), "r") do |f| + File.open(File.join(SEED_STORAGE_PATH, TEST_NORMAL_REPO_PATH, ".gitignore"), "r") do |f| expect(f.read.each_line.to_a).not_to include(".DS_Store\n") end end after(:all) do - FileUtils.rm_rf(TEST_NORMAL_REPO_PATH) + FileUtils.rm_rf(SEED_STORAGE_PATH, TEST_NORMAL_REPO_PATH) ensure_seeds end end @@ -429,7 +536,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe "#delete_branch" do before(:all) do - @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH) @repo.delete_branch("feature") end @@ -449,7 +556,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe "#create_branch" do before(:all) do - @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH) end it "should create a new branch" do @@ -496,7 +603,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe "#remote_delete" do before(:all) do - @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH) @repo.remote_delete("expendable") end @@ -512,7 +619,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe "#remote_add" do before(:all) do - @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH) @repo.remote_add("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL) end @@ -528,7 +635,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe "#remote_update" do before(:all) do - @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH) @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH) end @@ -551,7 +658,7 @@ describe Gitlab::Git::Repository, seed_helper: true do before(:context) do # Add new commits so that there's a renamed file in the commit history - repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH).rugged commit_with_old_name = new_commit_edit_old_file(repo) rename_commit = new_commit_move_file(repo) @@ -560,7 +667,7 @@ describe Gitlab::Git::Repository, seed_helper: true do after(:context) do # Erase our commits so other tests get the original repo - repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH).rugged repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) end @@ -885,7 +992,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#autocrlf' do before(:all) do - @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH) @repo.rugged.config['core.autocrlf'] = true end @@ -900,14 +1007,14 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#autocrlf=' do before(:all) do - @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH) @repo.rugged.config['core.autocrlf'] = false end it 'should set the autocrlf option to the provided option' do @repo.autocrlf = :input - File.open(File.join(TEST_MUTABLE_REPO_PATH, '.git', 'config')) do |config_file| + File.open(File.join(SEED_STORAGE_PATH, TEST_MUTABLE_REPO_PATH, '.git', 'config')) do |config_file| expect(config_file.read).to match('autocrlf = input') end end @@ -942,12 +1049,65 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#ref_name_for_sha' do + let(:ref_path) { 'refs/heads' } + let(:sha) { repository.find_branch('master').dereferenced_target.id } + let(:ref_name) { 'refs/heads/master' } + + it 'returns the ref name for the given sha' do + expect(repository.ref_name_for_sha(ref_path, sha)).to eq(ref_name) + end + + it "returns an empty name if the ref doesn't exist" do + expect(repository.ref_name_for_sha(ref_path, "000000")).to eq("") + end + + it "raise an exception if the ref is empty" do + expect { repository.ref_name_for_sha(ref_path, "") }.to raise_error(ArgumentError) + end + + it "raise an exception if the ref is nil" do + expect { repository.ref_name_for_sha(ref_path, nil) }.to raise_error(ArgumentError) + end + end + + describe '#find_commits' do + it 'should return a return a collection of commits' do + commits = repository.find_commits + + expect(commits).not_to be_empty + expect(commits).to all( be_a_kind_of(Gitlab::Git::Commit) ) + end + + context 'while applying a sort order based on the `order` option' do + it "allows ordering topologically (no parents shown before their children)" do + expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_TOPO) + + repository.find_commits(order: :topo) + end + + it "allows ordering by date" do + expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE | Rugged::SORT_TOPO) + + repository.find_commits(order: :date) + end + + it "applies no sorting by default" do + expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_NONE) + + repository.find_commits + end + end + end + describe '#branches with deleted branch' do before(:each) do ref = double() allow(ref).to receive(:name) { 'bad-branch' } allow(ref).to receive(:target) { raise Rugged::ReferenceError } - allow(repository.rugged).to receive(:branches) { [ref] } + branches = double() + allow(branches).to receive(:each) { [ref].each } + allow(repository.rugged).to receive(:branches) { branches } end it 'should return empty branches' do @@ -956,20 +1116,8 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#branch_count' do - before(:each) do - valid_ref = double(:ref) - invalid_ref = double(:ref) - - allow(valid_ref).to receive_messages(name: 'master', target: double(:target)) - - allow(invalid_ref).to receive_messages(name: 'bad-branch') - allow(invalid_ref).to receive(:target) { raise Rugged::ReferenceError } - - allow(repository.rugged).to receive_messages(branches: [valid_ref, invalid_ref]) - end - it 'returns the number of branches' do - expect(repository.branch_count).to eq(1) + expect(repository.branch_count).to eq(9) end end @@ -999,7 +1147,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#copy_gitattributes" do - let(:attributes_path) { File.join(TEST_REPO_PATH, 'info/attributes') } + let(:attributes_path) { File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info/attributes') } it "raises an error with invalid ref" do expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef) @@ -1075,7 +1223,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#diffable' do - info_dir_path = attributes_path = File.join(TEST_REPO_PATH, 'info') + info_dir_path = attributes_path = File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info') attributes_path = File.join(info_dir_path, 'attributes') before(:all) do @@ -1143,7 +1291,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#local_branches' do before(:all) do - @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo = Gitlab::Git::Repository.new('default', File.join(TEST_MUTABLE_REPO_PATH, '.git')) end after(:all) do @@ -1158,6 +1306,29 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(@repo.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false) expect(@repo.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) end + + context 'with gitaly enabled' do + before { stub_gitaly } + after { Gitlab::GitalyClient.clear_stubs! } + + it 'gets the branches from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches). + and_return([]) + @repo.local_branches + end + + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches). + and_raise(GRPC::NotFound) + expect { @repo.local_branches }.to raise_error(Gitlab::Git::Repository::NoRepository) + end + + it 'wraps GRPC exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches). + and_raise(GRPC::Unknown) + expect { @repo.local_branches }.to raise_error(Gitlab::Git::CommandError) + end + end end def create_remote_branch(remote_name, branch_name, source_branch_name) @@ -1235,4 +1406,11 @@ describe Gitlab::Git::Repository, seed_helper: true do sha = Rugged::Commit.create(repo, options) repo.lookup(sha) end + + def stub_gitaly + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) + + stub = double(:stub) + allow(Gitaly::Ref::Stub).to receive(:new).and_return(stub) + end end diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb index d48629a296d..78894ba9409 100644 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -3,58 +3,54 @@ require 'spec_helper' describe Gitlab::Git::RevList, lib: true do let(:project) { create(:project, :repository) } - context "validations" do - described_class::ALLOWED_VARIABLES.each do |var| - context var do - it "accepts values starting with the project repo path" do - env = { var => "#{project.repository.path_to_repo}/objects" } - rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) - - expect(rev_list).to be_valid - end - - it "rejects values starting not with the project repo path" do - env = { var => "/some/other/path" } - rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) - - expect(rev_list).not_to be_valid - end - - it "rejects values containing the project repo path but not starting with it" do - env = { var => "/some/other/path/#{project.repository.path_to_repo}" } - rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) - - expect(rev_list).not_to be_valid - end - - it "ignores nil values" do - env = { var => nil } - rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) - - expect(rev_list).to be_valid - end - end - end + before do + expect(Gitlab::Git::Env).to receive(:all).and_return({ + GIT_OBJECT_DIRECTORY: 'foo', + GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar' + }) end - context "#execute" do - let(:env) { { "GIT_OBJECT_DIRECTORY" => project.repository.path_to_repo } } - let(:rev_list) { Gitlab::Git::RevList.new('oldrev', 'newrev', project: project, env: env) } - - it "calls out to `popen` without environment variables if the record is invalid" do - allow(rev_list).to receive(:valid?).and_return(false) - - expect(Open3).to receive(:popen3).with(hash_excluding(env), any_args) - - rev_list.execute + context "#new_refs" do + let(:rev_list) { Gitlab::Git::RevList.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } + + it 'calls out to `popen`' do + expect(Gitlab::Popen).to receive(:popen).with([ + Gitlab.config.git.bin_path, + "--git-dir=#{project.repository.path_to_repo}", + 'rev-list', + 'newrev', + '--not', + '--all' + ], + nil, + { + 'GIT_OBJECT_DIRECTORY' => 'foo', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' + }).and_return(["sha1\nsha2", 0]) + + expect(rev_list.new_refs).to eq(%w[sha1 sha2]) end + end - it "calls out to `popen` with environment variables if the record is valid" do - allow(rev_list).to receive(:valid?).and_return(true) - - expect(Open3).to receive(:popen3).with(hash_including(env), any_args) - - rev_list.execute + context "#missed_ref" do + let(:rev_list) { Gitlab::Git::RevList.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } + + it 'calls out to `popen`' do + expect(Gitlab::Popen).to receive(:popen).with([ + Gitlab.config.git.bin_path, + "--git-dir=#{project.repository.path_to_repo}", + 'rev-list', + '--max-count=1', + 'oldrev', + '^newrev' + ], + nil, + { + 'GIT_OBJECT_DIRECTORY' => 'foo', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' + }).and_return(["sha1\nsha2", 0]) + + expect(rev_list.missed_ref).to eq(%w[sha1 sha2]) end end end diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb index ad469e94735..67a9c974298 100644 --- a/spec/lib/gitlab/git/tag_spec.rb +++ b/spec/lib/gitlab/git/tag_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" describe Gitlab::Git::Tag, seed_helper: true do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } describe 'first tag' do let(:tag) { repository.tags.first } diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 83d2ff8f9b3..4b76a43e6b5 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" describe Gitlab::Git::Tree, seed_helper: true do context :repo do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) } it { expect(tree).to be_kind_of Array } @@ -19,6 +19,7 @@ describe Gitlab::Git::Tree, seed_helper: true 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.mode).to eq('40000') } context :subdir do let(:subdir) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files').first } diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb index bcca4d4c746..88c871855df 100644 --- a/spec/lib/gitlab/git/util_spec.rb +++ b/spec/lib/gitlab/git/util_spec.rb @@ -6,10 +6,10 @@ describe Gitlab::Git::Util do ["", 0], ["foo", 1], ["foo\n", 1], - ["foo\n\n", 2], + ["foo\n\n", 2] ].each do |string, line_count| it "counts #{line_count} lines in #{string.inspect}" do - expect(Gitlab::Git::Util.count_lines(string)).to eq(line_count) + expect(described_class.count_lines(string)).to eq(line_count) end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 48f7754bed8..25769977f24 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::GitAccess, lib: true do - let(:access) { Gitlab::GitAccess.new(actor, project, 'web', authentication_abilities: authentication_abilities) } + let(:access) { Gitlab::GitAccess.new(actor, project, 'ssh', authentication_abilities: authentication_abilities) } let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:actor) { user } @@ -183,7 +183,7 @@ describe Gitlab::GitAccess, lib: true do describe '#check_push_access!' do before { merge_into_protected_branch } - let(:unprotected_branch) { FFaker::Internet.user_name } + let(:unprotected_branch) { 'unprotected_branch' } let(:changes) do { push_new_branch: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow", @@ -211,9 +211,9 @@ describe Gitlab::GitAccess, lib: true do target_branch = project.repository.lookup('feature') source_branch = project.repository.create_file( user, - FFaker::InternetSE.login_user_name, - FFaker::HipsterIpsum.paragraph, - message: FFaker::HipsterIpsum.sentence, + 'filename', + 'This is the file content', + message: 'This is a good commit message', branch_name: unprotected_branch) rugged = project.repository.rugged author = { email: "email@example.com", time: Time.now, name: "Example Git User" } diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb index cc8daa535d6..cc8daa535d6 100644 --- a/spec/lib/git_ref_validator_spec.rb +++ b/spec/lib/gitlab/git_ref_validator_spec.rb diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb index 8eaf7aac264..36f0e6507c8 100644 --- a/spec/lib/gitlab/git_spec.rb +++ b/spec/lib/gitlab/git_spec.rb @@ -1,21 +1,8 @@ require 'spec_helper' describe Gitlab::Git, lib: true do - let(:committer_email) { FFaker::Internet.email } - - # I have to remove periods from the end of the name - # This happened when the user's name had a suffix (i.e. "Sr.") - # This seems to be what git does under the hood. For example, this commit: - # - # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?' - # - # results in this: - # - # $ git show --pretty - # ... - # Author: Foo Sr <foo@example.com> - # ... - let(:committer_name) { FFaker::Name.name.chomp("\.") } + let(:committer_email) { 'user@example.org' } + let(:committer_name) { 'John Doe' } describe 'committer_hash' do it "returns a hash containing the given email and name" do diff --git a/spec/lib/gitlab/gitaly_client/commit_spec.rb b/spec/lib/gitlab/gitaly_client/commit_spec.rb index 4684b1d1ac0..cf1bc74779e 100644 --- a/spec/lib/gitlab/gitaly_client/commit_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_spec.rb @@ -1,28 +1,24 @@ require 'spec_helper' describe Gitlab::GitalyClient::Commit do - describe '.diff_from_parent' do - let(:diff_stub) { double('Gitaly::Diff::Stub') } - let(:project) { create(:project, :repository) } - let(:repository_message) { Gitaly::Repository.new(path: project.repository.path) } - let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') } - - before do - allow(Gitaly::Diff::Stub).to receive(:new).and_return(diff_stub) - allow(diff_stub).to receive(:commit_diff).and_return([]) - end + let(:diff_stub) { double('Gitaly::Diff::Stub') } + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + let(:repository_message) { repository.gitaly_repository } + let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') } + describe '#diff_from_parent' do context 'when a commit has a parent' do it 'sends an RPC request with the parent ID as left commit' do request = Gitaly::CommitDiffRequest.new( repository: repository_message, left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660', - right_commit_id: commit.id, + right_commit_id: commit.id ) - expect(diff_stub).to receive(:commit_diff).with(request) + expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request) - described_class.diff_from_parent(commit) + described_class.new(repository).diff_from_parent(commit) end end @@ -32,17 +28,17 @@ describe Gitlab::GitalyClient::Commit do request = Gitaly::CommitDiffRequest.new( repository: repository_message, left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904', - right_commit_id: initial_commit.id, + right_commit_id: initial_commit.id ) - expect(diff_stub).to receive(:commit_diff).with(request) + expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request) - described_class.diff_from_parent(initial_commit) + described_class.new(repository).diff_from_parent(initial_commit) end end it 'returns a Gitlab::Git::DiffCollection' do - ret = described_class.diff_from_parent(commit) + ret = described_class.new(repository).diff_from_parent(commit) expect(ret).to be_kind_of(Gitlab::Git::DiffCollection) end @@ -50,9 +46,40 @@ describe Gitlab::GitalyClient::Commit do it 'passes options to Gitlab::Git::DiffCollection' do options = { max_files: 31, max_lines: 13 } - expect(Gitlab::Git::DiffCollection).to receive(:new).with([], options) + expect(Gitlab::Git::DiffCollection).to receive(:new).with(kind_of(Enumerable), options) + + described_class.new(repository).diff_from_parent(commit, options) + end + end + + describe '#commit_deltas' do + context 'when a commit has a parent' do + it 'sends an RPC request with the parent ID as left commit' do + request = Gitaly::CommitDeltaRequest.new( + repository: repository_message, + left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660', + right_commit_id: commit.id + ) + + expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request).and_return([]) - described_class.diff_from_parent(commit, options) + described_class.new(repository).commit_deltas(commit) + end + end + + context 'when a commit does not have a parent' do + it 'sends an RPC request with empty tree ref as left commit' do + initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') + request = Gitaly::CommitDeltaRequest.new( + repository: repository_message, + left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904', + right_commit_id: initial_commit.id + ) + + expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request).and_return([]) + + described_class.new(repository).commit_deltas(initial_commit) + end end end end diff --git a/spec/lib/gitlab/gitaly_client/notifications_spec.rb b/spec/lib/gitlab/gitaly_client/notifications_spec.rb index bb5d93994ad..b87dacb175b 100644 --- a/spec/lib/gitlab/gitaly_client/notifications_spec.rb +++ b/spec/lib/gitlab/gitaly_client/notifications_spec.rb @@ -2,12 +2,15 @@ require 'spec_helper' describe Gitlab::GitalyClient::Notifications do describe '#post_receive' do + let(:project) { create(:empty_project) } + let(:repo_path) { project.repository.path_to_repo } + subject { described_class.new(project.repository) } + it 'sends a post_receive message' do - repo_path = create(:empty_project).repository.path_to_repo expect_any_instance_of(Gitaly::Notifications::Stub). - to receive(:post_receive).with(post_receive_request_with_repo_path(repo_path)) + to receive(:post_receive).with(gitaly_request_with_repo_path(repo_path)) - described_class.new(repo_path).post_receive + subject.post_receive end end end diff --git a/spec/lib/gitlab/gitaly_client/ref_spec.rb b/spec/lib/gitlab/gitaly_client/ref_spec.rb new file mode 100644 index 00000000000..d8cd2dcbd2a --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/ref_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::Ref do + let(:project) { create(:empty_project) } + let(:repo_path) { project.repository.path_to_repo } + let(:client) { described_class.new(project.repository) } + + before do + allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) + end + + after do + # When we say `expect_any_instance_of(Gitaly::Ref::Stub)` a double is created, + # and because GitalyClient shares stubs these will get passed from example to + # example, which will cause an error, so we clean the stubs after each example. + Gitlab::GitalyClient.clear_stubs! + end + + describe '#branch_names' do + it 'sends a find_all_branch_names message' do + expect_any_instance_of(Gitaly::Ref::Stub). + to receive(:find_all_branch_names).with(gitaly_request_with_repo_path(repo_path)). + and_return([]) + + client.branch_names + end + end + + describe '#tag_names' do + it 'sends a find_all_tag_names message' do + expect_any_instance_of(Gitaly::Ref::Stub). + to receive(:find_all_tag_names).with(gitaly_request_with_repo_path(repo_path)). + and_return([]) + + client.tag_names + end + end + + describe '#default_branch_name' do + it 'sends a find_default_branch_name message' do + expect_any_instance_of(Gitaly::Ref::Stub). + to receive(:find_default_branch_name).with(gitaly_request_with_repo_path(repo_path)). + and_return(double(name: 'foo')) + + client.default_branch_name + end + end + + describe '#local_branches' do + it 'sends a find_local_branches message' do + expect_any_instance_of(Gitaly::Ref::Stub). + to receive(:find_local_branches).with(gitaly_request_with_repo_path(repo_path)). + and_return([]) + + client.local_branches + end + + it 'parses and sends the sort parameter' do + expect_any_instance_of(Gitaly::Ref::Stub). + to receive(:find_local_branches). + with(gitaly_request_with_params(sort_by: :UPDATED_DESC)). + and_return([]) + + client.local_branches(sort_by: 'updated_desc') + end + + it 'raises an argument error if an invalid sort_by parameter is passed' do + expect { client.local_branches(sort_by: 'invalid_sort') }.to raise_error(ArgumentError) + end + end +end diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb new file mode 100644 index 00000000000..08ee0dff6b2 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient, lib: true do + describe '.stub' do + before { described_class.clear_stubs! } + + context 'when passed a UNIX socket address' do + it 'passes the address as-is to GRPC' do + address = 'unix:/tmp/gitaly.sock' + allow(Gitlab.config.repositories).to receive(:storages).and_return({ + 'default' => { 'gitaly_address' => address } + }) + + expect(Gitaly::Commit::Stub).to receive(:new).with(address, any_args) + + described_class.stub(:commit, 'default') + end + end + + context 'when passed a TCP address' do + it 'strips tcp:// prefix before passing it to GRPC::Core::Channel initializer' do + address = 'localhost:9876' + prefixed_address = "tcp://#{address}" + + allow(Gitlab.config.repositories).to receive(:storages).and_return({ + 'default' => { 'gitaly_address' => prefixed_address } + }) + + expect(Gitaly::Commit::Stub).to receive(:new).with(address, any_args) + + described_class.stub(:commit, 'default') + end + end + end +end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 8b867fbe322..9d5e20841b5 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -215,9 +215,9 @@ describe Gitlab::GithubImport::Importer, lib: true do let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:repository) { double(id: 1, fork: false) } let(:source_sha) { create(:commit, project: project).id } - let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) } + let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha, user: octocat) } let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } - let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } + let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha, user: octocat) } let(:pull_request) do double( number: 1347, diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb index f34d09f2c1d..a4089592cf2 100644 --- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -43,7 +43,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do description: "*Created by: octocat*\n\nI'm having a problem with this.", state: 'opened', author_id: project.creator_id, - assignee_id: nil, + assignee_ids: [], created_at: created_at, updated_at: updated_at } @@ -64,7 +64,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do description: "*Created by: octocat*\n\nI'm having a problem with this.", state: 'closed', author_id: project.creator_id, - assignee_id: nil, + assignee_ids: [], created_at: created_at, updated_at: updated_at } @@ -77,19 +77,19 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do let(:raw_data) { double(base_data.merge(assignee: octocat)) } it 'returns nil as assignee_id when is not a GitLab user' do - expect(issue.attributes.fetch(:assignee_id)).to be_nil + expect(issue.attributes.fetch(:assignee_ids)).to be_empty end it 'returns GitLab user id associated with GitHub id as assignee_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') - expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id + expect(issue.attributes.fetch(:assignee_ids)).to eq [gl_user.id] end it 'returns GitLab user id associated with GitHub email as assignee_id' do gl_user = create(:user, email: octocat.email) - expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id + expect(issue.attributes.fetch(:assignee_ids)).to eq [gl_user.id] end end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 44423917944..b7c59918a76 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -4,15 +4,18 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:client) { double } let(:project) { create(:project, :repository) } let(:source_sha) { create(:commit, project: project).id } - let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } + let(:target_commit) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit) } + let(:target_sha) { target_commit.id } + let(:target_short_sha) { target_commit.id.to_s[0..7] } let(:repository) { double(id: 1, fork: false) } let(:source_repo) { repository } let(:source_branch) { double(ref: 'branch-merged', repo: source_repo, sha: source_sha) } let(:forked_source_repo) { double(id: 2, fork: true, name: 'otherproject', full_name: 'company/otherproject') } let(:target_repo) { repository } - let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) } - let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } - let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } + let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha, user: octocat) } + let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) } + let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) } + let(:branch_deleted_repo) { double(ref: 'master', repo: nil, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) } let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -61,7 +64,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do author_id: project.creator_id, assignee_id: nil, created_at: created_at, - updated_at: updated_at + updated_at: updated_at, + imported: true } expect(pull_request.attributes).to eq(expected) @@ -87,7 +91,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do author_id: project.creator_id, assignee_id: nil, created_at: created_at, - updated_at: updated_at + updated_at: updated_at, + imported: true } expect(pull_request.attributes).to eq(expected) @@ -114,7 +119,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do author_id: project.creator_id, assignee_id: nil, created_at: created_at, - updated_at: updated_at + updated_at: updated_at, + imported: true } expect(pull_request.attributes).to eq(expected) @@ -203,16 +209,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do context 'when source branch does not exist' do let(:raw_data) { double(base_data.merge(head: removed_branch)) } - it 'prefixes branch name with pull request number' do - expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch' + it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do + expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/removed-branch" end end context 'when source branch is from a fork' do let(:raw_data) { double(base_data.merge(head: forked_branch)) } - it 'prefixes branch name with pull request number and project with namespace to avoid collision' do - expect(pull_request.source_branch_name).to eq 'pull/1347/company/otherproject/master' + it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do + expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/master" + end + end + + context 'when source branch is from a deleted fork' do + let(:raw_data) { double(base_data.merge(head: branch_deleted_repo)) } + + it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do + expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/master" end end end @@ -229,8 +243,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do context 'when target branch does not exist' do let(:raw_data) { double(base_data.merge(base: removed_branch)) } - it 'prefixes branch name with pull request number' do - expect(pull_request.target_branch_name).to eq 'pull/1347/removed-branch' + it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do + expect(pull_request.target_branch_name).to eq 'gl-2e5d3239/1347/octocat/removed-branch' end end end @@ -290,6 +304,14 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + context 'when source repository does not exist anymore' do + let(:raw_data) { double(base_data.merge(head: branch_deleted_repo)) } + + it 'returns true' do + expect(pull_request.cross_project?).to eq true + end + end + context 'when source and target repositories are the same' do let(:raw_data) { double(base_data.merge(head: source_branch)) } @@ -299,6 +321,14 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + describe '#source_branch_exists?' do + let(:raw_data) { double(base_data.merge(head: forked_branch)) } + + it 'returns false when is a cross_project' do + expect(pull_request.source_branch_exists?).to eq false + end + end + describe '#url' do let(:raw_data) { double(base_data) } diff --git a/spec/lib/gitlab/gl_repository_spec.rb b/spec/lib/gitlab/gl_repository_spec.rb new file mode 100644 index 00000000000..ac3558ab386 --- /dev/null +++ b/spec/lib/gitlab/gl_repository_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe ::Gitlab::GlRepository do + describe '.parse' do + set(:project) { create(:project) } + + it 'parses a project gl_repository' do + expect(described_class.parse("project-#{project.id}")).to eq([project, false]) + end + + it 'parses a wiki gl_repository' do + expect(described_class.parse("wiki-#{project.id}")).to eq([project, true]) + end + + it 'throws an argument error on an invalid gl_repository' do + expect { described_class.parse("badformat-#{project.id}") }.to raise_error(ArgumentError) + end + end +end diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb index ccaa88a5c79..622a0f513f4 100644 --- a/spec/lib/gitlab/google_code_import/importer_spec.rb +++ b/spec/lib/gitlab/google_code_import/importer_spec.rb @@ -49,7 +49,7 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do expect(issue).not_to be_nil expect(issue.iid).to eq(169) expect(issue.author).to eq(project.creator) - expect(issue.assignee).to eq(mapped_user) + expect(issue.assignees).to eq([mapped_user]) expect(issue.state).to eq("closed") expect(issue.label_names).to include("Priority: Medium") expect(issue.label_names).to include("Status: Fixed") diff --git a/spec/lib/gitlab/health_checks/db_check_spec.rb b/spec/lib/gitlab/health_checks/db_check_spec.rb new file mode 100644 index 00000000000..33c6c24449c --- /dev/null +++ b/spec/lib/gitlab/health_checks/db_check_spec.rb @@ -0,0 +1,6 @@ +require 'spec_helper' +require_relative './simple_check_shared' + +describe Gitlab::HealthChecks::DbCheck do + include_examples 'simple_check', 'db_ping', 'Db', '1' +end diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb new file mode 100644 index 00000000000..45ccd3d6459 --- /dev/null +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -0,0 +1,127 @@ +require 'spec_helper' + +describe Gitlab::HealthChecks::FsShardsCheck do + let(:metric_class) { Gitlab::HealthChecks::Metric } + let(:result_class) { Gitlab::HealthChecks::Result } + let(:repository_storages) { [:default] } + let(:tmp_dir) { Dir.mktmpdir } + + let(:storages_paths) do + { + default: { path: tmp_dir } + }.with_indifferent_access + end + + before do + allow(described_class).to receive(:repository_storages) { repository_storages } + allow(described_class).to receive(:storages_paths) { storages_paths } + end + + after do + FileUtils.remove_entry_secure(tmp_dir) if Dir.exist?(tmp_dir) + end + + shared_examples 'filesystem checks' do + describe '#readiness' do + subject { described_class.readiness } + + context 'storage points to not existing folder' do + let(:storages_paths) do + { + default: { path: 'tmp/this/path/doesnt/exist' } + }.with_indifferent_access + end + + it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) } + end + + context 'storage points to directory that has both read and write rights' do + before do + FileUtils.chmod_R(0755, tmp_dir) + end + + it { is_expected.to include(result_class.new(true, nil, shard: :default)) } + + it 'cleans up files used for testing' do + expect(described_class).to receive(:storage_write_test).with(any_args).and_call_original + + subject + + expect(Dir.entries(tmp_dir).count).to eq(2) + end + + context 'read test fails' do + before do + allow(described_class).to receive(:storage_read_test).with(any_args).and_return(false) + end + + it { is_expected.to include(result_class.new(false, 'cannot read from storage', shard: :default)) } + end + + context 'write test fails' do + before do + allow(described_class).to receive(:storage_write_test).with(any_args).and_return(false) + end + + it { is_expected.to include(result_class.new(false, 'cannot write to storage', shard: :default)) } + end + end + end + + describe '#metrics' do + subject { described_class.metrics } + + context 'storage points to not existing folder' do + let(:storages_paths) do + { + default: { path: 'tmp/this/path/doesnt/exist' } + }.with_indifferent_access + end + + it { is_expected.to include(metric_class.new(:filesystem_accessible, 0, shard: :default)) } + it { is_expected.to include(metric_class.new(:filesystem_readable, 0, shard: :default)) } + it { is_expected.to include(metric_class.new(:filesystem_writable, 0, shard: :default)) } + + it { is_expected.to include(have_attributes(name: :filesystem_access_latency, value: be >= 0, labels: { shard: :default })) } + it { is_expected.to include(have_attributes(name: :filesystem_read_latency, value: be >= 0, labels: { shard: :default })) } + it { is_expected.to include(have_attributes(name: :filesystem_write_latency, value: be >= 0, labels: { shard: :default })) } + end + + context 'storage points to directory that has both read and write rights' do + before do + FileUtils.chmod_R(0755, tmp_dir) + end + + it { is_expected.to include(metric_class.new(:filesystem_accessible, 1, shard: :default)) } + it { is_expected.to include(metric_class.new(:filesystem_readable, 1, shard: :default)) } + it { is_expected.to include(metric_class.new(:filesystem_writable, 1, shard: :default)) } + + it { is_expected.to include(have_attributes(name: :filesystem_access_latency, value: be >= 0, labels: { shard: :default })) } + it { is_expected.to include(have_attributes(name: :filesystem_read_latency, value: be >= 0, labels: { shard: :default })) } + it { is_expected.to include(have_attributes(name: :filesystem_write_latency, value: be >= 0, labels: { shard: :default })) } + end + end + end + + context 'when popen always finds required binaries' do + before do + allow(Gitlab::Popen).to receive(:popen).and_wrap_original do |method, *args, &block| + begin + method.call(*args, &block) + rescue RuntimeError + raise 'expected not to happen' + end + end + end + + it_behaves_like 'filesystem checks' + end + + context 'when popen never finds required binaries' do + before do + allow(Gitlab::Popen).to receive(:popen).and_raise(Errno::ENOENT) + end + + it_behaves_like 'filesystem checks' + end +end diff --git a/spec/lib/gitlab/health_checks/redis_check_spec.rb b/spec/lib/gitlab/health_checks/redis_check_spec.rb new file mode 100644 index 00000000000..734cdcb893e --- /dev/null +++ b/spec/lib/gitlab/health_checks/redis_check_spec.rb @@ -0,0 +1,6 @@ +require 'spec_helper' +require_relative './simple_check_shared' + +describe Gitlab::HealthChecks::RedisCheck do + include_examples 'simple_check', 'redis_ping', 'Redis', 'PONG' +end diff --git a/spec/lib/gitlab/health_checks/simple_check_shared.rb b/spec/lib/gitlab/health_checks/simple_check_shared.rb new file mode 100644 index 00000000000..3f871d66034 --- /dev/null +++ b/spec/lib/gitlab/health_checks/simple_check_shared.rb @@ -0,0 +1,66 @@ +shared_context 'simple_check' do |metrics_prefix, check_name, success_result| + describe '#metrics' do + subject { described_class.metrics } + context 'Check is passing' do + before do + allow(described_class).to receive(:check).and_return success_result + end + + it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_success", value: 1)) } + it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_timeout", value: 0)) } + it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_latency", value: be >= 0)) } + end + + context 'Check is misbehaving' do + before do + allow(described_class).to receive(:check).and_return 'error!' + end + + it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_success", value: 0)) } + it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_timeout", value: 0)) } + it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_latency", value: be >= 0)) } + end + + context 'Check is timeouting' do + before do + allow(described_class).to receive(:check).and_return Timeout::Error.new + end + + it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_success", value: 0)) } + it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_timeout", value: 1)) } + it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_latency", value: be >= 0)) } + end + end + + describe '#readiness' do + subject { described_class.readiness } + context 'Check returns ok' do + before do + allow(described_class).to receive(:check).and_return success_result + end + + it { is_expected.to have_attributes(success: true) } + end + + context 'Check is misbehaving' do + before do + allow(described_class).to receive(:check).and_return 'error!' + end + + it { is_expected.to have_attributes(success: false, message: "unexpected #{check_name} check result: error!") } + end + + context 'Check is timeouting' do + before do + allow(described_class).to receive(:check ).and_return Timeout::Error.new + end + + it { is_expected.to have_attributes(success: false, message: "#{check_name} check timed out") } + end + end + + describe '#liveness' do + subject { described_class.readiness } + it { is_expected.to eq(Gitlab::HealthChecks::Result.new(true)) } + end +end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index e49799ad105..e57b3053871 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -57,4 +57,15 @@ describe Gitlab::Highlight, lib: true do end end end + + describe '#highlight' do + subject { described_class.highlight(file_name, file_content, nowrap: false) } + + it 'links dependencies via DependencyLinker' do + expect(Gitlab::DependencyLinker).to receive(:link). + with('file.name', 'Contents', anything).and_call_original + + described_class.highlight('file.name', 'Contents') + end + end end diff --git a/spec/lib/gitlab/i18n_spec.rb b/spec/lib/gitlab/i18n_spec.rb new file mode 100644 index 00000000000..52f2614d5ca --- /dev/null +++ b/spec/lib/gitlab/i18n_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +module Gitlab + describe I18n, lib: true do + let(:user) { create(:user, preferred_language: 'es') } + + describe '.set_locale' do + it 'sets the locale based on current user preferred language' do + Gitlab::I18n.set_locale(user) + + expect(FastGettext.locale).to eq('es') + expect(::I18n.locale).to eq(:es) + end + end + + describe '.reset_locale' do + it 'resets the locale to the default language' do + Gitlab::I18n.set_locale(user) + + Gitlab::I18n.reset_locale + + expect(FastGettext.locale).to eq('en') + expect(::I18n.locale).to eq(:en) + 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 002cffd3062..34f617e23a5 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -3,12 +3,13 @@ issues: - subscriptions - award_emoji - author -- assignee +- assignees - updated_by - milestone - notes - label_links - labels +- last_edited_by - todos - user_agent_detail - moved_to @@ -16,6 +17,7 @@ issues: - merge_requests_closing_issues - metrics - timelogs +- issue_assignees events: - author - project @@ -26,6 +28,7 @@ notes: - noteable - author - updated_by +- last_edited_by - resolved_by - todos - events @@ -71,6 +74,7 @@ merge_requests: - notes - label_links - labels +- last_edited_by - todos - target_project - source_project @@ -81,6 +85,7 @@ merge_requests: - merge_requests_closing_issues - metrics - timelogs +- head_pipeline merge_request_diff: - merge_request pipelines: @@ -89,16 +94,34 @@ pipelines: - statuses - builds - trigger_requests +- auto_canceled_by +- auto_canceled_pipelines +- auto_canceled_jobs +- pending_builds +- retryable_builds +- cancelable_statuses +- manual_actions +- artifacts +- pipeline_schedule +- merge_requests statuses: - project - pipeline - user +- auto_canceled_by variables: - project triggers: - project - trigger_requests - owner +pipeline_schedules: +- project +- owner +- pipelines +- last_pipeline +pipeline_schedule: +- pipelines deploy_keys: - user - deploy_keys_projects @@ -112,10 +135,18 @@ protected_branches: - project - merge_access_levels - push_access_levels +protected_tags: +- project +- create_access_levels merge_access_levels: - protected_branch push_access_levels: - protected_branch +create_access_levels: +- protected_tag +container_repositories: +- project +- name project: - taggings - base_tags @@ -143,6 +174,7 @@ project: - asana_service - gemnasium_service - slack_service +- microsoft_teams_service - mattermost_service - buildkite_service - bamboo_service @@ -156,6 +188,8 @@ project: - external_wiki_service - kubernetes_service - mock_ci_service +- mock_deployment_service +- mock_monitoring_service - forked_project_link - forked_from_project - forked_project_links @@ -170,6 +204,7 @@ project: - snippets - hooks - protected_branches +- protected_tags - project_members - users - requesters @@ -190,8 +225,10 @@ project: - builds - runner_projects - runners +- active_runners - variables - triggers +- pipeline_schedules - environments - deployments - project_feature @@ -199,7 +236,9 @@ project: - authorized_users - project_authorizations - route +- redirect_routes - statistics +- container_repositories - uploads award_emoji: - awardable diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb new file mode 100644 index 00000000000..42f3fc59f04 --- /dev/null +++ b/spec/lib/gitlab/import_export/fork_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe 'forked project import', services: true do + let(:user) { create(:user) } + let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') } + let!(:project) { create(:empty_project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } + let(:forked_from_project) { create(:project) } + let(:fork_link) { create(:forked_project_link, forked_from_project: project_with_repo) } + let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } + let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } + + let(:repo_restorer) do + Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: bundle_path, shared: shared, project: project) + end + + let!(:merge_request) do + create(:merge_request, source_project: fork_link.forked_to_project, target_project: project_with_repo) + end + + let(:saver) do + Gitlab::ImportExport::ProjectTreeSaver.new(project: project_with_repo, current_user: user, shared: shared) + end + + let(:restorer) do + Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project) + end + + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + + saver.save + repo_saver.save + + repo_restorer.restore + restorer.restore + end + + after do + FileUtils.rm_rf(export_path) + FileUtils.rm_rf(project_with_repo.repository.path_to_repo) + FileUtils.rm_rf(project.repository.path_to_repo) + end + + it 'can access the MR' do + expect(project.merge_requests.first.ensure_ref_fetched.first).to include('refs/merge-requests/1/head') + end +end diff --git a/spec/lib/gitlab/import_export/hash_util_spec.rb b/spec/lib/gitlab/import_export/hash_util_spec.rb new file mode 100644 index 00000000000..1c3a0b23ece --- /dev/null +++ b/spec/lib/gitlab/import_export/hash_util_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::HashUtil, lib: true do + let(:stringified_array) { [{ 'test' => 1 }] } + let(:stringified_array_with_date) { [{ 'test_date' => '2016-04-06 06:17:44 +0200' }] } + + describe '.deep_symbolize_array!' do + it 'symbolizes keys' do + expect { described_class.deep_symbolize_array!(stringified_array) }.to change { + stringified_array.first.keys.first + }.from('test').to(:test) + end + end + + describe '.deep_symbolize_array_with_date!' do + it 'symbolizes keys' do + expect { described_class.deep_symbolize_array_with_date!(stringified_array_with_date) }.to change { + stringified_array_with_date.first.keys.first + }.from('test_date').to(:test_date) + end + + it 'transforms date strings into Time objects' do + expect { described_class.deep_symbolize_array_with_date!(stringified_array_with_date) }.to change { + stringified_array_with_date.first.values.first.class + }.from(String).to(ActiveSupport::TimeWithZone) + end + end +end diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb new file mode 100644 index 00000000000..349be4596b6 --- /dev/null +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::MergeRequestParser do + let(:user) { create(:user) } + let!(:project) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') } + let(:forked_from_project) { create(:project) } + let(:fork_link) { create(:forked_project_link, forked_from_project: project) } + + let!(:merge_request) do + create(:merge_request, source_project: fork_link.forked_to_project, target_project: project) + end + + let(:parsed_merge_request) do + described_class.new(project, + merge_request.diff_head_sha, + merge_request, + merge_request.as_json).parse! + end + + after do + FileUtils.rm_rf(project.repository.path_to_repo) + end + + it 'has a source branch' do + expect(project.repository.branch_exists?(parsed_merge_request.source_branch)).to be true + end + + it 'has a target branch' do + expect(project.repository.branch_exists?(parsed_merge_request.target_branch)).to be true + end +end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index d9b67426818..e3599d6fe59 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -2,6 +2,7 @@ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", "visibility_level": 10, "archived": false, + "description_html": "description", "labels": [ { "id": 2, @@ -6981,28 +6982,6 @@ ], "services": [ { - "id": 164, - "title": null, - "project_id": 5, - "created_at": "2016-06-14T15:02:07.372Z", - "updated_at": "2016-06-14T15:02:07.372Z", - "active": false, - "properties": { - - }, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "build_events": true, - "category": "issue_tracker", - "type": "CustomIssueTrackerService", - "default": true, - "wiki_page_events": true - }, - { "id": 100, "title": "JetBrains TeamCity CI", "project_id": 5, @@ -7018,7 +6997,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "TeamcityService", "category": "ci", "default": false, "wiki_page_events": true @@ -7040,6 +7020,7 @@ "tag_push_events": true, "note_events": true, "pipeline_events": true, + "type": "SlackService", "category": "common", "default": false, "wiki_page_events": true @@ -7060,7 +7041,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "RedmineService", "category": "issue_tracker", "default": false, "wiki_page_events": true @@ -7081,7 +7063,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "PushoverService", "category": "common", "default": false, "wiki_page_events": true @@ -7102,7 +7085,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "PivotalTrackerService", "category": "common", "default": false, "wiki_page_events": true @@ -7124,7 +7108,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "JiraService", "category": "issue_tracker", "default": false, "wiki_page_events": true @@ -7145,7 +7130,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "IrkerService", "category": "common", "default": false, "wiki_page_events": true @@ -7167,6 +7153,7 @@ "tag_push_events": true, "note_events": true, "pipeline_events": true, + "type": "HipchatService", "category": "common", "default": false, "wiki_page_events": true @@ -7187,7 +7174,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "GemnasiumService", "category": "common", "default": false, "wiki_page_events": true @@ -7208,7 +7196,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "FlowdockService", "category": "common", "default": false, "wiki_page_events": true @@ -7229,7 +7218,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "ExternalWikiService", "category": "common", "default": false, "wiki_page_events": true @@ -7250,7 +7240,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "EmailsOnPushService", "category": "common", "default": false, "wiki_page_events": true @@ -7271,7 +7262,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "DroneCiService", "category": "ci", "default": false, "wiki_page_events": true @@ -7292,7 +7284,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "CustomIssueTrackerService", "category": "issue_tracker", "default": false, "wiki_page_events": true @@ -7313,7 +7306,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "CampfireService", "category": "common", "default": false, "wiki_page_events": true @@ -7334,7 +7328,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "BuildkiteService", "category": "ci", "default": false, "wiki_page_events": true @@ -7355,7 +7350,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "BambooService", "category": "ci", "default": false, "wiki_page_events": true @@ -7376,7 +7372,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "AssemblaService", "category": "common", "default": false, "wiki_page_events": true @@ -7397,7 +7394,8 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, + "type": "AssemblaService", "category": "common", "default": false, "wiki_page_events": true @@ -7418,7 +7416,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "category": "common", "default": false, "wiki_page_events": true, @@ -7455,6 +7453,24 @@ ] } ], + "protected_tags": [ + { + "id": 1, + "project_id": 9, + "name": "v*", + "created_at": "2017-04-04T13:48:13.426Z", + "updated_at": "2017-04-04T13:48:13.426Z", + "create_access_levels": [ + { + "id": 1, + "protected_tag_id": 1, + "access_level": 40, + "created_at": "2017-04-04T13:48:13.458Z", + "updated_at": "2017-04-04T13:48:13.458Z" + } + ] + } + ], "project_feature": { "builds_access_level": 0, "created_at": "2014-12-26T09:26:45.000Z", 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 c36f12dbd82..14338515892 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -30,6 +30,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED) end + it 'has the project html description' do + expect(Project.find_by_path('project').description_html).to eq('description') + end + it 'has the same label associated to two issues' do expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2) end @@ -64,6 +68,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(ProtectedBranch.first.push_access_levels).not_to be_empty end + it 'contains the create access levels on a protected tag' do + expect(ProtectedTag.first.create_access_levels).not_to be_empty + end + context 'event at forth level of the tree' do let(:event) { Event.where(title: 'test levels').first } @@ -82,6 +90,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(9) end + it 'has the correct time for merge request st_commits' do + st_commits = MergeRequestDiff.where.not(st_commits: nil).first.st_commits + + expect(st_commits.first[:committed_date]).to be_kind_of(Time) + end + it 'has labels associated to label links, associated to issues' do expect(Label.first.label_links.first.target).not_to be_nil end 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 012c22ec5ad..5aeb29b7fec 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:user) { create(:user) } - let(:project) { setup_project } + let!(:project) { setup_project } before do project.team << [user, :master] @@ -79,6 +79,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['merge_requests'].first['merge_request_diff']).not_to be_empty end + it 'has merge requests diff st_diffs' do + expect(saved_project_json['merge_requests'].first['merge_request_diff']['utf8_st_diffs']).not_to be_nil + end + it 'has merge requests comments' do expect(saved_project_json['merge_requests'].first['notes']).not_to be_empty end @@ -185,11 +189,21 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do end end end + + context 'project attributes' do + it 'contains the html description' do + expect(saved_project_json).to include("description_html" => 'description') + end + + it 'does not contain the runners token' do + expect(saved_project_json).not_to include("runners_token" => 'token') + end + end end end def setup_project - issue = create(:issue, assignee: user) + issue = create(:issue, assignees: [user]) snippet = create(:project_snippet) release = create(:release) group = create(:group) @@ -205,6 +219,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do releases: [release], group: group ) + project.update_column(:description_html, 'description') project_label = create(:label, project: project) group_label = create(:group_label, group: group) create(:label_link, label: project_label, target: issue) diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb index 48d74b07e27..d700af142be 100644 --- a/spec/lib/gitlab/import_export/reader_spec.rb +++ b/spec/lib/gitlab/import_export/reader_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::ImportExport::Reader, lib: true do let(:test_config) { 'spec/support/import_export/import_export.yml' } let(:project_tree_hash) do { - only: [:name, :path], + except: [:id, :created_at], include: [:issues, :labels, { merge_requests: { only: [:id], diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb index 57e412b0cef..5417c7534ea 100644 --- a/spec/lib/gitlab/import_export/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do relation_hash: relation_hash, members_mapper: members_mapper, user: user, - project_id: project.id) + project: project) end context 'hook object' do @@ -33,7 +33,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do 'tag_push_events' => false, 'note_events' => true, 'enable_ssl_verification' => true, - 'build_events' => false, + 'job_events' => false, 'wiki_page_events' => true, 'token' => token } @@ -60,7 +60,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do end context 'original service exists' do - let(:service_id) { Service.create(project: project).id } + let(:service_id) { create(:service, project: project).id } it 'does not have the original service_id' do expect(created_object.service_id).not_to eq(service_id) @@ -95,7 +95,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do 'random_id' => 99, 'milestone_id' => 99, 'project_id' => 99, - 'user_id' => 99, + 'user_id' => 99 } end diff --git a/spec/lib/gitlab/import_export/repo_bundler_spec.rb b/spec/lib/gitlab/import_export/repo_saver_spec.rb index a7f4e11271e..a7f4e11271e 100644 --- a/spec/lib/gitlab/import_export/repo_bundler_spec.rb +++ b/spec/lib/gitlab/import_export/repo_saver_spec.rb diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 1ad16a9b57d..c22fba11225 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -23,6 +23,8 @@ Issue: - weight - time_estimate - relative_position +- last_edited_at +- last_edited_by_id Event: - id - target_type @@ -154,6 +156,9 @@ MergeRequest: - approvals_before_merge - rebase_commit_sha - time_estimate +- last_edited_at +- last_edited_by_id +- head_pipeline_id MergeRequestDiff: - id - state @@ -183,6 +188,8 @@ Ci::Pipeline: - duration - user_id - lock_version +- auto_canceled_by_id +- pipeline_schedule_id CommitStatus: - id - project_id @@ -223,6 +230,8 @@ CommitStatus: - token - lock_version - coverage_regex +- auto_canceled_by_id +- retried Ci::Variable: - id - project_id @@ -240,6 +249,20 @@ Ci::Trigger: - updated_at - owner_id - description +- ref +Ci::PipelineSchedule: +- id +- description +- ref +- cron +- cron_timezone +- next_run_at +- project_id +- owner_id +- active +- deleted_at +- created_at +- updated_at DeployKey: - id - user_id @@ -269,7 +292,7 @@ Service: - tag_push_events - note_events - pipeline_events -- build_events +- job_events - category - default - wiki_page_events @@ -289,17 +312,24 @@ ProjectHook: - note_events - pipeline_events - enable_ssl_verification -- build_events +- job_events - wiki_page_events - token - group_id - confidential_issues_events +- repository_update_events ProtectedBranch: - id - project_id - name - created_at - updated_at +ProtectedTag: +- id +- project_id +- name +- created_at +- updated_at Project: - description - issues_enabled @@ -308,6 +338,29 @@ Project: - snippets_enabled - visibility_level - archived +- created_at +- updated_at +- last_activity_at +- star_count +- ci_id +- shared_runners_enabled +- build_coverage_regex +- build_allow_git_fetchs +- build_timeout +- pending_delete +- public_builds +- last_repository_check_failed +- last_repository_check_at +- container_registry_enabled +- only_allow_merge_if_pipeline_succeeds +- has_external_issue_tracker +- request_access_enabled +- has_external_wiki +- only_allow_merge_if_all_discussions_are_resolved +- auto_cancel_pending_pipelines +- printing_merge_request_link_enabled +- build_allow_git_fetch +- last_repository_updated_at Author: - name ProjectFeature: @@ -333,6 +386,14 @@ ProtectedBranch::PushAccessLevel: - access_level - created_at - updated_at +ProtectedTag::CreateAccessLevel: +- id +- protected_tag_id +- access_level +- created_at +- updated_at +- user_id +- group_id AwardEmoji: - id - user_id diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb index 071e5fac3f0..071e5fac3f0 100644 --- a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb +++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb diff --git a/spec/lib/gitlab/issuable_sorter_spec.rb b/spec/lib/gitlab/issuable_sorter_spec.rb new file mode 100644 index 00000000000..c9a434b2bcf --- /dev/null +++ b/spec/lib/gitlab/issuable_sorter_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Gitlab::IssuableSorter, lib: true do + let(:namespace1) { build(:namespace, id: 1) } + let(:project1) { build(:project, id: 1, namespace: namespace1) } + + let(:project2) { build(:project, id: 2, path: "a", namespace: project1.namespace) } + let(:project3) { build(:project, id: 3, path: "b", namespace: project1.namespace) } + + let(:namespace2) { build(:namespace, id: 2, path: "a") } + let(:namespace3) { build(:namespace, id: 3, path: "b") } + let(:project4) { build(:project, id: 4, path: "a", namespace: namespace2) } + let(:project5) { build(:project, id: 5, path: "b", namespace: namespace2) } + let(:project6) { build(:project, id: 6, path: "a", namespace: namespace3) } + + let(:unsorted) { [sorted[2], sorted[3], sorted[0], sorted[1]] } + + let(:sorted) do + [build(:issue, iid: 1, project: project1), + build(:issue, iid: 2, project: project1), + build(:issue, iid: 10, project: project1), + build(:issue, iid: 20, project: project1)] + end + + it 'sorts references by a given key' do + expect(described_class.sort(project1, unsorted)).to eq(sorted) + end + + context 'for JIRA issues' do + let(:sorted) do + [ExternalIssue.new('JIRA-1', project1), + ExternalIssue.new('JIRA-2', project1), + ExternalIssue.new('JIRA-10', project1), + ExternalIssue.new('JIRA-20', project1)] + end + + it 'sorts references by a given key' do + expect(described_class.sort(project1, unsorted)).to eq(sorted) + end + end + + context 'for references from multiple projects and namespaces' do + let(:sorted) do + [build(:issue, iid: 1, project: project1), + build(:issue, iid: 2, project: project1), + build(:issue, iid: 10, project: project1), + build(:issue, iid: 1, project: project2), + build(:issue, iid: 1, project: project3), + build(:issue, iid: 1, project: project4), + build(:issue, iid: 1, project: project5), + build(:issue, iid: 1, project: project6)] + end + let(:unsorted) do + [sorted[3], sorted[1], sorted[4], sorted[2], + sorted[6], sorted[5], sorted[0], sorted[7]] + end + + it 'sorts references by project and then by a given key' do + expect(subject.sort(project1, unsorted)).to eq(sorted) + end + end +end diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index 9a556cde5d5..087c4d8c92c 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::LDAP::Person do it 'uses the configured name attribute and handles values as an array' do name = 'John Doe' entry['cn'] = [name] - person = Gitlab::LDAP::Person.new(entry, 'ldapmain') + person = described_class.new(entry, 'ldapmain') expect(person.name).to eq(name) end @@ -30,7 +30,7 @@ describe Gitlab::LDAP::Person do it 'returns the value of mail, if present' do mail = 'john@example.com' entry['mail'] = mail - person = Gitlab::LDAP::Person.new(entry, 'ldapmain') + person = described_class.new(entry, 'ldapmain') expect(person.email).to eq([mail]) end @@ -38,7 +38,7 @@ describe Gitlab::LDAP::Person do it 'returns the value of userPrincipalName, if mail and email are not present' do user_principal_name = 'john.doe@example.com' entry['userPrincipalName'] = user_principal_name - person = Gitlab::LDAP::Person.new(entry, 'ldapmain') + person = described_class.new(entry, 'ldapmain') expect(person.email).to eq([user_principal_name]) end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 346cf0d117c..f4aab429931 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -108,6 +108,31 @@ describe Gitlab::LDAP::User, lib: true do it "creates a new user if not found" do expect{ ldap_user.save }.to change{ User.count }.by(1) end + + context 'when signup is disabled' do + before do + stub_application_setting signup_enabled: false + end + + it 'creates the user' do + ldap_user.save + + expect(gl_user).to be_persisted + end + end + + context 'when user confirmation email is enabled' do + before do + stub_application_setting send_user_confirmation_email: true + end + + it 'creates and confirms the user anyway' do + ldap_user.save + + expect(gl_user).to be_persisted + expect(gl_user).to be_confirmed + end + end end describe 'updating email' do diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index ab6e311b1e8..208a8d028cd 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::Metrics do expect(pool).to receive(:with).and_yield(connection) expect(connection).to receive(:write_points).with(an_instance_of(Array)) - expect(Gitlab::Metrics).to receive(:pool).and_return(pool) + expect(described_class).to receive(:pool).and_return(pool) described_class.submit_metrics([{ 'series' => 'kittens', 'tags' => {} }]) end @@ -64,7 +64,7 @@ describe Gitlab::Metrics do describe '.measure' do context 'without a transaction' do it 'returns the return value of the block' do - val = Gitlab::Metrics.measure(:foo) { 10 } + val = described_class.measure(:foo) { 10 } expect(val).to eq(10) end @@ -74,7 +74,7 @@ describe Gitlab::Metrics do let(:transaction) { Gitlab::Metrics::Transaction.new } before do - allow(Gitlab::Metrics).to receive(:current_transaction). + allow(described_class).to receive(:current_transaction). and_return(transaction) end @@ -88,11 +88,11 @@ describe Gitlab::Metrics do expect(transaction).to receive(:increment). with('foo_call_count', 1) - Gitlab::Metrics.measure(:foo) { 10 } + described_class.measure(:foo) { 10 } end it 'returns the return value of the block' do - val = Gitlab::Metrics.measure(:foo) { 10 } + val = described_class.measure(:foo) { 10 } expect(val).to eq(10) end @@ -105,7 +105,7 @@ describe Gitlab::Metrics do expect_any_instance_of(Gitlab::Metrics::Transaction). not_to receive(:add_tag) - Gitlab::Metrics.tag_transaction(:foo, 'bar') + described_class.tag_transaction(:foo, 'bar') end end @@ -113,13 +113,13 @@ describe Gitlab::Metrics do let(:transaction) { Gitlab::Metrics::Transaction.new } it 'adds the tag to the transaction' do - expect(Gitlab::Metrics).to receive(:current_transaction). + expect(described_class).to receive(:current_transaction). and_return(transaction) expect(transaction).to receive(:add_tag). with(:foo, 'bar') - Gitlab::Metrics.tag_transaction(:foo, 'bar') + described_class.tag_transaction(:foo, 'bar') end end end @@ -130,7 +130,7 @@ describe Gitlab::Metrics do expect_any_instance_of(Gitlab::Metrics::Transaction). not_to receive(:action=) - Gitlab::Metrics.action = 'foo' + described_class.action = 'foo' end end @@ -138,12 +138,12 @@ describe Gitlab::Metrics do it 'sets the action of a transaction' do trans = Gitlab::Metrics::Transaction.new - expect(Gitlab::Metrics).to receive(:current_transaction). + expect(described_class).to receive(:current_transaction). and_return(trans) expect(trans).to receive(:action=).with('foo') - Gitlab::Metrics.action = 'foo' + described_class.action = 'foo' end end end @@ -160,7 +160,7 @@ describe Gitlab::Metrics do expect_any_instance_of(Gitlab::Metrics::Transaction). not_to receive(:add_event) - Gitlab::Metrics.add_event(:meow) + described_class.add_event(:meow) end end @@ -170,10 +170,10 @@ describe Gitlab::Metrics do expect(transaction).to receive(:add_event).with(:meow) - expect(Gitlab::Metrics).to receive(:current_transaction). + expect(described_class).to receive(:current_transaction). and_return(transaction) - Gitlab::Metrics.add_event(:meow) + described_class.add_event(:meow) end end end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 6c84a4c8b73..828c953197d 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -40,6 +40,44 @@ describe Gitlab::OAuth::User, lib: true do let(:provider) { 'twitter' } describe 'signup' do + context 'when signup is disabled' do + before do + stub_application_setting signup_enabled: false + end + + it 'creates the user' do + stub_omniauth_config(allow_single_sign_on: ['twitter']) + + oauth_user.save + + expect(gl_user).to be_persisted + end + end + + context 'when user confirmation email is enabled' do + before do + stub_application_setting send_user_confirmation_email: true + end + + it 'creates and confirms the user anyway' do + stub_omniauth_config(allow_single_sign_on: ['twitter']) + + oauth_user.save + + expect(gl_user).to be_persisted + expect(gl_user).to be_confirmed + end + end + + it 'marks user as having password_automatically_set' do + stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter']) + + oauth_user.save + + expect(gl_user).to be_persisted + expect(gl_user).to be_password_automatically_set + end + shared_examples 'to verify compliance with allow_single_sign_on' do context 'provider is marked as external' do it 'marks user as external' do diff --git a/spec/lib/gitlab/other_markup.rb b/spec/lib/gitlab/other_markup_spec.rb index 8f5a353b381..c0f5fa9dc1f 100644 --- a/spec/lib/gitlab/other_markup.rb +++ b/spec/lib/gitlab/other_markup_spec.rb @@ -1,17 +1,19 @@ require 'spec_helper' describe Gitlab::OtherMarkup, lib: true do + let(:context) { {} } + context "XSS Checks" do links = { 'links' => { file: 'file.rdoc', input: 'XSS[JaVaScriPt:alert(1)]', - output: '<p><a>XSS</a></p>' + output: "\n" + '<p><a>XSS</a></p>' + "\n" } } links.each do |name, data| it "does not convert dangerous #{name} into HTML" do - expect(render(data[:file], data[:input], context)).to eql data[:output] + expect(render(data[:file], data[:input], context)).to eq(data[:output]) end end end diff --git a/spec/lib/gitlab/polling_interval_spec.rb b/spec/lib/gitlab/polling_interval_spec.rb new file mode 100644 index 00000000000..5ea8ecb1c30 --- /dev/null +++ b/spec/lib/gitlab/polling_interval_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Gitlab::PollingInterval, lib: true do + let(:polling_interval) { described_class } + + describe '.set_header' do + let(:headers) { {} } + let(:response) { double(headers: headers) } + + context 'when polling is disabled' do + before do + stub_application_setting(polling_interval_multiplier: 0) + end + + it 'sets value to -1' do + polling_interval.set_header(response, interval: 10_000) + + expect(headers['Poll-Interval']).to eq('-1') + end + end + + context 'when polling is enabled' do + before do + stub_application_setting(polling_interval_multiplier: 0.33333) + end + + it 'applies modifier to base interval' do + polling_interval.set_header(response, interval: 10_000) + + expect(headers['Poll-Interval']).to eq('3333') + end + end + end +end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 9a8096208db..1b8690ba613 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -22,11 +22,40 @@ describe Gitlab::ProjectSearchResults, lib: true do end describe 'blob search' do - let(:project) { create(:project, :repository) } - let(:results) { described_class.new(user, project, 'files').objects('blobs') } + let(:project) { create(:project, :public, :repository) } + + subject(:results) { described_class.new(user, project, 'files').objects('blobs') } + + context 'when repository is disabled' do + let(:project) { create(:project, :public, :repository, :repository_disabled) } + + it 'hides blobs from members' do + project.add_reporter(user) + + is_expected.to be_empty + end + + it 'hides blobs from non-members' do + is_expected.to be_empty + end + end + + context 'when repository is internal' do + let(:project) { create(:project, :public, :repository, :repository_private) } + + it 'finds blobs for members' do + project.add_reporter(user) + + is_expected.not_to be_empty + end + + it 'hides blobs from non-members' do + is_expected.to be_empty + end + end it 'finds by name' do - expect(results).to include(["files/images/wm.svg", nil]) + expect(results.map(&:first)).to include('files/images/wm.svg') end it 'finds by content' do @@ -41,8 +70,10 @@ describe Gitlab::ProjectSearchResults, lib: true do subject { described_class.parse_search_result(search_result) } - it "returns a valid OpenStruct object" do - is_expected.to be_an OpenStruct + it "returns a valid FoundBlob" do + is_expected.to be_an Gitlab::SearchResults::FoundBlob + expect(subject.id).to be_nil + expect(subject.path).to eq('CHANGELOG') expect(subject.filename).to eq('CHANGELOG') expect(subject.basename).to eq('CHANGELOG') expect(subject.ref).to eq('master') @@ -53,6 +84,7 @@ describe Gitlab::ProjectSearchResults, lib: true do context "when filename has extension" do let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" } + it { expect(subject.path).to eq('CONTRIBUTE.md') } it { expect(subject.filename).to eq('CONTRIBUTE.md') } it { expect(subject.basename).to eq('CONTRIBUTE') } end @@ -60,12 +92,53 @@ describe Gitlab::ProjectSearchResults, lib: true do context "when file under directory" do let(:search_result) { "master:a/b/c.md:5:a b c\n" } + it { expect(subject.path).to eq('a/b/c.md') } it { expect(subject.filename).to eq('a/b/c.md') } it { expect(subject.basename).to eq('a/b/c') } end end end + describe 'wiki search' do + let(:project) { create(:project, :public) } + let(:wiki) { build(:project_wiki, project: project) } + let!(:wiki_page) { wiki.create_page('Title', 'Content') } + + subject(:results) { described_class.new(user, project, 'Content').objects('wiki_blobs') } + + context 'when wiki is disabled' do + let(:project) { create(:project, :public, :wiki_disabled) } + + it 'hides wiki blobs from members' do + project.add_reporter(user) + + is_expected.to be_empty + end + + it 'hides wiki blobs from non-members' do + is_expected.to be_empty + end + end + + context 'when wiki is internal' do + let(:project) { create(:project, :public, :wiki_private) } + + it 'finds wiki blobs for members' do + project.add_reporter(user) + + is_expected.not_to be_empty + end + + it 'hides wiki blobs from non-members' do + is_expected.to be_empty + end + end + + it 'finds by content' do + expect(results).to include("master:Title.md:1:Content\n") + end + end + it 'does not list issues on private projects' do issue = create(:issue, project: project) @@ -75,7 +148,6 @@ describe Gitlab::ProjectSearchResults, lib: true do end describe 'confidential issues' do - let(:project) { create(:empty_project) } let(:query) { 'issue' } let(:author) { create(:user) } let(:assignee) { create(:user) } @@ -85,7 +157,7 @@ describe Gitlab::ProjectSearchResults, lib: true do let(:project) { create(:empty_project, :internal) } let!(:issue) { create(:issue, project: project, title: 'Issue 1') } let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } - let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) } + let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) } it 'does not list project confidential issues for non project members' do results = described_class.new(non_member, project, query) @@ -273,6 +345,7 @@ describe Gitlab::ProjectSearchResults, lib: true do context 'by commit hash' do let(:project) { create(:project, :public, :repository) } let(:commit) { project.repository.commit('0b4bc9a') } + commit_hashes = { short: '0b4bc9a', full: '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } commit_hashes.each do |type, commit_hash| diff --git a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb new file mode 100644 index 00000000000..d957dd932c4 --- /dev/null +++ b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Prometheus::Queries::DeploymentQuery, lib: true do + let(:environment) { create(:environment, slug: 'environment-slug') } + let(:deployment) { create(:deployment, environment: environment) } + + let(:client) { double('prometheus_client') } + subject { described_class.new(client) } + + around do |example| + time_without_subsecond_values = Time.local(2008, 9, 1, 12, 0, 0) + Timecop.freeze(time_without_subsecond_values) { example.run } + end + + it 'sends appropriate queries to prometheus' do + start_time = (deployment.created_at - 30.minutes).to_f + stop_time = (deployment.created_at + 30.minutes).to_f + created_at = deployment.created_at.to_f + + expect(client).to receive(:query_range).with('avg(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}) / 2^20', + start: start_time, stop: stop_time) + expect(client).to receive(:query).with('avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}[30m]))', + time: created_at) + expect(client).to receive(:query).with('avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}[30m]))', + time: stop_time) + + expect(client).to receive(:query_range).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[2m])) * 100', + start: start_time, stop: stop_time) + expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100', + time: created_at) + expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100', + time: stop_time) + + expect(subject.query(deployment.id)).to eq(memory_values: nil, memory_before: nil, memory_after: nil, + cpu_values: nil, cpu_before: nil, cpu_after: nil) + end +end diff --git a/spec/lib/gitlab/prometheus_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb index 280264188e2..2d8bd2f6b97 100644 --- a/spec/lib/gitlab/prometheus_spec.rb +++ b/spec/lib/gitlab/prometheus_client_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Prometheus, lib: true do +describe Gitlab::PrometheusClient, lib: true do include PrometheusHelpers subject { described_class.new(api_url: 'https://prometheus.example.com') } @@ -49,9 +49,43 @@ describe Gitlab::Prometheus, lib: true do end end + describe 'failure to reach a provided prometheus url' do + let(:prometheus_url) {"https://prometheus.invalid.example.com"} + + context 'exceptions are raised' do + it 'raises a Gitlab::PrometheusError error when a SocketError is rescued' do + req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError) + + expect { subject.send(:get, prometheus_url) } + .to raise_error(Gitlab::PrometheusError, "Can't connect to #{prometheus_url}") + expect(req_stub).to have_been_requested + end + + it 'raises a Gitlab::PrometheusError error when a SSLError is rescued' do + req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError) + + expect { subject.send(:get, prometheus_url) } + .to raise_error(Gitlab::PrometheusError, "#{prometheus_url} contains invalid SSL data") + expect(req_stub).to have_been_requested + end + + it 'raises a Gitlab::PrometheusError error when a HTTParty::Error is rescued' do + req_stub = stub_prometheus_request_with_exception(prometheus_url, HTTParty::Error) + + expect { subject.send(:get, prometheus_url) } + .to raise_error(Gitlab::PrometheusError, "Network connection error") + expect(req_stub).to have_been_requested + end + end + end + describe '#query' do let(:prometheus_query) { prometheus_cpu_query('env-slug') } - let(:query_url) { prometheus_query_url(prometheus_query) } + let(:query_url) { prometheus_query_with_time_url(prometheus_query, Time.now.utc) } + + around do |example| + Timecop.freeze { example.run } + end context 'when request returns vector results' do it 'returns data from the API call' do @@ -93,6 +127,20 @@ describe Gitlab::Prometheus, lib: true do Timecop.freeze { example.run } end + context 'when non utc time is passed' do + let(:time_stop) { Time.now.in_time_zone("Warsaw") } + let(:time_start) { time_stop - 8.hours } + + let(:query_url) { prometheus_query_range_url(prometheus_query, start: time_start.utc.to_f, stop: time_stop.utc.to_f) } + + it 'passed dates are properly converted to utc' do + req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector')) + + subject.query_range(prometheus_query, start: time_start, stop: time_stop) + expect(req_stub).to have_been_requested + end + end + context 'when a start time is passed' do let(:query_url) { prometheus_query_range_url(prometheus_query, start: 2.hours.ago) } diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index ba45e2d758c..72e947f2cc2 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -32,12 +32,6 @@ describe Gitlab::Regex, lib: true do it { is_expected.to match('foo@bar') } end - describe '.file_path_regex' do - subject { described_class.file_path_regex } - - it { is_expected.to match('foo@/bar') } - end - describe '.environment_slug_regex' do subject { described_class.environment_slug_regex } @@ -51,8 +45,8 @@ describe Gitlab::Regex, lib: true do it { is_expected.not_to match('foo-') } end - describe 'FULL_NAMESPACE_REGEX_STR' do - subject { %r{\A#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}\z} } + describe '.full_namespace_regex' do + subject { described_class.full_namespace_regex } it { is_expected.to match('gitlab.org') } it { is_expected.to match('gitlab.org/gitlab-git') } diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb index 0fb5d7646f2..f9025397107 100644 --- a/spec/lib/gitlab/repo_path_spec.rb +++ b/spec/lib/gitlab/repo_path_spec.rb @@ -1,11 +1,35 @@ require 'spec_helper' describe ::Gitlab::RepoPath do + describe '.parse' do + set(:project) { create(:project) } + + it 'parses a full repository path' do + expect(described_class.parse(project.repository.path)).to eq([project, false]) + end + + it 'parses a full wiki path' do + expect(described_class.parse(project.wiki.repository.path)).to eq([project, true]) + end + + it 'parses a relative repository path' do + expect(described_class.parse(project.full_path + '.git')).to eq([project, false]) + end + + it 'parses a relative wiki path' do + expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, true]) + end + + it 'parses a relative path starting with /' do + expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, false]) + end + end + describe '.strip_storage_path' do before do allow(Gitlab.config.repositories).to receive(:storages).and_return({ 'storage1' => { 'path' => '/foo' }, - 'storage2' => { 'path' => '/bar' }, + 'storage2' => { 'path' => '/bar' } }) end diff --git a/spec/lib/gitlab/request_profiler_spec.rb b/spec/lib/gitlab/request_profiler_spec.rb new file mode 100644 index 00000000000..ae9c06ebb7d --- /dev/null +++ b/spec/lib/gitlab/request_profiler_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::RequestProfiler, lib: true do + describe '.profile_token' do + it 'returns a token' do + expect(described_class.profile_token).to be_present + end + + it 'caches the token' do + expect(Rails.cache).to receive(:fetch).with('profile-token') + + described_class.profile_token + end + end + + describe '.remove_all_profiles' do + it 'removes Gitlab::RequestProfiler::PROFILES_DIR directory' do + dir = described_class::PROFILES_DIR + FileUtils.mkdir_p(dir) + + expect(Dir.exist?(dir)).to be true + + described_class.remove_all_profiles + expect(Dir.exist?(dir)).to be false + end + end +end diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index 4f6ef3c10fc..b106d156b75 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -211,6 +211,31 @@ describe Gitlab::Saml::User, lib: true do end end end + + context 'when signup is disabled' do + before do + stub_application_setting signup_enabled: false + end + + it 'creates the user' do + saml_user.save + + expect(gl_user).to be_persisted + end + end + + context 'when user confirmation email is enabled' do + before do + stub_application_setting send_user_confirmation_email: true + end + + it 'creates and confirms the user anyway' do + saml_user.save + + expect(gl_user).to be_persisted + expect(gl_user).to be_confirmed + end + end end describe 'blocking' do diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 847fb977400..31c3cd4d53c 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -72,9 +72,9 @@ describe Gitlab::SearchResults do let(:admin) { create(:admin) } let!(:issue) { create(:issue, project: project_1, title: 'Issue 1') } let!(:security_issue_1) { create(:issue, :confidential, project: project_1, title: 'Security issue 1', author: author) } - let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project_1, assignee: assignee) } + let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project_1, assignees: [assignee]) } let!(:security_issue_3) { create(:issue, :confidential, project: project_2, title: 'Security issue 3', author: author) } - let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignee: assignee) } + let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignees: [assignee]) } let!(:security_issue_5) { create(:issue, :confidential, project: project_4, title: 'Security issue 5') } it 'does not list confidential issues for non project members' do diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 4b08a02ec73..a97a0f8452b 100644 --- a/spec/lib/gitlab/backend/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -69,6 +69,15 @@ describe Gitlab::Shell, lib: true do expect(io).to have_received(:puts).with("key-42\tssh-rsa foo") end + it 'handles multiple spaces in the key' do + io = spy(:io) + adder = described_class.new(io) + + adder.add_key('key-42', "ssh-rsa foo") + + expect(io).to have_received(:puts).with("key-42\tssh-rsa foo") + 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") @@ -82,4 +91,45 @@ describe Gitlab::Shell, lib: true do end end end + + describe 'projects commands' do + let(:projects_path) { 'tmp/tests/shell-projects-test/bin/gitlab-projects' } + + before do + allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-projects-test') + allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) + end + + describe '#fetch_remote' do + it 'returns true when the command succeeds' do + expect(Gitlab::Popen).to receive(:popen) + .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800']).and_return([nil, 0]) + + expect(gitlab_shell.fetch_remote('current/storage', 'project/path', 'new/storage')).to be true + end + + it 'raises an exception when the command fails' do + expect(Gitlab::Popen).to receive(:popen) + .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800']).and_return(["error", 1]) + + expect { gitlab_shell.fetch_remote('current/storage', 'project/path', 'new/storage') }.to raise_error(Gitlab::Shell::Error, "error") + end + end + + describe '#import_repository' do + it 'returns true when the command succeeds' do + expect(Gitlab::Popen).to receive(:popen) + .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"]).and_return([nil, 0]) + + expect(gitlab_shell.import_repository('current/storage', 'project/path', 'https://gitlab.com/gitlab-org/gitlab-ce.git')).to be true + end + + it 'raises an exception when the command fails' do + expect(Gitlab::Popen).to receive(:popen) + .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"]).and_return(["error", 1]) + + expect { gitlab_shell.import_repository('current/storage', 'project/path', 'https://gitlab.com/gitlab-org/gitlab-ce.git') }.to raise_error(Gitlab::Shell::Error, "error") + end + end + end end diff --git a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb index 287bf62d9bd..6307f8c16a3 100644 --- a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb +++ b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::SidekiqStatus::ClientMiddleware do describe '#call' do it 'tracks the job in Redis' do - expect(Gitlab::SidekiqStatus).to receive(:set).with('123') + expect(Gitlab::SidekiqStatus).to receive(:set).with('123', Gitlab::SidekiqStatus::DEFAULT_EXPIRATION) described_class.new. call('Foo', { 'jid' => '123' }, double(:queue), double(:pool)) { nil } diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb index 56f06b61afb..496e50fbae4 100644 --- a/spec/lib/gitlab/sidekiq_status_spec.rb +++ b/spec/lib/gitlab/sidekiq_status_spec.rb @@ -73,4 +73,17 @@ describe Gitlab::SidekiqStatus do expect(key).to include('123') end end + + describe 'completed', :redis do + it 'returns the completed job' do + expect(described_class.completed_jids(%w(123))).to eq(['123']) + end + + it 'returns only the jobs completed' do + described_class.set('123') + described_class.set('456') + + expect(described_class.completed_jids(%w(123 456 789))).to eq(['789']) + end + end end diff --git a/spec/lib/gitlab/sidekiq_throttler_spec.rb b/spec/lib/gitlab/sidekiq_throttler_spec.rb index ff32e0e699d..6374ac80207 100644 --- a/spec/lib/gitlab/sidekiq_throttler_spec.rb +++ b/spec/lib/gitlab/sidekiq_throttler_spec.rb @@ -13,14 +13,14 @@ describe Gitlab::SidekiqThrottler do describe '#execute!' do it 'sets limits on the selected queues' do - Gitlab::SidekiqThrottler.execute! + described_class.execute! expect(Sidekiq::Queue['build'].limit).to eq 4 expect(Sidekiq::Queue['project_cache'].limit).to eq 4 end it 'does not set limits on other queues' do - Gitlab::SidekiqThrottler.execute! + described_class.execute! expect(Sidekiq::Queue['merge'].limit).to be_nil end diff --git a/spec/lib/gitlab/slash_commands/command_definition_spec.rb b/spec/lib/gitlab/slash_commands/command_definition_spec.rb index c9c2f314e57..5b9173d3d3f 100644 --- a/spec/lib/gitlab/slash_commands/command_definition_spec.rb +++ b/spec/lib/gitlab/slash_commands/command_definition_spec.rb @@ -167,6 +167,58 @@ describe Gitlab::SlashCommands::CommandDefinition do end end end + + context 'when the command defines parse_params block' do + before do + subject.parse_params_block = ->(raw) { raw.strip } + subject.action_block = ->(parsed) { self.received_arg = parsed } + end + + it 'executes the command passing the parsed param' do + subject.execute(context, {}, 'something ') + + expect(context.received_arg).to eq('something') + end + end + end + end + end + + describe '#explain' do + context 'when the command is not available' do + before do + subject.condition_block = proc { false } + subject.explanation = 'Explanation' + end + + it 'returns nil' do + result = subject.explain({}, {}, nil) + + expect(result).to be_nil + end + end + + context 'when the explanation is a static string' do + before do + subject.explanation = 'Explanation' + end + + it 'returns this static string' do + result = subject.explain({}, {}, nil) + + expect(result).to eq 'Explanation' + end + end + + context 'when the explanation is dynamic' do + before do + subject.explanation = proc { |arg| "Dynamic #{arg}" } + end + + it 'invokes the proc' do + result = subject.explain({}, {}, 'explanation') + + expect(result).to eq 'Dynamic explanation' end end end diff --git a/spec/lib/gitlab/slash_commands/dsl_spec.rb b/spec/lib/gitlab/slash_commands/dsl_spec.rb index 26217a0e3b2..33b49a5ddf9 100644 --- a/spec/lib/gitlab/slash_commands/dsl_spec.rb +++ b/spec/lib/gitlab/slash_commands/dsl_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::SlashCommands::Dsl do before :all do DummyClass = Struct.new(:project) do - include Gitlab::SlashCommands::Dsl + include Gitlab::SlashCommands::Dsl # rubocop:disable RSpec/DescribedClass desc 'A command with no args' command :no_args, :none do @@ -11,67 +11,99 @@ describe Gitlab::SlashCommands::Dsl do end params 'The first argument' - command :one_arg, :once, :first do |arg1| - arg1 + explanation 'Static explanation' + command :explanation_with_aliases, :once, :first do |arg| + arg end desc do "A dynamic description for #{noteable.upcase}" end params 'The first argument', 'The second argument' - command :two_args do |arg1, arg2| - [arg1, arg2] + command :dynamic_description do |args| + args.split end command :cc + explanation do |arg| + "Action does something with #{arg}" + end condition do project == 'foo' end command :cond_action do |arg| arg end + + parse_params do |raw_arg| + raw_arg.strip + end + command :with_params_parsing do |parsed| + parsed + end end end describe '.command_definitions' do it 'returns an array with commands definitions' do - no_args_def, one_arg_def, two_args_def, cc_def, cond_action_def = DummyClass.command_definitions + no_args_def, explanation_with_aliases_def, dynamic_description_def, + cc_def, cond_action_def, with_params_parsing_def = + DummyClass.command_definitions expect(no_args_def.name).to eq(:no_args) expect(no_args_def.aliases).to eq([:none]) expect(no_args_def.description).to eq('A command with no args') + 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.action_block).to be_a_kind_of(Proc) + expect(no_args_def.parse_params_block).to be_nil - expect(one_arg_def.name).to eq(:one_arg) - expect(one_arg_def.aliases).to eq([:once, :first]) - expect(one_arg_def.description).to eq('') - expect(one_arg_def.params).to eq(['The first argument']) - expect(one_arg_def.condition_block).to be_nil - expect(one_arg_def.action_block).to be_a_kind_of(Proc) + expect(explanation_with_aliases_def.name).to eq(:explanation_with_aliases) + expect(explanation_with_aliases_def.aliases).to eq([:once, :first]) + expect(explanation_with_aliases_def.description).to eq('') + 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.action_block).to be_a_kind_of(Proc) + expect(explanation_with_aliases_def.parse_params_block).to be_nil - expect(two_args_def.name).to eq(:two_args) - expect(two_args_def.aliases).to eq([]) - expect(two_args_def.to_h(noteable: "issue")[:description]).to eq('A dynamic description for ISSUE') - expect(two_args_def.params).to eq(['The first argument', 'The second argument']) - expect(two_args_def.condition_block).to be_nil - expect(two_args_def.action_block).to be_a_kind_of(Proc) + expect(dynamic_description_def.name).to eq(:dynamic_description) + expect(dynamic_description_def.aliases).to eq([]) + expect(dynamic_description_def.to_h(noteable: 'issue')[:description]).to eq('A dynamic description for ISSUE') + 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.action_block).to be_a_kind_of(Proc) + expect(dynamic_description_def.parse_params_block).to be_nil expect(cc_def.name).to eq(:cc) expect(cc_def.aliases).to eq([]) expect(cc_def.description).to eq('') + expect(cc_def.explanation).to eq('') expect(cc_def.params).to eq([]) expect(cc_def.condition_block).to be_nil expect(cc_def.action_block).to be_nil + expect(cc_def.parse_params_block).to be_nil expect(cond_action_def.name).to eq(:cond_action) expect(cond_action_def.aliases).to eq([]) expect(cond_action_def.description).to eq('') + 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.action_block).to be_a_kind_of(Proc) + expect(cond_action_def.parse_params_block).to be_nil + + expect(with_params_parsing_def.name).to eq(:with_params_parsing) + expect(with_params_parsing_def.aliases).to eq([]) + expect(with_params_parsing_def.description).to eq('') + 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.action_block).to be_a_kind_of(Proc) + expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc) end end end diff --git a/spec/lib/gitlab/string_range_marker_spec.rb b/spec/lib/gitlab/string_range_marker_spec.rb new file mode 100644 index 00000000000..7c77772b3f6 --- /dev/null +++ b/spec/lib/gitlab/string_range_marker_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Gitlab::StringRangeMarker, lib: true do + describe '#mark' do + context "when the rich text is html safe" do + let(:raw) { "abc <def>" } + let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def"><def></span>}.html_safe } + let(:inline_diffs) { [2..5] } + subject do + described_class.new(raw, rich).mark(inline_diffs) do |text, left:, right:| + "LEFT#{text}RIGHT" + end + end + + it 'marks the inline diffs' do + expect(subject).to eq(%{<span class="abc">abLEFTcRIGHT</span><span class="space">LEFT RIGHT</span><span class="def">LEFT<dRIGHTef></span>}) + expect(subject).to be_html_safe + end + end + + context "when the rich text is not html safe" do + let(:raw) { "abc <def>" } + let(:inline_diffs) { [2..5] } + subject do + described_class.new(raw).mark(inline_diffs) do |text, left:, right:| + "LEFT#{text}RIGHT" + end + end + + it 'marks the inline diffs' do + expect(subject).to eq(%{abLEFTc <dRIGHTef>}) + expect(subject).to be_html_safe + end + end + end +end diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb new file mode 100644 index 00000000000..2f5cf6c6e3b --- /dev/null +++ b/spec/lib/gitlab/string_regex_marker_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Gitlab::StringRegexMarker, lib: true do + describe '#mark' do + let(:raw) { %{"name": "AFNetworking"} } + let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe } + subject do + described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:| + %{<a href="#">#{text}</a>} + end + end + + it 'marks the inline diffs' do + expect(subject).to eq(%{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>}) + expect(subject).to be_html_safe + end + end +end diff --git a/spec/lib/gitlab/template/gitignore_template_spec.rb b/spec/lib/gitlab/template/gitignore_template_spec.rb index 9750a012e22..97797f42aaa 100644 --- a/spec/lib/gitlab/template/gitignore_template_spec.rb +++ b/spec/lib/gitlab/template/gitignore_template_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::Template::GitignoreTemplate do it 'returns the Gitignore object of a valid file' do ruby = subject.find('Ruby') - expect(ruby).to be_a Gitlab::Template::GitignoreTemplate + expect(ruby).to be_a described_class expect(ruby.name).to eq('Ruby') 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 e3b8321eda3..6541326d1de 100644 --- a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb +++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb @@ -25,7 +25,7 @@ describe Gitlab::Template::GitlabCiYmlTemplate do it 'returns the GitlabCiYml object of a valid file' do ruby = subject.find('Ruby') - expect(ruby).to be_a Gitlab::Template::GitlabCiYmlTemplate + expect(ruby).to be_a described_class expect(ruby.name).to eq('Ruby') end end diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb index 9213ced7b19..329d1d74970 100644 --- a/spec/lib/gitlab/template/issue_template_spec.rb +++ b/spec/lib/gitlab/template/issue_template_spec.rb @@ -37,7 +37,7 @@ describe Gitlab::Template::IssueTemplate do it 'returns the issue object of a valid file' do ruby = subject.find('bug', project) - expect(ruby).to be_a Gitlab::Template::IssueTemplate + expect(ruby).to be_a described_class expect(ruby.name).to eq('bug') end end diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb index 77dd3079e22..2b0056d9bab 100644 --- a/spec/lib/gitlab/template/merge_request_template_spec.rb +++ b/spec/lib/gitlab/template/merge_request_template_spec.rb @@ -37,7 +37,7 @@ describe Gitlab::Template::MergeRequestTemplate do it 'returns the merge request object of a valid file' do ruby = subject.find('bug', project) - expect(ruby).to be_a Gitlab::Template::MergeRequestTemplate + expect(ruby).to be_a described_class expect(ruby.name).to eq('bug') end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb new file mode 100644 index 00000000000..b47e1b56fa9 --- /dev/null +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe Gitlab::UsageData do + let!(:project) { create(:empty_project) } + let!(:project2) { create(:empty_project) } + let!(:board) { create(:board, project: project) } + + describe '#data' do + subject { described_class.data } + + it "gathers usage data" do + expect(subject.keys).to match_array(%i( + active_user_count + counts + recorded_at + mattermost_enabled + edition + version + uuid + hostname + )) + end + + it "gathers usage counts" do + count_data = subject[:counts] + + expect(count_data[:boards]).to eq(1) + expect(count_data[:projects]).to eq(2) + + expect(count_data.keys).to match_array(%i( + boards + ci_builds + ci_pipelines + ci_runners + ci_triggers + ci_pipeline_schedules + deploy_keys + deployments + environments + groups + issues + keys + labels + lfs_objects + merge_requests + milestones + notes + projects + projects_prometheus_active + pages_domains + protected_branches + releases + snippets + todos + uploads + web_hooks + )) + end + end + + describe '#license_usage_data' do + subject { described_class.license_usage_data } + + it "gathers license data" do + expect(subject[:uuid]).to eq(current_application_settings.uuid) + expect(subject[:version]).to eq(Gitlab::VERSION) + expect(subject[:active_user_count]).to eq(User.active.count) + expect(subject[:recorded_at]).to be_a(Time) + end + end +end diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index 369e55f61f1..0d87cf25dbb 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::UserAccess, lib: true do let(:project) { create(:project) } let(:user) { create(:user) } - describe 'can_push_to_branch?' do + describe '#can_push_to_branch?' do describe 'push to none protected branch' do it 'returns true if user is a master' do project.team << [user, :master] @@ -87,10 +87,10 @@ describe Gitlab::UserAccess, lib: true do expect(access.can_push_to_branch?(branch.name)).to be_falsey end - it 'returns true if branch does not exist and user has permission to merge' do + it 'returns false if branch does not exist' do project.team << [user, :developer] - expect(access.can_push_to_branch?(not_existing_branch.name)).to be_truthy + expect(access.can_push_to_branch?(not_existing_branch.name)).to be_falsey end end @@ -142,4 +142,117 @@ describe Gitlab::UserAccess, lib: true do end end end + + describe '#can_create_tag?' do + describe 'push to none protected tag' do + it 'returns true if user is a master' do + project.add_user(user, :master) + + expect(access.can_create_tag?('random_tag')).to be_truthy + end + + it 'returns true if user is a developer' do + project.add_user(user, :developer) + + expect(access.can_create_tag?('random_tag')).to be_truthy + end + + it 'returns false if user is a reporter' do + project.add_user(user, :reporter) + + expect(access.can_create_tag?('random_tag')).to be_falsey + end + end + + describe 'push to protected tag' do + let(:tag) { create(:protected_tag, project: project, name: "test") } + let(:not_existing_tag) { create :protected_tag, project: project } + + it 'returns true if user is a master' do + project.add_user(user, :master) + + expect(access.can_create_tag?(tag.name)).to be_truthy + end + + it 'returns false if user is a developer' do + project.add_user(user, :developer) + + expect(access.can_create_tag?(tag.name)).to be_falsey + end + + it 'returns false if user is a reporter' do + project.add_user(user, :reporter) + + expect(access.can_create_tag?(tag.name)).to be_falsey + end + end + + describe 'push to protected tag if allowed for developers' do + before do + @tag = create(:protected_tag, :developers_can_create, project: project) + end + + it 'returns true if user is a master' do + project.add_user(user, :master) + + expect(access.can_create_tag?(@tag.name)).to be_truthy + end + + it 'returns true if user is a developer' do + project.add_user(user, :developer) + + expect(access.can_create_tag?(@tag.name)).to be_truthy + end + + it 'returns false if user is a reporter' do + project.add_user(user, :reporter) + + expect(access.can_create_tag?(@tag.name)).to be_falsey + end + end + end + + describe '#can_delete_branch?' do + describe 'delete unprotected branch' do + it 'returns true if user is a master' do + project.add_user(user, :master) + + expect(access.can_delete_branch?('random_branch')).to be_truthy + end + + it 'returns true if user is a developer' do + project.add_user(user, :developer) + + expect(access.can_delete_branch?('random_branch')).to be_truthy + end + + it 'returns false if user is a reporter' do + project.add_user(user, :reporter) + + expect(access.can_delete_branch?('random_branch')).to be_falsey + end + end + + describe 'delete protected branch' do + let(:branch) { create(:protected_branch, project: project, name: "test") } + + it 'returns true if user is a master' do + project.add_user(user, :master) + + expect(access.can_delete_branch?(branch.name)).to be_truthy + end + + it 'returns false if user is a developer' do + project.add_user(user, :developer) + + expect(access.can_delete_branch?(branch.name)).to be_falsey + end + + it 'returns false if user is a reporter' do + project.add_user(user, :reporter) + + expect(access.can_delete_branch?(branch.name)).to be_falsey + end + end + end end diff --git a/spec/lib/gitlab/user_activities_spec.rb b/spec/lib/gitlab/user_activities_spec.rb new file mode 100644 index 00000000000..187d88c8c58 --- /dev/null +++ b/spec/lib/gitlab/user_activities_spec.rb @@ -0,0 +1,127 @@ +require 'spec_helper' + +describe Gitlab::UserActivities, :redis, lib: true do + let(:now) { Time.now } + + describe '.record' do + context 'with no time given' do + it 'uses Time.now and records an activity in Redis' do + Timecop.freeze do + now # eager-load now + described_class.record(42) + end + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]]) + end + end + end + + context 'with a time given' do + it 'uses the given time and records an activity in Redis' do + described_class.record(42, now) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]]) + end + end + end + end + + describe '.delete' do + context 'with a single key' do + context 'and key exists' do + it 'removes the pair from Redis' do + described_class.record(42, now) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]]) + end + + subject.delete(42) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) + end + end + end + + context 'and key does not exist' do + it 'removes the pair from Redis' do + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) + end + + subject.delete(42) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) + end + end + end + end + + context 'with multiple keys' do + context 'and all keys exist' do + it 'removes the pair from Redis' do + described_class.record(41, now) + described_class.record(42, now) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['41', now.to_i.to_s], ['42', now.to_i.to_s]]]) + end + + subject.delete(41, 42) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) + end + end + end + + context 'and some keys does not exist' do + it 'removes the existing pair from Redis' do + described_class.record(42, now) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]]) + end + + subject.delete(41, 42) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) + end + end + end + end + end + + describe 'Enumerable' do + before do + described_class.record(40, now) + described_class.record(41, now) + described_class.record(42, now) + end + + it 'allows to read the activities sequentially' do + expected = { '40' => now.to_i.to_s, '41' => now.to_i.to_s, '42' => now.to_i.to_s } + + actual = described_class.new.each_with_object({}) do |(key, time), actual| + actual[key] = time + end + + expect(actual).to eq(expected) + end + + context 'with many records' do + before do + 1_000.times { |i| described_class.record(i, now) } + end + + it 'is possible to loop through all the records' do + expect(described_class.new.count).to eq(1_000) + end + end + end +end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 535c96eeee9..fdbb55fc874 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -179,19 +179,84 @@ describe Gitlab::Workhorse, lib: true do describe '.git_http_ok' do let(:user) { create(:user) } + let(:repo_path) { repository.path_to_repo } + let(:action) { 'info_refs' } + let(:params) do + { GL_ID: "user-#{user.id}", GL_REPOSITORY: "project-#{project.id}", RepoPath: repo_path } + end + + subject { described_class.git_http_ok(repository, false, user, action) } - subject { described_class.git_http_ok(repository, user) } + it { expect(subject).to include(params) } + + context 'when is_wiki' do + let(:params) do + { GL_ID: "user-#{user.id}", GL_REPOSITORY: "wiki-#{project.id}", RepoPath: repo_path } + end - it { expect(subject).to eq({ GL_ID: "user-#{user.id}", RepoPath: repository.path_to_repo }) } + subject { described_class.git_http_ok(repository, true, user, action) } + + it { expect(subject).to include(params) } + end context 'when Gitaly is enabled' do + let(:gitaly_params) do + { + GitalyAddress: Gitlab::GitalyClient.address('default') + } + end + before do allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) end - it 'includes Gitaly params in the returned value' do - gitaly_socket_path = URI(Gitlab::GitalyClient.get_address('default')).path - expect(subject).to include({ GitalySocketPath: gitaly_socket_path }) + it 'includes a Repository param' do + repo_param = { Repository: { + path: repo_path, + storage_name: 'default', + relative_path: project.full_path + '.git' + } } + + expect(subject).to include(repo_param) + end + + context "when git_upload_pack action is passed" do + let(:action) { 'git_upload_pack' } + let(:feature_flag) { :post_upload_pack } + + context 'when action is enabled by feature flag' do + it 'includes Gitaly params in the returned value' do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag).and_return(true) + + expect(subject).to include(gitaly_params) + end + end + + context 'when action is not enabled by feature flag' do + it 'does not include Gitaly params in the returned value' do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag).and_return(false) + + expect(subject).not_to include(gitaly_params) + end + end + end + + context "when git_receive_pack action is passed" do + let(:action) { 'git_receive_pack' } + + it { expect(subject).not_to include(gitaly_params) } + end + + context "when info_refs action is passed" do + let(:action) { 'info_refs' } + + it { expect(subject).to include(gitaly_params) } + end + + context 'when action passed is not supported by Gitaly' do + let(:action) { 'download' } + + it { expect { subject }.to raise_exception('Unsupported action: download') } end end end diff --git a/spec/lib/light_url_builder_spec.rb b/spec/lib/light_url_builder_spec.rb deleted file mode 100644 index 3fe8cf43934..00000000000 --- a/spec/lib/light_url_builder_spec.rb +++ /dev/null @@ -1,119 +0,0 @@ -require 'spec_helper' - -describe Gitlab::UrlBuilder, lib: true do - describe '.build' do - context 'when passing a Commit' do - it 'returns a proper URL' do - commit = build_stubbed(:commit) - - url = described_class.build(commit) - - expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}" - end - end - - context 'when passing an Issue' do - it 'returns a proper URL' do - issue = build_stubbed(:issue, iid: 42) - - url = described_class.build(issue) - - expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}" - end - end - - context 'when passing a MergeRequest' do - it 'returns a proper URL' do - merge_request = build_stubbed(:merge_request, iid: 42) - - url = described_class.build(merge_request) - - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}" - end - end - - context 'when passing a Note' do - context 'on a Commit' do - it 'returns a proper URL' do - note = build_stubbed(:note_on_commit) - - url = described_class.build(note) - - expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" - end - end - - context 'on a Commit Diff' do - it 'returns a proper URL' do - note = build_stubbed(:diff_note_on_commit) - - url = described_class.build(note) - - expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" - end - end - - context 'on an Issue' do - it 'returns a proper URL' do - issue = create(:issue, iid: 42) - note = build_stubbed(:note_on_issue, noteable: issue) - - url = described_class.build(note) - - expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}" - end - end - - context 'on a MergeRequest' do - it 'returns a proper URL' do - merge_request = create(:merge_request, iid: 42) - note = build_stubbed(:note_on_merge_request, noteable: merge_request) - - url = described_class.build(note) - - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" - end - end - - context 'on a MergeRequest Diff' do - it 'returns a proper URL' do - merge_request = create(:merge_request, iid: 42) - note = build_stubbed(:diff_note_on_merge_request, noteable: merge_request) - - url = described_class.build(note) - - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" - end - end - - context 'on a ProjectSnippet' do - it 'returns a proper URL' do - project_snippet = create(:project_snippet) - note = build_stubbed(:note_on_project_snippet, noteable: project_snippet) - - url = described_class.build(note) - - expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}" - end - end - - context 'on another object' do - it 'returns a proper URL' do - project = build_stubbed(:empty_project) - - expect { described_class.build(project) }. - to raise_error(NotImplementedError, 'No URL builder defined for Project') - end - end - end - - context 'when passing a WikiPage' do - it 'returns a proper URL' do - wiki_page = build(:wiki_page) - url = described_class.build(wiki_page) - - expect(url).to eq "#{Gitlab.config.gitlab.url}#{wiki_page.wiki.wiki_base_path}/#{wiki_page.slug}" - end - end - end -end diff --git a/spec/lib/microsoft_teams/activity_spec.rb b/spec/lib/microsoft_teams/activity_spec.rb new file mode 100644 index 00000000000..7890ae2e7b0 --- /dev/null +++ b/spec/lib/microsoft_teams/activity_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe MicrosoftTeams::Activity do + subject { described_class.new(title: 'title', subtitle: 'subtitle', text: 'text', image: 'image') } + + describe '#prepare' do + it 'returns the correct JSON object' do + expect(subject.prepare).to eq({ + 'activityTitle' => 'title', + 'activitySubtitle' => 'subtitle', + 'activityText' => 'text', + 'activityImage' => 'image' + }) + end + end +end diff --git a/spec/lib/microsoft_teams/notifier_spec.rb b/spec/lib/microsoft_teams/notifier_spec.rb new file mode 100644 index 00000000000..3035693812f --- /dev/null +++ b/spec/lib/microsoft_teams/notifier_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe MicrosoftTeams::Notifier do + subject { described_class.new(webhook_url) } + + let(:webhook_url) { 'https://example.gitlab.com/'} + let(:header) { { 'Content-Type' => 'application/json' } } + let(:options) do + { + title: 'JohnDoe4/project2', + pretext: '[[JohnDoe4/project2](http://localhost/namespace2/gitlabhq)] Issue [#1 Awesome issue](http://localhost/namespace2/gitlabhq/issues/1) opened by user6', + activity: { + title: 'Issue opened by user6', + subtitle: 'in [JohnDoe4/project2](http://localhost/namespace2/gitlabhq)', + text: '[#1 Awesome issue](http://localhost/namespace2/gitlabhq/issues/1)', + image: 'http://someimage.com' + }, + attachments: 'please fix' + } + end + + let(:body) do + { + 'sections' => [ + { + 'activityTitle' => 'Issue opened by user6', + 'activitySubtitle' => 'in [JohnDoe4/project2](http://localhost/namespace2/gitlabhq)', + 'activityText' => '[#1 Awesome issue](http://localhost/namespace2/gitlabhq/issues/1)', + 'activityImage' => 'http://someimage.com' + }, + { + 'title' => 'Details', + 'facts' => [ + { + 'name' => 'Attachments', + 'value' => 'please fix' + } + ] + } + ], + 'title' => 'JohnDoe4/project2', + 'summary' => '[[JohnDoe4/project2](http://localhost/namespace2/gitlabhq)] Issue [#1 Awesome issue](http://localhost/namespace2/gitlabhq/issues/1) opened by user6' + } + end + + describe '#ping' do + before do + stub_request(:post, webhook_url).with(body: JSON(body), headers: { 'Content-Type' => 'application/json' }).to_return(status: 200, body: "", headers: {}) + end + + it 'expects to receive successfull answer' do + expect(subject.ping(options)).to be true + end + end +end |