diff options
Diffstat (limited to 'spec/lib')
189 files changed, 10517 insertions, 803 deletions
diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb new file mode 100644 index 00000000000..267318faed4 --- /dev/null +++ b/spec/lib/api/helpers/pagination_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe API::Helpers::Pagination do + let(:resource) { Project.all } + + subject do + Class.new.include(described_class).new + end + + describe '#paginate' do + let(:value) { spy('return value') } + + before do + allow(value).to receive(:to_query).and_return(value) + + allow(subject).to receive(:header).and_return(value) + allow(subject).to receive(:params).and_return(value) + allow(subject).to receive(:request).and_return(value) + end + + describe 'required instance methods' do + let(:return_spy) { spy } + + it 'requires some instance methods' do + expect_message(:header) + expect_message(:params) + expect_message(:request) + + subject.paginate(resource) + end + end + + context 'when resource can be paginated' do + before do + create_list(:empty_project, 3) + end + + describe 'first page' do + before do + allow(subject).to receive(:params) + .and_return({ page: 1, per_page: 2 }) + end + + it 'returns appropriate amount of resources' do + expect(subject.paginate(resource).count).to eq 2 + end + + it 'adds appropriate headers' do + expect_header('X-Total', '3') + expect_header('X-Total-Pages', '2') + expect_header('X-Per-Page', '2') + expect_header('X-Page', '1') + expect_header('X-Next-Page', '2') + expect_header('X-Prev-Page', '') + expect_header('Link', any_args) + + subject.paginate(resource) + end + end + + describe 'second page' do + before do + allow(subject).to receive(:params) + .and_return({ page: 2, per_page: 2 }) + end + + it 'returns appropriate amount of resources' do + expect(subject.paginate(resource).count).to eq 1 + end + + it 'adds appropriate headers' do + expect_header('X-Total', '3') + expect_header('X-Total-Pages', '2') + expect_header('X-Per-Page', '2') + expect_header('X-Page', '2') + expect_header('X-Next-Page', '') + expect_header('X-Prev-Page', '1') + expect_header('Link', any_args) + + subject.paginate(resource) + end + end + end + + def expect_header(name, value) + expect(subject).to receive(:header).with(name, value) + end + + def expect_message(method) + expect(subject).to receive(method) + .at_least(:once).and_return(value) + end + end +end diff --git a/spec/lib/banzai/filter/abstract_link_filter_spec.rb b/spec/lib/banzai/filter/abstract_link_filter_spec.rb deleted file mode 100644 index 1ee31a603e4..00000000000 --- a/spec/lib/banzai/filter/abstract_link_filter_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'spec_helper' - -describe Banzai::Filter::AbstractReferenceFilter do - let(:project) { create(:empty_project) } - - describe '#references_per_project' do - it 'returns a Hash containing references grouped per project paths' do - doc = Nokogiri::HTML.fragment("#1 #{project.to_reference}#2") - filter = described_class.new(doc, project: project) - - expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue) - expect(filter).to receive(:object_sym).twice.and_return(:issue) - - refs = filter.references_per_project - - expect(refs).to be_an_instance_of(Hash) - expect(refs[project.to_reference]).to eq(Set.new(%w[1 2])) - end - end - - describe '#projects_per_reference' do - it 'returns a Hash containing projects grouped per project paths' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(filter).to receive(:references_per_project). - and_return({ project.path_with_namespace => Set.new(%w[1]) }) - - expect(filter.projects_per_reference). - to eq({ project.path_with_namespace => project }) - end - end - - describe '#find_projects_for_paths' do - it 'returns a list of Projects for a list of paths' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(filter.find_projects_for_paths([project.path_with_namespace])). - to eq([project]) - end - end - - describe '#current_project_path' do - it 'returns the path of the current project' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(filter.current_project_path).to eq(project.path_with_namespace) - end - end -end diff --git a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb new file mode 100644 index 00000000000..27684882435 --- /dev/null +++ b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +describe Banzai::Filter::AbstractReferenceFilter do + let(:project) { create(:empty_project) } + + describe '#references_per_project' do + it 'returns a Hash containing references grouped per project paths' do + doc = Nokogiri::HTML.fragment("#1 #{project.path_with_namespace}#2") + filter = described_class.new(doc, project: project) + + expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue) + expect(filter).to receive(:object_sym).twice.and_return(:issue) + + refs = filter.references_per_project + + expect(refs).to be_an_instance_of(Hash) + expect(refs[project.path_with_namespace]).to eq(Set.new(%w[1 2])) + end + end + + describe '#projects_per_reference' do + it 'returns a Hash containing projects grouped per project paths' do + doc = Nokogiri::HTML.fragment('') + filter = described_class.new(doc, project: project) + + expect(filter).to receive(:references_per_project). + and_return({ project.path_with_namespace => Set.new(%w[1]) }) + + expect(filter.projects_per_reference). + to eq({ project.path_with_namespace => project }) + end + end + + describe '#find_projects_for_paths' do + let(:doc) { Nokogiri::HTML.fragment('') } + let(:filter) { described_class.new(doc, project: project) } + + context 'with RequestStore disabled' do + it 'returns a list of Projects for a list of paths' do + expect(filter.find_projects_for_paths([project.path_with_namespace])). + to eq([project]) + end + + it "return an empty array for paths that don't exist" do + expect(filter.find_projects_for_paths(['nonexistent/project'])). + to eq([]) + end + end + + context 'with RequestStore enabled' do + before do + RequestStore.begin! + end + + after do + RequestStore.end! + RequestStore.clear! + end + + it 'returns a list of Projects for a list of paths' do + expect(filter.find_projects_for_paths([project.path_with_namespace])). + to eq([project]) + end + + context "when no project with that path exists" do + it "returns no value" do + expect(filter.find_projects_for_paths(['nonexistent/project'])). + to eq([]) + end + + it "adds the ref to the project refs cache" do + project_refs_cache = {} + allow(filter).to receive(:project_refs_cache).and_return(project_refs_cache) + + filter.find_projects_for_paths(['nonexistent/project']) + + expect(project_refs_cache).to eq({ 'nonexistent/project' => nil }) + end + + context 'when the project refs cache includes nil values' do + before do + # adds { 'nonexistent/project' => nil } to cache + filter.project_from_ref_cached('nonexistent/project') + end + + it "return an empty array for paths that don't exist" do + expect(filter.find_projects_for_paths(['nonexistent/project'])). + to eq([]) + end + end + end + end + end + + describe '#current_project_path' do + it 'returns the path of the current project' do + doc = Nokogiri::HTML.fragment('') + filter = described_class.new(doc, project: project) + + expect(filter.current_project_path).to eq(project.path_with_namespace) + end + end +end diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb index e6c90ad87ee..9703e2315b8 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb @@ -59,9 +59,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do it 'ignores invalid commit IDs' do exp = act = "See #{commit1.id.reverse}...#{commit2.id}" - expect(project).to receive(:valid_repo?).and_return(true) - expect(project.repository).to receive(:commit).with(commit1.id.reverse) - expect(project.repository).to receive(:commit).with(commit2.id) + allow(project.repository).to receive(:commit).with(commit1.id.reverse) expect(reference_filter(act).to_html).to eq exp end @@ -100,14 +98,44 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do end end - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:reference) { range.to_reference(project) } + context 'cross-project / cross-namespace complete reference' do + let(:project2) { create(:project, :public) } + let(:reference) { "#{project2.path_with_namespace}@#{commit1.id}...#{commit2.id}" } - before do - range.project = project2 + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + end + + it 'link has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.text). + to eql("#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eql("Fixed (#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}.)") + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.path_with_namespace}@#{commit1.id.reverse}...#{commit2.id}" + expect(reference_filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.path_with_namespace}@#{commit1.id}...#{commit2.id.reverse}" + expect(reference_filter(act).to_html).to eq exp end + end + + context 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:project, :public, namespace: namespace) } + let(:project2) { create(:project, :public, path: "same-namespace", namespace: namespace) } + let(:reference) { "#{project2.path}@#{commit1.id}...#{commit2.id}" } it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -116,24 +144,65 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) end - it 'links with adjacent text' do + it 'link has valid text' do doc = reference_filter("Fixed (#{reference}.)") - exp = Regexp.escape("#{project2.to_reference}@#{range.reference_link_text}") - expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) + expect(doc.css('a').first.text). + to eql("#{project2.path}@#{commit1.short_id}...#{commit2.short_id}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eql("Fixed (#{project2.path}@#{commit1.short_id}...#{commit2.short_id}.)") end it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" + exp = act = "Fixed #{project2.path}@#{commit1.id.reverse}...#{commit2.id}" expect(reference_filter(act).to_html).to eq exp - exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" + exp = act = "Fixed #{project2.path}@#{commit1.id}...#{commit2.id.reverse}" + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:project, :public, namespace: namespace) } + let(:project2) { create(:project, :public, path: "same-namespace", namespace: namespace) } + let(:reference) { "#{project2.path}@#{commit1.id}...#{commit2.id}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + end + + it 'link has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.text). + to eql("#{project2.path}@#{commit1.short_id}...#{commit2.short_id}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eql("Fixed (#{project2.path}@#{commit1.short_id}...#{commit2.short_id}.)") + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.path}@#{commit1.id.reverse}...#{commit2.id}" + expect(reference_filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.path}@#{commit1.id}...#{commit2.id.reverse}" expect(reference_filter(act).to_html).to eq exp end end context 'cross-project URL reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:namespace) { create(:namespace) } let(:project2) { create(:project, :public, namespace: namespace) } let(:range) { CommitRange.new("#{commit1.id}...master", project) } let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: 'master') } diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index e0f08282551..2e6dcc3a434 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -41,6 +41,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do it 'links with adjacent text' do doc = reference_filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/) end @@ -48,8 +49,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do invalid = invalidate_reference(reference) exp = act = "See #{invalid}" - expect(project).to receive(:valid_repo?).and_return(true) - expect(project.repository).to receive(:commit).with(invalid) expect(reference_filter(act).to_html).to eq exp end @@ -95,34 +94,85 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do end end - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } + context 'cross-project / cross-namespace complete reference' do + let(:namespace) { create(:namespace) } let(:project2) { create(:project, :public, namespace: namespace) } let(:commit) { project2.commit } - let(:reference) { commit.to_reference(project) } + let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" } - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) + expect(doc.css('a').first.text).to eql("#{project2.path_with_namespace}@#{commit.short_id}") end - it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{project2.path_with_namespace}@#{commit.short_id}.)") + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:commit) { project2.commit } + let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" } + + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") - exp = Regexp.escape(project2.to_reference) - expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) + expect(doc.css('a').first.text).to eql("#{project2.path}@#{commit.short_id}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{project2.path}@#{commit.short_id}.)") end it 'ignores invalid commit IDs on the referenced project' do exp = act = "Committed #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:commit) { project2.commit } + let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" } + + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path}@#{commit.short_id}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{project2.path}@#{commit.short_id}.)") + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{invalidate_reference(reference)}" + expect(reference_filter(act).to_html).to eq exp end end context 'cross-project URL reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:namespace) { create(:namespace) } let(:project2) { create(:project, :public, namespace: namespace) } let(:commit) { project2.commit } let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) } diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb index 167397c736b..d9e4525cb28 100644 --- a/spec/lib/banzai/filter/external_link_filter_spec.rb +++ b/spec/lib/banzai/filter/external_link_filter_spec.rb @@ -80,4 +80,18 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do expect(filter(act).to_html).to eq(exp) end end + + context 'for protocol-relative links' do + let(:doc) { filter %q(<p><a href="//google.com/">Google</a></p>) } + + it 'adds rel="nofollow" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'nofollow' + end + + it 'adds rel="noreferrer" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'noreferrer' + end + end end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 8f0b2db3e8e..456dbac0698 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -8,7 +8,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end let(:project) { create(:empty_project, :public) } - let(:issue) { create(:issue, project: project) } + let(:issue) { create(:issue, project: project) } it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) @@ -24,7 +24,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do context 'internal reference' do it_behaves_like 'a reference containing an element node' - let(:reference) { issue.to_reference } + let(:reference) { "##{issue.iid}" } it 'ignores valid references when using non-default tracker' do allow(project).to receive(:default_issues_tracker?).and_return(false) @@ -42,7 +42,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'links with adjacent text' do doc = reference_filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + expect(doc.text).to eql("Fixed (#{reference}.)") end it 'ignores invalid issue IDs' do @@ -116,13 +116,56 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end end - context 'cross-project reference' do + context 'cross-project / cross-namespace complete reference' do it_behaves_like 'a reference containing an element node' - let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" } + + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(described_class).to receive(:find_object). + with(project2, issue.iid). + and_return(nil) + + exp = act = "Issue #{reference}" + expect(reference_filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + end + + it 'link has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path_with_namespace}##{issue.iid}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eq("Fixed (#{project2.path_with_namespace}##{issue.iid}.)") + end + + it 'ignores invalid issue IDs on the referenced project' do + exp = act = "Fixed #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project / same-namespace complete reference' do + it_behaves_like 'a reference containing an element node' + + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:issue) { create(:issue, project: project2) } - let(:reference) { issue.to_reference(project) } + let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" } it 'ignores valid references when cross-reference project uses external tracker' do expect_any_instance_of(described_class).to receive(:find_object). @@ -140,9 +183,16 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do to eq helper.url_for_issue(issue.iid, project2) end - it 'links with adjacent text' do + it 'link has valid text' do doc = reference_filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + + expect(doc.css('a').first.text).to eql("#{project2.path}##{issue.iid}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)") end it 'ignores invalid issue IDs on the referenced project' do @@ -150,9 +200,47 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end + end + + context 'cross-project shorthand reference' do + it_behaves_like 'a reference containing an element node' - it 'ignores out-of-bounds issue IDs on the referenced project' do - exp = act = "Fixed ##{Gitlab::Database::MAX_INT_VALUE + 1}" + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { "#{project2.path}##{issue.iid}" } + + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(described_class).to receive(:find_object). + with(project2, issue.iid). + and_return(nil) + + exp = act = "Issue #{reference}" + expect(reference_filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + end + + it 'link has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path}##{issue.iid}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)") + end + + it 'ignores invalid issue IDs on the referenced project' do + exp = act = "Fixed #{invalidate_reference(reference)}" expect(reference_filter(act).to_html).to eq exp end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 9c09f00ae8a..284641fb20a 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -4,7 +4,7 @@ require 'html/pipeline' describe Banzai::Filter::LabelReferenceFilter, lib: true do include FilterSpecHelper - let(:project) { create(:empty_project, :public) } + let(:project) { create(:empty_project, :public, name: 'sample-project') } let(:label) { create(:label, project: project) } let(:reference) { label.to_reference } @@ -48,6 +48,14 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name) end + context 'project that does not exist referenced' do + let(:result) { reference_filter('aaa/bbb~ccc') } + + it 'does not link reference' do + expect(result.to_html).to eq 'aaa/bbb~ccc' + end + end + describe 'label span element' do it 'includes default classes' do doc = reference_filter("Label #{reference}") @@ -334,14 +342,14 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end context 'with project reference' do - let(:reference) { project.to_reference + group_label.to_reference(format: :name) } + let(:reference) { "#{project.to_reference}#{group_label.to_reference(format: :name)}" } it 'links to a valid reference' do doc = reference_filter("See #{reference}", project: project) expect(doc.css('a').first.attr('href')).to eq urls. namespace_project_issues_url(project.namespace, project, label_name: group_label.name) - expect(doc.text).to eq 'See gfm references' + expect(doc.text).to eq "See gfm references" end it 'links with adjacent text' do @@ -357,68 +365,247 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end end - describe 'cross project label references' do - context 'valid project referenced' do - let(:another_project) { create(:empty_project, :public) } - let(:project_name) { another_project.name_with_namespace } - let(:label) { create(:label, project: another_project, color: '#00ff00') } - let(:reference) { label.to_reference(project) } + describe 'cross-project / cross-namespace complete reference' do + let(:project2) { create(:empty_project) } + let(:label) { create(:label, project: project2, color: '#00ff00') } + let(:reference) { "#{project2.path_with_namespace}~#{label.name}" } + let!(:result) { reference_filter("See #{reference}") } - let!(:result) { reference_filter("See #{reference}") } + it 'links to a valid reference' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project2.namespace, + project2, + label_name: label.name) + end - it 'points to referenced project issues page' do - expect(result.css('a').first.attr('href')) - .to eq urls.namespace_project_issues_url(another_project.namespace, - another_project, - label_name: label.name) - end + it 'has valid color' do + expect(result.css('a span').first.attr('style')).to match /background-color: #00ff00/ + end - it 'has valid color' do - expect(result.css('a span').first.attr('style')) - .to match /background-color: #00ff00/ - end + it 'has valid link text' do + expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name_with_namespace}" + end - it 'contains cross project content' do - expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}" - end + it 'has valid text' do + expect(result.text).to eq "See #{label.name} in #{project2.name_with_namespace}" end - context 'project that does not exist referenced' do - let(:result) { reference_filter('aaa/bbb~ccc') } + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" - it 'does not link reference' do - expect(result.to_html).to eq 'aaa/bbb~ccc' - end + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:empty_project, namespace: namespace) } + let(:label) { create(:label, project: project2, color: '#00ff00') } + let(:reference) { "#{project2.path_with_namespace}~#{label.name}" } + let!(:result) { reference_filter("See #{reference}") } + + it 'links to a valid reference' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project2.namespace, + project2, + label_name: label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')).to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name}" + end + + it 'has valid text' do + expect(result.text).to eq "See #{label.name} in #{project2.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:empty_project, namespace: namespace) } + let(:label) { create(:label, project: project2, color: '#00ff00') } + let(:reference) { "#{project2.path}~#{label.name}" } + let!(:result) { reference_filter("See #{reference}") } + + it 'links to a valid reference' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project2.namespace, + project2, + label_name: label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')). + to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name}" + end + + it 'has valid text' do + expect(result.text).to eq "See #{label.name} in #{project2.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp end end describe 'cross group label references' do - context 'valid project referenced' do - let(:group) { create(:group) } - let(:project) { create(:empty_project, :public, namespace: group) } - let(:another_group) { create(:group) } - let(:another_project) { create(:empty_project, :public, namespace: another_group) } - let(:project_name) { another_project.name_with_namespace } - let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') } - let(:reference) { another_project.to_reference + group_label.to_reference } - - let!(:result) { reference_filter("See #{reference}", project: project) } - - it 'points to referenced project issues page' do - expect(result.css('a').first.attr('href')) - .to eq urls.namespace_project_issues_url(another_project.namespace, - another_project, - label_name: group_label.name) - end + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:another_group) { create(:group) } + let(:another_project) { create(:empty_project, :public, namespace: another_group) } + let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') } + let(:reference) { "#{another_project.path_with_namespace}~#{group_label.name}" } + let!(:result) { reference_filter("See #{reference}", project: project) } - it 'has valid color' do - expect(result.css('a span').first.attr('style')) - .to match /background-color: #00ff00/ - end + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(another_project.namespace, + another_project, + label_name: group_label.name) + end - it 'contains cross project content' do - expect(result.css('a').first.text).to eq "#{group_label.name} in #{project_name}" - end + it 'has valid color' do + expect(result.css('a span').first.attr('style')). + to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text). + to eq "#{group_label.name} in #{another_project.name_with_namespace}" + end + + it 'has valid text' do + expect(result.text). + to eq "See #{group_label.name} in #{another_project.name_with_namespace}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'cross-project / same-group_label complete reference' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:another_project) { create(:empty_project, :public, namespace: group) } + let(:group_label) { create(:group_label, group: group, color: '#00ff00') } + let(:reference) { "#{another_project.path_with_namespace}~#{group_label.name}" } + let!(:result) { reference_filter("See #{reference}", project: project) } + + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')). + to eq urls.namespace_project_issues_url(another_project.namespace, + another_project, + label_name: group_label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')). + to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text). + to eq "#{group_label.name} in #{another_project.name}" + end + + it 'has valid text' do + expect(result.text). + to eq "See #{group_label.name} in #{another_project.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'same project / same group_label complete reference' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:group_label) { create(:group_label, group: group, color: '#00ff00') } + let(:reference) { "#{project.path_with_namespace}~#{group_label.name}" } + let!(:result) { reference_filter("See #{reference}", project: project) } + + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project.namespace, + project, + label_name: group_label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')) + .to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text).to eq group_label.name + end + + it 'has valid text' do + expect(result.text).to eq "See #{group_label.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'same project / same group_label shorthand reference' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:group_label) { create(:group_label, group: group, color: '#00ff00') } + let(:reference) { "#{project.path}~#{group_label.name}" } + let!(:result) { reference_filter("See #{reference}", project: project) } + + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project.namespace, + project, + label_name: group_label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')). + to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text).to eq group_label.name + end + + it 'has valid text' do + expect(result.text).to eq "See #{group_label.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp end end end diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb new file mode 100644 index 00000000000..51883782e19 --- /dev/null +++ b/spec/lib/banzai/filter/math_filter_spec.rb @@ -0,0 +1,127 @@ +require 'spec_helper' + +describe Banzai::Filter::MathFilter, lib: true do + include FilterSpecHelper + + it 'leaves regular inline code unchanged' do + input = "<code>2+2</code>" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'removes surrounding dollar signs and adds class code, math and js-render-math' do + doc = filter("$<code>2+2</code>$") + + expect(doc.to_s).to eq '<code class="code math js-render-math" data-math-style="inline">2+2</code>' + end + + it 'only removes surrounding dollar signs' do + doc = filter("test $<code>2+2</code>$ test") + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq 'test ' + expect(after.to_s).to eq ' test' + end + + it 'only removes surrounding single dollar sign' do + doc = filter("test $$<code>2+2</code>$$ test") + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq 'test $' + expect(after.to_s).to eq '$ test' + end + + it 'adds data-math-style inline attribute to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code['data-math-style']).to eq 'inline' + end + + it 'adds class code and math to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code[:class]).to include("code") + expect(code[:class]).to include("math") + end + + it 'adds js-render-math class to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code[:class]).to include("js-render-math") + end + + # Cases with faulty syntax. Should be a no-op + + it 'ignores cases with missing dolar sign at the end' do + input = "test $<code>2+2</code> test" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'ignores cases with missing dolar sign at the beginning' do + input = "test <code>2+2</code>$ test" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'ignores dollar signs if it is not adjacent' do + input = '<p>We check strictly $<code>2+2</code> and <code>2+2</code>$ </p>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'ignores dollar signs if they are inside another element' do + input = '<p>We check strictly <em>$</em><code>2+2</code><em>$</em></p>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + # Display math + + it 'adds data-math-style display attribute to display math' do + doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>') + pre = doc.xpath('descendant-or-self::pre').first + + expect(pre['data-math-style']).to eq 'display' + end + + it 'adds js-render-math class to display math' do + doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>') + pre = doc.xpath('descendant-or-self::pre').first + + expect(pre[:class]).to include("js-render-math") + end + + it 'ignores code blocks that are not math' do + input = '<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>2+2</code></pre>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'requires the pre to contain both code and math' do + input = '<pre class="highlight js-syntax-highlight plaintext math" v-pre="true"><code>2+2</code></pre>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'dollar signs around to display math' do + doc = filter('$<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>$') + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq '$' + expect(after.to_s).to eq '$' + 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 274258a045c..275010c1a2c 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do include FilterSpecHelper - let(:project) { create(:project, :public) } + let(:project) { create(:empty_project, :public) } let(:merge) { create(:merge_request, source_project: project) } it 'requires project context' do @@ -86,23 +86,97 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do end end - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:merge) { create(:merge_request, source_project: project2) } - let(:reference) { merge.to_reference(project) } + context 'cross-project / cross-namespace complete reference' do + let(:project2) { create(:empty_project, :public) } + let(:merge) { create(:merge_request, source_project: project2) } + let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" } it 'links to a valid reference' do doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_merge_request_url(project2.namespace, - project, merge) + project2, merge) end - it 'links with adjacent text' do + it 'link has valid text' do doc = reference_filter("Merge (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + + expect(doc.css('a').first.text).to eq(reference) + end + + it 'has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.text).to eq("Merge (#{reference}.)") + end + + it 'ignores invalid merge IDs on the referenced project' do + exp = act = "Merge #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let!(:merge) { create(:merge_request, source_project: project2) } + let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_merge_request_url(project2.namespace, + project2, merge) + end + + it 'link has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.css('a').first.text).to eq("#{project2.path}!#{merge.iid}") + end + + it 'has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.text).to eq("Merge (#{project2.path}!#{merge.iid}.)") + end + + it 'ignores invalid merge IDs on the referenced project' do + exp = act = "Merge #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let!(:merge) { create(:merge_request, source_project: project2) } + let(:reference) { "#{project2.path}!#{merge.iid}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_merge_request_url(project2.namespace, + project2, merge) + end + + it 'link has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.css('a').first.text).to eq("#{project2.path}!#{merge.iid}") + end + + it 'has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.text).to eq("Merge (#{project2.path}!#{merge.iid}.)") end it 'ignores invalid merge IDs on the referenced project' do diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index 7419863d848..73b5edb99b3 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -148,13 +148,51 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do end end - describe 'cross project milestone references' do - let(:another_project) { create(:empty_project, :public) } - let(:project_path) { another_project.path_with_namespace } - let(:milestone) { create(:milestone, project: another_project) } - let(:reference) { milestone.to_reference(project) } + describe 'cross-project / cross-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:another_project) { create(:empty_project, :public, namespace: namespace) } + let(:milestone) { create(:milestone, project: another_project) } + let(:reference) { "#{another_project.path_with_namespace}%#{milestone.iid}" } + let!(:result) { reference_filter("See #{reference}") } - let!(:result) { reference_filter("See #{reference}") } + it 'points to referenced project milestone page' do + expect(result.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(another_project.namespace, + another_project, + milestone) + end + + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text). + to eq("#{milestone.name} in #{another_project.path_with_namespace}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text). + to eq("See (#{milestone.name} in #{another_project.path_with_namespace}.)") + end + + it 'escapes the name attribute' do + allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="}) + + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.text). + to eq "#{milestone.name} in #{another_project.path_with_namespace}" + end + end + + describe 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:another_project) { create(:empty_project, :public, namespace: namespace) } + let(:milestone) { create(:milestone, project: another_project) } + let(:reference) { "#{another_project.path_with_namespace}%#{milestone.iid}" } + let!(:result) { reference_filter("See #{reference}") } it 'points to referenced project milestone page' do expect(result.css('a').first.attr('href')).to eq urls. @@ -163,14 +201,66 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do milestone) end - it 'contains cross project content' do - expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_path}" + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text). + to eq("#{milestone.name} in #{another_project.path}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text). + to eq("See (#{milestone.name} in #{another_project.path}.)") end it 'escapes the name attribute' do allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="}) + + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.text). + to eq "#{milestone.name} in #{another_project.path}" + end + end + + describe 'cross project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:another_project) { create(:empty_project, :public, namespace: namespace) } + let(:milestone) { create(:milestone, project: another_project) } + let(:reference) { "#{another_project.path}%#{milestone.iid}" } + let!(:result) { reference_filter("See #{reference}") } + + it 'points to referenced project milestone page' do + expect(result.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(another_project.namespace, + another_project, + milestone) + end + + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text). + to eq("#{milestone.name} in #{another_project.path}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text). + to eq("See (#{milestone.name} in #{another_project.path}.)") + end + + it 'escapes the name attribute' do + allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="}) + doc = reference_filter("See #{reference}") - expect(doc.css('a').first.text).to eq "#{milestone.name} in #{project_path}" + + expect(doc.css('a').first.text). + to eq "#{milestone.name} in #{another_project.path}" end end end diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 2bfa51deb20..df2dd173b57 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -175,7 +175,7 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw) doc = filter(image(escaped)) - expect(doc.at_css('img')['src']).to match '/raw/' + expect(doc.at_css('img')['src']).to eq "/#{project_path}/raw/#{Addressable::URI.escape(ref)}/#{escaped}" end context 'when requested path is a file in the repo' do diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb index 9b92d1a3926..e036514d283 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb @@ -79,11 +79,11 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do end end - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } + context 'cross-project / cross-namespace complete reference' do + let(:namespace) { create(:namespace) } let(:project2) { create(:empty_project, :public, namespace: namespace) } - let(:snippet) { create(:project_snippet, project: project2) } - let(:reference) { snippet.to_reference(project) } + let!(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" } it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -92,9 +92,82 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) end - it 'links with adjacent text' do + it 'link has valid text' do doc = reference_filter("See (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + + expect(doc.css('a').first.text).to eql(reference) + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{reference}.)") + end + + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let!(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end + + it 'link has valid text' do + doc = reference_filter("See (#{project2.path}$#{snippet.id}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path}$#{snippet.id}") + end + + it 'has valid text' do + doc = reference_filter("See (#{project2.path}$#{snippet.id}.)") + + expect(doc.text).to eql("See (#{project2.path}$#{snippet.id}.)") + end + + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let!(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { "#{project2.path}$#{snippet.id}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end + + it 'link has valid text' do + doc = reference_filter("See (#{project2.path}$#{snippet.id}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path}$#{snippet.id}") + end + + it 'has valid text' do + doc = reference_filter("See (#{project2.path}$#{snippet.id}.)") + + expect(doc.text).to eql("See (#{project2.path}$#{snippet.id}.)") end it 'ignores invalid snippet IDs on the referenced project' do diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb index 356dd01a03a..70b31f3a880 100644 --- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb +++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb @@ -22,7 +22,7 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do html = header(i, "Header #{i}") doc = filter(html) - expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}" + expect(doc.css("h#{i} a").first.attr('id')).to eq "user-content-header-#{i}" end end @@ -32,7 +32,12 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do expect(doc.css('h1 a').first.attr('class')).to eq 'anchor' end - it 'links to the id' do + it 'has a namespaced id' do + doc = filter(header(1, 'Header')) + expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-header' + end + + it 'links to the non-namespaced id' do doc = filter(header(1, 'Header')) expect(doc.css('h1 a').first.attr('href')).to eq '#header' end @@ -40,29 +45,29 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do describe 'generated IDs' do it 'translates spaces to dashes' do doc = filter(header(1, 'This header has spaces in it')) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it' + expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-has-spaces-in-it' end it 'squeezes multiple spaces and dashes' do doc = filter(header(1, 'This---header is poorly-formatted')) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted' + expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-poorly-formatted' end it 'removes punctuation' do doc = filter(header(1, "This, header! is, filled. with @ punctuation?")) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation' + expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-filled-with-punctuation' end it 'appends a unique number to duplicates' do doc = filter(header(1, 'One') + header(2, 'One')) - expect(doc.css('h1 a').first.attr('id')).to eq 'one' - expect(doc.css('h2 a').first.attr('id')).to eq 'one-1' + expect(doc.css('h1 a').first.attr('href')).to eq '#one' + expect(doc.css('h2 a').first.attr('href')).to eq '#one-1' end it 'supports Unicode' do doc = filter(header(1, '한글')) - expect(doc.css('h1 a').first.attr('id')).to eq '한글' + expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-한글' expect(doc.css('h1 a').first.attr('href')).to eq '#한글' end end diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb index 50a5d1a19ba..0af36776a54 100644 --- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb @@ -12,7 +12,17 @@ describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do context 'when the link has a data-issue attribute' do before { link['data-external-issue'] = 123 } - it_behaves_like "referenced feature visibility", "issues" + levels = [ProjectFeature::DISABLED, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] + + levels.each do |level| + it "creates reference when the feature is #{level}" do + project.project_feature.update(issues_access_level: level) + + visible_nodes = subject.nodes_visible_to_user(user, [link]) + + expect(visible_nodes).to include(link) + end + end end end diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb new file mode 100644 index 00000000000..015a7f80e03 --- /dev/null +++ b/spec/lib/bitbucket/collection_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +# Emulates paginator. It returns 2 pages with results +class TestPaginator + def initialize + @current_page = 0 + end + + def items + @current_page += 1 + + raise StopIteration if @current_page > 2 + + ["result_1_page_#{@current_page}", "result_2_page_#{@current_page}"] + end +end + +describe Bitbucket::Collection do + it "iterates paginator" do + collection = described_class.new(TestPaginator.new) + + expect(collection.to_a).to match(["result_1_page_1", "result_2_page_1", "result_1_page_2", "result_2_page_2"]) + end +end diff --git a/spec/lib/bitbucket/connection_spec.rb b/spec/lib/bitbucket/connection_spec.rb new file mode 100644 index 00000000000..14faeb231a9 --- /dev/null +++ b/spec/lib/bitbucket/connection_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Bitbucket::Connection do + before do + allow_any_instance_of(described_class).to receive(:provider).and_return(double(app_id: '', app_secret: '')) + end + + describe '#get' do + it 'calls OAuth2::AccessToken::get' do + expect_any_instance_of(OAuth2::AccessToken).to receive(:get).and_return(double(parsed: true)) + + connection = described_class.new({}) + + connection.get('/users') + end + end + + describe '#expired?' do + it 'calls connection.expired?' do + expect_any_instance_of(OAuth2::AccessToken).to receive(:expired?).and_return(true) + + expect(described_class.new({}).expired?).to be_truthy + end + end + + describe '#refresh!' do + it 'calls connection.refresh!' do + response = double(token: nil, expires_at: nil, expires_in: nil, refresh_token: nil) + + expect_any_instance_of(OAuth2::AccessToken).to receive(:refresh!).and_return(response) + + described_class.new({}).refresh! + end + end +end diff --git a/spec/lib/bitbucket/page_spec.rb b/spec/lib/bitbucket/page_spec.rb new file mode 100644 index 00000000000..04d5a0470b1 --- /dev/null +++ b/spec/lib/bitbucket/page_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Bitbucket::Page do + let(:response) { { 'values' => [{ 'username' => 'Ben' }], 'pagelen' => 2, 'next' => '' } } + + before do + # Autoloading hack + Bitbucket::Representation::User.new({}) + end + + describe '#items' do + it 'returns collection of needed objects' do + page = described_class.new(response, :user) + + expect(page.items.first).to be_a(Bitbucket::Representation::User) + expect(page.items.count).to eq(1) + end + end + + describe '#attrs' do + it 'returns attributes' do + page = described_class.new(response, :user) + + expect(page.attrs.keys).to include(:pagelen, :next) + end + end + + describe '#next?' do + it 'returns true' do + page = described_class.new(response, :user) + + expect(page.next?).to be_truthy + end + + it 'returns false' do + response['next'] = nil + page = described_class.new(response, :user) + + expect(page.next?).to be_falsey + end + end + + describe '#next' do + it 'returns next attribute' do + page = described_class.new(response, :user) + + expect(page.next).to eq('') + end + end +end diff --git a/spec/lib/bitbucket/paginator_spec.rb b/spec/lib/bitbucket/paginator_spec.rb new file mode 100644 index 00000000000..2c972da682e --- /dev/null +++ b/spec/lib/bitbucket/paginator_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Bitbucket::Paginator do + let(:last_page) { double(:page, next?: false, items: ['item_2']) } + let(:first_page) { double(:page, next?: true, next: last_page, items: ['item_1']) } + + describe 'items' do + it 'return items and raises StopIteration in the end' do + paginator = described_class.new(nil, nil, nil) + + allow(paginator).to receive(:fetch_next_page).and_return(first_page) + expect(paginator.items).to match(['item_1']) + + allow(paginator).to receive(:fetch_next_page).and_return(last_page) + expect(paginator.items).to match(['item_2']) + + allow(paginator).to receive(:fetch_next_page).and_return(nil) + expect{ paginator.items }.to raise_error(StopIteration) + end + end +end diff --git a/spec/lib/bitbucket/representation/comment_spec.rb b/spec/lib/bitbucket/representation/comment_spec.rb new file mode 100644 index 00000000000..fec243a9f96 --- /dev/null +++ b/spec/lib/bitbucket/representation/comment_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Bitbucket::Representation::Comment do + describe '#author' do + it { expect(described_class.new('user' => { 'username' => 'Ben' }).author).to eq('Ben') } + it { expect(described_class.new({}).author).to be_nil } + end + + describe '#note' do + it { expect(described_class.new('content' => { 'raw' => 'Text' }).note).to eq('Text') } + it { expect(described_class.new({}).note).to be_nil } + end + + describe '#created_at' do + it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) } + end + + describe '#updated_at' do + it { expect(described_class.new('updated_on' => Date.today).updated_at).to eq(Date.today) } + it { expect(described_class.new('created_on' => Date.today).updated_at).to eq(Date.today) } + end +end diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb new file mode 100644 index 00000000000..20f47224aa8 --- /dev/null +++ b/spec/lib/bitbucket/representation/issue_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Bitbucket::Representation::Issue do + describe '#iid' do + it { expect(described_class.new('id' => 1).iid).to eq(1) } + end + + describe '#kind' do + it { expect(described_class.new('kind' => 'bug').kind).to eq('bug') } + end + + describe '#milestone' do + it { expect(described_class.new({ 'milestone' => { 'name' => '1.0' } }).milestone).to eq('1.0') } + it { expect(described_class.new({}).milestone).to be_nil } + end + + describe '#author' do + it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' } }).author).to eq('Ben') } + it { expect(described_class.new({}).author).to be_nil } + end + + describe '#description' do + it { expect(described_class.new({ 'content' => { 'raw' => 'Text' } }).description).to eq('Text') } + it { expect(described_class.new({}).description).to be_nil } + end + + describe '#state' do + it { expect(described_class.new({ 'state' => 'invalid' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'wontfix' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'resolved' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'duplicate' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'closed' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'opened' }).state).to eq('opened') } + end + + describe '#title' do + it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') } + end + + describe '#created_at' do + it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) } + end + + describe '#updated_at' do + it { expect(described_class.new('edited_on' => Date.today).updated_at).to eq(Date.today) } + end +end diff --git a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb new file mode 100644 index 00000000000..673dcf22ce8 --- /dev/null +++ b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Bitbucket::Representation::PullRequestComment do + describe '#iid' do + it { expect(described_class.new('id' => 1).iid).to eq(1) } + end + + describe '#file_path' do + it { expect(described_class.new('inline' => { 'path' => '/path' }).file_path).to eq('/path') } + end + + describe '#old_pos' do + it { expect(described_class.new('inline' => { 'from' => 3 }).old_pos).to eq(3) } + end + + describe '#new_pos' do + it { expect(described_class.new('inline' => { 'to' => 3 }).new_pos).to eq(3) } + end + + describe '#parent_id' do + it { expect(described_class.new({ 'parent' => { 'id' => 2 } }).parent_id).to eq(2) } + it { expect(described_class.new({}).parent_id).to be_nil } + end + + describe '#inline?' do + it { expect(described_class.new('inline' => {}).inline?).to be_truthy } + it { expect(described_class.new({}).inline?).to be_falsey } + end + + describe '#has_parent?' do + it { expect(described_class.new('parent' => {}).has_parent?).to be_truthy } + it { expect(described_class.new({}).has_parent?).to be_falsey } + end +end diff --git a/spec/lib/bitbucket/representation/pull_request_spec.rb b/spec/lib/bitbucket/representation/pull_request_spec.rb new file mode 100644 index 00000000000..30453528be4 --- /dev/null +++ b/spec/lib/bitbucket/representation/pull_request_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Bitbucket::Representation::PullRequest do + describe '#iid' do + it { expect(described_class.new('id' => 1).iid).to eq(1) } + end + + describe '#author' do + it { expect(described_class.new({ 'author' => { 'username' => 'Ben' } }).author).to eq('Ben') } + it { expect(described_class.new({}).author).to be_nil } + end + + describe '#description' do + it { expect(described_class.new({ 'description' => 'Text' }).description).to eq('Text') } + it { expect(described_class.new({}).description).to be_nil } + end + + describe '#state' do + it { expect(described_class.new({ 'state' => 'MERGED' }).state).to eq('merged') } + it { expect(described_class.new({ 'state' => 'DECLINED' }).state).to eq('closed') } + it { expect(described_class.new({}).state).to eq('opened') } + end + + describe '#title' do + it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') } + end + + describe '#source_branch_name' do + it { expect(described_class.new({ source: { branch: { name: 'feature' } } }.with_indifferent_access).source_branch_name).to eq('feature') } + it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_name).to be_nil } + end + + describe '#source_branch_sha' do + it { expect(described_class.new({ source: { commit: { hash: 'abcd123' } } }.with_indifferent_access).source_branch_sha).to eq('abcd123') } + it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_sha).to be_nil } + end + + describe '#target_branch_name' do + it { expect(described_class.new({ destination: { branch: { name: 'master' } } }.with_indifferent_access).target_branch_name).to eq('master') } + it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_name).to be_nil } + end + + describe '#target_branch_sha' do + it { expect(described_class.new({ destination: { commit: { hash: 'abcd123' } } }.with_indifferent_access).target_branch_sha).to eq('abcd123') } + it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_sha).to be_nil } + end +end diff --git a/spec/lib/bitbucket/representation/repo_spec.rb b/spec/lib/bitbucket/representation/repo_spec.rb new file mode 100644 index 00000000000..adcd978e1b3 --- /dev/null +++ b/spec/lib/bitbucket/representation/repo_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Bitbucket::Representation::Repo do + describe '#has_wiki?' do + it { expect(described_class.new({ 'has_wiki' => false }).has_wiki?).to be_falsey } + it { expect(described_class.new({ 'has_wiki' => true }).has_wiki?).to be_truthy } + end + + describe '#name' do + it { expect(described_class.new({ 'name' => 'test' }).name).to eq('test') } + end + + describe '#valid?' do + it { expect(described_class.new({ 'scm' => 'hg' }).valid?).to be_falsey } + it { expect(described_class.new({ 'scm' => 'git' }).valid?).to be_truthy } + end + + describe '#full_name' do + it { expect(described_class.new({ 'full_name' => 'test_full' }).full_name).to eq('test_full') } + end + + describe '#description' do + it { expect(described_class.new({ 'description' => 'desc' }).description).to eq('desc') } + end + + describe '#issues_enabled?' do + it { expect(described_class.new({ 'has_issues' => false }).issues_enabled?).to be_falsey } + it { expect(described_class.new({ 'has_issues' => true }).issues_enabled?).to be_truthy } + end + + describe '#owner_and_slug' do + it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner_and_slug).to eq(['ben', 'test']) } + end + + describe '#owner' do + it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner).to eq('ben') } + end + + describe '#slug' do + it { expect(described_class.new({ 'full_name' => 'ben/test' }).slug).to eq('test') } + end + + describe '#clone_url' do + it 'builds url' do + data = { 'links' => { 'clone' => [ { 'name' => 'https', 'href' => 'https://bibucket.org/test/test.git' }] } } + expect(described_class.new(data).clone_url('abc')).to eq('https://x-token-auth:abc@bibucket.org/test/test.git') + end + end +end diff --git a/spec/lib/bitbucket/representation/user_spec.rb b/spec/lib/bitbucket/representation/user_spec.rb new file mode 100644 index 00000000000..f79ff4edb7b --- /dev/null +++ b/spec/lib/bitbucket/representation/user_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Bitbucket::Representation::User do + describe '#username' do + it 'returns correct value' do + user = described_class.new('username' => 'Ben') + + expect(user.username).to eq('Ben') + end + end +end diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb index 898f1e84ab0..0762fd7e56a 100644 --- a/spec/lib/ci/ansi2html_spec.rb +++ b/spec/lib/ci/ansi2html_spec.rb @@ -136,6 +136,14 @@ describe Ci::Ansi2html, lib: true do expect(subject.convert("<")[:html]).to eq('<') end + it "replaces newlines with line break tags" do + expect(subject.convert("\n")[:html]).to eq('<br>') + end + + it "groups carriage returns with newlines" do + expect(subject.convert("\r\n")[:html]).to eq('<br>') + end + describe "incremental update" do shared_examples 'stateable converter' do let(:pass1) { subject.convert(pre_text) } diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 84f21631719..f824e2e1efe 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -483,7 +483,7 @@ module Ci context 'when global variables are defined' do let(:variables) do - { VAR1: 'value1', VAR2: 'value2' } + { 'VAR1' => 'value1', 'VAR2' => 'value2' } end let(:config) do { @@ -495,18 +495,18 @@ module Ci it 'returns global variables' do expect(subject).to contain_exactly( - { key: :VAR1, value: 'value1', public: true }, - { key: :VAR2, value: 'value2', public: true } + { 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' } + { 'VAR1' => 'global1', 'VAR3' => 'global3' } end let(:job_variables) do - { VAR1: 'value1', VAR2: 'value2' } + { 'VAR1' => 'value1', 'VAR2' => 'value2' } end let(:config) do { @@ -518,9 +518,9 @@ module Ci 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 } + { key: 'VAR3', value: 'global3', public: true }, + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } ) end end @@ -535,13 +535,13 @@ module Ci context 'when syntax is correct' do let(:variables) do - { VAR1: 'value1', VAR2: 'value2' } + { '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 } + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } ) end end @@ -549,7 +549,7 @@ module Ci context 'when syntax is incorrect' do context 'when variables defined but invalid' do let(:variables) do - [ :VAR1, 'value1', :VAR2, 'value2' ] + [ 'VAR1', 'value1', 'VAR2', 'value2' ] end it 'raises error' do @@ -769,6 +769,19 @@ module Ci 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 @@ -1124,8 +1137,8 @@ EOT end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash") end - it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do - config = YAML.dump({ extra: { services: "test" } }) + 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 an array of strings") diff --git a/spec/lib/constraints/constrainer_helper_spec.rb b/spec/lib/constraints/constrainer_helper_spec.rb deleted file mode 100644 index 27c8d72aefc..00000000000 --- a/spec/lib/constraints/constrainer_helper_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe ConstrainerHelper, lib: true do - include ConstrainerHelper - - describe '#extract_resource_path' do - it { expect(extract_resource_path('/gitlab/')).to eq('gitlab') } - it { expect(extract_resource_path('///gitlab//')).to eq('gitlab') } - it { expect(extract_resource_path('/gitlab.atom')).to eq('gitlab') } - - context 'relative url' do - before do - allow(Gitlab::Application.config).to receive(:relative_url_root) { '/gitlab' } - end - - it { expect(extract_resource_path('/gitlab/foo')).to eq('foo') } - it { expect(extract_resource_path('/foo/bar')).to eq('foo/bar') } - end - end -end diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb index 42299b17c2b..96dacdc5cd2 100644 --- a/spec/lib/constraints/group_url_constrainer_spec.rb +++ b/spec/lib/constraints/group_url_constrainer_spec.rb @@ -4,16 +4,27 @@ describe GroupUrlConstrainer, lib: true do let!(:group) { create(:group, path: 'gitlab') } describe '#matches?' do - context 'root group' do - it { expect(subject.matches?(request '/gitlab')).to be_truthy } - it { expect(subject.matches?(request '/gitlab.atom')).to be_truthy } - it { expect(subject.matches?(request '/gitlab/edit')).to be_falsey } - it { expect(subject.matches?(request '/gitlab-ce')).to be_falsey } - it { expect(subject.matches?(request '/.gitlab')).to be_falsey } + context 'valid request' do + let(:request) { build_request(group.path) } + + it { expect(subject.matches?(request)).to be_truthy } + end + + context 'valid request for nested group' do + let!(:nested_group) { create(:group, path: 'nested', parent: group) } + let!(:request) { build_request('gitlab/nested') } + + it { expect(subject.matches?(request)).to be_truthy } + end + + context 'invalid request' do + let(:request) { build_request('foo') } + + it { expect(subject.matches?(request)).to be_falsey } end end - def request(path) - double(:request, path: path) + def build_request(path) + double(:request, params: { id: path }) end end diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb new file mode 100644 index 00000000000..94266f6653b --- /dev/null +++ b/spec/lib/constraints/project_url_constrainer_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe ProjectUrlConstrainer, lib: true do + let!(:project) { create(:project) } + let!(:namespace) { project.namespace } + + describe '#matches?' do + context 'valid request' do + let(:request) { build_request(namespace.path, project.path) } + + it { expect(subject.matches?(request)).to be_truthy } + end + + context 'invalid request' do + context "non-existing project" do + let(:request) { build_request('foo', 'bar') } + + it { expect(subject.matches?(request)).to be_falsey } + end + + context "project id ending with .git" do + let(:request) { build_request(namespace.path, project.path + '.git') } + + it { expect(subject.matches?(request)).to be_falsey } + end + end + end + + def build_request(namespace, project) + double(:request, params: { namespace_id: namespace, id: project }) + end +end diff --git a/spec/lib/constraints/user_url_constrainer_spec.rb b/spec/lib/constraints/user_url_constrainer_spec.rb index b3f8530c609..207b6fe6c9e 100644 --- a/spec/lib/constraints/user_url_constrainer_spec.rb +++ b/spec/lib/constraints/user_url_constrainer_spec.rb @@ -1,16 +1,23 @@ require 'spec_helper' describe UserUrlConstrainer, lib: true do - let!(:username) { create(:user, username: 'dz') } + let!(:user) { create(:user, username: 'dz') } describe '#matches?' do - it { expect(subject.matches?(request '/dz')).to be_truthy } - it { expect(subject.matches?(request '/dz.atom')).to be_truthy } - it { expect(subject.matches?(request '/dz/projects')).to be_falsey } - it { expect(subject.matches?(request '/gitlab')).to be_falsey } + context 'valid request' do + let(:request) { build_request(user.username) } + + it { expect(subject.matches?(request)).to be_truthy } + end + + context 'invalid request' do + let(:request) { build_request('foo') } + + it { expect(subject.matches?(request)).to be_falsey } + end end - def request(path) - double(:request, path: path) + def build_request(username) + double(:request, params: { username: username }) end end diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb index a6d8e6927e0..ec2f66b1136 100644 --- a/spec/lib/event_filter_spec.rb +++ b/spec/lib/event_filter_spec.rb @@ -7,6 +7,10 @@ describe EventFilter, lib: true do let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) } let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) } + let!(:created_event) { create(:event, action: Event::CREATED, project: public_project, target: public_project, author: source_user) } + let!(:updated_event) { create(:event, action: Event::UPDATED, project: public_project, target: public_project, author: source_user) } + let!(:closed_event) { create(:event, action: Event::CLOSED, project: public_project, target: public_project, author: source_user) } + let!(:reopened_event) { create(:event, action: Event::REOPENED, project: public_project, target: public_project, author: source_user) } let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) } let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) } let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) } @@ -21,6 +25,11 @@ describe EventFilter, lib: true do expect(events).to contain_exactly(merged_event) end + it 'applies issue filter' do + events = EventFilter.new(EventFilter.issue).apply_filter(Event.all) + expect(events).to contain_exactly(created_event, updated_event, closed_event, reopened_event) + end + it 'applies comments filter' do events = EventFilter.new(EventFilter.comments).apply_filter(Event.all) expect(events).to contain_exactly(comments_event) @@ -33,17 +42,17 @@ describe EventFilter, lib: true do it 'applies all filter' do events = EventFilter.new(EventFilter.all).apply_filter(Event.all) - expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event) + expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event) end it 'applies no filter' do events = EventFilter.new(nil).apply_filter(Event.all) - expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event) + expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event) end it 'applies unknown filter' do events = EventFilter.new('').apply_filter(Event.all) - expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event) + expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event) end end end diff --git a/spec/lib/gitlab/allowable_spec.rb b/spec/lib/gitlab/allowable_spec.rb new file mode 100644 index 00000000000..87733d53e92 --- /dev/null +++ b/spec/lib/gitlab/allowable_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::Allowable do + subject do + Class.new.include(described_class).new + end + + describe '#can?' do + let(:user) { create(:user) } + + context 'when user is allowed to do something' do + let(:project) { create(:empty_project, :public) } + + it 'reports correct ability to perform action' do + expect(subject.can?(user, :read_project, project)).to be true + end + end + + context 'when user is not allowed to do something' do + let(:project) { create(:empty_project, :private) } + + it 'reports correct ability to perform action' do + expect(subject.can?(user, :read_project, project)).to be false + end + end + end +end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 4aba783dc33..ba199917f5c 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -8,10 +8,14 @@ module Gitlab let(:html) { 'H<sub>2</sub>O' } context "without project" do + before do + allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults) + end + it "converts the input using Asciidoctor and default options" do expected_asciidoc_opts = { safe: :secure, - backend: :html5, + backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS } @@ -27,7 +31,7 @@ module Gitlab it "merges the options with default ones" do expected_asciidoc_opts = { safe: :safe, - backend: :html5, + backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo'] } diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index c9d64e99f88..f251c0dd25a 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -47,54 +47,76 @@ describe Gitlab::Auth, lib: true do project.create_drone_ci_service(active: true) project.drone_ci_service.update(token: 'token') - ip = 'ip' - - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'drone-ci-token') - expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'drone-ci-token') + expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)) end it 'recognizes master passwords' do user = create(:user, password: 'password') - ip = 'ip' - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username) - expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) + expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) end it 'recognizes user lfs tokens' do user = create(:user) - ip = 'ip' token = Gitlab::LfsToken.new(user).token - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username) - expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) + expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) end it 'recognizes deploy key lfs tokens' do key = create(:deploy_key) - ip = 'ip' token = Gitlab::LfsToken.new(key).token - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}") - expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") + expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) end - it 'recognizes OAuth tokens' do - user = create(:user) - application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id) - ip = 'ip' + context "while using OAuth tokens as passwords" do + it 'succeeds for OAuth tokens with the `api` scope' do + user = create(:user) + application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) + token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2') + expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) + end - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2') - expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) + it 'fails for OAuth tokens with other scopes' do + user = create(:user) + application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) + token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user") + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'oauth2') + expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) + end + end + + context "while using personal access tokens as passwords" do + it 'succeeds for personal access tokens with the `api` scope' do + user = create(:user) + personal_access_token = create(:personal_access_token, user: user, scopes: ['api']) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email) + expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities)) + end + + it 'fails for personal access tokens with other scopes' do + user = create(:user) + personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email) + expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) + end end it 'returns double nil for invalid credentials' do login = 'foo' - ip = 'ip' - expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login) - expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new) end end diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb new file mode 100644 index 00000000000..f84782ab440 --- /dev/null +++ b/spec/lib/gitlab/backup/manager_spec.rb @@ -0,0 +1,209 @@ +require 'spec_helper' + +describe Backup::Manager, lib: true do + include StubENV + + let(:progress) { StringIO.new } + + before do + allow(progress).to receive(:puts) + allow(progress).to receive(:print) + + allow_any_instance_of(String).to receive(:color) do |string, _color| + string + end + + @old_progress = $progress # rubocop:disable Style/GlobalVars + $progress = progress # rubocop:disable Style/GlobalVars + end + + after do + $progress = @old_progress # rubocop:disable Style/GlobalVars + end + + describe '#remove_old' do + let(:files) do + [ + '1451606400_2016_01_01_gitlab_backup.tar', + '1451520000_2015_12_31_gitlab_backup.tar', + '1450742400_2015_12_22_gitlab_backup.tar', + '1449878400_gitlab_backup.tar', + '1449014400_gitlab_backup.tar', + 'manual_gitlab_backup.tar' + ] + end + + before do + allow(Dir).to receive(:chdir).and_yield + allow(Dir).to receive(:glob).and_return(files) + allow(FileUtils).to receive(:rm) + allow(Time).to receive(:now).and_return(Time.utc(2016)) + end + + context 'when keep_time is zero' do + before do + allow(Gitlab.config.backup).to receive(:keep_time).and_return(0) + + subject.remove_old + end + + it 'removes no files' do + expect(FileUtils).not_to have_received(:rm) + end + + it 'prints a skipped message' do + expect(progress).to have_received(:puts).with('skipping') + end + end + + context 'when there are no files older than keep_time' do + before do + allow(Gitlab.config.backup).to receive(:keep_time).and_return(2592000) + + 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 keep_time is set to remove files' do + before do + allow(Gitlab.config.backup).to receive(:keep_time).and_return(1) + + subject.remove_old + end + + it 'removes matching files with a human-readable timestamp' do + expect(FileUtils).to have_received(:rm).with(files[1]) + expect(FileUtils).to have_received(:rm).with(files[2]) + end + + it 'removes matching files without a human-readable timestamp' do + expect(FileUtils).to have_received(:rm).with(files[3]) + expect(FileUtils).to have_received(:rm).with(files[4]) + end + + it 'does not remove files that are not old enough' do + expect(FileUtils).not_to have_received(:rm).with(files[0]) + end + + it 'does not remove non-matching files' do + expect(FileUtils).not_to have_received(:rm).with(files[5]) + end + + it 'prints a done message' do + expect(progress).to have_received(:puts).with('done. (4 removed)') + end + end + + context 'when removing a file fails' do + let(:file) { files[1] } + let(:message) { "Permission denied @ unlink_internal - #{file}" } + + before do + allow(Gitlab.config.backup).to receive(:keep_time).and_return(1) + allow(FileUtils).to receive(:rm).with(file).and_raise(Errno::EACCES, message) + + subject.remove_old + 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]) + end + + it 'sets the correct removed count' do + expect(progress).to have_received(:puts).with('done. (3 removed)') + end + + it 'prints the error from file that could not be removed' do + expect(progress).to have_received(:puts).with(a_string_matching(message)) + end + end + 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([]) + end + + it 'fails the operation and prints an error' do + expect { subject.unpack }.to raise_error SystemExit + expect(progress).to have_received(:puts) + .with(a_string_matching('No backups found')) + end + end + + context 'when there are two backup files in the directory and BACKUP variable is not set' do + before do + allow(Dir).to receive(:glob).and_return( + [ + '1451606400_2016_01_01_gitlab_backup.tar', + '1451520000_2015_12_31_gitlab_backup.tar', + ] + ) + end + + it 'fails the operation and prints an error' do + expect { subject.unpack }.to raise_error SystemExit + expect(progress).to have_received(:puts) + .with(a_string_matching('Found more than one backup')) + end + end + + context 'when BACKUP variable is set to a non-existing file' do + before do + allow(Dir).to receive(:glob).and_return( + [ + '1451606400_2016_01_01_gitlab_backup.tar' + ] + ) + allow(File).to receive(:exist?).and_return(false) + + stub_env('BACKUP', 'wrong') + end + + it 'fails the operation and prints an error' do + expect { subject.unpack }.to raise_error SystemExit + expect(File).to have_received(:exist?).with('wrong_gitlab_backup.tar') + expect(progress).to have_received(:puts) + .with(a_string_matching('The backup file wrong_gitlab_backup.tar does not exist')) + end + end + + context 'when BACKUP variable is set to a correct file' do + before do + allow(Dir).to receive(:glob).and_return( + [ + '1451606400_2016_01_01_gitlab_backup.tar' + ] + ) + allow(File).to receive(:exist?).and_return(true) + allow(Kernel).to receive(:system).and_return(true) + allow(YAML).to receive(:load_file).and_return(gitlab_version: Gitlab::VERSION) + + stub_env('BACKUP', '1451606400_2016_01_01') + end + + it 'unpacks the file' do + subject.unpack + + expect(Kernel).to have_received(:system) + .with("tar", "-xf", "1451606400_2016_01_01_gitlab_backup.tar") + expect(progress).to have_received(:puts).with(a_string_matching('done')) + end + end + end +end diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb index 38eebb2a176..70f03021d36 100644 --- a/spec/lib/gitlab/badge/build/status_spec.rb +++ b/spec/lib/gitlab/badge/build/status_spec.rb @@ -69,8 +69,8 @@ describe Gitlab::Badge::Build::Status do new_build.success! end - it 'reports the compound status' do - expect(badge.status).to eq 'failed' + it 'does not take outdated pipeline into account' do + expect(badge.status).to eq 'success' end end end diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb deleted file mode 100644 index 7543c29bcc4..00000000000 --- a/spec/lib/gitlab/bitbucket_import/client_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' - -describe Gitlab::BitbucketImport::Client, lib: true do - include ImportSpecHelper - - let(:token) { '123456' } - let(:secret) { 'secret' } - let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) } - - before do - stub_omniauth_provider('bitbucket') - end - - it 'all OAuth client options are symbols' do - client.consumer.options.keys.each do |key| - expect(key).to be_kind_of(Symbol) - end - end - - context 'issues' do - let(:per_page) { 50 } - let(:count) { 95 } - let(:sample_issues) do - issues = [] - - count.times do |i| - issues << { local_id: i } - end - - issues - end - let(:first_sample_data) { { count: count, issues: sample_issues[0..per_page - 1] } } - let(:second_sample_data) { { count: count, issues: sample_issues[per_page..count] } } - let(:project_id) { 'namespace/repo' } - - it 'retrieves issues over a number of pages' do - stub_request(:get, - "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=0"). - to_return(status: 200, - body: first_sample_data.to_json, - headers: {}) - - stub_request(:get, - "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=50"). - to_return(status: 200, - body: second_sample_data.to_json, - headers: {}) - - issues = client.issues(project_id) - expect(issues.count).to eq(95) - end - end - - context 'project import' do - it 'calls .from_project with no errors' do - project = create(:empty_project) - project.import_url = "ssh://git@bitbucket.org/test/test.git" - project.create_or_update_import_data(credentials: - { user: "git", - password: nil, - bb_session: { bitbucket_access_token: "test", - bitbucket_access_token_secret: "test" } }) - - expect { described_class.from_project(project) }.not_to raise_error - end - end -end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index aa00f32becb..72b1ba36b58 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -18,15 +18,21 @@ describe Gitlab::BitbucketImport::Importer, lib: true do "closed" # undocumented status ] end + let(:sample_issues_statuses) do issues = [] statuses.map.with_index do |status, index| issues << { - local_id: index, - status: status, + id: index, + state: status, title: "Issue #{index}", - content: "Some content to issue #{index}" + kind: 'bug', + content: { + raw: "Some content to issue #{index}", + markup: "markdown", + html: "Some content to issue #{index}" + } } end @@ -34,14 +40,16 @@ describe Gitlab::BitbucketImport::Importer, lib: true do end let(:project_identifier) { 'namespace/repo' } + let(:data) do { 'bb_session' => { - 'bitbucket_access_token' => "123456", - 'bitbucket_access_token_secret' => "secret" + 'bitbucket_token' => "123456", + 'bitbucket_refresh_token' => "secret" } } end + let(:project) do create( :project, @@ -49,42 +57,70 @@ describe Gitlab::BitbucketImport::Importer, lib: true do import_data: ProjectImportData.new(credentials: data) ) end + let(:importer) { Gitlab::BitbucketImport::Importer.new(project) } + let(:issues_statuses_sample_data) do { count: sample_issues_statuses.count, - issues: sample_issues_statuses + values: sample_issues_statuses } end context 'issues statuses' do before do + # HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this + Bitbucket::Representation::Issue.new({}) + stub_request( :get, - "https://bitbucket.org/api/1.0/repositories/#{project_identifier}" - ).to_return(status: 200, body: { has_issues: true }.to_json) + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}" + ).to_return(status: 200, + headers: { "Content-Type" => "application/json" }, + body: { has_issues: true, full_name: project_identifier }.to_json) stub_request( :get, - "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues?limit=50&sort=utc_created_on&start=0" - ).to_return(status: 200, body: issues_statuses_sample_data.to_json) + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues?pagelen=50&sort=created_on" + ).to_return(status: 200, + headers: { "Content-Type" => "application/json" }, + body: issues_statuses_sample_data.to_json) + + stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on"). + with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }). + to_return(status: 200, + body: "", + headers: {}) sample_issues_statuses.each_with_index do |issue, index| stub_request( :get, - "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues/#{issue[:local_id]}/comments" + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues/#{issue[:id]}/comments?pagelen=50&sort=created_on" ).to_return( status: 200, - body: [{ author_info: { username: "username" }, utc_created_on: index }].to_json + headers: { "Content-Type" => "application/json" }, + body: { author_info: { username: "username" }, utc_created_on: index }.to_json ) end + + stub_request( + :get, + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/pullrequests?pagelen=50&sort=created_on&state=ALL" + ).to_return(status: 200, + headers: { "Content-Type" => "application/json" }, + body: {}.to_json) end - it 'map statuses to open or closed' do + it 'maps statuses to open or closed' do importer.execute expect(project.issues.where(state: "closed").size).to eq(5) expect(project.issues.where(state: "opened").size).to eq(2) end + + it 'calls import_wiki' do + expect(importer).to receive(:import_wiki) + importer.execute + end end end diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb index e1c60e07b4d..773d0d4d288 100644 --- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb @@ -2,14 +2,19 @@ require 'spec_helper' describe Gitlab::BitbucketImport::ProjectCreator, lib: true do let(:user) { create(:user) } + let(:repo) do - { - name: 'Vim', - slug: 'vim', - is_private: true, - owner: "asd" - }.with_indifferent_access + double(name: 'Vim', + slug: 'vim', + description: 'Test repo', + is_private: true, + owner: "asd", + full_name: 'Vim repo', + visibility_level: Gitlab::VisibilityLevel::PRIVATE, + clone_url: 'ssh://git@bitbucket.org/asd/vim.git', + has_wiki?: false) end + let(:namespace){ create(:group, owner: user) } let(:token) { "asdasd12345" } let(:secret) { "sekrettt" } @@ -22,7 +27,7 @@ describe Gitlab::BitbucketImport::ProjectCreator, lib: true do it 'creates project' do allow_any_instance_of(Project).to receive(:add_import_job) - project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user, access_params) + project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, 'vim', namespace, user, access_params) project = project_creator.execute expect(project.import_url).to eq("ssh://git@bitbucket.org/asd/vim.git") diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb new file mode 100644 index 00000000000..a2d84977f58 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Command, service: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + describe '#execute' do + subject do + described_class.new(project, user, params).execute + end + + context 'when no command is available' do + let(:params) { { text: 'issue show 1' } } + let(:project) { create(:project, has_external_issue_tracker: true) } + + it 'displays 404 messages' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to start_with('404 not found') + end + end + + context 'when an unknown command is triggered' do + let(:params) { { command: '/gitlab', text: "unknown command 123" } } + + it 'displays the help message' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to start_with('Available commands') + expect(subject[:text]).to match('/gitlab issue show') + end + end + + context 'the user can not create an issue' do + let(:params) { { text: "issue create my new issue" } } + + it 'rejects the actions' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to start_with('Whoops! That action is not allowed') + end + end + + context 'issue is successfully created' do + let(:params) { { text: "issue create my new issue" } } + + before do + project.team << [user, :master] + end + + it 'presents the issue' do + expect(subject[:text]).to match("my new issue") + end + + it 'shows a link to the new issue' do + expect(subject[:text]).to match(/\/issues\/\d+/) + end + end + + context 'searching for an issue' do + let(:params) { { text: 'issue search find me' } } + let!(:issue) { create(:issue, project: project, title: 'find me') } + + before do + project.team << [user, :master] + end + + context 'a single issue is found' do + it 'presents the issue' do + expect(subject[:text]).to match(issue.title) + end + end + + context 'multiple issues found' do + let!(:issue2) { create(:issue, project: project, title: "someone find me") } + + it 'shows a link to the new issue' do + expect(subject[:text]).to match(issue.title) + expect(subject[:text]).to match(issue2.title) + end + end + end + + context 'when trying to do deployment' do + let(:params) { { text: 'deploy staging to production' } } + let!(:build) { create(:ci_build, project: project) } + let!(:staging) { create(:environment, name: 'staging', project: project) } + let!(:deployment) { create(:deployment, environment: staging, deployable: build) } + let!(:manual) do + create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production') + end + + context 'and user can not create deployment' do + it 'returns action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to start_with('Whoops! That action is not allowed') + end + end + + context 'and user does have deployment permission' do + before do + project.team << [user, :developer] + end + + it 'returns action' do + expect(subject[:text]).to include('Deployment from staging to production started.') + expect(subject[:response_type]).to be(:in_channel) + end + + context 'when duplicate action exists' do + let!(:manual2) do + create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production') + end + + it 'returns error' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to include('Too many actions defined') + end + end + end + end + end + + describe '#match_command' do + subject { described_class.new(project, user, params).match_command.first } + + context 'IssueShow is triggered' do + let(:params) { { text: 'issue show 123' } } + + it { is_expected.to eq(Gitlab::ChatCommands::IssueShow) } + end + + context 'IssueCreate is triggered' do + let(:params) { { text: 'issue create my title' } } + + it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) } + end + + context 'IssueSearch is triggered' do + let(:params) { { text: 'issue search my query' } } + + it { is_expected.to eq(Gitlab::ChatCommands::IssueSearch) } + end + end +end diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb new file mode 100644 index 00000000000..bd8099c92da --- /dev/null +++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Deploy, service: true do + describe '#execute' do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:regex_match) { described_class.match('deploy staging to production') } + + before do + project.team << [user, :master] + end + + subject do + described_class.new(project, user).execute(regex_match) + end + + context 'if no environment is defined' do + it 'returns nil' do + expect(subject).to be_nil + end + end + + context 'with environment' do + let!(:staging) { create(:environment, name: 'staging', project: project) } + let!(:build) { create(:ci_build, project: project) } + let!(:deployment) { create(:deployment, environment: staging, deployable: build) } + + context 'without actions' do + it 'returns nil' do + expect(subject).to be_nil + end + end + + context 'with action' do + let!(:manual1) do + create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production') + end + + it 'returns success result' do + expect(subject.type).to eq(:success) + expect(subject.message).to include('Deployment from staging to production started') + end + + context 'when duplicate action exists' do + let!(:manual2) do + create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production') + end + + it 'returns error' do + expect(subject.type).to eq(:error) + expect(subject.message).to include('Too many actions defined') + end + end + + context 'when teardown action exists' do + let!(:teardown) do + create(:ci_build, :manual, :teardown_environment, + project: project, pipeline: build.pipeline, + name: 'teardown', environment: 'production') + end + + it 'returns success result' do + expect(subject.type).to eq(:success) + expect(subject.message).to include('Deployment from staging to production started') + end + end + end + end + end + + describe 'self.match' do + it 'matches the environment' do + match = described_class.match('deploy staging to production') + + expect(match[:from]).to eq('staging') + expect(match[:to]).to eq('production') + end + end +end diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb new file mode 100644 index 00000000000..6c71e79ff6d --- /dev/null +++ b/spec/lib/gitlab/chat_commands/issue_create_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::IssueCreate, service: true do + describe '#execute' do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:regex_match) { described_class.match("issue create bird is the word") } + + before do + project.team << [user, :master] + end + + subject do + described_class.new(project, user).execute(regex_match) + end + + context 'without description' do + it 'creates the issue' do + expect { subject }.to change { project.issues.count }.by(1) + + expect(subject.title).to eq('bird is the word') + end + end + + context 'with description' do + let(:description) { "Surfin bird" } + let(:regex_match) { described_class.match("issue create bird is the word\n#{description}") } + + it 'creates the issue with description' do + subject + + expect(Issue.last.description).to eq(description) + end + end + + context "with more newlines between the title and the description" do + let(:description) { "Surfin bird" } + let(:regex_match) { described_class.match("issue create bird is the word\n\n#{description}\n") } + + it 'creates the issue' do + expect { subject }.to change { project.issues.count }.by(1) + end + end + end + + describe '.match' do + it 'matches the title without description' do + match = described_class.match("issue create my title") + + expect(match[:title]).to eq('my title') + expect(match[:description]).to eq("") + end + + it 'matches the title with description' do + match = described_class.match("issue create my title\n\ndescription") + + expect(match[:title]).to eq('my title') + expect(match[:description]).to eq('description') + end + + it 'matches the alias new' do + match = described_class.match("issue new my title") + + expect(match).not_to be_nil + expect(match[:title]).to eq('my title') + end + end +end diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb new file mode 100644 index 00000000000..24c06a967fa --- /dev/null +++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::IssueSearch, service: true do + describe '#execute' do + let!(:issue) { create(:issue, title: 'find me') } + let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') } + let(:project) { issue.project } + let(:user) { issue.author } + let(:regex_match) { described_class.match("issue search find") } + + subject do + described_class.new(project, user).execute(regex_match) + end + + context 'when the user has no access' do + it 'only returns the open issues' do + expect(subject).not_to include(confidential) + end + end + + context 'the user has access' do + before do + project.team << [user, :master] + end + + it 'returns all results' do + expect(subject).to include(confidential, issue) + end + end + + context 'without hits on the query' do + it 'returns an empty collection' do + expect(subject).to be_empty + end + end + end + + describe 'self.match' do + let(:query) { "my search keywords" } + it 'matches the query' do + match = described_class.match("issue search #{query}") + + expect(match[:query]).to eq(query) + end + end +end diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb new file mode 100644 index 00000000000..2eab73e49e5 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::IssueShow, service: true do + describe '#execute' do + let(:issue) { create(:issue) } + let(:project) { issue.project } + let(:user) { issue.author } + let(:regex_match) { described_class.match("issue show #{issue.iid}") } + + before do + project.team << [user, :master] + end + + subject do + described_class.new(project, user).execute(regex_match) + end + + context 'the issue exists' do + it 'returns the issue' do + expect(subject.iid).to be issue.iid + end + + context 'when its reference is given' do + let(:regex_match) { described_class.match("issue show #{issue.to_reference}") } + + it 'shows the issue' do + expect(subject.iid).to be issue.iid + end + end + end + + context 'the issue does not exist' do + let(:regex_match) { described_class.match("issue show 2343242") } + + it "returns nil" do + expect(subject).to be_nil + end + end + end + + describe 'self.match' do + it 'matches the iid' do + match = described_class.match("issue show 123") + + expect(match[:iid]).to eq("123") + end + end +end diff --git a/spec/lib/gitlab/chat_name_token_spec.rb b/spec/lib/gitlab/chat_name_token_spec.rb new file mode 100644 index 00000000000..8c1e6efa9db --- /dev/null +++ b/spec/lib/gitlab/chat_name_token_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::ChatNameToken, lib: true do + context 'when using unknown token' do + let(:token) { } + + subject { described_class.new(token).get } + + it 'returns empty data' do + is_expected.to be_nil + end + end + + context 'when storing data' do + let(:data) { { key: 'value' } } + + subject { described_class.new(@token) } + + before do + @token = described_class.new.store!(data) + end + + it 'returns stored data' do + expect(subject.get).to eq(data) + end + + context 'and after deleting them' do + before do + subject.delete + end + + it 'data are removed' do + expect(subject.get).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 39069b49978..98effecdbbc 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -56,7 +56,6 @@ describe Gitlab::Checks::ChangeAccess, lib: true do it 'returns an error if the user is not allowed to do forced pushes to protected branches' do expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) - expect(user_access).to receive(:can_do_action?).with(:force_push_code_to_protected_branches).and_return(false) expect(subject.status).to be(false) expect(subject.message).to eq('You are not allowed to force push code to a protected branch on this project.') @@ -88,8 +87,6 @@ describe Gitlab::Checks::ChangeAccess, lib: true do end it 'returns an error if the user is not allowed to delete protected branches' do - expect(user_access).to receive(:can_do_action?).with(:remove_protected_branches).and_return(false) - expect(subject.status).to be(false) expect(subject.message).to eq('You are not allowed to delete protected branches from this project.') end diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb new file mode 100644 index 00000000000..f6288011494 --- /dev/null +++ b/spec/lib/gitlab/checks/force_push_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Gitlab::Checks::ChangeAccess, lib: true do + let(:project) { create(:project) } + + context "exit code checking" do + it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do + allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0]) + + expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error + end + + it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do + allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) + + expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError) + end + end +end diff --git a/spec/lib/gitlab/ci/build/credentials/factory_spec.rb b/spec/lib/gitlab/ci/build/credentials/factory_spec.rb new file mode 100644 index 00000000000..10b4b7a8826 --- /dev/null +++ b/spec/lib/gitlab/ci/build/credentials/factory_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Credentials::Factory do + let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) } + + subject { Gitlab::Ci::Build::Credentials::Factory.new(build).create! } + + class TestProvider + def initialize(build); end + end + + before do + allow_any_instance_of(Gitlab::Ci::Build::Credentials::Factory).to receive(:providers).and_return([TestProvider]) + end + + context 'when provider is valid' do + before do + allow_any_instance_of(TestProvider).to receive(:valid?).and_return(true) + end + + it 'generates an array of credentials objects' do + is_expected.to be_kind_of(Array) + is_expected.not_to be_empty + expect(subject.first).to be_kind_of(TestProvider) + end + end + + context 'when provider is not valid' do + before do + allow_any_instance_of(TestProvider).to receive(:valid?).and_return(false) + end + + it 'generates an array without specific credential object' do + is_expected.to be_kind_of(Array) + is_expected.to be_empty + end + end +end diff --git a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry_spec.rb new file mode 100644 index 00000000000..84e44dd53e2 --- /dev/null +++ b/spec/lib/gitlab/ci/build/credentials/registry_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Credentials::Registry do + let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) } + let(:registry_url) { 'registry.example.com:5005' } + + subject { Gitlab::Ci::Build::Credentials::Registry.new(build) } + + before do + stub_container_registry_config(host_port: registry_url) + end + + it 'contains valid DockerRegistry credentials' do + expect(subject).to be_kind_of(Gitlab::Ci::Build::Credentials::Registry) + + expect(subject.username).to eq 'gitlab-ci-token' + expect(subject.password).to eq build.token + expect(subject.url).to eq registry_url + expect(subject.type).to eq 'registry' + end + + describe '.valid?' do + subject { Gitlab::Ci::Build::Credentials::Registry.new(build).valid? } + + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it { is_expected.to be_truthy } + end + + context 'when registry is disabled' do + before do + stub_container_registry_config(enabled: false) + end + + it { is_expected.to be_falsey } + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb index c09a0a9c793..5c31423fdee 100644 --- a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Artifacts do +describe Gitlab::Ci::Config::Entry::Artifacts do let(:entry) { described_class.new(config) } describe 'validation' do diff --git a/spec/lib/gitlab/ci/config/node/attributable_spec.rb b/spec/lib/gitlab/ci/config/entry/attributable_spec.rb index 24d9daafd88..fde03c51e2c 100644 --- a/spec/lib/gitlab/ci/config/node/attributable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/attributable_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Attributable do +describe Gitlab::Ci::Config::Entry::Attributable do let(:node) { Class.new } let(:instance) { node.new } diff --git a/spec/lib/gitlab/ci/config/node/boolean_spec.rb b/spec/lib/gitlab/ci/config/entry/boolean_spec.rb index deafa8bf8a7..5f067cad93c 100644 --- a/spec/lib/gitlab/ci/config/node/boolean_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/boolean_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Boolean do +describe Gitlab::Ci::Config::Entry::Boolean do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/node/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb index e251210949c..70a327c5183 100644 --- a/spec/lib/gitlab/ci/config/node/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Cache do +describe Gitlab::Ci::Config::Entry::Cache do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/node/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb index e373c40706f..b8b0825a1c7 100644 --- a/spec/lib/gitlab/ci/config/node/commands_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Commands do +describe Gitlab::Ci::Config::Entry::Commands do let(:entry) { described_class.new(config) } context 'when entry config value is an array' do diff --git a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb b/spec/lib/gitlab/ci/config/entry/configurable_spec.rb new file mode 100644 index 00000000000..ae7e628b5b5 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/configurable_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Configurable do + let(:entry) { Class.new } + + before do + entry.include(described_class) + end + + describe 'validations' do + let(:validator) { entry.validator.new(instance) } + + before do + entry.class_eval do + attr_reader :config + + def initialize(config) + @config = config + end + end + + validator.validate + end + + context 'when entry validator is invalid' do + let(:instance) { entry.new('ls') } + + it 'returns invalid validator' do + expect(validator).to be_invalid + end + end + + context 'when entry instance is valid' do + let(:instance) { entry.new(key: 'value') } + + it 'returns valid validator' do + expect(validator).to be_valid + end + end + end + + describe 'configured entries' do + before do + entry.class_eval do + entry :object, Object, description: 'test object' + end + end + + describe '.nodes' do + it 'has valid nodes' do + expect(entry.nodes).to include :object + end + + it 'creates a node factory' do + expect(entry.nodes[:object]) + .to be_an_instance_of Gitlab::Ci::Config::Entry::Factory + end + + it 'returns a duplicated factory object' do + first_factory = entry.nodes[:object] + second_factory = entry.nodes[:object] + + expect(first_factory).not_to be_equal(second_factory) + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index df925ff1afd..2adbed2154f 100644 --- a/spec/lib/gitlab/ci/config/node/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Environment do +describe Gitlab::Ci::Config::Entry::Environment do let(:entry) { described_class.new(config) } before { entry.compose! } @@ -196,22 +196,5 @@ describe Gitlab::Ci::Config::Node::Environment do end end end - - context 'when invalid URL is used' do - let(:config) { { name: 'test', url: 'invalid-example.gitlab.com' } } - - describe '#valid?' do - it 'is not valid' do - expect(entry).not_to be_valid - end - end - - describe '#errors?' do - it 'contains error about invalid URL' do - expect(entry.errors) - .to include "environment url must be a valid url" - end - end - end end end diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/entry/factory_spec.rb index a699089c563..00dad5d9591 100644 --- a/spec/lib/gitlab/ci/config/node/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/factory_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Factory do +describe Gitlab::Ci::Config::Entry::Factory do describe '#create!' do - let(:factory) { described_class.new(node) } - let(:node) { Gitlab::Ci::Config::Node::Script } + let(:factory) { described_class.new(entry) } + let(:entry) { Gitlab::Ci::Config::Entry::Script } context 'when setting a concrete value' do it 'creates entry with valid value' do @@ -54,7 +54,7 @@ describe Gitlab::Ci::Config::Node::Factory do context 'when not setting a value' do it 'raises error' do expect { factory.create! }.to raise_error( - Gitlab::Ci::Config::Node::Factory::InvalidFactory + Gitlab::Ci::Config::Entry::Factory::InvalidFactory ) end end @@ -66,12 +66,12 @@ describe Gitlab::Ci::Config::Node::Factory do .create! expect(entry) - .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified + .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified end end context 'when passing metadata' do - let(:node) { spy('node') } + let(:entry) { spy('entry') } it 'passes metadata as a parameter' do factory @@ -79,7 +79,7 @@ describe Gitlab::Ci::Config::Node::Factory do .metadata(some: 'hash') .create! - expect(node).to have_received(:new) + expect(entry).to have_received(:new) .with('some value', { some: 'hash' }) end end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 12232ff7e2f..e64c8d46bd8 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Global do +describe Gitlab::Ci::Config::Entry::Global do let(:global) { described_class.new(hash) } describe '.nodes' do @@ -13,7 +13,7 @@ describe Gitlab::Ci::Config::Node::Global do end end - context 'when hash is valid' do + context 'when configuration is valid' do context 'when some entries defined' do let(:hash) do { before_script: ['ls', 'pwd'], @@ -40,9 +40,9 @@ describe Gitlab::Ci::Config::Node::Global do it 'creates node object using valid class' do expect(global.descendants.first) - .to be_an_instance_of Gitlab::Ci::Config::Node::Script + .to be_an_instance_of Gitlab::Ci::Config::Entry::Script expect(global.descendants.second) - .to be_an_instance_of Gitlab::Ci::Config::Node::Image + .to be_an_instance_of Gitlab::Ci::Config::Entry::Image end it 'sets correct description for nodes' do @@ -60,9 +60,9 @@ describe Gitlab::Ci::Config::Node::Global do end context 'when not composed' do - describe '#before_script' do + describe '#before_script_value' do it 'returns nil' do - expect(global.before_script).to be nil + expect(global.before_script_value).to be nil end end @@ -82,40 +82,40 @@ describe Gitlab::Ci::Config::Node::Global do end end - describe '#before_script' do + describe '#before_script_value' do it 'returns correct script' do - expect(global.before_script).to eq ['ls', 'pwd'] + expect(global.before_script_value).to eq ['ls', 'pwd'] end end - describe '#image' do + describe '#image_value' do it 'returns valid image' do - expect(global.image).to eq 'ruby:2.2' + expect(global.image_value).to eq 'ruby:2.2' end end - describe '#services' do + describe '#services_value' do it 'returns array of services' do - expect(global.services).to eq ['postgres:9.1', 'mysql:5.5'] + expect(global.services_value).to eq ['postgres:9.1', 'mysql:5.5'] end end - describe '#after_script' do + describe '#after_script_value' do it 'returns after script' do - expect(global.after_script).to eq ['make clean'] + expect(global.after_script_value).to eq ['make clean'] end end - describe '#variables' do + describe '#variables_value' do it 'returns variables' do - expect(global.variables).to eq(VAR: 'value') + expect(global.variables_value).to eq(VAR: 'value') end end - describe '#stages' do + describe '#stages_value' do context 'when stages key defined' do it 'returns array of stages' do - expect(global.stages).to eq %w[build pages] + expect(global.stages_value).to eq %w[build pages] end end @@ -126,21 +126,21 @@ describe Gitlab::Ci::Config::Node::Global do end it 'returns array of types as stages' do - expect(global.stages).to eq %w[test deploy] + expect(global.stages_value).to eq %w[test deploy] end end end - describe '#cache' do + describe '#cache_value' do it 'returns cache configuration' do - expect(global.cache) + expect(global.cache_value) .to eq(key: 'k', untracked: true, paths: ['public/']) end end - describe '#jobs' do + describe '#jobs_value' do it 'returns jobs configuration' do - expect(global.jobs).to eq( + expect(global.jobs_value).to eq( rspec: { name: :rspec, script: %w[rspec ls], before_script: ['ls', 'pwd'], @@ -181,25 +181,25 @@ describe Gitlab::Ci::Config::Node::Global do it 'contains unspecified nodes' do expect(global.descendants.first) - .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified + .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified end end - describe '#variables' do + describe '#variables_value' do it 'returns default value for variables' do - expect(global.variables).to eq({}) + expect(global.variables_value).to eq({}) end end - describe '#stages' do + describe '#stages_value' do it 'returns an array of default stages' do - expect(global.stages).to eq %w[build test deploy] + expect(global.stages_value).to eq %w[build test deploy] end end - describe '#cache' do + describe '#cache_value' do it 'returns correct cache definition' do - expect(global.cache).to eq(key: 'a') + expect(global.cache_value).to eq(key: 'a') end end end @@ -217,37 +217,52 @@ describe Gitlab::Ci::Config::Node::Global do { variables: nil, rspec: { script: 'rspec' } } end - describe '#variables' do + describe '#variables_value' do it 'undefined entry returns a default value' do - expect(global.variables).to eq({}) + expect(global.variables_value).to eq({}) end end end end - context 'when hash is not valid' do + context 'when configuration is not valid' do before { global.compose! } - let(:hash) do - { before_script: 'ls' } - end + context 'when before script is not an array' do + let(:hash) do + { before_script: 'ls' } + end - describe '#valid?' do - it 'is not valid' do - expect(global).not_to be_valid + describe '#valid?' do + it 'is not valid' do + expect(global).not_to be_valid + end end - end - describe '#errors' do - it 'reports errors from child nodes' do - expect(global.errors) - .to include 'before_script config should be an array of strings' + describe '#errors' do + it 'reports errors from child nodes' do + expect(global.errors) + .to include 'before_script config should be an array of strings' + end + end + + describe '#before_script_value' do + it 'returns nil' do + expect(global.before_script_value).to be_nil + end end end - describe '#before_script' do - it 'returns nil' do - expect(global.before_script).to be_nil + context 'when job does not have commands' do + let(:hash) do + { before_script: ['echo 123'], rspec: { stage: 'test' } } + end + + describe '#errors' do + it 'reports errors about missing script' do + expect(global.errors) + .to include "jobs:rspec script can't be blank" + end end end end @@ -281,15 +296,15 @@ describe Gitlab::Ci::Config::Node::Global do { cache: { key: 'a' }, rspec: { script: 'ls' } } end - context 'when node exists' do + context 'when entry exists' do it 'returns correct entry' do expect(global[:cache]) - .to be_an_instance_of Gitlab::Ci::Config::Node::Cache + .to be_an_instance_of Gitlab::Ci::Config::Entry::Cache expect(global[:jobs][:rspec][:script].value).to eq ['ls'] end end - context 'when node does not exist' do + context 'when entry does not exist' do it 'always return unspecified node' do expect(global[:some][:unknown][:node]) .not_to be_specified diff --git a/spec/lib/gitlab/ci/config/node/hidden_spec.rb b/spec/lib/gitlab/ci/config/entry/hidden_spec.rb index 61e2a554419..459362761e6 100644 --- a/spec/lib/gitlab/ci/config/node/hidden_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/hidden_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Hidden do +describe Gitlab::Ci::Config::Entry::Hidden do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/node/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index d11bb39f328..3c99cb0a1ee 100644 --- a/spec/lib/gitlab/ci/config/node/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Image do +describe Gitlab::Ci::Config::Entry::Image do let(:entry) { described_class.new(config) } describe 'validation' do diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 91f676dae03..fc9b8b86dc4 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Job do +describe Gitlab::Ci::Config::Entry::Job do let(:entry) { described_class.new(config, name: :rspec) } describe 'validations' do @@ -19,8 +19,7 @@ describe Gitlab::Ci::Config::Node::Job do let(:entry) { described_class.new(config, name: ''.to_sym) } it 'reports error' do - expect(entry.errors) - .to include "job name can't be blank" + expect(entry.errors).to include "job name can't be blank" end end end @@ -56,6 +55,15 @@ describe Gitlab::Ci::Config::Node::Job do end end end + + context 'when script is not provided' do + let(:config) { { stage: 'test' } } + + it 'returns error about missing script entry' do + expect(entry).not_to be_valid + expect(entry.errors).to include "job script can't be blank" + end + end end end @@ -78,7 +86,7 @@ describe Gitlab::Ci::Config::Node::Job do before { entry.compose!(deps) } let(:config) do - { image: 'some_image', cache: { key: 'test' } } + { script: 'rspec', image: 'some_image', cache: { key: 'test' } } end it 'overrides global config' do diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb index 929809339ef..aaebf783962 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Jobs do +describe Gitlab::Ci::Config::Entry::Jobs do let(:entry) { described_class.new(config) } describe 'validations' do @@ -74,9 +74,9 @@ describe Gitlab::Ci::Config::Node::Jobs do it 'creates valid descendant nodes' do expect(entry.descendants.count).to eq 3 expect(entry.descendants.first(2)) - .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job)) + .to all(be_an_instance_of(Gitlab::Ci::Config::Entry::Job)) expect(entry.descendants.last) - .to be_an_instance_of(Gitlab::Ci::Config::Node::Hidden) + .to be_an_instance_of(Gitlab::Ci::Config::Entry::Hidden) end end diff --git a/spec/lib/gitlab/ci/config/node/key_spec.rb b/spec/lib/gitlab/ci/config/entry/key_spec.rb index 8cda43173fe..a55e5b4b8ac 100644 --- a/spec/lib/gitlab/ci/config/node/key_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/key_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Key do +describe Gitlab::Ci::Config::Entry::Key do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/node/paths_spec.rb b/spec/lib/gitlab/ci/config/entry/paths_spec.rb index 6fd744b3975..e60c9aaf661 100644 --- a/spec/lib/gitlab/ci/config/node/paths_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/paths_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Paths do +describe Gitlab::Ci::Config::Entry::Paths do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/entry/script_spec.rb index 219a7e981d3..aa99cee2690 100644 --- a/spec/lib/gitlab/ci/config/node/script_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/script_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Script do +describe Gitlab::Ci::Config::Entry::Script do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/node/services_spec.rb b/spec/lib/gitlab/ci/config/entry/services_spec.rb index be0fe46befd..66fad3b6b16 100644 --- a/spec/lib/gitlab/ci/config/node/services_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/services_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Services do +describe Gitlab::Ci::Config::Entry::Services do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/node/stage_spec.rb b/spec/lib/gitlab/ci/config/entry/stage_spec.rb index fb9ec70762a..70c8a0a355a 100644 --- a/spec/lib/gitlab/ci/config/node/stage_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/stage_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Stage do +describe Gitlab::Ci::Config::Entry::Stage do let(:stage) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/node/stages_spec.rb b/spec/lib/gitlab/ci/config/entry/stages_spec.rb index 1a3818d8997..182c8d867c7 100644 --- a/spec/lib/gitlab/ci/config/node/stages_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/stages_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Stages do +describe Gitlab::Ci::Config::Entry::Stages do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/node/trigger_spec.rb b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb index a4a3e36754e..e4ee44f1274 100644 --- a/spec/lib/gitlab/ci/config/node/trigger_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Trigger do +describe Gitlab::Ci::Config::Entry::Trigger do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/entry/undefined_spec.rb index 6bde8602963..fdf48d84192 100644 --- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/undefined_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Undefined do +describe Gitlab::Ci::Config::Entry::Undefined do let(:entry) { described_class.new } describe '#leaf?' do diff --git a/spec/lib/gitlab/ci/config/node/unspecified_spec.rb b/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb index ba3ceef24ce..66f88fa35b6 100644 --- a/spec/lib/gitlab/ci/config/node/unspecified_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Unspecified do +describe Gitlab::Ci::Config::Entry::Unspecified do let(:unspecified) { described_class.new(entry) } let(:entry) { spy('Entry') } diff --git a/spec/lib/gitlab/ci/config/node/validatable_spec.rb b/spec/lib/gitlab/ci/config/entry/validatable_spec.rb index 64b77fd6e03..d1856801827 100644 --- a/spec/lib/gitlab/ci/config/node/validatable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/validatable_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Validatable do - let(:node) { Class.new } +describe Gitlab::Ci::Config::Entry::Validatable do + let(:entry) { Class.new } before do - node.include(described_class) + entry.include(described_class) end describe '.validator' do before do - node.class_eval do + entry.class_eval do attr_accessor :test_attribute validations do @@ -19,34 +19,34 @@ describe Gitlab::Ci::Config::Node::Validatable do end it 'returns validator' do - expect(node.validator.superclass) - .to be Gitlab::Ci::Config::Node::Validator + expect(entry.validator.superclass) + .to be Gitlab::Ci::Config::Entry::Validator end it 'returns only one validator to mitigate leaks' do - expect { node.validator }.not_to change { node.validator } + expect { entry.validator }.not_to change { entry.validator } end - context 'when validating node instance' do - let(:node_instance) { node.new } + context 'when validating entry instance' do + let(:entry_instance) { entry.new } context 'when attribute is valid' do before do - node_instance.test_attribute = 'valid' + entry_instance.test_attribute = 'valid' end it 'instance of validator is valid' do - expect(node.validator.new(node_instance)).to be_valid + expect(entry.validator.new(entry_instance)).to be_valid end end context 'when attribute is not valid' do before do - node_instance.test_attribute = nil + entry_instance.test_attribute = nil end it 'instance of validator is invalid' do - expect(node.validator.new(node_instance)).to be_invalid + expect(entry.validator.new(entry_instance)).to be_invalid end end end diff --git a/spec/lib/gitlab/ci/config/node/validator_spec.rb b/spec/lib/gitlab/ci/config/entry/validator_spec.rb index 090fd63b844..ad7e6f07d3c 100644 --- a/spec/lib/gitlab/ci/config/node/validator_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/validator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Validator do +describe Gitlab::Ci::Config::Entry::Validator do let(:validator) { Class.new(described_class) } let(:validator_instance) { validator.new(node) } let(:node) { spy('node') } diff --git a/spec/lib/gitlab/ci/config/node/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb index 4b6d971ec71..58327d08904 100644 --- a/spec/lib/gitlab/ci/config/node/variables_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::Variables do +describe Gitlab::Ci::Config::Entry::Variables do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/node/configurable_spec.rb b/spec/lib/gitlab/ci/config/node/configurable_spec.rb deleted file mode 100644 index c468ecf957b..00000000000 --- a/spec/lib/gitlab/ci/config/node/configurable_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Config::Node::Configurable do - let(:node) { Class.new } - - before do - node.include(described_class) - end - - describe 'validations' do - let(:validator) { node.validator.new(instance) } - - before do - node.class_eval do - attr_reader :config - - def initialize(config) - @config = config - end - end - - validator.validate - end - - context 'when node validator is invalid' do - let(:instance) { node.new('ls') } - - it 'returns invalid validator' do - expect(validator).to be_invalid - end - end - - context 'when node instance is valid' do - let(:instance) { node.new(key: 'value') } - - it 'returns valid validator' do - expect(validator).to be_valid - end - end - end - - describe 'configured nodes' do - before do - node.class_eval do - node :object, Object, description: 'test object' - end - end - - describe '.nodes' do - it 'has valid nodes' do - expect(node.nodes).to include :object - end - - it 'creates a node factory' do - expect(node.nodes[:object]) - .to be_an_instance_of Gitlab::Ci::Config::Node::Factory - end - - it 'returns a duplicated factory object' do - first_factory = node.nodes[:object] - second_factory = node.nodes[:object] - - expect(first_factory).not_to be_equal(second_factory) - end - end - end -end diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb new file mode 100644 index 00000000000..b3c07347de1 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Cancelable do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#text' do + it 'does not override status text' do + expect(status).to receive(:text) + + subject.text + end + end + + describe '#icon' do + it 'does not override status icon' do + expect(status).to receive(:icon) + + subject.icon + end + end + + describe '#label' do + it 'does not override status label' do + expect(status).to receive(:label) + + subject.label + end + end + + describe '#group' do + it 'does not override status group' do + expect(status).to receive(:group) + + subject.group + end + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/cancel" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'ban' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Cancel' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is cancelable' do + let(:build) do + create(:ci_build, :running) + end + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not cancelable' do + let(:build) { create(:ci_build, :success) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb new file mode 100644 index 00000000000..40b96b1807b --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/common_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Common do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:project) { build.project } + + subject do + Gitlab::Ci::Status::Core + .new(build, user) + .extend(described_class) + end + + describe '#has_action?' do + it { is_expected.not_to have_action } + end + + describe '#has_details?' do + context 'when user has access to read build' do + before { project.team << [user, :developer] } + + it { is_expected.to have_details } + end + + context 'when user does not have access to read build' do + before { project.update(public_builds: false) } + + it { is_expected.not_to have_details } + end + end + + describe '#details_path' do + it 'links to the build details page' do + expect(subject.details_path).to include "builds/#{build.id}" + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb new file mode 100644 index 00000000000..0c40fca0c1a --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -0,0 +1,242 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Factory do + let(:user) { create(:user) } + let(:project) { build.project } + let(:status) { factory.fabricate! } + let(:factory) { described_class.new(build, user) } + + before { project.team << [user, :developer] } + + context 'when build is successful' do + let(:build) { create(:ci_build, :success) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Success + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Retryable] + end + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'passed' + expect(status.icon).to eq 'icon_status_success' + expect(status.label).to eq 'passed' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is failed' do + context 'when build is not allowed to fail' do + let(:build) { create(:ci_build, :failed) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Retryable] + end + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'failed' + expect(status.icon).to eq 'icon_status_failed' + expect(status.label).to eq 'failed' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is allowed to fail' do + let(:build) { create(:ci_build, :failed, :allowed_to_fail) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Retryable, + Gitlab::Ci::Status::Build::FailedAllowed] + end + + it 'fabricates a failed but allowed build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::FailedAllowed + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'failed' + expect(status.icon).to eq 'icon_status_warning' + expect(status.label).to eq 'failed (allowed to fail)' + expect(status).to have_details + expect(status).to have_action + expect(status.action_title).to include 'Retry' + expect(status.action_path).to include 'retry' + end + end + end + + context 'when build is a canceled' do + let(:build) { create(:ci_build, :canceled) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Canceled + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Retryable] + end + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'canceled' + expect(status.icon).to eq 'icon_status_canceled' + expect(status.label).to eq 'canceled' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is running' do + let(:build) { create(:ci_build, :running) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Running + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Cancelable] + end + + it 'fabricates a canceable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'running' + expect(status.icon).to eq 'icon_status_running' + expect(status.label).to eq 'running' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is pending' do + let(:build) { create(:ci_build, :pending) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Pending + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Cancelable] + end + + it 'fabricates a cancelable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'pending' + expect(status.icon).to eq 'icon_status_pending' + expect(status.label).to eq 'pending' + expect(status).to have_details + expect(status).to have_action + end + end + + context 'when build is skipped' do + let(:build) { create(:ci_build, :skipped) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped + end + + it 'does not match extended statuses' do + expect(factory.extended_statuses).to be_empty + end + + it 'fabricates a core skipped status' do + expect(status).to be_a Gitlab::Ci::Status::Skipped + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'skipped' + expect(status.icon).to eq 'icon_status_skipped' + expect(status.label).to eq 'skipped' + expect(status).to have_details + expect(status).not_to have_action + end + end + + context 'when build is a manual action' do + context 'when build is a play action' do + let(:build) { create(:ci_build, :playable) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Play] + end + + it 'fabricates a core skipped status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Play + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'manual' + expect(status.icon).to eq 'icon_status_manual' + expect(status.label).to eq 'manual play action' + expect(status).to have_details + expect(status).to have_action + expect(status.action_path).to include 'play' + end + end + + context 'when build is an environment stop action' do + let(:build) { create(:ci_build, :playable, :teardown_environment) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Stop] + end + + it 'fabricates a core skipped status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Stop + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'manual' + expect(status.icon).to eq 'icon_status_manual' + expect(status.label).to eq 'manual stop action' + expect(status).to have_details + expect(status).to have_action + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb new file mode 100644 index 00000000000..20f71459738 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::FailedAllowed do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#text' do + it 'does not override status text' do + expect(status).to receive(:text) + + subject.text + end + end + + describe '#icon' do + it 'returns a warning icon' do + expect(subject.icon).to eq 'icon_status_warning' + end + end + + describe '#label' do + it 'returns information about failed but allowed to fail status' do + expect(subject.label).to eq 'failed (allowed to fail)' + end + end + + describe '#group' do + it 'returns status failed with warnings status group' do + expect(subject.group).to eq 'failed_with_warnings' + end + end + + describe 'action details' do + describe '#has_action?' do + it 'does not decorate action details' do + expect(status).to receive(:has_action?) + + subject.has_action? + end + end + + describe '#action_path' do + it 'does not decorate action path' do + expect(status).to receive(:action_path) + + subject.action_path + end + end + + describe '#action_icon' do + it 'does not decorate action icon' do + expect(status).to receive(:action_icon) + + subject.action_icon + end + end + + describe '#action_title' do + it 'does not decorate action title' do + expect(status).to receive(:action_title) + + subject.action_title + end + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is failed' do + context 'when build is allowed to fail' do + let(:build) { create(:ci_build, :failed, :allowed_to_fail) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not allowed to fail' do + let(:build) { create(:ci_build, :failed) } + + it 'is not a correct match' do + expect(subject).not_to be true + end + end + end + + context 'when build did not fail' do + context 'when build is allowed to fail' do + let(:build) { create(:ci_build, :success, :allowed_to_fail) } + + it 'is not a correct match' do + expect(subject).not_to be true + end + end + + context 'when build is not allowed to fail' do + let(:build) { create(:ci_build, :success) } + + it 'is not a correct match' do + expect(subject).not_to be true + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb new file mode 100644 index 00000000000..f1b50a59ce6 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Play do + let(:status) { double('core') } + let(:user) { double('user') } + + subject { described_class.new(status) } + + describe '#text' do + it { expect(subject.text).to eq 'manual' } + end + + describe '#label' do + it { expect(subject.label).to eq 'manual play action' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_manual' } + end + + describe '#group' do + it { expect(subject.group).to eq 'manual' } + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/play" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'play' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Play' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is playable' do + context 'when build stops an environment' do + let(:build) do + create(:ci_build, :playable, :teardown_environment) + end + + it 'does not match' do + expect(subject).to be false + end + end + + context 'when build does not stop an environment' do + let(:build) { create(:ci_build, :playable) } + + it 'is a correct match' do + expect(subject).to be true + end + end + end + + context 'when build is not playable' do + let(:build) { create(:ci_build) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb new file mode 100644 index 00000000000..62036f8ec5d --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Retryable do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#text' do + it 'does not override status text' do + expect(status).to receive(:text) + + subject.text + end + end + + describe '#icon' do + it 'does not override status icon' do + expect(status).to receive(:icon) + + subject.icon + end + end + + describe '#label' do + it 'does not override status label' do + expect(status).to receive(:label) + + subject.label + end + end + + describe '#group' do + it 'does not override status group' do + expect(status).to receive(:group) + + subject.group + end + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/retry" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'refresh' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Retry' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is retryable' do + let(:build) do + create(:ci_build, :success) + end + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not retryable' do + let(:build) { create(:ci_build, :running) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb new file mode 100644 index 00000000000..597e02e86e4 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Stop do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#text' do + it { expect(subject.text).to eq 'manual' } + end + + describe '#label' do + it { expect(subject.label).to eq 'manual stop action' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_manual' } + end + + describe '#group' do + it { expect(subject.group).to eq 'manual' } + end + + describe 'action details' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + describe '#has_action?' do + context 'when user is allowed to update build' do + before { build.project.team << [user, :developer] } + + it { is_expected.to have_action } + end + + context 'when user is not allowed to update build' do + it { is_expected.not_to have_action } + end + end + + describe '#action_path' do + it { expect(subject.action_path).to include "#{build.id}/play" } + end + + describe '#action_icon' do + it { expect(subject.action_icon).to eq 'stop' } + end + + describe '#action_title' do + it { expect(subject.action_title).to eq 'Stop' } + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is playable' do + context 'when build stops an environment' do + let(:build) do + create(:ci_build, :playable, :teardown_environment) + end + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build does not stop an environment' do + let(:build) { create(:ci_build, :playable) } + + it 'does not match' do + expect(subject).to be false + end + end + end + + context 'when build is not playable' do + let(:build) { create(:ci_build) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb new file mode 100644 index 00000000000..38412fe2e4f --- /dev/null +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Canceled do + subject do + described_class.new(double('subject'), double('user')) + end + + describe '#text' do + it { expect(subject.label).to eq 'canceled' } + end + + describe '#label' do + it { expect(subject.label).to eq 'canceled' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_canceled' } + end + + describe '#group' do + it { expect(subject.group).to eq 'canceled' } + end +end diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb new file mode 100644 index 00000000000..6d847484693 --- /dev/null +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Created do + subject do + described_class.new(double('subject'), double('user')) + end + + describe '#text' do + it { expect(subject.label).to eq 'created' } + end + + describe '#label' do + it { expect(subject.label).to eq 'created' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_created' } + end + + describe '#group' do + it { expect(subject.group).to eq 'created' } + end +end diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb new file mode 100644 index 00000000000..c2d74ca5cde --- /dev/null +++ b/spec/lib/gitlab/ci/status/extended_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Extended do + subject do + Class.new.include(described_class) + end + + it 'requires subclass to implement matcher' do + expect { subject.matches?(double, double) } + .to raise_error(NotImplementedError) + end +end diff --git a/spec/lib/gitlab/ci/status/external/common_spec.rb b/spec/lib/gitlab/ci/status/external/common_spec.rb new file mode 100644 index 00000000000..5a97d98b55f --- /dev/null +++ b/spec/lib/gitlab/ci/status/external/common_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::External::Common do + let(:user) { create(:user) } + let(:project) { external_status.project } + let(:external_target_url) { 'http://example.gitlab.com/status' } + + let(:external_status) do + create(:generic_commit_status, target_url: external_target_url) + end + + subject do + Gitlab::Ci::Status::Core + .new(external_status, user) + .extend(described_class) + end + + describe '#has_action?' do + it { is_expected.not_to have_action } + end + + describe '#has_details?' do + context 'when user has access to read commit status' do + before { project.team << [user, :developer] } + + it { is_expected.to have_details } + end + + context 'when user does not have access to read commit status' do + it { is_expected.not_to have_details } + end + end + + describe '#details_path' do + it 'links to the external target URL' do + expect(subject.details_path).to eq external_target_url + end + end +end diff --git a/spec/lib/gitlab/ci/status/external/factory_spec.rb b/spec/lib/gitlab/ci/status/external/factory_spec.rb new file mode 100644 index 00000000000..c96fd53e730 --- /dev/null +++ b/spec/lib/gitlab/ci/status/external/factory_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::External::Factory do + let(:user) { create(:user) } + let(:project) { resource.project } + let(:status) { factory.fabricate! } + let(:factory) { described_class.new(resource, user) } + let(:external_url) { 'http://gitlab.com/status' } + + before do + project.team << [user, :developer] + end + + context 'when external status has a simple core status' do + HasStatus::AVAILABLE_STATUSES.each do |simple_status| + context "when core status is #{simple_status}" do + let(:resource) do + create(:generic_commit_status, status: simple_status, + target_url: external_url) + end + + let(:expected_status) do + Gitlab::Ci::Status.const_get(simple_status.capitalize) + end + + it "fabricates a core status #{simple_status}" do + expect(status).to be_a expected_status + end + + it 'extends core status with common methods' do + expect(status).to have_details + expect(status).not_to have_action + expect(status.details_path).to eq external_url + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb new file mode 100644 index 00000000000..bbf9c7c83a3 --- /dev/null +++ b/spec/lib/gitlab/ci/status/factory_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Factory do + let(:user) { create(:user) } + let(:fabricated_status) { factory.fabricate! } + let(:factory) { described_class.new(resource, user) } + + context 'when object has a core status' do + HasStatus::AVAILABLE_STATUSES.each do |simple_status| + context "when simple core status is #{simple_status}" do + let(:resource) { double('resource', status: simple_status) } + + let(:expected_status) do + Gitlab::Ci::Status.const_get(simple_status.capitalize) + end + + it "fabricates a core status #{simple_status}" do + expect(fabricated_status).to be_a expected_status + end + + it "matches a valid core status for #{simple_status}" do + expect(factory.core_status).to be_a expected_status + end + + it "does not match any extended statuses for #{simple_status}" do + expect(factory.extended_statuses).to be_empty + end + end + end + end + + context 'when resource supports multiple extended statuses' do + let(:resource) { double('resource', status: :success) } + + let(:first_extended_status) do + Class.new(SimpleDelegator) do + def first_method + 'first return value' + end + + def second_method + 'second return value' + end + + def self.matches?(*) + true + end + end + end + + let(:second_extended_status) do + Class.new(SimpleDelegator) do + def first_method + 'decorated return value' + end + + def third_method + 'third return value' + end + + def self.matches?(*) + true + end + end + end + + shared_examples 'compound decorator factory' do + it 'fabricates compound decorator' do + expect(fabricated_status.first_method).to eq 'decorated return value' + expect(fabricated_status.second_method).to eq 'second return value' + expect(fabricated_status.third_method).to eq 'third return value' + end + + it 'delegates to core status' do + expect(fabricated_status.text).to eq 'passed' + end + + it 'latest matches status becomes a status name' do + expect(fabricated_status.class).to eq second_extended_status + end + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Success + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [first_extended_status, second_extended_status] + end + end + + context 'when exclusive statuses are matches' do + before do + allow(described_class).to receive(:extended_statuses) + .and_return([[first_extended_status, second_extended_status]]) + end + + it 'does not fabricate compound decorator' do + expect(fabricated_status.first_method).to eq 'first return value' + expect(fabricated_status.second_method).to eq 'second return value' + expect(fabricated_status).not_to respond_to(:third_method) + end + + it 'delegates to core status' do + expect(fabricated_status.text).to eq 'passed' + end + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Success + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses).to eq [first_extended_status] + end + end + + context 'when exclusive statuses are not matched' do + before do + allow(described_class).to receive(:extended_statuses) + .and_return([[first_extended_status], [second_extended_status]]) + end + + it_behaves_like 'compound decorator factory' + end + + context 'when using simplified status grouping' do + before do + allow(described_class).to receive(:extended_statuses) + .and_return([first_extended_status, second_extended_status]) + end + + it_behaves_like 'compound decorator factory' + end + end +end diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb new file mode 100644 index 00000000000..990d686d22c --- /dev/null +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Failed do + subject do + described_class.new(double('subject'), double('user')) + end + + describe '#text' do + it { expect(subject.label).to eq 'failed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'failed' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_failed' } + end + + describe '#group' do + it { expect(subject.group).to eq 'failed' } + end +end diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb new file mode 100644 index 00000000000..7bb6579c317 --- /dev/null +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pending do + subject do + described_class.new(double('subject'), double('user')) + end + + describe '#text' do + it { expect(subject.label).to eq 'pending' } + end + + describe '#label' do + it { expect(subject.label).to eq 'pending' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_pending' } + end + + describe '#group' do + it { expect(subject.group).to eq 'pending' } + end +end diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb new file mode 100644 index 00000000000..d665674bf70 --- /dev/null +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pipeline::Common do + let(:user) { create(:user) } + let(:project) { create(:empty_project, :private) } + let(:pipeline) { create(:ci_pipeline, project: project) } + + subject do + Gitlab::Ci::Status::Core + .new(pipeline, user) + .extend(described_class) + end + + describe '#has_action?' do + it { is_expected.not_to have_action } + end + + describe '#has_details?' do + context 'when user has access to read pipeline' do + before { project.team << [user, :developer] } + + it { is_expected.to have_details } + end + + context 'when user does not have access to read pipeline' do + it { is_expected.not_to have_details } + end + end + + describe '#details_path' do + it 'links to the pipeline details page' do + expect(subject.details_path) + .to include "pipelines/#{pipeline.id}" + end + end +end diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb new file mode 100644 index 00000000000..b10a447c27a --- /dev/null +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pipeline::Factory do + let(:user) { create(:user) } + let(:project) { pipeline.project } + let(:status) { factory.fabricate! } + let(:factory) { described_class.new(pipeline, user) } + + before do + project.team << [user, :developer] + end + + context 'when pipeline has a core status' do + HasStatus::AVAILABLE_STATUSES.each do |simple_status| + context "when core status is #{simple_status}" do + let(:pipeline) { create(:ci_pipeline, status: simple_status) } + + let(:expected_status) do + Gitlab::Ci::Status.const_get(simple_status.capitalize) + end + + it "matches correct core status for #{simple_status}" do + expect(factory.core_status).to be_a expected_status + end + + it 'does not matche extended statuses' do + expect(factory.extended_statuses).to be_empty + end + + it "fabricates a core status #{simple_status}" do + expect(status).to be_a expected_status + end + + it 'extends core status with common pipeline methods' do + expect(status).to have_details + expect(status).not_to have_action + expect(status.details_path) + .to include "pipelines/#{pipeline.id}" + end + end + end + end + + context 'when pipeline has warnings' do + let(:pipeline) do + create(:ci_pipeline, status: :success) + end + + before do + create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline) + end + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Success + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::SuccessWarning] + end + + it 'fabricates extended "success with warnings" status' do + expect(status).to be_a Gitlab::Ci::Status::SuccessWarning + end + + it 'extends core status with common pipeline method' do + expect(status).to have_details + expect(status.details_path).to include "pipelines/#{pipeline.id}" + end + end +end diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb new file mode 100644 index 00000000000..852d6c06baf --- /dev/null +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Running do + subject do + described_class.new(double('subject'), double('user')) + end + + describe '#text' do + it { expect(subject.label).to eq 'running' } + end + + describe '#label' do + it { expect(subject.label).to eq 'running' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_running' } + end + + describe '#group' do + it { expect(subject.group).to eq 'running' } + end +end diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb new file mode 100644 index 00000000000..e00b356a24b --- /dev/null +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Skipped do + subject do + described_class.new(double('subject'), double('user')) + end + + describe '#text' do + it { expect(subject.label).to eq 'skipped' } + end + + describe '#label' do + it { expect(subject.label).to eq 'skipped' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_skipped' } + end + + describe '#group' do + it { expect(subject.group).to eq 'skipped' } + end +end diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb new file mode 100644 index 00000000000..8814a7614a0 --- /dev/null +++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Stage::Common do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + + let(:stage) do + build(:ci_stage, pipeline: pipeline, name: 'test') + end + + subject do + Class.new(Gitlab::Ci::Status::Core) + .new(stage, user).extend(described_class) + end + + it 'does not have action' do + expect(subject).not_to have_action + end + + it 'links to the pipeline details page' do + expect(subject.details_path) + .to include "pipelines/#{pipeline.id}" + expect(subject.details_path) + .to include "##{stage.name}" + end + + context 'when user has permission to read pipeline' do + before do + project.team << [user, :master] + end + + it 'has details' do + expect(subject).to have_details + end + end + + context 'when user does not have permission to read pipeline' do + it 'does not have details' do + expect(subject).not_to have_details + end + end +end diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb new file mode 100644 index 00000000000..bbb40e2c1ab --- /dev/null +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Stage::Factory do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + + let(:stage) do + build(:ci_stage, pipeline: pipeline, name: 'test') + end + + subject do + described_class.new(stage, user) + end + + let(:status) do + subject.fabricate! + end + + before do + project.team << [user, :developer] + end + + context 'when stage has a core status' do + HasStatus::AVAILABLE_STATUSES.each do |core_status| + context "when core status is #{core_status}" do + before do + create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status) + create(:commit_status, pipeline: pipeline, stage: 'test', status: core_status) + create(:ci_build, pipeline: pipeline, stage: 'build', status: :failed) + end + + it "fabricates a core status #{core_status}" do + expect(status).to be_a( + Gitlab::Ci::Status.const_get(core_status.capitalize)) + end + + it 'extends core status with common stage methods' do + expect(status).to have_details + expect(status.details_path).to include "pipelines/#{pipeline.id}" + expect(status.details_path).to include "##{stage.name}" + end + end + end + end + + context 'when stage has warnings' do + let(:stage) do + build(:ci_stage, name: 'test', status: :success, pipeline: pipeline) + end + + before do + create(:ci_build, :allowed_to_fail, :failed, + stage: 'test', pipeline: stage.pipeline) + end + + it 'fabricates extended "success with warnings" status' do + expect(status) + .to be_a Gitlab::Ci::Status::SuccessWarning + end + + it 'extends core status with common stage method' do + expect(status).to have_details + expect(status.details_path).to include "pipelines/#{pipeline.id}##{stage.name}" + end + end +end diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb new file mode 100644 index 00000000000..4a89e1faf40 --- /dev/null +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Success do + subject do + described_class.new(double('subject'), double('user')) + end + + describe '#text' do + it { expect(subject.label).to eq 'passed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'passed' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_success' } + end + + describe '#group' do + it { expect(subject.group).to eq 'success' } + end +end diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb new file mode 100644 index 00000000000..7e2269397c6 --- /dev/null +++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::SuccessWarning do + subject do + described_class.new(double('status')) + end + + describe '#test' do + it { expect(subject.text).to eq 'passed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'passed with warnings' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_warning' } + end + + describe '#group' do + it { expect(subject.group).to eq 'success_with_warnings' } + end + + describe '.matches?' do + let(:matchable) { double('matchable') } + + context 'when matchable subject is successful' do + before do + allow(matchable).to receive(:success?).and_return(true) + end + + context 'when matchable subject has warnings' do + before do + allow(matchable).to receive(:has_warnings?).and_return(true) + end + + it 'is a correct match' do + expect(described_class.matches?(matchable, double)).to eq true + end + end + + context 'when matchable subject does not have warnings' do + before do + allow(matchable).to receive(:has_warnings?).and_return(false) + end + + it 'does not match' do + expect(described_class.matches?(matchable, double)).to eq false + end + end + end + + context 'when matchable subject is not successful' do + before do + allow(matchable).to receive(:success?).and_return(false) + end + + context 'when matchable subject has warnings' do + before do + allow(matchable).to receive(:has_warnings?).and_return(true) + end + + it 'does not match' do + expect(described_class.matches?(matchable, double)).to eq false + end + end + + context 'when matchable subject does not have warnings' do + it 'does not match' do + expect(described_class.matches?(matchable, double)).to eq false + end + end + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb new file mode 100644 index 00000000000..0267e8c2f69 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::CodeEventFetcher do + let(:stage_name) { :code } + + it_behaves_like 'default query config' do + it 'has a default order' do + expect(event.order).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb new file mode 100644 index 00000000000..e8fc67acf05 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::CodeStage do + let(:stage_name) { :code } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb new file mode 100644 index 00000000000..9d2ba481919 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -0,0 +1,339 @@ +require 'spec_helper' + +describe 'cycle analytics events' do + let(:project) { create(:project) } + let(:from_date) { 10.days.ago } + let(:user) { create(:user, :admin) } + let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } + + let(:events) do + CycleAnalytics.new(project, { from: from_date, current_user: user })[stage].events + end + + before do + allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context]) + + setup(context) + end + + describe '#issue_events' do + let(:stage) { :issue } + + it 'has the total time' do + expect(events.first[:total_time]).not_to be_empty + end + + it 'has a title' do + expect(events.first[:title]).to eq(context.title) + end + + it 'has the URL' do + expect(events.first[:url]).not_to be_nil + end + + it 'has an iid' do + expect(events.first[:iid]).to eq(context.iid.to_s) + end + + it 'has a created_at timestamp' do + expect(events.first[:created_at]).to end_with('ago') + end + + it "has the author's URL" do + expect(events.first[:author][:web_url]).not_to be_nil + end + + it "has the author's avatar URL" do + expect(events.first[:author][:avatar_url]).not_to be_nil + end + + it "has the author's name" do + expect(events.first[:author][:name]).to eq(context.author.name) + end + end + + describe '#plan_events' do + let(:stage) { :plan } + + it 'has a title' do + expect(events.first[:title]).not_to be_nil + end + + it 'has a sha short ID' do + expect(events.first[:short_sha]).not_to be_nil + end + + it 'has the URL' do + expect(events.first[:commit_url]).not_to be_nil + end + + it 'has the total time' do + expect(events.first[:total_time]).not_to be_empty + end + + it "has the author's URL" do + expect(events.first[:author][:web_url]).not_to be_nil + end + + it "has the author's avatar URL" do + expect(events.first[:author][:avatar_url]).not_to be_nil + end + + it "has the author's name" do + expect(events.first[:author][:name]).not_to be_nil + end + end + + describe '#code_events' do + let(:stage) { :code } + + before do + create_commit_referencing_issue(context) + end + + it 'has the total time' do + expect(events.first[:total_time]).not_to be_empty + end + + it 'has a title' do + expect(events.first[:title]).to eq('Awesome merge_request') + end + + it 'has an iid' do + expect(events.first[:iid]).to eq(context.iid.to_s) + end + + it 'has a created_at timestamp' do + expect(events.first[:created_at]).to end_with('ago') + end + + it "has the author's URL" do + expect(events.first[:author][:web_url]).not_to be_nil + end + + it "has the author's avatar URL" do + expect(events.first[:author][:avatar_url]).not_to be_nil + end + + it "has the author's name" do + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) + end + end + + describe '#test_events' do + let(:stage) { :test } + + let(:merge_request) { MergeRequest.first } + let!(:pipeline) do + create(:ci_pipeline, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha, + project: context.project) + end + + before do + create(:ci_build, pipeline: pipeline, status: :success, author: user) + create(:ci_build, pipeline: pipeline, status: :success, author: user) + + pipeline.run! + pipeline.succeed! + end + + it 'has the name' do + expect(events.first[:name]).not_to be_nil + end + + it 'has the ID' do + expect(events.first[:id]).not_to be_nil + end + + it 'has the URL' do + expect(events.first[:url]).not_to be_nil + end + + it 'has the branch name' do + expect(events.first[:branch]).not_to be_nil + end + + it 'has the branch URL' do + expect(events.first[:branch][:url]).not_to be_nil + end + + it 'has the short SHA' do + expect(events.first[:short_sha]).not_to be_nil + end + + it 'has the commit URL' do + expect(events.first[:commit_url]).not_to be_nil + end + + it 'has the date' do + expect(events.first[:date]).not_to be_nil + end + + it 'has the total time' do + expect(events.first[:total_time]).not_to be_empty + end + end + + describe '#review_events' do + let(:stage) { :review } + let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } + + it 'has the total time' do + expect(events.first[:total_time]).not_to be_empty + end + + it 'has a title' do + expect(events.first[:title]).to eq('Awesome merge_request') + end + + it 'has an iid' do + expect(events.first[:iid]).to eq(context.iid.to_s) + end + + it 'has the URL' do + expect(events.first[:url]).not_to be_nil + end + + it 'has a state' do + expect(events.first[:state]).not_to be_nil + end + + it 'has a created_at timestamp' do + expect(events.first[:created_at]).not_to be_nil + end + + it "has the author's URL" do + expect(events.first[:author][:web_url]).not_to be_nil + end + + it "has the author's avatar URL" do + expect(events.first[:author][:avatar_url]).not_to be_nil + end + + it "has the author's name" do + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) + end + end + + describe '#staging_events' do + let(:stage) { :staging } + let(:merge_request) { MergeRequest.first } + let!(:pipeline) do + create(:ci_pipeline, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha, + project: context.project) + end + + before do + create(:ci_build, pipeline: pipeline, status: :success, author: user) + create(:ci_build, pipeline: pipeline, status: :success, author: user) + + pipeline.run! + pipeline.succeed! + + merge_merge_requests_closing_issue(context) + deploy_master + end + + it 'has the name' do + expect(events.first[:name]).not_to be_nil + end + + it 'has the ID' do + expect(events.first[:id]).not_to be_nil + end + + it 'has the URL' do + expect(events.first[:url]).not_to be_nil + end + + it 'has the branch name' do + expect(events.first[:branch]).not_to be_nil + end + + it 'has the branch URL' do + expect(events.first[:branch][:url]).not_to be_nil + end + + it 'has the short SHA' do + expect(events.first[:short_sha]).not_to be_nil + end + + it 'has the commit URL' do + expect(events.first[:commit_url]).not_to be_nil + end + + it 'has the date' do + expect(events.first[:date]).not_to be_nil + end + + it 'has the total time' do + expect(events.first[:total_time]).not_to be_empty + end + + it "has the author's URL" do + expect(events.first[:author][:web_url]).not_to be_nil + end + + it "has the author's avatar URL" do + expect(events.first[:author][:avatar_url]).not_to be_nil + end + + it "has the author's name" do + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) + end + end + + describe '#production_events' do + let(:stage) { :production } + let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } + + before do + merge_merge_requests_closing_issue(context) + deploy_master + end + + it 'has the total time' do + expect(events.first[:total_time]).not_to be_empty + end + + it 'has a title' do + expect(events.first[:title]).to eq(context.title) + end + + it 'has the URL' do + expect(events.first[:url]).not_to be_nil + end + + it 'has an iid' do + expect(events.first[:iid]).to eq(context.iid.to_s) + end + + it 'has a created_at timestamp' do + expect(events.first[:created_at]).to end_with('ago') + end + + it "has the author's URL" do + expect(events.first[:author][:web_url]).not_to be_nil + end + + it "has the author's avatar URL" do + expect(events.first[:author][:avatar_url]).not_to be_nil + end + + it "has the author's name" do + expect(events.first[:author][:name]).to eq(context.author.name) + end + end + + def setup(context) + milestone = create(:milestone, project: project) + context.update(milestone: milestone) + mr = create_merge_request_closing_issue(context) + + ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) + end +end diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb new file mode 100644 index 00000000000..fd9fa2fee49 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::IssueEventFetcher do + let(:stage_name) { :issue } + + it_behaves_like 'default query config' +end diff --git a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb new file mode 100644 index 00000000000..3127f01989d --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::IssueStage do + let(:stage_name) { :issue } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb new file mode 100644 index 00000000000..2d85e712db0 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb @@ -0,0 +1,127 @@ +require 'spec_helper' + +describe Gitlab::CycleAnalytics::Permissions do + let(:project) { create(:empty_project, public_builds: false) } + let(:user) { create(:user) } + + subject { described_class.get(user: user, project: project) } + + context 'user with no relation to the project' do + it 'has no permissions to issue stage' do + expect(subject[:issue]).to eq(false) + end + + it 'has no permissions to test stage' do + expect(subject[:test]).to eq(false) + end + + it 'has no permissions to staging stage' do + expect(subject[:staging]).to eq(false) + end + + it 'has no permissions to production stage' do + expect(subject[:production]).to eq(false) + end + + it 'has no permissions to code stage' do + expect(subject[:code]).to eq(false) + end + + it 'has no permissions to review stage' do + expect(subject[:review]).to eq(false) + end + + it 'has no permissions to plan stage' do + expect(subject[:plan]).to eq(false) + end + end + + context 'user is master' do + before do + project.team << [user, :master] + end + + it 'has permissions to issue stage' do + expect(subject[:issue]).to eq(true) + end + + it 'has permissions to test stage' do + expect(subject[:test]).to eq(true) + end + + it 'has permissions to staging stage' do + expect(subject[:staging]).to eq(true) + end + + it 'has permissions to production stage' do + expect(subject[:production]).to eq(true) + end + + it 'has permissions to code stage' do + expect(subject[:code]).to eq(true) + end + + it 'has permissions to review stage' do + expect(subject[:review]).to eq(true) + end + + it 'has permissions to plan stage' do + expect(subject[:plan]).to eq(true) + end + end + + context 'user has no build permissions' do + before do + project.team << [user, :guest] + end + + it 'has permissions to issue stage' do + expect(subject[:issue]).to eq(true) + end + + it 'has no permissions to test stage' do + expect(subject[:test]).to eq(false) + end + + it 'has no permissions to staging stage' do + expect(subject[:staging]).to eq(false) + end + end + + context 'user has no merge request permissions' do + before do + project.team << [user, :guest] + end + + it 'has permissions to issue stage' do + expect(subject[:issue]).to eq(true) + end + + it 'has no permissions to code stage' do + expect(subject[:code]).to eq(false) + end + + it 'has no permissions to review stage' do + expect(subject[:review]).to eq(false) + end + end + + context 'user has no issue permissions' do + before do + project.team << [user, :developer] + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + end + + it 'has permissions to code stage' do + expect(subject[:code]).to eq(true) + end + + it 'has no permissions to issue stage' do + expect(subject[:issue]).to eq(false) + end + + it 'has no permissions to production stage' do + expect(subject[:production]).to eq(false) + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb new file mode 100644 index 00000000000..2e5dc5b5547 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::PlanEventFetcher do + let(:stage_name) { :plan } + + it_behaves_like 'default query config' do + context 'no commits' do + it 'does not blow up if there are no commits' do + allow(event).to receive(:event_result).and_return([{}]) + + expect { event.fetch }.not_to raise_error + end + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb new file mode 100644 index 00000000000..4c715921ad6 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::PlanStage do + let(:stage_name) { :plan } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb new file mode 100644 index 00000000000..74001181305 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::ProductionEventFetcher do + let(:stage_name) { :production } + + it_behaves_like 'default query config' +end diff --git a/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb new file mode 100644 index 00000000000..916684b81eb --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::ProductionStage do + let(:stage_name) { :production } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb new file mode 100644 index 00000000000..4f67c95ed4c --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::ReviewEventFetcher do + let(:stage_name) { :review } + + it_behaves_like 'default query config' +end diff --git a/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb new file mode 100644 index 00000000000..1412c8dfa08 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::ReviewStage do + let(:stage_name) { :review } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb new file mode 100644 index 00000000000..9c5e57342e9 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +shared_examples 'default query config' do + let(:project) { create(:empty_project) } + let(:event) { described_class.new(project: project, stage: stage_name, options: { from: 1.day.ago }) } + + it 'has the stage attribute' do + expect(event.stage).not_to be_nil + end + + it 'has the projection attributes' do + expect(event.projections).not_to be_nil + end +end diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb new file mode 100644 index 00000000000..08425acbfc8 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +shared_examples 'base stage' do + let(:stage) { described_class.new(project: double, options: {}) } + + before do + allow(stage).to receive(:median).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) + end + + it 'has the median data value' do + expect(stage.as_json[:value]).not_to be_nil + end + + it 'has the median data stage' do + expect(stage.as_json[:title]).not_to be_nil + end + + it 'has the median data description' do + expect(stage.as_json[:description]).not_to be_nil + end + + it 'has the title' do + expect(stage.title).to eq(stage_name.to_s.capitalize) + end + + it 'has the events' do + expect(stage.events).not_to be_nil + end +end diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb new file mode 100644 index 00000000000..fb6b6c4a8d2 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Gitlab::CycleAnalytics::StageSummary, models: true do + let(:project) { create(:project) } + let(:from) { 1.day.ago } + let(:user) { create(:user, :admin) } + subject { described_class.new(project, from: Time.now, current_user: user).data } + + describe "#new_issues" do + it "finds the number of issues created after the 'from date'" do + Timecop.freeze(5.days.ago) { create(:issue, project: project) } + Timecop.freeze(5.days.from_now) { create(:issue, project: project) } + + expect(subject.first[:value]).to eq(1) + end + + it "doesn't find issues from other projects" do + Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) } + + expect(subject.first[:value]).to eq(0) + end + end + + describe "#commits" do + it "finds the number of commits created after the 'from date'" do + Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') } + Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') } + + expect(subject.second[:value]).to eq(1) + end + + it "doesn't find commits from other projects" do + Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') } + + expect(subject.second[:value]).to eq(0) + end + + it "finds a large (> 100) snumber of commits if present" do + Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) } + + expect(subject.second[:value]).to eq(100) + end + end + + describe "#deploys" do + it "finds the number of deploys made created after the 'from date'" do + Timecop.freeze(5.days.ago) { create(:deployment, project: project) } + Timecop.freeze(5.days.from_now) { create(:deployment, project: project) } + + expect(subject.third[:value]).to eq(1) + end + + it "doesn't find commits from other projects" do + Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) } + + expect(subject.third[:value]).to eq(0) + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb new file mode 100644 index 00000000000..bbc82496340 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::StagingEventFetcher do + let(:stage_name) { :staging } + + it_behaves_like 'default query config' do + it 'has a default order' do + expect(event.order).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb new file mode 100644 index 00000000000..8154b3ac701 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::StagingStage do + let(:stage_name) { :staging } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb new file mode 100644 index 00000000000..6639fa54e0e --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::TestEventFetcher do + let(:stage_name) { :test } + + it_behaves_like 'default query config' do + it 'has a default order' do + expect(event.order).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb new file mode 100644 index 00000000000..eacde22cd56 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::TestStage do + let(:stage_name) { :test } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/updater_spec.rb b/spec/lib/gitlab/cycle_analytics/updater_spec.rb new file mode 100644 index 00000000000..eff54cd3692 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/updater_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Gitlab::CycleAnalytics::Updater do + describe 'updates authors' do + let(:user) { create(:user) } + let(:events) { [{ 'author_id' => user.id }] } + + it 'maps the correct user' do + described_class.update!(events, from: 'author_id', to: 'author', klass: User) + + expect(events.first['author']).to eq(user) + end + end + + describe 'updates builds' do + let(:build) { create(:ci_build) } + let(:events) { [{ 'id' => build.id }] } + + it 'maps the correct build' do + described_class.update!(events, from: 'id', to: 'build', klass: ::Ci::Build) + + expect(events.first['build']).to eq(build) + end + end +end diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb new file mode 100644 index 00000000000..f2bc15d39d7 --- /dev/null +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Gitlab::Diff::FileCollection::MergeRequestDiff do + let(:merge_request) { create(:merge_request) } + let(:diff_files) { described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files } + + it 'does not highlight binary files' do + allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => false)) + + expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) + + diff_files + end + + it 'does not highlight file if blob is not accessable' do + allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil) + + expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) + + diff_files + end + + it 'does not files marked as undiffable in .gitattributes' do + allow_any_instance_of(Repository).to receive(:diffable?).and_return(false) + + expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) + + diff_files + end +end diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 0650cb291e5..38475792d93 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -46,4 +46,28 @@ describe Gitlab::Diff::File, lib: true do expect(diff_file.collapsed?).to eq(false) end end + + describe '#old_content_commit' do + it 'returns base commit' do + old_content_commit = diff_file.old_content_commit + + expect(old_content_commit.id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + end + end + + describe '#old_blob' do + it 'returns blob of commit of base commit' do + old_data = diff_file.old_blob.data + + expect(old_data).to include('raise "System commands must be given as an array of strings"') + end + end + + describe '#blob' do + it 'returns blob of new commit' do + data = diff_file.blob.data + + expect(data).to include('raise RuntimeError, "System commands must be given as an array of strings"') + end + end end diff --git a/spec/lib/gitlab/email/email_shared_blocks.rb b/spec/lib/gitlab/email/email_shared_blocks.rb index 19298e261e3..9d806fc524d 100644 --- a/spec/lib/gitlab/email/email_shared_blocks.rb +++ b/spec/lib/gitlab/email/email_shared_blocks.rb @@ -18,7 +18,7 @@ shared_context :email_shared_context do end end -shared_examples :email_shared_examples do +shared_examples :reply_processing_shared_examples do context "when the user could not be found" do before do user.destroy diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb index cb3651e3845..08897a4c310 100644 --- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb @@ -3,7 +3,7 @@ require_relative '../email_shared_blocks' describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do include_context :email_shared_context - it_behaves_like :email_shared_examples + it_behaves_like :reply_processing_shared_examples before do stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo") diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 48660d1dd1b..cebbeff50cf 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -3,7 +3,7 @@ require_relative '../email_shared_blocks' describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do include_context :email_shared_context - it_behaves_like :email_shared_examples + it_behaves_like :reply_processing_shared_examples before do stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") diff --git a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb new file mode 100644 index 00000000000..a444257754b --- /dev/null +++ b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require_relative '../email_shared_blocks' + +describe Gitlab::Email::Handler::UnsubscribeHandler, lib: true do + include_context :email_shared_context + + before do + stub_incoming_email_setting(enabled: true, address: 'reply+%{key}@appmail.adventuretime.ooo') + stub_config_setting(host: 'localhost') + end + + let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}+unsubscribe") } + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + let(:noteable) { create(:issue, project: project) } + + let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) } + + context 'when notification concerns a commit' do + let(:commit) { create(:commit, project: project) } + let!(:sent_notification) { SentNotification.record(commit, user.id, mail_key) } + + it 'handler does not raise an error' do + expect { receiver.execute }.not_to raise_error + end + end + + context 'user is unsubscribed' do + it 'leaves user unsubscribed' do + expect { receiver.execute }.not_to change { noteable.subscribed?(user) }.from(false) + end + end + + context 'user is subscribed' do + before do + noteable.subscribe(user) + end + + it 'unsubscribes user from notable' do + expect { receiver.execute }.to change { noteable.subscribed?(user) }.from(true).to(false) + end + end + + context 'when the noteable could not be found' do + before do + noteable.destroy + end + + it 'raises a NoteableNotFoundError' do + expect { receiver.execute }.to raise_error(Gitlab::Email::NoteableNotFoundError) + end + end + + context 'when no sent notification for the mail key could be found' do + let(:email_raw) { fixture_file('emails/wrong_mail_key.eml') } + + it 'raises a SentNotificationNotFoundError' do + expect { receiver.execute }.to raise_error(Gitlab::Email::SentNotificationNotFoundError) + end + end +end diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb index 6f8e9a4be64..28698e89c33 100644 --- a/spec/lib/gitlab/email/reply_parser_spec.rb +++ b/spec/lib/gitlab/email/reply_parser_spec.rb @@ -88,8 +88,6 @@ describe Gitlab::Email::ReplyParser, lib: true do expect(test_parse_body(fixture_file("emails/inline_reply.eml"))). to eq( <<-BODY.strip_heredoc.chomp - On Wed, Oct 8, 2014 at 11:12 AM, techAPJ <info@unconfigured.discourse.org> wrote: - > techAPJ <https://meta.discourse.org/users/techapj> > November 28 > @@ -206,5 +204,9 @@ describe Gitlab::Email::ReplyParser, lib: true do it "properly renders email reply from MS Outlook client" do expect(test_parse_body(fixture_file("emails/outlook.eml"))).to eq("Microsoft Outlook 2010") end + + it "properly renders html-only email from MS Outlook" do + expect(test_parse_body(fixture_file("emails/outlook_html.eml"))).to eq("Microsoft Outlook 2010") + end end end diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb new file mode 100644 index 00000000000..e5ba13bbaf8 --- /dev/null +++ b/spec/lib/gitlab/file_detector_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Gitlab::FileDetector do + describe '.types_in_paths' do + it 'returns the file types for the given paths' do + expect(described_class.types_in_paths(%w(README.md CHANGELOG VERSION VERSION))). + to eq(%i{readme changelog version}) + end + + it 'does not include unrecognized file paths' do + expect(described_class.types_in_paths(%w(README.md foo.txt))). + to eq(%i{readme}) + end + end + + describe '.type_of' do + it 'returns the type of a README file' do + expect(described_class.type_of('README.md')).to eq(:readme) + 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) + end + end + + it 'returns the type of a license file' do + %w(LICENSE LICENCE COPYING).each do |file| + expect(described_class.type_of(file)).to eq(:license) + end + end + + it 'returns the type of a version file' do + expect(described_class.type_of('VERSION')).to eq(:version) + end + + it 'returns the type of a .gitignore file' do + expect(described_class.type_of('.gitignore')).to eq(:gitignore) + end + + it 'returns the type of a Koding config file' do + expect(described_class.type_of('.koding.yml')).to eq(:koding) + end + + it 'returns the type of a GitLab CI config file' do + expect(described_class.type_of('.gitlab-ci.yml')).to eq(:gitlab_ci) + end + + it 'returns the type of an avatar' do + %w(logo.gif logo.png logo.jpg).each do |file| + expect(described_class.type_of(file)).to eq(:avatar) + end + end + + it 'returns nil for an unknown file' do + expect(described_class.type_of('foo.txt')).to be_nil + end + end +end diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index 6b3dfebd85d..f4703dc704f 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -29,7 +29,7 @@ describe Gitlab::Gfm::ReferenceRewriter do context 'description with ignored elements' do let(:text) do "Hi. This references #1, but not `#2`\n" + - '<pre>and not !1</pre>' + '<pre>and not !1</pre>' end it { is_expected.to include issue_first.to_reference(new_project) } @@ -64,7 +64,7 @@ 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 } + let(:project_ref) { old_project.to_reference(new_project) } context 'label referenced by id' do let(:text) { '#1 and ~123' } @@ -80,7 +80,7 @@ describe Gitlab::Gfm::ReferenceRewriter do 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 } + let(:project_ref) { old_project.to_reference(new_project) } before do old_project.update(namespace: old_group) diff --git a/spec/lib/gitlab/git/attributes_spec.rb b/spec/lib/gitlab/git/attributes_spec.rb new file mode 100644 index 00000000000..9c011e34c11 --- /dev/null +++ b/spec/lib/gitlab/git/attributes_spec.rb @@ -0,0 +1,150 @@ +require 'spec_helper' + +describe Gitlab::Git::Attributes, seed_helper: true do + let(:path) do + File.join(SEED_REPOSITORY_PATH, 'with-git-attributes.git') + end + + subject { described_class.new(path) } + + describe '#attributes' do + context 'using a path with attributes' do + it 'returns the attributes as a Hash' do + expect(subject.attributes('test.txt')).to eq({ 'text' => true }) + end + + it 'returns a Hash containing multiple attributes' do + expect(subject.attributes('test.sh')). + to eq({ 'eol' => 'lf', 'gitlab-language' => 'shell' }) + end + + it 'returns a Hash containing attributes for a file with multiple extensions' do + expect(subject.attributes('test.haml.html')). + to eq({ 'gitlab-language' => 'haml' }) + end + + it 'returns a Hash containing attributes for a file in a directory' do + expect(subject.attributes('foo/bar.txt')).to eq({ 'foo' => true }) + end + + it 'returns a Hash containing attributes with query string parameters' do + expect(subject.attributes('foo.cgi')). + to eq({ 'key' => 'value?p1=v1&p2=v2' }) + end + + it 'returns a Hash containing the attributes for an absolute path' do + expect(subject.attributes('/test.txt')).to eq({ 'text' => true }) + end + + it 'returns a Hash containing the attributes when a pattern is defined using an absolute path' do + # When a path is given without a leading slash it should still match + # patterns defined with a leading slash. + expect(subject.attributes('foo.png')). + to eq({ 'gitlab-language' => 'png' }) + + expect(subject.attributes('/foo.png')). + to eq({ 'gitlab-language' => 'png' }) + end + + it 'returns an empty Hash for a defined path without attributes' do + expect(subject.attributes('bla/bla.txt')).to eq({}) + end + + context 'when the "binary" option is set for a path' do + it 'returns true for the "binary" option' do + expect(subject.attributes('test.binary')['binary']).to eq(true) + end + + it 'returns false for the "diff" option' do + expect(subject.attributes('test.binary')['diff']).to eq(false) + end + end + end + + context 'using a path without any attributes' do + it 'returns an empty Hash' do + expect(subject.attributes('test.foo')).to eq({}) + end + end + end + + describe '#patterns' do + it 'parses a file with entries' do + expect(subject.patterns).to be_an_instance_of(Hash) + end + + it 'parses an entry that uses a tab to separate the pattern and attributes' do + expect(subject.patterns[File.join(path, '*.md')]). + to eq({ 'gitlab-language' => 'markdown' }) + end + + it 'stores patterns in reverse order' do + first = subject.patterns.to_a[0] + + expect(first[0]).to eq(File.join(path, 'bla/bla.txt')) + end + + # It's a bit hard to test for something _not_ being processed. As such we'll + # just test the number of entries. + it 'ignores any comments and empty lines' do + expect(subject.patterns.length).to eq(10) + end + + it 'does not parse anything when the attributes file does not exist' do + expect(File).to receive(:exist?). + with(File.join(path, 'info/attributes')). + and_return(false) + + expect(subject.patterns).to eq({}) + end + end + + describe '#parse_attributes' do + it 'parses a boolean attribute' do + expect(subject.parse_attributes('text')).to eq({ 'text' => true }) + end + + it 'parses a negated boolean attribute' do + expect(subject.parse_attributes('-text')).to eq({ 'text' => false }) + end + + it 'parses a key-value pair' do + expect(subject.parse_attributes('foo=bar')).to eq({ 'foo' => 'bar' }) + end + + it 'parses multiple attributes' do + input = 'boolean key=value -negated' + + expect(subject.parse_attributes(input)). + to eq({ 'boolean' => true, 'key' => 'value', 'negated' => false }) + end + + it 'parses attributes with query string parameters' do + expect(subject.parse_attributes('foo=bar?baz=1')). + to eq({ 'foo' => 'bar?baz=1' }) + end + end + + describe '#each_line' do + it 'iterates over every line in the attributes file' do + args = [String] * 14 # the number of lines in the file + + expect { |b| subject.each_line(&b) }.to yield_successive_args(*args) + end + + it 'does not yield when the attributes file does not exist' do + expect(File).to receive(:exist?). + with(File.join(path, 'info/attributes')). + and_return(false) + + expect { |b| subject.each_line(&b) }.not_to yield_control + end + + it 'does not yield when the attributes file has an unsupported encoding' do + path = File.join(SEED_REPOSITORY_PATH, 'with-invalid-git-attributes.git') + attrs = described_class.new(path) + + expect { |b| attrs.each_line(&b) }.not_to yield_control + end + end +end diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb new file mode 100644 index 00000000000..e169f5af6b6 --- /dev/null +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -0,0 +1,66 @@ +# coding: utf-8 +require "spec_helper" + +describe Gitlab::Git::Blame, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:blame) do + Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md") + end + + context "each count" do + it do + data = [] + blame.each do |commit, line| + data << { + commit: commit, + line: line + } + end + + expect(data.size).to eq(95) + expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) + expect(data.first[:line]).to eq("# Contribute to GitLab") + end + end + + context "ISO-8859 encoding" do + let(:blame) do + Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") + end + + it 'converts to UTF-8' do + data = [] + blame.each do |commit, line| + data << { + commit: commit, + line: line + } + end + + expect(data.size).to eq(1) + expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) + expect(data.first[:line]).to eq("Ä ü") + end + end + + context "unknown encoding" do + let(:blame) do + Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") + end + + it 'converts to UTF-8' do + expect(CharlockHolmes::EncodingDetector).to receive(:detect).and_return(nil) + data = [] + blame.each do |commit, line| + data << { + commit: commit, + line: line + } + end + + expect(data.size).to eq(1) + expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) + expect(data.first[:line]).to eq(" ") + end + end +end diff --git a/spec/lib/gitlab/git/blob_snippet_spec.rb b/spec/lib/gitlab/git/blob_snippet_spec.rb new file mode 100644 index 00000000000..79b1311ac91 --- /dev/null +++ b/spec/lib/gitlab/git/blob_snippet_spec.rb @@ -0,0 +1,19 @@ +# encoding: UTF-8 + +require "spec_helper" + +describe Gitlab::Git::BlobSnippet, seed_helper: true do + describe :data do + context 'empty lines' do + let(:snippet) { Gitlab::Git::BlobSnippet.new('master', nil, nil, nil) } + + it { expect(snippet.data).to be_nil } + end + + context 'present lines' do + let(:snippet) { Gitlab::Git::BlobSnippet.new('master', ['wow', 'much'], 1, 'wow.rb') } + + it { expect(snippet.data).to eq("wow\nmuch") } + end + end +end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb new file mode 100644 index 00000000000..84f79ec2391 --- /dev/null +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -0,0 +1,489 @@ +# encoding: utf-8 + +require "spec_helper" + +describe Gitlab::Git::Blob, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + describe :initialize do + let(:blob) { Gitlab::Git::Blob.new(name: 'test') } + + it 'handles nil data' do + expect(blob.name).to eq('test') + expect(blob.size).to eq(nil) + expect(blob.loaded_size).to eq(nil) + end + end + + describe :find do + context 'file in subdir' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") } + + it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) } + it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) } + it { expect(blob.path).to eq("files/ruby/popen.rb") } + it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) } + it { expect(blob.size).to eq(669) } + it { expect(blob.mode).to eq("100644") } + end + + context 'file in root' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, ".gitignore") } + + it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") } + it { expect(blob.name).to eq(".gitignore") } + it { expect(blob.path).to eq(".gitignore") } + it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") } + it { expect(blob.size).to eq(241) } + it { expect(blob.mode).to eq("100644") } + it { expect(blob).not_to be_binary } + end + + context 'file in root with leading slash' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "/.gitignore") } + + it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") } + it { expect(blob.name).to eq(".gitignore") } + it { expect(blob.path).to eq(".gitignore") } + it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") } + it { expect(blob.size).to eq(241) } + it { expect(blob.mode).to eq("100644") } + end + + context 'non-exist file' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "missing.rb") } + + it { expect(blob).to be_nil } + end + + context 'six submodule' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'six') } + + it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') } + it { expect(blob.data).to eq('') } + + it 'does not get messed up by load_all_data!' do + blob.load_all_data!(repository) + expect(blob.data).to eq('') + end + + it 'does not mark the blob as binary' do + expect(blob).not_to be_binary + end + end + + context 'large file' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'files/images/6049019_460s.jpg') } + let(:blob_size) { 111803 } + + it { expect(blob.size).to eq(blob_size) } + it { expect(blob.data.length).to eq(blob_size) } + + it 'check that this test is sane' do + expect(blob.size).to be <= Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE + end + + it 'can load all data' do + blob.load_all_data!(repository) + expect(blob.data.length).to eq(blob_size) + end + + it 'marks the blob as binary' do + expect(Gitlab::Git::Blob).to receive(:new). + with(hash_including(binary: true)). + and_call_original + + expect(blob).to be_binary + end + end + end + + describe :raw do + let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::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 } + + context 'large file' do + it 'limits the size of a large file' do + blob_size = Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE + 1 + buffer = Array.new(blob_size, 0) + rugged_blob = Rugged::Blob.from_buffer(repository.rugged, buffer.join('')) + blob = Gitlab::Git::Blob.raw(repository, rugged_blob) + + expect(blob.size).to eq(blob_size) + expect(blob.loaded_size).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) + expect(blob.data.length).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) + expect(blob.truncated?).to be_truthy + + blob.load_all_data!(repository) + expect(blob.loaded_size).to eq(blob_size) + end + end + end + + describe 'encoding' do + context 'file with russian text' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") } + + it { expect(blob.name).to eq("russian.rb") } + it { expect(blob.data.lines.first).to eq("Хороший файл") } + it { expect(blob.size).to eq(23) } + it { expect(blob.truncated?).to be_falsey } + # Run it twice since data is encoded after the first run + it { expect(blob.truncated?).to be_falsey } + it { expect(blob.mode).to eq("100755") } + end + + context 'file with Chinese text' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/テスト.txt") } + + it { expect(blob.name).to eq("テスト.txt") } + it { expect(blob.data).to include("これはテスト") } + it { expect(blob.size).to eq(340) } + it { expect(blob.mode).to eq("100755") } + it { expect(blob.truncated?).to be_falsey } + end + + context 'file with ISO-8859 text' do + let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::LastCommit::ID, "encoding/iso8859.txt") } + + it { expect(blob.name).to eq("iso8859.txt") } + it { expect(blob.loaded_size).to eq(4) } + it { expect(blob.size).to eq(4) } + it { expect(blob.mode).to eq("100644") } + it { expect(blob.truncated?).to be_falsey } + end + end + + describe 'mode' do + context 'file regular' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', + 'files/ruby/regex.rb' + ) + end + + it { expect(blob.name).to eq('regex.rb') } + it { expect(blob.path).to eq('files/ruby/regex.rb') } + it { expect(blob.size).to eq(1200) } + it { expect(blob.mode).to eq("100644") } + end + + context 'file binary' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', + 'files/executables/ls' + ) + end + + it { expect(blob.name).to eq('ls') } + it { expect(blob.path).to eq('files/executables/ls') } + it { expect(blob.size).to eq(110080) } + it { expect(blob.mode).to eq("100755") } + end + + context 'file symlink to regular' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', + 'files/links/ruby-style-guide.md' + ) + end + + it { expect(blob.name).to eq('ruby-style-guide.md') } + it { expect(blob.path).to eq('files/links/ruby-style-guide.md') } + it { expect(blob.size).to eq(31) } + it { expect(blob.mode).to eq("120000") } + end + + context 'file symlink to binary' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', + 'files/links/touch' + ) + end + + it { expect(blob.name).to eq('touch') } + it { expect(blob.path).to eq('files/links/touch') } + it { expect(blob.size).to eq(20) } + it { expect(blob.mode).to eq("120000") } + end + end + + describe :commit do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + let(:commit_options) do + { + file: { + content: 'Lorem ipsum...', + path: 'documents/story.txt' + }, + author: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + committer: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + commit: { + message: 'Wow such commit', + branch: 'fix-mode' + } + } + end + + let(:commit_sha) { Gitlab::Git::Blob.commit(repository, commit_options) } + let(:commit) { repository.lookup(commit_sha) } + + it 'should add file with commit' do + # Commit message valid + expect(commit.message).to eq('Wow such commit') + + tree = commit.tree.to_a.find { |tree| tree[:name] == 'documents' } + + # Directory was created + expect(tree[:type]).to eq(:tree) + + # File was created + expect(repository.lookup(tree[:oid]).first[:name]).to eq('story.txt') + end + + describe "ref updating" do + it 'creates a commit but does not udate a ref' do + commit_opts = commit_options.tap{ |opts| opts[:commit][:update_ref] = false} + commit_sha = Gitlab::Git::Blob.commit(repository, commit_opts) + commit = repository.lookup(commit_sha) + + # Commit message valid + expect(commit.message).to eq('Wow such commit') + + # Does not update any related ref + expect(repository.lookup("fix-mode").oid).not_to eq(commit.oid) + expect(repository.lookup("HEAD").oid).not_to eq(commit.oid) + end + end + + describe 'reject updates' do + it 'should reject updates' do + commit_options[:file][:update] = false + commit_options[:file][:path] = 'files/executables/ls' + + expect{ commit_sha }.to raise_error('Filename already exists; update not allowed') + end + end + + describe 'file modes' do + it 'should preserve file modes with commit' do + commit_options[:file][:path] = 'files/executables/ls' + + entry = Gitlab::Git::Blob::find_entry_by_path(repository, commit.tree.oid, commit_options[:file][:path]) + expect(entry[:filemode]).to eq(0100755) + end + end + end + + describe :rename do + let(:repository) { Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) } + let(:rename_options) do + { + file: { + path: 'NEWCONTRIBUTING.md', + previous_path: 'CONTRIBUTING.md', + content: 'Lorem ipsum...', + update: true + }, + author: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + committer: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + commit: { + message: 'Rename readme', + branch: 'master' + } + } + end + + let(:rename_options2) do + { + file: { + content: 'Lorem ipsum...', + path: 'bin/new_executable', + previous_path: 'bin/executable', + }, + author: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + committer: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + commit: { + message: 'Updates toberenamed.txt', + branch: 'master', + update_ref: false + } + } + end + + it 'maintains file permissions when renaming' do + mode = 0o100755 + + Gitlab::Git::Blob.rename(repository, rename_options2) + + expect(repository.rugged.index.get(rename_options2[:file][:path])[:mode]).to eq(mode) + end + + it 'renames the file with commit and not change file permissions' do + ref = rename_options[:commit][:branch] + + expect(repository.rugged.index.get('CONTRIBUTING.md')).not_to be_nil + expect { Gitlab::Git::Blob.rename(repository, rename_options) }.to change { repository.commit_count(ref) }.by(1) + + expect(repository.rugged.index.get('CONTRIBUTING.md')).to be_nil + expect(repository.rugged.index.get('NEWCONTRIBUTING.md')).not_to be_nil + end + end + + describe :remove do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + let(:commit_options) do + { + file: { + path: 'README.md' + }, + author: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + committer: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + commit: { + message: 'Remove readme', + branch: 'feature' + } + } + end + + let(:commit_sha) { Gitlab::Git::Blob.remove(repository, commit_options) } + let(:commit) { repository.lookup(commit_sha) } + let(:blob) { Gitlab::Git::Blob.find(repository, commit_sha, "README.md") } + + it 'should remove file with commit' do + # Commit message valid + expect(commit.message).to eq('Remove readme') + + # File was removed + expect(blob).to be_nil + end + end + + describe :lfs_pointers do + context 'file a valid lfs pointer' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'files/lfs/image.jpg' + ) + end + + it { expect(blob.lfs_pointer?).to eq(true) } + it { expect(blob.lfs_oid).to eq("4206f951d2691c78aac4c0ce9f2b23580b2c92cdcc4336e1028742c0274938e0") } + it { expect(blob.lfs_size).to eq("19548") } + it { expect(blob.id).to eq("f4d76af13003d1106be7ac8c5a2a3d37ddf32c2a") } + it { expect(blob.name).to eq("image.jpg") } + it { expect(blob.path).to eq("files/lfs/image.jpg") } + it { expect(blob.size).to eq(130) } + it { expect(blob.mode).to eq("100644") } + end + + describe 'file an invalid lfs pointer' do + context 'with correct version header but incorrect size and oid' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'files/lfs/archive-invalid.tar' + ) + end + + it { expect(blob.lfs_pointer?).to eq(false) } + it { expect(blob.lfs_oid).to eq(nil) } + it { expect(blob.lfs_size).to eq(nil) } + it { expect(blob.id).to eq("f8a898db217a5a85ed8b3d25b34c1df1d1094c46") } + it { expect(blob.name).to eq("archive-invalid.tar") } + it { expect(blob.path).to eq("files/lfs/archive-invalid.tar") } + it { expect(blob.size).to eq(43) } + it { expect(blob.mode).to eq("100644") } + end + + context 'with correct version header and size but incorrect size and oid' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'files/lfs/picture-invalid.png' + ) + end + + it { expect(blob.lfs_pointer?).to eq(false) } + it { expect(blob.lfs_oid).to eq(nil) } + it { expect(blob.lfs_size).to eq("1575078") } + it { expect(blob.id).to eq("5ae35296e1f95c1ef9feda1241477ed29a448572") } + it { expect(blob.name).to eq("picture-invalid.png") } + it { expect(blob.path).to eq("files/lfs/picture-invalid.png") } + it { expect(blob.size).to eq(57) } + it { expect(blob.mode).to eq("100644") } + end + + context 'with correct version header and size but invalid size and oid' do + let(:blob) do + Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'files/lfs/file-invalid.zip' + ) + end + + it { expect(blob.lfs_pointer?).to eq(false) } + it { expect(blob.lfs_oid).to eq(nil) } + it { expect(blob.lfs_size).to eq(nil) } + it { expect(blob.id).to eq("d831981bd876732b85a1bcc6cc01210c9f36248f") } + it { expect(blob.name).to eq("file-invalid.zip") } + it { expect(blob.path).to eq("files/lfs/file-invalid.zip") } + it { expect(blob.size).to eq(60) } + it { expect(blob.mode).to eq("100644") } + end + end + end +end diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb new file mode 100644 index 00000000000..78234b396c5 --- /dev/null +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -0,0 +1,31 @@ +require "spec_helper" + +describe Gitlab::Git::Branch, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + subject { repository.branches } + + it { is_expected.to be_kind_of Array } + + describe '#size' do + subject { super().size } + it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) } + end + + describe 'first branch' do + let(:branch) { repository.branches.first } + + it { expect(branch.name).to eq(SeedRepo::Repo::BRANCHES.first) } + it { expect(branch.dereferenced_target.sha).to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") } + end + + describe 'master branch' do + let(:branch) do + repository.branches.find { |branch| branch.name == 'master' } + end + + it { expect(branch.dereferenced_target.sha).to eq(SeedRepo::LastCommit::ID) } + end + + it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) } +end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb new file mode 100644 index 00000000000..e1be6784c20 --- /dev/null +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -0,0 +1,408 @@ +require "spec_helper" + +describe Gitlab::Git::Commit, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) } + let(:rugged_commit) do + repository.rugged.lookup(SeedRepo::Commit::ID) + end + + describe "Commit info" do + before do + repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + + @committer = { + email: 'mike@smith.com', + name: "Mike Smith", + time: Time.now + } + + @author = { + email: 'john@smith.com', + name: "John Smith", + time: Time.now + } + + @parents = [repo.head.target] + @gitlab_parents = @parents.map { |c| Gitlab::Git::Commit.decorate(c) } + @tree = @parents.first.tree + + sha = Rugged::Commit.create( + repo, + author: @author, + committer: @committer, + tree: @tree, + parents: @parents, + message: "Refactoring specs", + update_ref: "HEAD" + ) + + @raw_commit = repo.lookup(sha) + @commit = Gitlab::Git::Commit.new(@raw_commit) + end + + it { expect(@commit.short_id).to eq(@raw_commit.oid[0..10]) } + it { expect(@commit.id).to eq(@raw_commit.oid) } + it { expect(@commit.sha).to eq(@raw_commit.oid) } + it { expect(@commit.safe_message).to eq(@raw_commit.message) } + it { expect(@commit.created_at).to eq(@raw_commit.author[:time]) } + it { expect(@commit.date).to eq(@raw_commit.committer[:time]) } + it { expect(@commit.author_email).to eq(@author[:email]) } + 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.different_committer?).to be_truthy } + it { expect(@commit.parents).to eq(@gitlab_parents) } + it { expect(@commit.parent_id).to eq(@parents.first.oid) } + it { expect(@commit.no_commit_message).to eq("--no commit message") } + it { expect(@commit.tree).to eq(@tree) } + + after do + # Erase the new commit so other tests get the original repo + repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) + end + end + + context 'Class methods' do + describe :find do + it "should return first head commit if without params" do + expect(Gitlab::Git::Commit.last(repository).id).to eq( + repository.raw.head.target.oid + ) + end + + it "should return valid commit" do + expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_valid_commit + end + + it "should return valid commit for tag" do + expect(Gitlab::Git::Commit.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + end + + it "should return nil for non-commit ids" do + blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") + expect(Gitlab::Git::Commit.find(repository, blob.id)).to be_nil + end + + it "should return nil for parent of non-commit object" do + blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") + expect(Gitlab::Git::Commit.find(repository, "#{blob.id}^")).to be_nil + end + + it "should return nil for nonexisting ids" do + expect(Gitlab::Git::Commit.find(repository, "+123_4532530XYZ")).to be_nil + end + + context 'with broken repo' do + let(:repository) { Gitlab::Git::Repository.new(TEST_BROKEN_REPO_PATH) } + + it 'returns nil' do + expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_nil + end + end + end + + describe :last_for_path do + context 'no path' do + subject { Gitlab::Git::Commit.last_for_path(repository, 'master') } + + describe '#id' do + subject { super().id } + it { is_expected.to eq(SeedRepo::LastCommit::ID) } + end + end + + context 'path' do + subject { Gitlab::Git::Commit.last_for_path(repository, 'master', 'files/ruby') } + + describe '#id' do + subject { super().id } + it { is_expected.to eq(SeedRepo::Commit::ID) } + end + end + + context 'ref + path' do + subject { Gitlab::Git::Commit.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') } + + describe '#id' do + subject { super().id } + it { is_expected.to eq(SeedRepo::BigCommit::ID) } + end + end + end + + describe "where" do + context 'path is empty string' do + subject do + commits = Gitlab::Git::Commit.where( + repo: repository, + ref: 'master', + path: '', + limit: 10 + ) + + commits.map { |c| c.id } + end + + it 'has 10 elements' do + expect(subject.size).to eq(10) + end + it { is_expected.to include(SeedRepo::EmptyCommit::ID) } + end + + context 'path is nil' do + subject do + commits = Gitlab::Git::Commit.where( + repo: repository, + ref: 'master', + path: nil, + limit: 10 + ) + + commits.map { |c| c.id } + end + + it 'has 10 elements' do + expect(subject.size).to eq(10) + end + it { is_expected.to include(SeedRepo::EmptyCommit::ID) } + end + + context 'ref is branch name' do + subject do + commits = Gitlab::Git::Commit.where( + repo: repository, + ref: 'master', + path: 'files', + limit: 3, + offset: 1 + ) + + commits.map { |c| c.id } + end + + it 'has 3 elements' do + expect(subject.size).to eq(3) + end + it { is_expected.to include("d14d6c0abdd253381df51a723d58691b2ee1ab08") } + it { is_expected.not_to include("eb49186cfa5c4338011f5f590fac11bd66c5c631") } + end + + context 'ref is commit id' do + subject do + commits = Gitlab::Git::Commit.where( + repo: repository, + ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e", + path: 'files', + limit: 3, + offset: 1 + ) + + commits.map { |c| c.id } + end + + it 'has 3 elements' do + expect(subject.size).to eq(3) + end + it { is_expected.to include("2f63565e7aac07bcdadb654e253078b727143ec4") } + it { is_expected.not_to include(SeedRepo::Commit::ID) } + end + + context 'ref is tag' do + subject do + commits = Gitlab::Git::Commit.where( + repo: repository, + ref: 'v1.0.0', + path: 'files', + limit: 3, + offset: 1 + ) + + commits.map { |c| c.id } + end + + it 'has 3 elements' do + expect(subject.size).to eq(3) + end + it { is_expected.to include("874797c3a73b60d2187ed6e2fcabd289ff75171e") } + it { is_expected.not_to include(SeedRepo::Commit::ID) } + end + end + + describe :between do + subject do + commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID) + commits.map { |c| c.id } + end + + it 'has 1 element' do + expect(subject.size).to eq(1) + end + it { is_expected.to include(SeedRepo::Commit::ID) } + it { is_expected.not_to include(SeedRepo::FirstCommit::ID) } + end + + describe :find_all do + context 'max_count' do + subject do + commits = Gitlab::Git::Commit.find_all( + repository, + max_count: 50 + ) + + commits.map { |c| c.id } + end + + it 'has 31 elements' do + expect(subject.size).to eq(33) + end + it { is_expected.to include(SeedRepo::Commit::ID) } + it { is_expected.to include(SeedRepo::Commit::PARENT_ID) } + it { is_expected.to include(SeedRepo::FirstCommit::ID) } + end + + context 'ref + max_count + skip' do + subject do + commits = Gitlab::Git::Commit.find_all( + repository, + ref: 'master', + max_count: 50, + skip: 1 + ) + + commits.map { |c| c.id } + end + + it 'has 23 elements' do + expect(subject.size).to eq(24) + end + it { is_expected.to include(SeedRepo::Commit::ID) } + it { is_expected.to include(SeedRepo::FirstCommit::ID) } + it { is_expected.not_to include(SeedRepo::LastCommit::ID) } + end + + context 'contains feature + max_count' do + subject do + commits = Gitlab::Git::Commit.find_all( + repository, + contains: 'feature', + max_count: 7 + ) + + commits.map { |c| c.id } + end + + it 'has 7 elements' do + expect(subject.size).to eq(7) + end + + it { is_expected.not_to include(SeedRepo::Commit::PARENT_ID) } + it { is_expected.not_to include(SeedRepo::Commit::ID) } + it { is_expected.to include(SeedRepo::BigCommit::ID) } + end + end + end + + describe :init_from_rugged do + let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) } + subject { gitlab_commit } + + describe '#id' do + subject { super().id } + it { is_expected.to eq(SeedRepo::Commit::ID) } + end + end + + describe :init_from_hash do + let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) } + subject { commit } + + describe '#id' do + subject { super().id } + it { is_expected.to eq(sample_commit_hash[:id])} + end + + describe '#message' do + subject { super().message } + it { is_expected.to eq(sample_commit_hash[:message])} + end + end + + describe :stats do + subject { commit.stats } + + describe '#additions' do + subject { super().additions } + it { is_expected.to eq(11) } + end + + describe '#deletions' do + subject { super().deletions } + it { is_expected.to eq(6) } + end + end + + describe :to_diff do + subject { commit.to_diff } + + it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" } + it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} + end + + describe :has_zero_stats? do + it { expect(commit.has_zero_stats?).to eq(false) } + end + + describe :to_patch do + subject { commit.to_patch } + + it { is_expected.to include "From #{SeedRepo::Commit::ID}" } + it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} + end + + describe :to_hash do + let(:hash) { commit.to_hash } + subject { hash } + + it { is_expected.to be_kind_of Hash } + + describe '#keys' do + subject { super().keys.sort } + it { is_expected.to match(sample_commit_hash.keys.sort) } + end + end + + describe :diffs do + subject { commit.diffs } + + it { is_expected.to be_kind_of Gitlab::Git::DiffCollection } + it { expect(subject.count).to eq(2) } + it { expect(subject.first).to be_kind_of Gitlab::Git::Diff } + end + + describe :ref_names do + let(:commit) { Gitlab::Git::Commit.find(repository, 'master') } + subject { commit.ref_names(repository) } + + it 'has 1 element' do + expect(subject.size).to eq(1) + end + it { is_expected.to include("master") } + it { is_expected.not_to include("feature") } + end + + def sample_commit_hash + { + author_email: "dmitriy.zaporozhets@gmail.com", + author_name: "Dmitriy Zaporozhets", + authored_date: "2012-02-27 20:51:12 +0200", + committed_date: "2012-02-27 20:51:12 +0200", + committer_email: "dmitriy.zaporozhets@gmail.com", + committer_name: "Dmitriy Zaporozhets", + id: SeedRepo::Commit::ID, + message: "tree css fixes", + parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"] + } + end +end diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb new file mode 100644 index 00000000000..f66b68e4218 --- /dev/null +++ b/spec/lib/gitlab/git/compare_spec.rb @@ -0,0 +1,109 @@ +require "spec_helper" + +describe Gitlab::Git::Compare, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, false) } + let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, true) } + + describe :commits do + subject do + compare.commits.map(&:id) + end + + it 'has 8 elements' do + expect(subject.size).to eq(8) + end + + it { is_expected.to include(SeedRepo::Commit::PARENT_ID) } + it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) } + + context 'non-existing base ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) } + + it { is_expected.to be_empty } + end + + context 'non-existing head ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') } + + it { is_expected.to be_empty } + end + + context 'base ref is equal to head ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) } + + it { is_expected.to be_empty } + end + + context 'providing nil as base ref or head ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, nil, nil) } + + it { is_expected.to be_empty } + end + end + + describe :diffs do + subject do + compare.diffs.map(&:new_path) + end + + it 'has 10 elements' do + expect(subject.size).to eq(10) + end + + it { is_expected.to include('files/ruby/popen.rb') } + it { is_expected.not_to include('LICENSE') } + + context 'non-existing base ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) } + + it { is_expected.to be_empty } + end + + context 'non-existing head ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') } + + it { is_expected.to be_empty } + end + end + + describe :same do + subject do + compare.same + end + + it { is_expected.to eq(false) } + + context 'base ref is equal to head ref' do + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) } + + it { is_expected.to eq(true) } + end + end + + describe :commits_straight do + subject do + compare_straight.commits.map(&:id) + end + + it 'has 8 elements' do + expect(subject.size).to eq(8) + end + + it { is_expected.to include(SeedRepo::Commit::PARENT_ID) } + it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) } + end + + describe :diffs_straight do + subject do + compare_straight.diffs.map(&:new_path) + end + + it 'has 10 elements' do + expect(subject.size).to eq(10) + end + + it { is_expected.to include('files/ruby/popen.rb') } + it { is_expected.not_to include('LICENSE') } + end +end diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb new file mode 100644 index 00000000000..4fa72c565ae --- /dev/null +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -0,0 +1,460 @@ +require 'spec_helper' + +describe Gitlab::Git::DiffCollection, seed_helper: true do + subject do + Gitlab::Git::DiffCollection.new( + iterator, + max_files: max_files, + max_lines: max_lines, + all_diffs: all_diffs, + no_collapse: no_collapse + ) + end + let(:iterator) { Array.new(file_count, fake_diff(line_length, line_count)) } + let(:file_count) { 0 } + let(:line_length) { 1 } + let(:line_count) { 1 } + let(:max_files) { 10 } + let(:max_lines) { 100 } + let(:all_diffs) { false } + let(:no_collapse) { true } + + describe '#to_a' do + subject { super().to_a } + it { is_expected.to be_kind_of ::Array } + end + + describe :decorate! do + let(:file_count) { 3 } + + it 'modifies the array in place' do + count = 0 + subject.decorate! { |d| !d.nil? && count += 1 } + expect(subject.to_a).to eq([1, 2, 3]) + expect(count).to eq(3) + end + + it 'avoids future iterator iterations' do + subject.decorate! { |d| d unless d.nil? } + + expect(iterator).not_to receive(:each) + + subject.overflow? + end + end + + context 'overflow handling' do + context 'adding few enough files' do + let(:file_count) { 3 } + + context 'and few enough lines' do + let(:line_count) { 10 } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('3') } + end + it { expect(subject.size).to eq(3) } + + context 'when limiting is disabled' do + let(:all_diffs) { true } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('3') } + end + it { expect(subject.size).to eq(3) } + end + end + + context 'and too many lines' do + let(:line_count) { 1000 } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_truthy } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('0+') } + end + it { expect(subject.size).to eq(0) } + + context 'when limiting is disabled' do + let(:all_diffs) { true } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('3') } + end + it { expect(subject.size).to eq(3) } + end + end + end + + context 'adding too many files' do + let(:file_count) { 11 } + + context 'and few enough lines' do + let(:line_count) { 1 } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_truthy } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('10+') } + end + it { expect(subject.size).to eq(10) } + + context 'when limiting is disabled' do + let(:all_diffs) { true } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('11') } + end + it { expect(subject.size).to eq(11) } + end + end + + context 'and too many lines' do + let(:line_count) { 30 } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_truthy } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('3+') } + end + it { expect(subject.size).to eq(3) } + + context 'when limiting is disabled' do + let(:all_diffs) { true } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('11') } + end + it { expect(subject.size).to eq(11) } + end + end + end + + context 'adding exactly the maximum number of files' do + let(:file_count) { 10 } + + context 'and few enough lines' do + let(:line_count) { 1 } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('10') } + end + it { expect(subject.size).to eq(10) } + end + end + + context 'adding too many bytes' do + let(:file_count) { 10 } + let(:line_length) { 5200 } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_truthy } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('9+') } + end + it { expect(subject.size).to eq(9) } + + context 'when limiting is disabled' do + let(:all_diffs) { true } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('10') } + end + it { expect(subject.size).to eq(10) } + end + end + end + + describe 'empty collection' do + subject { Gitlab::Git::DiffCollection.new([]) } + + describe '#overflow?' do + subject { super().overflow? } + it { is_expected.to be_falsey } + end + + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_truthy } + end + + describe '#size' do + subject { super().size } + it { is_expected.to eq(0) } + end + + describe '#real_size' do + subject { super().real_size } + it { is_expected.to eq('0')} + end + end + + describe :each do + context 'when diff are too large' do + let(:collection) do + Gitlab::Git::DiffCollection.new([{ diff: 'a' * 204800 }]) + end + + it 'yields Diff instances even when they are too large' do + expect { |b| collection.each(&b) }. + to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + end + + it 'prunes diffs that are too large' do + diff = nil + + collection.each do |d| + diff = d + end + + expect(diff.diff).to eq('') + end + end + + context 'when diff is quite large will collapse by default' do + let(:iterator) { [{ diff: 'a' * 20480 }] } + + context 'when no collapse is set' do + let(:no_collapse) { true } + + it 'yields Diff instances even when they are quite big' do + expect { |b| subject.each(&b) }. + to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + end + + it 'does not prune diffs' do + diff = nil + + subject.each do |d| + diff = d + end + + expect(diff.diff).not_to eq('') + end + end + + context 'when no collapse is unset' do + let(:no_collapse) { false } + + it 'yields Diff instances even when they are quite big' do + expect { |b| subject.each(&b) }. + to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + end + + it 'prunes diffs that are quite big' do + diff = nil + + subject.each do |d| + diff = d + end + + expect(diff.diff).to eq('') + end + + context 'when go over safe limits on files' do + let(:iterator) { [ fake_diff(1, 1) ] * 4 } + + before(:each) do + stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines }) + end + + it 'prunes diffs by default even little ones' do + subject.each_with_index do |d, i| + if i < 2 + expect(d.diff).not_to eq('') + else # 90 lines + expect(d.diff).to eq('') + end + end + end + end + + context 'when go over safe limits on lines' do + let(:iterator) do + [ + fake_diff(1, 45), + fake_diff(1, 45), + fake_diff(1, 20480), + fake_diff(1, 1) + ] + end + + before(:each) do + stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 }) + end + + it 'prunes diffs by default even little ones' do + subject.each_with_index do |d, i| + if i < 2 + expect(d.diff).not_to eq('') + else # 90 lines + expect(d.diff).to eq('') + end + end + end + end + + context 'when go over safe limits on bytes' do + let(:iterator) do + [ + fake_diff(1, 45), + fake_diff(1, 45), + fake_diff(1, 20480), + fake_diff(1, 1) + ] + end + + before(:each) do + stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 }) + end + + it 'prunes diffs by default even little ones' do + subject.each_with_index do |d, i| + if i < 2 + expect(d.diff).not_to eq('') + else # > 80 bytes + expect(d.diff).to eq('') + end + end + end + end + end + + context 'when limiting is disabled' do + let(:all_diffs) { true } + + it 'yields Diff instances even when they are quite big' do + expect { |b| subject.each(&b) }. + to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + end + + it 'does not prune diffs' do + diff = nil + + subject.each do |d| + diff = d + end + + expect(diff.diff).not_to eq('') + end + end + end + end + + def fake_diff(line_length, line_count) + { 'diff' => "#{'a' * line_length}\n" * line_count } + end +end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb new file mode 100644 index 00000000000..4c55532d165 --- /dev/null +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -0,0 +1,287 @@ +require "spec_helper" + +describe Gitlab::Git::Diff, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + before do + @raw_diff_hash = { + diff: <<EOT.gsub(/^ {8}/, "").sub(/\n$/, ""), + --- a/.gitmodules + +++ b/.gitmodules + @@ -4,3 +4,6 @@ + [submodule "gitlab-shell"] + \tpath = gitlab-shell + \turl = https://github.com/gitlabhq/gitlab-shell.git + +[submodule "gitlab-grack"] + + path = gitlab-grack + + url = https://gitlab.com/gitlab-org/gitlab-grack.git + +EOT + new_path: ".gitmodules", + old_path: ".gitmodules", + a_mode: '100644', + b_mode: '100644', + new_file: false, + renamed_file: false, + deleted_file: false, + too_large: false + } + + @rugged_diff = repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths: + [".gitmodules"]).patches.first + end + + describe '.new' do + context 'using a Hash' do + context 'with a small diff' do + let(:diff) { described_class.new(@raw_diff_hash) } + + it 'initializes the diff' do + expect(diff.to_hash).to eq(@raw_diff_hash) + end + + it 'does not prune the diff' do + expect(diff).not_to be_too_large + end + end + + context 'using a diff that is too large' do + it 'prunes the diff' do + diff = described_class.new(diff: 'a' * 204800) + + expect(diff.diff).to be_empty + expect(diff).to be_too_large + end + end + end + + context 'using a Rugged::Patch' do + context 'with a small diff' do + let(:diff) { described_class.new(@rugged_diff) } + + it 'initializes the diff' do + expect(diff.to_hash).to eq(@raw_diff_hash.merge(too_large: nil)) + end + + it 'does not prune the diff' do + expect(diff).not_to be_too_large + end + end + + context 'using a diff that is too large' do + it 'prunes the diff' do + expect_any_instance_of(String).to receive(:bytesize). + and_return(1024 * 1024 * 1024) + + diff = described_class.new(@rugged_diff) + + expect(diff.diff).to be_empty + expect(diff).to be_too_large + end + end + + context 'using a collapsable diff that is too large' do + before do + # 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. + stub_const("#{described_class}::DIFF_SIZE_LIMIT", 150) + stub_const("#{described_class}::DIFF_COLLAPSE_LIMIT", 100) + end + + it 'prunes the diff as a large diff instead of as a collapsed diff' do + diff = described_class.new(@rugged_diff, collapse: true) + + expect(diff.diff).to be_empty + expect(diff).to be_too_large + expect(diff).not_to be_collapsed + end + end + + context 'using a large binary diff' do + it 'does not prune the diff' do + expect_any_instance_of(Rugged::Diff::Delta).to receive(:binary?). + and_return(true) + + diff = described_class.new(@rugged_diff) + + expect(diff.diff).not_to be_empty + end + end + end + end + + describe 'straight diffs' do + let(:options) { { straight: true } } + let(:diffs) { described_class.between(repository, 'feature', 'master', options) } + + it 'has the correct size' do + expect(diffs.size).to eq(24) + end + + context 'diff' do + it 'is an instance of Diff' do + expect(diffs.first).to be_kind_of(described_class) + end + + it 'has the correct new_path' do + expect(diffs.first.new_path).to eq('.DS_Store') + end + + it 'has the correct diff' do + expect(diffs.first.diff).to include('Binary files /dev/null and b/.DS_Store differ') + end + end + end + + describe '.between' do + let(:diffs) { described_class.between(repository, 'feature', 'master') } + subject { diffs } + + it { is_expected.to be_kind_of Gitlab::Git::DiffCollection } + + describe '#size' do + subject { super().size } + + it { is_expected.to eq(1) } + end + + context 'diff' do + subject { diffs.first } + + it { is_expected.to be_kind_of described_class } + + describe '#new_path' do + subject { super().new_path } + + it { is_expected.to eq('files/ruby/feature.rb') } + end + + describe '#diff' do + subject { super().diff } + + it { is_expected.to include '+class Feature' } + end + end + end + + describe '.filter_diff_options' do + let(:options) { { max_size: 100, invalid_opt: true } } + + context "without default options" do + let(:filtered_options) { described_class.filter_diff_options(options) } + + it "should filter invalid options" do + expect(filtered_options).not_to have_key(:invalid_opt) + end + end + + context "with default options" do + let(:filtered_options) do + default_options = { max_size: 5, bad_opt: 1, ignore_whitespace: true } + described_class.filter_diff_options(options, default_options) + end + + it "should filter invalid options" do + expect(filtered_options).not_to have_key(:invalid_opt) + expect(filtered_options).not_to have_key(:bad_opt) + end + + it "should merge with default options" do + expect(filtered_options).to have_key(:ignore_whitespace) + end + + it "should override default options" do + expect(filtered_options).to have_key(:max_size) + expect(filtered_options[:max_size]).to eq(100) + end + end + end + + describe '#submodule?' do + before do + commit = repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e') + @diffs = commit.parents[0].diff(commit).patches + end + + it { expect(described_class.new(@diffs[0]).submodule?).to eq(false) } + it { expect(described_class.new(@diffs[1]).submodule?).to eq(true) } + end + + describe '#line_count' do + it 'returns the correct number of lines' do + diff = described_class.new(@rugged_diff) + + expect(diff.line_count).to eq(9) + end + end + + describe '#too_large?' do + it 'returns true for a diff that is too large' do + diff = described_class.new(diff: 'a' * 204800) + + expect(diff.too_large?).to eq(true) + end + + it 'returns false for a diff that is small enough' do + diff = described_class.new(diff: 'a') + + expect(diff.too_large?).to eq(false) + end + + it 'returns true for a diff that was explicitly marked as being too large' do + diff = described_class.new(diff: 'a') + + diff.prune_large_diff! + + expect(diff.too_large?).to eq(true) + end + end + + describe '#collapsed?' do + it 'returns false by default even on quite big diff' do + diff = described_class.new(diff: 'a' * 20480) + + expect(diff).not_to be_collapsed + end + + it 'returns false by default for a diff that is small enough' do + diff = described_class.new(diff: 'a') + + expect(diff).not_to be_collapsed + end + + it 'returns true for a diff that was explicitly marked as being collapsed' do + diff = described_class.new(diff: 'a') + + diff.prune_collapsed_diff! + + expect(diff).to be_collapsed + end + end + + describe '#collapsible?' do + it 'returns true for a diff that is quite large' do + diff = described_class.new(diff: 'a' * 20480) + + expect(diff).to be_collapsible + end + + it 'returns false for a diff that is small enough' do + diff = described_class.new(diff: 'a') + + expect(diff).not_to be_collapsible + end + end + + describe '#prune_collapsed_diff!' do + it 'prunes the diff' do + diff = described_class.new(diff: "foo\nbar") + + diff.prune_collapsed_diff! + + expect(diff.diff).to eq('') + expect(diff.line_count).to eq(0) + end + end +end diff --git a/spec/lib/gitlab/git/encoding_helper_spec.rb b/spec/lib/gitlab/git/encoding_helper_spec.rb new file mode 100644 index 00000000000..83311536893 --- /dev/null +++ b/spec/lib/gitlab/git/encoding_helper_spec.rb @@ -0,0 +1,84 @@ +require "spec_helper" + +describe Gitlab::Git::EncodingHelper do + let(:ext_class) { Class.new { extend Gitlab::Git::EncodingHelper } } + let(:binary_string) { File.join(SEED_REPOSITORY_PATH, 'gitlab_logo.png') } + + describe '#encode!' do + [ + [ + 'leaves ascii only string as is', + 'ascii only string', + 'ascii only string' + ], + [ + 'leaves valid utf8 string as is', + 'multibyte string №∑∉', + 'multibyte string №∑∉' + ], + [ + 'removes invalid bytes from ASCII-8bit encoded multibyte string. This can occur when a git diff match line truncates in the middle of a multibyte character. This occurs after the second word in this example. The test string is as short as we can get while still triggering the error condition when not looking at `detect[:confidence]`.', + "mu ns\xC3\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ".force_encoding('ASCII-8BIT'), + "mu ns\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ", + ], + ].each do |description, test_string, xpect| + it description do + expect(ext_class.encode!(test_string)).to eq(xpect) + end + end + + it 'leaves binary string as is' do + expect(ext_class.encode!(binary_string)).to eq(binary_string) + end + end + + describe '#encode_utf8' do + [ + [ + "encodes valid utf8 encoded string to utf8", + "λ, λ, λ".encode("UTF-8"), + "λ, λ, λ".encode("UTF-8"), + ], + [ + "encodes valid ASCII-8BIT encoded string to utf8", + "ascii only".encode("ASCII-8BIT"), + "ascii only".encode("UTF-8"), + ], + [ + "encodes valid ISO-8859-1 encoded string to utf8", + "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("ISO-8859-1", "UTF-8"), + "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("UTF-8"), + ], + ].each do |description, test_string, xpect| + it description do + r = ext_class.encode_utf8(test_string.force_encoding('UTF-8')) + expect(r).to eq(xpect) + expect(r.encoding.name).to eq('UTF-8') + end + end + end + + describe '#clean' do + [ + [ + 'leaves ascii only string as is', + 'ascii only string', + 'ascii only string' + ], + [ + 'leaves valid utf8 string as is', + 'multibyte string №∑∉', + 'multibyte string №∑∉' + ], + [ + 'removes invalid bytes from ASCII-8bit encoded multibyte string.', + "Lorem ipsum\xC3\n dolor sit amet, xy\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg".force_encoding('ASCII-8BIT'), + "Lorem ipsum\n dolor sit amet, xyàyùabcdùefg", + ], + ].each do |description, test_string, xpect| + it description do + expect(ext_class.encode!(test_string)).to eq(xpect) + end + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb new file mode 100644 index 00000000000..2a915bf426f --- /dev/null +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -0,0 +1,1184 @@ +require "spec_helper" + +describe Gitlab::Git::Repository, seed_helper: true do + include Gitlab::Git::EncodingHelper + + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + describe "Respond to" do + subject { repository } + + it { is_expected.to respond_to(:raw) } + it { is_expected.to respond_to(:rugged) } + it { is_expected.to respond_to(:root_ref) } + it { is_expected.to respond_to(:tags) } + end + + describe "#discover_default_branch" do + let(:master) { 'master' } + let(:feature) { 'feature' } + let(:feature2) { 'feature2' } + + it "returns 'master' when master exists" do + expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master]) + expect(repository.discover_default_branch).to eq('master') + end + + it "returns non-master when master exists but default branch is set to something else" do + File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/feature') + expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master]) + expect(repository.discover_default_branch).to eq('feature') + File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/master') + end + + it "returns a non-master branch when only one exists" do + expect(repository).to receive(:branch_names).at_least(:once).and_return([feature]) + expect(repository.discover_default_branch).to eq('feature') + end + + it "returns a non-master branch when more than one exists and master does not" do + expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, feature2]) + expect(repository.discover_default_branch).to eq('feature') + end + + it "returns nil when no branch exists" do + expect(repository).to receive(:branch_names).at_least(:once).and_return([]) + expect(repository.discover_default_branch).to be_nil + end + end + + describe :branch_names do + subject { repository.branch_names } + + it 'has SeedRepo::Repo::BRANCHES.size elements' do + expect(subject.size).to eq(SeedRepo::Repo::BRANCHES.size) + end + it { is_expected.to include("master") } + it { is_expected.not_to include("branch-from-space") } + end + + describe :tag_names do + subject { repository.tag_names } + + it { is_expected.to be_kind_of Array } + it 'has SeedRepo::Repo::TAGS.size elements' do + expect(subject.size).to eq(SeedRepo::Repo::TAGS.size) + end + + describe '#last' do + subject { super().last } + it { is_expected.to eq("v1.2.1") } + end + it { is_expected.to include("v1.0.0") } + it { is_expected.not_to include("v5.0.0") } + end + + shared_examples 'archive check' do |extenstion| + it { expect(metadata['ArchivePath']).to match(/tmp\/gitlab-git-test.git\/gitlab-git-test-master-#{SeedRepo::LastCommit::ID}/) } + it { expect(metadata['ArchivePath']).to end_with extenstion } + end + + describe :archive do + let(:metadata) { repository.archive_metadata('master', '/tmp') } + + it_should_behave_like 'archive check', '.tar.gz' + end + + describe :archive_zip do + let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip') } + + it_should_behave_like 'archive check', '.zip' + end + + describe :archive_bz2 do + let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2') } + + it_should_behave_like 'archive check', '.tar.bz2' + end + + describe :archive_fallback do + let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup') } + + it_should_behave_like 'archive check', '.tar.gz' + end + + describe :size do + subject { repository.size } + + it { is_expected.to be < 2 } + end + + describe :has_commits? do + it { expect(repository.has_commits?).to be_truthy } + end + + describe :empty? do + it { expect(repository.empty?).to be_falsey } + end + + describe :bare? do + it { expect(repository.bare?).to be_truthy } + end + + describe :heads do + let(:heads) { repository.heads } + subject { heads } + + it { is_expected.to be_kind_of Array } + + describe '#size' do + subject { super().size } + it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) } + end + + context :head do + subject { heads.first } + + describe '#name' do + subject { super().name } + it { is_expected.to eq("feature") } + end + + context :commit do + subject { heads.first.dereferenced_target.sha } + + it { is_expected.to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") } + end + end + end + + describe :ref_names do + let(:ref_names) { repository.ref_names } + subject { ref_names } + + it { is_expected.to be_kind_of Array } + + describe '#first' do + subject { super().first } + it { is_expected.to eq('feature') } + end + + describe '#last' do + subject { super().last } + it { is_expected.to eq('v1.2.1') } + end + end + + describe :search_files do + let(:results) { repository.search_files('rails', 'master') } + subject { results } + + it { is_expected.to be_kind_of Array } + + describe '#first' do + subject { super().first } + it { is_expected.to be_kind_of Gitlab::Git::BlobSnippet } + end + + context 'blob result' do + subject { results.first } + + describe '#ref' do + subject { super().ref } + it { is_expected.to eq('master') } + end + + describe '#filename' do + subject { super().filename } + it { is_expected.to eq('CHANGELOG') } + end + + describe '#startline' do + subject { super().startline } + it { is_expected.to eq(35) } + end + + describe '#data' do + subject { super().data } + it { is_expected.to include "Ability to filter by multiple labels" } + end + end + end + + context :submodules do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + context 'where repo has submodules' do + let(:submodules) { repository.submodules('master') } + let(:submodule) { submodules.first } + + it { expect(submodules).to be_kind_of Hash } + it { expect(submodules.empty?).to be_falsey } + + it 'should have valid data' do + expect(submodule).to eq([ + "six", { + "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d", + "path" => "six", + "url" => "git://github.com/randx/six.git" + } + ]) + end + + it 'should handle nested submodules correctly' do + nested = submodules['nested/six'] + expect(nested['path']).to eq('nested/six') + expect(nested['url']).to eq('git://github.com/randx/six.git') + expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196') + end + + it 'should handle deeply nested submodules correctly' do + nested = submodules['deeper/nested/six'] + expect(nested['path']).to eq('deeper/nested/six') + expect(nested['url']).to eq('git://github.com/randx/six.git') + expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196') + end + + it 'should not have an entry for an invalid submodule' do + expect(submodules).not_to have_key('invalid/path') + end + + it 'should not have an entry for an uncommited submodule dir' do + submodules = repository.submodules('fix-existing-submodule-dir') + expect(submodules).not_to have_key('submodule-existing-dir') + end + + it 'should handle tags correctly' do + submodules = repository.submodules('v1.2.1') + + expect(submodules.first).to eq([ + "six", { + "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d", + "path" => "six", + "url" => "git://github.com/randx/six.git" + } + ]) + end + end + + context 'where repo doesn\'t have submodules' do + let(:submodules) { repository.submodules('6d39438') } + it 'should return an empty hash' do + expect(submodules).to be_empty + end + end + end + + describe :commit_count do + it { expect(repository.commit_count("master")).to eq(25) } + it { expect(repository.commit_count("feature")).to eq(9) } + end + + describe "#reset" do + change_path = File.join(TEST_NORMAL_REPO_PATH, "CHANGELOG") + untracked_path = File.join(TEST_NORMAL_REPO_PATH, "UNTRACKED") + tracked_path = File.join(TEST_NORMAL_REPO_PATH, "files", "ruby", "popen.rb") + + change_text = "New changelog text" + untracked_text = "This file is untracked" + + reset_commit = SeedRepo::LastCommit::ID + + context "--hard" do + before(:all) do + # Modify a tracked file + File.open(change_path, "w") do |f| + f.write(change_text) + end + + # Add an untracked file to the working directory + File.open(untracked_path, "w") do |f| + f.write(untracked_text) + end + + @normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) + @normal_repo.reset("HEAD", :hard) + end + + it "should replace the working directory with the content of the index" do + File.open(change_path, "r") do |f| + expect(f.each_line.first).not_to eq(change_text) + end + + File.open(tracked_path, "r") do |f| + expect(f.each_line.to_a[8]).to include('raise RuntimeError, "System commands') + end + end + + it "should not touch untracked files" do + expect(File.exist?(untracked_path)).to be_truthy + end + + it "should move the HEAD to the correct commit" do + new_head = @normal_repo.rugged.head.target.oid + expect(new_head).to eq(reset_commit) + end + + it "should move the tip of the master branch to the correct commit" do + new_tip = @normal_repo.rugged.references["refs/heads/master"]. + target.oid + + expect(new_tip).to eq(reset_commit) + end + + after(:all) do + # Fast-forward to the original HEAD + FileUtils.rm_rf(TEST_NORMAL_REPO_PATH) + ensure_seeds + end + end + end + + describe "#checkout" do + new_branch = "foo_branch" + + context "-b" do + before(:all) do + @normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) + @normal_repo.checkout(new_branch, { b: true }, "origin/feature") + end + + it "should create a new branch" do + expect(@normal_repo.rugged.branches[new_branch]).not_to be_nil + end + + it "should move the HEAD to the correct commit" do + expect(@normal_repo.rugged.head.target.oid).to( + eq(@normal_repo.rugged.branches["origin/feature"].target.oid) + ) + end + + it "should refresh the repo's #heads collection" do + head_names = @normal_repo.heads.map { |h| h.name } + expect(head_names).to include(new_branch) + end + + after(:all) do + FileUtils.rm_rf(TEST_NORMAL_REPO_PATH) + ensure_seeds + end + end + + context "without -b" do + context "and specifying a nonexistent branch" do + it "should not do anything" do + normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) + + expect { normal_repo.checkout(new_branch) }.to raise_error(Rugged::ReferenceError) + expect(normal_repo.rugged.branches[new_branch]).to be_nil + expect(normal_repo.rugged.head.target.oid).to( + eq(normal_repo.rugged.branches["master"].target.oid) + ) + + head_names = normal_repo.heads.map { |h| h.name } + expect(head_names).not_to include(new_branch) + end + + after(:all) do + FileUtils.rm_rf(TEST_NORMAL_REPO_PATH) + ensure_seeds + end + end + + context "and with a valid branch" do + before(:all) do + @normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) + @normal_repo.rugged.branches.create("feature", "origin/feature") + @normal_repo.checkout("feature") + end + + it "should move the HEAD to the correct commit" do + expect(@normal_repo.rugged.head.target.oid).to( + eq(@normal_repo.rugged.branches["feature"].target.oid) + ) + end + + it "should update the working directory" do + File.open(File.join(TEST_NORMAL_REPO_PATH, ".gitignore"), "r") do |f| + expect(f.read.each_line.to_a).not_to include(".DS_Store\n") + end + end + + after(:all) do + FileUtils.rm_rf(TEST_NORMAL_REPO_PATH) + ensure_seeds + end + end + end + end + + describe "#delete_branch" do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo.delete_branch("feature") + end + + it "should remove the branch from the repo" do + expect(@repo.rugged.branches["feature"]).to be_nil + end + + it "should update the repo's #heads collection" do + expect(@repo.heads).not_to include("feature") + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + end + + describe "#create_branch" do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + end + + it "should create a new branch" do + expect(@repo.create_branch('new_branch', 'master')).not_to be_nil + end + + it "should create a new branch with the right name" do + expect(@repo.create_branch('another_branch', 'master').name).to eq('another_branch') + end + + it "should fail if we create an existing branch" do + @repo.create_branch('duplicated_branch', 'master') + expect{@repo.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") + end + + it "should fail if we create a branch from a non existing ref" do + expect{@repo.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + end + + describe "#remote_names" do + let(:remotes) { repository.remote_names } + + it "should have one entry: 'origin'" do + expect(remotes.size).to eq(1) + expect(remotes.first).to eq("origin") + end + end + + describe "#refs_hash" do + let(:refs) { repository.refs_hash } + + it "should have as many entries as branches and tags" do + expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS + # We flatten in case a commit is pointed at by more than one branch and/or tag + expect(refs.values.flatten.size).to eq(expected_refs.size) + end + end + + describe "#remote_delete" do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo.remote_delete("expendable") + end + + it "should remove the remote" do + expect(@repo.rugged.remotes).not_to include("expendable") + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + end + + describe "#remote_add" do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo.remote_add("new_remote", SeedHelper::GITLAB_URL) + end + + it "should add the remote" do + expect(@repo.rugged.remotes.each_name.to_a).to include("new_remote") + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + end + + describe "#remote_update" do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH) + end + + it "should add the remote" do + expect(@repo.rugged.remotes["expendable"].url).to( + eq(TEST_NORMAL_REPO_PATH) + ) + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + end + + describe "#log" do + commit_with_old_name = nil + commit_with_new_name = nil + rename_commit = nil + + before(:all) do + # Add new commits so that there's a renamed file in the commit history + repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + + commit_with_old_name = new_commit_edit_old_file(repo) + rename_commit = new_commit_move_file(repo) + commit_with_new_name = new_commit_edit_new_file(repo) + end + + context "where 'follow' == true" do + options = { ref: "master", follow: true } + + context "and 'path' is a directory" do + let(:log_commits) do + repository.log(options.merge(path: "encoding")) + end + + it "should not follow renames" do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_old_name) + end + end + + context "and 'path' is a file that matches the new filename" do + let(:log_commits) do + repository.log(options.merge(path: "encoding/CHANGELOG")) + end + + it "should follow renames" do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end + + context "and 'path' is a file that matches the old filename" do + let(:log_commits) do + repository.log(options.merge(path: "CHANGELOG")) + end + + it "should not follow renames" do + expect(log_commits).to include(commit_with_old_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_new_name) + end + end + + context "unknown ref" do + let(:log_commits) { repository.log(options.merge(ref: 'unknown')) } + + it "should return empty" do + expect(log_commits).to eq([]) + end + end + end + + context "where 'follow' == false" do + options = { follow: false } + + context "and 'path' is a directory" do + let(:log_commits) do + repository.log(options.merge(path: "encoding")) + end + + it "should not follow renames" do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_old_name) + end + end + + context "and 'path' is a file that matches the new filename" do + let(:log_commits) do + repository.log(options.merge(path: "encoding/CHANGELOG")) + end + + it "should not follow renames" do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_old_name) + end + end + + context "and 'path' is a file that matches the old filename" do + let(:log_commits) do + repository.log(options.merge(path: "CHANGELOG")) + end + + it "should not follow renames" do + expect(log_commits).to include(commit_with_old_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_new_name) + end + end + + context "and 'path' includes a directory that used to be a file" do + let(:log_commits) do + repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt")) + end + + it "should return a list of commits" do + expect(log_commits.size).to eq(1) + end + end + end + + context "compare results between log_by_walk and log_by_shell" do + let(:options) { { ref: "master" } } + let(:commits_by_walk) { repository.log(options).map(&:oid) } + let(:commits_by_shell) { repository.log(options.merge({ disable_walk: true })).map(&:oid) } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + + context "with limit" do + let(:options) { { ref: "master", limit: 1 } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + end + + context "with offset" do + let(:options) { { ref: "master", offset: 1 } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + end + + context "with skip_merges" do + let(:options) { { ref: "master", skip_merges: true } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + end + + context "with path" do + let(:options) { { ref: "master", path: "encoding" } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + + context "with follow" do + let(:options) { { ref: "master", path: "encoding", follow: true } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + end + end + end + + context "where provides 'after' timestamp" do + options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') } + + it "should returns commits on or after that timestamp" do + commits = repository.log(options) + + expect(commits.size).to be > 0 + satisfy do + commits.all? { |commit| commit.created_at >= options[:after] } + end + end + end + + context "where provides 'before' timestamp" do + options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') } + + it "should returns commits on or before that timestamp" do + commits = repository.log(options) + + expect(commits.size).to be > 0 + satisfy do + commits.all? { |commit| commit.created_at <= options[:before] } + end + end + end + + after(:all) do + # Erase our commits so other tests get the original repo + repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) + end + end + + describe "#commits_between" do + context 'two SHAs' do + let(:first_sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' } + let(:second_sha) { '0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326' } + + it 'returns the number of commits between' do + expect(repository.commits_between(first_sha, second_sha).count).to eq(3) + end + end + + context 'SHA and master branch' do + let(:sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' } + let(:branch) { 'master' } + + it 'returns the number of commits between a sha and a branch' do + expect(repository.commits_between(sha, branch).count).to eq(5) + end + + it 'returns the number of commits between a branch and a sha' do + expect(repository.commits_between(branch, sha).count).to eq(0) # sha is before branch + end + end + + context 'two branches' do + let(:first_branch) { 'feature' } + let(:second_branch) { 'master' } + + it 'returns the number of commits between' do + expect(repository.commits_between(first_branch, second_branch).count).to eq(17) + end + end + end + + describe '#count_commits_between' do + subject { repository.count_commits_between('feature', 'master') } + + it { is_expected.to eq(17) } + end + + describe "branch_names_contains" do + subject { repository.branch_names_contains(SeedRepo::LastCommit::ID) } + + it { is_expected.to include('master') } + it { is_expected.not_to include('feature') } + it { is_expected.not_to include('fix') } + end + + describe '#autocrlf' do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo.rugged.config['core.autocrlf'] = true + end + + it 'return the value of the autocrlf option' do + expect(@repo.autocrlf).to be(true) + end + + after(:all) do + @repo.rugged.config.delete('core.autocrlf') + end + end + + describe '#autocrlf=' do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + @repo.rugged.config['core.autocrlf'] = false + end + + it 'should set the autocrlf option to the provided option' do + @repo.autocrlf = :input + + File.open(File.join(TEST_MUTABLE_REPO_PATH, '.git', 'config')) do |config_file| + expect(config_file.read).to match('autocrlf = input') + end + end + + after(:all) do + @repo.rugged.config.delete('core.autocrlf') + end + end + + describe '#find_branch' do + it 'should return a Branch for master' do + branch = repository.find_branch('master') + + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') + end + + it 'should handle non-existent branch' do + branch = repository.find_branch('this-is-garbage') + + expect(branch).to eq(nil) + end + + it 'should reload Rugged::Repository and return master' do + expect(Rugged::Repository).to receive(:new).twice.and_call_original + + repository.find_branch('master') + branch = repository.find_branch('master', force_reload: true) + + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') + end + end + + describe '#branches with deleted branch' do + before(:each) do + ref = double() + allow(ref).to receive(:name) { 'bad-branch' } + allow(ref).to receive(:target) { raise Rugged::ReferenceError } + allow(repository.rugged).to receive(:branches) { [ref] } + end + + it 'should return empty branches' do + expect(repository.branches).to eq([]) + end + end + + describe '#branch_count' do + before(:each) do + valid_ref = double(:ref) + invalid_ref = double(:ref) + + allow(valid_ref).to receive_messages(name: 'master', target: double(:target)) + + allow(invalid_ref).to receive_messages(name: 'bad-branch') + allow(invalid_ref).to receive(:target) { raise Rugged::ReferenceError } + + allow(repository.rugged).to receive_messages(branches: [valid_ref, invalid_ref]) + end + + it 'returns the number of branches' do + expect(repository.branch_count).to eq(1) + end + end + + describe '#mkdir' do + let(:commit_options) do + { + author: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + committer: { + email: 'user@example.com', + name: 'Test User', + time: Time.now + }, + commit: { + message: 'Test message', + branch: 'refs/heads/fix', + } + } + end + + def generate_diff_for_path(path) + "diff --git a/#{path}/.gitkeep b/#{path}/.gitkeep +new file mode 100644 +index 0000000..e69de29 +--- /dev/null ++++ b/#{path}/.gitkeep\n" + end + + shared_examples 'mkdir diff check' do |path, expected_path| + it 'creates a directory' do + result = repository.mkdir(path, commit_options) + expect(result).not_to eq(nil) + + # Verify another mkdir doesn't create a directory that already exists + expect{ repository.mkdir(path, commit_options) }.to raise_error('Directory already exists') + end + end + + describe 'creates a directory in root directory' do + it_should_behave_like 'mkdir diff check', 'new_dir', 'new_dir' + end + + describe 'creates a directory in subdirectory' do + it_should_behave_like 'mkdir diff check', 'files/ruby/test', 'files/ruby/test' + end + + describe 'creates a directory in subdirectory with a slash' do + it_should_behave_like 'mkdir diff check', '/files/ruby/test2', 'files/ruby/test2' + end + + describe 'creates a directory in subdirectory with multiple slashes' do + it_should_behave_like 'mkdir diff check', '//files/ruby/test3', 'files/ruby/test3' + end + + describe 'handles relative paths' do + it_should_behave_like 'mkdir diff check', 'files/ruby/../test_relative', 'files/test_relative' + end + + describe 'creates nested directories' do + it_should_behave_like 'mkdir diff check', 'files/missing/test', 'files/missing/test' + end + + it 'does not attempt to create a directory with invalid relative path' do + expect{ repository.mkdir('../files/missing/test', commit_options) }.to raise_error('Invalid path') + end + + it 'does not attempt to overwrite a file' do + expect{ repository.mkdir('README.md', commit_options) }.to raise_error('Directory already exists as a file') + end + + it 'does not attempt to overwrite a directory' do + expect{ repository.mkdir('files', commit_options) }.to raise_error('Directory already exists') + end + end + + describe "#ls_files" do + let(:master_file_paths) { repository.ls_files("master") } + let(:not_existed_branch) { repository.ls_files("not_existed_branch") } + + it "read every file paths of master branch" do + expect(master_file_paths.length).to equal(40) + end + + it "reads full file paths of master branch" do + expect(master_file_paths).to include("files/html/500.html") + end + + it "dose not read submodule directory and empty directory of master branch" do + expect(master_file_paths).not_to include("six") + end + + it "does not include 'nil'" do + expect(master_file_paths).not_to include(nil) + end + + it "returns empty array when not existed branch" do + expect(not_existed_branch.length).to equal(0) + end + end + + describe "#copy_gitattributes" do + let(:attributes_path) { File.join(TEST_REPO_PATH, 'info/attributes') } + + it "raises an error with invalid ref" do + expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef) + end + + context "with no .gitattrbutes" do + before(:each) do + repository.copy_gitattributes("master") + end + + it "does not have an info/attributes" do + expect(File.exist?(attributes_path)).to be_falsey + end + + after(:each) do + FileUtils.rm_rf(attributes_path) + end + end + + context "with .gitattrbutes" do + before(:each) do + repository.copy_gitattributes("gitattributes") + end + + it "has an info/attributes" do + expect(File.exist?(attributes_path)).to be_truthy + end + + it "has the same content in info/attributes as .gitattributes" do + contents = File.open(attributes_path, "rb") { |f| f.read } + expect(contents).to eq("*.md binary\n") + end + + after(:each) do + FileUtils.rm_rf(attributes_path) + end + end + + context "with updated .gitattrbutes" do + before(:each) do + repository.copy_gitattributes("gitattributes") + repository.copy_gitattributes("gitattributes-updated") + end + + it "has an info/attributes" do + expect(File.exist?(attributes_path)).to be_truthy + end + + it "has the updated content in info/attributes" do + contents = File.read(attributes_path) + expect(contents).to eq("*.txt binary\n") + end + + after(:each) do + FileUtils.rm_rf(attributes_path) + end + end + + context "with no .gitattrbutes in HEAD but with previous info/attributes" do + before(:each) do + repository.copy_gitattributes("gitattributes") + repository.copy_gitattributes("master") + end + + it "does not have an info/attributes" do + expect(File.exist?(attributes_path)).to be_falsey + end + + after(:each) do + FileUtils.rm_rf(attributes_path) + end + end + end + + describe '#diffable' do + info_dir_path = attributes_path = File.join(TEST_REPO_PATH, 'info') + attributes_path = File.join(info_dir_path, 'attributes') + + before(:all) do + FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path) + File.write(attributes_path, "*.md -diff\n") + end + + it "should return true for files which are text and do not have attributes" do + blob = Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'LICENSE' + ) + expect(repository.diffable?(blob)).to be_truthy + end + + it "should return false for binary files which do not have attributes" do + blob = Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'files/images/logo-white.png' + ) + expect(repository.diffable?(blob)).to be_falsey + end + + it "should return false for text files which have been marked as not being diffable in attributes" do + blob = Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'README.md' + ) + expect(repository.diffable?(blob)).to be_falsey + end + + after(:all) do + FileUtils.rm_rf(info_dir_path) + end + end + + describe '#tag_exists?' do + it 'returns true for an existing tag' do + tag = repository.tag_names.first + + expect(repository.tag_exists?(tag)).to eq(true) + end + + it 'returns false for a non-existing tag' do + expect(repository.tag_exists?('v9000')).to eq(false) + end + end + + describe '#branch_exists?' do + it 'returns true for an existing branch' do + expect(repository.branch_exists?('master')).to eq(true) + end + + it 'returns false for a non-existing branch' do + expect(repository.branch_exists?('kittens')).to eq(false) + end + + it 'returns false when using an invalid branch name' do + expect(repository.branch_exists?('.bla')).to eq(false) + end + end + + describe '#local_branches' do + before(:all) do + @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + + it 'returns the local branches' do + create_remote_branch('joe', 'remote_branch', 'master') + @repo.create_branch('local_branch', 'master') + + expect(@repo.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false) + expect(@repo.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) + end + end + + def create_remote_branch(remote_name, branch_name, source_branch_name) + source_branch = @repo.branches.find { |branch| branch.name == source_branch_name } + rugged = @repo.rugged + rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha) + end + + # Build the options hash that's passed to Rugged::Commit#create + def commit_options(repo, index, message) + options = {} + options[:tree] = index.write_tree(repo) + options[:author] = { + email: "test@example.com", + name: "Test Author", + time: Time.gm(2014, "mar", 3, 20, 15, 1) + } + options[:committer] = { + email: "test@example.com", + name: "Test Author", + time: Time.gm(2014, "mar", 3, 20, 15, 1) + } + options[:message] ||= message + options[:parents] = repo.empty? ? [] : [repo.head.target].compact + options[:update_ref] = "HEAD" + + options + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the + # contents of CHANGELOG with a single new line of text. + def new_commit_edit_old_file(repo) + oid = repo.write("I replaced the changelog with this text", :blob) + index = repo.index + index.read_tree(repo.head.target.tree) + index.add(path: "CHANGELOG", oid: oid, mode: 0100644) + + options = commit_options( + repo, + index, + "Edit CHANGELOG in its original location" + ) + + sha = Rugged::Commit.create(repo, options) + repo.lookup(sha) + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the + # contents of encoding/CHANGELOG with new text. + def new_commit_edit_new_file(repo) + oid = repo.write("I'm a new changelog with different text", :blob) + index = repo.index + index.read_tree(repo.head.target.tree) + index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644) + + options = commit_options(repo, index, "Edit encoding/CHANGELOG") + + sha = Rugged::Commit.create(repo, options) + repo.lookup(sha) + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Moves the + # CHANGELOG file to the encoding/ directory. + def new_commit_move_file(repo) + blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid] + file_content = repo.lookup(blob_oid).content + oid = repo.write(file_content, :blob) + index = repo.index + index.read_tree(repo.head.target.tree) + index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644) + index.remove("CHANGELOG") + + options = commit_options(repo, index, "Move CHANGELOG to encoding/") + + sha = Rugged::Commit.create(repo, options) + repo.lookup(sha) + end +end diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb new file mode 100644 index 00000000000..1f9c987be0b --- /dev/null +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe Gitlab::Git::RevList, lib: true do + let(:project) { create(:project) } + + context "validations" do + described_class::ALLOWED_VARIABLES.each do |var| + context var do + it "accepts values starting with the project repo path" do + env = { var => "#{project.repository.path_to_repo}/objects" } + rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) + + expect(rev_list).to be_valid + end + + it "rejects values starting not with the project repo path" do + env = { var => "/some/other/path" } + rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) + + expect(rev_list).not_to be_valid + end + + it "rejects values containing the project repo path but not starting with it" do + env = { var => "/some/other/path/#{project.repository.path_to_repo}" } + rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) + + expect(rev_list).not_to be_valid + end + + it "ignores nil values" do + env = { var => nil } + rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) + + expect(rev_list).to be_valid + end + end + end + end + + context "#execute" do + let(:env) { { "GIT_OBJECT_DIRECTORY" => project.repository.path_to_repo } } + let(:rev_list) { Gitlab::Git::RevList.new('oldrev', 'newrev', project: project, env: env) } + + it "calls out to `popen` without environment variables if the record is invalid" do + allow(rev_list).to receive(:valid?).and_return(false) + + expect(Open3).to receive(:popen3).with(hash_excluding(env), any_args) + + rev_list.execute + end + + it "calls out to `popen` with environment variables if the record is valid" do + allow(rev_list).to receive(:valid?).and_return(true) + + expect(Open3).to receive(:popen3).with(hash_including(env), any_args) + + rev_list.execute + end + end +end diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb new file mode 100644 index 00000000000..ad469e94735 --- /dev/null +++ b/spec/lib/gitlab/git/tag_spec.rb @@ -0,0 +1,25 @@ +require "spec_helper" + +describe Gitlab::Git::Tag, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + + describe 'first tag' do + let(:tag) { repository.tags.first } + + it { expect(tag.name).to eq("v1.0.0") } + it { expect(tag.target).to eq("f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8") } + it { expect(tag.dereferenced_target.sha).to eq("6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9") } + it { expect(tag.message).to eq("Release") } + end + + describe 'last tag' do + let(:tag) { repository.tags.last } + + it { expect(tag.name).to eq("v1.2.1") } + it { expect(tag.target).to eq("2ac1f24e253e08135507d0830508febaaccf02ee") } + it { expect(tag.dereferenced_target.sha).to eq("fa1b1e6c004a68b7d8763b86455da9e6b23e36d6") } + it { expect(tag.message).to eq("Version 1.2.1") } + end + + it { expect(repository.tags.size).to eq(SeedRepo::Repo::TAGS.size) } +end diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb new file mode 100644 index 00000000000..688e2a75373 --- /dev/null +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -0,0 +1,76 @@ +require "spec_helper" + +describe Gitlab::Git::Tree, seed_helper: true do + context :repo do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) } + + it { expect(tree).to be_kind_of Array } + it { expect(tree.empty?).to be_falsey } + it { expect(tree.select(&:dir?).size).to eq(2) } + it { expect(tree.select(&:file?).size).to eq(10) } + it { expect(tree.select(&:submodule?).size).to eq(2) } + + describe :dir do + let(:dir) { tree.select(&:dir?).first } + + it { expect(dir).to be_kind_of Gitlab::Git::Tree } + it { expect(dir.id).to eq('3c122d2b7830eca25235131070602575cf8b41a1') } + it { expect(dir.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(dir.name).to eq('encoding') } + it { expect(dir.path).to eq('encoding') } + + context :subdir do + let(:subdir) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files').first } + + it { expect(subdir).to be_kind_of Gitlab::Git::Tree } + it { expect(subdir.id).to eq('a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba') } + 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') } + end + + context :subdir_file do + let(:subdir_file) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first } + + it { expect(subdir_file).to be_kind_of Gitlab::Git::Tree } + it { expect(subdir_file.id).to eq('7e3e39ebb9b2bf433b4ad17313770fbe4051649c') } + 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') } + end + end + + describe :file do + let(:file) { tree.select(&:file?).first } + + it { expect(file).to be_kind_of Gitlab::Git::Tree } + it { expect(file.id).to eq('dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82') } + it { expect(file.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(file.name).to eq('.gitignore') } + end + + describe :readme do + let(:file) { tree.select(&:readme?).first } + + it { expect(file).to be_kind_of Gitlab::Git::Tree } + it { expect(file.name).to eq('README.md') } + end + + describe :contributing do + let(:file) { tree.select(&:contributing?).first } + + it { expect(file).to be_kind_of Gitlab::Git::Tree } + it { expect(file.name).to eq('CONTRIBUTING.md') } + end + + describe :submodule do + let(:submodule) { tree.select(&:submodule?).first } + + it { expect(submodule).to be_kind_of Gitlab::Git::Tree } + it { expect(submodule.id).to eq('79bceae69cb5750d6567b223597999bfa91cb3b9') } + it { expect(submodule.commit_id).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + it { expect(submodule.name).to eq('gitlab-shell') } + end + end +end diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb new file mode 100644 index 00000000000..8d43b570e98 --- /dev/null +++ b/spec/lib/gitlab/git/util_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Gitlab::Git::Util do + describe :count_lines do + [ + ["", 0], + ["foo", 1], + ["foo\n", 1], + ["foo\n\n", 2], + ].each do |string, line_count| + it "counts #{line_count} lines in #{string.inspect}" do + expect(Gitlab::Git::Util.count_lines(string)).to eq(line_count) + end + end + end +end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index f1d0a190002..44b84afde47 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -50,7 +50,7 @@ describe Gitlab::GitAccess, lib: true do end end - describe 'download_access_check' do + describe '#check_download_access!' do subject { access.check('git-upload-pack', '_any') } describe 'master permissions' do @@ -82,7 +82,7 @@ describe Gitlab::GitAccess, lib: true do end end - describe 'without acccess to project' do + describe 'without access to project' do context 'pull code' do it { expect(subject.allowed?).to be_falsey } end @@ -112,7 +112,7 @@ describe Gitlab::GitAccess, lib: true do end describe 'deploy key permissions' do - let(:key) { create(:deploy_key) } + let(:key) { create(:deploy_key, user: user) } let(:actor) { key } context 'pull code' do @@ -136,7 +136,7 @@ describe Gitlab::GitAccess, lib: true do end context 'from private project' do - let(:project) { create(:project, :internal) } + let(:project) { create(:project, :private) } it { expect(subject).not_to be_allowed } end @@ -183,7 +183,7 @@ describe Gitlab::GitAccess, lib: true do end end - describe 'push_access_check' do + describe '#check_push_access!' do before { merge_into_protected_branch } let(:unprotected_branch) { FFaker::Internet.user_name } @@ -231,7 +231,7 @@ describe Gitlab::GitAccess, lib: true do permissions_matrix[role].each do |action, allowed| context action do - subject { access.push_access_check(changes[action]) } + subject { access.send(:check_push_access!, changes[action]) } it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey } end end @@ -353,13 +353,13 @@ describe Gitlab::GitAccess, lib: true do end end - shared_examples 'can not push code' do + shared_examples 'pushing code' do |can| subject { access.check('git-receive-pack', '_any') } context 'when project is authorized' do before { authorize } - it { expect(subject).not_to be_allowed } + it { expect(subject).public_send(can, be_allowed) } end context 'when unauthorized' do @@ -386,7 +386,7 @@ describe Gitlab::GitAccess, lib: true do describe 'build authentication abilities' do let(:authentication_abilities) { build_authentication_abilities } - it_behaves_like 'can not push code' do + it_behaves_like 'pushing code', :not_to do def authorize project.team << [user, :reporter] end @@ -394,12 +394,26 @@ describe Gitlab::GitAccess, lib: true do end describe 'deploy key permissions' do - let(:key) { create(:deploy_key) } + let(:key) { create(:deploy_key, user: user, can_push: can_push) } let(:actor) { key } - it_behaves_like 'can not push code' do - def authorize - key.projects << project + context 'when deploy_key can push' do + let(:can_push) { true } + + it_behaves_like 'pushing code', :to do + def authorize + key.projects << project + end + end + end + + context 'when deploy_key cannot push' do + let(:can_push) { false } + + it_behaves_like 'pushing code', :not_to do + def authorize + key.projects << project + end end end end diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 576aa5c366f..a5d172233cc 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -26,4 +26,29 @@ describe Gitlab::GitAccessWiki, lib: true do def changes ['6f6d7e7ed 570e7b2ab refs/heads/master'] end + + describe '#access_check_download!' do + subject { access.check('git-upload-pack', '_any') } + + before do + project.team << [user, :developer] + end + + context 'when wiki feature is enabled' do + it 'give access to download wiki code' do + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED) + + expect(subject.allowed?).to be_truthy + end + end + + context 'when wiki feature is disabled' do + it 'does not give access to download wiki code' do + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) + + expect(subject.allowed?).to be_falsey + expect(subject.message).to match(/You are not allowed to download code/) + end + end + end end diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb index e5300dbba1e..462caa5b5fe 100644 --- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -49,14 +49,20 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do end describe '#valid?' do - it 'returns true when raw repo is present' do + it 'returns true when raw sha and ref are present' do branch = described_class.new(project, double(raw)) expect(branch.valid?).to eq true end - it 'returns false when raw repo is blank' do - branch = described_class.new(project, double(raw.merge(repo: nil))) + it 'returns false when raw sha is blank' do + branch = described_class.new(project, double(raw.merge(sha: nil))) + + expect(branch.valid?).to eq false + end + + it 'returns false when raw ref is blank' do + branch = described_class.new(project, double(raw.merge(ref: nil))) expect(branch.valid?).to eq false end diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index e829b936343..21f2a9e225b 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -45,20 +45,46 @@ describe Gitlab::GithubImport::Client, lib: true do end end - context 'when provider does not specity an API endpoint' do - it 'uses GitHub root API endpoint' do - expect(client.api.api_endpoint).to eq 'https://api.github.com/' + describe '#api_endpoint' do + context 'when provider does not specity an API endpoint' do + it 'uses GitHub root API endpoint' do + expect(client.api.api_endpoint).to eq 'https://api.github.com/' + end end - end - context 'when provider specify a custom API endpoint' do - before do - github_provider['args']['client_options']['site'] = 'https://github.company.com/' + context 'when provider specify a custom API endpoint' do + before do + github_provider['args']['client_options']['site'] = 'https://github.company.com/' + end + + it 'uses the custom API endpoint' do + expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options) + expect(client.api.api_endpoint).to eq 'https://github.company.com/' + end + end + + context 'when given a host' do + subject(:client) { described_class.new(token, host: 'https://try.gitea.io/') } + + it 'builds a endpoint with the given host and the default API version' do + expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/' + end end - it 'uses the custom API endpoint' do - expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options) - expect(client.api.api_endpoint).to eq 'https://github.company.com/' + context 'when given an API version' do + subject(:client) { described_class.new(token, api_version: 'v3') } + + it 'does not use the API version without a host' do + expect(client.api.api_endpoint).to eq 'https://api.github.com/' + end + end + + context 'when given a host and version' do + subject(:client) { described_class.new(token, host: 'https://try.gitea.io/', api_version: 'v3') } + + it 'builds a endpoint with the given options' do + expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/' + end end end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 7478f86bd28..72421832ffc 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -1,172 +1,251 @@ require 'spec_helper' describe Gitlab::GithubImport::Importer, lib: true do - describe '#execute' do + shared_examples 'Gitlab::GithubImport::Importer#execute' do + let(:expected_not_called) { [] } + before do - allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + allow(project).to receive(:import_data).and_return(double.as_null_object) end - context 'when an error occurs' do - let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) } - let(:octocat) { double(id: 123456, login: 'octocat') } - let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } - let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } - let(:repository) { double(id: 1, fork: false) } - let(:source_sha) { create(:commit, project: project).id } - let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } - let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } - let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } - - let(:label1) do - double( - name: 'Bug', - color: 'ff0000', - url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug' - ) - end + it 'calls import methods' do + importer = described_class.new(project) - let(:label2) do - double( - name: nil, - color: 'ff0000', - url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug' - ) - end + expected_called = [ + :import_labels, :import_milestones, :import_pull_requests, :import_issues, + :import_wiki, :import_releases, :handle_errors + ] - let(:milestone) do - double( - number: 1347, - state: 'open', - title: '1.0', - description: 'Version 1.0', - due_on: nil, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/milestones/1' - ) - end + expected_called -= expected_not_called - let(:issue1) do - double( - number: 1347, - milestone: nil, - state: 'open', - title: 'Found a bug', - body: "I'm having a problem with this.", - assignee: nil, - user: octocat, - comments: 0, - pull_request: nil, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347', - labels: [double(name: 'Label #1')], - ) - end + aggregate_failures do + expected_called.each do |method_name| + expect(importer).to receive(method_name) + end - let(:issue2) do - double( - number: 1348, - milestone: nil, - state: 'open', - title: nil, - body: "I'm having a problem with this.", - assignee: nil, - user: octocat, - comments: 0, - pull_request: nil, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348', - labels: [double(name: 'Label #2')], - ) - end + expect(importer).to receive(:import_comments).with(:issues) + expect(importer).to receive(:import_comments).with(:pull_requests) - let(:pull_request) do - double( - number: 1347, - milestone: nil, - state: 'open', - title: 'New feature', - body: 'Please pull these awesome changes', - head: source_branch, - base: target_branch, - assignee: nil, - user: octocat, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - merged_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347', - labels: [double(name: 'Label #3')], - ) + expected_not_called.each do |method_name| + expect(importer).not_to receive(method_name) + end end - let(:release1) do - double( - tag_name: 'v1.0.0', - name: 'First release', - body: 'Release v1.0.0', - draft: false, - created_at: created_at, - updated_at: updated_at, - url: 'https://api.github.com/repos/octocat/Hello-World/releases/1' - ) + importer.execute + end + end + + shared_examples 'Gitlab::GithubImport::Importer#execute an error occurs' do + before do + allow(project).to receive(:import_data).and_return(double.as_null_object) + + allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + + allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) + allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) + + allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2]) + allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone]) + allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) + allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request]) + allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([]) + allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([]) + allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) + allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) + end + let(:octocat) { double(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + let(:label1) do + double( + name: 'Bug', + color: 'ff0000', + url: "#{api_root}/repos/octocat/Hello-World/labels/bug" + ) + end + + let(:label2) do + double( + name: nil, + color: 'ff0000', + url: "#{api_root}/repos/octocat/Hello-World/labels/bug" + ) + end + + let(:milestone) do + double( + id: 1347, # For Gitea + number: 1347, + state: 'open', + title: '1.0', + description: 'Version 1.0', + due_on: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/milestones/1" + ) + end + + let(:issue1) do + double( + number: 1347, + milestone: nil, + state: 'open', + title: 'Found a bug', + body: "I'm having a problem with this.", + assignee: nil, + user: octocat, + comments: 0, + pull_request: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/issues/1347", + labels: [double(name: 'Label #1')] + ) + end + + let(:issue2) do + double( + number: 1348, + milestone: nil, + state: 'open', + title: nil, + body: "I'm having a problem with this.", + assignee: nil, + user: octocat, + comments: 0, + pull_request: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/issues/1348", + labels: [double(name: 'Label #2')] + ) + end + + let(:repository) { double(id: 1, fork: false) } + let(:source_sha) { create(:commit, project: project).id } + let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } + let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } + let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } + let(:pull_request) do + double( + number: 1347, + milestone: nil, + state: 'open', + title: 'New feature', + body: 'Please pull these awesome changes', + head: source_branch, + base: target_branch, + assignee: nil, + user: octocat, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + merged_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/pulls/1347", + labels: [double(name: 'Label #2')] + ) + end + + let(:release1) do + double( + tag_name: 'v1.0.0', + name: 'First release', + body: 'Release v1.0.0', + draft: false, + created_at: created_at, + updated_at: updated_at, + url: "#{api_root}/repos/octocat/Hello-World/releases/1" + ) + end + + let(:release2) do + double( + tag_name: 'v2.0.0', + name: 'Second release', + body: nil, + draft: false, + created_at: created_at, + updated_at: updated_at, + url: "#{api_root}/repos/octocat/Hello-World/releases/2" + ) + end + + it 'returns true' do + expect(described_class.new(project).execute).to eq true + end + + it 'does not raise an error' do + expect { described_class.new(project).execute }.not_to raise_error + end + + it 'stores error messages' do + error = { + message: 'The remote data could not be fully imported.', + errors: [ + { type: :label, url: "#{api_root}/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, + { type: :issue, url: "#{api_root}/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" }, + { type: :wiki, errors: "Gitlab::Shell::Error" } + ] + } + + unless project.gitea_import? + error[:errors] << { type: :release, url: "#{api_root}/repos/octocat/Hello-World/releases/2", errors: "Validation failed: Description can't be blank" } end - let(:release2) do - double( - tag_name: 'v2.0.0', - name: 'Second release', - body: nil, - draft: false, - created_at: created_at, - updated_at: updated_at, - url: 'https://api.github.com/repos/octocat/Hello-World/releases/2' + described_class.new(project).execute + + expect(project.import_error).to eq error.to_json + end + end + + let(:project) { create(:project, import_url: "#{repo_root}/octocat/Hello-World.git", wiki_access_level: ProjectFeature::DISABLED) } + let(:credentials) { { user: 'joe' } } + + context 'when importing a GitHub project' do + let(:api_root) { 'https://api.github.com' } + let(:repo_root) { 'https://github.com' } + + it_behaves_like 'Gitlab::GithubImport::Importer#execute' + it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' + + describe '#client' do + it 'instantiates a Client' do + allow(project).to receive(:import_data).and_return(double(credentials: credentials)) + expect(Gitlab::GithubImport::Client).to receive(:new).with( + credentials[:user], + {} ) - end - before do - allow(project).to receive(:import_data).and_return(double.as_null_object) - allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) - allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2]) - allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone]) - allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) - allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request]) - allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([]) - allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([]) - allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) - allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) - allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) + described_class.new(project).client end + end + end - it 'returns true' do - expect(described_class.new(project).execute).to eq true - end + context 'when importing a Gitea project' do + let(:api_root) { 'https://try.gitea.io/api/v1' } + let(:repo_root) { 'https://try.gitea.io' } + before do + project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git") + end - it 'does not raise an error' do - expect { described_class.new(project).execute }.not_to raise_error - end + it_behaves_like 'Gitlab::GithubImport::Importer#execute' do + let(:expected_not_called) { [:import_releases] } + end + it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' + + describe '#client' do + it 'instantiates a Client' do + allow(project).to receive(:import_data).and_return(double(credentials: credentials)) + expect(Gitlab::GithubImport::Client).to receive(:new).with( + credentials[:user], + { host: "#{repo_root}:443/foo", api_version: 'v1' } + ) - it 'stores error messages' do - error = { - message: 'The remote data could not be fully imported.', - errors: [ - { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, - { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" }, - { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." }, - { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." }, - { type: :wiki, errors: "Gitlab::Shell::Error" }, - { type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" } - ] - } - - described_class.new(project).execute - - expect(project.import_error).to eq error.to_json + described_class.new(project).client end end end diff --git a/spec/lib/gitlab/github_import/issuable_formatter_spec.rb b/spec/lib/gitlab/github_import/issuable_formatter_spec.rb new file mode 100644 index 00000000000..6bc5f98ed2c --- /dev/null +++ b/spec/lib/gitlab/github_import/issuable_formatter_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::IssuableFormatter, lib: true do + let(:raw_data) do + double(number: 42) + end + let(:project) { double(import_type: 'github') } + let(:issuable_formatter) { described_class.new(project, raw_data) } + + describe '#project_association' do + it { expect { issuable_formatter.project_association }.to raise_error(NotImplementedError) } + end + + describe '#number' do + it { expect(issuable_formatter.number).to eq(42) } + end + + describe '#find_condition' do + it { expect(issuable_formatter.find_condition).to eq({ iid: 42 }) } + end +end diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb index c2f1f6b91a1..e31ed9c1fa0 100644 --- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -23,9 +23,9 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do } end - subject(:issue) { described_class.new(project, raw_data)} + subject(:issue) { described_class.new(project, raw_data) } - describe '#attributes' do + shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do context 'when issue is open' do let(:raw_data) { double(base_data.merge(state: 'open')) } @@ -83,7 +83,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end context 'when it has a milestone' do - let(:milestone) { double(number: 45) } + let(:milestone) { double(id: 42, number: 42) } let(:raw_data) { double(base_data.merge(milestone: milestone)) } it 'returns nil when milestone does not exist' do @@ -91,7 +91,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end it 'returns milestone when it exists' do - milestone = create(:milestone, project: project, iid: 45) + milestone = create(:milestone, project: project, iid: 42) expect(issue.attributes.fetch(:milestone)).to eq milestone end @@ -118,6 +118,28 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end end + shared_examples 'Gitlab::GithubImport::IssueFormatter#number' do + let(:raw_data) { double(base_data.merge(number: 1347)) } + + it 'returns issue number' do + expect(issue.number).to eq 1347 + end + end + + context 'when importing a GitHub project' do + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number' + end + + context 'when importing a Gitea project' do + before do + project.update(import_type: 'gitea') + end + + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number' + end + describe '#has_comments?' do context 'when number of comments is greater than zero' do let(:raw_data) { double(base_data.merge(comments: 1)) } @@ -136,28 +158,20 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end end - describe '#number' do - let(:raw_data) { double(base_data.merge(number: 1347)) } - - it 'returns pull request number' do - expect(issue.number).to eq 1347 - end - end - - describe '#valid?' do + describe '#pull_request?' do context 'when mention a pull request' do let(:raw_data) { double(base_data.merge(pull_request: double)) } - it 'returns false' do - expect(issue.valid?).to eq false + it 'returns true' do + expect(issue.pull_request?).to eq true end end context 'when does not mention a pull request' do let(:raw_data) { double(base_data.merge(pull_request: nil)) } - it 'returns true' do - expect(issue.valid?).to eq true + it 'returns false' do + expect(issue.pull_request?).to eq false end end end diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb index 09337c99a07..6d38041c468 100644 --- a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb @@ -6,7 +6,6 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:base_data) do { - number: 1347, state: 'open', title: '1.0', description: 'Version 1.0', @@ -16,12 +15,15 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do closed_at: nil } end + let(:iid_attr) { :number } - subject(:formatter) { described_class.new(project, raw_data)} + subject(:formatter) { described_class.new(project, raw_data) } + + shared_examples 'Gitlab::GithubImport::MilestoneFormatter#attributes' do + let(:data) { base_data.merge(iid_attr => 1347) } - describe '#attributes' do context 'when milestone is open' do - let(:raw_data) { double(base_data.merge(state: 'open')) } + let(:raw_data) { double(data.merge(state: 'open')) } it 'returns formatted attributes' do expected = { @@ -40,7 +42,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do end context 'when milestone is closed' do - let(:raw_data) { double(base_data.merge(state: 'closed')) } + let(:raw_data) { double(data.merge(state: 'closed')) } it 'returns formatted attributes' do expected = { @@ -60,7 +62,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do context 'when milestone has a due date' do let(:due_date) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { double(base_data.merge(due_on: due_date)) } + let(:raw_data) { double(data.merge(due_on: due_date)) } it 'returns formatted attributes' do expected = { @@ -78,4 +80,17 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do end end end + + context 'when importing a GitHub project' do + it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes' + end + + context 'when importing a Gitea project' do + let(:iid_attr) { :id } + before do + project.update(import_type: 'gitea') + end + + it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes' + end end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 302f0fc0623..2b3256edcb2 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -32,9 +32,9 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do } end - subject(:pull_request) { described_class.new(project, raw_data)} + subject(:pull_request) { described_class.new(project, raw_data) } - describe '#attributes' do + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do context 'when pull request is open' do let(:raw_data) { double(base_data.merge(state: 'open')) } @@ -149,7 +149,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when it has a milestone' do - let(:milestone) { double(number: 45) } + let(:milestone) { double(id: 42, number: 42) } let(:raw_data) { double(base_data.merge(milestone: milestone)) } it 'returns nil when milestone does not exist' do @@ -157,22 +157,22 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end it 'returns milestone when it exists' do - milestone = create(:milestone, project: project, iid: 45) + milestone = create(:milestone, project: project, iid: 42) expect(pull_request.attributes.fetch(:milestone)).to eq milestone end end end - describe '#number' do - let(:raw_data) { double(base_data.merge(number: 1347)) } + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#number' do + let(:raw_data) { double(base_data) } it 'returns pull request number' do expect(pull_request.number).to eq 1347 end end - describe '#source_branch_name' do + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' do context 'when source branch exists' do let(:raw_data) { double(base_data) } @@ -190,7 +190,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end - describe '#target_branch_name' do + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do context 'when source branch exists' do let(:raw_data) { double(base_data) } @@ -208,6 +208,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + context 'when importing a GitHub project' do + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' + end + + context 'when importing a Gitea project' do + before do + project.update(import_type: 'gitea') + end + + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' + end + describe '#valid?' do context 'when source, and target repos are not a fork' do let(:raw_data) { double(base_data) } diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb index 47d6f1007d1..bb758a8a202 100644 --- a/spec/lib/gitlab/identifier_spec.rb +++ b/spec/lib/gitlab/identifier_spec.rb @@ -40,7 +40,7 @@ describe Gitlab::Identifier do describe '#identify_using_commit' do it "returns the User for an existing commit author's Email address" do - commit = double(:commit, author_email: user.email) + commit = double(:commit, author: user, author_email: user.email) expect(project).to receive(:commit).with('123').and_return(commit) @@ -62,10 +62,9 @@ describe Gitlab::Identifier do end it 'caches the found users per Email' do - commit = double(:commit, author_email: user.email) + commit = double(:commit, author: user, author_email: user.email) expect(project).to receive(:commit).with('123').twice.and_return(commit) - expect(User).to receive(:find_by).once.and_call_original 2.times do expect(identifier.identify_using_commit(project, '123')).to eq(user) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 02b11bd999a..7fb6829f582 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -15,6 +15,7 @@ issues: - events - merge_requests_closing_issues - metrics +- timelogs events: - author - project @@ -77,6 +78,7 @@ merge_requests: - events - merge_requests_closing_issues - metrics +- timelogs merge_request_diff: - merge_request pipelines: @@ -116,6 +118,7 @@ project: - base_tags - tag_taggings - tags +- chat_services - creator - group - namespace @@ -127,6 +130,8 @@ project: - emails_on_push_service - builds_email_service - pipelines_email_service +- mattermost_slash_commands_service +- slack_slash_commands_service - irker_service - pivotaltracker_service - hipchat_service @@ -135,6 +140,7 @@ project: - asana_service - gemnasium_service - slack_service +- mattermost_service - buildkite_service - bamboo_service - teamcity_service @@ -145,6 +151,7 @@ project: - bugzilla_service - gitlab_issue_tracker_service - external_wiki_service +- kubernetes_service - forked_project_link - forked_from_project - forked_project_links @@ -184,8 +191,15 @@ project: - environments - deployments - project_feature +- authorized_users +- project_authorizations +- route +- statistics award_emoji: - awardable - user priorities: -- label
\ No newline at end of file +- label +timelogs: +- trackable +- user diff --git a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb index 5ae178414cc..08a42fd27a2 100644 --- a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb @@ -1,12 +1,14 @@ require 'spec_helper' describe Gitlab::ImportExport::AvatarRestorer, lib: true do + include UploadHelpers + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } let(:project) { create(:empty_project) } before do allow_any_instance_of(described_class).to receive(:avatar_export_file) - .and_return(Rails.root + "spec/fixtures/dk.png") + .and_return(uploaded_image_temp_path) end after do diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index 770e8b0c2f4..b069696b5c7 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe Gitlab::ImportExport::MembersMapper, services: true do describe 'map members' do - let(:user) { create(:user) } + let(:user) { create(:admin, authorized_projects_populated: true) } let(:project) { create(:project, :public, name: 'searchable_project') } - let(:user2) { create(:user) } + let(:user2) { create(:user, authorized_projects_populated: true) } let(:exported_user_id) { 99 } let(:exported_members) do [{ @@ -24,7 +24,7 @@ describe Gitlab::ImportExport::MembersMapper, services: true do { "id" => exported_user_id, "email" => user2.email, - "username" => user2.username + "username" => 'test' } }, { @@ -48,6 +48,10 @@ describe Gitlab::ImportExport::MembersMapper, services: true do exported_members: exported_members, user: user, project: project) end + it 'includes the exported user ID in the map' do + expect(members_mapper.map.keys).to include(exported_user_id) + end + it 'maps a project member' do expect(members_mapper.map[exported_user_id]).to eq(user2.id) end @@ -56,16 +60,37 @@ describe Gitlab::ImportExport::MembersMapper, services: true do expect(members_mapper.map[-1]).to eq(user.id) end - it 'updates missing author IDs on missing project member' do - members_mapper.map[-1] + it 'has invited members with no user' do + members_mapper.map - expect(members_mapper.missing_author_ids.first).to eq(-1) + expect(ProjectMember.find_by_invite_email('invite@test.com')).not_to be_nil end - it 'has invited members with no user' do + it 'authorizes the users to the project' do members_mapper.map - expect(ProjectMember.find_by_invite_email('invite@test.com')).not_to be_nil + expect(user.authorized_project?(project)).to be true + expect(user2.authorized_project?(project)).to be true + end + + context 'user is not an admin' do + let(:user) { create(:user, authorized_projects_populated: true) } + + it 'does not map a project member' do + expect(members_mapper.map[exported_user_id]).to eq(user.id) + end + + it 'defaults to importer project member if it does not exist' do + expect(members_mapper.map[-1]).to eq(user.id) + end + end + + context 'chooses the one with an email first' do + let(:user3) { create(:user, username: 'test') } + + it 'maps the project member that has a matching email first' do + expect(members_mapper.map[exported_user_id]).to eq(user2.id) + end end end end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index ed9df468ced..2c0750c3377 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -15,6 +15,28 @@ "type": "ProjectLabel", "priorities": [ ] + }, + { + "id": 3, + "title": "test3", + "color": "#428bca", + "group_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "project_id": null, + "type": "GroupLabel", + "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": [ @@ -2517,7 +2539,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_build_succeeds": true, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -6548,7 +6570,9 @@ "url": null }, "erased_by_id": null, - "erased_at": null + "erased_at": null, + "type": "Ci::Build", + "token": "abcd" }, { "id": 72, @@ -7409,6 +7433,28 @@ "category": "common", "default": false, "wiki_page_events": true + }, + { + "id": 101, + "title": "JenkinsDeprecated", + "project_id": 5, + "created_at": "2016-06-14T15:01:51.031Z", + "updated_at": "2016-06-14T15:01:51.031Z", + "active": false, + "properties": { + + }, + "template": false, + "push_events": true, + "issues_events": true, + "merge_requests_events": true, + "tag_push_events": true, + "note_events": true, + "build_events": true, + "category": "common", + "default": false, + "wiki_page_events": true, + "type": "JenkinsDeprecatedService" } ], "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 3038ab53ad8..4b07fa53bf5 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -189,6 +189,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do 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') + + expect(restored_project_json).to be true + end + end end end end diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb index 3aa492a8ab1..db0084d6823 100644 --- a/spec/lib/gitlab/import_export/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::ImportExport::RelationFactory, lib: true do let(:project) { create(:empty_project) } let(:members_mapper) { double('members_mapper').as_null_object } - let(:user) { create(:user) } + let(:user) { create(:admin) } let(:created_object) do described_class.create(relation_sym: relation_sym, relation_hash: relation_hash, @@ -122,4 +122,60 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do expect(created_object.values).not_to include(99) end end + + context 'Notes user references' do + let(:relation_sym) { :notes } + let(:new_user) { create(:user) } + let(:exported_member) do + { + "id" => 111, + "access_level" => 30, + "source_id" => 1, + "source_type" => "Project", + "user_id" => 3, + "notification_level" => 3, + "created_at" => "2016-11-18T09:29:42.634Z", + "updated_at" => "2016-11-18T09:29:42.634Z", + "user" => { + "id" => 999, + "email" => new_user.email, + "username" => new_user.username + } + } + end + + let(:relation_hash) do + { + "id" => 4947, + "note" => "merged", + "noteable_type" => "MergeRequest", + "author_id" => 999, + "created_at" => "2016-11-18T09:29:42.634Z", + "updated_at" => "2016-11-18T09:29:42.634Z", + "project_id" => 1, + "attachment" => { + "url" => nil + }, + "noteable_id" => 377, + "system" => true, + "author" => { + "name" => "Administrator" + }, + "events" => [ + + ] + } + end + + let(:members_mapper) do + Gitlab::ImportExport::MembersMapper.new( + exported_members: [exported_member], + user: user, + project: project) + end + + it 'maps the right author to the imported note' do + expect(created_object.author).to eq(new_user) + end + end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 07a2c316899..493bc2db21a 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -20,6 +20,7 @@ Issue: - lock_version - milestone_id - weight +- time_estimate Event: - id - target_type @@ -78,6 +79,7 @@ Milestone: - project_id - description - due_date +- start_date - created_at - updated_at - state @@ -149,6 +151,7 @@ MergeRequest: - milestone_id - approvals_before_merge - rebase_commit_sha +- time_estimate MergeRequestDiff: - id - state @@ -246,6 +249,8 @@ DeployKey: - type - fingerprint - public +- can_push +- last_used_at Service: - id - type @@ -258,6 +263,7 @@ Service: - template - push_events - issues_events +- commit_events - merge_requests_events - tag_push_events - note_events @@ -339,4 +345,12 @@ LabelPriority: - label_id - priority - created_at -- updated_at
\ No newline at end of file +- updated_at +Timelog: +- id +- time_spent +- trackable_id +- trackable_type +- user_id +- created_at +- updated_at diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb new file mode 100644 index 00000000000..8cea38e9ff8 --- /dev/null +++ b/spec/lib/gitlab/import_sources_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Gitlab::ImportSources do + describe '.options' do + it 'returns a hash' do + expected = + { + 'GitHub' => 'github', + 'Bitbucket' => 'bitbucket', + 'GitLab.com' => 'gitlab', + 'Google Code' => 'google_code', + 'FogBugz' => 'fogbugz', + 'Repo by URL' => 'git', + 'GitLab export' => 'gitlab_project', + 'Gitea' => 'gitea' + } + + expect(described_class.options).to eq(expected) + end + end + + describe '.values' do + it 'returns an array' do + expected = + [ + 'github', + 'bitbucket', + 'gitlab', + 'google_code', + 'fogbugz', + 'git', + 'gitlab_project', + 'gitea' + ] + + expect(described_class.values).to eq(expected) + end + end + + describe '.importer_names' do + it 'returns an array of importer names' do + expected = + [ + 'github', + 'bitbucket', + 'gitlab', + 'google_code', + 'fogbugz', + 'gitlab_project', + 'gitea' + ] + + expect(described_class.importer_names).to eq(expected) + end + end + + describe '.importer' do + import_sources = { + 'github' => Gitlab::GithubImport::Importer, + 'bitbucket' => Gitlab::BitbucketImport::Importer, + 'gitlab' => Gitlab::GitlabImport::Importer, + 'google_code' => Gitlab::GoogleCodeImport::Importer, + 'fogbugz' => Gitlab::FogbugzImport::Importer, + 'git' => nil, + 'gitlab_project' => Gitlab::ImportExport::Importer, + 'gitea' => Gitlab::GithubImport::Importer + } + + import_sources.each do |name, klass| + it "returns #{klass} when given #{name}" do + expect(described_class.importer(name)).to eq(klass) + end + end + end + + describe '.title' do + import_sources = { + 'github' => 'GitHub', + 'bitbucket' => 'Bitbucket', + 'gitlab' => 'GitLab.com', + 'google_code' => 'Google Code', + 'fogbugz' => 'FogBugz', + 'git' => 'Repo by URL', + 'gitlab_project' => 'GitLab export', + 'gitea' => 'Gitea' + } + + import_sources.each do |name, title| + it "returns #{title} when given #{name}" do + expect(described_class.title(name)).to eq(title) + end + end + end +end diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index 1dcf2c0668b..7e951e3fcdd 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -23,6 +23,48 @@ describe Gitlab::IncomingEmail, lib: true do end end + describe 'self.supports_wildcard?' do + context 'address contains the wildard placeholder' do + before do + stub_incoming_email_setting(address: 'replies+%{key}@example.com') + end + + it 'confirms that wildcard is supported' do + expect(described_class.supports_wildcard?).to be_truthy + end + end + + context "address doesn't contain the wildcard placeholder" do + before do + stub_incoming_email_setting(address: 'replies@example.com') + end + + it 'returns that wildcard is not supported' do + expect(described_class.supports_wildcard?).to be_falsey + end + end + + context 'address is not set' do + before do + stub_incoming_email_setting(address: nil) + end + + it 'returns that wildard is not supported' do + expect(described_class.supports_wildcard?).to be_falsey + end + end + end + + context 'self.unsubscribe_address' do + before do + stub_incoming_email_setting(address: 'replies+%{key}@example.com') + end + + it 'returns the address with interpolated reply key and unsubscribe suffix' do + expect(described_class.unsubscribe_address('key')).to eq('replies+key+unsubscribe@example.com') + end + end + context "self.reply_address" do before do stub_incoming_email_setting(address: "replies+%{key}@example.com") diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb new file mode 100644 index 00000000000..c9bd52a3b8f --- /dev/null +++ b/spec/lib/gitlab/kubernetes_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::Kubernetes do + include described_class + + describe '#container_exec_url' do + let(:api_url) { 'https://example.com' } + let(:namespace) { 'default' } + let(:pod_name) { 'pod1' } + let(:container_name) { 'container1' } + + subject(:result) { URI::parse(container_exec_url(api_url, namespace, pod_name, container_name)) } + + it { expect(result.scheme).to eq('wss') } + it { expect(result.host).to eq('example.com') } + it { expect(result.path).to eq('/api/v1/namespaces/default/pods/pod1/exec') } + it { expect(result.query).to eq('container=container1&stderr=true&stdin=true&stdout=true&tty=true&command=sh&command=-c&command=bash+%7C%7C+sh') } + + context 'with a HTTP API URL' do + let(:api_url) { 'http://example.com' } + + it { expect(result.scheme).to eq('ws') } + end + + context 'with a path prefix in the API URL' do + let(:api_url) { 'https://example.com/prefix/' } + it { expect(result.path).to eq('/prefix/api/v1/namespaces/default/pods/pod1/exec') } + end + + context 'with arguments that need urlencoding' do + let(:namespace) { 'default namespace' } + let(:pod_name) { 'pod 1' } + let(:container_name) { 'container 1' } + + it { expect(result.path).to eq('/api/v1/namespaces/default%20namespace/pods/pod%201/exec') } + it { expect(result.query).to match(/\Acontainer=container\+1&/) } + end + end +end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index 534bcbf39fe..b9d12c3c24c 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -15,9 +15,9 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } it 'should block user in GitLab' do + expect(access).to receive(:block_user).with(user, 'does not exist anymore') + access.allowed? - expect(user).to be_blocked - expect(user).to be_ldap_blocked end end @@ -34,9 +34,9 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } it 'blocks user in GitLab' do + expect(access).to receive(:block_user).with(user, 'is disabled in Active Directory') + access.allowed? - expect(user).to be_blocked - expect(user).to be_ldap_blocked end end @@ -53,7 +53,10 @@ describe Gitlab::LDAP::Access, lib: true do end it 'does not unblock user in GitLab' do + expect(access).not_to receive(:unblock_user) + access.allowed? + expect(user).to be_blocked expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic end @@ -65,8 +68,9 @@ describe Gitlab::LDAP::Access, lib: true do end it 'unblocks user in GitLab' do + expect(access).to receive(:unblock_user).with(user, 'is not disabled anymore') + access.allowed? - expect(user).not_to be_blocked end end end @@ -87,9 +91,9 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } it 'blocks user in GitLab' do + expect(access).to receive(:block_user).with(user, 'does not exist anymore') + access.allowed? - expect(user).to be_blocked - expect(user).to be_ldap_blocked end end @@ -99,11 +103,54 @@ describe Gitlab::LDAP::Access, lib: true do end it 'unblocks the user if it exists' do + expect(access).to receive(:unblock_user).with(user, 'is available again') + access.allowed? - expect(user).not_to be_blocked end end end end end + + describe '#block_user' do + before do + user.activate + allow(Gitlab::AppLogger).to receive(:info) + + access.block_user user, 'reason' + end + + it 'blocks the user' do + expect(user).to be_blocked + expect(user).to be_ldap_blocked + end + + it 'logs the reason' do + expect(Gitlab::AppLogger).to have_received(:info).with( + "LDAP account \"123456\" reason, " \ + "blocking Gitlab user \"#{user.name}\" (#{user.email})" + ) + end + end + + describe '#unblock_user' do + before do + user.ldap_block + allow(Gitlab::AppLogger).to receive(:info) + + access.unblock_user user, 'reason' + end + + it 'activates the user' do + expect(user).not_to be_blocked + expect(user).not_to be_ldap_blocked + end + + it 'logs the reason' do + Gitlab::AppLogger.info( + "LDAP account \"123456\" reason, " \ + "unblocking Gitlab user \"#{user.name}\" (#{user.email})" + ) + end + end end diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index 835853a83a4..cab2e9908ff 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -1,20 +1,155 @@ require 'spec_helper' describe Gitlab::LDAP::Config, lib: true do - let(:config) { Gitlab::LDAP::Config.new provider } - let(:provider) { 'ldapmain' } + include LdapHelpers + + let(:config) { Gitlab::LDAP::Config.new('ldapmain') } describe '#initalize' do it 'requires a provider' do expect{ Gitlab::LDAP::Config.new }.to raise_error ArgumentError end - it "works" do + it 'works' do expect(config).to be_a described_class end - it "raises an error if a unknow provider is used" do + it 'raises an error if a unknown provider is used' do expect{ Gitlab::LDAP::Config.new 'unknown' }.to raise_error(RuntimeError) end end + + describe '#adapter_options' do + it 'constructs basic options' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 386, + 'method' => 'plain' + } + ) + + expect(config.adapter_options).to eq( + host: 'ldap.example.com', + port: 386, + encryption: nil + ) + end + + it 'includes authentication options when auth is configured' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'method' => 'ssl', + 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'password' => 'super_secret' + } + ) + + expect(config.adapter_options).to eq( + host: 'ldap.example.com', + port: 686, + encryption: :simple_tls, + auth: { + method: :simple, + username: 'uid=admin,dc=example,dc=com', + password: 'super_secret' + } + ) + end + end + + describe '#omniauth_options' do + it 'constructs basic options' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 386, + 'base' => 'ou=users,dc=example,dc=com', + 'method' => 'plain', + 'uid' => 'uid' + } + ) + + expect(config.omniauth_options).to include( + host: 'ldap.example.com', + port: 386, + base: 'ou=users,dc=example,dc=com', + method: 'plain', + filter: '(uid=%{username})' + ) + expect(config.omniauth_options.keys).not_to include(:bind_dn, :password) + end + + it 'includes authentication options when auth is configured' do + stub_ldap_config( + options: { + 'uid' => 'sAMAccountName', + 'user_filter' => '(memberOf=cn=group1,ou=groups,dc=example,dc=com)', + 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'password' => 'super_secret' + } + ) + + expect(config.omniauth_options).to include( + filter: '(&(sAMAccountName=%{username})(memberOf=cn=group1,ou=groups,dc=example,dc=com))', + bind_dn: 'uid=admin,dc=example,dc=com', + password: 'super_secret' + ) + end + end + + describe '#has_auth?' do + it 'is true when password is set' do + stub_ldap_config( + options: { + 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'password' => 'super_secret' + } + ) + + expect(config.has_auth?).to be_truthy + end + + it 'is true when bind_dn is set and password is empty' do + stub_ldap_config( + options: { + 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'password' => '' + } + ) + + expect(config.has_auth?).to be_truthy + end + + it 'is false when password and bind_dn are not set' do + stub_ldap_config(options: { 'bind_dn' => nil, 'password' => nil }) + + expect(config.has_auth?).to be_falsey + end + end + + describe '#attributes' do + it 'uses default attributes when no custom attributes are configured' do + expect(config.attributes).to eq(config.default_attributes) + end + + it 'merges the configuration attributes with default attributes' do + stub_ldap_config( + options: { + 'attributes' => { + 'username' => %w(sAMAccountName), + 'email' => %w(userPrincipalName) + } + } + ) + + expect(config.attributes).to include({ + 'username' => %w(sAMAccountName), + 'email' => %w(userPrincipalName), + 'name' => 'cn' + }) + end + end end diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb new file mode 100644 index 00000000000..9a556cde5d5 --- /dev/null +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::LDAP::Person do + include LdapHelpers + + let(:entry) { ldap_user_entry('john.doe') } + + before do + stub_ldap_config( + options: { + 'attributes' => { + 'name' => 'cn', + 'email' => %w(mail email userPrincipalName) + } + } + ) + end + + describe '#name' do + it 'uses the configured name attribute and handles values as an array' do + name = 'John Doe' + entry['cn'] = [name] + person = Gitlab::LDAP::Person.new(entry, 'ldapmain') + + expect(person.name).to eq(name) + end + end + + describe '#email' do + it 'returns the value of mail, if present' do + mail = 'john@example.com' + entry['mail'] = mail + person = Gitlab::LDAP::Person.new(entry, 'ldapmain') + + expect(person.email).to eq([mail]) + end + + it 'returns the value of userPrincipalName, if mail and email are not present' do + user_principal_name = 'john.doe@example.com' + entry['userPrincipalName'] = user_principal_name + person = Gitlab::LDAP::Person.new(entry, 'ldapmain') + + expect(person.email).to eq([user_principal_name]) + end + end +end diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index bcaffd27909..fb470ea7568 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::Metrics::RackMiddleware do end it 'tags a transaction with the method and path of the route in the grape endpoint' do - route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)") + route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)") endpoint = double(:endpoint, route: route) env['api.endpoint'] = endpoint @@ -117,7 +117,7 @@ describe Gitlab::Metrics::RackMiddleware do let(:transaction) { middleware.transaction_from_env(env) } it 'tags a transaction with the method and path of the route in the grape endpount' do - route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)") + route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)") endpoint = double(:endpoint, route: route) env['api.endpoint'] = endpoint @@ -126,5 +126,16 @@ describe Gitlab::Metrics::RackMiddleware do expect(transaction.action).to eq('Grape#GET /projects/:id/archive') end + + it 'does not tag a transaction if route infos are missing' do + endpoint = double(:endpoint) + allow(endpoint).to receive(:route).and_raise + + env['api.endpoint'] = endpoint + + middleware.tag_endpoint(transaction, env) + + expect(transaction.action).to be_nil + end end end diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index 117a15264da..fd3769d75b5 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -22,7 +22,7 @@ describe Gitlab::Middleware::Go, lib: true do resp = middleware.call(env) expect(resp[0]).to eq(200) expect(resp[1]['Content-Type']).to eq('text/html') - expected_body = "<!DOCTYPE html><html><head><meta content='localhost/group/project git http://localhost/group/project.git' name='go-import'></head></html>\n" + expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/group/project git http://#{Gitlab.config.gitlab.host}/group/project.git' name='go-import'></head></html>\n" expect(resp[2].body).to eq([expected_body]) end end diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb new file mode 100644 index 00000000000..8d925460f01 --- /dev/null +++ b/spec/lib/gitlab/middleware/multipart_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +require 'tempfile' + +describe Gitlab::Middleware::Multipart do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + + it 'opens top-level files' do + Tempfile.open('top-level') do |tempfile| + env = post_env({ 'file' => tempfile.path }, { 'file.name' => 'filename' }, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['file'] + expect(file).to be_a(::UploadedFile) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + it 'rejects headers signed with the wrong secret' do + env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, 'x' * 32, 'gitlab-workhorse') + + expect { middleware.call(env) }.to raise_error(JWT::VerificationError) + end + + it 'rejects headers signed with the wrong issuer' do + env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, Gitlab::Workhorse.secret, 'acme-inc') + + expect { middleware.call(env) }.to raise_error(JWT::InvalidIssuerError) + end + + it 'opens files one level deep' do + Tempfile.open('one-level') do |tempfile| + in_params = { 'user' => { 'avatar' => { '.name' => 'filename' } } } + env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['user']['avatar'] + expect(file).to be_a(::UploadedFile) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + it 'opens files two levels deep' do + Tempfile.open('two-levels') do |tempfile| + in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => 'filename' } } } } + env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['project']['milestone']['themesong'] + expect(file).to be_a(::UploadedFile) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + def post_env(rewritten_fields, params, secret, issuer) + token = JWT.encode({ 'iss' => issuer, 'rewritten_fields' => rewritten_fields }, secret, 'HS256') + Rack::MockRequest.env_for( + '/', + method: 'post', + params: params, + described_class::RACK_ENV_KEY => token + ) + end +end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 78c669e8fa5..fc9e1cb430a 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -137,11 +137,12 @@ describe Gitlab::OAuth::User, lib: true do 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(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) end context "and no account for the LDAP user" do it "creates a user with dual LDAP and omniauth identities" do + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + oauth_user.save expect(gl_user).to be_valid @@ -159,6 +160,8 @@ describe Gitlab::OAuth::User, lib: true do 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') } it "adds the omniauth identity to the LDAP account" do + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + oauth_user.save expect(gl_user).to be_valid @@ -172,6 +175,24 @@ describe Gitlab::OAuth::User, lib: true do ]) end end + + context 'when an LDAP person is not found by uid' do + it 'tries to find an LDAP person by DN and adds the omniauth identity to the user' do + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) + allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user) + + oauth_user.save + + 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: 'twitter', extern_uid: uid } + ] + ) + end + end end context "and no corresponding LDAP person" do diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 29abb4d4d07..d94eb52f838 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -6,22 +6,73 @@ describe Gitlab::ProjectSearchResults, lib: true do let(:query) { 'hello world' } describe 'initialize with empty ref' do - let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, '') } + let(:results) { described_class.new(user, project, query, '') } it { expect(results.project).to eq(project) } - it { expect(results.repository_ref).to be_nil } it { expect(results.query).to eq('hello world') } end describe 'initialize with ref' do let(:ref) { 'refs/heads/test' } - let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, ref) } + let(:results) { described_class.new(user, project, query, ref) } it { expect(results.project).to eq(project) } it { expect(results.repository_ref).to eq(ref) } it { expect(results.query).to eq('hello world') } end + describe 'blob search' do + let(:results) { described_class.new(user, project, 'files').objects('blobs') } + + it 'finds by name' do + expect(results).to include(["files/images/wm.svg", nil]) + end + + it 'finds by content' do + blob = results.select { |result| result.first == "CHANGELOG" }.flatten.last + + expect(blob.filename).to eq("CHANGELOG") + end + + describe 'parsing results' do + let(:results) { project.repository.search_files_by_content('feature', 'master') } + let(:search_result) { results.first } + + subject { described_class.parse_search_result(search_result) } + + it "returns a valid OpenStruct object" do + is_expected.to be_an OpenStruct + expect(subject.filename).to eq('CHANGELOG') + expect(subject.basename).to eq('CHANGELOG') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(188) + expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") + end + + context "when filename has extension" do + let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" } + + it { expect(subject.filename).to eq('CONTRIBUTE.md') } + it { expect(subject.basename).to eq('CONTRIBUTE') } + end + + context "when file under directory" do + let(:search_result) { "master:a/b/c.md:5:a b c\n" } + + it { expect(subject.filename).to eq('a/b/c.md') } + it { expect(subject.basename).to eq('a/b/c') } + end + end + end + + it 'does not list issues on private projects' do + issue = create(:issue, project: project) + + results = described_class.new(user, project, issue.title) + + expect(results.objects('issues')).not_to include issue + end + describe 'confidential issues' do let(:query) { 'issue' } let(:author) { create(:user) } @@ -29,6 +80,7 @@ describe Gitlab::ProjectSearchResults, lib: true do let(:non_member) { create(:user) } let(:member) { create(:user) } let(:admin) { create(:admin) } + let(:project) { create(:empty_project, :internal) } let!(:issue) { create(:issue, project: project, title: 'Issue 1') } let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) } @@ -66,7 +118,7 @@ describe Gitlab::ProjectSearchResults, lib: true do end it 'lists project confidential issues for assignee' do - results = described_class.new(assignee, project.id, query) + results = described_class.new(assignee, project, query) issues = results.objects('issues') expect(issues).to include issue @@ -97,4 +149,148 @@ describe Gitlab::ProjectSearchResults, lib: true do expect(results.issues_count).to eq 3 end end + + describe 'notes search' do + it 'lists notes' do + project = create(:empty_project, :public) + note = create(:note, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).to include note + end + + it "doesn't list issue notes when access is restricted" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).not_to include note + end + + it "doesn't list merge_request notes when access is restricted" do + project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_merge_request, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).not_to include note + end + end + + # Examples for commit access level test + # + # params: + # * search_phrase + # * commit + # + shared_examples 'access restricted commits' do + context 'when project is internal' do + let(:project) { create(:project, :internal) } + + it 'does not search if user is not authenticated' do + commits = described_class.new(nil, project, search_phrase).objects('commits') + + expect(commits).to be_empty + end + + it 'searches if user is authenticated' do + commits = described_class.new(user, project, search_phrase).objects('commits') + + expect(commits).to contain_exactly commit + end + end + + context 'when project is private' do + let!(:creator) { create(:user, username: 'private-project-author') } + let!(:private_project) { create(:project, :private, creator: creator, namespace: creator.namespace) } + let(:team_master) do + user = create(:user, username: 'private-project-master') + private_project.team << [user, :master] + user + end + let(:team_reporter) do + user = create(:user, username: 'private-project-reporter') + private_project.team << [user, :reporter] + user + end + + it 'does not show commit to stranger' do + commits = described_class.new(nil, private_project, search_phrase).objects('commits') + + expect(commits).to be_empty + end + + context 'team access' do + it 'shows commit to creator' do + commits = described_class.new(creator, private_project, search_phrase).objects('commits') + + expect(commits).to contain_exactly commit + end + + it 'shows commit to master' do + commits = described_class.new(team_master, private_project, search_phrase).objects('commits') + + expect(commits).to contain_exactly commit + end + + it 'shows commit to reporter' do + commits = described_class.new(team_reporter, private_project, search_phrase).objects('commits') + + expect(commits).to contain_exactly commit + end + end + end + end + + describe 'commit search' do + context 'by commit message' do + let(:project) { create(:project, :public) } + let(:commit) { project.repository.commit('59e29889be61e6e0e5e223bfa9ac2721d31605b8') } + let(:message) { 'Sorry, I did a mistake' } + + it 'finds commit by message' do + commits = described_class.new(user, project, message).objects('commits') + + expect(commits).to contain_exactly commit + end + + it 'handles when no commit match' do + commits = described_class.new(user, project, 'not really an existing description').objects('commits') + + expect(commits).to be_empty + end + + it_behaves_like 'access restricted commits' do + let(:search_phrase) { message } + let(:commit) { project.repository.commit('59e29889be61e6e0e5e223bfa9ac2721d31605b8') } + end + end + + context 'by commit hash' do + let(:project) { create(:project, :public) } + let(:commit) { project.repository.commit('0b4bc9a') } + commit_hashes = { short: '0b4bc9a', full: '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } + + commit_hashes.each do |type, commit_hash| + it "shows commit by #{type} hash id" do + commits = described_class.new(user, project, commit_hash).objects('commits') + + expect(commits).to contain_exactly commit + end + end + + it 'handles not existing commit hash correctly' do + commits = described_class.new(user, project, 'deadbeef').objects('commits') + + expect(commits).to be_empty + end + + it_behaves_like 'access restricted commits' do + let(:search_phrase) { '0b4bc9a49' } + let(:commit) { project.repository.commit('0b4bc9a') } + end + end + end end diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb index e5406fb2d33..917c5c46db1 100644 --- a/spec/lib/gitlab/redis_spec.rb +++ b/spec/lib/gitlab/redis_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Redis do - let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s } + include StubENV before(:each) { clear_raw_config } after(:each) { clear_raw_config } @@ -72,6 +72,20 @@ describe Gitlab::Redis do expect(url2).not_to end_with('foobar') end + + context 'when yml file with env variable' do + let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_config_with_env.yml') } + + before do + stub_env('TEST_GITLAB_REDIS_URL', 'redis://redishost:6379') + end + + it 'reads redis url from env variable' do + stub_const("#{described_class}::CONFIG_FILE", redis_config) + + expect(described_class.url).to eq 'redis://redishost:6379' + end + end end describe '._raw_config' do diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index c51b10bdc69..c78cd30157e 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -29,4 +29,20 @@ describe Gitlab::Regex, lib: true do describe 'file path regex' do it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) } end + + describe 'environment slug regex' do + def be_matched + match(Gitlab::Regex.environment_slug_regex) + end + + it { expect('foo').to be_matched } + it { expect('foo-1').to be_matched } + + it { expect('FOO').not_to be_matched } + it { expect('foo/1').not_to be_matched } + it { expect('foo.1').not_to be_matched } + it { expect('foo*1').not_to be_matched } + it { expect('9foo').not_to be_matched } + it { expect('foo-').not_to be_matched } + end end diff --git a/spec/lib/gitlab/routing_spec.rb b/spec/lib/gitlab/routing_spec.rb new file mode 100644 index 00000000000..01d5acfc15b --- /dev/null +++ b/spec/lib/gitlab/routing_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Routing do + context 'when module is included' do + subject do + Class.new.include(described_class).new + end + + it 'makes it possible to access url helpers' do + expect(subject).to respond_to(:namespace_project_path) + end + end + + context 'when module is not included' do + subject do + Class.new.include(described_class.url_helpers).new + end + + it 'exposes url helpers module through a method' do + expect(subject).to respond_to(:namespace_project_path) + end + end +end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index dfbefad6367..9614aad3e73 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -12,35 +12,57 @@ describe Gitlab::SearchResults do let!(:milestone) { create(:milestone, project: project, title: 'foo') } let(:results) { described_class.new(user, Project.all, 'foo') } - describe '#projects_count' do - it 'returns the total amount of projects' do - expect(results.projects_count).to eq(1) + context 'as a user with access' do + before do + project.team << [user, :developer] end - end - describe '#issues_count' do - it 'returns the total amount of issues' do - expect(results.issues_count).to eq(1) + describe '#projects_count' do + it 'returns the total amount of projects' do + expect(results.projects_count).to eq(1) + end end - end - describe '#merge_requests_count' do - it 'returns the total amount of merge requests' do - expect(results.merge_requests_count).to eq(1) + describe '#issues_count' do + it 'returns the total amount of issues' do + expect(results.issues_count).to eq(1) + end + end + + describe '#merge_requests_count' do + it 'returns the total amount of merge requests' do + expect(results.merge_requests_count).to eq(1) + end end - end - describe '#milestones_count' do - it 'returns the total amount of milestones' do - expect(results.milestones_count).to eq(1) + describe '#milestones_count' do + it 'returns the total amount of milestones' do + expect(results.milestones_count).to eq(1) + end end + + it 'includes merge requests from source and target projects' do + forked_project = create(:empty_project, forked_from_project: project) + 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') + + expect(results.objects('merge_requests')).to include merge_request_2 + end + end + + it 'does not list issues on private projects' do + private_project = create(:empty_project, :private) + issue = create(:issue, project: private_project, title: 'foo') + + expect(results.objects('issues')).not_to include issue end describe 'confidential issues' do - let(:project_1) { create(:empty_project) } - let(:project_2) { create(:empty_project) } - let(:project_3) { create(:empty_project) } - let(:project_4) { create(:empty_project) } + let(:project_1) { create(:empty_project, :internal) } + let(:project_2) { create(:empty_project, :internal) } + let(:project_3) { create(:empty_project, :internal) } + let(:project_4) { create(:empty_project, :internal) } let(:query) { 'issue' } let(:limit_projects) { Project.where(id: [project_1.id, project_2.id, project_3.id]) } let(:author) { create(:user) } @@ -139,4 +161,11 @@ describe Gitlab::SearchResults do expect(results.issues_count).to eq 5 end end + + it 'does not list merge requests on projects with limited access' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + + expect(results.objects('merge_requests')).not_to include merge_request + end end diff --git a/spec/lib/gitlab/serialize/ci/variables_spec.rb b/spec/lib/gitlab/serialize/ci/variables_spec.rb new file mode 100644 index 00000000000..7ea74da5252 --- /dev/null +++ b/spec/lib/gitlab/serialize/ci/variables_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Gitlab::Serialize::Ci::Variables do + subject do + described_class.load(described_class.dump(object)) + end + + let(:object) do + [{ key: :key, value: 'value', public: true }, + { key: 'wee', value: 1, public: false }] + end + + it 'converts keys into strings' do + is_expected.to eq([ + { key: 'key', value: 'value', public: true }, + { key: 'wee', value: 1, public: false }]) + end +end diff --git a/spec/lib/gitlab/sidekiq_throttler_spec.rb b/spec/lib/gitlab/sidekiq_throttler_spec.rb new file mode 100644 index 00000000000..ff32e0e699d --- /dev/null +++ b/spec/lib/gitlab/sidekiq_throttler_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::SidekiqThrottler do + before do + Sidekiq.options[:concurrency] = 35 + + stub_application_setting( + sidekiq_throttling_enabled: true, + sidekiq_throttling_factor: 0.1, + sidekiq_throttling_queues: %w[build project_cache] + ) + end + + describe '#execute!' do + it 'sets limits on the selected queues' do + Gitlab::SidekiqThrottler.execute! + + expect(Sidekiq::Queue['build'].limit).to eq 4 + expect(Sidekiq::Queue['project_cache'].limit).to eq 4 + end + + it 'does not set limits on other queues' do + Gitlab::SidekiqThrottler.execute! + + expect(Sidekiq::Queue['merge'].limit).to be_nil + end + end +end diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb index 0cdbab87544..849edb09476 100644 --- a/spec/lib/gitlab/sql/union_spec.rb +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -1,16 +1,26 @@ require 'spec_helper' describe Gitlab::SQL::Union, lib: true do + let(:relation_1) { User.where(email: 'alice@example.com').select(:id) } + let(:relation_2) { User.where(email: 'bob@example.com').select(:id) } + + def to_sql(relation) + relation.reorder(nil).to_sql + end + describe '#to_sql' do it 'returns a String joining relations together using a UNION' do - rel1 = User.where(email: 'alice@example.com') - rel2 = User.where(email: 'bob@example.com') - union = described_class.new([rel1, rel2]) + union = described_class.new([relation_1, relation_2]) + + expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") + end - sql1 = rel1.reorder(nil).to_sql - sql2 = rel2.reorder(nil).to_sql + it 'skips Model.none segements' do + empty_relation = User.none + union = described_class.new([empty_relation, relation_1, relation_2]) - expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}") + 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 end end diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index d3c3b800b94..369e55f61f1 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -66,7 +66,8 @@ describe Gitlab::UserAccess, lib: true do end describe 'push to protected branch' do - let(:branch) { create :protected_branch, project: project } + let(:branch) { create :protected_branch, project: project, name: "test" } + let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project } it 'returns true if user is a master' do project.team << [user, :master] @@ -85,6 +86,12 @@ describe Gitlab::UserAccess, lib: true do expect(access.can_push_to_branch?(branch.name)).to be_falsey end + + it 'returns true if branch does not exist and user has permission to merge' do + project.team << [user, :developer] + + expect(access.can_push_to_branch?(not_existing_branch.name)).to be_truthy + end end describe 'push to protected branch if allowed for developers' do diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb new file mode 100644 index 00000000000..f2c152cdcd4 --- /dev/null +++ b/spec/lib/gitlab/view/presenter/base_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Gitlab::View::Presenter::Base do + let(:project) { double(:project) } + let(:presenter_class) do + Struct.new(:subject).include(described_class) + end + + describe '.presenter?' do + it 'returns true' do + presenter = presenter_class.new(project) + + expect(presenter.class).to be_presenter + end + end + + describe '.presents' do + it 'exposes #subject with the given keyword' do + presenter_class.presents(:foo) + presenter = presenter_class.new(project) + + expect(presenter.foo).to eq(project) + end + end + + describe '#can?' do + context 'user is not allowed' do + it 'returns false' do + presenter = presenter_class.new(build_stubbed(:empty_project)) + + expect(presenter.can?(nil, :read_project)).to be_falsy + end + end + + context 'user is allowed' do + it 'returns true' do + presenter = presenter_class.new(build_stubbed(:empty_project, :public)) + + expect(presenter.can?(nil, :read_project)).to be_truthy + end + end + + context 'subject is overriden' do + it 'returns true' do + presenter = presenter_class.new(build_stubbed(:empty_project, :public)) + + expect(presenter.can?(nil, :read_project, build_stubbed(:empty_project))).to be_falsy + end + end + end +end diff --git a/spec/lib/gitlab/view/presenter/delegated_spec.rb b/spec/lib/gitlab/view/presenter/delegated_spec.rb new file mode 100644 index 00000000000..888ab80cad5 --- /dev/null +++ b/spec/lib/gitlab/view/presenter/delegated_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Gitlab::View::Presenter::Delegated do + let(:project) { double(:project, bar: 'baz') } + let(:presenter_class) do + Class.new(described_class) + end + + it 'includes Gitlab::View::Presenter::Base' do + expect(described_class).to include(Gitlab::View::Presenter::Base) + end + + describe '#initialize' do + it 'takes arbitrary key/values and exposes them' do + presenter = presenter_class.new(project, user: 'user', foo: 'bar') + + expect(presenter.user).to eq('user') + expect(presenter.foo).to eq('bar') + end + end + + describe 'delegation' do + it 'forwards missing methods to subject' do + presenter = presenter_class.new(project) + + expect(presenter.bar).to eq('baz') + end + end +end diff --git a/spec/lib/gitlab/view/presenter/factory_spec.rb b/spec/lib/gitlab/view/presenter/factory_spec.rb new file mode 100644 index 00000000000..55c5ecbf92f --- /dev/null +++ b/spec/lib/gitlab/view/presenter/factory_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Gitlab::View::Presenter::Factory do + let(:build) { Ci::Build.new } + + describe '#initialize' do + context 'without optional parameters' do + it 'takes a subject and optional params' do + presenter = described_class.new(build) + + expect { presenter }.not_to raise_error + end + end + + context 'with optional parameters' do + it 'takes a subject and optional params' do + presenter = described_class.new(build, user: 'user') + + expect { presenter }.not_to raise_error + end + end + end + + describe '#fabricate!' do + it 'exposes given params' do + presenter = described_class.new(build, user: 'user', foo: 'bar').fabricate! + + expect(presenter.user).to eq('user') + expect(presenter.foo).to eq('bar') + end + + it 'detects the presenter based on the given subject' do + presenter = described_class.new(build).fabricate! + + expect(presenter).to be_a(Ci::BuildPresenter) + end + end +end diff --git a/spec/lib/gitlab/view/presenter/simple_spec.rb b/spec/lib/gitlab/view/presenter/simple_spec.rb new file mode 100644 index 00000000000..b489bdf1981 --- /dev/null +++ b/spec/lib/gitlab/view/presenter/simple_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Gitlab::View::Presenter::Simple do + let(:project) { double(:project) } + let(:presenter_class) do + Class.new(described_class) + end + + it 'includes Gitlab::View::Presenter::Base' do + expect(described_class).to include(Gitlab::View::Presenter::Base) + end + + describe '#initialize' do + it 'takes arbitrary key/values and exposes them' do + presenter = presenter_class.new(project, user: 'user', foo: 'bar') + + expect(presenter.user).to eq('user') + expect(presenter.foo).to eq('bar') + end + end + + describe 'delegation' do + it 'does not forward missing methods to subject' do + presenter = presenter_class.new(project) + + expect { presenter.foo }.to raise_error(NoMethodError) + end + end +end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index b5b685da904..4b1cd466677 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -37,6 +37,42 @@ describe Gitlab::Workhorse, lib: true do end end + describe '.terminal_websocket' do + def terminal(ca_pem: nil) + out = { + subprotocols: ['foo'], + url: 'wss://example.com/terminal.ws', + headers: { 'Authorization' => ['Token x'] } + } + out[:ca_pem] = ca_pem if ca_pem + out + end + + def workhorse(ca_pem: nil) + out = { + 'Terminal' => { + 'Subprotocols' => ['foo'], + 'Url' => 'wss://example.com/terminal.ws', + 'Header' => { 'Authorization' => ['Token x'] } + } + } + out['Terminal']['CAPem'] = ca_pem if ca_pem + out + end + + context 'without ca_pem' do + subject { Gitlab::Workhorse.terminal_websocket(terminal) } + + it { is_expected.to eq(workhorse) } + end + + context 'with ca_pem' do + subject { Gitlab::Workhorse.terminal_websocket(terminal(ca_pem: "foo")) } + + it { is_expected.to eq(workhorse(ca_pem: "foo")) } + end + end + describe '.send_git_diff' do let(:diff_refs) { double(base_sha: "base", head_sha: "head") } subject { described_class.send_git_patch(repository, diff_refs) } @@ -138,4 +174,27 @@ describe Gitlab::Workhorse, lib: true do described_class.verify_api_request!(headers) end end + + describe '.git_http_ok' do + let(:user) { create(:user) } + + subject { described_class.git_http_ok(repository, user) } + + it { expect(subject).to eq({ GL_ID: "user-#{user.id}", RepoPath: repository.path_to_repo }) } + + context 'when Gitaly socket path is present' do + let(:gitaly_socket_path) { '/tmp/gitaly.sock' } + + before do + allow(Gitlab.config.gitaly).to receive(:socket_path).and_return(gitaly_socket_path) + end + + it 'includes Gitaly params in the returned value' do + expect(subject).to include({ + GitalyResourcePath: "/projects/#{repository.project.id}/git-http/info-refs", + GitalySocketPath: gitaly_socket_path, + }) + end + end + end end diff --git a/spec/lib/light_url_builder_spec.rb b/spec/lib/light_url_builder_spec.rb new file mode 100644 index 00000000000..a826b24419a --- /dev/null +++ b/spec/lib/light_url_builder_spec.rb @@ -0,0 +1,119 @@ +require 'spec_helper' + +describe Gitlab::UrlBuilder, lib: true do + describe '.build' do + context 'when passing a Commit' do + it 'returns a proper URL' do + commit = build_stubbed(:commit) + + url = described_class.build(commit) + + expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}" + end + end + + context 'when passing an Issue' do + it 'returns a proper URL' do + issue = build_stubbed(:issue, iid: 42) + + url = described_class.build(issue) + + expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}" + end + end + + context 'when passing a MergeRequest' do + it 'returns a proper URL' do + merge_request = build_stubbed(:merge_request, iid: 42) + + url = described_class.build(merge_request) + + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}" + end + end + + context 'when passing a Note' do + context 'on a Commit' do + it 'returns a proper URL' do + note = build_stubbed(:note_on_commit) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end + + context 'on a Commit Diff' do + it 'returns a proper URL' do + note = build_stubbed(:diff_note_on_commit) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end + + context 'on an Issue' do + it 'returns a proper URL' do + issue = create(:issue, iid: 42) + note = build_stubbed(:note_on_issue, noteable: issue) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}" + end + end + + context 'on a MergeRequest' do + it 'returns a proper URL' do + merge_request = create(:merge_request, iid: 42) + note = build_stubbed(:note_on_merge_request, noteable: merge_request) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + context 'on a MergeRequest Diff' do + it 'returns a proper URL' do + merge_request = create(:merge_request, iid: 42) + note = build_stubbed(:diff_note_on_merge_request, noteable: merge_request) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + context 'on a ProjectSnippet' do + it 'returns a proper URL' do + project_snippet = create(:project_snippet) + note = build_stubbed(:note_on_project_snippet, noteable: project_snippet) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}" + end + end + + context 'on another object' do + it 'returns a proper URL' do + project = build_stubbed(:project) + + expect { described_class.build(project) }. + to raise_error(NotImplementedError, 'No URL builder defined for Project') + end + end + end + + context 'when passing a WikiPage' do + it 'returns a proper URL' do + wiki_page = build(:wiki_page) + url = described_class.build(wiki_page) + + expect(url).to eq "#{Gitlab.config.gitlab.url}#{wiki_page.wiki.wiki_base_path}/#{wiki_page.slug}" + end + end + end +end diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb new file mode 100644 index 00000000000..dc11a414717 --- /dev/null +++ b/spec/lib/mattermost/client_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Mattermost::Client do + let(:user) { build(:user) } + + subject { described_class.new(user) } + + context 'JSON parse error' do + before do + Struct.new("Request", :body, :success?) + end + + it 'yields an error on malformed JSON' do + bad_json = Struct::Request.new("I'm not json", true) + expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError) + end + + it 'shows a client error if the request was unsuccessful' do + bad_request = Struct::Request.new("true", false) + + expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError) + end + end +end diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb new file mode 100644 index 00000000000..5ccf1100898 --- /dev/null +++ b/spec/lib/mattermost/command_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe Mattermost::Command do + let(:params) { { 'token' => 'token', team_id: 'abc' } } + + before do + Mattermost::Session.base_uri('http://mattermost.example.com') + + allow_any_instance_of(Mattermost::Client).to receive(:with_session). + and_yield(Mattermost::Session.new(nil)) + end + + describe '#create' do + let(:params) do + { team_id: 'abc', + trigger: 'gitlab' + } + end + + subject { described_class.new(nil).create(params) } + + context 'for valid trigger word' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + with(body: { + team_id: 'abc', + trigger: 'gitlab' }.to_json). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { token: 'token' }.to_json + ) + end + + it 'returns a token' do + is_expected.to eq('token') + end + end + + context 'for error message' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + id: 'api.command.duplicate_trigger.app_error', + message: 'This trigger word is already in use. Please choose another word.', + detailed_error: '', + request_id: 'obc374man7bx5r3dbc1q5qhf3r', + status_code: 500 + }.to_json + ) + end + + it 'raises an error with message' do + expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.') + end + end + end +end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb new file mode 100644 index 00000000000..74d12e37181 --- /dev/null +++ b/spec/lib/mattermost/session_spec.rb @@ -0,0 +1,123 @@ +require 'spec_helper' + +describe Mattermost::Session, type: :request do + let(:user) { create(:user) } + + let(:gitlab_url) { "http://gitlab.com" } + let(:mattermost_url) { "http://mattermost.com" } + + subject { described_class.new(user) } + + # Needed for doorkeeper to function + it { is_expected.to respond_to(:current_resource_owner) } + it { is_expected.to respond_to(:request) } + it { is_expected.to respond_to(:authorization) } + it { is_expected.to respond_to(:strategy) } + + before do + described_class.base_uri(mattermost_url) + end + + describe '#with session' do + let(:location) { 'http://location.tld' } + let!(:stub) do + WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login"). + to_return(headers: { 'location' => location }, status: 307) + end + + context 'without oauth uri' do + it 'makes a request to the oauth uri' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with oauth_uri' do + let!(:doorkeeper) do + Doorkeeper::Application.create( + name: "GitLab Mattermost", + redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete", + scopes: "") + end + + context 'without token_uri' do + it 'can not create a session' do + expect do + subject.with_session + end.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with token_uri' do + let(:state) { "state" } + let(:params) do + { response_type: "code", + client_id: doorkeeper.uid, + redirect_uri: "#{mattermost_url}/signup/gitlab/complete", + state: state } + end + let(:location) do + "#{gitlab_url}/oauth/authorize?#{URI.encode_www_form(params)}" + end + + before do + WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete"). + with(query: hash_including({ 'state' => state })). + to_return do |request| + post "/oauth/token", + client_id: doorkeeper.uid, + client_secret: doorkeeper.secret, + redirect_uri: params[:redirect_uri], + grant_type: 'authorization_code', + code: request.uri.query_values['code'] + + if response.status == 200 + { headers: { 'token' => 'thisworksnow' }, status: 202 } + end + end + + WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout"). + to_return(headers: { Authorization: 'token thisworksnow' }, status: 200) + end + + it 'can setup a session' do + subject.with_session do |session| + end + + expect(subject.token).not_to be_nil + end + + it 'returns the value of the block' do + result = subject.with_session do |session| + "value" + end + + expect(result).to eq("value") + end + end + end + + context 'with lease' do + before do + allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk') + end + + it 'tries to obtain a lease' do + expect(subject).to receive(:lease_try_obtain) + expect(Gitlab::ExclusiveLease).to receive(:cancel) + + # Cannot setup a session, but we should still cancel the lease + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'without lease' do + before do + allow(subject).to receive(:lease_try_obtain).and_return(nil) + end + + it 'returns a NoSessionError error' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + end +end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb new file mode 100644 index 00000000000..2d14be6bcc2 --- /dev/null +++ b/spec/lib/mattermost/team_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Mattermost::Team do + before do + Mattermost::Session.base_uri('http://mattermost.example.com') + + allow_any_instance_of(Mattermost::Client).to receive(:with_session). + and_yield(Mattermost::Session.new(nil)) + end + + describe '#all' do + subject { described_class.new(nil).all } + + context 'for valid request' do + let(:response) do + [{ + "id" => "xiyro8huptfhdndadpz8r3wnbo", + "create_at" => 1482174222155, + "update_at" => 1482174222155, + "delete_at" => 0, + "display_name" => "chatops", + "name" => "chatops", + "email" => "admin@example.com", + "type" => "O", + "company_name" => "", + "allowed_domains" => "", + "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", + "allow_open_invite" => false }] + end + + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: response.to_json + ) + end + + it 'returns a token' do + is_expected.to eq(response) + end + end + + context 'for error message' do + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + id: 'api.team.list.app_error', + message: 'Cannot list teams.', + detailed_error: '', + request_id: 'obc374man7bx5r3dbc1q5qhf3r', + status_code: 500 + }.to_json + ) + end + + it 'raises an error with message' do + expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.') + end + end + end +end |