diff options
Diffstat (limited to 'spec/lib')
167 files changed, 8513 insertions, 2797 deletions
diff --git a/spec/lib/additional_email_headers_interceptor_spec.rb b/spec/lib/additional_email_headers_interceptor_spec.rb index 580450eef1e..b5c1a360ba9 100644 --- a/spec/lib/additional_email_headers_interceptor_spec.rb +++ b/spec/lib/additional_email_headers_interceptor_spec.rb @@ -1,12 +1,29 @@ require 'spec_helper' describe AdditionalEmailHeadersInterceptor do - it 'adds Auto-Submitted header' do - mail = ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello').deliver + let(:mail) do + ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello') + end + + before do + mail.deliver_now + end + it 'adds Auto-Submitted header' do expect(mail.header['To'].value).to eq('test@mail.com') expect(mail.header['From'].value).to eq('info@mail.com') expect(mail.header['Auto-Submitted'].value).to eq('auto-generated') expect(mail.header['X-Auto-Response-Suppress'].value).to eq('All') end + + context 'when the same mail object is sent twice' do + before do + mail.deliver_now + end + + it 'does not add the Auto-Submitted header twice' do + expect(mail.header['Auto-Submitted'].value).to eq('auto-generated') + expect(mail.header['X-Auto-Response-Suppress'].value).to eq('All') + end + end end diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb index 97d612e6347..ca76d6f0881 100644 --- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb +++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb @@ -15,9 +15,13 @@ describe Banzai::Filter::GollumTagsFilter do context 'linking internal images' do it 'creates img tag if image exists' do - file = Gollum::File.new(project_wiki.wiki) - expect(file).to receive(:path).and_return('images/image.jpg') - expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(file) + gollum_file_double = double('Gollum::File', + mime_type: 'image/jpeg', + name: 'images/image.jpg', + path: 'images/image.jpg', + raw_data: '') + wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double) + expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(wiki_file) tag = '[[images/image.jpg]]' doc = filter("See #{tag}", project_wiki: project_wiki) diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 9c74c9b8c99..3c98b18f99b 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -317,6 +317,68 @@ describe Banzai::Filter::IssueReferenceFilter do end end + context 'group context' do + let(:group) { create(:group) } + let(:context) { { project: nil, group: group } } + + it 'ignores shorthanded issue reference' do + reference = "##{issue.iid}" + text = "Fixed #{reference}" + + expect(reference_filter(text, context).to_html).to eq(text) + end + + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(described_class).to receive(:find_object) + .with(project, issue.iid) + .and_return(nil) + + reference = "#{project.full_path}##{issue.iid}" + text = "Issue #{reference}" + + expect(reference_filter(text, context).to_html).to eq(text) + end + + it 'links to a valid reference for complete cross-reference' do + reference = "#{project.full_path}##{issue.iid}" + doc = reference_filter("See #{reference}", context) + + expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project) + end + + it 'ignores reference for shorthand cross-reference' do + reference = "#{project.path}##{issue.iid}" + text = "See #{reference}" + + expect(reference_filter(text, context).to_html).to eq(text) + end + + it 'links to a valid reference for url cross-reference' do + reference = helper.url_for_issue(issue.iid, project) + "#note_123" + + doc = reference_filter("See #{reference}", context) + + expect(doc.css('a').first.attr('href')).to eq(helper.url_for_issue(issue.iid, project) + "#note_123") + end + + it 'links to a valid reference for cross-reference in link href' do + reference = "#{helper.url_for_issue(issue.iid, project) + "#note_123"}" + reference_link = %{<a href="#{reference}">Reference</a>} + + doc = reference_filter("See #{reference_link}", context) + + expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project) + "#note_123" + end + + it 'links to a valid reference for issue reference in the link href' do + reference = issue.to_reference(group) + reference_link = %{<a href="#{reference}">Reference</a>} + doc = reference_filter("See #{reference_link}", context) + + expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project) + end + end + describe '#issues_per_project' do context 'using an internal issue tracker' do it 'returns a Hash containing the issues per project' do diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 2cd30a5e302..862b1fe3fd3 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -594,4 +594,16 @@ describe Banzai::Filter::LabelReferenceFilter do expect(reference_filter(act).to_html).to eq exp end end + + describe 'group context' do + it 'points to referenced project issues page' do + project = create(:project) + label = create(:label, project: project) + reference = "#{project.full_path}~#{label.name}" + + result = reference_filter("See #{reference}", { project: nil, group: create(:group) } ) + + expect(result.css('a').first.attr('href')).to eq(urls.project_issues_url(project, label_name: label.name)) + end + end end diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index ed2788f8a33..158844e25ae 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -214,4 +214,14 @@ describe Banzai::Filter::MergeRequestReferenceFilter do expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/) end end + + context 'group context' do + it 'links to a valid reference' do + reference = "#{project.full_path}!#{merge.iid}" + + result = reference_filter("See #{reference}", { project: nil, group: create(:group) } ) + + expect(result.css('a').first.attr('href')).to eq(urls.project_merge_request_url(project, merge)) + end + end end diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index ebd6c79077e..84578668133 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -296,7 +296,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do context 'project milestones' do let(:milestone) { create(:milestone, project: project) } - let(:reference) { milestone.to_reference } + let(:reference) { milestone.to_reference(format: :iid) } include_examples 'reference parsing' @@ -343,4 +343,15 @@ describe Banzai::Filter::MilestoneReferenceFilter do expect(doc.css('a')).to be_empty end end + + context 'group context' do + it 'links to a valid reference' do + milestone = create(:milestone, project: project) + reference = "#{project.full_path}%#{milestone.iid}" + + result = reference_filter("See #{reference}", { project: nil, group: create(:group) } ) + + expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone)) + end + end end diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index 35a32a46eff..17a620ef603 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -47,9 +47,11 @@ describe Banzai::Filter::SanitizationFilter do describe 'custom whitelist' do it 'customizes the whitelist only once' do instance = described_class.new('Foo') + control_count = instance.whitelist[:transformers].size + 3.times { instance.whitelist } - expect(instance.whitelist[:transformers].size).to eq 4 + expect(instance.whitelist[:transformers].size).to eq control_count end it 'sanitizes `class` attribute from all elements' do @@ -63,8 +65,8 @@ describe Banzai::Filter::SanitizationFilter do expect(filter(act).to_html).to eq %q{<span>def</span>} end - it 'allows `style` attribute on table elements' do - html = <<-HTML.strip_heredoc + it 'allows `text-align` property in `style` attribute on table elements' do + html = <<~HTML <table> <tr><th style="text-align: center">Head</th></tr> <tr><td style="text-align: right">Body</th></tr> @@ -77,6 +79,20 @@ describe Banzai::Filter::SanitizationFilter do expect(doc.at_css('td')['style']).to eq 'text-align: right' end + it 'disallows other properties in `style` attribute on table elements' do + html = <<~HTML + <table> + <tr><th style="text-align: foo">Head</th></tr> + <tr><td style="position: fixed; height: 50px; width: 50px; background: red; z-index: 999; font-size: 36px; text-align: center">Body</th></tr> + </table> + HTML + + doc = filter(html) + + expect(doc.at_css('th')['style']).to be_nil + expect(doc.at_css('td')['style']).to eq 'text-align: center' + end + it 'allows `span` elements' do exp = act = %q{<span>Hello</span>} expect(filter(act).to_html).to eq exp @@ -87,6 +103,20 @@ describe Banzai::Filter::SanitizationFilter do expect(filter(act).to_html).to eq exp end + it 'disallows the `name` attribute globally, allows on `a`' do + html = <<~HTML + <img name="getElementById" src=""> + <span name="foo" class="bar">Hi</span> + <a name="foo" class="bar">Bye</a> + HTML + + doc = filter(html) + + expect(doc.at_css('img')).not_to have_attribute('name') + expect(doc.at_css('span')).not_to have_attribute('name') + expect(doc.at_css('a')).to have_attribute('name') + end + it 'allows `summary` elements' do exp = act = '<summary>summary line</summary>' expect(filter(act).to_html).to eq exp @@ -187,6 +217,11 @@ describe Banzai::Filter::SanitizationFilter do output: '<img>' }, + 'protocol-based JS injection: Unicode' => { + input: %Q(<a href="\u0001java\u0003script:alert('XSS')">foo</a>), + output: '<a>foo</a>' + }, + 'protocol-based JS injection: spaces and entities' => { input: '<a href="  javascript:alert(\'XSS\');">foo</a>', output: '<a href="">foo</a>' diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb index 90ac4c7b238..3a07a6dc179 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb @@ -201,4 +201,14 @@ describe Banzai::Filter::SnippetReferenceFilter do expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) end end + + context 'group context' do + it 'links to a valid reference' do + reference = "#{project.full_path}$#{snippet.id}" + + result = reference_filter("See #{reference}", { project: nil, group: create(:group) } ) + + expect(result.css('a').first.attr('href')).to eq(urls.project_snippet_url(project, snippet)) + end + end end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 34dac1db69a..fc03741976e 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -208,6 +208,39 @@ describe Banzai::Filter::UserReferenceFilter do end end + context 'in group context' do + let(:group) { create(:group) } + let(:group_member) { create(:user) } + + before do + group.add_developer(group_member) + end + + let(:context) { { author: group_member, project: nil, group: group } } + + it 'supports a special @all mention' do + reference = User.reference_prefix + 'all' + doc = reference_filter("Hey #{reference}", context) + + expect(doc.css('a').length).to eq(1) + expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) + end + + it 'supports mentioning a single user' do + reference = group_member.to_reference + doc = reference_filter("Hey #{reference}", context) + + expect(doc.css('a').first.attr('href')).to eq urls.user_url(group_member) + end + + it 'supports mentioning a group' do + reference = group.to_reference + doc = reference_filter("Hey #{reference}", context) + + expect(doc.css('a').first.attr('href')).to eq urls.user_url(group) + end + end + describe '#namespaces' do it 'returns a Hash containing all Namespaces' do document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>") diff --git a/spec/lib/banzai/pipeline/email_pipeline_spec.rb b/spec/lib/banzai/pipeline/email_pipeline_spec.rb new file mode 100644 index 00000000000..6a11ca2f9d5 --- /dev/null +++ b/spec/lib/banzai/pipeline/email_pipeline_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +describe Banzai::Pipeline::EmailPipeline do + describe '.filters' do + it 'returns the expected type' do + expect(described_class.filters).to be_kind_of(Banzai::FilterArray) + end + + it 'excludes ImageLazyLoadFilter' do + expect(described_class.filters).not_to be_empty + expect(described_class.filters).not_to include(Banzai::Filter::ImageLazyLoadFilter) + end + end +end diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb index da42272bbef..81a04a2d46d 100644 --- a/spec/lib/banzai/renderer_spec.rb +++ b/spec/lib/banzai/renderer_spec.rb @@ -31,7 +31,14 @@ describe Banzai::Renderer do let(:object) { fake_object(fresh: false) } it 'caches and returns the result' do - expect(object).to receive(:refresh_markdown_cache!).with(do_update: true) + expect(object).to receive(:refresh_markdown_cache!) + + is_expected.to eq('field_html') + end + + it "skips database caching on a GitLab read-only instance" do + allow(Gitlab::Database).to receive(:read_only?).and_return(true) + expect(object).to receive(:refresh_markdown_cache!) is_expected.to eq('field_html') end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb deleted file mode 100644 index 1efd3113a43..00000000000 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ /dev/null @@ -1,1697 +0,0 @@ -require 'spec_helper' - -module Ci - describe GitlabCiYamlProcessor, :lib do - subject { described_class.new(config, path) } - let(:path) { 'path' } - - describe 'our current .gitlab-ci.yml' do - let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") } - - it 'is valid' do - error_message = described_class.validation_message(config) - - expect(error_message).to be_nil - end - end - - describe '#build_attributes' do - subject { described_class.new(config, path).build_attributes(:rspec) } - - describe 'coverage entry' do - describe 'code coverage regexp' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - coverage: '/Code coverage: \d+\.\d+/' }) - end - - it 'includes coverage regexp in build attributes' do - expect(subject) - .to include(coverage_regex: 'Code coverage: \d+\.\d+') - end - end - end - - describe 'retry entry' do - context 'when retry count is specified' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', retry: 1 }) - end - - it 'includes retry count in build options attribute' do - expect(subject[:options]).to include(retry: 1) - end - end - - context 'when retry count is not specified' do - let(:config) do - YAML.dump(rspec: { script: 'rspec' }) - end - - it 'does not persist retry count in the database' do - expect(subject[:options]).not_to have_key(:retry) - end - end - end - - describe 'allow failure entry' do - context 'when job is a manual action' do - context 'when allow_failure is defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - when: 'manual', - allow_failure: false }) - end - - it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false - end - end - - context 'when allow_failure is not defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - when: 'manual' }) - end - - it 'is allowed to fail' do - expect(subject[:allow_failure]).to be true - end - end - end - - context 'when job is not a manual action' do - context 'when allow_failure is defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - allow_failure: false }) - end - - it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false - end - end - - context 'when allow_failure is not defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec' }) - end - - it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false - end - end - end - end - end - - describe '#stage_seeds' do - context 'when no refs policy is specified' do - let(:config) do - YAML.dump(production: { stage: 'deploy', script: 'cap prod' }, - rspec: { stage: 'test', script: 'rspec' }, - spinach: { stage: 'test', script: 'spinach' }) - end - - let(:pipeline) { create(:ci_empty_pipeline) } - - it 'correctly fabricates a stage seeds object' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 2 - expect(seeds.first.stage[:name]).to eq 'test' - expect(seeds.second.stage[:name]).to eq 'deploy' - expect(seeds.first.builds.dig(0, :name)).to eq 'rspec' - expect(seeds.first.builds.dig(1, :name)).to eq 'spinach' - expect(seeds.second.builds.dig(0, :name)).to eq 'production' - end - end - - context 'when refs policy is specified' do - let(:config) do - YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] }, - spinach: { stage: 'test', script: 'spinach', only: ['tags'] }) - end - - let(:pipeline) do - create(:ci_empty_pipeline, ref: 'feature', tag: true) - end - - it 'returns stage seeds only assigned to master to master' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 1 - expect(seeds.first.stage[:name]).to eq 'test' - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - end - end - - context 'when source policy is specified' do - let(:config) do - YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] }, - spinach: { stage: 'test', script: 'spinach', only: ['schedules'] }) - end - - let(:pipeline) do - create(:ci_empty_pipeline, source: :schedule) - end - - it 'returns stage seeds only assigned to schedules' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 1 - expect(seeds.first.stage[:name]).to eq 'test' - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - end - end - - context 'when kubernetes policy is specified' do - let(:pipeline) { create(:ci_empty_pipeline) } - - let(:config) do - YAML.dump( - spinach: { stage: 'test', script: 'spinach' }, - production: { - stage: 'deploy', - script: 'cap', - only: { kubernetes: 'active' } - } - ) - end - - context 'when kubernetes is active' do - let(:project) { create(:kubernetes_project) } - let(:pipeline) { create(:ci_empty_pipeline, project: project) } - - it 'returns seeds for kubernetes dependent job' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 2 - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - expect(seeds.second.builds.dig(0, :name)).to eq 'production' - end - end - - context 'when kubernetes is not active' do - it 'does not return seeds for kubernetes dependent job' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 1 - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - end - end - end - end - - describe "#builds_for_stage_and_ref" do - let(:type) { 'test' } - - it "returns builds if no branch specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec" } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - - describe 'only' do - it "does not return builds if only has another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["deploy"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) - end - - it "does not return builds if only has regexp with another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["/^deploy$/"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) - end - - it "returns builds if only has specified this branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["master"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - end - - it "returns builds if only has a list of branches including specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: %w(master deploy) } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "returns builds if only has a branches keyword specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["branches"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "does not return builds if only has a tags keyword" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["tags"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns builds if only has special keywords specified and source matches" do - possibilities = [{ keyword: 'pushes', source: 'push' }, - { keyword: 'web', source: 'web' }, - { keyword: 'triggers', source: 'trigger' }, - { keyword: 'schedules', source: 'schedule' }, - { keyword: 'api', source: 'api' }, - { keyword: 'external', source: 'external' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) - end - end - - it "does not return builds if only has special keywords specified and source doesn't match" do - possibilities = [{ keyword: 'pushes', source: 'web' }, - { keyword: 'web', source: 'push' }, - { keyword: 'triggers', source: 'schedule' }, - { keyword: 'schedules', source: 'external' }, - { keyword: 'api', source: 'trigger' }, - { keyword: 'external', source: 'api' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) - end - end - - it "returns builds if only has current repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["branches@path"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "does not return builds if only has different repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["branches@fork"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns build only for specified type" do - config = YAML.dump({ - 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"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, 'fork') - - expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) - expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1) - end - - context 'for invalid value' do - let(:config) { { rspec: { script: "rspec", type: "test", only: only } } } - let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - - context 'when it is integer' do - let(:only) { 1 } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:only has to be either an array of conditions or a hash') - end - end - - context 'when it is an array of integers' do - let(:only) { [1, 1] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:only config should be an array of strings or regexps') - end - end - - context 'when it is invalid regex' do - let(:only) { ["/*invalid/"] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:only config should be an array of strings or regexps') - end - end - end - end - - describe 'except' do - it "returns builds if except has another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", except: ["deploy"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - end - - it "returns builds if except has regexp with another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", except: ["/^deploy$/"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - end - - it "does not return builds if except has specified this branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", except: ["master"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) - end - - it "does not return builds if except has a list of branches including specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: %w(master deploy) } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "does not return builds if except has a branches keyword specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["branches"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns builds if except has a tags keyword" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["tags"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "does not return builds if except has special keywords specified and source matches" do - possibilities = [{ keyword: 'pushes', source: 'push' }, - { keyword: 'web', source: 'web' }, - { keyword: 'triggers', source: 'trigger' }, - { keyword: 'schedules', source: 'schedule' }, - { keyword: 'api', source: 'api' }, - { keyword: 'external', source: 'external' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) - end - end - - it "returns builds if except has special keywords specified and source doesn't match" do - possibilities = [{ keyword: 'pushes', source: 'web' }, - { keyword: 'web', source: 'push' }, - { keyword: 'triggers', source: 'schedule' }, - { keyword: 'schedules', source: 'external' }, - { keyword: 'api', source: 'trigger' }, - { keyword: 'external', source: 'api' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) - end - end - - it "does not return builds if except has current repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["branches@path"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns builds if except has different repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["branches@fork"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "returns build except specified type" do - config = YAML.dump({ - 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"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, 'fork') - - expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) - expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0) - expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0) - end - - context 'for invalid value' do - let(:config) { { rspec: { script: "rspec", except: except } } } - let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - - context 'when it is integer' do - let(:except) { 1 } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:except has to be either an array of conditions or a hash') - end - end - - context 'when it is an array of integers' do - let(:except) { [1, 1] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:except config should be an array of strings or regexps') - end - end - - context 'when it is invalid regex' do - let(:except) { ["/*invalid/"] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:except config should be an array of strings or regexps') - end - end - end - end - end - - describe "Scripts handling" do - let(:config_data) { YAML.dump(config) } - let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) } - - subject { config_processor.builds_for_stage_and_ref("test", "master").first } - - describe "before_script" do - context "in global context" do - let(:config) do - { - before_script: ["global script"], - test: { script: ["script"] } - } - end - - it "return commands with scripts concencaced" do - expect(subject[:commands]).to eq("global script\nscript") - end - end - - context "overwritten in local context" do - let(:config) do - { - before_script: ["global script"], - test: { before_script: ["local script"], script: ["script"] } - } - end - - it "return commands with scripts concencaced" do - expect(subject[:commands]).to eq("local script\nscript") - end - end - end - - describe "script" do - let(:config) do - { - test: { script: ["script"] } - } - end - - it "return commands with scripts concencaced" do - expect(subject[:commands]).to eq("script") - end - end - - describe "after_script" do - context "in global context" do - let(:config) do - { - after_script: ["after_script"], - test: { script: ["script"] } - } - end - - it "return after_script in options" do - expect(subject[:options][:after_script]).to eq(["after_script"]) - end - end - - context "overwritten in local context" do - let(:config) do - { - after_script: ["local after_script"], - test: { after_script: ["local after_script"], script: ["script"] } - } - end - - it "return after_script in options" do - expect(subject[:options][:after_script]).to eq(["local after_script"]) - end - end - end - end - - describe "Image and service handling" do - context "when extended docker configuration is used" do - it "returns image and service when defined" do - config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, - services: ["mysql", { name: "docker:dind", alias: "docker", - entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"] }], - before_script: ["pwd"], - rspec: { script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, - services: [{ name: "mysql" }, - { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"] }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - - it "returns image and service when overridden for job" do - config = YAML.dump({ image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, - services: [{ name: "postgresql", alias: "db-pg", - entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"] }, "docker:dind"], - script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, - services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"] }, - { name: "docker:dind" }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - end - - context "when etended docker configuration is not used" do - it "returns image and service when defined" do - config = YAML.dump({ image: "ruby:2.1", - services: ["mysql", "docker:dind"], - before_script: ["pwd"], - rspec: { script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.1" }, - services: [{ name: "mysql" }, { name: "docker:dind" }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - - it "returns image and service when overridden for job" do - config = YAML.dump({ image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.5" }, - services: [{ name: "postgresql" }, { name: "docker:dind" }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - end - end - - describe 'Variables' do - let(:config_processor) { GitlabCiYamlProcessor.new(YAML.dump(config), path) } - - subject { config_processor.builds.first[:yaml_variables] } - - context 'when global variables are defined' do - let(:variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - let(:config) do - { - variables: variables, - before_script: ['pwd'], - rspec: { script: 'rspec' } - } - end - - it 'returns global variables' do - expect(subject).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - end - end - - context 'when job and global variables are defined' do - let(:global_variables) do - { 'VAR1' => 'global1', 'VAR3' => 'global3' } - end - let(:job_variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - let(:config) do - { - before_script: ['pwd'], - variables: global_variables, - rspec: { script: 'rspec', variables: job_variables } - } - end - - it 'returns all unique variables' do - expect(subject).to contain_exactly( - { key: 'VAR3', value: 'global3', public: true }, - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - end - end - - context 'when job variables are defined' do - let(:config) do - { - before_script: ['pwd'], - rspec: { script: 'rspec', variables: variables } - } - end - - context 'when syntax is correct' do - let(:variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - - it 'returns job variables' do - expect(subject).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - end - end - - context 'when syntax is incorrect' do - context 'when variables defined but invalid' do - let(:variables) do - %w(VAR1 value1 VAR2 value2) - end - - it 'raises error' do - expect { subject } - .to raise_error(GitlabCiYamlProcessor::ValidationError, - /jobs:rspec:variables config should be a hash of key value pairs/) - end - end - - context 'when variables key defined but value not specified' do - let(:variables) do - nil - end - - it 'returns empty array' do - ## - # When variables config is empty, we assume this is a valid - # configuration, see issue #18775 - # - expect(subject).to be_an_instance_of(Array) - expect(subject).to be_empty - end - end - end - end - - context 'when job variables are not defined' do - let(:config) do - { - before_script: ['pwd'], - rspec: { script: 'rspec' } - } - end - - it 'returns empty array' do - expect(subject).to be_an_instance_of(Array) - expect(subject).to be_empty - end - end - end - - describe "When" do - %w(on_success on_failure always).each do |when_state| - it "returns #{when_state} when defined" do - config = YAML.dump({ - rspec: { script: "rspec", when: when_state } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - builds = config_processor.builds_for_stage_and_ref("test", "master") - expect(builds.size).to eq(1) - expect(builds.first[:when]).to eq(when_state) - end - end - end - - describe 'cache' do - context 'when cache definition has unknown keys' do - it 'raises relevant validation error' do - config = YAML.dump( - { cache: { untracked: true, invalid: 'key' }, - rspec: { script: 'rspec' } }) - - expect { GitlabCiYamlProcessor.new(config) }.to raise_error( - GitlabCiYamlProcessor::ValidationError, - 'cache config contains unknown keys: invalid' - ) - end - end - - it "returns cache when defined globally" do - config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, - rspec: { - script: "rspec" - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( - paths: ["logs/", "binaries/"], - untracked: true, - key: 'key', - policy: 'pull-push' - ) - end - - it "returns cache when defined in a job" do - config = YAML.dump({ - rspec: { - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, - script: "rspec" - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( - paths: ["logs/", "binaries/"], - untracked: true, - key: 'key', - policy: 'pull-push' - ) - end - - it "overwrite cache when defined for a job and globally" do - config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, - rspec: { - script: "rspec", - cache: { paths: ["test/"], untracked: false, key: 'local' } - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( - paths: ["test/"], - untracked: false, - key: 'local', - policy: 'pull-push' - ) - end - end - - describe "Artifacts" do - it "returns artifacts when defined" do - config = YAML.dump({ - image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { - artifacts: { - paths: ["logs/", "binaries/"], - untracked: true, - name: "custom_name", - expire_in: "7d" - }, - script: "rspec" - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.1" }, - services: [{ name: "mysql" }], - artifacts: { - name: "custom_name", - paths: ["logs/", "binaries/"], - untracked: true, - expire_in: "7d" - } - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - end - - %w[on_success on_failure always].each do |when_state| - it "returns artifacts for when #{when_state} defined" do - config = YAML.dump({ - rspec: { - script: "rspec", - artifacts: { paths: ["logs/", "binaries/"], when: when_state } - } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - builds = config_processor.builds_for_stage_and_ref("test", "master") - expect(builds.size).to eq(1) - expect(builds.first[:options][:artifacts][:when]).to eq(when_state) - end - end - end - - describe '#environment' do - let(:config) do - { - deploy_to_production: { stage: 'deploy', script: 'test', environment: environment } - } - end - - let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') } - - context 'when a production environment is specified' do - let(:environment) { 'production' } - - it 'does return production' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to eq(environment) - expect(builds.first[:options]).to include(environment: { name: environment, action: "start" }) - end - end - - context 'when hash is specified' do - let(:environment) do - { name: 'production', - url: 'http://production.gitlab.com' } - end - - it 'does return production and URL' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to eq(environment[:name]) - expect(builds.first[:options]).to include(environment: environment) - end - - context 'the url has a port as variable' do - let(:environment) do - { name: 'production', - url: 'http://production.gitlab.com:$PORT' } - end - - it 'allows a variable for the port' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to eq(environment[:name]) - expect(builds.first[:options]).to include(environment: environment) - end - end - end - - context 'when no environment is specified' do - let(:environment) { nil } - - it 'does return nil environment' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to be_nil - end - end - - context 'is not a string' do - let(:environment) { 1 } - - it 'raises error' do - expect { builds }.to raise_error( - 'jobs:deploy_to_production:environment config should be a hash or a string') - end - end - - context 'is not a valid string' do - let(:environment) { 'production:staging' } - - it 'raises error' do - expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}") - end - end - - context 'when on_stop is specified' do - let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } } - let(:config) { { review: review, close_review: close_review }.compact } - - context 'with matching job' do - let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } } - - it 'does return a list of builds' do - expect(builds.size).to eq(2) - expect(builds.first[:environment]).to eq('review') - end - end - - context 'without matching job' do - let(:close_review) { nil } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review is not defined') - end - end - - context 'with close job without environment' do - let(:close_review) { { stage: 'deploy', script: 'test' } } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined') - end - end - - context 'with close job for different environment' do - let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review have different environment name') - end - end - - context 'with close job without stop action' do - let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined') - end - end - end - end - - describe "Dependencies" do - let(:config) do - { - build1: { stage: 'build', script: 'test' }, - build2: { stage: 'build', script: 'test' }, - test1: { stage: 'test', script: 'test', dependencies: dependencies }, - test2: { stage: 'test', script: 'test' }, - deploy: { stage: 'test', script: 'test' } - } - end - - subject { GitlabCiYamlProcessor.new(YAML.dump(config)) } - - context 'no dependencies' do - let(:dependencies) { } - - it { expect { subject }.not_to raise_error } - end - - context 'dependencies to builds' do - let(:dependencies) { %w(build1 build2) } - - it { expect { subject }.not_to raise_error } - end - - context 'dependencies to builds defined as symbols' do - let(:dependencies) { [:build1, :build2] } - - it { expect { subject }.not_to raise_error } - end - - context 'undefined dependency' do - let(:dependencies) { ['undefined'] } - - it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') } - end - - context 'dependencies to deploy' do - let(:dependencies) { ['deploy'] } - - it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') } - end - end - - describe "Hidden jobs" do - let(:config_processor) { GitlabCiYamlProcessor.new(config) } - subject { config_processor.builds_for_stage_and_ref("test", "master") } - - shared_examples 'hidden_job_handling' do - it "doesn't create jobs that start with dot" do - expect(subject.size).to eq(1) - expect(subject.first).to eq({ - stage: "test", - stage_idx: 1, - name: "normal_job", - commands: "test", - coverage_regex: nil, - tag_list: [], - options: { - script: ["test"] - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - end - end - - context 'when hidden job have a script definition' do - let(:config) do - YAML.dump({ - '.hidden_job' => { image: 'ruby:2.1', script: 'test' }, - 'normal_job' => { script: 'test' } - }) - end - - it_behaves_like 'hidden_job_handling' - end - - context "when hidden job doesn't have a script definition" do - let(:config) do - YAML.dump({ - '.hidden_job' => { image: 'ruby:2.1' }, - 'normal_job' => { script: 'test' } - }) - end - - it_behaves_like 'hidden_job_handling' - end - end - - describe "YAML Alias/Anchor" do - let(:config_processor) { GitlabCiYamlProcessor.new(config) } - subject { config_processor.builds_for_stage_and_ref("build", "master") } - - shared_examples 'job_templates_handling' do - it "is correctly supported for jobs" do - expect(subject.size).to eq(2) - expect(subject.first).to eq({ - stage: "build", - stage_idx: 0, - name: "job1", - commands: "execute-script-for-job", - coverage_regex: nil, - tag_list: [], - options: { - script: ["execute-script-for-job"] - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - expect(subject.second).to eq({ - stage: "build", - stage_idx: 0, - name: "job2", - commands: "execute-script-for-job", - coverage_regex: nil, - tag_list: [], - options: { - script: ["execute-script-for-job"] - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - end - end - - context 'when template is a job' do - let(:config) do - <<EOT -job1: &JOBTMPL - stage: build - script: execute-script-for-job - -job2: *JOBTMPL -EOT - end - - it_behaves_like 'job_templates_handling' - end - - context 'when template is a hidden job' do - let(:config) do - <<EOT -.template: &JOBTMPL - stage: build - script: execute-script-for-job - -job1: *JOBTMPL - -job2: *JOBTMPL -EOT - end - - it_behaves_like 'job_templates_handling' - end - - context 'when job adds its own keys to a template definition' do - let(:config) do - <<EOT -.template: &JOBTMPL - stage: build - -job1: - <<: *JOBTMPL - script: execute-script-for-job - -job2: - <<: *JOBTMPL - script: execute-script-for-job -EOT - end - - it_behaves_like 'job_templates_handling' - end - end - - describe "Error handling" do - it "fails to parse YAML" do - expect {GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError) - end - - it "indicates that object is invalid" do - expect {GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError) - end - - it "returns errors if tags parameter is invalid" do - config = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings") - end - - it "returns errors if before_script parameter is invalid" do - config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script config should be an array of strings") - end - - it "returns errors if job before_script parameter is not an array of strings" do - config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings") - end - - it "returns errors if after_script parameter is invalid" do - config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script config should be an array of strings") - end - - it "returns errors if job after_script parameter is not an array of strings" do - config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings") - end - - it "returns errors if image parameter is invalid" do - config = YAML.dump({ image: ["test"], rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image config should be a hash or a string") - end - - it "returns errors if job name is blank" do - config = YAML.dump({ '' => { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:job name can't be blank") - end - - it "returns errors if job name is non-string" do - config = YAML.dump({ 10 => { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:10 name should be a symbol") - end - - it "returns errors if job image parameter is invalid" do - config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string") - end - - it "returns errors if services parameter is not an array" do - config = YAML.dump({ services: "test", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be a array") - end - - it "returns errors if services parameter is not an array of strings" do - config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string") - end - - it "returns errors if job services parameter is not an array" do - config = YAML.dump({ rspec: { script: "test", services: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be a array") - end - - it "returns errors if job services parameter is not an array of strings" do - config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string") - end - - it "returns error if job configuration is invalid" do - config = YAML.dump({ extra: "bundle update" }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash") - end - - it "returns errors if services configuration is not correct" do - config = YAML.dump({ extra: { script: 'rspec', services: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be a array") - end - - it "returns errors if there are no jobs defined" do - config = YAML.dump({ before_script: ["bundle update"] }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job") - end - - it "returns errors if there are no visible jobs defined" do - config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job") - end - - it "returns errors if job allow_failure parameter is not an boolean" do - config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value") - end - - it "returns errors if job stage is not a string" do - config = YAML.dump({ rspec: { script: "test", type: 1 } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be a string") - end - - it "returns errors if job stage is not a pre-defined stage" do - config = YAML.dump({ rspec: { script: "test", type: "acceptance" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") - end - - it "returns errors if job stage is not a defined stage" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") - end - - it "returns errors if stages is not an array" do - config = YAML.dump({ stages: "test", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings") - end - - it "returns errors if stages is not an array of strings" do - config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings") - end - - it "returns errors if variables is not a map" do - config = YAML.dump({ variables: "test", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs") - end - - it "returns errors if variables is not a map of key-value strings" do - config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs") - end - - it "returns errors if job when is not on_success, on_failure or always" do - config = YAML.dump({ rspec: { script: "test", when: 1 } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual") - end - - it "returns errors if job artifacts:name is not an a string" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string") - end - - it "returns errors if job artifacts:when is not an a predefined value" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always") - end - - it "returns errors if job artifacts:expire_in is not an a string" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") - end - - it "returns errors if job artifacts:expire_in is not an a valid duration" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") - end - - it "returns errors if job artifacts:untracked is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value") - end - - it "returns errors if job artifacts:paths is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings") - end - - it "returns errors if cache:untracked is not an array of strings" do - config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:untracked config should be a boolean value") - end - - it "returns errors if cache:paths is not an array of strings" do - config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths config should be an array of strings") - end - - it "returns errors if cache:key is not a string" do - config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:key config should be a string or symbol") - end - - it "returns errors if job cache:key is not an a string" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol") - end - - it "returns errors if job cache:untracked is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value") - end - - it "returns errors if job cache:paths is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings") - end - - it "returns errors if job dependencies is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") - end - end - - describe "Validate configuration templates" do - templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml") - - templates.each do |file| - it "does not return errors for #{file}" do - file = File.read(file) - - expect { GitlabCiYamlProcessor.new(file) }.not_to raise_error - end - end - end - - describe "#validation_message" do - context "when the YAML could not be parsed" do - it "returns an error about invalid configutaion" do - content = YAML.dump("invalid: yaml: test") - - expect(GitlabCiYamlProcessor.validation_message(content)) - .to eq "Invalid configuration format" - end - end - - context "when the tags parameter is invalid" do - it "returns an error about invalid tags" do - content = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) - - expect(GitlabCiYamlProcessor.validation_message(content)) - .to eq "jobs:rspec tags should be an array of strings" - end - end - - context "when YAML content is empty" do - it "returns an error about missing content" do - expect(GitlabCiYamlProcessor.validation_message('')) - .to eq "Please provide content of .gitlab-ci.yml" - end - end - - context "when the YAML is valid" do - it "does not return any errors" do - content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) - - expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil - end - end - end - end -end diff --git a/spec/lib/github/client_spec.rb b/spec/lib/github/client_spec.rb new file mode 100644 index 00000000000..b846096fe25 --- /dev/null +++ b/spec/lib/github/client_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Github::Client do + let(:connection) { spy } + let(:rate_limit) { double(get: [false, 1]) } + let(:client) { described_class.new({}) } + let(:results) { double } + let(:response) { double } + + before do + allow(Faraday).to receive(:new).and_return(connection) + allow(Github::RateLimit).to receive(:new).with(connection).and_return(rate_limit) + end + + describe '#get' do + before do + allow(Github::Response).to receive(:new).with(results).and_return(response) + end + + it 'uses a default per_page param' do + expect(connection).to receive(:get).with('/foo', per_page: 100).and_return(results) + + expect(client.get('/foo')).to eq(response) + end + + context 'with per_page given' do + it 'overwrites the default per_page' do + expect(connection).to receive(:get).with('/foo', per_page: 30).and_return(results) + + expect(client.get('/foo', per_page: 30)).to eq(response) + end + end + end +end diff --git a/spec/lib/github/import/legacy_diff_note_spec.rb b/spec/lib/github/import/legacy_diff_note_spec.rb new file mode 100644 index 00000000000..8c50b46cacb --- /dev/null +++ b/spec/lib/github/import/legacy_diff_note_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe Github::Import::LegacyDiffNote do + describe '#type' do + it 'returns the original note type' do + expect(described_class.new.type).to eq('LegacyDiffNote') + end + end +end diff --git a/spec/lib/github/import/note_spec.rb b/spec/lib/github/import/note_spec.rb new file mode 100644 index 00000000000..fcdccd9e097 --- /dev/null +++ b/spec/lib/github/import/note_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe Github::Import::Note do + describe '#type' do + it 'returns the original note type' do + expect(described_class.new.type).to eq('Note') + end + end +end diff --git a/spec/lib/gitlab/app_logger_spec.rb b/spec/lib/gitlab/app_logger_spec.rb new file mode 100644 index 00000000000..c86d30ce6df --- /dev/null +++ b/spec/lib/gitlab/app_logger_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Gitlab::AppLogger, :request_store do + subject { described_class } + + it 'builds a logger once' do + expect(::Logger).to receive(:new).and_call_original + + subject.info('hello world') + subject.error('hello again') + end +end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index f685bb83d0d..54a853c9ce3 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Auth do describe 'constants' do it 'API_SCOPES contains all scopes for API access' do - expect(subject::API_SCOPES).to eq [:api, :read_user] + expect(subject::API_SCOPES).to eq %i[api read_user sudo] end it 'OPENID_SCOPES contains all scopes for OpenID Connect' do @@ -16,12 +16,32 @@ describe Gitlab::Auth do expect(subject::DEFAULT_SCOPES).to eq [:api] end - it 'OPTIONAL_SCOPES contains all non-default scopes' do - expect(subject::OPTIONAL_SCOPES).to eq %i[read_user read_registry openid] + it 'optional_scopes contains all non-default scopes' do + stub_container_registry_config(enabled: true) + + expect(subject.optional_scopes).to eq %i[read_user sudo read_registry openid] end - it 'REGISTRY_SCOPES contains all registry related scopes' do - expect(subject::REGISTRY_SCOPES).to eq %i[read_registry] + context 'registry_scopes' do + context 'when registry is disabled' do + before do + stub_container_registry_config(enabled: false) + end + + it 'is empty' do + expect(subject.registry_scopes).to eq [] + end + end + + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it 'contains all registry related scopes' do + expect(subject.registry_scopes).to eq %i[read_registry] + end + end end end @@ -144,28 +164,34 @@ describe Gitlab::Auth do personal_access_token = create(:personal_access_token, scopes: ['api']) expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities)) + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, full_authentication_abilities)) end - it 'succeeds for personal access tokens with the `read_registry` scope' do - personal_access_token = create(:personal_access_token, scopes: ['read_registry']) + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it 'succeeds for personal access tokens with the `read_registry` scope' do + personal_access_token = create(:personal_access_token, scopes: ['read_registry']) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image])) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:read_container_image])) + end end it 'succeeds if it is an impersonation token' do impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api']) expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_token, full_authentication_abilities)) + expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_access_token, full_authentication_abilities)) end it 'limits abilities based on scope' do personal_access_token = create(:personal_access_token, scopes: ['read_user']) expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [])) + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [])) end it 'fails if password is nil' do @@ -208,7 +234,7 @@ describe Gitlab::Auth do it 'throws an error suggesting user create a PAT when internal auth is disabled' do allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled?) { false } - expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalTokenError) + expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError) end end diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb new file mode 100644 index 00000000000..1a4ea2bac48 --- /dev/null +++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migration, schema: 20170929131201 do + let(:migration) { described_class.new } + + let(:base1) { create(:project) } + let(:base1_fork1) { create(:project) } + let(:base1_fork2) { create(:project) } + + let(:base2) { create(:project) } + let(:base2_fork1) { create(:project) } + let(:base2_fork2) { create(:project) } + + let(:fork_of_fork) { create(:project) } + let(:fork_of_fork2) { create(:project) } + let(:second_level_fork) { create(:project) } + let(:third_level_fork) { create(:project) } + + let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) } + let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) } + + let!(:forked_project_links) { table(:forked_project_links) } + let!(:fork_networks) { table(:fork_networks) } + let!(:fork_network_members) { table(:fork_network_members) } + + before do + # The fork-network relation created for the forked project + fork_networks.create(id: 1, root_project_id: base1.id) + fork_network_members.create(project_id: base1.id, fork_network_id: 1) + fork_networks.create(id: 2, root_project_id: base2.id) + fork_network_members.create(project_id: base2.id, fork_network_id: 2) + + # Normal fork links + forked_project_links.create(id: 1, forked_from_project_id: base1.id, forked_to_project_id: base1_fork1.id) + forked_project_links.create(id: 2, forked_from_project_id: base1.id, forked_to_project_id: base1_fork2.id) + forked_project_links.create(id: 3, forked_from_project_id: base2.id, forked_to_project_id: base2_fork1.id) + forked_project_links.create(id: 4, forked_from_project_id: base2.id, forked_to_project_id: base2_fork2.id) + + # Fork links + forked_project_links.create(id: 5, forked_from_project_id: base1_fork1.id, forked_to_project_id: fork_of_fork.id) + forked_project_links.create(id: 6, forked_from_project_id: base1_fork1.id, forked_to_project_id: fork_of_fork2.id) + + # Forks 3 levels down + forked_project_links.create(id: 7, forked_from_project_id: fork_of_fork.id, forked_to_project_id: second_level_fork.id) + forked_project_links.create(id: 8, forked_from_project_id: second_level_fork.id, forked_to_project_id: third_level_fork.id) + + migration.perform(1, 8) + end + + it 'creates a memberships for the direct forks' do + base1_fork1_membership = fork_network_members.find_by(fork_network_id: fork_network1.id, + project_id: base1_fork1.id) + base1_fork2_membership = fork_network_members.find_by(fork_network_id: fork_network1.id, + project_id: base1_fork2.id) + base2_fork1_membership = fork_network_members.find_by(fork_network_id: fork_network2.id, + project_id: base2_fork1.id) + base2_fork2_membership = fork_network_members.find_by(fork_network_id: fork_network2.id, + project_id: base2_fork2.id) + + expect(base1_fork1_membership.forked_from_project_id).to eq(base1.id) + expect(base1_fork2_membership.forked_from_project_id).to eq(base1.id) + expect(base2_fork1_membership.forked_from_project_id).to eq(base2.id) + expect(base2_fork2_membership.forked_from_project_id).to eq(base2.id) + end + + it 'adds the fork network members for forks of forks' do + fork_of_fork_membership = fork_network_members.find_by(project_id: fork_of_fork.id, + fork_network_id: fork_network1.id) + fork_of_fork2_membership = fork_network_members.find_by(project_id: fork_of_fork2.id, + fork_network_id: fork_network1.id) + second_level_fork_membership = fork_network_members.find_by(project_id: second_level_fork.id, + fork_network_id: fork_network1.id) + third_level_fork_membership = fork_network_members.find_by(project_id: third_level_fork.id, + fork_network_id: fork_network1.id) + + expect(fork_of_fork_membership.forked_from_project_id).to eq(base1_fork1.id) + expect(fork_of_fork2_membership.forked_from_project_id).to eq(base1_fork1.id) + expect(second_level_fork_membership.forked_from_project_id).to eq(fork_of_fork.id) + expect(third_level_fork_membership.forked_from_project_id).to eq(second_level_fork.id) + end + + it 'reschedules itself when there are missing members' do + allow(migration).to receive(:missing_members?).and_return(true) + + expect(BackgroundMigrationWorker) + .to receive(:perform_in).with(described_class::RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [1, 3]) + + migration.perform(1, 3) + end + + it 'can be repeated without effect' do + expect { fork_network_members.count }.not_to change { migration.perform(1, 7) } + end + + it 'knows it is finished for this range' do + expect(migration.missing_members?(1, 7)).to be_falsy + end + + context 'with more forks' do + before do + forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id) + forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id) + end + + it 'only processes a single batch of links at a time' do + expect(fork_network_members.count).to eq(10) + + migration.perform(8, 10) + + expect(fork_network_members.count).to eq(12) + end + + it 'knows when not all memberships withing a batch have been created' do + expect(migration.missing_members?(8, 10)).to be_truthy + end + end +end diff --git a/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb new file mode 100644 index 00000000000..26d48cc8201 --- /dev/null +++ b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys, :migration, schema: 20171005130944 do + context 'when GpgKey exists' do + let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User3.public_key) } + + before do + GpgKeySubkey.destroy_all + end + + it 'generate the subkeys' do + expect do + described_class.new.perform(gpg_key.id) + end.to change { gpg_key.subkeys.count }.from(0).to(2) + end + + it 'schedules the signature update worker' do + expect(InvalidGpgSignatureUpdateWorker).to receive(:perform_async).with(gpg_key.id) + + described_class.new.perform(gpg_key.id) + end + end + + context 'when GpgKey does not exist' do + it 'does not do anything' do + expect(Gitlab::Gpg).not_to receive(:subkeys_from_key) + expect(InvalidGpgSignatureUpdateWorker).not_to receive(:perform_async) + + described_class.new.perform(123) + end + end +end diff --git a/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb new file mode 100644 index 00000000000..5c471cbdeda --- /dev/null +++ b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange, :migration, schema: 20170907170235 do + let!(:redirect_routes) { table(:redirect_routes) } + let!(:routes) { table(:routes) } + + before do + routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1') + routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2') + routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3') + routes.create!(id: 4, source_id: 4, source_type: 'Namespace', path: 'foo4') + routes.create!(id: 5, source_id: 5, source_type: 'Namespace', path: 'foo5') + + # Valid redirects + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar2') + redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'bar3') + + # Conflicting redirects + redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'foo1') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo2') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo3') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo4') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5') + end + + it 'deletes the conflicting redirect_routes in the range' do + expect(redirect_routes.count).to eq(8) + + expect do + described_class.new.perform(1, 3) + end.to change { redirect_routes.where("path like 'foo%'").count }.from(5).to(2) + + expect do + described_class.new.perform(4, 5) + end.to change { redirect_routes.where("path like 'foo%'").count }.from(2).to(0) + + expect(redirect_routes.count).to eq(3) + end +end diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb index c0427639746..4d3fdbd9554 100644 --- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do +describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :truncate do describe '#perform' do - set(:merge_request) { create(:merge_request) } - set(:merge_request_diff) { merge_request.merge_request_diff } + let(:merge_request) { create(:merge_request) } + let(:merge_request_diff) { merge_request.merge_request_diff } let(:updated_merge_request_diff) { MergeRequestDiff.find(merge_request_diff.id) } def diffs_to_hashes(diffs) @@ -31,8 +31,8 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do end it 'creates correct entries in the merge_request_diff_commits table' do - expect(updated_merge_request_diff.merge_request_diff_commits.count).to eq(commits.count) - expect(updated_merge_request_diff.commits.map(&:to_hash)).to eq(commits) + expect(updated_merge_request_diff.merge_request_diff_commits.count).to eq(expected_commits.count) + expect(updated_merge_request_diff.commits.map(&:to_hash)).to eq(expected_commits) end it 'creates correct entries in the merge_request_diff_files table' do @@ -70,8 +70,8 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do before do merge_request.reload_diff(true) - convert_to_yaml(start_id, merge_request_diff.commits, merge_request_diff.diffs) - convert_to_yaml(stop_id, updated_merge_request_diff.commits, updated_merge_request_diff.diffs) + convert_to_yaml(start_id, merge_request_diff.commits, diffs_to_hashes(merge_request_diff.merge_request_diff_files)) + convert_to_yaml(stop_id, updated_merge_request_diff.commits, diffs_to_hashes(updated_merge_request_diff.merge_request_diff_files)) MergeRequestDiffCommit.delete_all MergeRequestDiffFile.delete_all @@ -80,10 +80,32 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do context 'when BUFFER_ROWS is exceeded' do before do stub_const("#{described_class}::BUFFER_ROWS", 1) + + allow(Gitlab::Database).to receive(:bulk_insert).and_call_original end - it 'updates and continues' do - expect(described_class::MergeRequestDiff).to receive(:transaction).twice + it 'inserts commit rows in chunks of BUFFER_ROWS' do + # There are 29 commits in each diff, so we should have slices of 20 + 9 + 20 + 9. + stub_const("#{described_class}::BUFFER_ROWS", 20) + + expect(Gitlab::Database).to receive(:bulk_insert) + .with('merge_request_diff_commits', anything) + .exactly(4) + .times + .and_call_original + + subject.perform(start_id, stop_id) + end + + it 'inserts diff rows in chunks of DIFF_FILE_BUFFER_ROWS' do + # There are 20 files in each diff, so we should have slices of 20 + 20. + stub_const("#{described_class}::DIFF_FILE_BUFFER_ROWS", 20) + + expect(Gitlab::Database).to receive(:bulk_insert) + .with('merge_request_diff_files', anything) + .exactly(2) + .times + .and_call_original subject.perform(start_id, stop_id) end @@ -91,32 +113,102 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do context 'when BUFFER_ROWS is not exceeded' do it 'only updates once' do - expect(described_class::MergeRequestDiff).to receive(:transaction).once + expect(Gitlab::Database).to receive(:bulk_insert) + .with('merge_request_diff_commits', anything) + .once + .and_call_original + + expect(Gitlab::Database).to receive(:bulk_insert) + .with('merge_request_diff_files', anything) + .once + .and_call_original subject.perform(start_id, stop_id) end end - end - context 'when the merge request diff update fails' do - before do - allow(described_class::MergeRequestDiff) - .to receive(:update_all).and_raise(ActiveRecord::Rollback) - end + context 'when some rows were already inserted due to a previous failure' do + before do + subject.perform(start_id, stop_id) - it 'does not add any diff commits' do - expect { subject.perform(merge_request_diff.id, merge_request_diff.id) } - .not_to change { MergeRequestDiffCommit.count } + convert_to_yaml(start_id, merge_request_diff.commits, diffs_to_hashes(merge_request_diff.merge_request_diff_files)) + convert_to_yaml(stop_id, updated_merge_request_diff.commits, diffs_to_hashes(updated_merge_request_diff.merge_request_diff_files)) + end + + it 'does not raise' do + expect { subject.perform(start_id, stop_id) }.not_to raise_exception + end + + it 'logs a message' do + expect(Rails.logger).to receive(:info) + .with( + a_string_matching(described_class.name).and(matching([start_id, stop_id].inspect)) + ) + .twice + + subject.perform(start_id, stop_id) + end + + it 'ends up with the correct rows' do + expect(updated_merge_request_diff.commits.count).to eq(29) + expect(updated_merge_request_diff.raw_diffs.count).to eq(20) + end end - it 'does not add any diff files' do - expect { subject.perform(merge_request_diff.id, merge_request_diff.id) } - .not_to change { MergeRequestDiffFile.count } + context 'when the merge request diff update fails' do + let(:exception) { ActiveRecord::RecordNotFound } + + let(:perform_ignoring_exceptions) do + begin + subject.perform(start_id, stop_id) + rescue described_class::Error + end + end + + before do + allow_any_instance_of(described_class::MergeRequestDiff::ActiveRecord_Relation) + .to receive(:update_all).and_raise(exception) + end + + it 'raises an error' do + expect { subject.perform(start_id, stop_id) } + .to raise_exception(described_class::Error) + end + + it 'logs the error' do + expect(Rails.logger).to receive(:info).with( + a_string_matching(described_class.name) + .and(matching([start_id, stop_id].inspect)) + .and(matching(exception.name)) + ) + + perform_ignoring_exceptions + end + + it 'still adds diff commits' do + expect { perform_ignoring_exceptions } + .to change { MergeRequestDiffCommit.count } + end + + it 'still adds diff files' do + expect { perform_ignoring_exceptions } + .to change { MergeRequestDiffFile.count } + end end end context 'when the merge request diff has valid commits and diffs' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } + let(:diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } + let(:expected_diffs) { diffs } + + include_examples 'updated MR diff' + end + + context 'when the merge request diff has diffs but no commits' do + let(:commits) { nil } + let(:expected_commits) { [] } let(:diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } let(:expected_diffs) { diffs } @@ -125,6 +217,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do context 'when the merge request diffs do not have too_large set' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:expected_diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } let(:diffs) do @@ -136,6 +229,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do context 'when the merge request diffs do not have a_mode and b_mode set' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:expected_diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } let(:diffs) do @@ -147,6 +241,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do context 'when the merge request diffs have binary content' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:expected_diffs) { diffs } # The start of a PDF created by Illustrator @@ -175,6 +270,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do context 'when the merge request diff has commits, but no diffs' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:diffs) { [] } let(:expected_diffs) { diffs } @@ -183,6 +279,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do context 'when the merge request diffs have invalid content' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:diffs) { ['--broken-diff'] } let(:expected_diffs) { [] } @@ -192,6 +289,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do context 'when the merge request diffs are Rugged::Patch instances' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) } + let(:expected_commits) { commits } let(:diffs) { first_commit.rugged_diff_from_parent.patches } let(:expected_diffs) { [] } @@ -201,6 +299,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do context 'when the merge request diffs are Rugged::Diff::Delta instances' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) } + let(:expected_commits) { commits } let(:diffs) { first_commit.rugged_diff_from_parent.deltas } let(:expected_diffs) { [] } diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index c56b08b18a2..cb52d971047 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event do +describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event, :migration, schema: 20170608152748 do describe '#commit_title' do it 'returns nil when there are no commits' do expect(described_class.new.commit_title).to be_nil @@ -215,9 +215,17 @@ end # to a specific version of the database where said table is still present. # describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170825154015 do + let(:user_class) do + Class.new(ActiveRecord::Base) do + self.table_name = 'users' + end + end + let(:migration) { described_class.new } - let(:project) { create(:project_empty_repo) } - let(:author) { create(:user) } + let(:user_class) { table(:users) } + let(:author) { build(:user).becomes(user_class).tap(&:save!).becomes(User) } + let(:namespace) { create(:namespace, owner: author) } + let(:project) { create(:project_empty_repo, namespace: namespace, creator: author) } # We can not rely on FactoryGirl as the state of Event may change in ways that # the background migration does not expect, hence we use the Event class of diff --git a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb index 59f69d1e4b1..7b5a00c6111 100644 --- a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder do end describe '#perform' do - it 'renames the path of system-uploads', truncate: true do + it 'renames the path of system-uploads', :truncate do upload = create(:upload, model: create(:project), path: 'uploads/system/project/avatar.jpg') migration.perform('uploads/system/', 'uploads/-/system/') diff --git a/spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb b/spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb new file mode 100644 index 00000000000..dfbf1bb681a --- /dev/null +++ b/spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::NormalizeLdapExternUidsRange, :migration, schema: 20170921101004 do + let!(:identities) { table(:identities) } + + before do + # LDAP identities + (1..4).each do |i| + identities.create!(id: i, provider: 'ldapmain', extern_uid: " uid = foo #{i}, ou = People, dc = example, dc = com ", user_id: i) + end + + # Non-LDAP identity + identities.create!(id: 5, provider: 'foo', extern_uid: " uid = foo 5, ou = People, dc = example, dc = com ", user_id: 5) + + # Another LDAP identity + identities.create!(id: 6, provider: 'ldapmain', extern_uid: " uid = foo 6, ou = People, dc = example, dc = com ", user_id: 6) + end + + it 'normalizes the LDAP identities in the range' do + described_class.new.perform(1, 3) + expect(identities.find(1).extern_uid).to eq("uid=foo 1,ou=people,dc=example,dc=com") + expect(identities.find(2).extern_uid).to eq("uid=foo 2,ou=people,dc=example,dc=com") + expect(identities.find(3).extern_uid).to eq("uid=foo 3,ou=people,dc=example,dc=com") + expect(identities.find(4).extern_uid).to eq(" uid = foo 4, ou = People, dc = example, dc = com ") + expect(identities.find(5).extern_uid).to eq(" uid = foo 5, ou = People, dc = example, dc = com ") + expect(identities.find(6).extern_uid).to eq(" uid = foo 6, ou = People, dc = example, dc = com ") + + described_class.new.perform(4, 6) + expect(identities.find(1).extern_uid).to eq("uid=foo 1,ou=people,dc=example,dc=com") + expect(identities.find(2).extern_uid).to eq("uid=foo 2,ou=people,dc=example,dc=com") + expect(identities.find(3).extern_uid).to eq("uid=foo 3,ou=people,dc=example,dc=com") + expect(identities.find(4).extern_uid).to eq("uid=foo 4,ou=people,dc=example,dc=com") + expect(identities.find(5).extern_uid).to eq(" uid = foo 5, ou = People, dc = example, dc = com ") + expect(identities.find(6).extern_uid).to eq("uid=foo 6,ou=people,dc=example,dc=com") + end +end diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb new file mode 100644 index 00000000000..2c2684a6fc9 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do + let(:migration) { described_class.new } + let(:base1) { create(:project) } + let(:base1_fork1) { create(:project) } + let(:base1_fork2) { create(:project) } + + let(:base2) { create(:project) } + let(:base2_fork1) { create(:project) } + let(:base2_fork2) { create(:project) } + + let!(:forked_project_links) { table(:forked_project_links) } + let!(:fork_networks) { table(:fork_networks) } + let!(:fork_network_members) { table(:fork_network_members) } + + let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) } + let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) } + + before do + # A normal fork link + forked_project_links.create(id: 1, + forked_from_project_id: base1.id, + forked_to_project_id: base1_fork1.id) + forked_project_links.create(id: 2, + forked_from_project_id: base1.id, + forked_to_project_id: base1_fork2.id) + + forked_project_links.create(id: 3, + forked_from_project_id: base2.id, + forked_to_project_id: base2_fork1.id) + forked_project_links.create(id: 4, + forked_from_project_id: base2_fork1.id, + forked_to_project_id: create(:project).id) + + forked_project_links.create(id: 5, + forked_from_project_id: base2.id, + forked_to_project_id: base2_fork2.id) + + migration.perform(1, 3) + end + + it 'it creates the fork network' do + expect(fork_network1).not_to be_nil + expect(fork_network2).not_to be_nil + end + + it 'does not create a fork network for a fork-of-fork' do + # perfrom the entire batch + migration.perform(1, 5) + + expect(fork_networks.find_by(root_project_id: base2_fork1.id)).to be_nil + end + + it 'creates memberships for the root of fork networks' do + base1_membership = fork_network_members.find_by(fork_network_id: fork_network1.id, + project_id: base1.id) + base2_membership = fork_network_members.find_by(fork_network_id: fork_network2.id, + project_id: base2.id) + + expect(base1_membership).not_to be_nil + expect(base2_membership).not_to be_nil + end + + it 'skips links that had their source project deleted' do + forked_project_links.create(id: 6, forked_from_project_id: 99999, forked_to_project_id: create(:project).id) + + migration.perform(5, 8) + + expect(fork_networks.find_by(root_project_id: 99999)).to be_nil + end + + it 'schedules a job for inserting memberships for forks-of-forks' do + delay = Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY + + expect(BackgroundMigrationWorker) + .to receive(:perform_in).with(delay, "CreateForkNetworkMembershipsRange", [1, 3]) + + migration.perform(1, 3) + end + + it 'only processes a single batch of links at a time' do + expect(fork_network_members.count).to eq(5) + + migration.perform(3, 5) + + expect(fork_network_members.count).to eq(7) + end + + it 'can be repeated without effect' do + expect { migration.perform(1, 3) }.not_to change { fork_network_members.count } + end +end diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb index 8772d3d5ada..b68301a066a 100644 --- a/spec/lib/gitlab/backup/manager_spec.rb +++ b/spec/lib/gitlab/backup/manager_spec.rb @@ -26,6 +26,9 @@ describe Backup::Manager do [ '1451606400_2016_01_01_1.2.3_gitlab_backup.tar', '1451520000_2015_12_31_4.5.6_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6-pre_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6-rc1_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6-pre-ee_gitlab_backup.tar', '1451510000_2015_12_30_gitlab_backup.tar', '1450742400_2015_12_22_gitlab_backup.tar', '1449878400_gitlab_backup.tar', @@ -57,6 +60,30 @@ describe Backup::Manager do end end + context 'when no valid file is found' do + let(:files) do + [ + '14516064000_2016_01_01_1.2.3_gitlab_backup.tar', + 'foo_1451520000_2015_12_31_4.5.6_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6-foo_gitlab_backup.tar' + ] + end + + before do + allow(Gitlab.config.backup).to receive(:keep_time).and_return(1) + + subject.remove_old + end + + it 'removes no files' do + expect(FileUtils).not_to have_received(:rm) + end + + it 'prints a done message' do + expect(progress).to have_received(:puts).with('done. (0 removed)') + end + end + context 'when there are no files older than keep_time' do before do # Set to 30 days @@ -84,16 +111,22 @@ describe Backup::Manager do it 'removes matching files with a human-readable versioned timestamp' do expect(FileUtils).to have_received(:rm).with(files[1]) - end - - it 'removes matching files with a human-readable non-versioned timestamp' do expect(FileUtils).to have_received(:rm).with(files[2]) expect(FileUtils).to have_received(:rm).with(files[3]) end - it 'removes matching files without a human-readable timestamp' do + it 'removes matching files with a human-readable versioned timestamp with tagged EE' do expect(FileUtils).to have_received(:rm).with(files[4]) + end + + it 'removes matching files with a human-readable non-versioned timestamp' do expect(FileUtils).to have_received(:rm).with(files[5]) + expect(FileUtils).to have_received(:rm).with(files[6]) + end + + it 'removes matching files without a human-readable timestamp' do + expect(FileUtils).to have_received(:rm).with(files[7]) + expect(FileUtils).to have_received(:rm).with(files[8]) end it 'does not remove files that are not old enough' do @@ -101,11 +134,11 @@ describe Backup::Manager do end it 'does not remove non-matching files' do - expect(FileUtils).not_to have_received(:rm).with(files[6]) + expect(FileUtils).not_to have_received(:rm).with(files[9]) end it 'prints a done message' do - expect(progress).to have_received(:puts).with('done. (5 removed)') + expect(progress).to have_received(:puts).with('done. (8 removed)') end end @@ -121,14 +154,15 @@ describe Backup::Manager do end it 'removes the remaining expected files' do - expect(FileUtils).to have_received(:rm).with(files[2]) - expect(FileUtils).to have_received(:rm).with(files[3]) expect(FileUtils).to have_received(:rm).with(files[4]) expect(FileUtils).to have_received(:rm).with(files[5]) + expect(FileUtils).to have_received(:rm).with(files[6]) + expect(FileUtils).to have_received(:rm).with(files[7]) + expect(FileUtils).to have_received(:rm).with(files[8]) end it 'sets the correct removed count' do - expect(progress).to have_received(:puts).with('done. (4 removed)') + expect(progress).to have_received(:puts).with('done. (7 removed)') end it 'prints the error from file that could not be removed' do @@ -138,10 +172,6 @@ describe Backup::Manager do end describe '#unpack' do - before do - allow(Dir).to receive(:chdir) - end - context 'when there are no backup files in the directory' do before do allow(Dir).to receive(:glob).and_return([]) diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb index f8c8b83a3ac..633e319f46d 100644 --- a/spec/lib/gitlab/checks/force_push_spec.rb +++ b/spec/lib/gitlab/checks/force_push_spec.rb @@ -3,15 +3,15 @@ require 'spec_helper' describe Gitlab::Checks::ForcePush do let(:project) { create(:project, :repository) } - context "exit code checking", skip_gitaly_mock: true do + context "exit code checking", :skip_gitaly_mock 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]) + allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['normal output', 0]) 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]) + allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['error', 1]) expect { described_class.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError) end diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb index e49ecadde20..33540eab5d6 100644 --- a/spec/lib/ci/ansi2html_spec.rb +++ b/spec/lib/gitlab/ci/ansi2html_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::Ansi2html do +describe Gitlab::Ci::Ansi2html do subject { described_class } it "prints non-ansi as-is" do @@ -195,6 +195,32 @@ describe Ci::Ansi2html do end end + context "with section markers" do + let(:section_name) { 'test_section' } + let(:section_start_time) { Time.new(2017, 9, 20).utc } + let(:section_duration) { 3.seconds } + let(:section_end_time) { section_start_time + section_duration } + let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"} + let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"} + let(:section_start_html) do + '<div class="hidden" data-action="start"'\ + " data-timestamp=\"#{section_start_time.to_i}\" data-section=\"#{section_name}\">"\ + "#{section_start[0...-5]}</div>" + end + let(:section_end_html) do + '<div class="hidden" data-action="end"'\ + " data-timestamp=\"#{section_end_time.to_i}\" data-section=\"#{section_name}\">"\ + "#{section_end[0...-5]}</div>" + end + + it "prints light red" do + text = "#{section_start}\e[91mHello\e[0m\n#{section_end}" + html = %{#{section_start_html}<span class="term-fg-l-red">Hello</span><br>#{section_end_html}} + + expect(convert_html(text)).to eq(html) + end + end + describe "truncates" do let(:text) { "Hello World" } let(:stream) { StringIO.new(text) } diff --git a/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb new file mode 100644 index 00000000000..15eb01eb472 --- /dev/null +++ b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Policy::Kubernetes do + let(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when kubernetes service is active' do + set(:project) { create(:kubernetes_project) } + + it 'is satisfied by a kubernetes pipeline' do + expect(described_class.new('active')) + .to be_satisfied_by(pipeline) + end + end + + context 'when kubernetes service is inactive' do + set(:project) { create(:project) } + + it 'is not satisfied by a pipeline without kubernetes available' do + expect(described_class.new('active')) + .not_to be_satisfied_by(pipeline) + end + end + + context 'when kubernetes policy is invalid' do + it 'raises an error' do + expect { described_class.new('unknown') } + .to raise_error(described_class::UnknownPolicyError) + end + end +end diff --git a/spec/lib/gitlab/ci/build/policy/refs_spec.rb b/spec/lib/gitlab/ci/build/policy/refs_spec.rb new file mode 100644 index 00000000000..7211187e511 --- /dev/null +++ b/spec/lib/gitlab/ci/build/policy/refs_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Policy::Refs do + describe '#satisfied_by?' do + context 'when matching ref' do + let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'master') } + + it 'is satisfied when pipeline branch matches' do + expect(described_class.new(%w[master deploy])) + .to be_satisfied_by(pipeline) + end + + it 'is not satisfied when pipeline branch does not match' do + expect(described_class.new(%w[feature fix])) + .not_to be_satisfied_by(pipeline) + end + end + + context 'when maching tags' do + context 'when pipeline runs for a tag' do + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: 'feature', tag: true) + end + + it 'is satisfied when tags matcher is specified' do + expect(described_class.new(%w[master tags])) + .to be_satisfied_by(pipeline) + end + end + + context 'when pipeline is not created for a tag' do + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: 'feature', tag: false) + end + + it 'is not satisfied when tag match is specified' do + expect(described_class.new(%w[master tags])) + .not_to be_satisfied_by(pipeline) + end + end + end + + context 'when also matching a path' do + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: 'master') + end + + it 'is satisfied when provided patch matches specified one' do + expect(described_class.new(%W[master@#{pipeline.project_full_path}])) + .to be_satisfied_by(pipeline) + end + + it 'is not satisfied when path differs' do + expect(described_class.new(%w[master@some/fork/repository])) + .not_to be_satisfied_by(pipeline) + end + end + + context 'when maching a source' do + let(:pipeline) { build_stubbed(:ci_pipeline, source: :push) } + + it 'is satisifed when provided source keyword matches' do + expect(described_class.new(%w[pushes])) + .to be_satisfied_by(pipeline) + end + + it 'is not satisfied when provided source keyword does not match' do + expect(described_class.new(%w[triggers])) + .not_to be_satisfied_by(pipeline) + end + end + + context 'when matching a ref by a regular expression' do + let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'docs-something') } + + it 'is satisfied when regexp matches pipeline ref' do + expect(described_class.new(['/docs-.*/'])) + .to be_satisfied_by(pipeline) + end + + it 'is not satisfied when regexp does not match pipeline ref' do + expect(described_class.new(['/fix-.*/'])) + .not_to be_satisfied_by(pipeline) + end + end + end +end diff --git a/spec/lib/gitlab/ci/build/policy_spec.rb b/spec/lib/gitlab/ci/build/policy_spec.rb new file mode 100644 index 00000000000..20ee3dd3e89 --- /dev/null +++ b/spec/lib/gitlab/ci/build/policy_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Policy do + let(:policy) { spy('policy specification') } + + before do + stub_const("#{described_class}::Something", policy) + end + + describe '.fabricate' do + context 'when policy exists' do + it 'fabricates and initializes relevant policy' do + specs = described_class.fabricate(something: 'some value') + + expect(specs).to be_an Array + expect(specs).to be_one + expect(policy).to have_received(:new).with('some value') + end + end + + context 'when some policies are not defined' do + it 'gracefully skips unknown policies' do + expect { described_class.fabricate(unknown: 'first') } + .to raise_error(NameError) + end + end + + context 'when passing a nil value as specs' do + it 'returns an empty array' do + specs = described_class.fabricate(nil) + + expect(specs).to be_an Array + expect(specs).to be_empty + end + end + end +end diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/gitlab/ci/charts_spec.rb index f0769deef21..f8188675013 100644 --- a/spec/lib/ci/charts_spec.rb +++ b/spec/lib/gitlab/ci/charts_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Ci::Charts do +describe Gitlab::Ci::Charts do context "pipeline_times" do let(:project) { create(:project) } - let(:chart) { Ci::Charts::PipelineTime.new(project) } + let(:chart) { Gitlab::Ci::Charts::PipelineTime.new(project) } subject { chart.pipeline_times } diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb index 809fda11879..2a3f7807fdb 100644 --- a/spec/lib/gitlab/ci/cron_parser_spec.rb +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -77,8 +77,20 @@ describe Gitlab::Ci::CronParser do it_behaves_like "returns time in the future" - it 'converts time in server time zone' do - expect(subject.hour).to eq(hour_in_utc) + context 'when PST (Pacific Standard Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 1, 1)) do + expect(subject.hour).to eq(hour_in_utc) + end + end + end + + context 'when PDT (Pacific Daylight Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 6, 1)) do + expect(subject.hour).to eq(hour_in_utc) + end + end end end end @@ -100,8 +112,20 @@ describe Gitlab::Ci::CronParser do it_behaves_like "returns time in the future" - it 'converts time in server time zone' do - expect(subject.hour).to eq(hour_in_utc) + context 'when CET (Central European Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 1, 1)) do + expect(subject.hour).to eq(hour_in_utc) + end + end + end + + context 'when CEST (Central European Summer Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 6, 1)) do + expect(subject.hour).to eq(hour_in_utc) + end + end end end @@ -111,8 +135,20 @@ describe Gitlab::Ci::CronParser do it_behaves_like "returns time in the future" - it 'converts time in server time zone' do - expect(subject.hour).to eq(hour_in_utc) + context 'when EST (Eastern Standard Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 1, 1)) do + expect(subject.hour).to eq(hour_in_utc) + end + end + end + + context 'when EDT (Eastern Daylight Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 6, 1)) do + expect(subject.hour).to eq(hour_in_utc) + end + end end end end diff --git a/spec/lib/ci/mask_secret_spec.rb b/spec/lib/gitlab/ci/mask_secret_spec.rb index f7b753b022b..3789a142248 100644 --- a/spec/lib/ci/mask_secret_spec.rb +++ b/spec/lib/gitlab/ci/mask_secret_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::MaskSecret do +describe Gitlab::Ci::MaskSecret do subject { described_class } describe '#mask' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb new file mode 100644 index 00000000000..f54e2326b06 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Create do + set(:project) { create(:project) } + set(:user) { create(:user) } + + let(:pipeline) do + build(:ci_pipeline_with_one_job, project: project, + ref: 'master') + end + + let(:command) do + double('command', project: project, + current_user: user, + seeds_block: nil) + end + + let(:step) { described_class.new(pipeline, command) } + + before do + step.perform! + end + + context 'when pipeline is ready to be saved' do + it 'saves a pipeline' do + expect(pipeline).to be_persisted + end + + it 'does not break the chain' do + expect(step.break?).to be false + end + + it 'creates stages' do + expect(pipeline.reload.stages).to be_one + end + end + + context 'when pipeline has validation errors' do + let(:pipeline) do + build(:ci_pipeline, project: project, ref: nil) + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + + it 'appends validation error' do + expect(pipeline.errors.to_a) + .to include /Failed to persist the pipeline/ + end + end + + context 'when there is a seed block present' do + let(:seeds) { spy('pipeline seeds') } + + let(:command) do + double('command', project: project, + current_user: user, + seeds_block: seeds) + end + + it 'executes the block' do + expect(seeds).to have_received(:call).with(pipeline) + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb new file mode 100644 index 00000000000..e165e0fac2a --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Sequence do + set(:project) { create(:project) } + set(:user) { create(:user) } + + let(:pipeline) { build_stubbed(:ci_pipeline) } + let(:command) { double('command' ) } + let(:first_step) { spy('first step') } + let(:second_step) { spy('second step') } + let(:sequence) { [first_step, second_step] } + + subject do + described_class.new(pipeline, command, sequence) + end + + context 'when one of steps breaks the chain' do + before do + allow(first_step).to receive(:break?).and_return(true) + end + + it 'does not process the second step' do + subject.build! do |pipeline, sequence| + expect(sequence).not_to be_complete + end + + expect(second_step).not_to have_received(:perform!) + end + + it 'returns a pipeline object' do + expect(subject.build!).to eq pipeline + end + end + + context 'when all chains are executed correctly' do + before do + sequence.each do |step| + allow(step).to receive(:break?).and_return(false) + end + end + + it 'iterates through entire sequence' do + subject.build! do |pipeline, sequence| + expect(sequence).to be_complete + end + + expect(first_step).to have_received(:perform!) + expect(second_step).to have_received(:perform!) + end + + it 'returns a pipeline object' do + expect(subject.build!).to eq pipeline + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb new file mode 100644 index 00000000000..32bd5de829b --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Skip do + set(:project) { create(:project) } + set(:user) { create(:user) } + set(:pipeline) { create(:ci_pipeline, project: project) } + + let(:command) do + double('command', project: project, + current_user: user, + ignore_skip_ci: false, + save_incompleted: true) + end + + let(:step) { described_class.new(pipeline, command) } + + context 'when pipeline has been skipped by a user' do + before do + allow(pipeline).to receive(:git_commit_message) + .and_return('commit message [ci skip]') + + step.perform! + end + + it 'should break the chain' do + expect(step.break?).to be true + end + + it 'skips the pipeline' do + expect(pipeline.reload).to be_skipped + end + end + + context 'when pipeline has not been skipped' do + before do + step.perform! + end + + it 'should not break the chain' do + expect(step.break?).to be false + end + + it 'should not skip a pipeline chain' do + expect(pipeline.reload).not_to be_skipped + end + end + + context 'when [ci skip] should be ignored' do + let(:command) do + double('command', project: project, + current_user: user, + ignore_skip_ci: true) + end + + it 'does not break the chain' do + step.perform! + + expect(step.break?).to be false + end + end + + context 'when pipeline should be skipped but not persisted' do + let(:command) do + double('command', project: project, + current_user: user, + ignore_skip_ci: false, + save_incompleted: false) + end + + before do + allow(pipeline).to receive(:git_commit_message) + .and_return('commit message [ci skip]') + + step.perform! + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + + it 'does not skip pipeline' do + expect(pipeline.reload).not_to be_skipped + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb new file mode 100644 index 00000000000..0bbdd23f4d6 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: ref, project: project) + end + + let(:command) do + double('command', project: project, current_user: user) + end + + let(:step) { described_class.new(pipeline, command) } + + let(:ref) { 'master' } + + context 'when users has no ability to run a pipeline' do + before do + step.perform! + end + + it 'adds an error about insufficient permissions' do + expect(pipeline.errors.to_a) + .to include /Insufficient permissions/ + end + + it 'breaks the pipeline builder chain' do + expect(step.break?).to eq true + end + end + + context 'when user has ability to create a pipeline' do + before do + project.add_developer(user) + + step.perform! + end + + it 'does not invalidate the pipeline' do + expect(pipeline).to be_valid + end + + it 'does not break the chain' do + expect(step.break?).to eq false + end + end + + describe '#allowed_to_create?' do + subject { step.allowed_to_create? } + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it { is_expected.to be_truthy } + + context 'when the branch is protected' do + let!(:protected_branch) do + create(:protected_branch, project: project, name: ref) + end + + it { is_expected.to be_falsey } + + context 'when developers are allowed to merge' do + let!(:protected_branch) do + create(:protected_branch, + :developers_can_merge, + project: project, + name: ref) + end + + it { is_expected.to be_truthy } + end + end + + context 'when the tag is protected' do + let(:ref) { 'v1.0.0' } + + let!(:protected_tag) do + create(:protected_tag, project: project, name: ref) + end + + it { is_expected.to be_falsey } + + context 'when developers are allowed to create the tag' do + let!(:protected_tag) do + create(:protected_tag, + :developers_can_create, + project: project, + name: ref) + end + + it { is_expected.to be_truthy } + end + end + end + + context 'when user is a master' do + before do + project.add_master(user) + end + + it { is_expected.to be_truthy } + + context 'when the branch is protected' do + let!(:protected_branch) do + create(:protected_branch, project: project, name: ref) + end + + it { is_expected.to be_truthy } + end + + context 'when the tag is protected' do + let(:ref) { 'v1.0.0' } + + let!(:protected_tag) do + create(:protected_tag, project: project, name: ref) + end + + it { is_expected.to be_truthy } + + context 'when no one can create the tag' do + let!(:protected_tag) do + create(:protected_tag, + :no_one_can_create, + project: project, + name: ref) + end + + it { is_expected.to be_falsey } + end + end + end + + context 'when owner cannot create pipeline' do + it { is_expected.to be_falsey } + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb new file mode 100644 index 00000000000..8357af38f92 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb @@ -0,0 +1,130 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Validate::Config do + set(:project) { create(:project) } + set(:user) { create(:user) } + + let(:command) do + double('command', project: project, + current_user: user, + save_incompleted: true) + end + + let!(:step) { described_class.new(pipeline, command) } + + before do + step.perform! + end + + context 'when pipeline has no YAML configuration' do + let(:pipeline) do + build_stubbed(:ci_pipeline, project: project) + end + + it 'appends errors about missing configuration' do + expect(pipeline.errors.to_a) + .to include 'Missing .gitlab-ci.yml file' + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + end + + context 'when YAML configuration contains errors' do + let(:pipeline) do + build(:ci_pipeline, project: project, config: 'invalid YAML') + end + + it 'appends errors about YAML errors' do + expect(pipeline.errors.to_a) + .to include 'Invalid configuration format' + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + + context 'when saving incomplete pipeline is allowed' do + let(:command) do + double('command', project: project, + current_user: user, + save_incompleted: true) + end + + it 'fails the pipeline' do + expect(pipeline.reload).to be_failed + end + + it 'sets a config error failure reason' do + expect(pipeline.reload.config_error?).to eq true + end + end + + context 'when saving incomplete pipeline is not allowed' do + let(:command) do + double('command', project: project, + current_user: user, + save_incompleted: false) + end + + it 'does not drop pipeline' do + expect(pipeline).not_to be_failed + expect(pipeline).not_to be_persisted + end + end + end + + context 'when pipeline has no stages / jobs' do + let(:config) do + { rspec: { + script: 'ls', + only: ['something'] + } } + end + + let(:pipeline) do + build(:ci_pipeline, project: project, config: config) + end + + it 'appends an error about missing stages' do + expect(pipeline.errors.to_a) + .to include 'No stages / jobs for this pipeline.' + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + end + + context 'when pipeline contains configuration validation errors' do + let(:config) { { rspec: {} } } + + let(:pipeline) do + build(:ci_pipeline, project: project, config: config) + end + + it 'appends configuration validation errors to pipeline errors' do + expect(pipeline.errors.to_a) + .to include "jobs:rspec config can't be blank" + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + end + + context 'when pipeline is correct and complete' do + let(:pipeline) do + build(:ci_pipeline_with_one_job, project: project) + end + + it 'does not invalidate the pipeline' do + expect(pipeline).to be_valid + end + + it 'does not break the chain' do + expect(step.break?).to be false + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb new file mode 100644 index 00000000000..bb356efe9ad --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + + let(:command) do + double('command', project: project, current_user: user) + end + + let!(:step) { described_class.new(pipeline, command) } + + before do + step.perform! + end + + context 'when pipeline ref and sha exists' do + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: 'master', sha: '123', project: project) + end + + it 'does not break the chain' do + expect(step.break?).to be false + end + + it 'does not append pipeline errors' do + expect(pipeline.errors).to be_empty + end + end + + context 'when pipeline ref does not exist' do + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: 'something', project: project) + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + + it 'adds an error about missing ref' do + expect(pipeline.errors.to_a) + .to include 'Reference not found' + end + end + + context 'when pipeline does not have SHA set' do + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: 'master', sha: nil, project: project) + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + + it 'adds an error about missing SHA' do + expect(pipeline.errors.to_a) + .to include 'Commit not found' + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline_duration_spec.rb b/spec/lib/gitlab/ci/pipeline/duration_spec.rb index b26728a843c..7c9836e2da6 100644 --- a/spec/lib/gitlab/ci/pipeline_duration_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/duration_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::PipelineDuration do +describe Gitlab::Ci::Pipeline::Duration do let(:calculated_duration) { calculate(data) } shared_examples 'calculating duration' do @@ -107,9 +107,9 @@ describe Gitlab::Ci::PipelineDuration do def calculate(data) periods = data.shuffle.map do |(first, last)| - Gitlab::Ci::PipelineDuration::Period.new(first, last) + described_class::Period.new(first, last) end - Gitlab::Ci::PipelineDuration.from_periods(periods.sort_by(&:first)) + described_class.from_periods(periods.sort_by(&:first)) end end diff --git a/spec/lib/gitlab/ci/stage/seed_spec.rb b/spec/lib/gitlab/ci/stage/seed_spec.rb index 9ecd128faca..3fe8d50c49a 100644 --- a/spec/lib/gitlab/ci/stage/seed_spec.rb +++ b/spec/lib/gitlab/ci/stage/seed_spec.rb @@ -11,6 +11,12 @@ describe Gitlab::Ci::Stage::Seed do described_class.new(pipeline, 'test', builds) end + describe '#size' do + it 'returns a number of jobs in the stage' do + expect(subject.size).to eq 2 + end + end + describe '#stage' do it 'returns hash attributes of a stage' do expect(subject.stage).to be_a Hash diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb index 5a7a42d84c0..9cdebaa5cf2 100644 --- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -66,7 +66,7 @@ describe Gitlab::Ci::Status::Build::Cancelable do end describe '#action_icon' do - it { expect(subject.action_icon).to eq 'icon_action_cancel' } + it { expect(subject.action_icon).to eq 'cancel' } end describe '#action_title' do diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index 8768302eda1..2b32e47e9ba 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -30,7 +30,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'passed' - expect(status.icon).to eq 'icon_status_success' + expect(status.icon).to eq 'status_success' expect(status.favicon).to eq 'favicon_status_success' expect(status.label).to eq 'passed' expect(status).to have_details @@ -57,7 +57,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'failed' - expect(status.icon).to eq 'icon_status_failed' + expect(status.icon).to eq 'status_failed' expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq 'failed' expect(status).to have_details @@ -84,7 +84,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'failed' - expect(status.icon).to eq 'icon_status_warning' + expect(status.icon).to eq 'warning' expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq 'failed (allowed to fail)' expect(status).to have_details @@ -113,7 +113,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'canceled' - expect(status.icon).to eq 'icon_status_canceled' + expect(status.icon).to eq 'status_canceled' expect(status.favicon).to eq 'favicon_status_canceled' expect(status.label).to eq 'canceled' expect(status).to have_details @@ -139,7 +139,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'running' - expect(status.icon).to eq 'icon_status_running' + expect(status.icon).to eq 'status_running' expect(status.favicon).to eq 'favicon_status_running' expect(status.label).to eq 'running' expect(status).to have_details @@ -165,7 +165,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'pending' - expect(status.icon).to eq 'icon_status_pending' + expect(status.icon).to eq 'status_pending' expect(status.favicon).to eq 'favicon_status_pending' expect(status.label).to eq 'pending' expect(status).to have_details @@ -190,7 +190,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'skipped' - expect(status.icon).to eq 'icon_status_skipped' + expect(status.icon).to eq 'status_skipped' expect(status.favicon).to eq 'favicon_status_skipped' expect(status.label).to eq 'skipped' expect(status).to have_details @@ -219,7 +219,7 @@ describe Gitlab::Ci::Status::Build::Factory 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.icon).to eq 'status_manual' expect(status.favicon).to eq 'favicon_status_manual' expect(status.label).to include 'manual play action' expect(status).to have_details @@ -274,7 +274,7 @@ describe Gitlab::Ci::Status::Build::Factory 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.icon).to eq '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 diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb index 20f71459738..79a65fc67e8 100644 --- a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb +++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Build::FailedAllowed do describe '#icon' do it 'returns a warning icon' do - expect(subject.icon).to eq 'icon_status_warning' + expect(subject.icon).to eq 'warning' 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 32b2e62e4e0..81d5f553fd1 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -46,7 +46,7 @@ describe Gitlab::Ci::Status::Build::Play do end describe '#action_icon' do - it { expect(subject.action_icon).to eq 'icon_action_play' } + it { expect(subject.action_icon).to eq 'play' } end describe '#action_title' do diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb index 21026f2c968..14d42e0d70f 100644 --- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -66,7 +66,7 @@ describe Gitlab::Ci::Status::Build::Retryable do end describe '#action_icon' do - it { expect(subject.action_icon).to eq 'icon_action_retry' } + it { expect(subject.action_icon).to eq 'retry' } end describe '#action_title' do diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index e0425103f41..18e250772f0 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -38,7 +38,7 @@ describe Gitlab::Ci::Status::Build::Stop do end describe '#action_icon' do - it { expect(subject.action_icon).to eq 'icon_action_stop' } + it { expect(subject.action_icon).to eq 'stop' } end describe '#action_title' do diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index 530639a5897..dc74d7e28c5 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Canceled do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_canceled' } + it { expect(subject.icon).to eq 'status_canceled' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb index aef982e17f1..ce4333f2aca 100644 --- a/spec/lib/gitlab/ci/status/created_spec.rb +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Created do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_created' } + it { expect(subject.icon).to eq 'status_created' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb index 9a25743885c..a4a92117c7f 100644 --- a/spec/lib/gitlab/ci/status/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Failed do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_failed' } + it { expect(subject.icon).to eq 'status_failed' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb index 6fdc3801d71..0463f2e1aff 100644 --- a/spec/lib/gitlab/ci/status/manual_spec.rb +++ b/spec/lib/gitlab/ci/status/manual_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Manual do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_manual' } + it { expect(subject.icon).to eq 'status_manual' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb index ffc53f0506b..0e25358dd8a 100644 --- a/spec/lib/gitlab/ci/status/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Pending do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_pending' } + it { expect(subject.icon).to eq 'status_pending' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb index 0babf1fb54e..9c9d431bb5d 100644 --- a/spec/lib/gitlab/ci/status/running_spec.rb +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Running do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_running' } + it { expect(subject.icon).to eq 'status_running' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb index 670747c9f0b..63694ca0ea6 100644 --- a/spec/lib/gitlab/ci/status/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Skipped do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_skipped' } + it { expect(subject.icon).to eq 'status_skipped' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb index ff65b074808..2f67df71c4f 100644 --- a/spec/lib/gitlab/ci/status/success_spec.rb +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Success do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_success' } + it { expect(subject.icon).to eq 'status_success' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb index 7e2269397c6..4582354e739 100644 --- a/spec/lib/gitlab/ci/status/success_warning_spec.rb +++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::SuccessWarning do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_warning' } + it { expect(subject.icon).to eq 'status_warning' } end describe '#group' do diff --git a/spec/lib/gitlab/ci/trace/section_parser_spec.rb b/spec/lib/gitlab/ci/trace/section_parser_spec.rb new file mode 100644 index 00000000000..ca53ff87c6f --- /dev/null +++ b/spec/lib/gitlab/ci/trace/section_parser_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe Gitlab::Ci::Trace::SectionParser do + def lines_with_pos(text) + pos = 0 + StringIO.new(text).each_line do |line| + yield line, pos + pos += line.bytesize + 1 # newline + end + end + + def build_lines(text) + to_enum(:lines_with_pos, text) + end + + def section(name, start, duration, text) + end_ = start + duration + "section_start:#{start.to_i}:#{name}\r\033[0K#{text}section_end:#{end_.to_i}:#{name}\r\033[0K" + end + + let(:lines) { build_lines('') } + subject { described_class.new(lines) } + + describe '#sections' do + before do + subject.parse! + end + + context 'empty trace' do + let(:lines) { build_lines('') } + + it { expect(subject.sections).to be_empty } + end + + context 'with a sectionless trace' do + let(:lines) { build_lines("line 1\nline 2\n") } + + it { expect(subject.sections).to be_empty } + end + + context 'with trace markers' do + let(:start_time) { Time.new(2017, 10, 5).utc } + let(:section_b_duration) { 1.second } + let(:section_a) { section('a', start_time, 0, 'a line') } + let(:section_b) { section('b', start_time, section_b_duration, "another line\n") } + let(:lines) { build_lines(section_a + section_b) } + + it { expect(subject.sections.size).to eq(2) } + it { expect(subject.sections[1][:name]).to eq('b') } + it { expect(subject.sections[1][:date_start]).to eq(start_time) } + it { expect(subject.sections[1][:date_end]).to eq(start_time + section_b_duration) } + end + end + + describe '#parse!' do + context 'multiple "section_" but no complete markers' do + let(:lines) { build_lines('section_section_section_') } + + it 'must find 3 possible section start but no complete sections' do + expect(subject).to receive(:find_next_marker).exactly(3).times.and_call_original + + subject.parse! + + expect(subject.sections).to be_empty + end + end + + context 'trace with UTF-8 chars' do + let(:line) { 'GitLab ❤️ 狸 (tanukis)\n' } + let(:trace) { section('test_section', Time.new(2017, 10, 5).utc, 3.seconds, line) } + let(:lines) { build_lines(trace) } + + it 'must handle correctly byte positioning' do + expect(subject).to receive(:find_next_marker).exactly(2).times.and_call_original + + subject.parse! + + sections = subject.sections + + expect(sections.size).to eq(1) + s = sections[0] + len = s[:byte_end] - s[:byte_start] + expect(trace.byteslice(s[:byte_start], len)).to eq(line) + end + end + end +end diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb index 9cb0b62590a..3546532b9b4 100644 --- a/spec/lib/gitlab/ci/trace_spec.rb +++ b/spec/lib/gitlab/ci/trace_spec.rb @@ -61,6 +61,93 @@ describe Gitlab::Ci::Trace do end end + describe '#extract_sections' do + let(:log) { 'No sections' } + let(:sections) { trace.extract_sections } + + before do + trace.set(log) + end + + context 'no sections' do + it 'returs []' do + expect(trace.extract_sections).to eq([]) + end + end + + context 'multiple sections available' do + let(:log) { File.read(expand_fixture_path('trace/trace_with_sections')) } + let(:sections_data) do + [ + { name: 'prepare_script', lines: 2, duration: 3.seconds }, + { name: 'get_sources', lines: 4, duration: 1.second }, + { name: 'restore_cache', lines: 0, duration: 0.seconds }, + { name: 'download_artifacts', lines: 0, duration: 0.seconds }, + { name: 'build_script', lines: 2, duration: 1.second }, + { name: 'after_script', lines: 0, duration: 0.seconds }, + { name: 'archive_cache', lines: 0, duration: 0.seconds }, + { name: 'upload_artifacts', lines: 0, duration: 0.seconds } + ] + end + + it "returns valid sections" do + expect(sections).not_to be_empty + expect(sections.size).to eq(sections_data.size), + "expected #{sections_data.size} sections, got #{sections.size}" + + buff = StringIO.new(log) + sections.each_with_index do |s, i| + expected = sections_data[i] + + expect(s[:name]).to eq(expected[:name]) + expect(s[:date_end] - s[:date_start]).to eq(expected[:duration]) + + buff.seek(s[:byte_start], IO::SEEK_SET) + length = s[:byte_end] - s[:byte_start] + lines = buff.read(length).count("\n") + expect(lines).to eq(expected[:lines]) + end + end + end + + context 'logs contains "section_start"' do + let(:log) { "section_start:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_end:1506417477:a_section\r\033[0K"} + + it "returns only one section" do + expect(sections).not_to be_empty + expect(sections.size).to eq(1) + + section = sections[0] + expect(section[:name]).to eq('a_section') + expect(section[:byte_start]).not_to eq(section[:byte_end]), "got an empty section" + end + end + + context 'missing section_end' do + let(:log) { "section_start:1506417476:a_section\r\033[0KSome logs\nNo section_end\n"} + + it "returns no sections" do + expect(sections).to be_empty + end + end + + context 'missing section_start' do + let(:log) { "Some logs\nNo section_start\nsection_end:1506417476:a_section\r\033[0K"} + + it "returns no sections" do + expect(sections).to be_empty + end + end + + context 'inverted section_start section_end' do + let(:log) { "section_end:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_start:1506417477:a_section\r\033[0K"} + + it "returns no sections" do + expect(sections).to be_empty + end + end + end + describe '#set' do before do trace.set("12") diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb new file mode 100644 index 00000000000..d72f8553f55 --- /dev/null +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -0,0 +1,1716 @@ +require 'spec_helper' + +module Gitlab + module Ci + describe YamlProcessor, :lib do + subject { described_class.new(config) } + + describe 'our current .gitlab-ci.yml' do + let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") } + + it 'is valid' do + error_message = described_class.validation_message(config) + + expect(error_message).to be_nil + end + end + + describe '#build_attributes' do + subject { described_class.new(config).build_attributes(:rspec) } + + describe 'coverage entry' do + describe 'code coverage regexp' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + coverage: '/Code coverage: \d+\.\d+/' }) + end + + it 'includes coverage regexp in build attributes' do + expect(subject) + .to include(coverage_regex: 'Code coverage: \d+\.\d+') + end + end + end + + describe 'retry entry' do + context 'when retry count is specified' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', retry: 1 }) + end + + it 'includes retry count in build options attribute' do + expect(subject[:options]).to include(retry: 1) + end + end + + context 'when retry count is not specified' do + let(:config) do + YAML.dump(rspec: { script: 'rspec' }) + end + + it 'does not persist retry count in the database' do + expect(subject[:options]).not_to have_key(:retry) + end + end + end + + describe 'allow failure entry' do + context 'when job is a manual action' do + context 'when allow_failure is defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + when: 'manual', + allow_failure: false }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + + context 'when allow_failure is not defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + when: 'manual' }) + end + + it 'is allowed to fail' do + expect(subject[:allow_failure]).to be true + end + end + end + + context 'when job is not a manual action' do + context 'when allow_failure is defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + allow_failure: false }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + + context 'when allow_failure is not defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec' }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + end + end + end + + describe '#stage_seeds' do + context 'when no refs policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod' }, + rspec: { stage: 'test', script: 'rspec' }, + spinach: { stage: 'test', script: 'spinach' }) + end + + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'correctly fabricates a stage seeds object' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 2 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.second.stage[:name]).to eq 'deploy' + expect(seeds.first.builds.dig(0, :name)).to eq 'rspec' + expect(seeds.first.builds.dig(1, :name)).to eq 'spinach' + expect(seeds.second.builds.dig(0, :name)).to eq 'production' + end + end + + context 'when refs policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] }, + spinach: { stage: 'test', script: 'spinach', only: ['tags'] }) + end + + let(:pipeline) do + create(:ci_empty_pipeline, ref: 'feature', tag: true) + end + + it 'returns stage seeds only assigned to master to master' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + + context 'when source policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] }, + spinach: { stage: 'test', script: 'spinach', only: ['schedules'] }) + end + + let(:pipeline) do + create(:ci_empty_pipeline, source: :schedule) + end + + it 'returns stage seeds only assigned to schedules' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + + context 'when kubernetes policy is specified' do + let(:config) do + YAML.dump( + spinach: { stage: 'test', script: 'spinach' }, + production: { + stage: 'deploy', + script: 'cap', + only: { kubernetes: 'active' } + } + ) + end + + context 'when kubernetes is active' do + let(:project) { create(:kubernetes_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + + it 'returns seeds for kubernetes dependent job' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 2 + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + expect(seeds.second.builds.dig(0, :name)).to eq 'production' + end + end + + context 'when kubernetes is not active' do + it 'does not return seeds for kubernetes dependent job' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + end + end + + describe "#pipeline_stage_builds" do + let(:type) { 'test' } + + it "returns builds if no branch specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec" } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + + describe 'only' do + it "does not return builds if only has another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["deploy"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0) + end + + it "does not return builds if only has regexp with another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["/^deploy$/"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0) + end + + it "returns builds if only has specified this branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["master"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1) + end + + it "returns builds if only has a list of branches including specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: %w(master deploy) } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1) + end + + it "returns builds if only has a branches keyword specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["branches"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1) + end + + it "does not return builds if only has a tags keyword" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["tags"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0) + end + + it "returns builds if only has special keywords specified and source matches" do + possibilities = [{ keyword: 'pushes', source: 'push' }, + { keyword: 'web', source: 'web' }, + { keyword: 'triggers', source: 'trigger' }, + { keyword: 'schedules', source: 'schedule' }, + { keyword: 'api', source: 'api' }, + { keyword: 'external', source: 'external' }] + + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1) + end + end + + it "does not return builds if only has special keywords specified and source doesn't match" do + possibilities = [{ keyword: 'pushes', source: 'web' }, + { keyword: 'web', source: 'push' }, + { keyword: 'triggers', source: 'schedule' }, + { keyword: 'schedules', source: 'external' }, + { keyword: 'api', source: 'trigger' }, + { keyword: 'external', source: 'api' }] + + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0) + end + end + + it "returns builds if only has current repository path" do + seed_pipeline = pipeline(ref: 'deploy') + + config = YAML.dump({ + before_script: ["pwd"], + rspec: { + script: "rspec", + type: type, + only: ["branches@#{seed_pipeline.project_full_path}"] + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(1) + end + + it "does not return builds if only has different repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["branches@fork"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0) + end + + it "returns build only for specified type" do + config = YAML.dump({ + 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"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "deploy")).size).to eq(2) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "deploy")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "master")).size).to eq(1) + end + + context 'for invalid value' do + let(:config) { { rspec: { script: "rspec", type: "test", only: only } } } + let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + + context 'when it is integer' do + let(:only) { 1 } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:only has to be either an array of conditions or a hash') + end + end + + context 'when it is an array of integers' do + let(:only) { [1, 1] } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:only config should be an array of strings or regexps') + end + end + + context 'when it is invalid regex' do + let(:only) { ["/*invalid/"] } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:only config should be an array of strings or regexps') + end + end + end + end + + describe 'except' do + it "returns builds if except has another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", except: ["deploy"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1) + end + + it "returns builds if except has regexp with another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", except: ["/^deploy$/"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1) + end + + it "does not return builds if except has specified this branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", except: ["master"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0) + end + + it "does not return builds if except has a list of branches including specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: %w(master deploy) } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0) + end + + it "does not return builds if except has a branches keyword specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["branches"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0) + end + + it "returns builds if except has a tags keyword" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["tags"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1) + end + + it "does not return builds if except has special keywords specified and source matches" do + possibilities = [{ keyword: 'pushes', source: 'push' }, + { keyword: 'web', source: 'web' }, + { keyword: 'triggers', source: 'trigger' }, + { keyword: 'schedules', source: 'schedule' }, + { keyword: 'api', source: 'api' }, + { keyword: 'external', source: 'external' }] + + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0) + end + end + + it "returns builds if except has special keywords specified and source doesn't match" do + possibilities = [{ keyword: 'pushes', source: 'web' }, + { keyword: 'web', source: 'push' }, + { keyword: 'triggers', source: 'schedule' }, + { keyword: 'schedules', source: 'external' }, + { keyword: 'api', source: 'trigger' }, + { keyword: 'external', source: 'api' }] + + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1) + end + end + + it "does not return builds if except has current repository path" do + seed_pipeline = pipeline(ref: 'deploy') + + config = YAML.dump({ + before_script: ["pwd"], + rspec: { + script: "rspec", + type: type, + except: ["branches@#{seed_pipeline.project_full_path}"] + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(0) + end + + it "returns builds if except has different repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["branches@fork"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1) + end + + it "returns build except specified type" do + master_pipeline = pipeline(ref: 'master') + test_pipeline = pipeline(ref: 'test') + deploy_pipeline = pipeline(ref: 'deploy') + + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@#{test_pipeline.project_full_path}"] }, + staging: { script: "deploy", type: "deploy", except: ["master"] }, + production: { script: "deploy", type: "deploy", except: ["master@#{master_pipeline.project_full_path}"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds("deploy", deploy_pipeline).size).to eq(2) + expect(config_processor.pipeline_stage_builds("test", test_pipeline).size).to eq(0) + expect(config_processor.pipeline_stage_builds("deploy", master_pipeline).size).to eq(0) + end + + context 'for invalid value' do + let(:config) { { rspec: { script: "rspec", except: except } } } + let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + + context 'when it is integer' do + let(:except) { 1 } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:except has to be either an array of conditions or a hash') + end + end + + context 'when it is an array of integers' do + let(:except) { [1, 1] } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:except config should be an array of strings or regexps') + end + end + + context 'when it is invalid regex' do + let(:except) { ["/*invalid/"] } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:except config should be an array of strings or regexps') + end + end + end + end + end + + describe "Scripts handling" do + let(:config_data) { YAML.dump(config) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data) } + + subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first } + + describe "before_script" do + context "in global context" do + let(:config) do + { + before_script: ["global script"], + test: { script: ["script"] } + } + end + + it "return commands with scripts concencaced" do + expect(subject[:commands]).to eq("global script\nscript") + end + end + + context "overwritten in local context" do + let(:config) do + { + before_script: ["global script"], + test: { before_script: ["local script"], script: ["script"] } + } + end + + it "return commands with scripts concencaced" do + expect(subject[:commands]).to eq("local script\nscript") + end + end + end + + describe "script" do + let(:config) do + { + test: { script: ["script"] } + } + end + + it "return commands with scripts concencaced" do + expect(subject[:commands]).to eq("script") + end + end + + describe "after_script" do + context "in global context" do + let(:config) do + { + after_script: ["after_script"], + test: { script: ["script"] } + } + end + + it "return after_script in options" do + expect(subject[:options][:after_script]).to eq(["after_script"]) + end + end + + context "overwritten in local context" do + let(:config) do + { + after_script: ["local after_script"], + test: { after_script: ["local after_script"], script: ["script"] } + } + end + + it "return after_script in options" do + expect(subject[:options][:after_script]).to eq(["local after_script"]) + end + end + end + end + + describe "Image and service handling" do + context "when extended docker configuration is used" do + it "returns image and service when defined" do + config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, + services: ["mysql", { name: "docker:dind", alias: "docker", + entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }], + before_script: ["pwd"], + rspec: { script: "rspec" } }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "mysql" }, + { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + + it "returns image and service when overridden for job" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "postgresql", alias: "db-pg", + entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }, "docker:dind"], + script: "rspec" } }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }, + { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + end + + context "when etended docker configuration is not used" do + it "returns image and service when defined" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql", "docker:dind"], + before_script: ["pwd"], + rspec: { script: "rspec" } }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.1" }, + services: [{ name: "mysql" }, { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + + it "returns image and service when overridden for job" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.5" }, + services: [{ name: "postgresql" }, { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + end + end + + describe 'Variables' do + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + + subject { config_processor.builds.first[:yaml_variables] } + + context 'when global variables are defined' do + let(:variables) do + { 'VAR1' => 'value1', 'VAR2' => 'value2' } + end + let(:config) do + { + variables: variables, + before_script: ['pwd'], + rspec: { script: 'rspec' } + } + end + + it 'returns global variables' do + expect(subject).to contain_exactly( + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } + ) + end + end + + context 'when job and global variables are defined' do + let(:global_variables) do + { 'VAR1' => 'global1', 'VAR3' => 'global3' } + end + let(:job_variables) do + { 'VAR1' => 'value1', 'VAR2' => 'value2' } + end + let(:config) do + { + before_script: ['pwd'], + variables: global_variables, + rspec: { script: 'rspec', variables: job_variables } + } + end + + it 'returns all unique variables' do + expect(subject).to contain_exactly( + { key: 'VAR3', value: 'global3', public: true }, + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } + ) + end + end + + context 'when job variables are defined' do + let(:config) do + { + before_script: ['pwd'], + rspec: { script: 'rspec', variables: variables } + } + end + + context 'when syntax is correct' do + let(:variables) do + { 'VAR1' => 'value1', 'VAR2' => 'value2' } + end + + it 'returns job variables' do + expect(subject).to contain_exactly( + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } + ) + end + end + + context 'when syntax is incorrect' do + context 'when variables defined but invalid' do + let(:variables) do + %w(VAR1 value1 VAR2 value2) + end + + it 'raises error' do + expect { subject } + .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + /jobs:rspec:variables config should be a hash of key value pairs/) + end + end + + context 'when variables key defined but value not specified' do + let(:variables) do + nil + end + + it 'returns empty array' do + ## + # When variables config is empty, we assume this is a valid + # configuration, see issue #18775 + # + expect(subject).to be_an_instance_of(Array) + expect(subject).to be_empty + end + end + end + end + + context 'when job variables are not defined' do + let(:config) do + { + before_script: ['pwd'], + rspec: { script: 'rspec' } + } + end + + it 'returns empty array' do + expect(subject).to be_an_instance_of(Array) + expect(subject).to be_empty + end + end + end + + describe "When" do + %w(on_success on_failure always).each do |when_state| + it "returns #{when_state} when defined" do + config = YAML.dump({ + rspec: { script: "rspec", when: when_state } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master")) + expect(builds.size).to eq(1) + expect(builds.first[:when]).to eq(when_state) + end + end + end + + describe 'cache' do + context 'when cache definition has unknown keys' do + it 'raises relevant validation error' do + config = YAML.dump( + { cache: { untracked: true, invalid: 'key' }, + rspec: { script: 'rspec' } }) + + expect { Gitlab::Ci::YamlProcessor.new(config) }.to raise_error( + Gitlab::Ci::YamlProcessor::ValidationError, + 'cache config contains unknown keys: invalid' + ) + end + end + + it "returns cache when defined globally" do + config = YAML.dump({ + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, + rspec: { + script: "rspec" + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq( + paths: ["logs/", "binaries/"], + untracked: true, + key: 'key', + policy: 'pull-push' + ) + end + + it "returns cache when defined in a job" do + config = YAML.dump({ + rspec: { + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, + script: "rspec" + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq( + paths: ["logs/", "binaries/"], + untracked: true, + key: 'key', + policy: 'pull-push' + ) + end + + it "overwrite cache when defined for a job and globally" do + config = YAML.dump({ + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, + rspec: { + script: "rspec", + cache: { paths: ["test/"], untracked: false, key: 'local' } + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq( + paths: ["test/"], + untracked: false, + key: 'local', + policy: 'pull-push' + ) + end + end + + describe "Artifacts" do + it "returns artifacts when defined" do + config = YAML.dump({ + image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { + artifacts: { + paths: ["logs/", "binaries/"], + untracked: true, + name: "custom_name", + expire_in: "7d" + }, + script: "rspec" + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.1" }, + services: [{ name: "mysql" }], + artifacts: { + name: "custom_name", + paths: ["logs/", "binaries/"], + untracked: true, + expire_in: "7d" + } + }, + when: "on_success", + allow_failure: false, + environment: nil, + yaml_variables: [] + }) + end + + %w[on_success on_failure always].each do |when_state| + it "returns artifacts for when #{when_state} defined" do + config = YAML.dump({ + rspec: { + script: "rspec", + artifacts: { paths: ["logs/", "binaries/"], when: when_state } + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master")) + expect(builds.size).to eq(1) + expect(builds.first[:options][:artifacts][:when]).to eq(when_state) + end + end + end + + describe '#environment' do + let(:config) do + { + deploy_to_production: { stage: 'deploy', script: 'test', environment: environment } + } + end + + let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + let(:builds) { processor.pipeline_stage_builds('deploy', pipeline(ref: 'master')) } + + context 'when a production environment is specified' do + let(:environment) { 'production' } + + it 'does return production' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to eq(environment) + expect(builds.first[:options]).to include(environment: { name: environment, action: "start" }) + end + end + + context 'when hash is specified' do + let(:environment) do + { name: 'production', + url: 'http://production.gitlab.com' } + end + + it 'does return production and URL' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to eq(environment[:name]) + expect(builds.first[:options]).to include(environment: environment) + end + + context 'the url has a port as variable' do + let(:environment) do + { name: 'production', + url: 'http://production.gitlab.com:$PORT' } + end + + it 'allows a variable for the port' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to eq(environment[:name]) + expect(builds.first[:options]).to include(environment: environment) + end + end + end + + context 'when no environment is specified' do + let(:environment) { nil } + + it 'does return nil environment' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to be_nil + end + end + + context 'is not a string' do + let(:environment) { 1 } + + it 'raises error' do + expect { builds }.to raise_error( + 'jobs:deploy_to_production:environment config should be a hash or a string') + end + end + + context 'is not a valid string' do + let(:environment) { 'production:staging' } + + it 'raises error' do + expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}") + end + end + + context 'when on_stop is specified' do + let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } } + let(:config) { { review: review, close_review: close_review }.compact } + + context 'with matching job' do + let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } } + + it 'does return a list of builds' do + expect(builds.size).to eq(2) + expect(builds.first[:environment]).to eq('review') + end + end + + context 'without matching job' do + let(:close_review) { nil } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review is not defined') + end + end + + context 'with close job without environment' do + let(:close_review) { { stage: 'deploy', script: 'test' } } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined') + end + end + + context 'with close job for different environment' do + let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review have different environment name') + end + end + + context 'with close job without stop action' do + let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined') + end + end + end + end + + describe "Dependencies" do + let(:config) do + { + build1: { stage: 'build', script: 'test' }, + build2: { stage: 'build', script: 'test' }, + test1: { stage: 'test', script: 'test', dependencies: dependencies }, + test2: { stage: 'test', script: 'test' }, + deploy: { stage: 'test', script: 'test' } + } + end + + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + + context 'no dependencies' do + let(:dependencies) { } + + it { expect { subject }.not_to raise_error } + end + + context 'dependencies to builds' do + let(:dependencies) { %w(build1 build2) } + + it { expect { subject }.not_to raise_error } + end + + context 'dependencies to builds defined as symbols' do + let(:dependencies) { [:build1, :build2] } + + it { expect { subject }.not_to raise_error } + end + + context 'undefined dependency' do + let(:dependencies) { ['undefined'] } + + it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') } + end + + context 'dependencies to deploy' do + let(:dependencies) { ['deploy'] } + + it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') } + end + end + + describe "Hidden jobs" do + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } + subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")) } + + shared_examples 'hidden_job_handling' do + it "doesn't create jobs that start with dot" do + expect(subject.size).to eq(1) + expect(subject.first).to eq({ + stage: "test", + stage_idx: 1, + name: "normal_job", + commands: "test", + coverage_regex: nil, + tag_list: [], + options: { + script: ["test"] + }, + when: "on_success", + allow_failure: false, + environment: nil, + yaml_variables: [] + }) + end + end + + context 'when hidden job have a script definition' do + let(:config) do + YAML.dump({ + '.hidden_job' => { image: 'ruby:2.1', script: 'test' }, + 'normal_job' => { script: 'test' } + }) + end + + it_behaves_like 'hidden_job_handling' + end + + context "when hidden job doesn't have a script definition" do + let(:config) do + YAML.dump({ + '.hidden_job' => { image: 'ruby:2.1' }, + 'normal_job' => { script: 'test' } + }) + end + + it_behaves_like 'hidden_job_handling' + end + end + + describe "YAML Alias/Anchor" do + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } + subject { config_processor.pipeline_stage_builds("build", pipeline(ref: "master")) } + + shared_examples 'job_templates_handling' do + it "is correctly supported for jobs" do + expect(subject.size).to eq(2) + expect(subject.first).to eq({ + stage: "build", + stage_idx: 0, + name: "job1", + commands: "execute-script-for-job", + coverage_regex: nil, + tag_list: [], + options: { + script: ["execute-script-for-job"] + }, + when: "on_success", + allow_failure: false, + environment: nil, + yaml_variables: [] + }) + expect(subject.second).to eq({ + stage: "build", + stage_idx: 0, + name: "job2", + commands: "execute-script-for-job", + coverage_regex: nil, + tag_list: [], + options: { + script: ["execute-script-for-job"] + }, + when: "on_success", + allow_failure: false, + environment: nil, + yaml_variables: [] + }) + end + end + + context 'when template is a job' do + let(:config) do + <<EOT +job1: &JOBTMPL + stage: build + script: execute-script-for-job + +job2: *JOBTMPL +EOT + end + + it_behaves_like 'job_templates_handling' + end + + context 'when template is a hidden job' do + let(:config) do + <<EOT +.template: &JOBTMPL + stage: build + script: execute-script-for-job + +job1: *JOBTMPL + +job2: *JOBTMPL +EOT + end + + it_behaves_like 'job_templates_handling' + end + + context 'when job adds its own keys to a template definition' do + let(:config) do + <<EOT +.template: &JOBTMPL + stage: build + +job1: + <<: *JOBTMPL + script: execute-script-for-job + +job2: + <<: *JOBTMPL + script: execute-script-for-job +EOT + end + + it_behaves_like 'job_templates_handling' + end + end + + describe "Error handling" do + it "fails to parse YAML" do + expect {Gitlab::Ci::YamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError) + end + + it "indicates that object is invalid" do + expect {Gitlab::Ci::YamlProcessor.new("invalid_yaml")}.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) + end + + it "returns errors if tags parameter is invalid" do + config = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings") + end + + it "returns errors if before_script parameter is invalid" do + config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "before_script config should be an array of strings") + end + + it "returns errors if job before_script parameter is not an array of strings" do + config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings") + end + + it "returns errors if after_script parameter is invalid" do + config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "after_script config should be an array of strings") + end + + it "returns errors if job after_script parameter is not an array of strings" do + config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings") + end + + it "returns errors if image parameter is invalid" do + config = YAML.dump({ image: ["test"], rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "image config should be a hash or a string") + end + + it "returns errors if job name is blank" do + config = YAML.dump({ '' => { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:job name can't be blank") + end + + it "returns errors if job name is non-string" do + config = YAML.dump({ 10 => { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:10 name should be a symbol") + end + + it "returns errors if job image parameter is invalid" do + config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string") + end + + it "returns errors if services parameter is not an array" do + config = YAML.dump({ services: "test", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services config should be a array") + end + + it "returns errors if services parameter is not an array of strings" do + config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") + end + + it "returns errors if job services parameter is not an array" do + config = YAML.dump({ rspec: { script: "test", services: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services config should be a array") + end + + it "returns errors if job services parameter is not an array of strings" do + config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") + end + + it "returns error if job configuration is invalid" do + config = YAML.dump({ extra: "bundle update" }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra config should be a hash") + end + + it "returns errors if services configuration is not correct" do + config = YAML.dump({ extra: { script: 'rspec', services: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra:services config should be a array") + end + + it "returns errors if there are no jobs defined" do + config = YAML.dump({ before_script: ["bundle update"] }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job") + end + + it "returns errors if there are no visible jobs defined" do + config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job") + end + + it "returns errors if job allow_failure parameter is not an boolean" do + config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value") + end + + it "returns errors if job stage is not a string" do + config = YAML.dump({ rspec: { script: "test", type: 1 } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:type config should be a string") + end + + it "returns errors if job stage is not a pre-defined stage" do + config = YAML.dump({ rspec: { script: "test", type: "acceptance" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") + end + + it "returns errors if job stage is not a defined stage" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") + end + + it "returns errors if stages is not an array" do + config = YAML.dump({ stages: "test", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings") + end + + it "returns errors if stages is not an array of strings" do + config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings") + end + + it "returns errors if variables is not a map" do + config = YAML.dump({ variables: "test", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs") + end + + it "returns errors if variables is not a map of key-value strings" do + config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs") + end + + it "returns errors if job when is not on_success, on_failure or always" do + config = YAML.dump({ rspec: { script: "test", when: 1 } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual") + end + + it "returns errors if job artifacts:name is not an a string" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string") + end + + it "returns errors if job artifacts:when is not an a predefined value" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always") + end + + it "returns errors if job artifacts:expire_in is not an a string" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") + end + + it "returns errors if job artifacts:expire_in is not an a valid duration" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") + end + + it "returns errors if job artifacts:untracked is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value") + end + + it "returns errors if job artifacts:paths is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings") + end + + it "returns errors if cache:untracked is not an array of strings" do + config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:untracked config should be a boolean value") + end + + it "returns errors if cache:paths is not an array of strings" do + config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:paths config should be an array of strings") + end + + it "returns errors if cache:key is not a string" do + config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:key config should be a string or symbol") + end + + it "returns errors if job cache:key is not an a string" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol") + end + + it "returns errors if job cache:untracked is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value") + end + + it "returns errors if job cache:paths is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings") + end + + it "returns errors if job dependencies is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") + end + end + + describe "Validate configuration templates" do + templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml") + + templates.each do |file| + it "does not return errors for #{file}" do + file = File.read(file) + + expect { Gitlab::Ci::YamlProcessor.new(file) }.not_to raise_error + end + end + end + + describe "#validation_message" do + context "when the YAML could not be parsed" do + it "returns an error about invalid configutaion" do + content = YAML.dump("invalid: yaml: test") + + expect(Gitlab::Ci::YamlProcessor.validation_message(content)) + .to eq "Invalid configuration format" + end + end + + context "when the tags parameter is invalid" do + it "returns an error about invalid tags" do + content = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) + + expect(Gitlab::Ci::YamlProcessor.validation_message(content)) + .to eq "jobs:rspec tags should be an array of strings" + end + end + + context "when YAML content is empty" do + it "returns an error about missing content" do + expect(Gitlab::Ci::YamlProcessor.validation_message('')) + .to eq "Please provide content of .gitlab-ci.yml" + end + end + + context "when the YAML is valid" do + it "does not return any errors" do + content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + + expect(Gitlab::Ci::YamlProcessor.validation_message(content)).to be_nil + end + end + end + + def pipeline(**attributes) + build_stubbed(:ci_empty_pipeline, **attributes) + end + end + end +end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 15012495247..ef7d766a13d 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -254,6 +254,46 @@ describe Gitlab::ClosingIssueExtractor do expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Implement: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Implements: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Implemented: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Implementing: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "implement: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "implements: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "implemented: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "implementing: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + context 'with an external issue tracker reference' do it 'extracts the referenced issue' do jira_project = create(:jira_project, name: 'JIRA_EXT1') @@ -347,10 +387,10 @@ describe Gitlab::ClosingIssueExtractor do end it "fetches cross-project URL references" do - message = "Closes #{urls.project_issue_url(issue2.project, issue2)} and #{reference}" + message = "Closes #{urls.project_issue_url(issue2.project, issue2)}, #{reference} and #{urls.project_issue_url(other_issue.project, other_issue)}" expect(subject.closed_by_message(message)) - .to match_array([issue, issue2]) + .to match_array([issue, issue2, other_issue]) end it "ignores invalid cross-project URL references" do diff --git a/spec/lib/gitlab/conflict/file_collection_spec.rb b/spec/lib/gitlab/conflict/file_collection_spec.rb index a4d7628b03a..5944ce8049a 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 do let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start') } - let(:file_collection) { described_class.read_only(merge_request) } + let(:file_collection) { described_class.new(merge_request) } describe '#files' do it 'returns an array of Conflict::Files' do diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index 5356e9742b4..bf981d2f6f6 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -8,9 +8,10 @@ describe Gitlab::Conflict::File do let(:our_commit) { rugged.branches['conflict-resolvable'].target } let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) } let(:index) { rugged.merge_commits(our_commit, their_commit) } - let(:conflict) { index.conflicts.last } - let(:merge_file_result) { index.merge_file('files/ruby/regex.rb') } - let(:conflict_file) { described_class.new(merge_file_result, conflict, merge_request: merge_request) } + let(:rugged_conflict) { index.conflicts.last } + let(:raw_conflict_content) { index.merge_file('files/ruby/regex.rb')[:data] } + let(:raw_conflict_file) { Gitlab::Git::Conflict::File.new(repository, our_commit.oid, rugged_conflict, raw_conflict_content) } + let(:conflict_file) { described_class.new(raw_conflict_file, merge_request: merge_request) } describe '#resolve_lines' do let(:section_keys) { conflict_file.sections.map { |section| section[:id] }.compact } @@ -48,18 +49,18 @@ describe Gitlab::Conflict::File do end end - it 'raises MissingResolution when passed a hash without resolutions for all sections' do + it 'raises ResolutionError when passed a hash without resolutions for all sections' do empty_hash = section_keys.map { |key| [key, nil] }.to_h invalid_hash = section_keys.map { |key| [key, 'invalid'] }.to_h expect { conflict_file.resolve_lines({}) } - .to raise_error(Gitlab::Conflict::File::MissingResolution) + .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError) expect { conflict_file.resolve_lines(empty_hash) } - .to raise_error(Gitlab::Conflict::File::MissingResolution) + .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError) expect { conflict_file.resolve_lines(invalid_hash) } - .to raise_error(Gitlab::Conflict::File::MissingResolution) + .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError) end end @@ -144,7 +145,7 @@ describe Gitlab::Conflict::File do end context 'with an example file' do - let(:file) do + let(:raw_conflict_content) do <<FILE # Ensure there is no match line header here def username_regexp @@ -220,7 +221,6 @@ end FILE end - let(:conflict_file) { described_class.new({ data: file }, conflict, merge_request: merge_request) } let(:sections) { conflict_file.sections } it 'sets the correct match line headers' do diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index d57ffcae8e1..492659a82b0 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::CurrentSettings do it 'falls back to DB if Redis returns an empty value' do expect(ApplicationSetting).to receive(:cached).and_return(nil) - expect(ApplicationSetting).to receive(:last).and_call_original + expect(ApplicationSetting).to receive(:last).and_call_original.twice expect(current_application_settings).to be_a(ApplicationSetting) end diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index cb430b47463..befdc18d1aa 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -47,7 +47,7 @@ describe Gitlab::DataBuilder::Push do include_examples 'deprecated repository hook data' it 'does not raise an error when given nil commits' do - expect { described_class.build(spy, spy, spy, spy, spy, nil) } + expect { described_class.build(spy, spy, spy, spy, 'refs/tags/v1.1.0', nil) } .not_to raise_error end end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 1bcdc369c44..3c8350b3aad 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -914,4 +914,126 @@ describe Gitlab::Database::MigrationHelpers do .to raise_error(RuntimeError, /Your database user is not allowed/) end end + + describe '#bulk_queue_background_migration_jobs_by_range', :sidekiq do + context 'when the model has an ID column' do + let!(:id1) { create(:user).id } + let!(:id2) { create(:user).id } + let!(:id3) { create(:user).id } + + before do + User.class_eval do + include EachBatch + end + end + + context 'with enough rows to bulk queue jobs more than once' do + before do + stub_const('Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE', 1) + end + + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2) + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]]) + expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]]) + end + end + + it 'queues jobs in groups of buffer size 1' do + expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]]]) + expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id3, id3]]]) + + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2) + end + end + + context 'with not enough rows to bulk queue jobs more than once' do + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2) + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]]) + expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]]) + end + end + + it 'queues jobs in bulk all at once (big buffer size)' do + expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]], + ['FooJob', [id3, id3]]]) + + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2) + end + end + + context 'without specifying batch_size' do + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob') + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]]) + end + end + end + end + + context "when the model doesn't have an ID column" do + it 'raises error (for now)' do + expect do + model.bulk_queue_background_migration_jobs_by_range(ProjectAuthorization, 'FooJob') + end.to raise_error(StandardError, /does not have an ID/) + end + end + end + + describe '#queue_background_migration_jobs_by_range_at_intervals', :sidekiq do + context 'when the model has an ID column' do + let!(:id1) { create(:user).id } + let!(:id2) { create(:user).id } + let!(:id3) { create(:user).id } + + around do |example| + Timecop.freeze { example.run } + end + + before do + User.class_eval do + include EachBatch + end + end + + context 'with batch_size option' do + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds, batch_size: 2) + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]]) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]]) + expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f) + end + end + end + + context 'without batch_size option' do + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds) + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]]) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f) + end + end + end + end + + context "when the model doesn't have an ID column" do + it 'raises error (for now)' do + expect do + model.queue_background_migration_jobs_by_range_at_intervals(ProjectAuthorization, 'FooJob', 10.seconds) + end.to raise_error(StandardError, /does not have an ID/) + end + 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 index 90aa4f63dd5..596cc435bd9 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb @@ -229,7 +229,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca end end - describe '#track_rename', redis: true do + describe '#track_rename', :redis do it 'tracks a rename in redis' do key = 'rename:FakeRenameReservedPathMigrationV1:namespace' @@ -246,7 +246,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca end end - describe '#reverts_for_type', redis: true do + describe '#reverts_for_type', :redis do it 'yields for each tracked rename' do subject.track_rename('project', 'old_path', 'new_path') subject.track_rename('project', 'old_path2', 'new_path2') 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 index 32ac0b88a9b..1143182531f 100644 --- 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 @@ -241,7 +241,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : end end - describe '#revert_renames', redis: true do + describe '#revert_renames', :redis do it 'renames the routes back to the previous values' do project = create(:project, :repository, path: 'a-project', namespace: namespace) subject.rename_namespace(namespace) 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 index 595e06a9748..8922370b0a0 100644 --- 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 @@ -115,7 +115,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr end end - describe '#revert_renames', redis: true do + describe '#revert_renames', :redis do it 'renames the routes back to the previous values' do subject.rename_project(project) diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 5fa94999d25..7aeb85b8f5a 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -256,4 +256,26 @@ describe Gitlab::Database do expect(described_class.false_value).to eq 0 end end + + describe '#sanitize_timestamp' do + let(:max_timestamp) { Time.at((1 << 31) - 1) } + + subject { described_class.sanitize_timestamp(timestamp) } + + context 'with a timestamp smaller than MAX_TIMESTAMP_VALUE' do + let(:timestamp) { max_timestamp - 10.years } + + it 'returns the given timestamp' do + expect(subject).to eq(timestamp) + end + end + + context 'with a timestamp larger than MAX_TIMESTAMP_VALUE' do + let(:timestamp) { max_timestamp + 1.second } + + it 'returns MAX_TIMESTAMP_VALUE' do + expect(subject).to eq(max_timestamp) + end + end + end end diff --git a/spec/lib/gitlab/diff/diff_refs_spec.rb b/spec/lib/gitlab/diff/diff_refs_spec.rb index c73708d90a8..f9bfb4c469e 100644 --- a/spec/lib/gitlab/diff/diff_refs_spec.rb +++ b/spec/lib/gitlab/diff/diff_refs_spec.rb @@ -3,6 +3,61 @@ require 'spec_helper' describe Gitlab::Diff::DiffRefs do let(:project) { create(:project, :repository) } + describe '#==' do + let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } + subject { commit.diff_refs } + + context 'when shas are missing' do + let(:other) { described_class.new(base_sha: subject.base_sha, start_sha: subject.start_sha, head_sha: nil) } + + it 'returns false' do + expect(subject).not_to eq(other) + end + end + + context 'when shas are equal' do + let(:other) { described_class.new(base_sha: subject.base_sha, start_sha: subject.start_sha, head_sha: subject.head_sha) } + + it 'returns true' do + expect(subject).to eq(other) + end + end + + context 'when shas are unequal' do + let(:other) { described_class.new(base_sha: subject.base_sha, start_sha: subject.start_sha, head_sha: subject.head_sha.reverse) } + + it 'returns false' do + expect(subject).not_to eq(other) + end + end + + context 'when shas are truncated' do + context 'when sha prefixes are too short' do + let(:other) { described_class.new(base_sha: subject.base_sha[0, 4], start_sha: subject.start_sha[0, 4], head_sha: subject.head_sha[0, 4]) } + + it 'returns false' do + expect(subject).not_to eq(other) + end + end + + context 'when sha prefixes are equal' do + let(:other) { described_class.new(base_sha: subject.base_sha[0, 10], start_sha: subject.start_sha[0, 10], head_sha: subject.head_sha[0, 10]) } + + it 'returns true' do + expect(subject).to eq(other) + end + end + + context 'when sha prefixes are unequal' do + let(:other) { described_class.new(base_sha: subject.base_sha[0, 10], start_sha: subject.start_sha[0, 10], head_sha: subject.head_sha[0, 10].reverse) } + + it 'returns false' do + expect(subject).not_to eq(other) + end + end + end + end + describe '#compare_in' do context 'with diff refs for the initial commit' do let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } diff --git a/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb new file mode 100644 index 00000000000..2f99febe04e --- /dev/null +++ b/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Gitlab::Diff::Formatters::ImageFormatter do + it_behaves_like "position formatter" do + let(:base_attrs) do + { + base_sha: 123, + start_sha: 456, + head_sha: 789, + old_path: 'old_image.png', + new_path: 'new_image.png', + position_type: 'image' + } + end + + let(:attrs) do + base_attrs.merge(width: 100, height: 100, x: 1, y: 2) + end + end +end diff --git a/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb new file mode 100644 index 00000000000..897dc917f6a --- /dev/null +++ b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Gitlab::Diff::Formatters::TextFormatter do + let!(:base) do + { + base_sha: 123, + start_sha: 456, + head_sha: 789, + old_path: 'old_path.txt', + new_path: 'new_path.txt' + } + end + + let!(:complete) do + base.merge(old_line: 1, new_line: 2) + end + + it_behaves_like "position formatter" do + let(:base_attrs) { base } + + let(:attrs) { complete } + end + + # Specific text formatter examples + let!(:formatter) { described_class.new(attrs) } + + describe '#line_age' do + subject { formatter.line_age } + + context ' when there is only new_line' do + let(:attrs) { base.merge(new_line: 1) } + + it { is_expected.to eq('new') } + end + + context ' when there is only old_line' do + let(:attrs) { base.merge(old_line: 1) } + + it { is_expected.to eq('old') } + end + end +end diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index 8af49ed50ff..80c8c189665 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -143,4 +143,21 @@ eos it { expect(parser.parse([])).to eq([]) } it { expect(parser.parse(nil)).to eq([]) } end + + describe 'tolerates special diff markers in a content' do + it "counts lines correctly" do + diff = <<~END + --- a/test + +++ b/test + @@ -1,2 +1,2 @@ + +ipsum + +++ b + -ipsum + END + + lines = parser.parse(diff.lines).to_a + + expect(lines.size).to eq(3) + end + end end diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index d4a2a852c12..677eb373d22 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Diff::Position do let(:project) { create(:project, :repository) } - describe "position for an added file" do + describe "position for an added text file" do let(:commit) { project.commit("2ea1f3dec713d940208fb5ce4a38765ecb5d3f73") } subject do @@ -40,13 +40,38 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0) + line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 0) expect(subject.line_code(project.repository)).to eq(line_code) end end end + describe "position for an added image file" do + let(:commit) { project.commit("33f3729a45c02fc67d00adb1b8bca394b0e761d9") } + + subject do + described_class.new( + old_path: "files/images/6049019_460s.jpg", + new_path: "files/images/6049019_460s.jpg", + width: 100, + height: 100, + x: 1, + y: 100, + diff_refs: commit.diff_refs, + position_type: "image" + ) + end + + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.new_file?).to be true + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + describe "position for a changed file" do let(:commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") } @@ -83,7 +108,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 15) + line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 15) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -124,7 +149,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line) + line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, subject.old_line) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -164,7 +189,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 13, subject.old_line) + line_code = Gitlab::Git.diff_line_code(subject.file_path, 13, subject.old_line) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -208,7 +233,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 5) + line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 5) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -249,7 +274,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line) + line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, subject.old_line) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -289,7 +314,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 4, subject.old_line) + line_code = Gitlab::Git.diff_line_code(subject.file_path, 4, subject.old_line) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -332,13 +357,50 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 0, subject.old_line) + line_code = Gitlab::Git.diff_line_code(subject.file_path, 0, subject.old_line) expect(subject.line_code(project.repository)).to eq(line_code) end end end + describe "position for a missing ref" do + let(:diff_refs) do + Gitlab::Diff::DiffRefs.new( + base_sha: "not_existing_sha", + head_sha: "existing_sha" + ) + end + + subject do + described_class.new( + old_path: "files/ruby/feature.rb", + new_path: "files/ruby/feature.rb", + old_line: 3, + new_line: nil, + diff_refs: diff_refs + ) + end + + describe "#diff_file" do + it "does not raise exception" do + expect { subject.diff_file(project.repository) }.not_to raise_error + end + end + + describe "#diff_line" do + it "does not raise exception" do + expect { subject.diff_line(project.repository) }.not_to raise_error + end + end + + describe "#line_code" do + it "does not raise exception" do + expect { subject.line_code(project.repository) }.not_to raise_error + end + end + end + describe "position for a file in the initial commit" do let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") } @@ -374,7 +436,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0) + line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 0) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -422,34 +484,100 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 0, subject.old_line) + line_code = Gitlab::Git.diff_line_code(subject.file_path, 0, subject.old_line) expect(subject.line_code(project.repository)).to eq(line_code) end end end - describe "#to_json" do - let(:hash) do - { + describe '#==' do + let(:commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") } + + subject do + described_class.new( old_path: "files/ruby/popen.rb", new_path: "files/ruby/popen.rb", old_line: nil, new_line: 14, - base_sha: nil, - head_sha: nil, - start_sha: nil - } + diff_refs: commit.diff_refs + ) + end + + context 'when positions are equal' do + let(:other) { described_class.new(subject.to_h) } + + it 'returns true' do + expect(subject).to eq(other) + end + end + + context 'when positions are equal, except for truncated shas' do + let(:other) { described_class.new(subject.to_h.merge(start_sha: subject.start_sha[0, 10])) } + + it 'returns true' do + expect(subject).to eq(other) + end end - let(:diff_position) { described_class.new(hash) } + context 'when positions are unequal' do + let(:other) { described_class.new(subject.to_h.merge(start_sha: subject.start_sha.reverse)) } + + it 'returns false' do + expect(subject).not_to eq(other) + end + end + end + + describe "#to_json" do + shared_examples "diff position json" do + it "returns the position as JSON" do + expect(JSON.parse(diff_position.to_json)).to eq(hash.stringify_keys) + end + + it "works when nested under another hash" do + expect(JSON.parse(JSON.generate(pos: diff_position))).to eq('pos' => hash.stringify_keys) + end + end + + context "for text positon" do + let(:hash) do + { + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 14, + base_sha: nil, + head_sha: nil, + start_sha: nil, + position_type: "text" + } + end + + let(:diff_position) { described_class.new(hash) } - it "returns the position as JSON" do - expect(JSON.parse(diff_position.to_json)).to eq(hash.stringify_keys) + it_behaves_like "diff position json" end - it "works when nested under another hash" do - expect(JSON.parse(JSON.generate(pos: diff_position))).to eq('pos' => hash.stringify_keys) + context "for image positon" do + let(:hash) do + { + old_path: "files/any.img", + new_path: "files/any.img", + base_sha: nil, + head_sha: nil, + start_sha: nil, + width: 100, + height: 100, + x: 1, + y: 100, + position_type: "image" + } + end + + let(:diff_position) { described_class.new(hash) } + + it_behaves_like "diff position json" end end end diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 8beebc10040..e5138705443 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -71,6 +71,10 @@ describe Gitlab::Diff::PositionTracer do Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id) end + def text_position_attrs + [:old_line, :new_line] + end + def position(attrs = {}) attrs.reverse_merge!( diff_refs: old_diff_refs @@ -91,7 +95,11 @@ describe Gitlab::Diff::PositionTracer do expect(new_position.diff_refs).to eq(new_diff_refs) attrs.each do |attr, value| - expect(new_position.send(attr)).to eq(value) + if text_position_attrs.include?(attr) + expect(new_position.formatter.send(attr)).to eq(value) + else + expect(new_position.send(attr)).to eq(value) + end end end end @@ -110,7 +118,11 @@ describe Gitlab::Diff::PositionTracer do expect(change_position.diff_refs).to eq(change_diff_refs) attrs.each do |attr, value| - expect(change_position.send(attr)).to eq(value) + if text_position_attrs.include?(attr) + expect(change_position.formatter.send(attr)).to eq(value) + else + expect(change_position.send(attr)).to eq(value) + end end end end @@ -1761,17 +1773,9 @@ describe Gitlab::Diff::PositionTracer do let(:merge_commit) do update_file_again_commit - committer = repository.user_to_committer(current_user) - - options = { - message: "Merge branches", - author: committer, - committer: committer - } - merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project) - repository.merge(current_user, merge_request.diff_head_sha, merge_request, options) + repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches") project.commit(branch_name) end diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index 8b14b227e65..9151c66afb3 100644 --- a/spec/lib/gitlab/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -6,6 +6,9 @@ describe Gitlab::EncodingHelper do describe '#encode!' do [ + ["nil", nil, nil], + ["empty string", "".encode("ASCII-8BIT"), "".encode("UTF-8")], + ["invalid utf-8 encoded string", "my bad string\xE5".force_encoding("UTF-8"), "my bad string"], [ 'leaves ascii only string as is', 'ascii only string', @@ -81,6 +84,9 @@ describe Gitlab::EncodingHelper do describe '#encode_utf8' do [ + ["nil", nil, nil], + ["empty string", "".encode("ASCII-8BIT"), "".encode("UTF-8")], + ["invalid utf-8 encoded string", "my bad string\xE5".force_encoding("UTF-8"), "my bad stringå"], [ "encodes valid utf8 encoded string to utf8", "λ, λ, λ".encode("UTF-8"), @@ -95,12 +101,18 @@ describe Gitlab::EncodingHelper do "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") + ], + [ + # Test case from https://gitlab.com/gitlab-org/gitlab-ce/issues/39227 + "Equifax branch name", + "refs/heads/Equifax".encode("UTF-8"), + "refs/heads/Equifax".encode("UTF-8") ] ].each do |description, test_string, xpect| it description do - r = ext_class.encode_utf8(test_string.force_encoding('UTF-8')) + r = ext_class.encode_utf8(test_string) expect(r).to eq(xpect) - expect(r.encoding.name).to eq('UTF-8') + expect(r.encoding.name).to eq('UTF-8') if xpect end end diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb index c1ed47cf64a..7322a326b01 100644 --- a/spec/lib/gitlab/exclusive_lease_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -47,6 +47,18 @@ describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do end end + describe '.get_uuid' do + it 'gets the uuid if lease with the key associated exists' do + uuid = described_class.new(unique_key, timeout: 3600).try_obtain + + expect(described_class.get_uuid(unique_key)).to eq(uuid) + end + + it 'returns false if the lease does not exist' do + expect(described_class.get_uuid(unique_key)).to be false + end + end + describe '.cancel' do it 'can cancel a lease' do uuid = new_lease(unique_key) diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb index 695fd6f8573..8e524f9b05a 100644 --- a/spec/lib/gitlab/file_detector_spec.rb +++ b/spec/lib/gitlab/file_detector_spec.rb @@ -18,6 +18,10 @@ describe Gitlab::FileDetector do expect(described_class.type_of('README.md')).to eq(:readme) end + it 'returns nil for a README file in a directory' do + expect(described_class.type_of('foo/README.md')).to be_nil + end + it 'returns the type of a changelog file' do %w(CHANGELOG HISTORY CHANGES NEWS).each do |file| expect(described_class.type_of(file)).to eq(:changelog) @@ -52,6 +56,14 @@ describe Gitlab::FileDetector do end end + it 'returns the type of an issue template' do + expect(described_class.type_of('.gitlab/issue_templates/foo.md')).to eq(:issue_template) + end + + it 'returns the type of a merge request template' do + expect(described_class.type_of('.gitlab/merge_request_templates/foo.md')).to eq(:merge_request_template) + end + it 'returns nil for an unknown file' do expect(described_class.type_of('foo.txt')).to be_nil end diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index a3d323fe28a..7dc06c90078 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -1,11 +1,14 @@ require 'spec_helper' describe Gitlab::Gfm::ReferenceRewriter do - let(:text) { 'some text' } - let(:old_project) { create(:project, name: 'old-project') } - let(:new_project) { create(:project, name: 'new-project') } + let(:group) { create(:group) } + let(:old_project) { create(:project, name: 'old-project', group: group) } + let(:new_project) { create(:project, name: 'new-project', group: group) } let(:user) { create(:user) } + let(:old_project_ref) { old_project.to_reference(new_project) } + let(:text) { 'some text' } + before do old_project.team << [user, :reporter] end @@ -39,7 +42,7 @@ describe Gitlab::Gfm::ReferenceRewriter do it { is_expected.not_to include merge_request.to_reference(new_project) } end - context 'description ambigous elements' do + context 'rewrite ambigous references' do context 'url' do let(:url) { 'http://gitlab.com/#1' } let(:text) { "This references #1, but not #{url}" } @@ -66,23 +69,21 @@ describe Gitlab::Gfm::ReferenceRewriter do context 'description with project labels' do let!(:label) { create(:label, id: 123, name: 'test', project: old_project) } - let(:project_ref) { old_project.to_reference(new_project) } context 'label referenced by id' do let(:text) { '#1 and ~123' } - it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} } + it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~123} } end context 'label referenced by text' do let(:text) { '#1 and ~"test"' } - it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} } + it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~123} } end end context 'description with group labels' do let(:old_group) { create(:group) } let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) } - let(:project_ref) { old_project.to_reference(new_project) } before do old_project.update(namespace: old_group) @@ -90,21 +91,53 @@ describe Gitlab::Gfm::ReferenceRewriter do context 'label referenced by id' do let(:text) { '#1 and ~321' } - it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} } + it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~321} } end context 'label referenced by text' do let(:text) { '#1 and ~"group label"' } - it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} } + it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~321} } end end end + end + + context 'reference contains project milestone' do + let!(:milestone) do + create(:milestone, title: '9.0', project: old_project) + end + + let(:text) { 'milestone: %"9.0"' } + + it { is_expected.to eq %Q[milestone: #{old_project_ref}%"9.0"] } + end + + context 'when referring to group milestone' do + let!(:milestone) do + create(:milestone, title: '10.0', group: group) + end + + let(:text) { 'milestone %"10.0"' } + + it { is_expected.to eq text } + end + + context 'when referable has a nil reference' do + before do + create(:milestone, title: '9.0', project: old_project) + + allow_any_instance_of(Milestone) + .to receive(:to_reference) + .and_return(nil) + end - context 'reference contains milestone' do - let(:milestone) { create(:milestone) } - let(:text) { "milestone ref: #{milestone.to_reference}" } + let(:text) { 'milestone: %"9.0"' } - it { is_expected.to eq text } + it 'raises an error that should be fixed' do + expect { subject }.to raise_error( + described_class::RewriteError, + 'Unspecified reference detected for Milestone' + ) end end end diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index 465c2012b05..793228701cf 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -73,7 +73,7 @@ describe Gitlab::Git::Blame, seed_helper: true do it_behaves_like 'blaming a file' end - context 'when Gitaly blame feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly blame feature is disabled', :skip_gitaly_mock do it_behaves_like 'blaming a file' end end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 66ba00acb7d..c04a9688503 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -112,17 +112,20 @@ describe Gitlab::Git::Blob, seed_helper: true do it_behaves_like 'finding blobs' end - context 'when project_raw_show Gitaly feature is disabled', skip_gitaly_mock: true do + context 'when project_raw_show Gitaly feature is disabled', :skip_gitaly_mock do it_behaves_like 'finding blobs' end end shared_examples 'finding blobs by ID' do let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) } + let(:bad_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::BigCommit::ID) } + it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) } it { expect(raw_blob.data[0..10]).to eq("require \'fi") } it { expect(raw_blob.size).to eq(669) } it { expect(raw_blob.truncated?).to be_falsey } + it { expect(bad_blob).to be_nil } context 'large file' do it 'limits the size of a large file' do @@ -140,6 +143,16 @@ describe Gitlab::Git::Blob, seed_helper: true do expect(blob.loaded_size).to eq(blob_size) end end + + context 'when sha references a tree' do + it 'returns nil' do + tree = Gitlab::Git::Commit.find(repository, 'master').tree + + blob = Gitlab::Git::Blob.raw(repository, tree.oid) + + expect(blob).to be_nil + end + end end describe '.raw' do @@ -147,7 +160,7 @@ describe Gitlab::Git::Blob, seed_helper: true do it_behaves_like 'finding blobs by ID' end - context 'when the blob_raw Gitaly feature is disabled', skip_gitaly_mock: true do + context 'when the blob_raw Gitaly feature is disabled', :skip_gitaly_mock do it_behaves_like 'finding blobs by ID' end end @@ -223,6 +236,51 @@ describe Gitlab::Git::Blob, seed_helper: true do end end + describe '.batch_lfs_pointers' do + let(:tree_object) { Gitlab::Git::Commit.find(repository, 'master').tree } + + let(:non_lfs_blob) do + Gitlab::Git::Blob.find( + repository, + 'master', + 'README.md' + ) + end + + let(:lfs_blob) do + Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'files/lfs/image.jpg' + ) + end + + it 'returns a list of Gitlab::Git::Blob' do + blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id]) + + expect(blobs.count).to eq(1) + expect(blobs).to all( be_a(Gitlab::Git::Blob) ) + end + + it 'silently ignores tree objects' do + blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid]) + + expect(blobs).to eq([]) + end + + it 'silently ignores non lfs objects' do + blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id]) + + expect(blobs).to eq([]) + end + + it 'avoids loading large blobs into memory' do + expect(repository).not_to receive(:lookup) + + described_class.batch_lfs_pointers(repository, [non_lfs_blob.id]) + end + end + describe 'encoding' do context 'file with russian text' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") } diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index 318a7b7a332..708870060e7 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -7,6 +7,38 @@ describe Gitlab::Git::Branch, seed_helper: true do it { is_expected.to be_kind_of Array } + describe '.find' do + subject { described_class.find(repository, branch) } + + before do + allow(repository).to receive(:find_branch).with(branch) + .and_call_original + end + + context 'when finding branch via branch name' do + let(:branch) { 'master' } + + it 'returns a branch object' do + expect(subject).to be_a(described_class) + expect(subject.name).to eq(branch) + + expect(repository).to have_received(:find_branch).with(branch) + end + end + + context 'when the branch is already a branch' do + let(:commit) { repository.commit('master') } + let(:branch) { described_class.new(repository, 'master', commit.sha, commit) } + + it 'returns a branch object' do + expect(subject).to be_a(described_class) + expect(subject).to eq(branch) + + expect(repository).not_to have_received(:find_branch).with(branch) + end + 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 14d64d8c4da..9f4e3c49adc 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -65,34 +65,12 @@ describe Gitlab::Git::Commit, seed_helper: true do end describe "Commit info from gitaly commit" do - let(:id) { 'f00' } - let(:parent_ids) { %w(b45 b46) } let(:subject) { "My commit".force_encoding('ASCII-8BIT') } let(:body) { subject + "My body".force_encoding('ASCII-8BIT') } - let(:committer) do - Gitaly::CommitAuthor.new( - name: generate(:name), - email: generate(:email), - date: Google::Protobuf::Timestamp.new(seconds: 123) - ) - end - let(:author) do - Gitaly::CommitAuthor.new( - name: generate(:name), - email: generate(:email), - date: Google::Protobuf::Timestamp.new(seconds: 456) - ) - end - let(:gitaly_commit) do - Gitaly::GitCommit.new( - id: id, - subject: subject, - body: body, - author: author, - committer: committer, - parent_ids: parent_ids - ) - end + let(:gitaly_commit) { build(:gitaly_commit, subject: subject, body: body) } + let(:id) { gitaly_commit.id } + let(:committer) { gitaly_commit.committer } + let(:author) { gitaly_commit.author } let(:commit) { described_class.new(repository, gitaly_commit) } it { expect(commit.short_id).to eq(id[0..10]) } @@ -104,7 +82,7 @@ describe Gitlab::Git::Commit, seed_helper: true do it { expect(commit.author_name).to eq(author.name) } it { expect(commit.committer_name).to eq(committer.name) } it { expect(commit.committer_email).to eq(committer.email) } - it { expect(commit.parent_ids).to eq(parent_ids) } + it { expect(commit.parent_ids).to eq(gitaly_commit.parent_ids) } context 'no body' do let(:body) { "".force_encoding('ASCII-8BIT') } @@ -181,7 +159,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe '.where' do + shared_examples '.where' do context 'path is empty string' do subject do commits = described_class.where( @@ -279,6 +257,14 @@ describe Gitlab::Git::Commit, seed_helper: true do end end + describe '.where with gitaly' do + it_should_behave_like '.where' + end + + describe '.where without gitaly', :skip_gitaly_mock do + it_should_behave_like '.where' + end + describe '.between' do subject do commits = described_class.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID) @@ -350,7 +336,7 @@ describe Gitlab::Git::Commit, seed_helper: true do it_behaves_like 'finding all commits' end - context 'when Gitaly find_all_commits feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly find_all_commits feature is disabled', :skip_gitaly_mock do it_behaves_like 'finding all commits' context 'while applying a sort order based on the `order` option' do @@ -401,7 +387,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe '#stats' do + shared_examples '#stats' do subject { commit.stats } describe '#additions' do @@ -415,6 +401,14 @@ describe Gitlab::Git::Commit, seed_helper: true do end end + describe '#stats with gitaly on' do + it_should_behave_like '#stats' + end + + describe '#stats with gitaly disabled', :skip_gitaly_mock do + it_should_behave_like '#stats' + end + describe '#to_diff' do subject { commit.to_diff } diff --git a/spec/lib/gitlab/git/committer_spec.rb b/spec/lib/gitlab/git/committer_spec.rb deleted file mode 100644 index b0ddbb51449..00000000000 --- a/spec/lib/gitlab/git/committer_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::Committer do - let(:name) { 'Jane Doe' } - let(:email) { 'janedoe@example.com' } - let(:gl_id) { 'user-123' } - - subject { described_class.new(name, email, gl_id) } - - describe '#==' do - def eq_other(name, email, gl_id) - eq(described_class.new(name, email, gl_id)) - end - - it { expect(subject).to eq_other(name, email, gl_id) } - - it { expect(subject).not_to eq_other(nil, nil, nil) } - it { expect(subject).not_to eq_other(name + 'x', email, gl_id) } - it { expect(subject).not_to eq_other(name, email + 'x', gl_id) } - it { expect(subject).not_to eq_other(name, email, gl_id + 'x') } - end -end diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/git/conflict/parser_spec.rb index fce606a2bb5..7b035a381f1 100644 --- a/spec/lib/gitlab/conflict/parser_spec.rb +++ b/spec/lib/gitlab/git/conflict/parser_spec.rb @@ -1,11 +1,9 @@ require 'spec_helper' -describe Gitlab::Conflict::Parser do - let(:parser) { described_class.new } - - describe '#parse' do +describe Gitlab::Git::Conflict::Parser do + describe '.parse' do def parse_text(text) - parser.parse(text, our_path: 'README.md', their_path: 'README.md') + described_class.parse(text, our_path: 'README.md', their_path: 'README.md') end context 'when the file has valid conflicts' do @@ -87,33 +85,37 @@ CONFLICT end let(:lines) do - parser.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb') + described_class.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb') + end + let(:old_line_numbers) do + lines.select { |line| line[:type] != 'new' }.map { |line| line[:line_old] } end + let(:new_line_numbers) do + lines.select { |line| line[:type] != 'old' }.map { |line| line[:line_new] } + end + let(:line_indexes) { lines.map { |line| line[:line_obj_index] } } it 'sets our lines as new lines' do - expect(lines[8..13]).to all(have_attributes(type: 'new')) - expect(lines[26..27]).to all(have_attributes(type: 'new')) - expect(lines[56..57]).to all(have_attributes(type: 'new')) + expect(lines[8..13]).to all(include(type: 'new')) + expect(lines[26..27]).to all(include(type: 'new')) + expect(lines[56..57]).to all(include(type: 'new')) end it 'sets their lines as old lines' do - expect(lines[14..19]).to all(have_attributes(type: 'old')) - expect(lines[28..29]).to all(have_attributes(type: 'old')) - expect(lines[58..59]).to all(have_attributes(type: 'old')) + expect(lines[14..19]).to all(include(type: 'old')) + expect(lines[28..29]).to all(include(type: 'old')) + expect(lines[58..59]).to all(include(type: 'old')) end it 'sets non-conflicted lines as both' do - expect(lines[0..7]).to all(have_attributes(type: nil)) - expect(lines[20..25]).to all(have_attributes(type: nil)) - expect(lines[30..55]).to all(have_attributes(type: nil)) - expect(lines[60..62]).to all(have_attributes(type: nil)) + expect(lines[0..7]).to all(include(type: nil)) + expect(lines[20..25]).to all(include(type: nil)) + expect(lines[30..55]).to all(include(type: nil)) + expect(lines[60..62]).to all(include(type: nil)) end - it 'sets consecutive line numbers for index, old_pos, and new_pos' do - old_line_numbers = lines.select { |line| line.type != 'new' }.map(&:old_pos) - new_line_numbers = lines.select { |line| line.type != 'old' }.map(&:new_pos) - - expect(lines.map(&:index)).to eq(0.upto(62).to_a) + it 'sets consecutive line numbers for line_obj_index, line_old, and line_new' do + expect(line_indexes).to eq(0.upto(62).to_a) expect(old_line_numbers).to eq(1.upto(53).to_a) expect(new_line_numbers).to eq(1.upto(53).to_a) end @@ -123,12 +125,12 @@ CONFLICT context 'when there is a non-start delimiter first' do it 'raises UnexpectedDelimiter when there is a middle delimiter first' do expect { parse_text('=======') } - .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter) end it 'raises UnexpectedDelimiter when there is an end delimiter first' do expect { parse_text('>>>>>>> README.md') } - .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter) end it 'does not raise when there is an end delimiter for a different path first' do @@ -143,12 +145,12 @@ CONFLICT it 'raises UnexpectedDelimiter when it is followed by an end delimiter' do expect { parse_text(start_text + '>>>>>>> README.md' + end_text) } - .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter) end it 'raises UnexpectedDelimiter when it is followed by another start delimiter' do expect { parse_text(start_text + start_text + end_text) } - .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter) end it 'does not raise when it is followed by a start delimiter for a different path' do @@ -163,12 +165,12 @@ CONFLICT it 'raises UnexpectedDelimiter when it is followed by another middle delimiter' do expect { parse_text(start_text + '=======' + end_text) } - .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter) end it 'raises UnexpectedDelimiter when it is followed by a start delimiter' do expect { parse_text(start_text + start_text + end_text) } - .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter) end it 'does not raise when it is followed by a start delimiter for another path' do @@ -181,25 +183,25 @@ CONFLICT start_text = "<<<<<<< README.md\n=======\n" expect { parse_text(start_text) } - .to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::MissingEndDelimiter) expect { parse_text(start_text + '>>>>>>> some-other-path.md') } - .to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::MissingEndDelimiter) end end context 'other file types' do it 'raises UnmergeableFile when lines is blank, indicating a binary file' do expect { parse_text('') } - .to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) + .to raise_error(Gitlab::Git::Conflict::Parser::UnmergeableFile) expect { parse_text(nil) } - .to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) + .to raise_error(Gitlab::Git::Conflict::Parser::UnmergeableFile) end it 'raises UnmergeableFile when the file is over 200 KB' do expect { parse_text('a' * 204801) } - .to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) + .to raise_error(Gitlab::Git::Conflict::Parser::UnmergeableFile) end # All text from Rugged has an encoding of ASCII_8BIT, so force that in @@ -214,7 +216,7 @@ CONFLICT context 'when the file contains non-UTF-8 characters' do it 'raises UnsupportedEncoding' do expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) } - .to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding) + .to raise_error(Gitlab::Git::Conflict::Parser::UnsupportedEncoding) end end end diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 3494f0cc98d..ee657101f4c 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -341,8 +341,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end context 'when diff is quite large will collapse by default' do - let(:iterator) { [{ diff: 'a' * (Gitlab::Git::Diff.collapse_limit + 1) }] } - let(:max_files) { 100 } + let(:iterator) { [{ diff: 'a' * 20480 }] } context 'when no collapse is set' do let(:expanded) { true } diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index d39b33a0c05..4a7b06003fc 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -31,36 +31,6 @@ EOT [".gitmodules"]).patches.first end - describe 'size limit feature toggles' do - context 'when the feature gitlab_git_diff_size_limit_increase is enabled' do - before do - stub_feature_flags(gitlab_git_diff_size_limit_increase: true) - end - - it 'returns 200 KB for size_limit' do - expect(described_class.size_limit).to eq(200.kilobytes) - end - - it 'returns 100 KB for collapse_limit' do - expect(described_class.collapse_limit).to eq(100.kilobytes) - end - end - - context 'when the feature gitlab_git_diff_size_limit_increase is disabled' do - before do - stub_feature_flags(gitlab_git_diff_size_limit_increase: false) - end - - it 'returns 100 KB for size_limit' do - expect(described_class.size_limit).to eq(100.kilobytes) - end - - it 'returns 10 KB for collapse_limit' do - expect(described_class.collapse_limit).to eq(10.kilobytes) - end - end - end - describe '.new' do context 'using a Hash' do context 'with a small diff' do @@ -77,7 +47,7 @@ EOT context 'using a diff that is too large' do it 'prunes the diff' do - diff = described_class.new(diff: 'a' * (described_class.size_limit + 1)) + diff = described_class.new(diff: 'a' * 204800) expect(diff.diff).to be_empty expect(diff).to be_too_large @@ -115,8 +85,8 @@ EOT # The patch total size is 200, with lines between 21 and 54. # This is a quick-and-dirty way to test this. Ideally, a new patch is # added to the test repo with a size that falls between the real limits. - allow(Gitlab::Git::Diff).to receive(:size_limit).and_return(150) - allow(Gitlab::Git::Diff).to receive(:collapse_limit).and_return(100) + stub_const("#{described_class}::SIZE_LIMIT", 150) + stub_const("#{described_class}::COLLAPSE_LIMIT", 100) end it 'prunes the diff as a large diff instead of as a collapsed diff' do @@ -356,7 +326,7 @@ EOT describe '#collapsed?' do it 'returns true for a diff that is quite large' do - diff = described_class.new({ diff: 'a' * (described_class.collapse_limit + 1) }, expanded: false) + diff = described_class.new({ diff: 'a' * 20480 }, expanded: false) expect(diff).to be_collapsed end diff --git a/spec/lib/gitlab/git/env_spec.rb b/spec/lib/gitlab/git/env_spec.rb index d9df99bfe05..03836d49518 100644 --- a/spec/lib/gitlab/git/env_spec.rb +++ b/spec/lib/gitlab/git/env_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Git::Env do - describe "#set" do + describe ".set" do context 'with RequestStore.store disabled' do before do allow(RequestStore).to receive(:active?).and_return(false) @@ -34,25 +34,57 @@ describe Gitlab::Git::Env do end end - describe "#all" do + 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') + 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' + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => ['bar'] }) end end end - describe "#[]" do + describe ".to_env_hash" do + context 'with RequestStore.store enabled' do + using RSpec::Parameterized::TableSyntax + + let(:key) { 'GIT_OBJECT_DIRECTORY' } + subject { described_class.to_env_hash } + + where(:input, :output) do + nil | nil + 'foo' | 'foo' + [] | '' + ['foo'] | 'foo' + %w[foo bar] | 'foo:bar' + end + + with_them do + before do + allow(RequestStore).to receive(:active?).and_return(true) + described_class.set(key.to_sym => input) + end + + it 'puts the right value in the hash' do + if output + expect(subject.fetch(key)).to eq(output) + else + expect(subject.has_key?(key)).to eq(false) + end + end + end + end + end + + describe ".[]" do context 'with RequestStore.store enabled' do before do allow(RequestStore).to receive(:active?).and_return(true) diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb index ea3e4680b1d..2fe1f5603ce 100644 --- a/spec/lib/gitlab/git/hook_spec.rb +++ b/spec/lib/gitlab/git/hook_spec.rb @@ -14,6 +14,7 @@ describe Gitlab::Git::Hook do let(:repo_path) { repository.path } let(:user) { create(:user) } let(:gl_id) { Gitlab::GlId.gl_id(user) } + let(:gl_username) { user.username } def create_hook(name) FileUtils.mkdir_p(File.join(repo_path, 'hooks')) @@ -28,6 +29,7 @@ describe Gitlab::Git::Hook do f.write(<<-HOOK) echo 'regular message from the hook' echo 'error message from the hook' 1>&2 + echo 'error message from the hook line 2' 1>&2 exit 1 HOOK end @@ -41,6 +43,7 @@ describe Gitlab::Git::Hook do let(:env) do { 'GL_ID' => gl_id, + 'GL_USERNAME' => gl_username, 'PWD' => repo_path, 'GL_PROTOCOL' => 'web', 'GL_REPOSITORY' => gl_repository @@ -58,7 +61,7 @@ describe Gitlab::Git::Hook do .with(env, hook_path, chdir: repo_path).and_call_original end - status, errors = hook.trigger(gl_id, blank, blank, ref) + status, errors = hook.trigger(gl_id, gl_username, blank, blank, ref) expect(status).to be true expect(errors).to be_blank end @@ -71,9 +74,9 @@ describe Gitlab::Git::Hook do blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(gl_id, blank, blank, ref) + status, errors = hook.trigger(gl_id, gl_username, blank, blank, ref) expect(status).to be false - expect(errors).to eq("error message from the hook\n") + expect(errors).to eq("error message from the hook<br>error message from the hook line 2<br>") end end end @@ -85,7 +88,7 @@ describe Gitlab::Git::Hook do blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(gl_id, blank, blank, ref) + status, errors = hook.trigger(gl_id, gl_username, blank, blank, ref) expect(status).to be true expect(errors).to be_nil end diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb index e9c0209fe3b..3ed3feb4c74 100644 --- a/spec/lib/gitlab/git/hooks_service_spec.rb +++ b/spec/lib/gitlab/git/hooks_service_spec.rb @@ -1,24 +1,26 @@ require 'spec_helper' describe Gitlab::Git::HooksService, seed_helper: true do - let(:committer) { Gitlab::Git::Committer.new('Jane Doe', 'janedoe@example.com', 'user-456') } + let(:gl_id) { 'user-456' } + let(:gl_username) { 'janedoe' } + let(:user) { Gitlab::Git::User.new(gl_username, 'Jane Doe', 'janedoe@example.com', gl_id) } let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') } let(:service) { described_class.new } - - before do - @blankrev = Gitlab::Git::BLANK_SHA - @oldrev = SeedRepo::Commit::PARENT_ID - @newrev = SeedRepo::Commit::ID - @ref = 'refs/heads/feature' - end + let(:blankrev) { Gitlab::Git::BLANK_SHA } + let(:oldrev) { SeedRepo::Commit::PARENT_ID } + let(:newrev) { SeedRepo::Commit::ID } + let(:ref) { 'refs/heads/feature' } describe '#execute' do context 'when receive hooks were successful' do - it 'calls post-receive hook' do - hook = double(trigger: [true, nil]) + let(:hook) { double(:hook) } + + it 'calls all three hooks' do expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) + expect(hook).to receive(:trigger).with(gl_id, gl_username, blankrev, newrev, ref) + .exactly(3).times.and_return([true, nil]) - service.execute(committer, repository, @blankrev, @newrev, @ref) { } + service.execute(user, repository, blankrev, newrev, ref) { } end end @@ -28,7 +30,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(committer, repository, @blankrev, @newrev, @ref) + service.execute(user, repository, blankrev, newrev, ref) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end end @@ -40,7 +42,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(committer, repository, @blankrev, @newrev, @ref) + service.execute(user, repository, blankrev, newrev, ref) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end end diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb new file mode 100644 index 00000000000..c2d2c6e1bc8 --- /dev/null +++ b/spec/lib/gitlab/git/lfs_changes_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::Git::LfsChanges do + let(:project) { create(:project, :repository) } + let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + let(:blob_object_id) { '0c304a93cb8430108629bbbcaa27db3343299bc0' } + + subject { described_class.new(project.repository, newrev) } + + describe 'new_pointers' do + before do + allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects).and_return([blob_object_id]) + end + + it 'uses rev-list to find new objects' do + rev_list = double + allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list) + + expect(rev_list).to receive(:new_objects).and_return([]) + + subject.new_pointers + end + + it 'filters new objects to find lfs pointers' do + expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id]) + + subject.new_pointers(object_limit: 1) + end + + it 'limits new_objects using object_limit' do + expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, []) + + subject.new_pointers(object_limit: 0) + end + end + + describe 'all_pointers' do + it 'uses rev-list to find all objects' do + rev_list = double + allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list) + allow(rev_list).to receive(:all_objects).and_return([blob_object_id]) + + expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id]) + + subject.all_pointers + end + end +end diff --git a/spec/lib/gitlab/git/popen_spec.rb b/spec/lib/gitlab/git/popen_spec.rb new file mode 100644 index 00000000000..2b65bc1cf15 --- /dev/null +++ b/spec/lib/gitlab/git/popen_spec.rb @@ -0,0 +1,132 @@ +require 'spec_helper' + +describe 'Gitlab::Git::Popen' do + let(:path) { Rails.root.join('tmp').to_s } + + let(:klass) do + Class.new(Object) do + include Gitlab::Git::Popen + end + end + + context 'popen' do + context 'zero status' do + let(:result) { klass.new.popen(%w(ls), path) } + let(:output) { result.first } + let(:status) { result.last } + + it { expect(status).to be_zero } + it { expect(output).to include('tests') } + end + + context 'non-zero status' do + let(:result) { klass.new.popen(%w(cat NOTHING), path) } + let(:output) { result.first } + let(:status) { result.last } + + it { expect(status).to eq(1) } + it { expect(output).to include('No such file or directory') } + end + + context 'unsafe string command' do + it 'raises an error when it gets called with a string argument' do + expect { klass.new.popen('ls', path) }.to raise_error(RuntimeError) + end + end + + context 'with custom options' do + let(:vars) { { 'foobar' => 123, 'PWD' => path } } + let(:options) { { chdir: path } } + + it 'calls popen3 with the provided environment variables' do + expect(Open3).to receive(:popen3).with(vars, 'ls', options) + + klass.new.popen(%w(ls), path, { 'foobar' => 123 }) + end + end + + context 'use stdin' do + let(:result) { klass.new.popen(%w[cat], path) { |stdin| stdin.write 'hello' } } + let(:output) { result.first } + let(:status) { result.last } + + it { expect(status).to be_zero } + it { expect(output).to eq('hello') } + end + end + + context 'popen_with_timeout' do + let(:timeout) { 1.second } + + context 'no timeout' do + context 'zero status' do + let(:result) { klass.new.popen_with_timeout(%w(ls), timeout, path) } + let(:output) { result.first } + let(:status) { result.last } + + it { expect(status).to be_zero } + it { expect(output).to include('tests') } + end + + context 'non-zero status' do + let(:result) { klass.new.popen_with_timeout(%w(cat NOTHING), timeout, path) } + let(:output) { result.first } + let(:status) { result.last } + + it { expect(status).to eq(1) } + it { expect(output).to include('No such file or directory') } + end + + context 'unsafe string command' do + it 'raises an error when it gets called with a string argument' do + expect { klass.new.popen_with_timeout('ls', timeout, path) }.to raise_error(RuntimeError) + end + end + end + + context 'timeout' do + context 'timeout' do + it "raises a Timeout::Error" do + expect { klass.new.popen_with_timeout(%w(sleep 1000), timeout, path) }.to raise_error(Timeout::Error) + end + + it "handles processes that do not shutdown correctly" do + expect { klass.new.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error) + end + end + + context 'timeout period' do + let(:time_taken) do + begin + start = Time.now + klass.new.popen_with_timeout(%w(sleep 1000), timeout, path) + rescue + Time.now - start + end + end + + it { expect(time_taken).to be >= timeout } + end + + context 'clean up' do + let(:instance) { klass.new } + + it 'kills the child process' do + expect(instance).to receive(:kill_process_group_for_pid).and_wrap_original do |m, *args| + # is the PID, and it should not be running at this point + m.call(*args) + + pid = args.first + begin + Process.getpgid(pid) + raise "The child process should have been killed" + rescue Errno::ESRCH + end + end + + expect { instance.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error) + end + end + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 556a148c3bc..1d4d0c300eb 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -54,7 +54,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#rugged" do - describe 'when storage is broken', broken_storage: true do + describe 'when storage is broken', :broken_storage do it 'raises a storage exception when storage is not available' do broken_repo = described_class.new('broken', 'a/path.git', '') @@ -68,31 +68,52 @@ describe Gitlab::Git::Repository, seed_helper: true do expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Repository::NoRepository) end - context 'with no Git env stored' do - before do - expect(Gitlab::Git::Env).to receive(:all).and_return({}) - end + describe 'alternates keyword argument' do + context 'with no Git env stored' do + before do + allow(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: []) + it "is passed an empty array" do + expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: []) - repository.rugged + repository.rugged + end 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' - }) + context 'with absolute and relative Git object dir envvars stored' do + before do + allow(Gitlab::Git::Env).to receive(:all).and_return({ + 'GIT_OBJECT_DIRECTORY_RELATIVE' => './objects/foo', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['./objects/bar', './objects/baz'], + 'GIT_OBJECT_DIRECTORY' => 'ignored', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => %w[ignored ignored], + 'GIT_OTHER' => 'another_env' + }) + end + + it "is passed the relative object dir envvars after being converted to absolute ones" do + alternates = %w[foo bar baz].map { |d| File.join(repository.path, './objects', d) } + expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: alternates) + + repository.rugged + end 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]) + context 'with only absolute Git object dir envvars stored' do + before do + allow(Gitlab::Git::Env).to receive(:all).and_return({ + 'GIT_OBJECT_DIRECTORY' => 'foo', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => %w[bar baz], + 'GIT_OTHER' => 'another_env' + }) + end + + it "is passed the absolute object dir envvars as is" do + expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: %w[foo bar baz]) - repository.rugged + repository.rugged + end end end end @@ -384,11 +405,45 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - context 'when Gitaly commit_count feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly commit_count feature is disabled', :skip_gitaly_mock do it_behaves_like 'simple commit counting' end end + describe '#has_local_branches?' do + shared_examples 'check for local branches' do + it { expect(repository.has_local_branches?).to eq(true) } + + context 'mutable' do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } + + after do + ensure_seeds + end + + it 'returns false when there are no branches' do + # Sanity check + expect(repository.has_local_branches?).to eq(true) + + FileUtils.rm_rf(File.join(repository.path, 'packed-refs')) + heads_dir = File.join(repository.path, 'refs/heads') + FileUtils.rm_rf(heads_dir) + FileUtils.mkdir_p(heads_dir) + + expect(repository.has_local_branches?).to eq(false) + end + end + end + + context 'with gitaly' do + it_behaves_like 'check for local branches' + end + + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like 'check for local branches' + end + end + describe "#delete_branch" do shared_examples "deleting a branch" do let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } @@ -419,7 +474,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like "deleting a branch" end - context "when Gitaly delete_branch is disabled", skip_gitaly_mock: true do + context "when Gitaly delete_branch is disabled", :skip_gitaly_mock do it_behaves_like "deleting a branch" end end @@ -455,7 +510,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'creating a branch' end - context 'when Gitaly create_branch feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly create_branch feature is disabled', :skip_gitaly_mock do it_behaves_like 'creating a branch' end end @@ -481,7 +536,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'raises an error if it failed' do - expect(Gitlab::Popen).to receive(:popen).and_return(['Error', 1]) + expect(@repo).to receive(:popen).and_return(['Error', 1]) expect do @repo.delete_refs('refs/heads/fix') @@ -504,10 +559,10 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "#remote_delete" do + describe "#remove_remote" do before(:all) do @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.remote_delete("expendable") + @repo.remove_remote("expendable") end it "should remove the remote" do @@ -520,14 +575,16 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "#remote_add" do + describe "#remote_update" do before(:all) do @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.remote_add("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL) + @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH) end it "should add the remote" do - expect(@repo.rugged.remotes.each_name.to_a).to include("new_remote") + expect(@repo.rugged.remotes["expendable"].url).to( + eq(TEST_NORMAL_REPO_PATH) + ) end after(:all) do @@ -536,21 +593,58 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "#remote_update" do - before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH) + describe '#fetch_mirror' do + let(:new_repository) do + Gitlab::Git::Repository.new('default', 'my_project.git', '') end - it "should add the remote" do - expect(@repo.rugged.remotes["expendable"].url).to( - eq(TEST_NORMAL_REPO_PATH) - ) + subject { new_repository.fetch_mirror(repository.path) } + + before do + Gitlab::Shell.new.add_repository('default', 'my_project') end - after(:all) do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) - ensure_seeds + after do + Gitlab::Shell.new.remove_repository(TestEnv.repos_path, 'my_project') + end + + it 'fetches a url as a mirror remote' do + subject + + expect(refs(new_repository.path)).to eq(refs(repository.path)) + end + + context 'with keep-around refs' do + let(:sha) { SeedRepo::Commit::ID } + let(:keep_around_ref) { "refs/keep-around/#{sha}" } + let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" } + + before do + repository.rugged.references.create(keep_around_ref, sha, force: true) + repository.rugged.references.create(tmp_ref, sha, force: true) + end + + it 'includes the temporary and keep-around refs' do + subject + + expect(refs(new_repository.path)).to include(keep_around_ref) + expect(refs(new_repository.path)).to include(tmp_ref) + end + end + end + + describe '#remote_tags' do + let(:target_commit_id) { SeedRepo::Commit::ID } + + subject { repository.remote_tags('upstream') } + + it 'gets the remote tags' do + expect(repository).to receive(:list_remote_tags).with('upstream') + .and_return(["#{target_commit_id}\trefs/tags/v0.0.1\n"]) + + expect(subject.first).to be_an_instance_of(Gitlab::Git::Tag) + expect(subject.first.name).to eq('v0.0.1') + expect(subject.first.dereferenced_target.id).to eq(target_commit_id) end end @@ -895,7 +989,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'extended commit counting' end - context 'when Gitaly count_commits feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly count_commits feature is disabled', :skip_gitaly_mock do it_behaves_like 'extended commit counting' end end @@ -962,7 +1056,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'finding a branch' end - context 'when Gitaly find_branch feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly find_branch feature is disabled', :skip_gitaly_mock do it_behaves_like 'finding a branch' it 'should reload Rugged::Repository and return master' do @@ -1080,8 +1174,35 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#merged_branch_names' do + context 'when branch names are passed' do + it 'only returns the names we are asking' do + names = repository.merged_branch_names(%w[merge-test]) + + expect(names).to contain_exactly('merge-test') + end + + it 'does not return unmerged branch names' do + names = repository.merged_branch_names(%w[feature]) + + expect(names).to be_empty + end + end + + context 'when no branch names are specified' do + it 'returns all merged branch names' do + names = repository.merged_branch_names + + expect(names).to include('merge-test') + expect(names).to include('fix-mode') + expect(names).not_to include('feature') + end + end + end + describe "#ls_files" do let(:master_file_paths) { repository.ls_files("master") } + let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") } let(:not_existed_branch) { repository.ls_files("not_existed_branch") } it "read every file paths of master branch" do @@ -1103,6 +1224,10 @@ describe Gitlab::Git::Repository, seed_helper: true do it "returns empty array when not existed branch" do expect(not_existed_branch.length).to equal(0) end + + it "returns valid utf-8 data" do + expect(utf8_file_paths.map { |file| file.force_encoding('utf-8') }).to all(be_valid_encoding) + end end describe "#copy_gitattributes" do @@ -1204,7 +1329,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'checks the existence of refs' end - context 'when Gitaly ref_exists feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly ref_exists feature is disabled', :skip_gitaly_mock do it_behaves_like 'checks the existence of refs' end end @@ -1226,7 +1351,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'checks the existence of tags' end - context 'when Gitaly ref_exists_tags feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly ref_exists_tags feature is disabled', :skip_gitaly_mock do it_behaves_like 'checks the existence of tags' end end @@ -1250,11 +1375,29 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'checks the existence of branches' end - context 'when Gitaly ref_exists_branches feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly ref_exists_branches feature is disabled', :skip_gitaly_mock do it_behaves_like 'checks the existence of branches' end end + describe '#batch_existence' do + let(:refs) { ['deadbeef', SeedRepo::RubyBlob::ID, '909e6157199'] } + + it 'returns existing refs back' do + result = repository.batch_existence(refs) + + expect(result).to eq([SeedRepo::RubyBlob::ID]) + end + + context 'existing: true' do + it 'inverts meaning and returns non-existing refs' do + result = repository.batch_existence(refs, existing: false) + + expect(result).to eq(%w(deadbeef 909e6157199)) + end + end + end + describe '#local_branches' do before(:all) do @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') @@ -1327,11 +1470,275 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'languages' - context 'with rugged', skip_gitaly_mock: true do + context 'with rugged', :skip_gitaly_mock do it_behaves_like 'languages' end end + describe '#with_repo_branch_commit' do + context 'when comparing with the same repository' do + let(:start_repository) { repository } + + context 'when the branch exists' do + let(:start_branch_name) { 'master' } + + it 'yields the commit' do + expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) } + .to yield_with_args(an_instance_of(Gitlab::Git::Commit)) + end + end + + context 'when the branch does not exist' do + let(:start_branch_name) { 'definitely-not-master' } + + it 'yields nil' do + expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) } + .to yield_with_args(nil) + end + end + end + + context 'when comparing with another repository' do + let(:start_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } + + context 'when the branch exists' do + let(:start_branch_name) { 'master' } + + it 'yields the commit' do + expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) } + .to yield_with_args(an_instance_of(Gitlab::Git::Commit)) + end + end + + context 'when the branch does not exist' do + let(:start_branch_name) { 'definitely-not-master' } + + it 'yields nil' do + expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) } + .to yield_with_args(nil) + end + end + end + end + + describe '#fetch_source_branch' do + let(:local_ref) { 'refs/merge-requests/1/head' } + + context 'when the branch exists' do + let(:source_branch) { 'master' } + + it 'writes the ref' do + expect(repository).to receive(:write_ref).with(local_ref, /\h{40}/) + + repository.fetch_source_branch(repository, source_branch, local_ref) + end + + it 'returns true' do + expect(repository.fetch_source_branch(repository, source_branch, local_ref)).to eq(true) + end + end + + context 'when the branch does not exist' do + let(:source_branch) { 'definitely-not-master' } + + it 'does not write the ref' do + expect(repository).not_to receive(:write_ref) + + repository.fetch_source_branch(repository, source_branch, local_ref) + end + + it 'returns false' do + expect(repository.fetch_source_branch(repository, source_branch, local_ref)).to eq(false) + end + end + end + + describe '#rm_branch' do + shared_examples "user deleting a branch" do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw } + let(:user) { create(:user) } + let(:branch_name) { "to-be-deleted-soon" } + + before do + project.team << [user, :developer] + repository.create_branch(branch_name) + end + + it "removes the branch from the repo" do + repository.rm_branch(branch_name, user: user) + + expect(repository.rugged.branches[branch_name]).to be_nil + end + end + + context "when Gitaly user_delete_branch is enabled" do + it_behaves_like "user deleting a branch" + end + + context "when Gitaly user_delete_branch is disabled", :skip_gitaly_mock do + it_behaves_like "user deleting a branch" + end + end + + describe '#write_ref' do + context 'validations' do + using RSpec::Parameterized::TableSyntax + + where(:ref_path, :ref) do + 'foo bar' | '123' + 'foobar' | "12\x003" + end + + with_them do + it 'raises ArgumentError' do + expect { repository.write_ref(ref_path, ref) }.to raise_error(ArgumentError) + end + end + end + end + + describe '#fetch' do + let(:git_path) { Gitlab.config.git.bin_path } + let(:remote_name) { 'my_remote' } + + subject { repository.fetch(remote_name) } + + it 'fetches the remote and returns true if the command was successful' do + expect(repository).to receive(:popen) + .with(%W(#{git_path} fetch #{remote_name}), repository.path) + .and_return(['', 0]) + + expect(subject).to be(true) + end + end + + describe '#merge' do + let(:repository) do + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + end + let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' } + let(:user) { build(:user) } + let(:target_branch) { 'test-merge-target-branch' } + + before do + repository.create_branch(target_branch, '6d394385cf567f80a8fd85055db1ab4c5295806f') + end + + after do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + + shared_examples '#merge' do + it 'can perform a merge' do + merge_commit_id = nil + result = repository.merge(user, source_sha, target_branch, 'Test merge') do |commit_id| + merge_commit_id = commit_id + end + + expect(result.newrev).to eq(merge_commit_id) + expect(result.repo_created).to eq(false) + expect(result.branch_created).to eq(false) + end + end + + context 'with gitaly' do + it_behaves_like '#merge' + end + + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like '#merge' + end + end + + describe '#ff_merge' do + let(:repository) do + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + end + let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } + let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } + let(:user) { build(:user) } + let(:target_branch) { 'test-ff-target-branch' } + + before do + repository.create_branch(target_branch, branch_head) + end + + after do + ensure_seeds + end + + subject { repository.ff_merge(user, source_sha, target_branch) } + + shared_examples '#ff_merge' do + it 'performs a ff_merge' do + expect(subject.newrev).to eq(source_sha) + expect(subject.repo_created).to be(false) + expect(subject.branch_created).to be(false) + + expect(repository.commit(target_branch).id).to eq(source_sha) + end + + context 'with a non-existing target branch' do + subject { repository.ff_merge(user, source_sha, 'this-isnt-real') } + + it 'throws an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + end + + context 'with a non-existing source commit' do + let(:source_sha) { 'f001' } + + it 'throws an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + end + + context 'when the source sha is not a descendant of the branch head' do + let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' } + + it "doesn't perform the ff_merge" do + expect { subject }.to raise_error(Gitlab::Git::CommitError) + + expect(repository.commit(target_branch).id).to eq(branch_head) + end + end + end + + context 'with gitaly' do + it "calls Gitaly's OperationService" do + expect_any_instance_of(Gitlab::GitalyClient::OperationService) + .to receive(:user_ff_branch).with(user, source_sha, target_branch) + .and_return(nil) + + subject + end + + it_behaves_like '#ff_merge' + end + + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like '#ff_merge' + end + end + + describe '#fetch' do + let(:git_path) { Gitlab.config.git.bin_path } + let(:remote_name) { 'my_remote' } + + subject { repository.fetch(remote_name) } + + it 'fetches the remote and returns true if the command was successful' do + expect(repository).to receive(:popen) + .with(%W(#{git_path} fetch #{remote_name}), repository.path) + .and_return(['', 0]) + + expect(subject).to be(true) + end + end + def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } rugged = repository.rugged @@ -1407,4 +1814,10 @@ describe Gitlab::Git::Repository, seed_helper: true do sha = Rugged::Commit.create(repo, options) repo.lookup(sha) end + + def refs(dir) + IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line| + line.split("\t").last + end + end end diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb index b051a088171..643a4b2d03e 100644 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -2,53 +2,82 @@ require 'spec_helper' describe Gitlab::Git::RevList do let(:project) { create(:project, :repository) } + let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } before do - expect(Gitlab::Git::Env).to receive(:all).and_return({ + allow(Gitlab::Git::Env).to receive(:all).and_return({ GIT_OBJECT_DIRECTORY: 'foo', GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar' }) end - context "#new_refs" do - let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } + def stub_popen_rev_list(*additional_args, output:) + expect(rev_list).to receive(:popen).with([ + Gitlab.config.git.bin_path, + "--git-dir=#{project.repository.path_to_repo}", + 'rev-list', + *additional_args + ], + nil, + { + 'GIT_OBJECT_DIRECTORY' => 'foo', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' + }).and_return([output, 0]) + end + context "#new_refs" do 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]) + stub_popen_rev_list('newrev', '--not', '--all', output: "sha1\nsha2") expect(rev_list.new_refs).to eq(%w[sha1 sha2]) end end + context '#new_objects' do + it 'fetches list of newly pushed objects using rev-list' do + stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") + + expect(rev_list.new_objects).to eq(%w[sha1 sha2]) + end + + it 'can skip pathless objects' do + stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2 path/to/file") + + expect(rev_list.new_objects(require_path: true)).to eq(%w[sha2]) + end + + it 'can return a lazy enumerator' do + stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") + + expect(rev_list.new_objects(lazy: true)).to be_a Enumerator::Lazy + end + + it 'can accept list of references to exclude' do + stub_popen_rev_list('newrev', '--not', 'master', '--objects', output: "sha1\nsha2") + + expect(rev_list.new_objects(not_in: ['master'])).to eq(%w[sha1 sha2]) + end + + it 'handles empty list of references to exclude as listing all known objects' do + stub_popen_rev_list('newrev', '--objects', output: "sha1\nsha2") + + expect(rev_list.new_objects(not_in: [])).to eq(%w[sha1 sha2]) + end + end + + context '#all_objects' do + it 'fetches list of all pushed objects using rev-list' do + stub_popen_rev_list('--all', '--objects', output: "sha1\nsha2") + + expect(rev_list.all_objects.force).to eq(%w[sha1 sha2]) + end + end + context "#missed_ref" do let(:rev_list) { described_class.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]) + stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', output: "sha1\nsha2") expect(rev_list.missed_ref).to eq(%w[sha1 sha2]) end diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb index c86353abb7c..72dabca793a 100644 --- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb +++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do let(:storage_name) { 'default' } - let(:circuit_breaker) { described_class.new(storage_name) } + let(:circuit_breaker) { described_class.new(storage_name, hostname) } let(:hostname) { Gitlab::Environment.hostname } let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" } @@ -10,19 +10,12 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: # Override test-settings for the circuitbreaker with something more realistic # for these specs. stub_storage_settings('default' => { - 'path' => TestEnv.repos_path, - 'failure_count_threshold' => 10, - 'failure_wait_time' => 30, - 'failure_reset_time' => 1800, - 'storage_timeout' => 5 + 'path' => TestEnv.repos_path }, 'broken' => { - 'path' => 'tmp/tests/non-existent-repositories', - 'failure_count_threshold' => 10, - 'failure_wait_time' => 30, - 'failure_reset_time' => 1800, - 'storage_timeout' => 5 - } + 'path' => 'tmp/tests/non-existent-repositories' + }, + 'nopath' => { 'path' => nil } ) end @@ -48,6 +41,10 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: expect(key_exists).to be_falsey end + + it 'does not break when there are no keys in redis' do + expect { described_class.reset_all! }.not_to raise_error + end end describe '.for_storage' do @@ -59,6 +56,14 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: expect(breaker).to be_a(described_class) expect(described_class.for_storage('default')).to eq(breaker) end + + it 'returns a broken circuit breaker for an unknown storage' do + expect(described_class.for_storage('unknown').circuit_broken?).to be_truthy + end + + it 'returns a broken circuit breaker when the path is not set' do + expect(described_class.for_storage('nopath').circuit_broken?).to be_truthy + end end describe '#initialize' do @@ -66,19 +71,79 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: expect(circuit_breaker.hostname).to eq(hostname) expect(circuit_breaker.storage).to eq('default') expect(circuit_breaker.storage_path).to eq(TestEnv.repos_path) - expect(circuit_breaker.failure_count_threshold).to eq(10) - expect(circuit_breaker.failure_wait_time).to eq(30) - expect(circuit_breaker.failure_reset_time).to eq(1800) - expect(circuit_breaker.storage_timeout).to eq(5) + end + end + + context 'circuitbreaker settings' do + before do + stub_application_setting(circuitbreaker_failure_count_threshold: 0, + circuitbreaker_failure_wait_time: 1, + circuitbreaker_failure_reset_time: 2, + circuitbreaker_storage_timeout: 3, + circuitbreaker_access_retries: 4, + circuitbreaker_backoff_threshold: 5) + end + + describe '#failure_count_threshold' do + it 'reads the value from settings' do + expect(circuit_breaker.failure_count_threshold).to eq(0) + end + end + + describe '#failure_wait_time' do + it 'reads the value from settings' do + expect(circuit_breaker.failure_wait_time).to eq(1) + end + end + + describe '#failure_reset_time' do + it 'reads the value from settings' do + expect(circuit_breaker.failure_reset_time).to eq(2) + end + end + + describe '#storage_timeout' do + it 'reads the value from settings' do + expect(circuit_breaker.storage_timeout).to eq(3) + end + end + + describe '#access_retries' do + it 'reads the value from settings' do + expect(circuit_breaker.access_retries).to eq(4) + end + end + + describe '#backoff_threshold' do + it 'reads the value from settings' do + expect(circuit_breaker.backoff_threshold).to eq(5) + end end end describe '#perform' do - it 'raises an exception with retry time when the circuit is open' do - allow(circuit_breaker).to receive(:circuit_broken?).and_return(true) + it 'raises the correct exception when the circuit is open' do + set_in_redis(:last_failure, 1.day.ago.to_f) + set_in_redis(:failure_count, 999) expect { |b| circuit_breaker.perform(&b) } - .to raise_error(Gitlab::Git::Storage::CircuitOpen) + .to raise_error do |exception| + expect(exception).to be_kind_of(Gitlab::Git::Storage::CircuitOpen) + expect(exception.retry_after).to eq(1800) + end + end + + it 'raises the correct exception when backing off' do + Timecop.freeze do + set_in_redis(:last_failure, 1.second.ago.to_f) + set_in_redis(:failure_count, 90) + + expect { |b| circuit_breaker.perform(&b) } + .to raise_error do |exception| + expect(exception).to be_kind_of(Gitlab::Git::Storage::Failing) + expect(exception.retry_after).to eq(30) + end + end end it 'yields the block' do @@ -88,6 +153,7 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: it 'checks if the storage is available' do expect(circuit_breaker).to receive(:check_storage_accessible!) + .and_call_original circuit_breaker.perform { 'hello world' } end @@ -103,204 +169,124 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: .to raise_error(Rugged::OSError) end - context 'with the feature disabled' do - it 'returns the block without checking accessibility' do - stub_feature_flags(git_storage_circuit_breaker: false) - - expect(circuit_breaker).not_to receive(:circuit_broken?) + it 'tracks that the storage was accessible' do + set_in_redis(:failure_count, 10) + set_in_redis(:last_failure, Time.now.to_f) - result = circuit_breaker.perform { 'hello' } + circuit_breaker.perform { '' } - expect(result).to eq('hello') - end + expect(value_from_redis(:failure_count).to_i).to eq(0) + expect(value_from_redis(:last_failure)).to be_empty + expect(circuit_breaker.failure_count).to eq(0) + expect(circuit_breaker.last_failure).to be_nil end - end - describe '#circuit_broken?' do - it 'is working when there is no last failure' do - set_in_redis(:last_failure, nil) - set_in_redis(:failure_count, 0) + it 'only performs the accessibility check once' do + expect(Gitlab::Git::Storage::ForkedStorageCheck) + .to receive(:storage_available?).once.and_call_original - expect(circuit_breaker.circuit_broken?).to be_falsey + 2.times { circuit_breaker.perform { '' } } end - it 'is broken when there was a recent failure' do - Timecop.freeze do - set_in_redis(:last_failure, 1.second.ago.to_f) - set_in_redis(:failure_count, 1) + it 'calls the check with the correct arguments' do + stub_application_setting(circuitbreaker_storage_timeout: 30, + circuitbreaker_access_retries: 3) - expect(circuit_breaker.circuit_broken?).to be_truthy - end - end - - it 'is broken when there are too many failures' do - set_in_redis(:last_failure, 1.day.ago.to_f) - set_in_redis(:failure_count, 200) + expect(Gitlab::Git::Storage::ForkedStorageCheck) + .to receive(:storage_available?).with(TestEnv.repos_path, 30, 3) + .and_call_original - expect(circuit_breaker.circuit_broken?).to be_truthy + circuit_breaker.perform { '' } end - context 'the `failure_wait_time` is set to 0' do + context 'with the feature disabled' do before do - stub_storage_settings('default' => { - 'failure_wait_time' => 0, - 'path' => TestEnv.repos_path - }) - end - - it 'is working even when there is a recent failure' do - Timecop.freeze do - set_in_redis(:last_failure, 0.seconds.ago.to_f) - set_in_redis(:failure_count, 1) - - expect(circuit_breaker.circuit_broken?).to be_falsey - end + stub_feature_flags(git_storage_circuit_breaker: false) end - end - end - describe "storage_available?" do - context 'the storage is available' do - it 'tracks that the storage was accessible an raises the error' do - expect(circuit_breaker).to receive(:track_storage_accessible) - - circuit_breaker.storage_available? - end + it 'returns the block without checking accessibility' do + expect(circuit_breaker).not_to receive(:check_storage_accessible!) - it 'only performs the check once' do - expect(Gitlab::Git::Storage::ForkedStorageCheck) - .to receive(:storage_available?).once.and_call_original + result = circuit_breaker.perform { 'hello' } - 2.times { circuit_breaker.storage_available? } + expect(result).to eq('hello') end - end - - context 'storage is not available' do - let(:storage_name) { 'broken' } - - it 'tracks that the storage was inaccessible' do - expect(circuit_breaker).to receive(:track_storage_inaccessible) - circuit_breaker.storage_available? - end - end - end + it 'allows enabling the feature using an ENV var' do + stub_env('GIT_STORAGE_CIRCUIT_BREAKER', 'true') + expect(circuit_breaker).to receive(:check_storage_accessible!) - describe '#check_storage_accessible!' do - it 'raises an exception with retry time when the circuit is open' do - allow(circuit_breaker).to receive(:circuit_broken?).and_return(true) + result = circuit_breaker.perform { 'hello' } - expect { circuit_breaker.check_storage_accessible! } - .to raise_error do |exception| - expect(exception).to be_kind_of(Gitlab::Git::Storage::CircuitOpen) - expect(exception.retry_after).to eq(30) + expect(result).to eq('hello') end end context 'the storage is not available' do let(:storage_name) { 'broken' } - it 'raises an error' do + it 'raises the correct exception' do expect(circuit_breaker).to receive(:track_storage_inaccessible) - expect { circuit_breaker.check_storage_accessible! } + expect { circuit_breaker.perform { '' } } .to raise_error do |exception| expect(exception).to be_kind_of(Gitlab::Git::Storage::Inaccessible) expect(exception.retry_after).to eq(30) end end - end - end - describe '#track_storage_inaccessible' do - around do |example| - Timecop.freeze { example.run } - end - - it 'records the failure time in redis' do - circuit_breaker.track_storage_inaccessible - - failure_time = value_from_redis(:last_failure) - - expect(Time.at(failure_time.to_i)).to be_within(1.second).of(Time.now) - end - - it 'sets the failure time on the breaker without reloading' do - circuit_breaker.track_storage_inaccessible - - expect(circuit_breaker).not_to receive(:get_failure_info) - expect(circuit_breaker.last_failure).to eq(Time.now) - end - - it 'increments the failure count in redis' do - set_in_redis(:failure_count, 10) - - circuit_breaker.track_storage_inaccessible - - expect(value_from_redis(:failure_count).to_i).to be(11) - end - - it 'increments the failure count on the breaker without reloading' do - set_in_redis(:failure_count, 10) - - circuit_breaker.track_storage_inaccessible + it 'tracks that the storage was inaccessible' do + Timecop.freeze do + expect { circuit_breaker.perform { '' } }.to raise_error(Gitlab::Git::Storage::Inaccessible) - expect(circuit_breaker).not_to receive(:get_failure_info) - expect(circuit_breaker.failure_count).to eq(11) + expect(value_from_redis(:failure_count).to_i).to eq(1) + expect(value_from_redis(:last_failure)).not_to be_empty + expect(circuit_breaker.failure_count).to eq(1) + expect(circuit_breaker.last_failure).to be_within(1.second).of(Time.now) + end + end end end - describe '#track_storage_accessible' do - it 'sets the failure count to zero in redis' do - set_in_redis(:failure_count, 10) - - circuit_breaker.track_storage_accessible - - expect(value_from_redis(:failure_count).to_i).to be(0) - end - - it 'sets the failure count to zero on the breaker without reloading' do - set_in_redis(:failure_count, 10) - - circuit_breaker.track_storage_accessible + describe '#circuit_broken?' do + it 'is working when there is no last failure' do + set_in_redis(:last_failure, nil) + set_in_redis(:failure_count, 0) - expect(circuit_breaker).not_to receive(:get_failure_info) - expect(circuit_breaker.failure_count).to eq(0) + expect(circuit_breaker.circuit_broken?).to be_falsey end - it 'removes the last failure time from redis' do - set_in_redis(:last_failure, Time.now.to_i) - - circuit_breaker.track_storage_accessible + it 'is broken when there are too many failures' do + set_in_redis(:last_failure, 1.day.ago.to_f) + set_in_redis(:failure_count, 200) - expect(circuit_breaker).not_to receive(:get_failure_info) - expect(circuit_breaker.last_failure).to be_nil + expect(circuit_breaker.circuit_broken?).to be_truthy end + end - it 'removes the last failure time from the breaker without reloading' do - set_in_redis(:last_failure, Time.now.to_i) - - circuit_breaker.track_storage_accessible + describe '#backing_off?' do + it 'is true when there was a recent failure' do + Timecop.freeze do + set_in_redis(:last_failure, 1.second.ago.to_f) + set_in_redis(:failure_count, 90) - expect(value_from_redis(:last_failure)).to be_empty + expect(circuit_breaker.backing_off?).to be_truthy + end end - it 'wont connect to redis when there are no failures' do - expect(Gitlab::Git::Storage.redis).to receive(:with).once - .and_call_original - expect(circuit_breaker).to receive(:track_storage_accessible) - .and_call_original - - circuit_breaker.track_storage_accessible - end - end + context 'the `failure_wait_time` is set to 0' do + before do + stub_application_setting(circuitbreaker_failure_wait_time: 0) + end - describe '#no_failures?' do - it 'is false when a failure was tracked' do - set_in_redis(:last_failure, Time.now.to_i) - set_in_redis(:failure_count, 1) + it 'is working even when there are failures' do + Timecop.freeze do + set_in_redis(:last_failure, 0.seconds.ago.to_f) + set_in_redis(:failure_count, 90) - expect(circuit_breaker.no_failures?).to be_falsey + expect(circuit_breaker.backing_off?).to be_falsey + end + end end end @@ -320,10 +306,4 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: expect(circuit_breaker.failure_count).to eq(7) end end - - describe '#cache_key' do - it 'includes storage and host' do - expect(circuit_breaker.cache_key).to eq(cache_key) - end - end end diff --git a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb index c708b15853a..39a5d020bb4 100644 --- a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb +++ b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb @@ -33,6 +33,21 @@ describe Gitlab::Git::Storage::ForkedStorageCheck, broken_storage: true, skip_da expect(runtime).to be < 1.0 end + it 'will try the specified amount of times before failing' do + allow(described_class).to receive(:check_filesystem_in_process) do + Process.spawn("sleep 10") + end + + expect(Process).to receive(:spawn).with('sleep 10').twice + .and_call_original + + runtime = Benchmark.realtime do + described_class.storage_available?(existing_path, 0.5, 2) + end + + expect(runtime).to be < 1.0 + end + describe 'when using paths with spaces' do let(:test_dir) { Rails.root.join('tmp', 'tests', 'storage_check') } let(:path_with_spaces) { File.join(test_dir, 'path with spaces') } diff --git a/spec/lib/gitlab/git/storage/health_spec.rb b/spec/lib/gitlab/git/storage/health_spec.rb index 2d3af387971..4a14a5201d1 100644 --- a/spec/lib/gitlab/git/storage/health_spec.rb +++ b/spec/lib/gitlab/git/storage/health_spec.rb @@ -20,36 +20,6 @@ describe Gitlab::Git::Storage::Health, clean_gitlab_redis_shared_state: true, br end end - describe '.load_for_keys' do - let(:subject) do - results = Gitlab::Git::Storage.redis.with do |redis| - fake_future = double - allow(fake_future).to receive(:value).and_return([host1_key]) - described_class.load_for_keys({ 'broken' => fake_future }, redis) - end - - # Make sure the `Redis#future is loaded - results.inject({}) do |result, (name, info)| - info.each { |i| i[:failure_count] = i[:failure_count].value.to_i } - - result[name] = info - - result - end - end - - it 'loads when there is no info in redis' do - expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 0 }]) - end - - it 'reads the correct values for a storage from redis' do - set_in_redis(host1_key, 5) - set_in_redis(host2_key, 7) - - expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 5 }]) - end - end - describe '.for_all_storages' do it 'loads health status for all configured storages' do healths = described_class.for_all_storages diff --git a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb new file mode 100644 index 00000000000..5db37f55e03 --- /dev/null +++ b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe Gitlab::Git::Storage::NullCircuitBreaker do + let(:storage) { 'default' } + let(:hostname) { 'localhost' } + let(:error) { nil } + + subject(:breaker) { described_class.new(storage, hostname, error: error) } + + context 'with an error' do + let(:error) { Gitlab::Git::Storage::Misconfiguration.new('error') } + + describe '#perform' do + it { expect { breaker.perform { 'ok' } }.to raise_error(error) } + end + + describe '#circuit_broken?' do + it { expect(breaker.circuit_broken?).to be_truthy } + end + + describe '#last_failure' do + it { Timecop.freeze { expect(breaker.last_failure).to eq(Time.now) } } + end + + describe '#failure_count' do + it { expect(breaker.failure_count).to eq(breaker.failure_count_threshold) } + end + + describe '#failure_info' do + it { Timecop.freeze { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(Time.now, breaker.failure_count_threshold)) } } + end + end + + context 'not broken' do + describe '#perform' do + it { expect(breaker.perform { 'ok' }).to eq('ok') } + end + + describe '#circuit_broken?' do + it { expect(breaker.circuit_broken?).to be_falsy } + end + + describe '#last_failure' do + it { expect(breaker.last_failure).to be_nil } + end + + describe '#failure_count' do + it { expect(breaker.failure_count).to eq(0) } + end + + describe '#failure_info' do + it { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(nil, 0)) } + end + end + + describe '#failure_count_threshold' do + before do + stub_application_setting(circuitbreaker_failure_count_threshold: 1) + end + + it { expect(breaker.failure_count_threshold).to eq(1) } + end + + it 'implements the CircuitBreaker interface' do + ours = described_class.public_instance_methods + theirs = Gitlab::Git::Storage::CircuitBreaker.public_instance_methods + + expect(theirs - ours).to be_empty + end +end diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb index cc10679ef1e..6c4f538bf01 100644 --- a/spec/lib/gitlab/git/tag_spec.rb +++ b/spec/lib/gitlab/git/tag_spec.rb @@ -29,7 +29,7 @@ describe Gitlab::Git::Tag, seed_helper: true do it_behaves_like 'Gitlab::Git::Repository#tags' end - context 'when Gitaly tags feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly tags feature is disabled', :skip_gitaly_mock do it_behaves_like 'Gitlab::Git::Repository#tags' end end diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index c07a2d91768..86f7bcb8e38 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -20,6 +20,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.flat_path).to eq('encoding') } it { expect(dir.mode).to eq('40000') } context :subdir do @@ -30,6 +31,7 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(subdir.commit_id).to eq(SeedRepo::Commit::ID) } it { expect(subdir.name).to eq('html') } it { expect(subdir.path).to eq('files/html') } + it { expect(subdir.flat_path).to eq('files/html') } end context :subdir_file do @@ -40,6 +42,7 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(subdir_file.commit_id).to eq(SeedRepo::Commit::ID) } it { expect(subdir_file.name).to eq('popen.rb') } it { expect(subdir_file.path).to eq('files/ruby/popen.rb') } + it { expect(subdir_file.flat_path).to eq('files/ruby/popen.rb') } end end diff --git a/spec/lib/gitlab/git/user_spec.rb b/spec/lib/gitlab/git/user_spec.rb new file mode 100644 index 00000000000..eb8db819045 --- /dev/null +++ b/spec/lib/gitlab/git/user_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe Gitlab::Git::User do + let(:username) { 'janedo' } + let(:name) { 'Jane Doe' } + let(:email) { 'janedoe@example.com' } + let(:gl_id) { 'user-123' } + let(:user) do + described_class.new(username, name, email, gl_id) + end + + subject { described_class.new(username, name, email, gl_id) } + + describe '.from_gitaly' do + let(:gitaly_user) do + Gitaly::User.new(gl_username: username, name: name, email: email, gl_id: gl_id) + end + + subject { described_class.from_gitaly(gitaly_user) } + + it { expect(subject).to eq(user) } + end + + describe '.from_gitlab' do + let(:user) { build(:user) } + subject { described_class.from_gitlab(user) } + + it { expect(subject).to eq(described_class.new(user.username, user.name, user.email, 'user-')) } + end + + describe '#==' do + def eq_other(username, name, email, gl_id) + eq(described_class.new(username, name, email, gl_id)) + end + + it { expect(subject).to eq_other(username, name, email, gl_id) } + + it { expect(subject).not_to eq_other(nil, nil, nil, nil) } + it { expect(subject).not_to eq_other(username + 'x', name, email, gl_id) } + it { expect(subject).not_to eq_other(username, name + 'x', email, gl_id) } + it { expect(subject).not_to eq_other(username, name, email + 'x', gl_id) } + it { expect(subject).not_to eq_other(username, name, email, gl_id + 'x') } + end + + describe '#to_gitaly' do + subject { user.to_gitaly } + + it 'creates a Gitaly::User with the correct data' do + expect(subject).to be_a(Gitaly::User) + expect(subject.gl_username).to eq(username) + expect(subject.name).to eq(name) + expect(subject.email).to eq(email) + expect(subject.gl_id).to eq(gl_id) + end + end +end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 458627ee4de..c9643c5da47 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -165,7 +165,7 @@ describe Gitlab::GitAccess do stub_application_setting(rsa_key_restriction: 4096) end - it 'does not allow keys which are too small', aggregate_failures: true do + it 'does not allow keys which are too small', :aggregate_failures do expect(actor).not_to be_valid expect { pull_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.') expect { push_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.') @@ -177,7 +177,7 @@ describe Gitlab::GitAccess do stub_application_setting(rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE) end - it 'does not allow keys which are too small', aggregate_failures: true do + it 'does not allow keys which are too small', :aggregate_failures do expect(actor).not_to be_valid expect { pull_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/) expect { push_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/) @@ -598,6 +598,19 @@ describe Gitlab::GitAccess do admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) end end + + context "when in a read-only GitLab instance" do + before do + create(:protected_branch, name: 'feature', project: project) + allow(Gitlab::Database).to receive(:read_only?) { true } + end + + # Only check admin; if an admin can't do it, other roles can't either + matrix = permissions_matrix[:admin].dup + matrix.each { |key, _| matrix[key] = false } + + run_permission_checks(admin: matrix) + end end describe 'build authentication abilities' do @@ -632,6 +645,16 @@ describe Gitlab::GitAccess do end end + context 'when the repository is read only' do + let(:project) { create(:project, :repository, :read_only) } + + it 'denies push access' do + project.add_master(user) + + expect { push_access_check }.to raise_unauthorized('The repository is temporarily read-only. Please try again later.') + end + end + describe 'deploy key permissions' do let(:key) { create(:deploy_key, user: user, can_push: can_push) } let(:actor) { key } diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 0376b4ee783..1056074264a 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -4,6 +4,7 @@ describe Gitlab::GitAccessWiki do let(:access) { described_class.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) } let(:project) { create(:project, :repository) } let(:user) { create(:user) } + let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] } let(:redirected_path) { nil } let(:authentication_abilities) do [ @@ -13,19 +14,27 @@ describe Gitlab::GitAccessWiki do ] end - describe 'push_allowed?' do - before do - create(:protected_branch, name: 'master', project: project) - project.team << [user, :developer] - end + describe '#push_access_check' do + context 'when user can :create_wiki' do + before do + create(:protected_branch, name: 'master', project: project) + project.team << [user, :developer] + end - subject { access.check('git-receive-pack', changes) } + subject { access.check('git-receive-pack', changes) } - it { expect { subject }.not_to raise_error } - end + it { expect { subject }.not_to raise_error } + + context 'when in a read-only GitLab instance' do + before do + allow(Gitlab::Database).to receive(:read_only?) { true } + end - def changes - ['6f6d7e7ed 570e7b2ab refs/heads/master'] + it 'does not give access to upload wiki code' do + expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "You can't push code to a read-only GitLab instance.") + end + end + end end describe '#access_check_download!' do diff --git a/spec/lib/gitlab/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb index e1fa8ae03f8..ba7fb168a3b 100644 --- a/spec/lib/gitlab/git_ref_validator_spec.rb +++ b/spec/lib/gitlab/git_ref_validator_spec.rb @@ -4,6 +4,7 @@ describe Gitlab::GitRefValidator do it { expect(described_class.validate('feature/new')).to be_truthy } it { expect(described_class.validate('implement_@all')).to be_truthy } it { expect(described_class.validate('my_new_feature')).to be_truthy } + it { expect(described_class.validate('my-branch')).to be_truthy } it { expect(described_class.validate('#1')).to be_truthy } it { expect(described_class.validate('feature/refs/heads/foo')).to be_truthy } it { expect(described_class.validate('feature/~new/')).to be_falsey } @@ -22,4 +23,8 @@ describe Gitlab::GitRefValidator do it { expect(described_class.validate('refs/remotes/')).to be_falsey } it { expect(described_class.validate('refs/heads/feature')).to be_falsey } it { expect(described_class.validate('refs/remotes/origin')).to be_falsey } + it { expect(described_class.validate('-')).to be_falsey } + it { expect(described_class.validate('-branch')).to be_falsey } + it { expect(described_class.validate('.tag')).to be_falsey } + it { expect(described_class.validate('my branch')).to be_falsey } end diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb index 4702a978f19..494dfe0e595 100644 --- a/spec/lib/gitlab/git_spec.rb +++ b/spec/lib/gitlab/git_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'spec_helper' describe Gitlab::Git do @@ -29,4 +30,12 @@ describe Gitlab::Git do end end end + + describe '.ref_name' do + it 'ensure ref is a valid UTF-8 string' do + utf8_invalid_ref = Gitlab::Git::BRANCH_REF_PREFIX + "an_invalid_ref_\xE5" + + expect(described_class.ref_name(utf8_invalid_ref)).to eq("an_invalid_ref_å") + end + end end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index f32fe5d8150..b2275119a04 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -51,6 +51,10 @@ describe Gitlab::GitalyClient::CommitService do expect(ret).to be_kind_of(Gitlab::GitalyClient::DiffStitcher) end + + it 'encodes paths correctly' do + expect { client.diff_from_parent(commit, paths: ['encoding/test.txt', 'encoding/テスト.txt', nil]) }.not_to raise_error + end end describe '#commit_deltas' do @@ -165,4 +169,29 @@ describe Gitlab::GitalyClient::CommitService do expect(subject).to eq("my diff") end end + + describe '#commit_stats' do + let(:request) do + Gitaly::CommitStatsRequest.new( + repository: repository_message, revision: revision + ) + end + let(:response) do + Gitaly::CommitStatsResponse.new( + oid: revision, + additions: 11, + deletions: 15 + ) + end + + subject { described_class.new(repository).commit_stats(revision) } + + it 'sends an RPC request' do + expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:commit_stats) + .with(request, kind_of(Hash)).and_return(response) + + expect(subject.additions).to eq(11) + expect(subject.deletions).to eq(15) + end + end end diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb new file mode 100644 index 00000000000..d9ec28ab02e --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -0,0 +1,126 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::OperationService do + let(:project) { create(:project) } + let(:repository) { project.repository.raw } + let(:client) { described_class.new(repository) } + let(:user) { create(:user) } + let(:gitaly_user) { Gitlab::Git::User.from_gitlab(user).to_gitaly } + + describe '#user_create_branch' do + let(:branch_name) { 'new' } + let(:start_point) { 'master' } + let(:request) do + Gitaly::UserCreateBranchRequest.new( + repository: repository.gitaly_repository, + branch_name: branch_name, + start_point: start_point, + user: gitaly_user + ) + end + let(:gitaly_commit) { build(:gitaly_commit) } + let(:commit_id) { gitaly_commit.id } + let(:gitaly_branch) do + Gitaly::Branch.new(name: branch_name, target_commit: gitaly_commit) + end + let(:response) { Gitaly::UserCreateBranchResponse.new(branch: gitaly_branch) } + let(:commit) { Gitlab::Git::Commit.new(repository, gitaly_commit) } + + subject { client.user_create_branch(branch_name, user, start_point) } + + it 'sends a user_create_branch message and returns a Gitlab::git::Branch' do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_create_branch).with(request, kind_of(Hash)) + .and_return(response) + + expect(subject.name).to eq(branch_name) + expect(subject.dereferenced_target).to eq(commit) + end + + context "when pre_receive_error is present" do + let(:response) do + Gitaly::UserCreateBranchResponse.new(pre_receive_error: "something failed") + end + + it "throws a PreReceive exception" do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_create_branch).with(request, kind_of(Hash)) + .and_return(response) + + expect { subject }.to raise_error( + Gitlab::Git::HooksService::PreReceiveError, "something failed") + end + end + end + + describe '#user_delete_branch' do + let(:branch_name) { 'my-branch' } + let(:request) do + Gitaly::UserDeleteBranchRequest.new( + repository: repository.gitaly_repository, + branch_name: branch_name, + user: gitaly_user + ) + end + let(:response) { Gitaly::UserDeleteBranchResponse.new } + + subject { client.user_delete_branch(branch_name, user) } + + it 'sends a user_delete_branch message' do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_delete_branch).with(request, kind_of(Hash)) + .and_return(response) + + subject + end + + context "when pre_receive_error is present" do + let(:response) do + Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "something failed") + end + + it "throws a PreReceive exception" do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_delete_branch).with(request, kind_of(Hash)) + .and_return(response) + + expect { subject }.to raise_error( + Gitlab::Git::HooksService::PreReceiveError, "something failed") + end + end + end + + describe '#user_ff_branch' do + let(:target_branch) { 'my-branch' } + let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } + let(:request) do + Gitaly::UserFFBranchRequest.new( + repository: repository.gitaly_repository, + branch: target_branch, + commit_id: source_sha, + user: gitaly_user + ) + end + let(:branch_update) do + Gitaly::OperationBranchUpdate.new( + commit_id: source_sha, + repo_created: false, + branch_created: false + ) + end + let(:response) { Gitaly::UserFFBranchResponse.new(branch_update: branch_update) } + + subject { client.user_ff_branch(user, source_sha, target_branch) } + + it 'sends a user_ff_branch message and returns a BranchUpdate object' do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_ff_branch).with(request, kind_of(Hash)) + .and_return(response) + + expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate) + expect(subject.newrev).to eq(source_sha) + expect(subject.repo_created).to be(false) + expect(subject.branch_created).to be(false) + end + end +end diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 6f59750b4da..8127b4842b7 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -84,14 +84,14 @@ describe Gitlab::GitalyClient::RefService do end end - describe '#find_ref_name', seed_helper: true do + describe '#find_ref_name', :seed_helper do subject { client.find_ref_name(SeedRepo::Commit::ID, 'refs/heads/master') } it { is_expected.to be_utf8 } it { is_expected.to eq('refs/heads/master') } end - describe '#ref_exists?', seed_helper: true do + describe '#ref_exists?', :seed_helper do it 'finds the master branch ref' do expect(client.ref_exists?('refs/heads/master')).to eq(true) end diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index fd5f984601e..cbc7ce1c1b0 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -73,4 +73,15 @@ describe Gitlab::GitalyClient::RepositoryService do client.apply_gitattributes(revision) end end + + describe '#has_local_branches?' do + it 'sends a has_local_branches message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:has_local_branches) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(value: true)) + + expect(client.has_local_branches?).to be(true) + end + end end diff --git a/spec/lib/gitlab/gitaly_client/util_spec.rb b/spec/lib/gitlab/gitaly_client/util_spec.rb new file mode 100644 index 00000000000..d1e0136f8c1 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/util_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::Util do + describe '.repository' do + let(:repository_storage) { 'default' } + let(:relative_path) { 'my/repo.git' } + let(:gl_repository) { 'project-1' } + let(:git_object_directory) { '.git/objects' } + let(:git_alternate_object_directory) { ['/dir/one', '/dir/two'] } + + subject do + described_class.repository(repository_storage, relative_path, gl_repository) + end + + it 'creates a Gitaly::Repository with the given data' do + allow(Gitlab::Git::Env).to receive(:[]).with('GIT_OBJECT_DIRECTORY_RELATIVE') + .and_return(git_object_directory) + allow(Gitlab::Git::Env).to receive(:[]).with('GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE') + .and_return(git_alternate_object_directory) + + expect(subject).to be_a(Gitaly::Repository) + expect(subject.storage_name).to eq(repository_storage) + expect(subject.relative_path).to eq(relative_path) + expect(subject.gl_repository).to eq(gl_repository) + expect(subject.git_object_directory).to eq(git_object_directory) + expect(subject.git_alternate_object_directories).to eq(git_alternate_object_directory) + end + end +end diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index 921e786a55c..a1f4e65b8d4 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -38,6 +38,144 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do end end + describe 'encode' do + [ + [nil, ""], + ["", ""], + [" ", " "], + %w(a1 a1), + ["编码", "\xE7\xBC\x96\xE7\xA0\x81".b] + ].each do |input, result| + it "encodes #{input.inspect} to #{result.inspect}" do + expect(described_class.encode(input)).to eq result + end + end + end + + describe 'allow_n_plus_1_calls' do + context 'when RequestStore is enabled', :request_store do + it 'returns the result of the allow_n_plus_1_calls block' do + expect(described_class.allow_n_plus_1_calls { "result" }).to eq("result") + end + end + + context 'when RequestStore is not active' do + it 'returns the result of the allow_n_plus_1_calls block' do + expect(described_class.allow_n_plus_1_calls { "something" }).to eq("something") + end + end + end + + describe 'enforce_gitaly_request_limits?' do + def call_gitaly(count = 1) + (1..count).each do + described_class.enforce_gitaly_request_limits(:test) + end + end + + context 'when RequestStore is enabled', :request_store do + it 'allows up the maximum number of allowed calls' do + expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) }.not_to raise_error + end + + context 'when the maximum number of calls has been reached' do + before do + call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) + end + + it 'fails on the next call' do + expect { call_gitaly(1) }.to raise_error(Gitlab::GitalyClient::TooManyInvocationsError) + end + end + + it 'allows the maximum number of calls to be exceeded within an allow_n_plus_1_calls block' do + expect do + described_class.allow_n_plus_1_calls do + call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) + end + end.not_to raise_error + end + + context 'when the maximum number of calls has been reached within an allow_n_plus_1_calls block' do + before do + described_class.allow_n_plus_1_calls do + call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) + end + end + + it 'allows up to the maximum number of calls outside of an allow_n_plus_1_calls block' do + expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) }.not_to raise_error + end + + it 'does not allow the maximum number of calls to be exceeded outside of an allow_n_plus_1_calls block' do + expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) }.to raise_error(Gitlab::GitalyClient::TooManyInvocationsError) + end + end + end + + context 'when RequestStore is not active' do + it 'does not raise errors when the maximum number of allowed calls is exceeded' do + expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 2) }.not_to raise_error + end + + it 'does not fail when the maximum number of calls is exceeded within an allow_n_plus_1_calls block' do + expect do + described_class.allow_n_plus_1_calls do + call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) + end + end.not_to raise_error + end + end + end + + describe 'get_request_count' do + context 'when RequestStore is enabled', :request_store do + context 'when enforce_gitaly_request_limits is called outside of allow_n_plus_1_calls blocks' do + before do + described_class.enforce_gitaly_request_limits(:call) + end + + it 'counts gitaly calls' do + expect(described_class.get_request_count).to eq(1) + end + end + + context 'when enforce_gitaly_request_limits is called inside and outside of allow_n_plus_1_calls blocks' do + before do + described_class.enforce_gitaly_request_limits(:call) + described_class.allow_n_plus_1_calls do + described_class.enforce_gitaly_request_limits(:call) + end + end + + it 'counts gitaly calls' do + expect(described_class.get_request_count).to eq(2) + end + end + + context 'when reset_counts is called' do + before do + described_class.enforce_gitaly_request_limits(:call) + described_class.reset_counts + end + + it 'resets counts' do + expect(described_class.get_request_count).to eq(0) + end + end + end + + context 'when RequestStore is not active' do + before do + described_class.enforce_gitaly_request_limits(:call) + end + + it 'returns zero' do + expect(described_class.get_request_count).to eq(0) + end + end + end + describe 'feature_enabled?' do let(:feature_name) { 'my_feature' } let(:real_feature_name) { "gitaly_#{feature_name}" } @@ -102,6 +240,22 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) end end + + context "when a feature is not persisted" do + it 'returns false when opt_into_all_features is off' do + allow(Feature).to receive(:persisted?).and_return(false) + allow(described_class).to receive(:opt_into_all_features?).and_return(false) + + expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) + end + + it 'returns true when the override is on' do + allow(Feature).to receive(:persisted?).and_return(false) + allow(described_class).to receive(:opt_into_all_features?).and_return(true) + + expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true) + end + end end context 'when the feature_status is OPT_OUT' do diff --git a/spec/lib/gitlab/github_import/wiki_formatter_spec.rb b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb index fcd90fab547..2662cc20b32 100644 --- a/spec/lib/gitlab/github_import/wiki_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::GithubImport::WikiFormatter do describe '#disk_path' do it 'appends .wiki to project path' do - expect(wiki.disk_path).to eq project.disk_path + '.wiki' + expect(wiki.disk_path).to eq project.wiki.disk_path end end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index b07462e4978..a6c99bc07d4 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -63,6 +63,45 @@ describe Gitlab::Gpg::Commit do it_behaves_like 'returns the cached signature on second call' end + context 'commit signed with a subkey' do + let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User3.emails.first } + + let!(:user) { create(:user, email: GpgHelpers::User3.emails.first) } + + let!(:gpg_key) do + create :gpg_key, key: GpgHelpers::User3.public_key, user: user + end + + let(:gpg_key_subkey) do + gpg_key.subkeys.find_by(fingerprint: '0522DD29B98F167CD8421752E38FFCAF75ABD92A') + end + + before do + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User3.signed_commit_signature, + GpgHelpers::User3.signed_commit_base_data + ] + ) + end + + it 'returns a valid signature' do + expect(described_class.new(commit).signature).to have_attributes( + commit_sha: commit_sha, + project: project, + gpg_key: gpg_key_subkey, + gpg_key_primary_keyid: gpg_key_subkey.keyid, + gpg_key_user_name: GpgHelpers::User3.names.first, + gpg_key_user_email: GpgHelpers::User3.emails.first, + verification_status: 'verified' + ) + end + + it_behaves_like 'returns the cached signature on second call' + end + context 'user email does not match the committer email, but is the same user' do let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first } diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index b9fd4d02156..d6000af0ecd 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -2,17 +2,16 @@ require 'rails_helper' RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do describe '#run' do - let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } - let!(:project) { create :project, :repository, path: 'sample-project' } + let(:signature) { [GpgHelpers::User1.signed_commit_signature, GpgHelpers::User1.signed_commit_base_data] } + let(:committer_email) { GpgHelpers::User1.emails.first } + let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } + let!(:project) { create :project, :repository, path: 'sample-project' } let!(:raw_commit) do raw_commit = double( :raw_commit, - signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ], + signature: signature, sha: commit_sha, - committer_email: GpgHelpers::User1.emails.first + committer_email: committer_email ) allow(raw_commit).to receive :save! @@ -29,12 +28,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do allow(Rugged::Commit).to receive(:extract_signature) .with(Rugged::Repository, commit_sha) - .and_return( - [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ] - ) + .and_return(signature) end context 'gpg signature did have an associated gpg key which was removed later' do @@ -183,5 +177,34 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do ) end end + + context 'gpg signature did not have an associated gpg subkey' do + let(:signature) { [GpgHelpers::User3.signed_commit_signature, GpgHelpers::User3.signed_commit_base_data] } + let(:committer_email) { GpgHelpers::User3.emails.first } + let!(:user) { create :user, email: GpgHelpers::User3.emails.first } + + let!(:invalid_gpg_signature) do + create :gpg_signature, + project: project, + commit_sha: commit_sha, + gpg_key: nil, + gpg_key_primary_keyid: GpgHelpers::User3.subkey_fingerprints.last[24..-1], + verification_status: 'unknown_key' + end + + it 'updates the signature to being valid when the missing gpg key is added' do + # InvalidGpgSignatureUpdater is called by the after_create hook + gpg_key = create(:gpg_key, key: GpgHelpers::User3.public_key, user: user) + subkey = gpg_key.subkeys.last + + expect(invalid_gpg_signature.reload).to have_attributes( + project: project, + commit_sha: commit_sha, + gpg_key_subkey_id: subkey.id, + gpg_key_primary_keyid: subkey.keyid, + verification_status: 'verified' + ) + end + end end end diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 11a2aea1915..ab9a166db00 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -28,6 +28,23 @@ describe Gitlab::Gpg do end end + describe '.subkeys_from_key' do + it 'returns the subkeys by primary key' do + all_subkeys = described_class.subkeys_from_key(GpgHelpers::User1.public_key) + subkeys = all_subkeys[GpgHelpers::User1.primary_keyid] + + expect(subkeys).to be_present + expect(subkeys.first[:keyid]).to be_present + expect(subkeys.first[:fingerprint]).to be_present + end + + it 'returns an empty array when there are not subkeys' do + all_subkeys = described_class.subkeys_from_key(GpgHelpers::User4.public_key) + + expect(all_subkeys[GpgHelpers::User4.primary_keyid]).to be_empty + end + end + describe '.user_infos_from_key' do it 'returns the names and emails' do user_infos = described_class.user_infos_from_key(GpgHelpers::User1.public_key) diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb index 08010c2d0e2..30686634af4 100644 --- a/spec/lib/gitlab/group_hierarchy_spec.rb +++ b/spec/lib/gitlab/group_hierarchy_spec.rb @@ -18,11 +18,22 @@ describe Gitlab::GroupHierarchy, :postgresql do expect(relation).to include(parent, child1) end + it 'can find ancestors upto a certain level' do + relation = described_class.new(Group.where(id: child2)).base_and_ancestors(upto: child1) + + expect(relation).to contain_exactly(child2) + end + it 'uses ancestors_base #initialize argument' do relation = described_class.new(Group.where(id: child2.id), Group.none).base_and_ancestors expect(relation).to include(parent, child1, child2) end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end end describe '#base_and_descendants' do @@ -43,6 +54,33 @@ describe Gitlab::GroupHierarchy, :postgresql do expect(relation).to include(parent, child1, child2) end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end + end + + describe '#descendants' do + it 'includes only the descendants' do + relation = described_class.new(Group.where(id: parent)).descendants + + expect(relation).to contain_exactly(child1, child2) + end + end + + describe '#ancestors' do + it 'includes only the ancestors' do + relation = described_class.new(Group.where(id: child2)).ancestors + + expect(relation).to contain_exactly(child1, parent) + end + + it 'can find ancestors upto a certain level' do + relation = described_class.new(Group.where(id: child2)).ancestors(upto: child1) + + expect(relation).to be_empty + end end describe '#all_groups' do @@ -73,5 +111,10 @@ describe Gitlab::GroupHierarchy, :postgresql do expect(relation).to include(child2) end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end end 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 index f5c9680bf59..4c1ca4349ea 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do let(:metric_class) { Gitlab::HealthChecks::Metric } let(:result_class) { Gitlab::HealthChecks::Result } - let(:repository_storages) { [:default] } + let(:repository_storages) { ['default'] } let(:tmp_dir) { Dir.mktmpdir } let(:storages_paths) do @@ -44,7 +44,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do describe '#readiness' do subject { described_class.readiness } - context 'storage has a tripped circuitbreaker', broken_storage: true do + context 'storage has a tripped circuitbreaker', :broken_storage do let(:repository_storages) { ['broken'] } let(:storages_paths) do Gitlab.config.repositories.storages @@ -64,7 +64,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do allow(described_class).to receive(:storage_circuitbreaker_test) { true } end - it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) } + 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 @@ -72,7 +72,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do FileUtils.chmod_R(0755, tmp_dir) end - it { is_expected.to include(result_class.new(true, nil, shard: :default)) } + 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 @@ -85,7 +85,7 @@ describe Gitlab::HealthChecks::FsShardsCheck 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)) } + it { is_expected.to include(result_class.new(false, 'cannot read from storage', shard: 'default')) } end context 'write test fails' do @@ -93,7 +93,7 @@ describe Gitlab::HealthChecks::FsShardsCheck 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)) } + it { is_expected.to include(result_class.new(false, 'cannot write to storage', shard: 'default')) } end end end @@ -109,7 +109,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do it 'provides metrics' do metrics = described_class.metrics - expect(metrics).to all(have_attributes(labels: { shard: :default })) + expect(metrics).to all(have_attributes(labels: { shard: 'default' })) expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) @@ -128,7 +128,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do it 'provides metrics' do metrics = described_class.metrics - expect(metrics).to all(have_attributes(labels: { shard: :default })) + expect(metrics).to all(have_attributes(labels: { shard: 'default' })) expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 1)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 1)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 1)) @@ -156,14 +156,14 @@ describe Gitlab::HealthChecks::FsShardsCheck do describe '#readiness' do subject { described_class.readiness } - it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) } + it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: 'default')) } end describe '#metrics' do it 'provides metrics' do metrics = described_class.metrics - expect(metrics).to all(have_attributes(labels: { shard: :default })) + expect(metrics).to all(have_attributes(labels: { shard: 'default' })) expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb new file mode 100644 index 00000000000..30da56bec16 --- /dev/null +++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +describe Gitlab::HookData::IssuableBuilder do + set(:user) { create(:user) } + + # This shared example requires a `builder` and `user` variable + shared_examples 'issuable hook data' do |kind| + let(:data) { builder.build(user: user) } + + include_examples 'project hook data' do + let(:project) { builder.issuable.project } + end + include_examples 'deprecated repository hook data' + + context "with a #{kind}" do + it 'contains issuable data' do + expect(data[:object_kind]).to eq(kind) + expect(data[:user]).to eq(user.hook_attrs) + expect(data[:project]).to eq(builder.issuable.project.hook_attrs) + expect(data[:object_attributes]).to eq(builder.issuable.hook_attrs) + expect(data[:changes]).to eq({}) + expect(data[:repository]).to eq(builder.issuable.project.hook_attrs.slice(:name, :url, :description, :homepage)) + end + + it 'does not contain certain keys' do + expect(data).not_to have_key(:assignees) + expect(data).not_to have_key(:assignee) + end + + describe 'changes are given' do + let(:changes) do + { + cached_markdown_version: %w[foo bar], + description: ['A description', 'A cool description'], + description_html: %w[foo bar], + in_progress_merge_commit_sha: %w[foo bar], + lock_version: %w[foo bar], + merge_jid: %w[foo bar], + title: ['A title', 'Hello World'], + title_html: %w[foo bar], + labels: [ + [{ id: 1, title: 'foo' }], + [{ id: 1, title: 'foo' }, { id: 2, title: 'bar' }] + ] + } + end + let(:data) { builder.build(user: user, changes: changes) } + + it 'populates the :changes hash' do + expect(data[:changes]).to match(hash_including({ + title: { previous: 'A title', current: 'Hello World' }, + description: { previous: 'A description', current: 'A cool description' }, + labels: { + previous: [{ id: 1, title: 'foo' }], + current: [{ id: 1, title: 'foo' }, { id: 2, title: 'bar' }] + } + })) + end + + it 'does not contain certain keys' do + expect(data[:changes]).not_to have_key('cached_markdown_version') + expect(data[:changes]).not_to have_key('description_html') + expect(data[:changes]).not_to have_key('lock_version') + expect(data[:changes]).not_to have_key('title_html') + expect(data[:changes]).not_to have_key('in_progress_merge_commit_sha') + expect(data[:changes]).not_to have_key('merge_jid') + end + end + end + end + + describe '#build' do + it_behaves_like 'issuable hook data', 'issue' do + let(:issuable) { create(:issue, description: 'A description') } + let(:builder) { described_class.new(issuable) } + end + + it_behaves_like 'issuable hook data', 'merge_request' do + let(:issuable) { create(:merge_request, description: 'A description') } + let(:builder) { described_class.new(issuable) } + end + + context 'issue is assigned' do + let(:issue) { create(:issue, assignees: [user]) } + let(:data) { described_class.new(issue).build(user: user) } + + it 'returns correct hook data' do + expect(data[:object_attributes]['assignee_id']).to eq(user.id) + expect(data[:assignees].first).to eq(user.hook_attrs) + expect(data).not_to have_key(:assignee) + end + end + + context 'merge_request is assigned' do + let(:merge_request) { create(:merge_request, assignee: user) } + let(:data) { described_class.new(merge_request).build(user: user) } + + it 'returns correct hook data' do + expect(data[:object_attributes]['assignee_id']).to eq(user.id) + expect(data[:assignee]).to eq(user.hook_attrs) + expect(data).not_to have_key(:assignees) + end + end + end +end diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb new file mode 100644 index 00000000000..6c529cdd051 --- /dev/null +++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::HookData::IssueBuilder do + set(:issue) { create(:issue) } + let(:builder) { described_class.new(issue) } + + describe '#build' do + let(:data) { builder.build } + + it 'includes safe attribute' do + %w[ + assignee_id + author_id + branch_name + closed_at + confidential + created_at + deleted_at + description + due_date + id + iid + last_edited_at + last_edited_by_id + milestone_id + moved_to_id + project_id + relative_position + state + time_estimate + title + updated_at + updated_by_id + ].each do |key| + expect(data).to include(key) + end + end + + it 'includes additional attrs' do + expect(data).to include(:total_time_spent) + expect(data).to include(:human_time_estimate) + expect(data).to include(:human_total_time_spent) + expect(data).to include(:assignee_ids) + end + end +end diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb new file mode 100644 index 00000000000..92bf87bbad4 --- /dev/null +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Gitlab::HookData::MergeRequestBuilder do + set(:merge_request) { create(:merge_request) } + let(:builder) { described_class.new(merge_request) } + + describe '#build' do + let(:data) { builder.build } + + it 'includes safe attribute' do + %w[ + assignee_id + author_id + created_at + deleted_at + description + head_pipeline_id + id + iid + last_edited_at + last_edited_by_id + merge_commit_sha + merge_error + merge_params + merge_status + merge_user_id + merge_when_pipeline_succeeds + milestone_id + ref_fetched + source_branch + source_project_id + state + target_branch + target_project_id + time_estimate + title + updated_at + updated_by_id + ].each do |key| + expect(data).to include(key) + end + end + + %i[source target].each do |key| + describe "#{key} key" do + include_examples 'project hook data', project_key: key do + let(:project) { merge_request.public_send("#{key}_project") } + end + end + end + + it 'includes additional attrs' do + expect(data).to include(:source) + expect(data).to include(:target) + expect(data).to include(:last_commit) + expect(data).to include(:work_in_progress) + expect(data).to include(:total_time_spent) + expect(data).to include(:human_time_estimate) + expect(data).to include(:human_total_time_spent) + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index beed4e77e8b..6c6b9154a0a 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -147,6 +147,10 @@ deploy_keys: - user - deploy_keys_projects - projects +cluster: +- project +- user +- service services: - project - service_hook @@ -177,6 +181,7 @@ project: - tag_taggings - tags - chat_services +- cluster - creator - group - namespace @@ -190,6 +195,7 @@ project: - mattermost_slash_commands_service - slack_slash_commands_service - irker_service +- packagist_service - pivotaltracker_service - prometheus_service - hipchat_service @@ -256,6 +262,7 @@ project: - environments - deployments - project_feature +- auto_devops - pages_domains - authorized_users - project_authorizations @@ -265,6 +272,10 @@ project: - container_repositories - uploads - members_and_requesters +- build_trace_section_names +- root_of_fork_network +- fork_network_member +- fork_network award_emoji: - awardable - user @@ -276,3 +287,6 @@ timelogs: - user push_event_payload: - event +issue_assignees: +- issue +- assignee
\ No newline at end of file diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb index c7fbc2bc92f..dd0ce0dae41 100644 --- a/spec/lib/gitlab/import_export/fork_spec.rb +++ b/spec/lib/gitlab/import_export/fork_spec.rb @@ -1,13 +1,15 @@ require 'spec_helper' describe 'forked project import' do + include ProjectForksHelper + let(:user) { create(:user) } let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } let!(:project) { create(: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.full_path) } let(:forked_from_project) { create(:project, :repository) } - let(:fork_link) { create(:forked_project_link, forked_from_project: project_with_repo) } + let(:forked_project) { fork_project(project_with_repo, nil, repository: true) } let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } @@ -16,7 +18,7 @@ describe 'forked project import' do end let!(:merge_request) do - create(:merge_request, source_project: fork_link.forked_to_project, target_project: project_with_repo) + create(:merge_request, source_project: forked_project, target_project: project_with_repo) end let(:saver) do diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb index 4d87f27ce05..473ba40fae7 100644 --- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -1,13 +1,14 @@ require 'spec_helper' describe Gitlab::ImportExport::MergeRequestParser do + include ProjectForksHelper + let(:user) { create(:user) } let!(:project) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } - let(:forked_from_project) { create(:project, :repository) } - let(:fork_link) { create(:forked_project_link, forked_from_project: project) } + let(:forked_project) { fork_project(project) } let!(:merge_request) do - create(:merge_request, source_project: fork_link.forked_to_project, target_project: project) + create(:merge_request, source_project: forked_project, target_project: project) end let(:parsed_merge_request) do diff --git a/spec/lib/gitlab/import_export/project.group.json b/spec/lib/gitlab/import_export/project.group.json new file mode 100644 index 00000000000..82a1fbd2fc5 --- /dev/null +++ b/spec/lib/gitlab/import_export/project.group.json @@ -0,0 +1,188 @@ +{ + "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", + "visibility_level": 10, + "archived": false, + "milestones": [ + { + "id": 1, + "title": "Project milestone", + "project_id": 8, + "description": "Project-level milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "group_id": null + } + ], + "labels": [ + { + "id": 2, + "title": "project label", + "color": "#428bca", + "project_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "type": "ProjectLabel", + "priorities": [ + { + "id": 1, + "project_id": 5, + "label_id": 1, + "priority": 1, + "created_at": "2016-10-18T09:35:43.338Z", + "updated_at": "2016-10-18T09:35:43.338Z" + } + ] + } + ], + "issues": [ + { + "id": 1, + "title": "Fugiat est minima quae maxime non similique.", + "assignee_id": null, + "project_id": 8, + "author_id": 1, + "created_at": "2017-07-07T18:13:01.138Z", + "updated_at": "2017-08-15T18:37:40.807Z", + "branch_name": null, + "description": "Quam totam fuga numquam in eveniet.", + "state": "opened", + "iid": 1, + "updated_by_id": 1, + "confidential": false, + "deleted_at": null, + "due_date": null, + "moved_to_id": null, + "lock_version": null, + "time_estimate": 0, + "closed_at": null, + "last_edited_at": null, + "last_edited_by_id": null, + "group_milestone_id": null, + "milestone": { + "id": 1, + "title": "Project milestone", + "project_id": 8, + "description": "Project-level milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "group_id": null + }, + "label_links": [ + { + "id": 11, + "label_id": 6, + "target_id": 1, + "target_type": "Issue", + "created_at": "2017-08-15T18:37:40.795Z", + "updated_at": "2017-08-15T18:37:40.795Z", + "label": { + "id": 6, + "title": "group label", + "color": "#A8D695", + "project_id": null, + "created_at": "2017-08-15T18:37:19.698Z", + "updated_at": "2017-08-15T18:37:19.698Z", + "template": false, + "description": "", + "group_id": 5, + "type": "GroupLabel", + "priorities": [] + } + }, + { + "id": 11, + "label_id": 2, + "target_id": 1, + "target_type": "Issue", + "created_at": "2017-08-15T18:37:40.795Z", + "updated_at": "2017-08-15T18:37:40.795Z", + "label": { + "id": 6, + "title": "project label", + "color": "#A8D695", + "project_id": null, + "created_at": "2017-08-15T18:37:19.698Z", + "updated_at": "2017-08-15T18:37:19.698Z", + "template": false, + "description": "", + "group_id": 5, + "type": "ProjectLabel", + "priorities": [] + } + } + ] + }, + { + "id": 2, + "title": "Fugiat est minima quae maxime non similique.", + "assignee_id": null, + "project_id": 8, + "author_id": 1, + "created_at": "2017-07-07T18:13:01.138Z", + "updated_at": "2017-08-15T18:37:40.807Z", + "branch_name": null, + "description": "Quam totam fuga numquam in eveniet.", + "state": "opened", + "iid": 2, + "updated_by_id": 1, + "confidential": false, + "deleted_at": null, + "due_date": null, + "moved_to_id": null, + "lock_version": null, + "time_estimate": 0, + "closed_at": null, + "last_edited_at": null, + "last_edited_by_id": null, + "group_milestone_id": null, + "milestone": { + "id": 2, + "title": "A group milestone", + "description": "Group-level milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "group_id": 100 + }, + "label_links": [ + { + "id": 11, + "label_id": 2, + "target_id": 1, + "target_type": "Issue", + "created_at": "2017-08-15T18:37:40.795Z", + "updated_at": "2017-08-15T18:37:40.795Z", + "label": { + "id": 2, + "title": "project label", + "color": "#A8D695", + "project_id": null, + "created_at": "2017-08-15T18:37:19.698Z", + "updated_at": "2017-08-15T18:37:19.698Z", + "template": false, + "description": "", + "group_id": 5, + "type": "ProjectLabel", + "priorities": [] + } + } + ] + } + ], + "snippets": [ + + ], + "hooks": [ + + ] +} diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 331b7cf2fea..9a68bbb379c 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -43,7 +43,7 @@ "issues": [ { "id": 40, - "title": "Voluptatem amet doloribus deleniti eos maxime repudiandae molestias.", + "title": "Voluptatem", "assignee_id": 1, "author_id": 22, "project_id": 5, @@ -60,6 +60,12 @@ "due_date": null, "moved_to_id": null, "test_ee_field": "test", + "issue_assignees": [ + { + "user_id": 1, + "issue_id": 1 + } + ], "milestone": { "id": 1, "title": "test milestone", @@ -75,8 +81,6 @@ "id": 487, "target_type": "Milestone", "target_id": 1, - "title": null, - "data": null, "project_id": 46, "created_at": "2016-06-14T15:02:04.418Z", "updated_at": "2016-06-14T15:02:04.418Z", @@ -364,8 +368,6 @@ "id": 487, "target_type": "Milestone", "target_id": 1, - "title": null, - "data": null, "project_id": 46, "created_at": "2016-06-14T15:02:04.418Z", "updated_at": "2016-06-14T15:02:04.418Z", @@ -2311,8 +2313,6 @@ "id": 487, "target_type": "Milestone", "target_id": 1, - "title": null, - "data": null, "project_id": 46, "created_at": "2016-06-14T15:02:04.418Z", "updated_at": "2016-06-14T15:02:04.418Z", @@ -2336,8 +2336,6 @@ "id": 240, "target_type": "Milestone", "target_id": 20, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:04.593Z", "updated_at": "2016-06-14T15:02:04.593Z", @@ -2348,8 +2346,6 @@ "id": 60, "target_type": "Milestone", "target_id": 20, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:04.593Z", "updated_at": "2016-06-14T15:02:04.593Z", @@ -2373,8 +2369,6 @@ "id": 241, "target_type": "Milestone", "target_id": 19, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:04.585Z", "updated_at": "2016-06-14T15:02:04.585Z", @@ -2385,41 +2379,6 @@ "id": 59, "target_type": "Milestone", "target_id": 19, - "title": null, - "data": { - "object_kind": "push", - "before": "0000000000000000000000000000000000000000", - "after": "de990aa15829d0ab182ad5a55b4c527846c0d39c", - "ref": "refs/heads/removable-group-owner", - "checkout_sha": "de990aa15829d0ab182ad5a55b4c527846c0d39c", - "message": null, - "user_id": 273486, - "user_name": "James Lopez", - "user_email": "james@jameslopez.es", - "project_id": 562317, - "repository": { - "name": "GitLab Community Edition", - "url": "git@gitlab.com:james11/gitlab-ce.git", - "description": "Version Control on your Server. See http://gitlab.org/gitlab-ce/ and the README for more information", - "homepage": "https://gitlab.com/james11/gitlab-ce", - "git_http_url": "https://gitlab.com/james11/gitlab-ce.git", - "git_ssh_url": "git@gitlab.com:james11/gitlab-ce.git", - "visibility_level": 20 - }, - "commits": [ - { - "id": "de990aa15829d0ab182ad5a55b4c527846c0d39c", - "message": "fixed last group owner issue and added test\\n", - "timestamp": "2015-10-29T16:10:27+00:00", - "url": "https://gitlab.com/james11/gitlab-ce/commit/de990aa15829d0ab182ad5a55b4c527846c0d39c", - "author": { - "name": "James Lopez", - "email": "james.lopez@vodafone.com" - } - } - ], - "total_commits_count": 1 - }, "project_id": 5, "created_at": "2016-06-14T15:02:04.585Z", "updated_at": "2016-06-14T15:02:04.585Z", @@ -2947,8 +2906,6 @@ "id": 221, "target_type": "MergeRequest", "target_id": 27, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:36.703Z", "updated_at": "2016-06-14T15:02:36.703Z", @@ -2959,8 +2916,6 @@ "id": 187, "target_type": "MergeRequest", "target_id": 27, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:36.703Z", "updated_at": "2016-06-14T15:02:36.703Z", @@ -3230,8 +3185,6 @@ "id": 222, "target_type": "MergeRequest", "target_id": 26, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:36.496Z", "updated_at": "2016-06-14T15:02:36.496Z", @@ -3242,8 +3195,6 @@ "id": 186, "target_type": "MergeRequest", "target_id": 26, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:36.496Z", "updated_at": "2016-06-14T15:02:36.496Z", @@ -3513,8 +3464,6 @@ "id": 223, "target_type": "MergeRequest", "target_id": 15, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:25.262Z", "updated_at": "2016-06-14T15:02:25.262Z", @@ -3525,8 +3474,6 @@ "id": 175, "target_type": "MergeRequest", "target_id": 15, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:25.262Z", "updated_at": "2016-06-14T15:02:25.262Z", @@ -4202,8 +4149,6 @@ "id": 224, "target_type": "MergeRequest", "target_id": 14, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:25.113Z", "updated_at": "2016-06-14T15:02:25.113Z", @@ -4214,8 +4159,6 @@ "id": 174, "target_type": "MergeRequest", "target_id": 14, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:25.113Z", "updated_at": "2016-06-14T15:02:25.113Z", @@ -4274,9 +4217,7 @@ { "id": 529, "target_type": "Note", - "target_id": 2521, - "title": "test levels", - "data": null, + "target_id": 793, "project_id": 4, "created_at": "2016-07-07T14:35:12.128Z", "updated_at": "2016-07-07T14:35:12.128Z", @@ -4749,8 +4690,6 @@ "id": 225, "target_type": "MergeRequest", "target_id": 13, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:24.636Z", "updated_at": "2016-06-14T15:02:24.636Z", @@ -4761,8 +4700,6 @@ "id": 173, "target_type": "MergeRequest", "target_id": 13, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:24.636Z", "updated_at": "2016-06-14T15:02:24.636Z", @@ -5247,8 +5184,6 @@ "id": 226, "target_type": "MergeRequest", "target_id": 12, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:24.253Z", "updated_at": "2016-06-14T15:02:24.253Z", @@ -5259,8 +5194,6 @@ "id": 172, "target_type": "MergeRequest", "target_id": 12, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:24.253Z", "updated_at": "2016-06-14T15:02:24.253Z", @@ -5506,8 +5439,6 @@ "id": 227, "target_type": "MergeRequest", "target_id": 11, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:23.865Z", "updated_at": "2016-06-14T15:02:23.865Z", @@ -5518,8 +5449,6 @@ "id": 171, "target_type": "MergeRequest", "target_id": 11, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:23.865Z", "updated_at": "2016-06-14T15:02:23.865Z", @@ -6195,8 +6124,6 @@ "id": 228, "target_type": "MergeRequest", "target_id": 10, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:23.660Z", "updated_at": "2016-06-14T15:02:23.660Z", @@ -6207,8 +6134,6 @@ "id": 170, "target_type": "MergeRequest", "target_id": 10, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:23.660Z", "updated_at": "2016-06-14T15:02:23.660Z", @@ -6478,8 +6403,6 @@ "id": 229, "target_type": "MergeRequest", "target_id": 9, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:22.927Z", "updated_at": "2016-06-14T15:02:22.927Z", @@ -6490,8 +6413,6 @@ "id": 169, "target_type": "MergeRequest", "target_id": 9, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:22.927Z", "updated_at": "2016-06-14T15:02:22.927Z", diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json index 2d8f3d4a566..02450478a77 100644 --- a/spec/lib/gitlab/import_export/project.light.json +++ b/spec/lib/gitlab/import_export/project.light.json @@ -5,9 +5,9 @@ "milestones": [ { "id": 1, - "title": "test milestone", + "title": "Project milestone", "project_id": 8, - "description": "test milestone", + "description": "Project-level milestone", "due_date": null, "created_at": "2016-06-14T15:02:04.415Z", "updated_at": "2016-06-14T15:02:04.415Z", @@ -19,7 +19,7 @@ "labels": [ { "id": 2, - "title": "test2", + "title": "A project label", "color": "#428bca", "project_id": 8, "created_at": "2016-07-22T08:55:44.161Z", @@ -63,30 +63,21 @@ "last_edited_at": null, "last_edited_by_id": null, "group_milestone_id": null, + "milestone": { + "id": 1, + "title": "Project milestone", + "project_id": 8, + "description": "Project-level milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "group_id": null + }, "label_links": [ { "id": 11, - "label_id": 6, - "target_id": 1, - "target_type": "Issue", - "created_at": "2017-08-15T18:37:40.795Z", - "updated_at": "2017-08-15T18:37:40.795Z", - "label": { - "id": 6, - "title": "group label", - "color": "#A8D695", - "project_id": null, - "created_at": "2017-08-15T18:37:19.698Z", - "updated_at": "2017-08-15T18:37:19.698Z", - "template": false, - "description": "", - "group_id": 5, - "type": "GroupLabel", - "priorities": [] - } - }, - { - "id": 11, "label_id": 2, "target_id": 1, "target_type": "Issue", @@ -94,14 +85,14 @@ "updated_at": "2017-08-15T18:37:40.795Z", "label": { "id": 6, - "title": "project label", + "title": "Another project label", "color": "#A8D695", "project_id": null, "created_at": "2017-08-15T18:37:19.698Z", "updated_at": "2017-08-15T18:37:19.698Z", "template": false, "description": "", - "group_id": 5, + "group_id": null, "type": "ProjectLabel", "priorities": [] } @@ -109,10 +100,6 @@ ] } ], - "snippets": [ - - ], - "hooks": [ - - ] + "snippets": [], + "hooks": [] } 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 d664d371028..76b01b6a1ec 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do context 'JSON' do it 'restores models based on JSON' do - expect(@restored_project_json).to be true + expect(@restored_project_json).to be_truthy end it 'restore correct project features' do @@ -57,16 +57,16 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(Ci::Pipeline.where(ref: nil)).not_to be_empty end - it 'restores the correct event with symbolised data' do - expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty - end - it 'preserves updated_at on issues' do issue = Issue.where(description: 'Aliquam enim illo et possimus.').first expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC') end + it 'has issue assignees' do + expect(Issue.where(title: 'Voluptatem').first.issue_assignees).not_to be_empty + end + it 'contains the merge access levels on a protected branch' do expect(ProtectedBranch.first.merge_access_levels).not_to be_empty end @@ -80,7 +80,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end context 'event at forth level of the tree' do - let(:event) { Event.where(title: 'test levels').first } + let(:event) { Event.where(action: 6).first } it 'restores the event' do expect(event).not_to be_nil @@ -186,6 +186,53 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end end + shared_examples 'restores project successfully' do + it 'correctly restores project' do + expect(shared.errors).to be_empty + expect(restored_project_json).to be_truthy + end + end + + shared_examples 'restores project correctly' do |**results| + it 'has labels' do + expect(project.labels.size).to eq(results.fetch(:labels, 0)) + end + + it 'has label priorities' do + expect(project.labels.first.priorities).not_to be_empty + end + + it 'has milestones' do + expect(project.milestones.size).to eq(results.fetch(:milestones, 0)) + end + + it 'has issues' do + expect(project.issues.size).to eq(results.fetch(:issues, 0)) + end + + it 'has issue with group label and project label' do + labels = project.issues.first.labels + + expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0)) + end + end + + shared_examples 'restores group correctly' do |**results| + it 'has group label' do + expect(project.group.labels.size).to eq(results.fetch(:labels, 0)) + end + + it 'has group milestone' do + expect(project.group.milestones.size).to eq(results.fetch(:milestones, 0)) + end + + it 'has issue with group label' do + labels = project.issues.first.labels + + expect(labels.where(type: "GroupLabel").count).to eq(results.fetch(:first_issue_labels, 0)) + end + end + context 'Light JSON' do let(:user) { create(:user) } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } @@ -194,33 +241,45 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do let(:restored_project_json) { project_tree_restorer.restore } before do - project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json") - allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') end - context 'project.json file access check' do - it 'does not read a symlink' do - Dir.mktmpdir do |tmpdir| - setup_symlink(tmpdir, 'project.json') - allow(shared).to receive(:export_path).and_call_original + context 'with a simple project' do + before do + project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json") + + restored_project_json + end - restored_project_json + it_behaves_like 'restores project correctly', + issues: 1, + labels: 1, + milestones: 1, + first_issue_labels: 1 - expect(shared.errors.first).to be_nil + context 'project.json file access check' do + it 'does not read a symlink' do + Dir.mktmpdir do |tmpdir| + setup_symlink(tmpdir, 'project.json') + allow(shared).to receive(:export_path).and_call_original + + restored_project_json + + expect(shared.errors).to be_empty + end end end - end - context 'when there is an existing build with build token' do - it 'restores project json correctly' do - create(:ci_build, token: 'abcd') + context 'when there is an existing build with build token' do + before do + create(:ci_build, token: 'abcd') + end - expect(restored_project_json).to be true + it_behaves_like 'restores project successfully' end end - context 'with group' do + context 'with a project that has a group' do let!(:project) do create(:project, :builds_disabled, @@ -231,43 +290,22 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end before do - project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json") + project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.group.json") restored_project_json end - it 'correctly restores project' do - expect(restored_project_json).to be_truthy - expect(shared.errors).to be_empty - end + it_behaves_like 'restores project successfully' + it_behaves_like 'restores project correctly', + issues: 2, + labels: 1, + milestones: 1, + first_issue_labels: 1 - it 'has labels' do - expect(project.labels.count).to eq(2) - end - - it 'creates group label' do - expect(project.group.labels.count).to eq(1) - end - - it 'has label priorities' do - expect(project.labels.first.priorities).not_to be_empty - end - - it 'has milestones' do - expect(project.milestones.count).to eq(1) - end - - it 'has issue' do - expect(project.issues.count).to eq(1) - expect(project.issues.first.labels.count).to eq(2) - end - - it 'has issue with group label and project label' do - labels = project.issues.first.labels - - expect(labels.where(type: "GroupLabel").count).to eq(1) - expect(labels.where(type: "ProjectLabel").count).to eq(1) - end + it_behaves_like 'restores group correctly', + labels: 1, + milestones: 1, + first_issue_labels: 1 end end 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 8e3554375e8..8da768ebd07 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -77,6 +77,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(saved_project_json['issues'].first['notes']).not_to be_empty end + it 'has issue assignees' do + expect(saved_project_json['issues'].first['issue_assignees']).not_to be_empty + end + it 'has author on issue comments' do expect(saved_project_json['issues'].first['notes'].first['author']).not_to be_empty end @@ -119,7 +123,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do it 'has no when YML attributes but only the DB column' do allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))) - expect_any_instance_of(Ci::GitlabCiYamlProcessor).not_to receive(:build_attributes) + expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes) saved_project_json end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 2f0723b658f..da8202ca668 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -25,12 +25,11 @@ Issue: - relative_position - last_edited_at - last_edited_by_id +- discussion_locked Event: - id - target_type - target_id -- title -- data - project_id - created_at - updated_at @@ -170,6 +169,7 @@ MergeRequest: - last_edited_at - last_edited_by_id - head_pipeline_id +- discussion_locked MergeRequestDiff: - id - state @@ -226,6 +226,8 @@ Ci::Pipeline: - lock_version - auto_canceled_by_id - pipeline_schedule_id +- config_source +- failure_reason - protected Ci::Stage: - id @@ -312,6 +314,32 @@ Ci::PipelineSchedule: - deleted_at - created_at - updated_at +Gcp::Cluster: +- id +- project_id +- user_id +- service_id +- enabled +- status +- status_reason +- project_namespace +- endpoint +- ca_cert +- encrypted_kubernetes_token +- encrypted_kubernetes_token_iv +- username +- encrypted_password +- encrypted_password_iv +- gcp_project_id +- gcp_cluster_zone +- gcp_cluster_name +- gcp_cluster_size +- gcp_machine_type +- gcp_operation_id +- encrypted_gcp_token +- encrypted_gcp_token_iv +- created_at +- updated_at DeployKey: - id - user_id @@ -414,6 +442,8 @@ Project: - last_repository_updated_at - ci_config_path - delete_error +- merge_requests_ff_only_enabled +- merge_requests_rebase_enabled Author: - name ProjectFeature: @@ -467,5 +497,16 @@ Timelog: - merge_request_id - issue_id - user_id +- spent_at - created_at - updated_at +ProjectAutoDevops: +- id +- enabled +- domain +- project_id +- created_at +- updated_at +IssueAssignee: +- user_id +- issue_id
\ No newline at end of file diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb index 8370adf9211..1785094af10 100644 --- a/spec/lib/gitlab/ldap/auth_hash_spec.rb +++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::LDAP::AuthHash do let(:auth_hash) do described_class.new( OmniAuth::AuthHash.new( - uid: '123456', + uid: given_uid, provider: 'ldapmain', info: info, extra: { @@ -32,6 +32,8 @@ describe Gitlab::LDAP::AuthHash do end context "without overridden attributes" do + let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' } + it "has the correct username" do expect(auth_hash.username).to eq("123456") end @@ -42,6 +44,8 @@ describe Gitlab::LDAP::AuthHash do end context "with overridden attributes" do + let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' } + let(:attributes) do { 'username' => %w(mail email), @@ -61,4 +65,22 @@ describe Gitlab::LDAP::AuthHash do expect(auth_hash.name).to eq("John Smith") end end + + describe '#uid' do + context 'when there is extraneous (but valid) whitespace' do + let(:given_uid) { 'uid =john smith , ou = people, dc= example,dc =com' } + + it 'removes the extraneous whitespace' do + expect(auth_hash.uid).to eq('uid=john smith,ou=people,dc=example,dc=com') + end + end + + context 'when there are upper case characters' do + let(:given_uid) { 'UID=John Smith,ou=People,dc=example,dc=com' } + + it 'downcases' do + expect(auth_hash.uid).to eq('uid=john smith,ou=people,dc=example,dc=com') + end + end + end end diff --git a/spec/lib/gitlab/ldap/authentication_spec.rb b/spec/lib/gitlab/ldap/authentication_spec.rb index 01b6282af0c..9d57a46c12b 100644 --- a/spec/lib/gitlab/ldap/authentication_spec.rb +++ b/spec/lib/gitlab/ldap/authentication_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe Gitlab::LDAP::Authentication do - let(:user) { create(:omniauth_user, extern_uid: dn) } - let(:dn) { 'uid=john,ou=people,dc=example,dc=com' } + let(:dn) { 'uid=John Smith, ou=People, dc=example, dc=com' } + let(:user) { create(:omniauth_user, extern_uid: Gitlab::LDAP::Person.normalize_dn(dn)) } let(:login) { 'john' } let(:password) { 'password' } diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb new file mode 100644 index 00000000000..8e21ecdf9ab --- /dev/null +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -0,0 +1,224 @@ +require 'spec_helper' + +describe Gitlab::LDAP::DN do + using RSpec::Parameterized::TableSyntax + + describe '#normalize_value' do + subject { described_class.normalize_value(given) } + + it_behaves_like 'normalizes a DN attribute value' + + context 'when the given DN is malformed' do + context 'when ending with a comma' do + let(:given) { 'John Smith,' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + end + end + + context 'when given a BER encoded attribute value with a space in it' do + let(:given) { '#aa aa' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"") + end + end + + context 'when given a BER encoded attribute value with a non-hex character in it' do + let(:given) { '#aaXaaa' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"") + end + end + + context 'when given a BER encoded attribute value with a non-hex character in it' do + let(:given) { '#aaaYaa' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"") + end + end + + context 'when given a hex pair with a non-hex character in it, inside double quotes' do + let(:given) { '"Sebasti\\cX\\a1n"' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"") + end + end + + context 'with an open (as opposed to closed) double quote' do + let(:given) { '"James' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + end + end + + context 'with an invalid escaped hex code' do + let(:given) { 'J\ames' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"') + end + end + + context 'with a value ending with the escape character' do + let(:given) { 'foo\\' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + end + end + end + end + + describe '#to_normalized_s' do + subject { described_class.new(given).to_normalized_s } + + it_behaves_like 'normalizes a DN' + + context 'when we do not support the given DN format' do + context 'multivalued RDNs' do + context 'without extraneous whitespace' do + let(:given) { 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' } + + it 'raises UnsupportedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError) + end + end + + context 'with extraneous whitespace' do + context 'around the phone number plus sign' do + let(:given) { 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' } + + it 'raises UnsupportedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError) + end + end + + context 'not around the phone number plus sign' do + let(:given) { 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' } + + it 'raises UnsupportedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError) + end + end + end + end + end + + context 'when the given DN is malformed' do + context 'when ending with a comma' do + let(:given) { 'uid=John Smith,' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + end + end + + context 'when given a BER encoded attribute value with a space in it' do + let(:given) { '0.9.2342.19200300.100.1.25=#aa aa' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"") + end + end + + context 'when given a BER encoded attribute value with a non-hex character in it' do + let(:given) { '0.9.2342.19200300.100.1.25=#aaXaaa' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"") + end + end + + context 'when given a BER encoded attribute value with a non-hex character in it' do + let(:given) { '0.9.2342.19200300.100.1.25=#aaaYaa' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"") + end + end + + context 'when given a hex pair with a non-hex character in it, inside double quotes' do + let(:given) { 'uid="Sebasti\\cX\\a1n"' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"") + end + end + + context 'without a name value pair' do + let(:given) { 'John' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + end + end + + context 'with an open (as opposed to closed) double quote' do + let(:given) { 'cn="James' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + end + end + + context 'with an invalid escaped hex code' do + let(:given) { 'cn=J\ames' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"') + end + end + + context 'with a value ending with the escape character' do + let(:given) { 'cn=\\' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + end + end + + context 'with an invalid OID attribute type name' do + let(:given) { '1.2.d=Value' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN OID attribute type name character "d"') + end + end + + context 'with a period in a non-OID attribute type name' do + let(:given) { 'd1.2=Value' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "."') + end + end + + context 'when starting with non-space, non-alphanumeric character' do + let(:given) { ' -uid=John Smith' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized first character of an RDN attribute type name "-"') + end + end + + context 'when given a UID with an escaped equal sign' do + let(:given) { 'uid\\=john' } + + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "\\"') + end + end + end + end + + def assert_generic_test(test_description, got, expected) + test_failure_message = "Failed test description: '#{test_description}'\n\n expected: \"#{expected}\"\n got: \"#{got}\"" + expect(got).to eq(expected), test_failure_message + end +end diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index 087c4d8c92c..d204050ef66 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -16,6 +16,34 @@ describe Gitlab::LDAP::Person do ) end + describe '.normalize_dn' do + subject { described_class.normalize_dn(given) } + + it_behaves_like 'normalizes a DN' + + context 'with an exception during normalization' do + let(:given) { 'John "Smith,' } # just something that will cause an exception + + it 'returns the given DN unmodified' do + expect(subject).to eq(given) + end + end + end + + describe '.normalize_uid' do + subject { described_class.normalize_uid(given) } + + it_behaves_like 'normalizes a DN attribute value' + + context 'with an exception during normalization' do + let(:given) { 'John "Smith,' } # just something that will cause an exception + + it 'returns the given UID unmodified' do + expect(subject).to eq(given) + end + end + end + describe '#name' do it 'uses the configured name attribute and handles values as an array' do name = 'John Doe' @@ -43,4 +71,9 @@ describe Gitlab::LDAP::Person do expect(person.email).to eq([user_principal_name]) end end + + def assert_generic_test(test_description, got, expected) + test_failure_message = "Failed test description: '#{test_description}'\n\n expected: #{expected}\n got: #{got}" + expect(got).to eq(expected), test_failure_message + end end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 6a6e465cea2..260df6e4dae 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::LDAP::User do } end let(:auth_hash) do - OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info) + OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info) end let(:ldap_user_upper_case) { described_class.new(auth_hash_upper_case) } let(:info_upper_case) do @@ -22,12 +22,12 @@ describe Gitlab::LDAP::User do } end let(:auth_hash_upper_case) do - OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info_upper_case) + OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info_upper_case) end describe '#changed?' do it "marks existing ldap user as changed" do - create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') + create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain') expect(ldap_user.changed?).to be_truthy end @@ -37,30 +37,32 @@ describe Gitlab::LDAP::User do end it "does not mark existing ldap user as changed" do - create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain') + create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain') ldap_user.gl_user.user_synced_attributes_metadata(provider: 'ldapmain', email: true) expect(ldap_user.changed?).to be_falsey end end describe '.find_by_uid_and_provider' do + let(:dn) { 'CN=John Åström, CN=Users, DC=Example, DC=com' } + it 'retrieves the correct user' do special_info = { name: 'John Åström', email: 'john@example.com', nickname: 'jastrom' } - special_hash = OmniAuth::AuthHash.new(uid: 'CN=John Åström,CN=Users,DC=Example,DC=com', provider: 'ldapmain', info: special_info) + special_hash = OmniAuth::AuthHash.new(uid: dn, provider: 'ldapmain', info: special_info) special_chars_user = described_class.new(special_hash) user = special_chars_user.save - expect(described_class.find_by_uid_and_provider(special_hash.uid, special_hash.provider)).to eq user + expect(described_class.find_by_uid_and_provider(dn, 'ldapmain')).to eq user end end describe 'find or create' do it "finds the user if already existing" do - create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') + create(:omniauth_user, extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain') expect { ldap_user.save }.not_to change { User.count } end @@ -70,7 +72,7 @@ describe Gitlab::LDAP::User do expect { ldap_user.save }.not_to change { User.count } existing_user.reload - expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' + expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com' expect(existing_user.ldap_identity.provider).to eql 'ldapmain' end @@ -79,7 +81,7 @@ describe Gitlab::LDAP::User do expect { ldap_user.save }.not_to change { User.count } existing_user.reload - expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' + expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com' expect(existing_user.ldap_identity.provider).to eql 'ldapmain' expect(existing_user.id).to eql ldap_user.gl_user.id end @@ -89,7 +91,7 @@ describe Gitlab::LDAP::User do expect { ldap_user_upper_case.save }.not_to change { User.count } existing_user.reload - expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' + expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com' expect(existing_user.ldap_identity.provider).to eql 'ldapmain' expect(existing_user.id).to eql ldap_user.gl_user.id end diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb index b576d7173f5..0803ce42fac 100644 --- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb @@ -4,35 +4,30 @@ describe Gitlab::Metrics::SidekiqMiddleware do let(:middleware) { described_class.new } let(:message) { { 'args' => ['test'], 'enqueued_at' => Time.new(2016, 6, 23, 6, 59).to_f } } - describe '#call' do - it 'tracks the transaction' do - worker = double(:worker, class: double(:class, name: 'TestWorker')) + def run(worker, message) + expect(Gitlab::Metrics::Transaction).to receive(:new) + .with('TestWorker#perform') + .and_call_original + + expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set) + .with(:sidekiq_queue_duration, instance_of(Float)) - expect(Gitlab::Metrics::Transaction).to receive(:new) - .with('TestWorker#perform') - .and_call_original + expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish) - expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set) - .with(:sidekiq_queue_duration, instance_of(Float)) + middleware.call(worker, message, :test) { nil } + end - expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish) + describe '#call' do + it 'tracks the transaction' do + worker = double(:worker, class: double(:class, name: 'TestWorker')) - middleware.call(worker, message, :test) { nil } + run(worker, message) end it 'tracks the transaction (for messages without `enqueued_at`)' do worker = double(:worker, class: double(:class, name: 'TestWorker')) - expect(Gitlab::Metrics::Transaction).to receive(:new) - .with('TestWorker#perform') - .and_call_original - - expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set) - .with(:sidekiq_queue_duration, instance_of(Float)) - - expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish) - - middleware.call(worker, {}, :test) { nil } + run(worker, {}) end it 'tracks any raised exceptions' do @@ -50,5 +45,18 @@ describe Gitlab::Metrics::SidekiqMiddleware do expect { middleware.call(worker, message, :test) } .to raise_error(RuntimeError) end + + it 'tags the metrics accordingly' do + tags = { one: 1, two: 2 } + worker = double(:worker, class: double(:class, name: 'TestWorker')) + allow(worker).to receive(:metrics_tags).and_return(tags) + + tags.each do |tag, value| + expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:add_tag) + .with(tag, value) + end + + run(worker, message) + end end end diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index 6af1564da19..67121937398 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -17,73 +17,115 @@ describe Gitlab::Middleware::Go do describe 'when go-get=1' do let(:current_user) { nil } - context 'with simple 2-segment project path' do - let!(:project) { create(:project, :private) } + shared_examples 'go-get=1' do |enabled_protocol:| + context 'with simple 2-segment project path' do + let!(:project) { create(:project, :private) } - context 'with subpackages' do - let(:path) { "#{project.full_path}/subpackage" } + context 'with subpackages' do + let(:path) { "#{project.full_path}/subpackage" } - it 'returns the full project path' do - expect_response_with_path(go, project.full_path) - end - end - - context 'without subpackages' do - let(:path) { project.full_path } - - it 'returns the full project path' do - expect_response_with_path(go, project.full_path) + it 'returns the full project path' do + expect_response_with_path(go, enabled_protocol, project.full_path) + end end - end - end - context 'with a nested project path' do - let(:group) { create(:group, :nested) } - let!(:project) { create(:project, :public, namespace: group) } + context 'without subpackages' do + let(:path) { project.full_path } - shared_examples 'a nested project' do - context 'when the project is public' do it 'returns the full project path' do - expect_response_with_path(go, project.full_path) + expect_response_with_path(go, enabled_protocol, project.full_path) end end + end - context 'when the project is private' do - before do - project.update_attribute(:visibility_level, Project::PRIVATE) - end + context 'with a nested project path' do + let(:group) { create(:group, :nested) } + let!(:project) { create(:project, :public, namespace: group) } - context 'with access to the project' do - let(:current_user) { project.creator } + shared_examples 'a nested project' do + context 'when the project is public' do + it 'returns the full project path' do + expect_response_with_path(go, enabled_protocol, project.full_path) + end + end + context 'when the project is private' do before do - project.team.add_master(current_user) + project.update_attribute(:visibility_level, Project::PRIVATE) end - it 'returns the full project path' do - expect_response_with_path(go, project.full_path) + context 'with access to the project' do + let(:current_user) { project.creator } + + before do + project.team.add_master(current_user) + end + + it 'returns the full project path' do + expect_response_with_path(go, enabled_protocol, project.full_path) + end end - end - context 'without access to the project' do - it 'returns the 2-segment group path' do - expect_response_with_path(go, group.full_path) + context 'without access to the project' do + it 'returns the 2-segment group path' do + expect_response_with_path(go, enabled_protocol, group.full_path) + end end end end + + context 'with subpackages' do + let(:path) { "#{project.full_path}/subpackage" } + + it_behaves_like 'a nested project' + end + + context 'with a subpackage that is not a valid project path' do + let(:path) { "#{project.full_path}/---subpackage" } + + it_behaves_like 'a nested project' + end + + context 'without subpackages' do + let(:path) { project.full_path } + + it_behaves_like 'a nested project' + end + end + + context 'with a bogus path' do + let(:path) { "http:;url=http://www.example.com'http-equiv='refresh'x='?go-get=1" } + + it 'skips go-import generation' do + expect(app).to receive(:call).and_return('no-go') + + go + end end + end - context 'with subpackages' do - let(:path) { "#{project.full_path}/subpackage" } + context 'with SSH disabled' do + before do + stub_application_setting(enabled_git_access_protocol: 'http') + end - it_behaves_like 'a nested project' + include_examples 'go-get=1', enabled_protocol: :http + end + + context 'with HTTP disabled' do + before do + stub_application_setting(enabled_git_access_protocol: 'ssh') end - context 'without subpackages' do - let(:path) { project.full_path } + include_examples 'go-get=1', enabled_protocol: :ssh + end - it_behaves_like 'a nested project' + context 'with nothing disabled' do + before do + stub_application_setting(enabled_git_access_protocol: nil) end + + include_examples 'go-get=1', enabled_protocol: nil end end @@ -97,10 +139,16 @@ describe Gitlab::Middleware::Go do middleware.call(env) end - def expect_response_with_path(response, path) + def expect_response_with_path(response, protocol, path) + repository_url = case protocol + when :ssh + "ssh://git@#{Gitlab.config.gitlab.host}/#{path}.git" + when :http, nil + "http://#{Gitlab.config.gitlab.host}/#{path}.git" + end expect(response[0]).to eq(200) expect(response[1]['Content-Type']).to eq('text/html') - expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git' name='go-import'></head></html>\n" + expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}" /></head></html>} expect(response[2].body).to eq([expected_body]) end end diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb new file mode 100644 index 00000000000..86be06ff595 --- /dev/null +++ b/spec/lib/gitlab/middleware/read_only_spec.rb @@ -0,0 +1,168 @@ +require 'spec_helper' + +describe Gitlab::Middleware::ReadOnly do + include Rack::Test::Methods + + RSpec::Matchers.define :be_a_redirect do + match do |response| + response.status == 301 + end + end + + RSpec::Matchers.define :disallow_request do + match do |middleware| + flash = middleware.send(:rack_flash) + flash['alert'] && flash['alert'].include?('You cannot do writing operations') + end + end + + RSpec::Matchers.define :disallow_request_in_json do + match do |response| + json_response = JSON.parse(response.body) + response.body.include?('You cannot do writing operations') && json_response.key?('message') + end + end + + let(:rack_stack) do + rack = Rack::Builder.new do + use ActionDispatch::Session::CacheStore + use ActionDispatch::Flash + use ActionDispatch::ParamsParser + end + + rack.run(subject) + rack.to_app + end + + subject { described_class.new(fake_app) } + + let(:request) { Rack::MockRequest.new(rack_stack) } + + context 'normal requests to a read-only Gitlab instance' do + let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } } + + before do + allow(Gitlab::Database).to receive(:read_only?) { true } + end + + it 'expects PATCH requests to be disallowed' do + response = request.patch('/test_request') + + expect(response).to be_a_redirect + expect(subject).to disallow_request + end + + it 'expects PUT requests to be disallowed' do + response = request.put('/test_request') + + expect(response).to be_a_redirect + expect(subject).to disallow_request + end + + it 'expects POST requests to be disallowed' do + response = request.post('/test_request') + + expect(response).to be_a_redirect + expect(subject).to disallow_request + end + + it 'expects a internal POST request to be allowed after a disallowed request' do + response = request.post('/test_request') + + expect(response).to be_a_redirect + + response = request.post("/api/#{API::API.version}/internal") + + expect(response).not_to be_a_redirect + end + + it 'expects DELETE requests to be disallowed' do + response = request.delete('/test_request') + + expect(response).to be_a_redirect + expect(subject).to disallow_request + end + + it 'expects POST of new file that looks like an LFS batch url to be disallowed' do + response = request.post('/root/gitlab-ce/new/master/app/info/lfs/objects/batch') + + expect(response).to be_a_redirect + expect(subject).to disallow_request + end + + context 'whitelisted requests' do + it 'expects DELETE request to logout to be allowed' do + response = request.delete('/users/sign_out') + + expect(response).not_to be_a_redirect + expect(subject).not_to disallow_request + end + + it 'expects a POST internal request to be allowed' do + response = request.post("/api/#{API::API.version}/internal") + + expect(response).not_to be_a_redirect + expect(subject).not_to disallow_request + end + + it 'expects a POST LFS request to batch URL to be allowed' do + response = request.post('/root/rouge.git/info/lfs/objects/batch') + + expect(response).not_to be_a_redirect + expect(subject).not_to disallow_request + end + + it 'expects a POST request to git-upload-pack URL to be allowed' do + response = request.post('/root/rouge.git/git-upload-pack') + + expect(response).not_to be_a_redirect + expect(subject).not_to disallow_request + end + + it 'expects requests to sidekiq admin to be allowed' do + response = request.post('/admin/sidekiq') + + expect(response).not_to be_a_redirect + expect(subject).not_to disallow_request + + response = request.get('/admin/sidekiq') + + expect(response).not_to be_a_redirect + expect(subject).not_to disallow_request + end + end + end + + context 'json requests to a read-only GitLab instance' do + let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'application/json' }, ['OK']] } } + let(:content_json) { { 'CONTENT_TYPE' => 'application/json' } } + + before do + allow(Gitlab::Database).to receive(:read_only?) { true } + end + + it 'expects PATCH requests to be disallowed' do + response = request.patch('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + + it 'expects PUT requests to be disallowed' do + response = request.put('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + + it 'expects POST requests to be disallowed' do + response = request.post('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + + it 'expects DELETE requests to be disallowed' do + response = request.delete('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + end +end diff --git a/spec/lib/gitlab/multi_collection_paginator_spec.rb b/spec/lib/gitlab/multi_collection_paginator_spec.rb new file mode 100644 index 00000000000..68bd4f93159 --- /dev/null +++ b/spec/lib/gitlab/multi_collection_paginator_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::MultiCollectionPaginator do + subject(:paginator) { described_class.new(Project.all.order(:id), Group.all.order(:id), per_page: 3) } + + it 'combines both collections' do + project = create(:project) + group = create(:group) + + expect(paginator.paginate(1)).to eq([project, group]) + end + + it 'includes elements second collection if first collection is empty' do + group = create(:group) + + expect(paginator.paginate(1)).to eq([group]) + end + + context 'with a full first page' do + let!(:all_groups) { create_list(:group, 4) } + let!(:all_projects) { create_list(:project, 4) } + + it 'knows the total count of the collection' do + expect(paginator.total_count).to eq(8) + end + + it 'fills the first page with elements of the first collection' do + expect(paginator.paginate(1)).to eq(all_projects.take(3)) + end + + it 'fils the second page with a mixture of of the first & second collection' do + first_collection_element = all_projects.last + second_collection_elements = all_groups.take(2) + + expected_collection = [first_collection_element] + second_collection_elements + + expect(paginator.paginate(2)).to eq(expected_collection) + end + + it 'fils the last page with elements from the second collection' do + expected_collection = all_groups[-2..-1] + + expect(paginator.paginate(3)).to eq(expected_collection) + end + end +end diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb index d5f4da3ce36..dbcc200b90b 100644 --- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb @@ -1,10 +1,11 @@ require 'spec_helper' describe Gitlab::OAuth::AuthHash do + let(:provider) { 'ldap'.freeze } let(:auth_hash) do described_class.new( OmniAuth::AuthHash.new( - provider: provider_ascii, + provider: provider, uid: uid_ascii, info: info_hash ) @@ -20,7 +21,6 @@ describe Gitlab::OAuth::AuthHash do let(:last_name_raw) { "K\xC3\xBC\xC3\xA7\xC3\xBCk" } let(:name_raw) { "Onur K\xC3\xBC\xC3\xA7\xC3\xBCk" } - let(:provider_ascii) { 'ldap'.force_encoding(Encoding::ASCII_8BIT) } let(:uid_ascii) { uid_raw.force_encoding(Encoding::ASCII_8BIT) } let(:email_ascii) { email_raw.force_encoding(Encoding::ASCII_8BIT) } let(:nickname_ascii) { nickname_raw.force_encoding(Encoding::ASCII_8BIT) } @@ -28,7 +28,6 @@ describe Gitlab::OAuth::AuthHash do let(:last_name_ascii) { last_name_raw.force_encoding(Encoding::ASCII_8BIT) } let(:name_ascii) { name_raw.force_encoding(Encoding::ASCII_8BIT) } - let(:provider_utf8) { provider_ascii.force_encoding(Encoding::UTF_8) } let(:uid_utf8) { uid_ascii.force_encoding(Encoding::UTF_8) } let(:email_utf8) { email_ascii.force_encoding(Encoding::UTF_8) } let(:nickname_utf8) { nickname_ascii.force_encoding(Encoding::UTF_8) } @@ -46,7 +45,7 @@ describe Gitlab::OAuth::AuthHash do end context 'defaults' do - it { expect(auth_hash.provider).to eql provider_utf8 } + it { expect(auth_hash.provider).to eq provider } it { expect(auth_hash.uid).to eql uid_utf8 } it { expect(auth_hash.email).to eql email_utf8 } it { expect(auth_hash.username).to eql nickname_utf8 } diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 8aaf320cbf5..c7471a21fda 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -4,6 +4,7 @@ describe Gitlab::OAuth::User do let(:oauth_user) { described_class.new(auth_hash) } let(:gl_user) { oauth_user.gl_user } let(:uid) { 'my-uid' } + let(:dn) { 'uid=user1,ou=people,dc=example' } let(:provider) { 'my-provider' } let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) } let(:info_hash) do @@ -197,7 +198,7 @@ describe Gitlab::OAuth::User do allow(ldap_user).to receive(:uid) { uid } allow(ldap_user).to receive(:username) { uid } allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] } - allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } + allow(ldap_user).to receive(:dn) { dn } end context "and no account for the LDAP user" do @@ -213,7 +214,7 @@ describe Gitlab::OAuth::User do identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } expect(identities_as_hash).to match_array( [ - { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'ldapmain', extern_uid: dn }, { provider: 'twitter', extern_uid: uid } ] ) @@ -221,7 +222,7 @@ describe Gitlab::OAuth::User do end context "and LDAP user has an account already" do - let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') } it "adds the omniauth identity to the LDAP account" do allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) @@ -234,7 +235,7 @@ describe Gitlab::OAuth::User do identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } expect(identities_as_hash).to match_array( [ - { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'ldapmain', extern_uid: dn }, { provider: 'twitter', extern_uid: uid } ] ) @@ -252,7 +253,7 @@ describe Gitlab::OAuth::User do expect(identities_as_hash) .to match_array( [ - { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'ldapmain', extern_uid: dn }, { provider: 'twitter', extern_uid: uid } ] ) @@ -310,8 +311,8 @@ describe Gitlab::OAuth::User do allow(ldap_user).to receive(:uid) { uid } allow(ldap_user).to receive(:username) { uid } allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] } - allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } - allow(oauth_user).to receive(:ldap_person).and_return(ldap_user) + allow(ldap_user).to receive(:dn) { dn } + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) end context "and no account for the LDAP user" do @@ -341,7 +342,7 @@ describe Gitlab::OAuth::User do end context 'and LDAP user has an account already' do - let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') } context 'dont block on create (LDAP)' do before do diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index 2f989397f7e..f1f188cbfb5 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -84,9 +84,9 @@ describe Gitlab::PathRegex do let(:top_level_words) do words = routes_not_starting_in_wildcard.map do |route| route.split('/')[1] - end.compact.uniq + end.compact - words + ee_top_level_words + files_in_public + Array(API::API.prefix.to_s) + (words + ee_top_level_words + files_in_public + Array(API::API.prefix.to_s)).uniq end let(:ee_top_level_words) do @@ -95,10 +95,11 @@ describe Gitlab::PathRegex do let(:files_in_public) do git = Gitlab.config.git.bin_path - `cd #{Rails.root} && #{git} ls-files public` + tracked = `cd #{Rails.root} && #{git} ls-files public` .split("\n") .map { |entry| entry.gsub('public/', '') } .uniq + tracked + %w(assets uploads) end # All routes that start with a namespaced path, that have 1 or more @@ -212,7 +213,7 @@ describe Gitlab::PathRegex do it 'accepts group routes' do expect(subject).to match('activity/') expect(subject).to match('group_members/') - expect(subject).to match('subgroups/') + expect(subject).to match('labels/') end it 'is not case sensitive' do @@ -245,7 +246,7 @@ describe Gitlab::PathRegex do it 'accepts group routes' do expect(subject).to match('activity/') expect(subject).to match('group_members/') - expect(subject).to match('subgroups/') + expect(subject).to match('labels/') end end @@ -267,7 +268,7 @@ describe Gitlab::PathRegex do it 'accepts group routes' do expect(subject).to match('activity/more/') expect(subject).to match('group_members/more/') - expect(subject).to match('subgroups/more/') + expect(subject).to match('labels/more/') end end end @@ -291,7 +292,7 @@ describe Gitlab::PathRegex do it 'rejects group routes' do expect(subject).not_to match('root/activity/') expect(subject).not_to match('root/group_members/') - expect(subject).not_to match('root/subgroups/') + expect(subject).not_to match('root/labels/') end end @@ -313,7 +314,7 @@ describe Gitlab::PathRegex do it 'rejects group routes' do expect(subject).not_to match('root/activity/more/') expect(subject).not_to match('root/group_members/more/') - expect(subject).not_to match('root/subgroups/more/') + expect(subject).not_to match('root/labels/more/') end end end @@ -348,7 +349,7 @@ describe Gitlab::PathRegex do it 'accepts group routes' do expect(subject).to match('activity/') expect(subject).to match('group_members/') - expect(subject).to match('subgroups/') + expect(subject).to match('labels/') end it 'is not case sensitive' do @@ -381,7 +382,7 @@ describe Gitlab::PathRegex do it 'accepts group routes' do expect(subject).to match('root/activity/') expect(subject).to match('root/group_members/') - expect(subject).to match('root/subgroups/') + expect(subject).to match('root/labels/') end it 'is not case sensitive' do diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb index 4567f220c11..b145ca36f26 100644 --- a/spec/lib/gitlab/popen_spec.rb +++ b/spec/lib/gitlab/popen_spec.rb @@ -14,7 +14,7 @@ describe 'Gitlab::Popen' do end it { expect(@status).to be_zero } - it { expect(@output).to include('cache') } + it { expect(@output).to include('tests') } end context 'non-zero status' do diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb index d19bd611919..57b0ef8d1ad 100644 --- a/spec/lib/gitlab/project_template_spec.rb +++ b/spec/lib/gitlab/project_template_spec.rb @@ -4,9 +4,9 @@ describe Gitlab::ProjectTemplate do describe '.all' do it 'returns a all templates' do expected = [ - described_class.new('rails', 'Ruby on Rails'), - described_class.new('spring', 'Spring'), - described_class.new('express', 'NodeJS Express') + described_class.new('rails', 'Ruby on Rails', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/rails'), + described_class.new('spring', 'Spring', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/spring'), + described_class.new('express', 'NodeJS Express', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/express') ] expect(described_class.all).to be_an(Array) @@ -31,7 +31,7 @@ describe Gitlab::ProjectTemplate do end describe 'instance methods' do - subject { described_class.new('phoenix', 'Phoenix Framework') } + subject { described_class.new('phoenix', 'Phoenix Framework', 'Phoenix description', 'link-to-template') } it { is_expected.to respond_to(:logo, :file, :archive_path) } end diff --git a/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb b/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb new file mode 100644 index 00000000000..8b58f0b3725 --- /dev/null +++ b/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe Gitlab::QuickActions::SpendTimeAndDateSeparator do + subject { described_class } + + shared_examples 'arg line with invalid parameters' do + it 'return nil' do + expect(subject.new(invalid_arg).execute).to eq(nil) + end + end + + shared_examples 'arg line with valid parameters' do + it 'return time and date array' do + expect(subject.new(valid_arg).execute).to eq(expected_response) + end + end + + describe '#execute' do + context 'invalid paramenter in arg line' do + context 'empty arg line' do + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { '' } + end + end + + context 'future date in arg line' do + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { '10m 6023-02-02' } + end + end + + context 'unparseable date(invalid mixes of delimiters)' do + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { '10m 2017.02-02' } + end + end + + context 'trash in arg line' do + let(:invalid_arg) { 'dfjkghdskjfghdjskfgdfg' } + + it 'return nil as time value' do + time_date_response = subject.new(invalid_arg).execute + + expect(time_date_response).to be_an_instance_of(Array) + expect(time_date_response.first).to eq(nil) + end + end + end + + context 'only time present in arg line' do + it_behaves_like 'arg line with valid parameters' do + let(:valid_arg) { '2m 3m 5m 1h' } + let(:time) { Gitlab::TimeTrackingFormatter.parse(valid_arg) } + let(:date) { DateTime.now.to_date } + let(:expected_response) { [time, date] } + end + end + + context 'simple time with date in arg line' do + it_behaves_like 'arg line with valid parameters' do + let(:raw_time) { '10m' } + let(:raw_date) { '2016-02-02' } + let(:valid_arg) { "#{raw_time} #{raw_date}" } + let(:date) { Date.parse(raw_date) } + let(:time) { Gitlab::TimeTrackingFormatter.parse(raw_time) } + let(:expected_response) { [time, date] } + end + end + + context 'composite time with date in arg line' do + it_behaves_like 'arg line with valid parameters' do + let(:raw_time) { '2m 10m 1h 3d' } + let(:raw_date) { '2016/02/02' } + let(:valid_arg) { "#{raw_time} #{raw_date}" } + let(:date) { Date.parse(raw_date) } + let(:time) { Gitlab::TimeTrackingFormatter.parse(raw_time) } + let(:expected_response) { [time, date] } + end + end + end +end diff --git a/spec/lib/gitlab/saml/auth_hash_spec.rb b/spec/lib/gitlab/saml/auth_hash_spec.rb new file mode 100644 index 00000000000..a555935aea3 --- /dev/null +++ b/spec/lib/gitlab/saml/auth_hash_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::Saml::AuthHash do + include LoginHelpers + + let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers) } } + subject(:saml_auth_hash) { described_class.new(omniauth_auth_hash) } + + let(:info_hash) do + { + name: 'John', + email: 'john@mail.com' + } + end + + let(:omniauth_auth_hash) do + OmniAuth::AuthHash.new(uid: 'my-uid', + provider: 'saml', + info: info_hash, + extra: { raw_info: OneLogin::RubySaml::Attributes.new(raw_info_attr) } ) + end + + before do + stub_saml_group_config(%w(Developers Freelancers Designers)) + end + + describe '#groups' do + it 'returns array of groups' do + expect(saml_auth_hash.groups).to eq(%w(Developers Freelancers)) + end + + context 'raw info hash attributes empty' do + let(:raw_info_attr) { {} } + + it 'returns an empty array' do + expect(saml_auth_hash.groups).to be_a(Array) + end + end + end +end diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index 19710029224..1765980e977 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -1,11 +1,16 @@ require 'spec_helper' describe Gitlab::Saml::User do + include LdapHelpers + include LoginHelpers + let(:saml_user) { described_class.new(auth_hash) } let(:gl_user) { saml_user.gl_user } let(:uid) { 'my-uid' } + let(:dn) { 'uid=user1,ou=people,dc=example' } let(:provider) { 'saml' } - let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new({ 'groups' => %w(Developers Freelancers Designers) }) }) } + let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers Designers) } } + let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new(raw_info_attr) }) } let(:info_hash) do { name: 'John', @@ -15,22 +20,6 @@ describe Gitlab::Saml::User do let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } describe '#save' do - def stub_omniauth_config(messages) - allow(Gitlab.config.omniauth).to receive_messages(messages) - end - - def stub_ldap_config(messages) - allow(Gitlab::LDAP::Config).to receive_messages(messages) - end - - def stub_basic_saml_config - allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } }) - end - - def stub_saml_group_config(groups) - allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } }) - end - before do stub_basic_saml_config end @@ -163,13 +152,17 @@ describe Gitlab::Saml::User do end context 'and a corresponding LDAP person' do + let(:adapter) { ldap_adapter('ldapmain') } + before do allow(ldap_user).to receive(:uid) { uid } allow(ldap_user).to receive(:username) { uid } allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) } - allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } - allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) - allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user) + allow(ldap_user).to receive(:dn) { dn } + allow(Gitlab::LDAP::Adapter).to receive(:new).and_return(adapter) + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).with(uid, adapter).and_return(ldap_user) + allow(Gitlab::LDAP::Person).to receive(:find_by_dn).with(dn, adapter).and_return(ldap_user) + allow(Gitlab::LDAP::Person).to receive(:find_by_email).with('john@mail.com', adapter).and_return(ldap_user) end context 'and no account for the LDAP user' do @@ -181,20 +174,86 @@ describe Gitlab::Saml::User do expect(gl_user.email).to eql 'john@mail.com' expect(gl_user.identities.length).to be 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } - expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: dn }, { provider: 'saml', extern_uid: uid }]) end end context 'and LDAP user has an account already' do + let(:auth_hash_base_attributes) do + { + uid: uid, + provider: provider, + info: info_hash, + extra: { + raw_info: OneLogin::RubySaml::Attributes.new( + { 'groups' => %w(Developers Freelancers Designers) } + ) + } + } + end + let(:auth_hash) { OmniAuth::AuthHash.new(auth_hash_base_attributes) } + let(:uid_types) { %w(uid dn email) } + before do create(:omniauth_user, email: 'john@mail.com', - extern_uid: 'uid=user1,ou=People,dc=example', + extern_uid: dn, provider: 'ldapmain', username: 'john') end + shared_examples 'find LDAP person' do |uid_type, uid| + let(:auth_hash) { OmniAuth::AuthHash.new(auth_hash_base_attributes.merge(uid: extern_uid)) } + + before do + nil_types = uid_types - [uid_type] + + nil_types.each do |type| + allow(Gitlab::LDAP::Person).to receive(:"find_by_#{type}").and_return(nil) + end + + allow(Gitlab::LDAP::Person).to receive(:"find_by_#{uid_type}").and_return(ldap_user) + end + + it 'adds the omniauth identity to the LDAP account' do + identities = [ + { provider: 'ldapmain', extern_uid: dn }, + { provider: 'saml', extern_uid: extern_uid } + ] + + identities_as_hash = gl_user.identities.map do |id| + { provider: id.provider, extern_uid: id.extern_uid } + end + + saml_user.save + + expect(gl_user).to be_valid + expect(gl_user.username).to eql 'john' + expect(gl_user.email).to eql 'john@mail.com' + expect(gl_user.identities.length).to be 2 + expect(identities_as_hash).to match_array(identities) + end + end + + context 'when uid is an uid' do + it_behaves_like 'find LDAP person', 'uid' do + let(:extern_uid) { uid } + end + end + + context 'when uid is a dn' do + it_behaves_like 'find LDAP person', 'dn' do + let(:extern_uid) { dn } + end + end + + context 'when uid is an email' do + it_behaves_like 'find LDAP person', 'email' do + let(:extern_uid) { 'john@mail.com' } + end + end + it 'adds the omniauth identity to the LDAP account' do saml_user.save @@ -203,7 +262,7 @@ describe Gitlab::Saml::User do expect(gl_user.email).to eql 'john@mail.com' expect(gl_user.identities.length).to be 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } - expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: dn }, { provider: 'saml', extern_uid: uid }]) end @@ -219,17 +278,21 @@ describe Gitlab::Saml::User do context 'user has SAML user, and wants to add their LDAP identity' do it 'adds the LDAP identity to the existing SAML user' do - create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'saml', username: 'john') - local_hash = OmniAuth::AuthHash.new(uid: 'uid=user1,ou=People,dc=example', provider: provider, info: info_hash) + create(:omniauth_user, email: 'john@mail.com', extern_uid: dn, provider: 'saml', username: 'john') + + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).with(dn, adapter).and_return(ldap_user) + + local_hash = OmniAuth::AuthHash.new(uid: dn, provider: provider, info: info_hash) local_saml_user = described_class.new(local_hash) + local_saml_user.save local_gl_user = local_saml_user.gl_user expect(local_gl_user).to be_valid expect(local_gl_user.identities.length).to be 2 identities_as_hash = local_gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } - expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, - { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' }]) + expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: dn }, + { provider: 'saml', extern_uid: dn }]) end end end @@ -325,4 +388,16 @@ describe Gitlab::Saml::User do end end end + + describe '#find_user' do + context 'raw info hash attributes empty' do + let(:raw_info_attr) { {} } + + it 'does not mark user as external' do + stub_saml_group_config(%w(Freelancers)) + + expect(saml_user.find_user.external).to be_falsy + end + end + end end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 4c5efbde69a..e44a7c23452 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::SearchResults do + include ProjectForksHelper + let(:user) { create(:user) } let!(:project) { create(:project, name: 'foo') } let!(:issue) { create(:issue, project: project, title: 'foo') } @@ -42,7 +44,7 @@ describe Gitlab::SearchResults do end it 'includes merge requests from source and target projects' do - forked_project = create(:project, forked_from_project: project) + forked_project = fork_project(project, user) merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo') results = described_class.new(user, Project.where(id: forked_project.id), 'foo') diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index c7930378240..2158b2837e2 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -15,10 +15,6 @@ describe Gitlab::Shell do it { is_expected.to respond_to :add_repository } it { is_expected.to respond_to :remove_repository } it { is_expected.to respond_to :fork_repository } - it { is_expected.to respond_to :add_namespace } - it { is_expected.to respond_to :rm_namespace } - it { is_expected.to respond_to :mv_namespace } - it { is_expected.to respond_to :exists? } it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") } @@ -48,14 +44,35 @@ describe Gitlab::Shell do end end - describe '#add_key' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] - ) + describe 'projects commands' do + let(:gitlab_shell_path) { File.expand_path('tmp/tests/gitlab-shell') } + let(:projects_path) { File.join(gitlab_shell_path, 'bin/gitlab-projects') } + let(:gitlab_shell_hooks_path) { File.join(gitlab_shell_path, 'hooks') } + + before do + allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(gitlab_shell_path) + allow(Gitlab.config.gitlab_shell).to receive(:hooks_path).and_return(gitlab_shell_hooks_path) + allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) + end + + describe '#mv_repository' do + it 'executes the command' do + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [projects_path, 'mv-project', 'storage/path', 'project/path.git', 'new/path.git'] + ) + gitlab_shell.mv_repository('storage/path', 'project/path', 'new/path') + end + end + + describe '#add_key' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] + ) - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end end end @@ -105,30 +122,42 @@ describe Gitlab::Shell do end describe '#add_repository' do - it 'creates a repository' do - created_path = File.join(TestEnv.repos_path, 'project', 'path.git') - hooks_path = File.join(created_path, 'hooks') - - begin - result = gitlab_shell.add_repository(TestEnv.repos_path, 'project/path') + shared_examples '#add_repository' do + let(:repository_storage) { 'default' } + let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } + let(:repo_name) { 'project/path' } + let(:created_path) { File.join(repository_storage_path, repo_name + '.git') } - repo_stat = File.stat(created_path) rescue nil - hooks_stat = File.lstat(hooks_path) rescue nil - hooks_dir = File.realpath(hooks_path) - ensure + after do FileUtils.rm_rf(created_path) end - expect(result).to be_truthy - expect(repo_stat.mode & 0o777).to eq(0o770) - expect(hooks_stat.symlink?).to be_truthy - expect(hooks_dir).to eq(gitlab_shell_hooks_path) + it 'creates a repository' do + expect(gitlab_shell.add_repository(repository_storage, repo_name)).to be_truthy + + expect(File.stat(created_path).mode & 0o777).to eq(0o770) + + hooks_path = File.join(created_path, 'hooks') + expect(File.lstat(hooks_path)).to be_symlink + expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path) + end + + it 'returns false when the command fails' do + FileUtils.mkdir_p(File.dirname(created_path)) + # This file will block the creation of the repo's .git directory. That + # should cause #add_repository to fail. + FileUtils.touch(created_path) + + expect(gitlab_shell.add_repository(repository_storage, repo_name)).to be_falsy + end end - it 'returns false when the command fails' do - expect(FileUtils).to receive(:mkdir_p).and_raise(Errno::EEXIST) + context 'with gitaly' do + it_behaves_like '#add_repository' + end - expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be_falsy + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like '#add_repository' end end @@ -136,7 +165,7 @@ describe Gitlab::Shell do it 'returns true when the command succeeds' do expect(Gitlab::Popen).to receive(:popen) .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'], - nil, popen_vars).and_return([nil, 0]) + nil, popen_vars).and_return([nil, 0]) expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be true end @@ -144,7 +173,7 @@ describe Gitlab::Shell do it 'returns false when the command fails' do expect(Gitlab::Popen).to receive(:popen) .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'], - nil, popen_vars).and_return(["error", 1]) + nil, popen_vars).and_return(["error", 1]) expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be false end @@ -304,7 +333,7 @@ describe Gitlab::Shell do end end - describe '#fetch_remote local', skip_gitaly_mock: true do + describe '#fetch_remote local', :skip_gitaly_mock do it_should_behave_like 'fetch_remote', false end @@ -330,4 +359,52 @@ describe Gitlab::Shell do end end end + + describe 'namespace actions' do + subject { described_class.new } + let(:storage_path) { Gitlab.config.repositories.storages.default.path } + + describe '#add_namespace' do + it 'creates a namespace' do + subject.add_namespace(storage_path, "mepmep") + + expect(subject.exists?(storage_path, "mepmep")).to be(true) + end + end + + describe '#exists?' do + context 'when the namespace does not exist' do + it 'returns false' do + expect(subject.exists?(storage_path, "non-existing")).to be(false) + end + end + + context 'when the namespace exists' do + it 'returns true' do + subject.add_namespace(storage_path, "mepmep") + + expect(subject.exists?(storage_path, "mepmep")).to be(true) + end + end + end + + describe '#remove' do + it 'removes the namespace' do + subject.add_namespace(storage_path, "mepmep") + subject.rm_namespace(storage_path, "mepmep") + + expect(subject.exists?(storage_path, "mepmep")).to be(false) + end + end + + describe '#mv_namespace' do + it 'renames the namespace' do + subject.add_namespace(storage_path, "mepmep") + subject.mv_namespace(storage_path, "mepmep", "2mep") + + expect(subject.exists?(storage_path, "mepmep")).to be(false) + expect(subject.exists?(storage_path, "2mep")).to be(true) + end + end + end end diff --git a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb new file mode 100644 index 00000000000..8fdbbacd04d --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe Gitlab::SidekiqMiddleware::MemoryKiller do + subject { described_class.new } + let(:pid) { 999 } + + let(:worker) { double(:worker, class: 'TestWorker') } + let(:job) { { 'jid' => 123 } } + let(:queue) { 'test_queue' } + + def run + thread = subject.call(worker, job, queue) { nil } + thread&.join + end + + before do + allow(subject).to receive(:get_rss).and_return(10.kilobytes) + allow(subject).to receive(:pid).and_return(pid) + end + + context 'when MAX_RSS is set to 0' do + before do + stub_const("#{described_class}::MAX_RSS", 0) + end + + it 'does nothing' do + expect(subject).not_to receive(:sleep) + + run + end + end + + context 'when MAX_RSS is exceeded' do + before do + stub_const("#{described_class}::MAX_RSS", 5.kilobytes) + end + + it 'sends the STP, TERM and KILL signals at expected times' do + expect(subject).to receive(:sleep).with(15 * 60).ordered + expect(Process).to receive(:kill).with('SIGSTP', pid).ordered + + expect(subject).to receive(:sleep).with(30).ordered + expect(Process).to receive(:kill).with('SIGTERM', pid).ordered + + expect(subject).to receive(:sleep).with(10).ordered + expect(Process).to receive(:kill).with('SIGKILL', pid).ordered + + run + end + end + + context 'when MAX_RSS is not exceeded' do + before do + stub_const("#{described_class}::MAX_RSS", 15.kilobytes) + end + + it 'does nothing' do + expect(subject).not_to receive(:sleep) + + run + end + end +end diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb index c2e77ef6b6c..884f27b212c 100644 --- a/spec/lib/gitlab/sidekiq_status_spec.rb +++ b/spec/lib/gitlab/sidekiq_status_spec.rb @@ -39,6 +39,18 @@ describe Gitlab::SidekiqStatus do end end + describe '.running?', :clean_gitlab_redis_shared_state do + it 'returns true if job is running' do + described_class.set('123') + + expect(described_class.running?('123')).to be(true) + end + + it 'returns false if job is not found' do + expect(described_class.running?('123')).to be(false) + end + end + describe '.num_running', :clean_gitlab_redis_shared_state do it 'returns 0 if all jobs have been completed' do expect(described_class.num_running(%w(123))).to eq(0) diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb index baf8f6644bf..fe6422c32b6 100644 --- a/spec/lib/gitlab/sql/union_spec.rb +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -22,5 +22,19 @@ describe Gitlab::SQL::Union do expect {User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") end + + it 'uses UNION ALL when removing duplicates is disabled' do + union = described_class + .new([relation_1, relation_2], remove_duplicates: false) + + expect(union.to_sql).to include('UNION ALL') + end + + it 'returns `NULL` if all relations are empty' do + empty_relation = User.none + union = described_class.new([empty_relation, empty_relation]) + + expect(union.to_sql).to eq('NULL') + end end end diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb new file mode 100644 index 00000000000..ecacea6bb35 --- /dev/null +++ b/spec/lib/gitlab/themes_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::Themes, lib: true do + describe '.body_classes' do + it 'returns a space-separated list of class names' do + css = described_class.body_classes + + expect(css).to include('ui_indigo') + expect(css).to include(' ui_dark ') + expect(css).to include(' ui_blue') + end + end + + describe '.by_id' do + it 'returns a Theme by its ID' do + expect(described_class.by_id(1).name).to eq 'Indigo' + expect(described_class.by_id(3).name).to eq 'Light' + end + end + + describe '.default' do + it 'returns the default application theme' do + allow(described_class).to receive(:default_id).and_return(2) + expect(described_class.default.id).to eq 2 + end + + it 'prevents an infinite loop when configuration default is invalid' do + default = described_class::APPLICATION_DEFAULT + themes = described_class::THEMES + + config = double(default_theme: 0).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + + config = double(default_theme: themes.size + 5).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + end + end + + describe '.each' do + it 'passes the block to the THEMES Array' do + ids = [] + described_class.each { |theme| ids << theme.id } + expect(ids).not_to be_empty + end + end +end diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index fdc3990132a..fc8991fd31f 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -39,7 +39,8 @@ describe Gitlab::UrlSanitizer do false | nil false | '' false | '123://invalid:url' - true | 'valid@project:url.git' + false | 'valid@project:url.git' + false | 'valid:pass@project:url.git' true | 'ssh://example.com' true | 'ssh://:@example.com' true | 'ssh://foo@example.com' @@ -81,24 +82,6 @@ describe Gitlab::UrlSanitizer do describe '#credentials' do context 'credentials in hash' do - where(:input, :output) do - { user: 'foo', password: 'bar' } | { user: 'foo', password: 'bar' } - { user: 'foo', password: '' } | { user: 'foo', password: nil } - { user: 'foo', password: nil } | { user: 'foo', password: nil } - { user: '', password: 'bar' } | { user: nil, password: 'bar' } - { user: '', password: '' } | { user: nil, password: nil } - { user: '', password: nil } | { user: nil, password: nil } - { user: nil, password: 'bar' } | { user: nil, password: 'bar' } - { user: nil, password: '' } | { user: nil, password: nil } - { user: nil, password: nil } | { user: nil, password: nil } - end - - with_them do - subject { described_class.new('user@example.com:path.git', credentials: input).credentials } - - it { is_expected.to eq(output) } - end - it 'overrides URL-provided credentials' do sanitizer = described_class.new('http://a:b@example.com', credentials: { user: 'c', password: 'd' }) @@ -116,10 +99,6 @@ describe Gitlab::UrlSanitizer do 'http://@example.com' | { user: nil, password: nil } 'http://example.com' | { user: nil, password: nil } - # Credentials from SCP-style URLs are not supported at present - 'foo@example.com:path' | { user: nil, password: nil } - 'foo:bar@example.com:path' | { user: nil, password: nil } - # Other invalid URLs nil | { user: nil, password: nil } '' | { user: nil, password: nil } @@ -174,4 +153,13 @@ describe Gitlab::UrlSanitizer do end end end + + context 'when credentials contains special chars' do + it 'should parse the URL without errors' do + url_sanitizer = described_class.new("https://foo:b?r@github.com/me/project.git") + + expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") + expect(url_sanitizer.full_url).to eq("https://foo:b?r@github.com/me/project.git") + end + end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 68429d792f2..a7b65e94706 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -26,6 +26,16 @@ describe Gitlab::UsageData do version uuid hostname + signup + ldap + gravatar + omniauth + reply_by_email + container_registry + gitlab_pages + gitlab_shared_runners + git + database )) end @@ -40,12 +50,19 @@ describe Gitlab::UsageData do ci_builds ci_internal_pipelines ci_external_pipelines + ci_pipeline_config_auto_devops + ci_pipeline_config_repository ci_runners ci_triggers ci_pipeline_schedules + auto_devops_enabled + auto_devops_disabled deploy_keys deployments environments + gcp_clusters + gcp_clusters_enabled + gcp_clusters_disabled in_review_folder groups issues @@ -82,6 +99,32 @@ describe Gitlab::UsageData do end end + describe '#features_usage_data_ce' do + subject { described_class.features_usage_data_ce } + + it 'gathers feature usage data' do + expect(subject[:signup]).to eq(current_application_settings.signup_enabled?) + expect(subject[:ldap]).to eq(Gitlab.config.ldap.enabled) + expect(subject[:gravatar]).to eq(current_application_settings.gravatar_enabled?) + expect(subject[:omniauth]).to eq(Gitlab.config.omniauth.enabled) + expect(subject[:reply_by_email]).to eq(Gitlab::IncomingEmail.enabled?) + expect(subject[:container_registry]).to eq(Gitlab.config.registry.enabled) + expect(subject[:gitlab_shared_runners]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled) + end + end + + describe '#components_usage_data' do + subject { described_class.components_usage_data } + + it 'gathers components usage data' do + expect(subject[:gitlab_pages][:enabled]).to eq(Gitlab.config.pages.enabled) + expect(subject[:gitlab_pages][:version]).to eq(Gitlab::Pages::VERSION) + expect(subject[:git][:version]).to eq(Gitlab::Git.version) + expect(subject[:database][:adapter]).to eq(Gitlab::Database.adapter_name) + expect(subject[:database][:version]).to eq(Gitlab::Database.version) + end + end + describe '#license_usage_data' do subject { described_class.license_usage_data } diff --git a/spec/lib/gitlab/utils/merge_hash_spec.rb b/spec/lib/gitlab/utils/merge_hash_spec.rb new file mode 100644 index 00000000000..4fa7bb31301 --- /dev/null +++ b/spec/lib/gitlab/utils/merge_hash_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +describe Gitlab::Utils::MergeHash do + describe '.crush' do + it 'can flatten a hash to each element' do + input = { hello: "world", this: { crushes: ["an entire", "hash"] } } + expected_result = [:hello, "world", :this, :crushes, "an entire", "hash"] + + expect(described_class.crush(input)).to eq(expected_result) + end + end + + describe '.elements' do + it 'deep merges an array of elements' do + input = [{ hello: ["world"] }, + { hello: "Everyone" }, + { hello: { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzień dobry'] } }, + "Goodbye", "Hallo"] + expected_output = [ + { + hello: + [ + "world", + "Everyone", + { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzień dobry'] } + ] + }, + "Goodbye" + ] + + expect(described_class.merge(input)).to eq(expected_output) + end + end +end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 699184ad9fe..249c77dc636 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -13,13 +13,51 @@ describe Gitlab::Workhorse do end describe ".send_git_archive" do + let(:ref) { 'master' } + let(:format) { 'zip' } + let(:storage_path) { Gitlab.config.gitlab.repository_downloads_path } + let(:base_params) { repository.archive_metadata(ref, storage_path, format) } + let(:gitaly_params) do + base_params.merge( + 'GitalyServer' => { + 'address' => Gitlab::GitalyClient.address(project.repository_storage), + 'token' => Gitlab::GitalyClient.token(project.repository_storage) + }, + 'GitalyRepository' => repository.gitaly_repository.to_h.deep_stringify_keys + ) + end + + subject do + described_class.send_git_archive(repository, ref: ref, format: format) + end + + context 'when Gitaly workhorse_archive feature is enabled' do + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) + + expect(key).to eq('Gitlab-Workhorse-Send-Data') + expect(command).to eq('git-archive') + expect(params).to include(gitaly_params) + end + end + + context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) + + expect(key).to eq('Gitlab-Workhorse-Send-Data') + expect(command).to eq('git-archive') + expect(params).to eq(base_params) + end + end + context "when the repository doesn't have an archive file path" do before do allow(project.repository).to receive(:archive_metadata).and_return(Hash.new) end it "raises an error" do - expect { described_class.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError) + expect { subject }.to raise_error(RuntimeError) end end end @@ -28,12 +66,34 @@ describe Gitlab::Workhorse do let(:diff_refs) { double(base_sha: "base", head_sha: "head") } subject { described_class.send_git_patch(repository, diff_refs) } - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) + context 'when Gitaly workhorse_send_git_patch feature is enabled' do + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) - expect(key).to eq("Gitlab-Workhorse-Send-Data") - expect(command).to eq("git-format-patch") - expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head") + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("git-format-patch") + expect(params).to eq({ + 'GitalyServer' => { + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + }, + 'RawPatchRequest' => Gitaly::RawPatchRequest.new( + repository: repository.gitaly_repository, + left_commit_id: 'base', + right_commit_id: 'head' + ).to_json + }.deep_stringify_keys) + end + end + + context 'when Gitaly workhorse_send_git_patch feature is disabled', :skip_gitaly_mock do + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) + + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("git-format-patch") + expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head") + end end end @@ -77,14 +137,36 @@ describe Gitlab::Workhorse do describe '.send_git_diff' do let(:diff_refs) { double(base_sha: "base", head_sha: "head") } - subject { described_class.send_git_patch(repository, diff_refs) } + subject { described_class.send_git_diff(repository, diff_refs) } - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) + context 'when Gitaly workhorse_send_git_diff feature is enabled' do + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) - expect(key).to eq("Gitlab-Workhorse-Send-Data") - expect(command).to eq("git-format-patch") - expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head") + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("git-diff") + expect(params).to eq({ + 'GitalyServer' => { + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + }, + 'RawDiffRequest' => Gitaly::RawDiffRequest.new( + repository: repository.gitaly_repository, + left_commit_id: 'base', + right_commit_id: 'head' + ).to_json + }.deep_stringify_keys) + end + end + + context 'when Gitaly workhorse_send_git_diff feature is disabled', :skip_gitaly_mock do + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) + + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("git-diff") + expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head") + end end end @@ -182,7 +264,13 @@ describe Gitlab::Workhorse do 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 } + { + GL_ID: "user-#{user.id}", + GL_USERNAME: user.username, + GL_REPOSITORY: "project-#{project.id}", + RepoPath: repo_path, + ShowAllRefs: false + } end subject { described_class.git_http_ok(repository, false, user, action) } @@ -191,7 +279,13 @@ describe Gitlab::Workhorse do context 'when is_wiki' do let(:params) do - { GL_ID: "user-#{user.id}", GL_REPOSITORY: "wiki-#{project.id}", RepoPath: repo_path } + { + GL_ID: "user-#{user.id}", + GL_USERNAME: user.username, + GL_REPOSITORY: "wiki-#{project.id}", + RepoPath: repo_path, + ShowAllRefs: false + } end subject { described_class.git_http_ok(repository, true, user, action) } @@ -214,14 +308,13 @@ describe Gitlab::Workhorse do end it 'includes a Repository param' do - repo_param = { Repository: { + repo_param = { storage_name: 'default', relative_path: project.full_path + '.git', - git_object_directory: '', - git_alternate_object_directories: [] - } } + gl_repository: "project-#{project.id}" + } - expect(subject).to include(repo_param) + expect(subject[:Repository]).to include(repo_param) end context "when git_upload_pack action is passed" do @@ -233,6 +326,12 @@ describe Gitlab::Workhorse do expect(subject).to include(gitaly_params) end + + context 'show_all_refs enabled' do + subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) } + + it { is_expected.to include(ShowAllRefs: true) } + end end context "when git_receive_pack action is passed" do @@ -245,6 +344,12 @@ describe Gitlab::Workhorse do let(:action) { 'info_refs' } it { expect(subject).to include(gitaly_params) } + + context 'show_all_refs enabled' do + subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) } + + it { is_expected.to include(ShowAllRefs: true) } + end end context 'when action passed is not supported by Gitaly' do @@ -336,7 +441,7 @@ describe Gitlab::Workhorse do end end - context 'when Gitaly workhorse_raw_show feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly workhorse_raw_show feature is disabled', :skip_gitaly_mock do it 'sets the header correctly' do key, command, params = decode_workhorse_header(subject) diff --git a/spec/lib/google_api/auth_spec.rb b/spec/lib/google_api/auth_spec.rb new file mode 100644 index 00000000000..87a3f43274f --- /dev/null +++ b/spec/lib/google_api/auth_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe GoogleApi::Auth do + let(:redirect_uri) { 'http://localhost:3000/google_api/authorizations/callback' } + let(:redirect_to) { 'http://localhost:3000/namaspace/project/clusters' } + + let(:client) do + GoogleApi::CloudPlatform::Client + .new(nil, redirect_uri, state: redirect_to) + end + + describe '#authorize_url' do + subject { client.authorize_url } + + it 'returns authorize_url' do + is_expected.to start_with('https://accounts.google.com/o/oauth2') + is_expected.to include(URI.encode(redirect_uri, URI::PATTERN::RESERVED)) + is_expected.to include(URI.encode(redirect_to, URI::PATTERN::RESERVED)) + end + end + + describe '#get_token' do + let(:token) do + double.tap do |dbl| + allow(dbl).to receive(:token).and_return('token') + allow(dbl).to receive(:expires_at).and_return('expires_at') + end + end + + before do + allow_any_instance_of(OAuth2::Strategy::AuthCode) + .to receive(:get_token).and_return(token) + end + + it 'returns token and expires_at' do + token, expires_at = client.get_token('xxx') + expect(token).to eq('token') + expect(expires_at).to eq('expires_at') + end + end +end diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb new file mode 100644 index 00000000000..acc5bd1da35 --- /dev/null +++ b/spec/lib/google_api/cloud_platform/client_spec.rb @@ -0,0 +1,128 @@ +require 'spec_helper' + +describe GoogleApi::CloudPlatform::Client do + let(:token) { 'token' } + let(:client) { described_class.new(token, nil) } + + describe '.session_key_for_redirect_uri' do + let(:state) { 'random_string' } + + subject { described_class.session_key_for_redirect_uri(state) } + + it 'creates a new session key' do + is_expected.to eq('cloud_platform_second_redirect_uri_random_string') + end + end + + describe '.new_session_key_for_redirect_uri' do + it 'generates a new session key' do + expect { |b| described_class.new_session_key_for_redirect_uri(&b) } + .to yield_with_args(String) + end + end + + describe '#validate_token' do + subject { client.validate_token(expires_at) } + + let(:expires_at) { 1.hour.since.utc.strftime('%s') } + + context 'when token is nil' do + let(:token) { nil } + + it { is_expected.to be_falsy } + end + + context 'when expires_at is nil' do + let(:expires_at) { nil } + + it { is_expected.to be_falsy } + end + + context 'when expires in 1 hour' do + it { is_expected.to be_truthy } + end + + context 'when expires in 10 minutes' do + let(:expires_at) { 5.minutes.since.utc.strftime('%s') } + + it { is_expected.to be_falsy } + end + end + + describe '#projects_zones_clusters_get' do + subject { client.projects_zones_clusters_get(spy, spy, spy) } + let(:gke_cluster) { double } + + before do + allow_any_instance_of(Google::Apis::ContainerV1::ContainerService) + .to receive(:get_zone_cluster).and_return(gke_cluster) + end + + it { is_expected.to eq(gke_cluster) } + end + + describe '#projects_zones_clusters_create' do + subject do + client.projects_zones_clusters_create( + spy, spy, cluster_name, cluster_size, machine_type: machine_type) + end + + let(:cluster_name) { 'test-cluster' } + let(:cluster_size) { 1 } + let(:machine_type) { 'n1-standard-4' } + let(:operation) { double } + + before do + allow_any_instance_of(Google::Apis::ContainerV1::ContainerService) + .to receive(:create_cluster).and_return(operation) + end + + it { is_expected.to eq(operation) } + + it 'sets corresponded parameters' do + expect_any_instance_of(Google::Apis::ContainerV1::CreateClusterRequest) + .to receive(:initialize).with( + { + "cluster": { + "name": cluster_name, + "initial_node_count": cluster_size, + "node_config": { + "machine_type": machine_type + } + } + } ) + + subject + end + end + + describe '#projects_zones_operations' do + subject { client.projects_zones_operations(spy, spy, spy) } + let(:operation) { double } + + before do + allow_any_instance_of(Google::Apis::ContainerV1::ContainerService) + .to receive(:get_zone_operation).and_return(operation) + end + + it { is_expected.to eq(operation) } + end + + describe '#parse_operation_id' do + subject { client.parse_operation_id(self_link) } + + context 'when expected url' do + let(:self_link) do + 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123' + end + + it { is_expected.to eq('ope-123') } + end + + context 'when unexpected url' do + let(:self_link) { '???' } + + it { is_expected.to be_nil } + end + end +end diff --git a/spec/lib/rspec_flaky/config_spec.rb b/spec/lib/rspec_flaky/config_spec.rb new file mode 100644 index 00000000000..83556787e85 --- /dev/null +++ b/spec/lib/rspec_flaky/config_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' + +describe RspecFlaky::Config, :aggregate_failures do + before do + # Stub these env variables otherwise specs don't behave the same on the CI + stub_env('FLAKY_RSPEC_GENERATE_REPORT', nil) + stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', nil) + stub_env('FLAKY_RSPEC_REPORT_PATH', nil) + stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', nil) + end + + describe '.generate_report?' do + context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is not set" do + it 'returns false' do + expect(described_class).not_to be_generate_report + end + end + + context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is set to 'false'" do + before do + stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'false') + end + + it 'returns false' do + expect(described_class).not_to be_generate_report + end + end + + context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is set to 'true'" do + before do + stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'true') + end + + it 'returns true' do + expect(described_class).to be_generate_report + end + end + end + + describe '.suite_flaky_examples_report_path' do + context "when ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] is not set" do + it 'returns the default path' do + expect(Rails.root).to receive(:join).with('rspec_flaky/suite-report.json') + .and_return('root/rspec_flaky/suite-report.json') + + expect(described_class.suite_flaky_examples_report_path).to eq('root/rspec_flaky/suite-report.json') + end + end + + context "when ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] is set" do + before do + stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', 'foo/suite-report.json') + end + + it 'returns the value of the env variable' do + expect(described_class.suite_flaky_examples_report_path).to eq('foo/suite-report.json') + end + end + end + + describe '.flaky_examples_report_path' do + context "when ENV['FLAKY_RSPEC_REPORT_PATH'] is not set" do + it 'returns the default path' do + expect(Rails.root).to receive(:join).with('rspec_flaky/report.json') + .and_return('root/rspec_flaky/report.json') + + expect(described_class.flaky_examples_report_path).to eq('root/rspec_flaky/report.json') + end + end + + context "when ENV['FLAKY_RSPEC_REPORT_PATH'] is set" do + before do + stub_env('FLAKY_RSPEC_REPORT_PATH', 'foo/report.json') + end + + it 'returns the value of the env variable' do + expect(described_class.flaky_examples_report_path).to eq('foo/report.json') + end + end + end + + describe '.new_flaky_examples_report_path' do + context "when ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] is not set" do + it 'returns the default path' do + expect(Rails.root).to receive(:join).with('rspec_flaky/new-report.json') + .and_return('root/rspec_flaky/new-report.json') + + expect(described_class.new_flaky_examples_report_path).to eq('root/rspec_flaky/new-report.json') + end + end + + context "when ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] is set" do + before do + stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', 'foo/new-report.json') + end + + it 'returns the value of the env variable' do + expect(described_class.new_flaky_examples_report_path).to eq('foo/new-report.json') + end + end + end +end diff --git a/spec/lib/rspec_flaky/flaky_example_spec.rb b/spec/lib/rspec_flaky/flaky_example_spec.rb index cbfc1e538ab..d19c34bebb3 100644 --- a/spec/lib/rspec_flaky/flaky_example_spec.rb +++ b/spec/lib/rspec_flaky/flaky_example_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe RspecFlaky::FlakyExample do +describe RspecFlaky::FlakyExample, :aggregate_failures do let(:flaky_example_attrs) do { example_id: 'spec/foo/bar_spec.rb:2', @@ -9,6 +9,7 @@ describe RspecFlaky::FlakyExample do description: 'hello world', first_flaky_at: 1234, last_flaky_at: 2345, + last_flaky_job: 'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/12', last_attempts_count: 2, flaky_reports: 1 } @@ -27,57 +28,78 @@ describe RspecFlaky::FlakyExample do end let(:example) { double(example_attrs) } + before do + # Stub these env variables otherwise specs don't behave the same on the CI + stub_env('CI_PROJECT_URL', nil) + stub_env('CI_JOB_ID', nil) + end + describe '#initialize' do shared_examples 'a valid FlakyExample instance' do - it 'returns valid attributes' do - flaky_example = described_class.new(args) + let(:flaky_example) { described_class.new(args) } + it 'returns valid attributes' do expect(flaky_example.uid).to eq(flaky_example_attrs[:uid]) - expect(flaky_example.example_id).to eq(flaky_example_attrs[:example_id]) + expect(flaky_example.file).to eq(flaky_example_attrs[:file]) + expect(flaky_example.line).to eq(flaky_example_attrs[:line]) + expect(flaky_example.description).to eq(flaky_example_attrs[:description]) + expect(flaky_example.first_flaky_at).to eq(expected_first_flaky_at) + expect(flaky_example.last_flaky_at).to eq(expected_last_flaky_at) + expect(flaky_example.last_attempts_count).to eq(flaky_example_attrs[:last_attempts_count]) + expect(flaky_example.flaky_reports).to eq(expected_flaky_reports) end end context 'when given an Rspec::Example' do - let(:args) { example } - - it_behaves_like 'a valid FlakyExample instance' + it_behaves_like 'a valid FlakyExample instance' do + let(:args) { example } + let(:expected_first_flaky_at) { nil } + let(:expected_last_flaky_at) { nil } + let(:expected_flaky_reports) { 0 } + end end context 'when given a hash' do - let(:args) { flaky_example_attrs } - - it_behaves_like 'a valid FlakyExample instance' + it_behaves_like 'a valid FlakyExample instance' do + let(:args) { flaky_example_attrs } + let(:expected_flaky_reports) { flaky_example_attrs[:flaky_reports] } + let(:expected_first_flaky_at) { flaky_example_attrs[:first_flaky_at] } + let(:expected_last_flaky_at) { flaky_example_attrs[:last_flaky_at] } + end end end - describe '#to_h' do - before do - # Stub these env variables otherwise specs don't behave the same on the CI - stub_env('CI_PROJECT_URL', nil) - stub_env('CI_JOB_ID', nil) - end + describe '#update_flakiness!' do + shared_examples 'an up-to-date FlakyExample instance' do + let(:flaky_example) { described_class.new(args) } - shared_examples 'a valid FlakyExample hash' do - let(:additional_attrs) { {} } + it 'updates the first_flaky_at' do + now = Time.now + expected_first_flaky_at = flaky_example.first_flaky_at ? flaky_example.first_flaky_at : now + Timecop.freeze(now) { flaky_example.update_flakiness! } - it 'returns a valid hash' do - flaky_example = described_class.new(args) - final_hash = flaky_example_attrs - .merge(last_flaky_at: instance_of(Time), last_flaky_job: nil) - .merge(additional_attrs) + expect(flaky_example.first_flaky_at).to eq(expected_first_flaky_at) + end + + it 'updates the last_flaky_at' do + now = Time.now + Timecop.freeze(now) { flaky_example.update_flakiness! } - expect(flaky_example.to_h).to match(hash_including(final_hash)) + expect(flaky_example.last_flaky_at).to eq(now) end - end - context 'when given an Rspec::Example' do - let(:args) { example } + it 'updates the flaky_reports' do + expected_flaky_reports = flaky_example.first_flaky_at ? flaky_example.flaky_reports + 1 : 1 + + expect { flaky_example.update_flakiness! }.to change { flaky_example.flaky_reports }.by(1) + expect(flaky_example.flaky_reports).to eq(expected_flaky_reports) + end + + context 'when passed a :last_attempts_count' do + it 'updates the last_attempts_count' do + flaky_example.update_flakiness!(last_attempts_count: 42) - context 'when run locally' do - it_behaves_like 'a valid FlakyExample hash' do - let(:additional_attrs) do - { first_flaky_at: instance_of(Time) } - end + expect(flaky_example.last_attempts_count).to eq(42) end end @@ -87,10 +109,45 @@ describe RspecFlaky::FlakyExample do stub_env('CI_JOB_ID', 42) end - it_behaves_like 'a valid FlakyExample hash' do - let(:additional_attrs) do - { first_flaky_at: instance_of(Time), last_flaky_job: "https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/42" } - end + it 'updates the last_flaky_job' do + flaky_example.update_flakiness! + + expect(flaky_example.last_flaky_job).to eq('https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/42') + end + end + end + + context 'when given an Rspec::Example' do + it_behaves_like 'an up-to-date FlakyExample instance' do + let(:args) { example } + end + end + + context 'when given a hash' do + it_behaves_like 'an up-to-date FlakyExample instance' do + let(:args) { flaky_example_attrs } + end + end + end + + describe '#to_h' do + shared_examples 'a valid FlakyExample hash' do + let(:additional_attrs) { {} } + + it 'returns a valid hash' do + flaky_example = described_class.new(args) + final_hash = flaky_example_attrs.merge(additional_attrs) + + expect(flaky_example.to_h).to eq(final_hash) + end + end + + context 'when given an Rspec::Example' do + let(:args) { example } + + it_behaves_like 'a valid FlakyExample hash' do + let(:additional_attrs) do + { first_flaky_at: nil, last_flaky_at: nil, last_flaky_job: nil, flaky_reports: 0 } end end end diff --git a/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb b/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb new file mode 100644 index 00000000000..06a8ba0d02e --- /dev/null +++ b/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe RspecFlaky::FlakyExamplesCollection, :aggregate_failures do + let(:collection_hash) do + { + a: { example_id: 'spec/foo/bar_spec.rb:2' }, + b: { example_id: 'spec/foo/baz_spec.rb:3' } + } + end + let(:collection_report) do + { + a: { + example_id: 'spec/foo/bar_spec.rb:2', + first_flaky_at: nil, + last_flaky_at: nil, + last_flaky_job: nil + }, + b: { + example_id: 'spec/foo/baz_spec.rb:3', + first_flaky_at: nil, + last_flaky_at: nil, + last_flaky_job: nil + } + } + end + + describe '.from_json' do + it 'accepts a JSON' do + collection = described_class.from_json(JSON.pretty_generate(collection_hash)) + + expect(collection.to_report).to eq(described_class.new(collection_hash).to_report) + end + end + + describe '#initialize' do + it 'accepts no argument' do + expect { described_class.new }.not_to raise_error + end + + it 'accepts a hash' do + expect { described_class.new(collection_hash) }.not_to raise_error + end + + it 'does not accept anything else' do + expect { described_class.new([1, 2, 3]) }.to raise_error(ArgumentError, "`collection` must be a Hash, Array given!") + end + end + + describe '#to_report' do + it 'calls #to_h on the values' do + collection = described_class.new(collection_hash) + + expect(collection.to_report).to eq(collection_report) + end + end + + describe '#-' do + it 'returns only examples that are not present in the given collection' do + collection1 = described_class.new(collection_hash) + collection2 = described_class.new( + a: { example_id: 'spec/foo/bar_spec.rb:2' }, + c: { example_id: 'spec/bar/baz_spec.rb:4' }) + + expect((collection2 - collection1).to_report).to eq( + c: { + example_id: 'spec/bar/baz_spec.rb:4', + first_flaky_at: nil, + last_flaky_at: nil, + last_flaky_job: nil + }) + end + + it 'fails if the given collection does not respond to `#key?`' do + collection = described_class.new(collection_hash) + + expect { collection - [1, 2, 3] }.to raise_error(ArgumentError, "`other` must respond to `#key?`, Array does not!") + end + end +end diff --git a/spec/lib/rspec_flaky/listener_spec.rb b/spec/lib/rspec_flaky/listener_spec.rb index 0e193bf408b..bfb7648b486 100644 --- a/spec/lib/rspec_flaky/listener_spec.rb +++ b/spec/lib/rspec_flaky/listener_spec.rb @@ -1,22 +1,35 @@ require 'spec_helper' -describe RspecFlaky::Listener do - let(:flaky_example_report) do +describe RspecFlaky::Listener, :aggregate_failures do + let(:already_flaky_example_uid) { '6e869794f4cfd2badd93eb68719371d1' } + let(:suite_flaky_example_report) do { - 'abc123' => { + already_flaky_example_uid => { example_id: 'spec/foo/bar_spec.rb:2', file: 'spec/foo/bar_spec.rb', line: 2, description: 'hello world', first_flaky_at: 1234, - last_flaky_at: instance_of(Time), - last_attempts_count: 2, + last_flaky_at: 4321, + last_attempts_count: 3, flaky_reports: 1, last_flaky_job: nil } } end - let(:example_attrs) do + let(:already_flaky_example_attrs) do + { + id: 'spec/foo/bar_spec.rb:2', + metadata: { + file_path: 'spec/foo/bar_spec.rb', + line_number: 2, + full_description: 'hello world' + }, + execution_result: double(status: 'passed', exception: nil) + } + end + let(:already_flaky_example) { RspecFlaky::FlakyExample.new(suite_flaky_example_report[already_flaky_example_uid]) } + let(:new_example_attrs) do { id: 'spec/foo/baz_spec.rb:3', metadata: { @@ -32,18 +45,19 @@ describe RspecFlaky::Listener do # Stub these env variables otherwise specs don't behave the same on the CI stub_env('CI_PROJECT_URL', nil) stub_env('CI_JOB_ID', nil) + stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', nil) end describe '#initialize' do shared_examples 'a valid Listener instance' do - let(:expected_all_flaky_examples) { {} } + let(:expected_suite_flaky_examples) { {} } it 'returns a valid Listener instance' do listener = described_class.new - expect(listener.to_report(listener.all_flaky_examples)) - .to match(hash_including(expected_all_flaky_examples)) - expect(listener.new_flaky_examples).to eq({}) + expect(listener.to_report(listener.suite_flaky_examples)) + .to eq(expected_suite_flaky_examples) + expect(listener.flaky_examples).to eq({}) end end @@ -51,16 +65,16 @@ describe RspecFlaky::Listener do it_behaves_like 'a valid Listener instance' end - context 'when a report file exists and set by ALL_FLAKY_RSPEC_REPORT_PATH' do + context 'when a report file exists and set by SUITE_FLAKY_RSPEC_REPORT_PATH' do let(:report_file) do Tempfile.new(%w[rspec_flaky_report .json]).tap do |f| - f.write(JSON.pretty_generate(flaky_example_report)) + f.write(JSON.pretty_generate(suite_flaky_example_report)) f.rewind end end before do - stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file.path) + stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', report_file.path) end after do @@ -69,74 +83,122 @@ describe RspecFlaky::Listener do end it_behaves_like 'a valid Listener instance' do - let(:expected_all_flaky_examples) { flaky_example_report } + let(:expected_suite_flaky_examples) { suite_flaky_example_report } end end end describe '#example_passed' do - let(:rspec_example) { double(example_attrs) } + let(:rspec_example) { double(new_example_attrs) } let(:notification) { double(example: rspec_example) } + let(:listener) { described_class.new(suite_flaky_example_report.to_json) } shared_examples 'a non-flaky example' do it 'does not change the flaky examples hash' do - expect { subject.example_passed(notification) } - .not_to change { subject.all_flaky_examples } + expect { listener.example_passed(notification) } + .not_to change { listener.flaky_examples } end end - describe 'when the RSpec example does not respond to attempts' do - it_behaves_like 'a non-flaky example' - end + shared_examples 'an existing flaky example' do + let(:expected_flaky_example) do + { + example_id: 'spec/foo/bar_spec.rb:2', + file: 'spec/foo/bar_spec.rb', + line: 2, + description: 'hello world', + first_flaky_at: 1234, + last_attempts_count: 2, + flaky_reports: 2, + last_flaky_job: nil + } + end - describe 'when the RSpec example has 1 attempt' do - let(:rspec_example) { double(example_attrs.merge(attempts: 1)) } + it 'changes the flaky examples hash' do + new_example = RspecFlaky::Example.new(rspec_example) - it_behaves_like 'a non-flaky example' + now = Time.now + Timecop.freeze(now) do + expect { listener.example_passed(notification) } + .to change { listener.flaky_examples[new_example.uid].to_h } + end + + expect(listener.flaky_examples[new_example.uid].to_h) + .to eq(expected_flaky_example.merge(last_flaky_at: now)) + end end - describe 'when the RSpec example has 2 attempts' do - let(:rspec_example) { double(example_attrs.merge(attempts: 2)) } - let(:expected_new_flaky_example) do + shared_examples 'a new flaky example' do + let(:expected_flaky_example) do { example_id: 'spec/foo/baz_spec.rb:3', file: 'spec/foo/baz_spec.rb', line: 3, description: 'hello GitLab', - first_flaky_at: instance_of(Time), - last_flaky_at: instance_of(Time), last_attempts_count: 2, flaky_reports: 1, last_flaky_job: nil } end - it 'does not change the flaky examples hash' do - expect { subject.example_passed(notification) } - .to change { subject.all_flaky_examples } - + it 'changes the all flaky examples hash' do new_example = RspecFlaky::Example.new(rspec_example) - expect(subject.all_flaky_examples[new_example.uid].to_h) - .to match(hash_including(expected_new_flaky_example)) + now = Time.now + Timecop.freeze(now) do + expect { listener.example_passed(notification) } + .to change { listener.flaky_examples[new_example.uid].to_h } + end + + expect(listener.flaky_examples[new_example.uid].to_h) + .to eq(expected_flaky_example.merge(first_flaky_at: now, last_flaky_at: now)) + end + end + + describe 'when the RSpec example does not respond to attempts' do + it_behaves_like 'a non-flaky example' + end + + describe 'when the RSpec example has 1 attempt' do + let(:rspec_example) { double(new_example_attrs.merge(attempts: 1)) } + + it_behaves_like 'a non-flaky example' + end + + describe 'when the RSpec example has 2 attempts' do + let(:rspec_example) { double(new_example_attrs.merge(attempts: 2)) } + + it_behaves_like 'a new flaky example' + + context 'with an existing flaky example' do + let(:rspec_example) { double(already_flaky_example_attrs.merge(attempts: 2)) } + + it_behaves_like 'an existing flaky example' end end end describe '#dump_summary' do - let(:rspec_example) { double(example_attrs) } - let(:notification) { double(example: rspec_example) } + let(:listener) { described_class.new(suite_flaky_example_report.to_json) } + let(:new_flaky_rspec_example) { double(new_example_attrs.merge(attempts: 2)) } + let(:already_flaky_rspec_example) { double(already_flaky_example_attrs.merge(attempts: 2)) } + let(:notification_new_flaky_rspec_example) { double(example: new_flaky_rspec_example) } + let(:notification_already_flaky_rspec_example) { double(example: already_flaky_rspec_example) } - context 'when a report file path is set by ALL_FLAKY_RSPEC_REPORT_PATH' do + context 'when a report file path is set by FLAKY_RSPEC_REPORT_PATH' do let(:report_file_path) { Rails.root.join('tmp', 'rspec_flaky_report.json') } + let(:new_report_file_path) { Rails.root.join('tmp', 'rspec_flaky_new_report.json') } before do - stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file_path) + stub_env('FLAKY_RSPEC_REPORT_PATH', report_file_path) + stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', new_report_file_path) FileUtils.rm(report_file_path) if File.exist?(report_file_path) + FileUtils.rm(new_report_file_path) if File.exist?(new_report_file_path) end after do FileUtils.rm(report_file_path) if File.exist?(report_file_path) + FileUtils.rm(new_report_file_path) if File.exist?(new_report_file_path) end context 'when FLAKY_RSPEC_GENERATE_REPORT == "false"' do @@ -144,12 +206,13 @@ describe RspecFlaky::Listener do stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'false') end - it 'does not write the report file' do - subject.example_passed(notification) + it 'does not write any report file' do + listener.example_passed(notification_new_flaky_rspec_example) - subject.dump_summary(nil) + listener.dump_summary(nil) expect(File.exist?(report_file_path)).to be(false) + expect(File.exist?(new_report_file_path)).to be(false) end end @@ -158,21 +221,39 @@ describe RspecFlaky::Listener do stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'true') end - it 'writes the report file' do - subject.example_passed(notification) + around do |example| + Timecop.freeze { example.run } + end + + it 'writes the report files' do + listener.example_passed(notification_new_flaky_rspec_example) + listener.example_passed(notification_already_flaky_rspec_example) - subject.dump_summary(nil) + listener.dump_summary(nil) expect(File.exist?(report_file_path)).to be(true) + expect(File.exist?(new_report_file_path)).to be(true) + + expect(File.read(report_file_path)) + .to eq(JSON.pretty_generate(listener.to_report(listener.flaky_examples))) + + new_example = RspecFlaky::Example.new(notification_new_flaky_rspec_example) + new_flaky_example = RspecFlaky::FlakyExample.new(new_example) + new_flaky_example.update_flakiness! + + expect(File.read(new_report_file_path)) + .to eq(JSON.pretty_generate(listener.to_report(new_example.uid => new_flaky_example))) end end end end describe '#to_report' do + let(:listener) { described_class.new(suite_flaky_example_report.to_json) } + it 'transforms the internal hash to a JSON-ready hash' do - expect(subject.to_report('abc123' => RspecFlaky::FlakyExample.new(flaky_example_report['abc123']))) - .to match(hash_including(flaky_example_report)) + expect(listener.to_report(already_flaky_example_uid => already_flaky_example)) + .to match(hash_including(suite_flaky_example_report)) end end end diff --git a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb index 7125bfcab59..a0fb86345f3 100644 --- a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb +++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb @@ -16,7 +16,12 @@ describe SystemCheck::App::GitUserDefaultSSHConfigCheck do end it 'only whitelists safe files' do - expect(described_class::WHITELIST).to contain_exactly('authorized_keys', 'authorized_keys2', 'known_hosts') + expect(described_class::WHITELIST).to contain_exactly( + 'authorized_keys', + 'authorized_keys2', + 'authorized_keys.lock', + 'known_hosts' + ) end describe '#skip?' do diff --git a/spec/lib/system_check/base_check_spec.rb b/spec/lib/system_check/base_check_spec.rb new file mode 100644 index 00000000000..faf8c99e772 --- /dev/null +++ b/spec/lib/system_check/base_check_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe SystemCheck::BaseCheck do + context 'helpers on instance level' do + it 'responds to SystemCheck::Helpers methods' do + expect(subject).to respond_to :fix_and_rerun, :for_more_information, :see_installation_guide_section, + :finished_checking, :start_checking, :try_fixing_it, :sanitized_message, :should_sanitize?, :omnibus_gitlab?, + :sudo_gitlab + end + + it 'responds to Gitlab::TaskHelpers methods' do + expect(subject).to respond_to :ask_to_continue, :os_name, :prompt, :run_and_match, :run_command, + :run_command!, :uid_for, :gid_for, :gitlab_user, :gitlab_user?, :warn_user_is_not_gitlab, :all_repos, + :repository_storage_paths_args, :user_home, :checkout_or_clone_version, :clone_repo, :checkout_version + end + end +end diff --git a/spec/lib/system_check/orphans/namespace_check_spec.rb b/spec/lib/system_check/orphans/namespace_check_spec.rb new file mode 100644 index 00000000000..2a61ff3ad65 --- /dev/null +++ b/spec/lib/system_check/orphans/namespace_check_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require 'rake_helper' + +describe SystemCheck::Orphans::NamespaceCheck do + let(:storages) { Gitlab.config.repositories.storages.reject { |key, _| key.eql? 'broken' } } + + before do + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + allow(subject).to receive(:fetch_disk_namespaces).and_return(disk_namespaces) + silence_output + end + + describe '#multi_check' do + context 'all orphans' do + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) } + + it 'prints list of all orphaned namespaces except @hashed' do + expect_list_of_orphans(%w(orphan1 orphan2)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) } + + it 'prints list of orphaned namespaces' do + expect_list_of_orphans(%w(orphan1 orphan2)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace and parents with same name as orphans' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let!(:second_level) { create(:group, path: 'second-level', parent: first_level) } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) } + + it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do + expect_list_of_orphans(%w(orphan1 orphan2 second-level)) + + subject.multi_check + end + end + + context 'no orphans' do + let(:disk_namespaces) { %w(@hashed) } + + it 'prints an empty list ignoring @hashed' do + expect_list_of_orphans([]) + + subject.multi_check + end + end + end + + def expect_list_of_orphans(orphans) + expect(subject).to receive(:print_orphans).with(orphans, 'default') + end +end diff --git a/spec/lib/system_check/orphans/repository_check_spec.rb b/spec/lib/system_check/orphans/repository_check_spec.rb new file mode 100644 index 00000000000..b0c2267d177 --- /dev/null +++ b/spec/lib/system_check/orphans/repository_check_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' +require 'rake_helper' + +describe SystemCheck::Orphans::RepositoryCheck do + let(:storages) { Gitlab.config.repositories.storages.reject { |key, _| key.eql? 'broken' } } + + before do + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + allow(subject).to receive(:fetch_disk_namespaces).and_return(disk_namespaces) + allow(subject).to receive(:fetch_disk_repositories).and_return(disk_repositories) + # silence_output + end + + describe '#multi_check' do + context 'all orphans' do + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) } + let(:disk_repositories) { %w(repo1.git repo2.git) } + + it 'prints list of all orphaned namespaces except @hashed' do + expect_list_of_orphans(%w(orphan1/repo1.git orphan1/repo2.git orphan2/repo1.git orphan2/repo2.git)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let!(:project) { create(:project, path: 'repo', namespace: first_level) } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) } + let(:disk_repositories) { %w(repo.git) } + + it 'prints list of orphaned namespaces' do + expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace and parents with same name as orphans' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let!(:second_level) { create(:group, path: 'second-level', parent: first_level) } + let!(:project) { create(:project, path: 'repo', namespace: first_level) } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) } + let(:disk_repositories) { %w(repo.git) } + + it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do + expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git second-level/repo.git)) + + subject.multi_check + end + end + + context 'no orphans' do + let(:disk_namespaces) { %w(@hashed) } + let(:disk_repositories) { %w(repo.git) } + + it 'prints an empty list ignoring @hashed' do + expect_list_of_orphans([]) + + subject.multi_check + end + end + end + + def expect_list_of_orphans(orphans) + expect(subject).to receive(:print_orphans).with(orphans, 'default') + end +end |