diff options
Diffstat (limited to 'spec/lib')
154 files changed, 5742 insertions, 1498 deletions
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb index 92a27e308d2..c5a854b5660 100644 --- a/spec/lib/backup/repository_spec.rb +++ b/spec/lib/backup/repository_spec.rb @@ -73,37 +73,27 @@ describe Backup::Repository do end end - describe '#delete_all_repositories', :seed_helper do - shared_examples('delete_all_repositories') do - before do - allow(FileUtils).to receive(:mkdir_p).and_call_original - allow(FileUtils).to receive(:mv).and_call_original - end - - after(:all) do - ensure_seeds - end - - it 'removes all repositories' do - # Sanity check: there should be something for us to delete - expect(list_repositories).to include(File.join(SEED_STORAGE_PATH, TEST_REPO_PATH)) + describe '#prepare_directories', :seed_helper do + before do + allow(FileUtils).to receive(:mkdir_p).and_call_original + allow(FileUtils).to receive(:mv).and_call_original + end - subject.delete_all_repositories('default', Gitlab.config.repositories.storages['default']) + after(:all) do + ensure_seeds + end - expect(list_repositories).to be_empty - end + it' removes all repositories' do + # Sanity check: there should be something for us to delete + expect(list_repositories).to include(File.join(SEED_STORAGE_PATH, TEST_REPO_PATH)) - def list_repositories - Dir[File.join(SEED_STORAGE_PATH, '*.git')] - end - end + subject.prepare_directories - context 'with gitaly' do - it_behaves_like 'delete_all_repositories' + expect(list_repositories).to be_empty end - context 'without gitaly', :skip_gitaly_mock do - it_behaves_like 'delete_all_repositories' + def list_repositories + Dir[File.join(SEED_STORAGE_PATH, '*.git')] end end diff --git a/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb index 41f957c4e00..d06c5535309 100644 --- a/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb +++ b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb @@ -7,6 +7,20 @@ describe Banzai::Filter::ImageLazyLoadFilter do %(<img src="#{path}" />) end + def image_with_class(path, class_attr = nil) + %(<img src="#{path}" class="#{class_attr}"/>) + end + + it 'adds a class attribute' do + doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('img')['class']).to eq 'lazy' + end + + it 'appends to the current class attribute' do + doc = filter(image_with_class('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg', 'test')) + expect(doc.at_css('img')['class']).to eq 'test lazy' + end + it 'transforms the image src to a data-src' do doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) expect(doc.at_css('img')['data-src']).to eq '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb index ab14d77d552..a515d07b072 100644 --- a/spec/lib/banzai/filter/markdown_filter_spec.rb +++ b/spec/lib/banzai/filter/markdown_filter_spec.rb @@ -3,17 +3,61 @@ require 'spec_helper' describe Banzai::Filter::MarkdownFilter do include FilterSpecHelper - context 'code block' do - it 'adds language to lang attribute when specified' do - result = filter("```html\nsome code\n```") + describe 'markdown engine from context' do + it 'defaults to CommonMark' do + expect_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark).to receive(:render).and_return('test') - expect(result).to start_with("<pre><code lang=\"html\">") + filter('test') end - it 'does not add language to lang attribute when not specified' do - result = filter("```\nsome code\n```") + it 'uses Redcarpet' do + expect_any_instance_of(Banzai::Filter::MarkdownEngines::Redcarpet).to receive(:render).and_return('test') - expect(result).to start_with("<pre><code>") + filter('test', { markdown_engine: :redcarpet }) + end + + it 'uses CommonMark' do + expect_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark).to receive(:render).and_return('test') + + filter('test', { markdown_engine: :common_mark }) + end + end + + describe 'code block' do + context 'using CommonMark' do + before do + stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark) + end + + it 'adds language to lang attribute when specified' do + result = filter("```html\nsome code\n```") + + expect(result).to start_with("<pre><code lang=\"html\">") + end + + it 'does not add language to lang attribute when not specified' do + result = filter("```\nsome code\n```") + + expect(result).to start_with("<pre><code>") + end + end + + context 'using Redcarpet' do + before do + stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :redcarpet) + end + + it 'adds language to lang attribute when specified' do + result = filter("```html\nsome code\n```") + + expect(result).to start_with("\n<pre><code lang=\"html\">") + end + + it 'does not add language to lang attribute when not specified' do + result = filter("```\nsome code\n```") + + expect(result).to start_with("\n<pre><code>") + end end end 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 a1dd72c498f..55c41e55437 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -210,6 +210,13 @@ describe Banzai::Filter::MergeRequestReferenceFilter do .to eq reference end + it 'commit ref tag is valid' do + doc = reference_filter("See #{reference}") + commit_ref_tag = doc.css('a').first.css('span.gfm.gfm-commit') + + expect(commit_ref_tag.text).to eq(commit.short_id) + end + it 'has valid text' do doc = reference_filter("See #{reference}") diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb index 9a2e521fdcf..919825a6102 100644 --- a/spec/lib/banzai/filter/redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/redactor_filter_spec.rb @@ -46,7 +46,7 @@ describe Banzai::Filter::RedactorFilter do it 'allows permitted Project references' do user = create(:user) project = create(:project) - project.add_master(user) + project.add_maintainer(user) link = reference_link(project: project.id, reference_type: 'test') doc = filter(link, current_user: user) diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index d930c608b18..0b3c2390304 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -54,6 +54,18 @@ describe Banzai::Filter::SanitizationFilter do expect(instance.whitelist[:transformers].size).to eq control_count end + it 'customizes the whitelist only once for different instances' do + instance1 = described_class.new('Foo1') + instance2 = described_class.new('Foo2') + control_count = instance1.whitelist[:transformers].size + + instance1.whitelist + instance2.whitelist + + expect(instance1.whitelist[:transformers].size).to eq control_count + expect(instance2.whitelist[:transformers].size).to eq control_count + end + it 'sanitizes `class` attribute from all elements' do act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>} exp = %q{<pre><code><span class="k">def</span></code></pre>} diff --git a/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb b/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb new file mode 100644 index 00000000000..744df3e0b96 --- /dev/null +++ b/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::Pipeline::EmojiPipeline do + def parse(text) + described_class.to_html(text, {}) + end + + it 'replaces emoji' do + expected_result = "Hello world #{Gitlab::Emoji.gl_emoji_tag('100')}" + + expect(parse('Hello world :100:')).to eq(expected_result) + end + + it 'filters out HTML tags' do + expected_result = "Hello <b>world</b> #{Gitlab::Emoji.gl_emoji_tag('100')}" + + expect(parse('Hello <b>world</b> :100:')).to eq(expected_result) + end +end diff --git a/spec/lib/bitbucket_server/client_spec.rb b/spec/lib/bitbucket_server/client_spec.rb new file mode 100644 index 00000000000..f926ae963a4 --- /dev/null +++ b/spec/lib/bitbucket_server/client_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe BitbucketServer::Client do + let(:base_uri) { 'https://test:7990/stash/' } + let(:options) { { base_uri: base_uri, user: 'bitbucket', password: 'mypassword' } } + let(:project) { 'SOME-PROJECT' } + let(:repo_slug) { 'my-repo' } + let(:headers) { { "Content-Type" => "application/json" } } + + subject { described_class.new(options) } + + describe '#pull_requests' do + let(:path) { "/projects/#{project}/repos/#{repo_slug}/pull-requests?state=ALL" } + + it 'requests a collection' do + expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :pull_request) + + subject.pull_requests(project, repo_slug) + end + + it 'throws an exception when connection fails' do + allow(BitbucketServer::Collection).to receive(:new).and_raise(OpenSSL::SSL::SSLError) + + expect { subject.pull_requests(project, repo_slug) }.to raise_error(described_class::ServerError) + end + end + + describe '#activities' do + let(:path) { "/projects/#{project}/repos/#{repo_slug}/pull-requests/1/activities" } + + it 'requests a collection' do + expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :activity) + + subject.activities(project, repo_slug, 1) + end + end + + describe '#repo' do + let(:path) { "/projects/#{project}/repos/#{repo_slug}" } + let(:url) { "#{base_uri}rest/api/1.0/projects/SOME-PROJECT/repos/my-repo" } + + it 'requests a specific repository' do + stub_request(:get, url).to_return(status: 200, headers: headers, body: '{}') + + subject.repo(project, repo_slug) + + expect(WebMock).to have_requested(:get, url) + end + end + + describe '#repos' do + let(:path) { "/repos" } + + it 'requests a collection' do + expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :repo) + + subject.repos + end + end + + describe '#create_branch' do + let(:branch) { 'test-branch' } + let(:sha) { '12345678' } + let(:url) { "#{base_uri}rest/api/1.0/projects/SOME-PROJECT/repos/my-repo/branches" } + + it 'requests Bitbucket to create a branch' do + stub_request(:post, url).to_return(status: 204, headers: headers, body: '{}') + + subject.create_branch(project, repo_slug, branch, sha) + + expect(WebMock).to have_requested(:post, url) + end + end + + describe '#delete_branch' do + let(:branch) { 'test-branch' } + let(:sha) { '12345678' } + let(:url) { "#{base_uri}rest/branch-utils/1.0/projects/SOME-PROJECT/repos/my-repo/branches" } + + it 'requests Bitbucket to create a branch' do + stub_request(:delete, url).to_return(status: 204, headers: headers, body: '{}') + + subject.delete_branch(project, repo_slug, branch, sha) + + expect(WebMock).to have_requested(:delete, url) + end + end +end diff --git a/spec/lib/bitbucket_server/connection_spec.rb b/spec/lib/bitbucket_server/connection_spec.rb new file mode 100644 index 00000000000..b5da4cb1a49 --- /dev/null +++ b/spec/lib/bitbucket_server/connection_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe BitbucketServer::Connection do + let(:options) { { base_uri: 'https://test:7990', user: 'bitbucket', password: 'mypassword' } } + let(:payload) { { 'test' => 1 } } + let(:headers) { { "Content-Type" => "application/json" } } + let(:url) { 'https://test:7990/rest/api/1.0/test?something=1' } + + subject { described_class.new(options) } + + describe '#get' do + it 'returns JSON body' do + WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_return(body: payload.to_json, status: 200, headers: headers) + + expect(subject.get(url, { something: 1 })).to eq(payload) + end + + it 'throws an exception if the response is not 200' do + WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_return(body: payload.to_json, status: 500, headers: headers) + + expect { subject.get(url) }.to raise_error(described_class::ConnectionError) + end + + it 'throws an exception if the response is not JSON' do + WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_return(body: 'bad data', status: 200, headers: headers) + + expect { subject.get(url) }.to raise_error(described_class::ConnectionError) + end + end + + describe '#post' do + let(:headers) { { 'Accept' => 'application/json', 'Content-Type' => 'application/json' } } + + it 'returns JSON body' do + WebMock.stub_request(:post, url).with(headers: headers).to_return(body: payload.to_json, status: 200, headers: headers) + + expect(subject.post(url, payload)).to eq(payload) + end + + it 'throws an exception if the response is not 200' do + WebMock.stub_request(:post, url).with(headers: headers).to_return(body: payload.to_json, status: 500, headers: headers) + + expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError) + end + end + + describe '#delete' do + let(:headers) { { 'Accept' => 'application/json', 'Content-Type' => 'application/json' } } + + context 'branch API' do + let(:branch_path) { '/projects/foo/repos/bar/branches' } + let(:branch_url) { 'https://test:7990/rest/branch-utils/1.0/projects/foo/repos/bar/branches' } + let(:path) { } + + it 'returns JSON body' do + WebMock.stub_request(:delete, branch_url).with(headers: headers).to_return(body: payload.to_json, status: 200, headers: headers) + + expect(subject.delete(:branches, branch_path, payload)).to eq(payload) + end + + it 'throws an exception if the response is not 200' do + WebMock.stub_request(:delete, branch_url).with(headers: headers).to_return(body: payload.to_json, status: 500, headers: headers) + + expect { subject.delete(:branches, branch_path, payload) }.to raise_error(described_class::ConnectionError) + end + end + end +end diff --git a/spec/lib/bitbucket_server/page_spec.rb b/spec/lib/bitbucket_server/page_spec.rb new file mode 100644 index 00000000000..cf419a9045b --- /dev/null +++ b/spec/lib/bitbucket_server/page_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe BitbucketServer::Page do + let(:response) { { 'values' => [{ 'description' => 'Test' }], 'isLastPage' => false, 'nextPageStart' => 2 } } + + before do + # Autoloading hack + BitbucketServer::Representation::PullRequest.new({}) + end + + describe '#items' do + it 'returns collection of needed objects' do + page = described_class.new(response, :pull_request) + + expect(page.items.first).to be_a(BitbucketServer::Representation::PullRequest) + expect(page.items.count).to eq(1) + end + end + + describe '#attrs' do + it 'returns attributes' do + page = described_class.new(response, :pull_request) + + expect(page.attrs.keys).to include(:isLastPage, :nextPageStart) + end + end + + describe '#next?' do + it 'returns true' do + page = described_class.new(response, :pull_request) + + expect(page.next?).to be_truthy + end + + it 'returns false' do + response['isLastPage'] = true + response.delete('nextPageStart') + page = described_class.new(response, :pull_request) + + expect(page.next?).to be_falsey + end + end + + describe '#next' do + it 'returns next attribute' do + page = described_class.new(response, :pull_request) + + expect(page.next).to eq(2) + end + end +end diff --git a/spec/lib/bitbucket_server/paginator_spec.rb b/spec/lib/bitbucket_server/paginator_spec.rb new file mode 100644 index 00000000000..2de50eba3c4 --- /dev/null +++ b/spec/lib/bitbucket_server/paginator_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe BitbucketServer::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']) } + let(:connection) { instance_double(BitbucketServer::Connection) } + + describe '#items' do + let(:paginator) { described_class.new(connection, 'http://more-data', :pull_request) } + let(:page_attrs) { { 'isLastPage' => false, 'nextPageStart' => 1 } } + + it 'returns items and raises StopIteration in the end' do + 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 + + it 'calls the connection with different offsets' do + expect(connection).to receive(:get).with('http://more-data', start: 0, limit: BitbucketServer::Paginator::PAGE_LENGTH).and_return(page_attrs) + + expect(paginator.items).to eq([]) + + expect(connection).to receive(:get).with('http://more-data', start: 1, limit: BitbucketServer::Paginator::PAGE_LENGTH).and_return({}) + + expect(paginator.items).to eq([]) + + expect { paginator.items }.to raise_error(StopIteration) + end + end +end diff --git a/spec/lib/bitbucket_server/representation/activity_spec.rb b/spec/lib/bitbucket_server/representation/activity_spec.rb new file mode 100644 index 00000000000..15c50e40472 --- /dev/null +++ b/spec/lib/bitbucket_server/representation/activity_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe BitbucketServer::Representation::Activity do + let(:activities) { JSON.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] } + let(:inline_comment) { activities.first } + let(:comment) { activities[3] } + let(:merge_event) { activities[4] } + + describe 'regular comment' do + subject { described_class.new(comment) } + + it { expect(subject.comment?).to be_truthy } + it { expect(subject.inline_comment?).to be_falsey } + it { expect(subject.comment).to be_a(BitbucketServer::Representation::Comment) } + it { expect(subject.created_at).to be_a(Time) } + end + + describe 'inline comment' do + subject { described_class.new(inline_comment) } + + it { expect(subject.comment?).to be_truthy } + it { expect(subject.inline_comment?).to be_truthy } + it { expect(subject.comment).to be_a(BitbucketServer::Representation::PullRequestComment) } + it { expect(subject.created_at).to be_a(Time) } + end + + describe 'merge event' do + subject { described_class.new(merge_event) } + + it { expect(subject.comment?).to be_falsey } + it { expect(subject.inline_comment?).to be_falsey } + it { expect(subject.committer_user).to eq('root') } + it { expect(subject.committer_email).to eq('test.user@example.com') } + it { expect(subject.merge_timestamp).to be_a(Time) } + it { expect(subject.created_at).to be_a(Time) } + it { expect(subject.merge_commit).to eq('839fa9a2d434eb697815b8fcafaecc51accfdbbc') } + end +end diff --git a/spec/lib/bitbucket_server/representation/comment_spec.rb b/spec/lib/bitbucket_server/representation/comment_spec.rb new file mode 100644 index 00000000000..53a20a1d80a --- /dev/null +++ b/spec/lib/bitbucket_server/representation/comment_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe BitbucketServer::Representation::Comment do + let(:activities) { JSON.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] } + let(:comment) { activities.first } + + subject { described_class.new(comment) } + + describe '#id' do + it { expect(subject.id).to eq(9) } + end + + describe '#author_username' do + it { expect(subject.author_username).to eq('root' ) } + end + + describe '#author_email' do + it { expect(subject.author_email).to eq('test.user@example.com' ) } + end + + describe '#note' do + it { expect(subject.note).to eq('is this a new line?') } + end + + describe '#created_at' do + it { expect(subject.created_at).to be_a(Time) } + end + + describe '#updated_at' do + it { expect(subject.created_at).to be_a(Time) } + end + + describe '#comments' do + it { expect(subject.comments.count).to eq(4) } + it { expect(subject.comments).to all( be_a(described_class) ) } + it { expect(subject.comments.map(&:note)).to match_array(["Hello world", "Ok", "hello", "hi"]) } + + # The thread should look like: + # + # is this a new line? (subject) + # -> Hello world (first) + # -> Ok (third) + # -> Hi (fourth) + # -> hello (second) + it 'comments have the right parent' do + first, second, third, fourth = subject.comments[0..4] + + expect(subject.parent_comment).to be_nil + expect(first.parent_comment).to eq(subject) + expect(second.parent_comment).to eq(subject) + expect(third.parent_comment).to eq(first) + expect(fourth.parent_comment).to eq(first) + end + end +end diff --git a/spec/lib/bitbucket_server/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket_server/representation/pull_request_comment_spec.rb new file mode 100644 index 00000000000..bd7e3597486 --- /dev/null +++ b/spec/lib/bitbucket_server/representation/pull_request_comment_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe BitbucketServer::Representation::PullRequestComment do + let(:activities) { JSON.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] } + let(:comment) { activities.second } + + subject { described_class.new(comment) } + + describe '#id' do + it { expect(subject.id).to eq(7) } + end + + describe '#from_sha' do + it { expect(subject.from_sha).to eq('c5f4288162e2e6218180779c7f6ac1735bb56eab') } + end + + describe '#to_sha' do + it { expect(subject.to_sha).to eq('a4c2164330f2549f67c13f36a93884cf66e976be') } + end + + describe '#to?' do + it { expect(subject.to?).to be_falsey } + end + + describe '#from?' do + it { expect(subject.from?).to be_truthy } + end + + describe '#added?' do + it { expect(subject.added?).to be_falsey } + end + + describe '#removed?' do + it { expect(subject.removed?).to be_falsey } + end + + describe '#new_pos' do + it { expect(subject.new_pos).to eq(11) } + end + + describe '#old_pos' do + it { expect(subject.old_pos).to eq(9) } + end + + describe '#file_path' do + it { expect(subject.file_path).to eq('CHANGELOG.md') } + end +end diff --git a/spec/lib/bitbucket_server/representation/pull_request_spec.rb b/spec/lib/bitbucket_server/representation/pull_request_spec.rb new file mode 100644 index 00000000000..4b8afdb006b --- /dev/null +++ b/spec/lib/bitbucket_server/representation/pull_request_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe BitbucketServer::Representation::PullRequest do + let(:sample_data) { JSON.parse(fixture_file('importers/bitbucket_server/pull_request.json')) } + + subject { described_class.new(sample_data) } + + describe '#author' do + it { expect(subject.author).to eq('root') } + end + + describe '#author_email' do + it { expect(subject.author_email).to eq('joe.montana@49ers.com') } + end + + describe '#description' do + it { expect(subject.description).to eq('Test') } + end + + describe '#iid' do + it { expect(subject.iid).to eq(7) } + end + + describe '#state' do + it { expect(subject.state).to eq('merged') } + + context 'declined pull requests' do + before do + sample_data['state'] = 'DECLINED' + end + + it 'returns closed' do + expect(subject.state).to eq('closed') + end + end + + context 'open pull requests' do + before do + sample_data['state'] = 'OPEN' + end + + it 'returns open' do + expect(subject.state).to eq('opened') + end + end + end + + describe '#merged?' do + it { expect(subject.merged?).to be_truthy } + end + + describe '#created_at' do + it { expect(subject.created_at.to_i).to eq(sample_data['createdDate'] / 1000) } + end + + describe '#updated_at' do + it { expect(subject.updated_at.to_i).to eq(sample_data['updatedDate'] / 1000) } + end + + describe '#title' do + it { expect(subject.title).to eq('Added a new line') } + end + + describe '#source_branch_name' do + it { expect(subject.source_branch_name).to eq('refs/heads/root/CODE_OF_CONDUCTmd-1530600625006') } + end + + describe '#source_branch_sha' do + it { expect(subject.source_branch_sha).to eq('074e2b4dddc5b99df1bf9d4a3f66cfc15481fdc8') } + end + + describe '#target_branch_name' do + it { expect(subject.target_branch_name).to eq('refs/heads/master') } + end + + describe '#target_branch_sha' do + it { expect(subject.target_branch_sha).to eq('839fa9a2d434eb697815b8fcafaecc51accfdbbc') } + end +end diff --git a/spec/lib/bitbucket_server/representation/repo_spec.rb b/spec/lib/bitbucket_server/representation/repo_spec.rb new file mode 100644 index 00000000000..3ac1030fbb0 --- /dev/null +++ b/spec/lib/bitbucket_server/representation/repo_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe BitbucketServer::Representation::Repo do + let(:sample_data) do + <<~DATA + { + "slug": "rouge", + "id": 1, + "name": "rouge", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "TEST", + "id": 1, + "name": "test", + "description": "Test", + "public": false, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "http://localhost:7990/projects/TEST" + } + ] + } + }, + "public": false, + "links": { + "clone": [ + { + "href": "http://root@localhost:7990/scm/test/rouge.git", + "name": "http" + }, + { + "href": "ssh://git@localhost:7999/test/rouge.git", + "name": "ssh" + } + ], + "self": [ + { + "href": "http://localhost:7990/projects/TEST/repos/rouge/browse" + } + ] + } + } + DATA + end + + subject { described_class.new(JSON.parse(sample_data)) } + + describe '#project_key' do + it { expect(subject.project_key).to eq('TEST') } + end + + describe '#project_name' do + it { expect(subject.project_name).to eq('test') } + end + + describe '#slug' do + it { expect(subject.slug).to eq('rouge') } + end + + describe '#browse_url' do + it { expect(subject.browse_url).to eq('http://localhost:7990/projects/TEST/repos/rouge/browse') } + end + + describe '#clone_url' do + it { expect(subject.clone_url).to eq('http://root@localhost:7990/scm/test/rouge.git') } + end + + describe '#description' do + it { expect(subject.description).to eq('Test') } + end + + describe '#full_name' do + it { expect(subject.full_name).to eq('test/rouge') } + end +end diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index e13406d1972..8947e2ac4fb 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -203,4 +203,30 @@ describe ExtractsPath do expect(extract_ref_without_atom('foo.atom')).to eq(nil) end end + + describe '#lfs_blob_ids' do + shared_examples '#lfs_blob_ids' do + let(:tag) { @project.repository.add_tag(@project.owner, 'my-annotated-tag', 'master', 'test tag') } + let(:ref) { tag.target } + let(:params) { { ref: ref, path: 'README.md' } } + + before do + @project = create(:project, :repository) + end + + it 'handles annotated tags' do + assign_ref_vars + + expect(lfs_blob_ids).to eq([]) + end + end + + context 'when gitaly is enabled' do + it_behaves_like '#lfs_blob_ids' + end + + context 'when gitaly is disabled', :skip_gitaly_mock do + it_behaves_like '#lfs_blob_ids' + end + end end diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 6eb10497428..f313e675654 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -39,18 +39,36 @@ describe Feature do end describe '.persisted?' do - it 'returns true for a persisted feature' do - Feature::FlipperFeature.create!(key: 'foo') + context 'when the feature is persisted' do + it 'returns true when feature name is a string' do + Feature::FlipperFeature.create!(key: 'foo') + + feature = double(:feature, name: 'foo') + + expect(described_class.persisted?(feature)).to eq(true) + end + + it 'returns true when feature name is a symbol' do + Feature::FlipperFeature.create!(key: 'foo') - feature = double(:feature, name: 'foo') + feature = double(:feature, name: :foo) - expect(described_class.persisted?(feature)).to eq(true) + expect(described_class.persisted?(feature)).to eq(true) + end end - it 'returns false for a feature that is not persisted' do - feature = double(:feature, name: 'foo') + context 'when the feature is not persisted' do + it 'returns false when feature name is a string' do + feature = double(:feature, name: 'foo') + + expect(described_class.persisted?(feature)).to eq(false) + end - expect(described_class.persisted?(feature)).to eq(false) + it 'returns false when feature name is a symbol' do + feature = double(:feature, name: :bar) + + expect(described_class.persisted?(feature)).to eq(false) + end end end diff --git a/spec/lib/gitlab/auth/activity_spec.rb b/spec/lib/gitlab/auth/activity_spec.rb new file mode 100644 index 00000000000..07854cb1eba --- /dev/null +++ b/spec/lib/gitlab/auth/activity_spec.rb @@ -0,0 +1,30 @@ +require 'fast_spec_helper' + +describe Gitlab::Auth::Activity do + describe '.each_counter' do + it 'has all static counters defined' do + described_class.each_counter do |counter| + expect(described_class).to respond_to(counter) + end + end + + it 'has all static incrementers defined' do + described_class.each_counter do |counter| + expect(described_class).to respond_to("#{counter}_increment!") + end + end + + it 'has all counters starting with `user_`' do + described_class.each_counter do |counter| + expect(counter).to start_with('user_') + end + end + + it 'yields counter method, name and description' do + described_class.each_counter do |method, name, description| + expect(method).to eq "#{name}_counter" + expect(description).to start_with('Counter of') + end + end + end +end diff --git a/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb b/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb index 43b68e69131..f39863fdda1 100644 --- a/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb +++ b/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb @@ -1,62 +1,30 @@ require 'spec_helper' describe Gitlab::Auth::BlockedUserTracker do - set(:user) { create(:user) } + describe '#log_blocked_user_activity!' do + context 'when user is not blocked' do + it 'does not log blocked user activity' do + expect_any_instance_of(SystemHooksService) + .not_to receive(:execute_hooks_for) + expect(Gitlab::AppLogger).not_to receive(:info) - describe '.log_if_user_blocked' do - it 'does not log if user failed to login due to undefined reason' do - expect_any_instance_of(SystemHooksService).not_to receive(:execute_hooks_for) + user = create(:user) - expect(described_class.log_if_user_blocked({})).to be_nil - end - - it 'gracefully handles malformed environment variables' do - env = { 'warden.options' => 'test' } - - expect(described_class.log_if_user_blocked(env)).to be_nil - end - - context 'failed login due to blocked user' do - let(:base_env) { { 'warden.options' => { message: User::BLOCKED_MESSAGE } } } - let(:env) { base_env.merge(request_env) } - - subject { described_class.log_if_user_blocked(env) } - - before do - expect_any_instance_of(SystemHooksService).to receive(:execute_hooks_for).with(user, :failed_login) - end - - context 'via GitLab login' do - let(:request_env) { { described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'user' => { 'login' => user.username } } } } - - it 'logs a blocked user' do - user.block! - - expect(subject).to be_truthy - end - - it 'logs a blocked user by e-mail' do - user.block! - env[described_class::ACTIVE_RECORD_REQUEST_PARAMS]['user']['login'] = user.email - - expect(subject).to be_truthy - end + described_class.new(user, spy('auth')).log_activity! end + end - context 'via LDAP login' do - let(:request_env) { { described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'username' => user.username } } } - - it 'logs a blocked user' do - user.block! - - expect(subject).to be_truthy - end + context 'when user is not blocked' do + it 'logs blocked user activity' do + user = create(:user, :blocked) - it 'logs a LDAP blocked user' do - user.ldap_block! + expect_any_instance_of(SystemHooksService) + .to receive(:execute_hooks_for) + .with(user, :failed_login) + expect(Gitlab::AppLogger).to receive(:info) + .with(/Failed login for blocked user/) - expect(subject).to be_truthy - end + described_class.new(user, spy('auth')).log_activity! end end end diff --git a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb index a251ab323d8..1969aed51da 100644 --- a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb +++ b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb @@ -1,31 +1,35 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, schema: 20180619121030 do +describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, schema: 20180619121030 do describe '#perform' do context 'when diff files can be deleted' do let(:merge_request) { create(:merge_request, :merged) } - let(:merge_request_diff) do + let!(:merge_request_diff) do merge_request.create_merge_request_diff merge_request.merge_request_diffs.first end + let(:perform) do + described_class.new.perform(MergeRequestDiff.pluck(:id)) + end + it 'deletes all merge request diff files' do - expect { described_class.new.perform(merge_request_diff.id) } + expect { perform } .to change { merge_request_diff.merge_request_diff_files.count } .from(20).to(0) end it 'updates state to without_files' do - expect { described_class.new.perform(merge_request_diff.id) } + expect { perform } .to change { merge_request_diff.reload.state } .from('collected').to('without_files') end it 'rollsback if something goes wrong' do - expect(MergeRequestDiffFile).to receive_message_chain(:where, :delete_all) + expect(described_class::MergeRequestDiffFile).to receive_message_chain(:where, :delete_all) .and_raise - expect { described_class.new.perform(merge_request_diff.id) } + expect { perform } .to raise_error merge_request_diff.reload @@ -35,35 +39,35 @@ describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, schema: 20180 end end - it 'deletes no merge request diff files when MR is not merged' do - merge_request = create(:merge_request, :opened) - merge_request.create_merge_request_diff - merge_request_diff = merge_request.merge_request_diffs.first - - expect { described_class.new.perform(merge_request_diff.id) } - .not_to change { merge_request_diff.merge_request_diff_files.count } - .from(20) - end - - it 'deletes no merge request diff files when diff is marked as "without_files"' do + it 'reschedules itself when should_wait_deadtuple_vacuum' do merge_request = create(:merge_request, :merged) - merge_request.create_merge_request_diff - merge_request_diff = merge_request.merge_request_diffs.first + first_diff = merge_request.merge_request_diff + second_diff = merge_request.create_merge_request_diff - merge_request_diff.clean! + Sidekiq::Testing.fake! do + worker = described_class.new + allow(worker).to receive(:should_wait_deadtuple_vacuum?) { true } - expect { described_class.new.perform(merge_request_diff.id) } - .not_to change { merge_request_diff.merge_request_diff_files.count } - .from(20) + worker.perform([first_diff.id, second_diff.id]) + + expect(described_class.name.demodulize).to be_scheduled_delayed_migration(5.minutes, [first_diff.id, second_diff.id]) + expect(BackgroundMigrationWorker.jobs.size).to eq(1) + end end + end - it 'deletes no merge request diff files when diff is the latest' do - merge_request = create(:merge_request, :merged) - merge_request_diff = merge_request.merge_request_diff + describe '#should_wait_deadtuple_vacuum?' do + it 'returns true when hitting merge_request_diff_files hits DEAD_TUPLES_THRESHOLD', :postgresql do + worker = described_class.new + threshold_query_result = [{ "n_dead_tup" => described_class::DEAD_TUPLES_THRESHOLD.to_s }] + normal_query_result = [{ "n_dead_tup" => '3' }] + + allow(worker) + .to receive(:execute_statement) + .with(/SELECT n_dead_tup */) + .and_return(threshold_query_result, normal_query_result) - expect { described_class.new.perform(merge_request_diff.id) } - .not_to change { merge_request_diff.merge_request_diff_files.count } - .from(20) + expect(worker.should_wait_deadtuple_vacuum?).to be(true) end end end diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb index 211e3aaa94b..0735ebd6dcb 100644 --- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -9,6 +9,11 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m let(:merge_request) { merge_requests.create!(iid: 1, target_project_id: project.id, source_project_id: project.id, target_branch: 'feature', source_branch: 'master').becomes(MergeRequest) } let(:merge_request_diff) { MergeRequest.find(merge_request.id).create_merge_request_diff } let(:updated_merge_request_diff) { MergeRequestDiff.find(merge_request_diff.id) } + let(:rugged) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.rugged + end + end before do allow_any_instance_of(MergeRequestDiff) @@ -299,11 +304,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m let(:commits) { merge_request_diff.commits.map(&:to_hash) } let(:first_commit) { project.repository.commit(merge_request_diff.head_commit_sha) } let(:expected_commits) { commits } - let(:diffs) do - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - first_commit.rugged_diff_from_parent.patches - end - end + let(:diffs) { rugged_diff(first_commit.sha).patches } let(:expected_diffs) { [] } include_examples 'updated MR diff' @@ -313,14 +314,15 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m let(:commits) { merge_request_diff.commits.map(&:to_hash) } let(:first_commit) { project.repository.commit(merge_request_diff.head_commit_sha) } let(:expected_commits) { commits } - let(:diffs) do - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - first_commit.rugged_diff_from_parent.deltas - end - end + let(:diffs) { rugged_diff(first_commit.sha).deltas } let(:expected_diffs) { [] } include_examples 'updated MR diff' end + + def rugged_diff(commit_sha) + rugged_commit = rugged.lookup(commit_sha) + rugged_commit.parents[0].diff(rugged_commit) + end end end diff --git a/spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb b/spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb new file mode 100644 index 00000000000..20af63bc6c8 --- /dev/null +++ b/spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::FixCrossProjectLabelLinks, :migration, schema: 20180702120647 do + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:issues_table) { table(:issues) } + let(:merge_requests_table) { table(:merge_requests) } + let(:labels_table) { table(:labels) } + let(:label_links_table) { table(:label_links) } + + let!(:group1) { namespaces_table.create(id: 10, type: 'Group', name: 'group1', path: 'group1') } + let!(:group2) { namespaces_table.create(id: 20, type: 'Group', name: 'group2', path: 'group2') } + + let!(:project1) { projects_table.create(id: 1, name: 'project1', path: 'group1/project1', namespace_id: 10) } + let!(:project2) { projects_table.create(id: 3, name: 'project2', path: 'group1/project2', namespace_id: 20) } + + let!(:label1) { labels_table.create(id: 1, title: 'bug', color: 'red', group_id: 10, type: 'GroupLabel') } + let!(:label2) { labels_table.create(id: 2, title: 'bug', color: 'red', group_id: 20, type: 'GroupLabel') } + + def create_merge_request(id, project_id) + merge_requests_table.create(id: id, + target_project_id: project_id, + target_branch: 'master', + source_project_id: project_id, + source_branch: 'mr name', + title: "mr name#{id}") + end + + def create_issue(id, project_id) + issues_table.create(id: id, title: "issue#{id}", project_id: project_id) + end + + def create_resource(target_type, id, project_id) + target_type == 'Issue' ? create_issue(id, project_id) : create_merge_request(id, project_id) + end + + shared_examples_for 'resource with cross-project labels' do + it 'updates only cross-project label links which exist in the local project or group' do + create_resource(target_type, 1, 1) + create_resource(target_type, 2, 3) + labels_table.create(id: 3, title: 'bug', color: 'red', project_id: 3, type: 'ProjectLabel') + link = label_links_table.create(label_id: 2, target_type: target_type, target_id: 1) + link2 = label_links_table.create(label_id: 3, target_type: target_type, target_id: 2) + + subject.perform(1, 100) + + expect(link.reload.label_id).to eq(1) + expect(link2.reload.label_id).to eq(3) + end + + it 'ignores cross-project label links if label color is different' do + labels_table.create(id: 3, title: 'bug', color: 'green', group_id: 20, type: 'GroupLabel') + create_resource(target_type, 1, 1) + link = label_links_table.create(label_id: 3, target_type: target_type, target_id: 1) + + subject.perform(1, 100) + + expect(link.reload.label_id).to eq(3) + end + + it 'ignores cross-project label links if label name is different' do + labels_table.create(id: 3, title: 'bug1', color: 'red', group_id: 20, type: 'GroupLabel') + create_resource(target_type, 1, 1) + link = label_links_table.create(label_id: 3, target_type: target_type, target_id: 1) + + subject.perform(1, 100) + + expect(link.reload.label_id).to eq(3) + end + + context 'with nested group' do + before do + namespaces_table.create(id: 11, type: 'Group', name: 'subgroup1', path: 'group1/subgroup1', parent_id: 10) + projects_table.create(id: 2, name: 'subproject1', path: 'group1/subgroup1/subproject1', namespace_id: 11) + create_resource(target_type, 1, 2) + end + + it 'ignores label links referencing ancestor group labels', :nested_groups do + labels_table.create(id: 4, title: 'bug', color: 'red', project_id: 2, type: 'ProjectLabel') + label_links_table.create(label_id: 4, target_type: target_type, target_id: 1) + link = label_links_table.create(label_id: 1, target_type: target_type, target_id: 1) + + subject.perform(1, 100) + + expect(link.reload.label_id).to eq(1) + end + + it 'checks also issues and MRs in subgroups', :nested_groups do + link = label_links_table.create(label_id: 2, target_type: target_type, target_id: 1) + + subject.perform(1, 100) + + expect(link.reload.label_id).to eq(1) + end + end + end + + context 'resource is Issue' do + it_behaves_like 'resource with cross-project labels' do + let(:target_type) { 'Issue' } + end + end + + context 'resource is Merge Request' do + it_behaves_like 'resource with cross-project labels' do + let(:target_type) { 'MergeRequest' } + end + end +end diff --git a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb index ee60e498b59..2e77e80ee46 100644 --- a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb +++ b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::BackgroundMigration::MovePersonalSnippetFiles do let(:snippet) do snippet = create(:personal_snippet) create_upload_for_snippet(snippet) - snippet.update_attributes!(description: markdown_linking_file(snippet)) + snippet.update!(description: markdown_linking_file(snippet)) snippet end diff --git a/spec/lib/gitlab/background_migration/remove_restricted_todos_spec.rb b/spec/lib/gitlab/background_migration/remove_restricted_todos_spec.rb new file mode 100644 index 00000000000..dae754112dc --- /dev/null +++ b/spec/lib/gitlab/background_migration/remove_restricted_todos_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::BackgroundMigration::RemoveRestrictedTodos, :migration, schema: 20180704204006 do + let(:projects) { table(:projects) } + let(:users) { table(:users) } + let(:todos) { table(:todos) } + let(:issues) { table(:issues) } + let(:assignees) { table(:issue_assignees) } + let(:project_authorizations) { table(:project_authorizations) } + let(:project_features) { table(:project_features) } + + let(:todo_params) { { author_id: 1, target_type: 'Issue', action: 1, state: :pending } } + + before do + users.create(id: 1, email: 'user@example.com', projects_limit: 10) + users.create(id: 2, email: 'reporter@example.com', projects_limit: 10) + users.create(id: 3, email: 'guest@example.com', projects_limit: 10) + + projects.create!(id: 1, name: 'project-1', path: 'project-1', visibility_level: 0, namespace_id: 1) + projects.create!(id: 2, name: 'project-2', path: 'project-2', visibility_level: 0, namespace_id: 1) + + issues.create(id: 1, project_id: 1) + issues.create(id: 2, project_id: 2) + + project_authorizations.create(user_id: 2, project_id: 2, access_level: 20) # reporter + project_authorizations.create(user_id: 3, project_id: 2, access_level: 10) # guest + + todos.create(todo_params.merge(user_id: 1, project_id: 1, target_id: 1)) # out of project ids range + todos.create(todo_params.merge(user_id: 1, project_id: 2, target_id: 2)) # non member + todos.create(todo_params.merge(user_id: 2, project_id: 2, target_id: 2)) # reporter + todos.create(todo_params.merge(user_id: 3, project_id: 2, target_id: 2)) # guest + end + + subject { described_class.new.perform(2, 5) } + + context 'when a project is private' do + it 'removes todos of users without project access' do + expect { subject }.to change { Todo.count }.from(4).to(3) + end + + context 'with a confidential issue' do + it 'removes todos of users without project access and guests for confidential issues' do + issues.create(id: 3, project_id: 2, confidential: true) + issues.create(id: 4, project_id: 1, confidential: true) # not in the batch + todos.create(todo_params.merge(user_id: 3, project_id: 2, target_id: 3)) + todos.create(todo_params.merge(user_id: 2, project_id: 2, target_id: 3)) + todos.create(todo_params.merge(user_id: 1, project_id: 1, target_id: 4)) + + expect { subject }.to change { Todo.count }.from(7).to(5) + end + end + end + + context 'when a project is public' do + before do + projects.find(2).update_attribute(:visibility_level, 20) + end + + context 'when all features have the same visibility as the project, no confidential issues' do + it 'does not remove any todos' do + expect { subject }.not_to change { Todo.count } + end + end + + context 'with confidential issues' do + before do + users.create(id: 4, email: 'author@example.com', projects_limit: 10) + users.create(id: 5, email: 'assignee@example.com', projects_limit: 10) + issues.create(id: 3, project_id: 2, confidential: true, author_id: 4) + assignees.create(user_id: 5, issue_id: 3) + + todos.create(todo_params.merge(user_id: 1, project_id: 2, target_id: 3)) # to be deleted + todos.create(todo_params.merge(user_id: 2, project_id: 2, target_id: 3)) # authorized user + todos.create(todo_params.merge(user_id: 3, project_id: 2, target_id: 3)) # to be deleted guest + todos.create(todo_params.merge(user_id: 4, project_id: 2, target_id: 3)) # conf issue author + todos.create(todo_params.merge(user_id: 5, project_id: 2, target_id: 3)) # conf issue assignee + end + + it 'removes confidential issue todos for non authorized users' do + expect { subject }.to change { Todo.count }.from(9).to(7) + end + end + + context 'features visibility restrictions' do + before do + todo_params.merge!(project_id: 2, user_id: 1, target_id: 3) + todos.create(todo_params.merge(user_id: 1, target_id: 3, target_type: 'MergeRequest')) + todos.create(todo_params.merge(user_id: 1, target_id: 3, target_type: 'Commit')) + end + + context 'when issues are restricted to project members' do + before do + project_features.create(issues_access_level: 10, project_id: 2) + end + + it 'removes non members issue todos' do + expect { subject }.to change { Todo.count }.from(6).to(5) + end + end + + context 'when merge requests are restricted to project members' do + before do + project_features.create(merge_requests_access_level: 10, project_id: 2) + end + + it 'removes non members issue todos' do + expect { subject }.to change { Todo.count }.from(6).to(5) + end + end + + context 'when repository and merge requests are restricted to project members' do + before do + project_features.create(repository_access_level: 10, merge_requests_access_level: 10, project_id: 2) + end + + it 'removes non members commit and merge requests todos' do + expect { subject }.to change { Todo.count }.from(6).to(4) + end + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/schedule_diff_files_deletion_spec.rb b/spec/lib/gitlab/background_migration/schedule_diff_files_deletion_spec.rb new file mode 100644 index 00000000000..ec8ba0ce127 --- /dev/null +++ b/spec/lib/gitlab/background_migration/schedule_diff_files_deletion_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::ScheduleDiffFilesDeletion, :migration, :sidekiq, schema: 20180619121030 do + describe '#perform' do + let(:merge_request_diffs) { table(:merge_request_diffs) } + let(:merge_requests) { table(:merge_requests) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + + before do + stub_const("#{described_class.name}::DIFF_BATCH_SIZE", 3) + + namespaces.create!(id: 1, name: 'gitlab', path: 'gitlab') + projects.create!(id: 1, namespace_id: 1, name: 'gitlab', path: 'gitlab') + + merge_requests.create!(id: 1, target_project_id: 1, source_project_id: 1, target_branch: 'feature', source_branch: 'master', state: 'merged') + + merge_request_diffs.create!(id: 1, merge_request_id: 1, state: 'collected') + merge_request_diffs.create!(id: 2, merge_request_id: 1, state: 'empty') + merge_request_diffs.create!(id: 3, merge_request_id: 1, state: 'without_files') + merge_request_diffs.create!(id: 4, merge_request_id: 1, state: 'collected') + merge_request_diffs.create!(id: 5, merge_request_id: 1, state: 'collected') + merge_request_diffs.create!(id: 6, merge_request_id: 1, state: 'collected') + merge_request_diffs.create!(id: 7, merge_request_id: 1, state: 'collected') + + merge_requests.update(1, latest_merge_request_diff_id: 7) + end + + it 'correctly schedules diff file deletion workers' do + Sidekiq::Testing.fake! do + Timecop.freeze do + described_class.new.perform + + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, [1, 4, 5]) + + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, [6]) + + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end + end +end diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb index 468f6ff6d24..6e21c846c0a 100644 --- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb @@ -1,10 +1,11 @@ require 'spec_helper' -describe Gitlab::BareRepositoryImport::Importer, repository: true do +describe Gitlab::BareRepositoryImport::Importer, :seed_helper do let!(:admin) { create(:admin) } let!(:base_dir) { Dir.mktmpdir + '/' } let(:bare_repository) { Gitlab::BareRepositoryImport::Repository.new(base_dir, File.join(base_dir, "#{project_path}.git")) } let(:gitlab_shell) { Gitlab::Shell.new } + let(:source_project) { TEST_REPO_PATH } subject(:importer) { described_class.new(admin, bare_repository) } @@ -17,16 +18,11 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do after do FileUtils.rm_rf(base_dir) + TestEnv.clean_test_path + ensure_seeds Rainbow.enabled = @rainbow end - around do |example| - # TODO migrate BareRepositoryImport https://gitlab.com/gitlab-org/gitaly/issues/953 - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - shared_examples 'importing a repository' do describe '.execute' do it 'creates a project for a repository in storage' do @@ -86,8 +82,8 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do importer.create_project_if_needed end - it 'creates the Git repo on disk with the proper symlink for hooks' do - create_bare_repository("#{project_path}.git") + it 'creates the Git repo on disk' do + prepare_repository("#{project_path}.git", source_project) importer.create_project_if_needed @@ -97,9 +93,6 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do expect(gitlab_shell.exists?(project.repository_storage, repo_path)).to be(true) expect(gitlab_shell.exists?(project.repository_storage, hook_path)).to be(true) - - full_hook_path = File.join(project.repository.path_to_repo, 'hooks') - expect(File.readlink(full_hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) end context 'hashed storage enabled' do @@ -148,7 +141,7 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do end it 'creates the Git repo in disk' do - create_bare_repository("#{project_path}.git") + prepare_repository("#{project_path}.git", source_project) importer.create_project_if_needed @@ -158,23 +151,23 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do expect(gitlab_shell.exists?(project.repository_storage, project.disk_path + '.wiki.git')).to be(true) end - it 'moves an existing project to the correct path' do + context 'with a repository already on disk' do + let!(:base_dir) { TestEnv.repos_path } # This is a quick way to get a valid repository instead of copying an # existing one. Since it's not persisted, the importer will try to # create the project. - project = build(:project, :legacy_storage, :repository) - original_commit_count = project.repository.commit_count - - legacy_path = Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path + let(:project) { build(:project, :legacy_storage, :repository) } + let(:project_path) { project.full_path } - bare_repo = Gitlab::BareRepositoryImport::Repository.new(legacy_path, project.repository.path) - gitlab_importer = described_class.new(admin, bare_repo) + it 'moves an existing project to the correct path' do + original_commit_count = project.repository.commit_count - expect(gitlab_importer).to receive(:create_project).and_call_original + expect(importer).to receive(:create_project).and_call_original - new_project = gitlab_importer.create_project_if_needed + new_project = importer.create_project_if_needed - expect(new_project.repository.commit_count).to eq(original_commit_count) + expect(new_project.repository.commit_count).to eq(original_commit_count) + end end end @@ -185,8 +178,8 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do it_behaves_like 'importing a repository' it 'creates the Wiki git repo in disk' do - create_bare_repository("#{project_path}.git") - create_bare_repository("#{project_path}.wiki.git") + prepare_repository("#{project_path}.git", source_project) + prepare_repository("#{project_path}.wiki.git", source_project) expect(Projects::CreateService).to receive(:new).with(admin, hash_including(skip_wiki: true, import_type: 'bare_repository')).and_call_original @@ -213,8 +206,13 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do end end - def create_bare_repository(project_path) + def prepare_repository(project_path, source_project) repo_path = File.join(base_dir, project_path) - Gitlab::Git::Repository.create(repo_path, bare: true) + + return create_bare_repository(repo_path) unless source_project + + cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{source_project} #{repo_path}) + + system(git_env, *cmd, chdir: SEED_STORAGE_PATH, out: '/dev/null', err: '/dev/null') end end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index 05c232d22cf..7a681bc6610 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -69,6 +69,7 @@ describe Gitlab::BitbucketImport::Importer do let(:project) do create( :project, + :repository, import_source: project_identifier, import_url: "https://bitbucket.org/#{project_identifier}.git", import_data_attributes: { credentials: data } @@ -85,10 +86,84 @@ describe Gitlab::BitbucketImport::Importer do } end + let(:sample) { RepoHelpers.sample_compare } + before do allow(importer).to receive(:gitlab_shell) { gitlab_shell } end + subject { described_class.new(project) } + + describe '#import_pull_requests' do + before do + allow(subject).to receive(:import_wiki) + allow(subject).to receive(:import_issues) + + pull_request = instance_double( + Bitbucket::Representation::PullRequest, + iid: 10, + source_branch_sha: sample.commits.last, + source_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.source_branch, + target_branch_sha: sample.commits.first, + target_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.target_branch, + title: 'This is a title', + description: 'This is a test pull request', + state: 'merged', + author: 'other', + created_at: Time.now, + updated_at: Time.now) + + # https://gitlab.com/gitlab-org/gitlab-test/compare/c1acaa58bbcbc3eafe538cb8274ba387047b69f8...5937ac0a7beb003549fc5fd26fc247ad + @inline_note = instance_double( + Bitbucket::Representation::PullRequestComment, + iid: 2, + file_path: '.gitmodules', + old_pos: nil, + new_pos: 4, + note: 'Hello world', + author: 'root', + created_at: Time.now, + updated_at: Time.now, + inline?: true, + has_parent?: false) + + @reply = instance_double( + Bitbucket::Representation::PullRequestComment, + iid: 3, + file_path: '.gitmodules', + note: 'Hello world', + author: 'root', + created_at: Time.now, + updated_at: Time.now, + inline?: true, + has_parent?: true, + parent_id: 2) + + comments = [@inline_note, @reply] + + allow(subject.client).to receive(:repo) + allow(subject.client).to receive(:pull_requests).and_return([pull_request]) + allow(subject.client).to receive(:pull_request_comments).with(anything, pull_request.iid).and_return(comments) + end + + it 'imports threaded discussions' do + expect { subject.execute }.to change { MergeRequest.count }.by(1) + + merge_request = MergeRequest.first + expect(merge_request.notes.count).to eq(2) + expect(merge_request.notes.map(&:discussion_id).uniq.count).to eq(1) + + notes = merge_request.notes.order(:id).to_a + start_note = notes.first + expect(start_note).to be_a(DiffNote) + expect(start_note.note).to eq(@inline_note.note) + + reply_note = notes.last + expect(reply_note).to be_a(DiffNote) + expect(reply_note.note).to eq(@reply.note) + end + end + context 'issues statuses' do before do # HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this diff --git a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb new file mode 100644 index 00000000000..70423823b89 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb @@ -0,0 +1,291 @@ +require 'spec_helper' + +describe Gitlab::BitbucketServerImport::Importer do + include ImportSpecHelper + + let(:project) { create(:project, :repository, import_url: 'http://my-bitbucket') } + let(:now) { Time.now.utc.change(usec: 0) } + let(:project_key) { 'TEST' } + let(:repo_slug) { 'rouge' } + let(:sample) { RepoHelpers.sample_compare } + + subject { described_class.new(project, recover_missing_commits: true) } + + before do + data = project.create_or_update_import_data( + data: { project_key: project_key, repo_slug: repo_slug }, + credentials: { base_uri: 'http://my-bitbucket', user: 'bitbucket', password: 'test' } + ) + data.save + project.save + end + + describe '#import_repository' do + before do + expect(subject).to receive(:import_pull_requests) + expect(subject).to receive(:delete_temp_branches) + end + + it 'adds a remote' do + expect(project.repository).to receive(:fetch_as_mirror) + .with('http://bitbucket:test@my-bitbucket', + refmap: [:heads, :tags, '+refs/pull-requests/*/to:refs/merge-requests/*/head'], + remote_name: 'bitbucket_server') + + subject.execute + end + end + + describe '#import_pull_requests' do + before do + allow(subject).to receive(:import_repository) + allow(subject).to receive(:delete_temp_branches) + allow(subject).to receive(:restore_branches) + + pull_request = instance_double( + BitbucketServer::Representation::PullRequest, + iid: 10, + source_branch_sha: sample.commits.last, + source_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.source_branch, + target_branch_sha: sample.commits.first, + target_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.target_branch, + title: 'This is a title', + description: 'This is a test pull request', + state: 'merged', + author: 'Test Author', + author_email: project.owner.email, + created_at: Time.now, + updated_at: Time.now, + merged?: true) + + allow(subject.client).to receive(:pull_requests).and_return([pull_request]) + + @merge_event = instance_double( + BitbucketServer::Representation::Activity, + comment?: false, + merge_event?: true, + committer_email: project.owner.email, + merge_timestamp: now, + merge_commit: '12345678' + ) + + @pr_note = instance_double( + BitbucketServer::Representation::Comment, + note: 'Hello world', + author_email: 'unknown@gmail.com', + author_username: 'The Flash', + comments: [], + created_at: now, + updated_at: now, + parent_comment: nil) + + @pr_comment = instance_double( + BitbucketServer::Representation::Activity, + comment?: true, + inline_comment?: false, + merge_event?: false, + comment: @pr_note) + end + + it 'imports merge event' do + expect(subject.client).to receive(:activities).and_return([@merge_event]) + + expect { subject.execute }.to change { MergeRequest.count }.by(1) + + merge_request = MergeRequest.first + expect(merge_request.metrics.merged_by).to eq(project.owner) + expect(merge_request.metrics.merged_at).to eq(@merge_event.merge_timestamp) + expect(merge_request.merge_commit_sha).to eq('12345678') + end + + it 'imports comments' do + expect(subject.client).to receive(:activities).and_return([@pr_comment]) + + expect { subject.execute }.to change { MergeRequest.count }.by(1) + + merge_request = MergeRequest.first + expect(merge_request.notes.count).to eq(1) + note = merge_request.notes.first + expect(note.note).to end_with(@pr_note.note) + expect(note.author).to eq(project.owner) + expect(note.created_at).to eq(@pr_note.created_at) + expect(note.updated_at).to eq(@pr_note.created_at) + end + + it 'imports threaded discussions' do + reply = instance_double( + BitbucketServer::Representation::PullRequestComment, + author_email: 'someuser@gitlab.com', + author_username: 'Batman', + note: 'I agree', + created_at: now, + updated_at: now) + + # https://gitlab.com/gitlab-org/gitlab-test/compare/c1acaa58bbcbc3eafe538cb8274ba387047b69f8...5937ac0a7beb003549fc5fd26fc247ad + inline_note = instance_double( + BitbucketServer::Representation::PullRequestComment, + file_type: 'ADDED', + from_sha: sample.commits.first, + to_sha: sample.commits.last, + file_path: '.gitmodules', + old_pos: nil, + new_pos: 4, + note: 'Hello world', + author_email: 'unknown@gmail.com', + author_username: 'Superman', + comments: [reply], + created_at: now, + updated_at: now, + parent_comment: nil) + + allow(reply).to receive(:parent_comment).and_return(inline_note) + + inline_comment = instance_double( + BitbucketServer::Representation::Activity, + comment?: true, + inline_comment?: true, + merge_event?: false, + comment: inline_note) + + expect(subject.client).to receive(:activities).and_return([inline_comment]) + + expect { subject.execute }.to change { MergeRequest.count }.by(1) + + merge_request = MergeRequest.first + expect(merge_request.notes.count).to eq(2) + expect(merge_request.notes.map(&:discussion_id).uniq.count).to eq(1) + + notes = merge_request.notes.order(:id).to_a + start_note = notes.first + expect(start_note.type).to eq('DiffNote') + expect(start_note.note).to end_with(inline_note.note) + expect(start_note.created_at).to eq(inline_note.created_at) + expect(start_note.updated_at).to eq(inline_note.updated_at) + expect(start_note.position.base_sha).to eq(inline_note.from_sha) + expect(start_note.position.start_sha).to eq(inline_note.from_sha) + expect(start_note.position.head_sha).to eq(inline_note.to_sha) + expect(start_note.position.old_line).to be_nil + expect(start_note.position.new_line).to eq(inline_note.new_pos) + + reply_note = notes.last + # Make sure author and reply context is included + expect(reply_note.note).to start_with("*By #{reply.author_username} (#{reply.author_email})*\n\n") + expect(reply_note.note).to end_with("> #{inline_note.note}\n\n#{reply.note}") + expect(reply_note.author).to eq(project.owner) + expect(reply_note.created_at).to eq(reply.created_at) + expect(reply_note.updated_at).to eq(reply.created_at) + expect(reply_note.position.base_sha).to eq(inline_note.from_sha) + expect(reply_note.position.start_sha).to eq(inline_note.from_sha) + expect(reply_note.position.head_sha).to eq(inline_note.to_sha) + expect(reply_note.position.old_line).to be_nil + expect(reply_note.position.new_line).to eq(inline_note.new_pos) + end + + it 'falls back to comments if diff comments fail to validate' do + reply = instance_double( + BitbucketServer::Representation::Comment, + author_email: 'someuser@gitlab.com', + author_username: 'Aquaman', + note: 'I agree', + created_at: now, + updated_at: now) + + # https://gitlab.com/gitlab-org/gitlab-test/compare/c1acaa58bbcbc3eafe538cb8274ba387047b69f8...5937ac0a7beb003549fc5fd26fc247ad + inline_note = instance_double( + BitbucketServer::Representation::PullRequestComment, + file_type: 'REMOVED', + from_sha: sample.commits.first, + to_sha: sample.commits.last, + file_path: '.gitmodules', + old_pos: 8, + new_pos: 9, + note: 'This is a note with an invalid line position.', + author_email: project.owner.email, + author_username: 'Owner', + comments: [reply], + created_at: now, + updated_at: now, + parent_comment: nil) + + inline_comment = instance_double( + BitbucketServer::Representation::Activity, + comment?: true, + inline_comment?: true, + merge_event?: false, + comment: inline_note) + + allow(reply).to receive(:parent_comment).and_return(inline_note) + + expect(subject.client).to receive(:activities).and_return([inline_comment]) + + expect { subject.execute }.to change { MergeRequest.count }.by(1) + + merge_request = MergeRequest.first + expect(merge_request.notes.count).to eq(2) + notes = merge_request.notes + + expect(notes.first.note).to start_with('*Comment on .gitmodules') + expect(notes.second.note).to start_with('*Comment on .gitmodules') + end + end + + describe 'inaccessible branches' do + let(:id) { 10 } + let(:temp_branch_from) { "gitlab/import/pull-request/#{id}/from" } + let(:temp_branch_to) { "gitlab/import/pull-request/#{id}/to" } + + before do + pull_request = instance_double( + BitbucketServer::Representation::PullRequest, + iid: id, + source_branch_sha: '12345678', + source_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.source_branch, + target_branch_sha: '98765432', + target_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.target_branch, + title: 'This is a title', + description: 'This is a test pull request', + state: 'merged', + author: 'Test Author', + author_email: project.owner.email, + created_at: Time.now, + updated_at: Time.now, + merged?: true) + + expect(subject.client).to receive(:pull_requests).and_return([pull_request]) + expect(subject.client).to receive(:activities).and_return([]) + expect(subject).to receive(:import_repository).twice + end + + it '#restore_branches' do + expect(subject).to receive(:restore_branches).and_call_original + expect(subject).to receive(:delete_temp_branches) + expect(subject.client).to receive(:create_branch) + .with(project_key, repo_slug, + temp_branch_from, + '12345678') + expect(subject.client).to receive(:create_branch) + .with(project_key, repo_slug, + temp_branch_to, + '98765432') + + expect { subject.execute }.to change { MergeRequest.count }.by(1) + end + + it '#delete_temp_branches' do + expect(subject.client).to receive(:create_branch).twice + expect(subject).to receive(:delete_temp_branches).and_call_original + expect(subject.client).to receive(:delete_branch) + .with(project_key, repo_slug, + temp_branch_from, + '12345678') + expect(subject.client).to receive(:delete_branch) + .with(project_key, repo_slug, + temp_branch_to, + '98765432') + expect(project.repository).to receive(:delete_branch).with(temp_branch_from) + expect(project.repository).to receive(:delete_branch).with(temp_branch_to) + + expect { subject.execute }.to change { MergeRequest.count }.by(1) + end + end +end diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 1cb8143a9e9..4df426c54ae 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -54,7 +54,7 @@ describe Gitlab::Checks::ChangeAccess do context 'as maintainer' do before do - project.add_master(user) + project.add_maintainer(user) end context 'deletion' do @@ -132,6 +132,16 @@ describe Gitlab::Checks::ChangeAccess do expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.') end + context 'when project repository is empty' do + let(:project) { create(:project) } + + it 'raises an error if the user is not allowed to push to protected branches' do + expect(user_access).to receive(:can_push_to_branch?).and_return(false) + + expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /Ask a project Owner or Maintainer to create a default branch/) + end + end + context 'branch deletion' do let(:newrev) { '0000000000000000000000000000000000000000' } let(:ref) { 'refs/heads/feature' } @@ -144,7 +154,7 @@ describe Gitlab::Checks::ChangeAccess do context 'if the user is allowed to delete protected branches' do before do - project.add_master(user) + project.add_maintainer(user) end context 'through the web interface' do diff --git a/spec/lib/gitlab/ci/build/artifacts/gzip_file_adapter_spec.rb b/spec/lib/gitlab/ci/build/artifacts/gzip_file_adapter_spec.rb new file mode 100644 index 00000000000..384329dda18 --- /dev/null +++ b/spec/lib/gitlab/ci/build/artifacts/gzip_file_adapter_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Artifacts::GzipFileAdapter do + describe '#initialize' do + context 'when stream is passed' do + let(:stream) { File.open(expand_fixture_path('junit/junit.xml.gz'), 'rb') } + + it 'initialized' do + expect { described_class.new(stream) }.not_to raise_error + end + end + + context 'when stream is not passed' do + let(:stream) { nil } + + it 'raises an error' do + expect { described_class.new(stream) }.to raise_error(described_class::InvalidStreamError) + end + end + end + + describe '#each_blob' do + let(:adapter) { described_class.new(stream) } + + context 'when stream is gzip file' do + context 'when gzip file contains one file' do + let(:stream) { File.open(expand_fixture_path('junit/junit.xml.gz'), 'rb') } + + it 'iterates content and file_name' do + expect { |b| adapter.each_blob(&b) } + .to yield_with_args(fixture_file('junit/junit.xml'), 'rspec.xml') + end + end + + context 'when gzip file contains three files' do + let(:stream) { File.open(expand_fixture_path('junit/junit_with_three_testsuites.xml.gz'), 'rb') } + + it 'iterates content and file_name' do + expect { |b| adapter.each_blob(&b) } + .to yield_successive_args( + [fixture_file('junit/junit_with_three_testsuites_1.xml'), 'rspec-3.xml'], + [fixture_file('junit/junit_with_three_testsuites_2.xml'), 'rspec-1.xml'], + [fixture_file('junit/junit_with_three_testsuites_3.xml'), 'rspec-2.xml']) + end + end + end + + context 'when stream is zip file' do + let(:stream) { File.open(expand_fixture_path('ci_build_artifacts.zip'), 'rb') } + + it 'raises an error' do + expect { |b| adapter.each_blob(&b) }.to raise_error(described_class::InvalidStreamError) + end + end + end +end diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb index 6a52ae01b2f..e327399d82d 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb @@ -2,13 +2,21 @@ require 'spec_helper' describe Gitlab::Ci::Build::Artifacts::Metadata do def metadata(path = '', **opts) - described_class.new(metadata_file_path, path, **opts) + described_class.new(metadata_file_stream, path, **opts) end let(:metadata_file_path) do Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' end + let(:metadata_file_stream) do + File.open(metadata_file_path) if metadata_file_path + end + + after do + metadata_file_stream&.close + end + context 'metadata file exists' do describe '#find_entries! empty string' do subject { metadata('').find_entries! } @@ -86,11 +94,21 @@ describe Gitlab::Ci::Build::Artifacts::Metadata do end context 'metadata file does not exist' do - let(:metadata_file_path) { '' } + let(:metadata_file_path) { nil } + + describe '#find_entries!' do + it 'raises error' do + expect { metadata.find_entries! }.to raise_error(described_class::InvalidStreamError, /Invalid stream/) + end + end + end + + context 'metadata file is invalid' do + let(:metadata_file_path) { Rails.root + 'spec/fixtures/ci_build_artifacts.zip' } describe '#find_entries!' do it 'raises error' do - expect { metadata.find_entries! }.to raise_error(Errno::ENOENT) + expect { metadata.find_entries! }.to raise_error(described_class::InvalidStreamError, /not in gzip format/) end end end diff --git a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb index 5c31423fdee..d48aac15f28 100644 --- a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb @@ -18,6 +18,14 @@ describe Gitlab::Ci::Config::Entry::Artifacts do expect(entry).to be_valid end end + + context "when value includes 'reports' keyword" do + let(:config) { { paths: %w[public/], reports: { junit: 'junit.xml' } } } + + it 'returns general artifact and report-type artifacts configuration' do + expect(entry.value).to eq config + end + end end context 'when entry value is not correct' do @@ -39,6 +47,15 @@ describe Gitlab::Ci::Config::Entry::Artifacts do .to include 'artifacts config contains unknown keys: test' end end + + context "when 'reports' keyword is not hash" do + let(:config) { { paths: %w[public/], reports: 'junit.xml' } } + + it 'reports error' do + expect(entry.errors) + .to include 'artifacts reports should be a hash' + end + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb index afa4a089418..8934aeb83db 100644 --- a/spec/lib/gitlab/ci/config/entry/commands_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb @@ -41,8 +41,7 @@ describe Gitlab::Ci::Config::Entry::Commands do describe '#errors' do it 'saves errors' do expect(entry.errors) - .to include 'commands config should be a ' \ - 'string or an array of strings' + .to include 'commands config should be an array of strings or a string' end end end diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb new file mode 100644 index 00000000000..b3a3a6bee1d --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Reports do + let(:entry) { described_class.new(config) } + + describe 'validation' do + context 'when entry config value is correct' do + let(:config) { { junit: %w[junit.xml] } } + + describe '#value' do + it 'returns artifacs configuration' do + expect(entry.value).to eq config + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'when value is not array' do + let(:config) { { junit: 'junit.xml' } } + + it 'converts to array' do + expect(entry.value).to eq({ junit: ['junit.xml'] } ) + end + end + end + + context 'when entry value is not correct' do + describe '#errors' do + context 'when value of attribute is invalid' do + let(:config) { { junit: 10 } } + + it 'reports error' do + expect(entry.errors) + .to include 'reports junit should be an array of strings or a string' + end + end + + context 'when there is an unknown key present' do + let(:config) { { codeclimate: 'codeclimate.json' } } + + it 'reports error' do + expect(entry.errors) + .to include 'reports config contains unknown keys: codeclimate' + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/parsers/junit_spec.rb b/spec/lib/gitlab/ci/parsers/junit_spec.rb new file mode 100644 index 00000000000..f7ec86f5385 --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/junit_spec.rb @@ -0,0 +1,118 @@ +require 'spec_helper' + +describe Gitlab::Ci::Parsers::Junit do + describe '#parse!' do + subject { described_class.new.parse!(junit, test_suite) } + + let(:test_suite) { Gitlab::Ci::Reports::TestSuite.new('rspec') } + let(:test_cases) { flattened_test_cases(test_suite) } + + context 'when data is JUnit style XML' do + context 'when there are no test cases' do + let(:junit) do + <<-EOF.strip_heredoc + <testsuite></testsuite> + EOF + end + + it 'raises an error and does not add any test cases' do + expect { subject }.to raise_error(described_class::JunitParserError) + + expect(test_cases.count).to eq(0) + end + end + + context 'when there is a test case' do + let(:junit) do + <<-EOF.strip_heredoc + <testsuite> + <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase> + </testsuite> + EOF + end + + it 'parses XML and adds a test case to a suite' do + expect { subject }.not_to raise_error + + expect(test_cases[0].classname).to eq('Calculator') + expect(test_cases[0].name).to eq('sumTest1') + expect(test_cases[0].execution_time).to eq(0.01) + end + end + + context 'when there are two test cases' do + let(:junit) do + <<-EOF.strip_heredoc + <testsuite> + <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase> + <testcase classname='Calculator' name='sumTest2' time='0.02'></testcase> + </testsuite> + EOF + end + + it 'parses XML and adds test cases to a suite' do + expect { subject }.not_to raise_error + + expect(test_cases[0].classname).to eq('Calculator') + expect(test_cases[0].name).to eq('sumTest1') + expect(test_cases[0].execution_time).to eq(0.01) + expect(test_cases[1].classname).to eq('Calculator') + expect(test_cases[1].name).to eq('sumTest2') + expect(test_cases[1].execution_time).to eq(0.02) + end + end + + context 'when there are two test suites' do + let(:junit) do + <<-EOF.strip_heredoc + <testsuites> + <testsuite> + <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase> + <testcase classname='Calculator' name='sumTest2' time='0.02'></testcase> + </testsuite> + <testsuite> + <testcase classname='Statemachine' name='happy path' time='100'></testcase> + <testcase classname='Statemachine' name='unhappy path' time='200'></testcase> + </testsuite> + </testsuites> + EOF + end + + it 'parses XML and adds test cases to a suite' do + expect { subject }.not_to raise_error + + expect(test_cases[0].classname).to eq('Calculator') + expect(test_cases[0].name).to eq('sumTest1') + expect(test_cases[0].execution_time).to eq(0.01) + expect(test_cases[1].classname).to eq('Calculator') + expect(test_cases[1].name).to eq('sumTest2') + expect(test_cases[1].execution_time).to eq(0.02) + expect(test_cases[2].classname).to eq('Statemachine') + expect(test_cases[2].name).to eq('happy path') + expect(test_cases[2].execution_time).to eq(100) + expect(test_cases[3].classname).to eq('Statemachine') + expect(test_cases[3].name).to eq('unhappy path') + expect(test_cases[3].execution_time).to eq(200) + end + end + end + + context 'when data is not JUnit style XML' do + let(:junit) { { testsuite: 'abc' }.to_json } + + it 'raises an error' do + expect { subject }.to raise_error(described_class::JunitParserError) + end + end + + private + + def flattened_test_cases(test_suite) + test_suite.test_cases.map do |status, value| + value.map do |key, test_case| + test_case + end + end.flatten + end + end +end diff --git a/spec/lib/gitlab/ci/parsers_spec.rb b/spec/lib/gitlab/ci/parsers_spec.rb new file mode 100644 index 00000000000..2fa83c4abae --- /dev/null +++ b/spec/lib/gitlab/ci/parsers_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Parsers do + describe '.fabricate!' do + subject { described_class.fabricate!(file_type) } + + context 'when file_type exists' do + let(:file_type) { 'junit' } + + it 'fabricates the class' do + is_expected.to be_a(described_class::Junit) + end + end + + context 'when file_type does not exist' do + let(:file_type) { 'undefined' } + + it 'raises an error' do + expect { subject }.to raise_error(NameError) + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb index a973ccda8de..8ba56d73838 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb @@ -99,9 +99,9 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do end end - context 'when user is a master' do + context 'when user is a maintainer' do before do - project.add_master(user) + project.add_maintainer(user) end it { is_expected.to be_truthy } diff --git a/spec/lib/gitlab/ci/reports/test_case_spec.rb b/spec/lib/gitlab/ci/reports/test_case_spec.rb new file mode 100644 index 00000000000..6932f79f0ce --- /dev/null +++ b/spec/lib/gitlab/ci/reports/test_case_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe Gitlab::Ci::Reports::TestCase do + describe '#initialize' do + let(:test_case) { described_class.new(**params)} + + context 'when both classname and name are given' do + context 'when test case is passed' do + let(:params) do + { + name: 'test-1', + classname: 'trace', + file: 'spec/trace_spec.rb', + execution_time: 1.23, + status: described_class::STATUS_SUCCESS, + system_output: nil + } + end + + it 'initializes an instance' do + expect { test_case }.not_to raise_error + + expect(test_case.name).to eq('test-1') + expect(test_case.classname).to eq('trace') + expect(test_case.file).to eq('spec/trace_spec.rb') + expect(test_case.execution_time).to eq(1.23) + expect(test_case.status).to eq(described_class::STATUS_SUCCESS) + expect(test_case.system_output).to be_nil + end + end + + context 'when test case is failed' do + let(:params) do + { + name: 'test-1', + classname: 'trace', + file: 'spec/trace_spec.rb', + execution_time: 1.23, + status: described_class::STATUS_FAILED, + system_output: "Failure/Error: is_expected.to eq(300) expected: 300 got: -100" + } + end + + it 'initializes an instance' do + expect { test_case }.not_to raise_error + + expect(test_case.name).to eq('test-1') + expect(test_case.classname).to eq('trace') + expect(test_case.file).to eq('spec/trace_spec.rb') + expect(test_case.execution_time).to eq(1.23) + expect(test_case.status).to eq(described_class::STATUS_FAILED) + expect(test_case.system_output) + .to eq('Failure/Error: is_expected.to eq(300) expected: 300 got: -100') + end + end + end + + context 'when classname is missing' do + let(:params) do + { + name: 'test-1', + file: 'spec/trace_spec.rb', + execution_time: 1.23, + status: described_class::STATUS_SUCCESS, + system_output: nil + } + end + + it 'raises an error' do + expect { test_case }.to raise_error(ArgumentError) + end + end + + context 'when name is missing' do + let(:params) do + { + classname: 'trace', + file: 'spec/trace_spec.rb', + execution_time: 1.23, + status: described_class::STATUS_SUCCESS, + system_output: nil + } + end + + it 'raises an error' do + expect { test_case }.to raise_error(ArgumentError) + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb new file mode 100644 index 00000000000..71c61e0345f --- /dev/null +++ b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +describe Gitlab::Ci::Reports::TestReportsComparer do + include TestReportsHelper + + let(:comparer) { described_class.new(base_reports, head_reports) } + let(:base_reports) { Gitlab::Ci::Reports::TestReports.new } + let(:head_reports) { Gitlab::Ci::Reports::TestReports.new } + + describe '#suite_comparers' do + subject { comparer.suite_comparers } + + context 'when head and base reports include two test suites' do + before do + base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + base_reports.get_suite('junit').add_test_case(create_test_case_java_success) + head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + head_reports.get_suite('junit').add_test_case(create_test_case_java_success) + end + + it 'returns test suite comparers with specified values' do + expect(subject[0]).to be_a(Gitlab::Ci::Reports::TestSuiteComparer) + expect(subject[0].name).to eq('rspec') + expect(subject[0].head_suite).to eq(head_reports.get_suite('rspec')) + expect(subject[0].base_suite).to eq(base_reports.get_suite('rspec')) + expect(subject[1]).to be_a(Gitlab::Ci::Reports::TestSuiteComparer) + expect(subject[1].name).to eq('junit') + expect(subject[1].head_suite).to eq(head_reports.get_suite('junit')) + expect(subject[1].base_suite).to eq(base_reports.get_suite('junit')) + end + end + end + + describe '#total_status' do + subject { comparer.total_status } + + context 'when all tests cases are success in head suites' do + before do + head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + head_reports.get_suite('junit').add_test_case(create_test_case_java_success) + end + + it 'returns the total status' do + is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) + end + end + + context 'when there is a failed test case in head suites' do + before do + head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + head_reports.get_suite('junit').add_test_case(create_test_case_java_failed) + end + + it 'returns the total status in head suite' do + is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED) + end + end + end + + describe '#total_count' do + subject { comparer.total_count } + + before do + head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + head_reports.get_suite('junit').add_test_case(create_test_case_java_failed) + end + + it 'returns the total test counts in head suites' do + is_expected.to eq(2) + end + end + + describe '#resolved_count' do + subject { comparer.resolved_count } + + context 'when there is a resolved test case in head suites' do + let(:create_test_case_java_resolved) do + create_test_case_java_failed.tap do |test_case| + test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) + end + end + + before do + base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + base_reports.get_suite('junit').add_test_case(create_test_case_java_failed) + head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved) + end + + it 'returns the correct count' do + is_expected.to eq(1) + end + end + + context 'when there are no resolved test cases in head suites' do + before do + base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + base_reports.get_suite('junit').add_test_case(create_test_case_java_failed) + head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + head_reports.get_suite('junit').add_test_case(create_test_case_java_failed) + end + + it 'returns the correct count' do + is_expected.to eq(0) + end + end + end + + describe '#failed_count' do + subject { comparer.failed_count } + + context 'when there is a failed test case in head suites' do + before do + head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + head_reports.get_suite('junit').add_test_case(create_test_case_java_failed) + end + + it 'returns the correct count' do + is_expected.to eq(1) + end + end + + context 'when there are no failed test cases in head suites' do + before do + head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + head_reports.get_suite('junit').add_test_case(create_test_case_rspec_success) + end + + it 'returns the correct count' do + is_expected.to eq(0) + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/test_reports_spec.rb b/spec/lib/gitlab/ci/reports/test_reports_spec.rb new file mode 100644 index 00000000000..74ff134b239 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/test_reports_spec.rb @@ -0,0 +1,132 @@ +require 'spec_helper' + +describe Gitlab::Ci::Reports::TestReports do + include TestReportsHelper + + let(:test_reports) { described_class.new } + + describe '#get_suite' do + subject { test_reports.get_suite(suite_name) } + + context 'when suite name is rspec' do + let(:suite_name) { 'rspec' } + + it { expect(subject.name).to eq('rspec') } + + it 'initializes a new test suite and returns it' do + expect(Gitlab::Ci::Reports::TestSuite).to receive(:new).and_call_original + + is_expected.to be_a(Gitlab::Ci::Reports::TestSuite) + end + + context 'when suite name is already allocated' do + before do + subject + end + + it 'does not initialize a new test suite' do + expect(Gitlab::Ci::Reports::TestSuite).not_to receive(:new) + + is_expected.to be_a(Gitlab::Ci::Reports::TestSuite) + end + end + end + end + + describe '#total_time' do + subject { test_reports.total_time } + + before do + test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + test_reports.get_suite('junit').add_test_case(create_test_case_java_success) + end + + it 'returns the total time' do + is_expected.to eq(6.66) + end + end + + describe '#total_count' do + subject { test_reports.total_count } + + before do + test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + test_reports.get_suite('junit').add_test_case(create_test_case_java_success) + end + + it 'returns the total count' do + is_expected.to eq(2) + end + end + + describe '#total_status' do + subject { test_reports.total_status } + + context 'when all test cases succeeded' do + before do + test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + test_reports.get_suite('junit').add_test_case(create_test_case_java_success) + end + + it 'returns correct total status' do + is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) + end + end + + context 'when there is a failed test case' do + before do + test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + test_reports.get_suite('junit').add_test_case(create_test_case_java_failed) + end + + it 'returns correct total status' do + is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED) + end + end + + context 'when there is a skipped test case' do + before do + test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + test_reports.get_suite('junit').add_test_case(create_test_case_java_skipped) + end + + it 'returns correct total status' do + is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) + end + end + + context 'when there is an error test case' do + before do + test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + test_reports.get_suite('junit').add_test_case(create_test_case_java_error) + end + + it 'returns correct total status' do + is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED) + end + end + end + + Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type| + describe "##{status_type}_count" do + subject { test_reports.public_send("#{status_type}_count") } + + context "when #{status_type} test case exists" do + before do + test_reports.get_suite('rspec').add_test_case(public_send("create_test_case_rspec_#{status_type}")) + test_reports.get_suite('junit').add_test_case(public_send("create_test_case_java_#{status_type}")) + end + + it 'returns the count' do + is_expected.to eq(2) + end + end + + context "when #{status_type} test case do not exist" do + it 'returns nothing' do + is_expected.to be(0) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb new file mode 100644 index 00000000000..6ab16e5518d --- /dev/null +++ b/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb @@ -0,0 +1,225 @@ +require 'spec_helper' + +describe Gitlab::Ci::Reports::TestSuiteComparer do + include TestReportsHelper + + let(:comparer) { described_class.new(name, base_suite, head_suite) } + let(:name) { 'rpsec' } + let(:base_suite) { Gitlab::Ci::Reports::TestSuite.new(name) } + let(:head_suite) { Gitlab::Ci::Reports::TestSuite.new(name) } + let(:test_case_success) { create_test_case_rspec_success } + let(:test_case_failed) { create_test_case_rspec_failed } + + let(:test_case_resolved) do + create_test_case_rspec_failed.tap do |test_case| + test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) + end + end + + describe '#new_failures' do + subject { comparer.new_failures } + + context 'when head sutie has a newly failed test case which does not exist in base' do + before do + base_suite.add_test_case(test_case_success) + head_suite.add_test_case(test_case_failed) + end + + it 'returns the failed test case' do + is_expected.to eq([test_case_failed]) + end + end + + context 'when head sutie still has a failed test case which failed in base' do + before do + base_suite.add_test_case(test_case_failed) + head_suite.add_test_case(test_case_failed) + end + + it 'does not return the failed test case' do + is_expected.to be_empty + end + end + + context 'when head sutie has a success test case which failed in base' do + before do + base_suite.add_test_case(test_case_failed) + head_suite.add_test_case(test_case_resolved) + end + + it 'does not return the failed test case' do + is_expected.to be_empty + end + end + end + + describe '#existing_failures' do + subject { comparer.existing_failures } + + context 'when head sutie has a newly failed test case which does not exist in base' do + before do + base_suite.add_test_case(test_case_success) + head_suite.add_test_case(test_case_failed) + end + + it 'returns the failed test case' do + is_expected.to be_empty + end + end + + context 'when head sutie still has a failed test case which failed in base' do + before do + base_suite.add_test_case(test_case_failed) + head_suite.add_test_case(test_case_failed) + end + + it 'does not return the failed test case' do + is_expected.to eq([test_case_failed]) + end + end + + context 'when head sutie has a success test case which failed in base' do + before do + base_suite.add_test_case(test_case_failed) + head_suite.add_test_case(test_case_resolved) + end + + it 'does not return the failed test case' do + is_expected.to be_empty + end + end + end + + describe '#resolved_failures' do + subject { comparer.resolved_failures } + + context 'when head sutie has a newly failed test case which does not exist in base' do + before do + base_suite.add_test_case(test_case_success) + head_suite.add_test_case(test_case_failed) + end + + it 'returns the failed test case' do + is_expected.to be_empty + end + + it 'returns the correct resolved count' do + expect(comparer.resolved_count).to eq(0) + end + end + + context 'when head sutie still has a failed test case which failed in base' do + before do + base_suite.add_test_case(test_case_failed) + head_suite.add_test_case(test_case_failed) + end + + it 'does not return the failed test case' do + is_expected.to be_empty + end + + it 'returns the correct resolved count' do + expect(comparer.resolved_count).to eq(0) + end + end + + context 'when head sutie has a success test case which failed in base' do + before do + base_suite.add_test_case(test_case_failed) + head_suite.add_test_case(test_case_resolved) + end + + it 'does not return the resolved test case' do + is_expected.to eq([test_case_resolved]) + end + + it 'returns the correct resolved count' do + expect(comparer.resolved_count).to eq(1) + end + end + end + + describe '#total_count' do + subject { comparer.total_count } + + before do + head_suite.add_test_case(test_case_success) + end + + it 'returns the total test counts in head suite' do + is_expected.to eq(1) + end + end + + describe '#failed_count' do + subject { comparer.failed_count } + + context 'when there are a new failure and an existing failure' do + let(:test_case_1_success) { create_test_case_rspec_success } + let(:test_case_2_failed) { create_test_case_rspec_failed } + + let(:test_case_1_failed) do + create_test_case_rspec_success.tap do |test_case| + test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_FAILED) + end + end + + before do + base_suite.add_test_case(test_case_1_success) + base_suite.add_test_case(test_case_2_failed) + head_suite.add_test_case(test_case_1_failed) + head_suite.add_test_case(test_case_2_failed) + end + + it 'returns the correct count' do + is_expected.to eq(2) + end + end + + context 'when there is a new failure' do + before do + base_suite.add_test_case(test_case_success) + head_suite.add_test_case(test_case_failed) + end + + it 'returns the correct count' do + is_expected.to eq(1) + end + end + + context 'when there is an existing failure' do + before do + base_suite.add_test_case(test_case_failed) + head_suite.add_test_case(test_case_failed) + end + + it 'returns the correct count' do + is_expected.to eq(1) + end + end + end + + describe '#total_status' do + subject { comparer.total_status } + + context 'when all test cases in head suite are success' do + before do + head_suite.add_test_case(test_case_success) + end + + it 'returns the total status in head suite' do + is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) + end + end + + context 'when there is a failed test case in head suite' do + before do + head_suite.add_test_case(test_case_failed) + end + + it 'returns the total status in head suite' do + is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED) + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb new file mode 100644 index 00000000000..cd34dbaf62f --- /dev/null +++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe Gitlab::Ci::Reports::TestSuite do + include TestReportsHelper + + let(:test_suite) { described_class.new('Rspec') } + let(:test_case_success) { create_test_case_rspec_success } + let(:test_case_failed) { create_test_case_rspec_failed } + let(:test_case_skipped) { create_test_case_rspec_skipped } + let(:test_case_error) { create_test_case_rspec_error } + + it { expect(test_suite.name).to eq('Rspec') } + + describe '#add_test_case' do + context 'when status of the test case is success' do + it 'stores data correctly' do + test_suite.add_test_case(test_case_success) + + expect(test_suite.test_cases[test_case_success.status][test_case_success.key]) + .to eq(test_case_success) + expect(test_suite.total_time).to eq(1.11) + end + end + + context 'when status of the test case is failed' do + it 'stores data correctly' do + test_suite.add_test_case(test_case_failed) + + expect(test_suite.test_cases[test_case_failed.status][test_case_failed.key]) + .to eq(test_case_failed) + expect(test_suite.total_time).to eq(2.22) + end + end + + context 'when two test cases are added' do + it 'sums up total time' do + test_suite.add_test_case(test_case_success) + test_suite.add_test_case(test_case_failed) + + expect(test_suite.total_time).to eq(3.33) + end + end + end + + describe '#total_count' do + subject { test_suite.total_count } + + before do + test_suite.add_test_case(test_case_success) + test_suite.add_test_case(test_case_failed) + end + + it { is_expected.to eq(2) } + end + + describe '#total_status' do + subject { test_suite.total_status } + + context 'when all test cases succeeded' do + before do + test_suite.add_test_case(test_case_success) + end + + it { is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) } + end + + context 'when a test case failed' do + before do + test_suite.add_test_case(test_case_success) + test_suite.add_test_case(test_case_failed) + end + + it { is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED) } + end + end + + Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type| + describe "##{status_type}" do + subject { test_suite.public_send("#{status_type}") } + + context "when #{status_type} test case exists" do + before do + test_suite.add_test_case(public_send("test_case_#{status_type}")) + end + + it 'returns all success test cases' do + is_expected.to eq( { public_send("test_case_#{status_type}").key => public_send("test_case_#{status_type}") }) + end + end + + context "when #{status_type} test case do not exist" do + it 'returns nothing' do + is_expected.to be_empty + end + end + end + end + + Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type| + describe "##{status_type}_count" do + subject { test_suite.public_send("#{status_type}_count") } + + context "when #{status_type} test case exists" do + before do + test_suite.add_test_case(public_send("test_case_#{status_type}")) + end + + it 'returns the count' do + is_expected.to eq(1) + end + end + + context "when #{status_type} test case do not exist" do + it 'returns nothing' do + is_expected.to be(0) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/failed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_spec.rb index cadb424ea2c..b6676b40fd3 100644 --- a/spec/lib/gitlab/ci/status/build/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/build/failed_spec.rb @@ -80,4 +80,31 @@ describe Gitlab::Ci::Status::Build::Failed do end end end + + describe 'covers all failure reasons' do + let(:status) { Gitlab::Ci::Status::Failed.new(build, user) } + let(:tooltip) { subject.status_tooltip } + + CommitStatus.failure_reasons.keys.each do |failure_reason| + context failure_reason do + before do + build.failure_reason = failure_reason + end + + it "is a valid status" do + expect { tooltip }.not_to raise_error + end + end + end + + context 'invalid failure message' do + before do + expect(build).to receive(:failure_reason) { 'invalid failure message' } + end + + it "is an invalid status" do + expect { tooltip }.to raise_error(/key not found:/) + end + end + end end diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index e2bb378f663..02f8c4c114b 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -46,7 +46,7 @@ describe Gitlab::Ci::Status::Build::Play do context 'when user can not push to the branch' do before do build.project.add_developer(user) - create(:protected_branch, :masters_can_push, + create(:protected_branch, :maintainers_can_push, name: build.ref, project: project) end diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb index 6ec35f8da7e..bb2d0a2c75c 100644 --- a/spec/lib/gitlab/ci/status/stage/common_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb @@ -27,7 +27,7 @@ describe Gitlab::Ci::Status::Stage::Common do context 'when user has permission to read pipeline' do before do - project.add_master(user) + project.add_maintainer(user) end it 'has details' do diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb index adb3ff4321f..46874662edd 100644 --- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb @@ -75,6 +75,14 @@ describe Gitlab::Ci::Variables::Collection::Item do expect(resource).to eq variable end + it 'supports using a hash with stringified values' do + variable = { 'key' => 'VARIABLE', 'value' => 'my value' } + + resource = described_class.fabricate(variable) + + expect(resource).to eq(key: 'VARIABLE', value: 'my value') + end + it 'supports using an active record resource' do variable = create(:ci_variable, key: 'CI_VAR', value: '123') resource = described_class.fabricate(variable) diff --git a/spec/lib/gitlab/cleanup/project_uploads_spec.rb b/spec/lib/gitlab/cleanup/project_uploads_spec.rb new file mode 100644 index 00000000000..37b38776775 --- /dev/null +++ b/spec/lib/gitlab/cleanup/project_uploads_spec.rb @@ -0,0 +1,278 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Cleanup::ProjectUploads do + subject { described_class.new(logger: logger) } + let(:logger) { double(:logger) } + + before do + allow(logger).to receive(:info).at_least(1).times + allow(logger).to receive(:debug).at_least(1).times + end + + describe '#run!' do + shared_examples_for 'moves the file' do + shared_examples_for 'a real run' do + let(:args) { [dry_run: false] } + + it 'moves the file to its proper location' do + subject.run!(*args) + + expect(File.exist?(path)).to be_falsey + expect(File.exist?(new_path)).to be_truthy + end + + it 'logs action as done' do + expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up...") + expect(logger).to receive(:info).with("Did #{action}") + + subject.run!(*args) + end + end + + shared_examples_for 'a dry run' do + it 'does not move the file' do + subject.run!(*args) + + expect(File.exist?(path)).to be_truthy + expect(File.exist?(new_path)).to be_falsey + end + + it 'logs action as able to be done' do + expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up. Dry run...") + expect(logger).to receive(:info).with("Can #{action}") + + subject.run!(*args) + end + end + + context 'when dry_run is false' do + let(:args) { [dry_run: false] } + + it_behaves_like 'a real run' + end + + context 'when dry_run is nil' do + let(:args) { [dry_run: nil] } + + it_behaves_like 'a real run' + end + + context 'when dry_run is true' do + let(:args) { [dry_run: true] } + + it_behaves_like 'a dry run' + end + + context 'with dry_run not specified' do + let(:args) { [] } + + it_behaves_like 'a dry run' + end + end + + shared_examples_for 'moves the file to lost and found' do + let(:action) { "move to lost and found #{path} -> #{new_path}" } + + it_behaves_like 'moves the file' + end + + shared_examples_for 'fixes the file' do + let(:action) { "fix #{path} -> #{new_path}" } + + it_behaves_like 'moves the file' + end + + context 'orphaned project upload file' do + context 'when an upload record matching the secret and filename is found' do + context 'when the project is still in legacy storage' do + let(:orphaned) { create(:upload, :issuable_upload, :with_file, model: create(:project, :legacy_storage)) } + let(:new_path) { orphaned.absolute_path } + let(:path) { File.join(FileUploader.root, 'some', 'wrong', 'location', orphaned.path) } + + before do + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.mv(new_path, path) + end + + it_behaves_like 'fixes the file' + end + + context 'when the project was moved to hashed storage' do + let(:orphaned) { create(:upload, :issuable_upload, :with_file) } + let(:new_path) { orphaned.absolute_path } + let(:path) { File.join(FileUploader.root, 'some', 'wrong', 'location', orphaned.path) } + + before do + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.mv(new_path, path) + end + + it_behaves_like 'fixes the file' + end + + context 'when the project is missing (the upload *record* is an orphan)' do + let(:orphaned) { create(:upload, :issuable_upload, :with_file, model: build(:project, :legacy_storage)) } + let!(:path) { orphaned.absolute_path } + let!(:new_path) { File.join(FileUploader.root, '-', 'project-lost-found', orphaned.model.full_path, orphaned.path) } + + before do + orphaned.model.delete + end + + it_behaves_like 'moves the file to lost and found' + end + + # We will probably want to add logic (Reschedule background upload) to + # cover Case 2 in https://gitlab.com/gitlab-org/gitlab-ce/issues/46535#note_75355104 + context 'when the file should be in object storage' do + context 'when the file otherwise has the correct local path' do + let!(:orphaned) { create(:upload, :issuable_upload, :object_storage, model: build(:project, :legacy_storage)) } + let!(:path) { File.join(FileUploader.root, orphaned.model.full_path, orphaned.path) } + + before do + stub_feature_flags(import_export_object_storage: true) + stub_uploads_object_storage(FileUploader) + + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.touch(path) + end + + it 'does not move the file' do + expect(File.exist?(path)).to be_truthy + + subject.run!(dry_run: false) + + expect(File.exist?(path)).to be_truthy + end + end + + # E.g. the upload file was orphaned, and then uploads were migrated to + # object storage + context 'when the file has the wrong local path' do + let!(:orphaned) { create(:upload, :issuable_upload, :object_storage, model: build(:project, :legacy_storage)) } + let!(:path) { File.join(FileUploader.root, 'wrong', orphaned.path) } + let!(:new_path) { File.join(FileUploader.root, '-', 'project-lost-found', 'wrong', orphaned.path) } + + before do + stub_feature_flags(import_export_object_storage: true) + stub_uploads_object_storage(FileUploader) + + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.touch(path) + end + + it_behaves_like 'moves the file to lost and found' + end + end + end + + context 'when a matching upload record can not be found' do + context 'when the file path fits the known pattern' do + let!(:orphaned) { create(:upload, :issuable_upload, :with_file, model: build(:project, :legacy_storage)) } + let!(:path) { orphaned.absolute_path } + let!(:new_path) { File.join(FileUploader.root, '-', 'project-lost-found', orphaned.model.full_path, orphaned.path) } + + before do + orphaned.delete + end + + it_behaves_like 'moves the file to lost and found' + end + + context 'when the file path does not fit the known pattern' do + let!(:invalid_path) { File.join('group', 'file.jpg') } + let!(:path) { File.join(FileUploader.root, invalid_path) } + let!(:new_path) { File.join(FileUploader.root, '-', 'project-lost-found', invalid_path) } + + before do + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.touch(path) + end + + after do + File.delete(path) if File.exist?(path) + end + + it_behaves_like 'moves the file to lost and found' + end + end + end + + context 'non-orphaned project upload file' do + it 'does not move the file' do + tracked = create(:upload, :issuable_upload, :with_file, model: build(:project, :legacy_storage)) + tracked_path = tracked.absolute_path + + expect(logger).not_to receive(:info).with(/move|fix/i) + expect(File.exist?(tracked_path)).to be_truthy + + subject.run!(dry_run: false) + + expect(File.exist?(tracked_path)).to be_truthy + end + end + + context 'ignorable cases' do + # Because we aren't concerned about these, and can save a lot of + # processing time by ignoring them. If we wish to cleanup hashed storage + # directories, it should simply require removing this test and modifying + # the find command. + context 'when the file is already in hashed storage' do + let(:project) { create(:project) } + + before do + expect(logger).not_to receive(:info).with(/move|fix/i) + end + + it 'does not move even an orphan file' do + orphaned = create(:upload, :issuable_upload, :with_file, model: project) + path = orphaned.absolute_path + orphaned.delete + + expect(File.exist?(path)).to be_truthy + + subject.run!(dry_run: false) + + expect(File.exist?(path)).to be_truthy + end + end + + it 'does not move any non-project (FileUploader) uploads' do + paths = [] + orphaned1 = create(:upload, :personal_snippet_upload, :with_file) + orphaned2 = create(:upload, :namespace_upload, :with_file) + orphaned3 = create(:upload, :attachment_upload, :with_file) + paths << orphaned1.absolute_path + paths << orphaned2.absolute_path + paths << orphaned3.absolute_path + Upload.delete_all + + expect(logger).not_to receive(:info).with(/move|fix/i) + paths.each do |path| + expect(File.exist?(path)).to be_truthy + end + + subject.run!(dry_run: false) + + paths.each do |path| + expect(File.exist?(path)).to be_truthy + end + end + + it 'does not move any uploads in tmp (which would interfere with ongoing upload activity)' do + path = File.join(FileUploader.root, 'tmp', 'foo.jpg') + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.touch(path) + + expect(logger).not_to receive(:info).with(/move|fix/i) + expect(File.exist?(path)).to be_truthy + + subject.run!(dry_run: false) + + expect(File.exist?(path)).to be_truthy + end + end + end +end diff --git a/spec/lib/gitlab/cleanup/remote_uploads_spec.rb b/spec/lib/gitlab/cleanup/remote_uploads_spec.rb new file mode 100644 index 00000000000..8d03baeb07b --- /dev/null +++ b/spec/lib/gitlab/cleanup/remote_uploads_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::Cleanup::RemoteUploads do + context 'when object_storage is enabled' do + let(:connection) { double } + let(:directory) { double } + let!(:uploads) do + [ + create(:upload, path: 'dir/file1', store: ObjectStorage::Store::REMOTE), + create(:upload, path: 'dir/file2', store: ObjectStorage::Store::LOCAL) + ] + end + let(:remote_files) do + [ + double(key: 'dir/file1'), + double(key: 'dir/file2'), + double(key: 'dir/file3'), + double(key: 'lost_and_found/dir/file3') + ] + end + + before do + stub_uploads_object_storage(FileUploader) + + expect(::Fog::Storage).to receive(:new).and_return(connection) + + expect(connection).to receive(:directories).and_return(double(get: directory)) + expect(directory).to receive(:files).and_return(remote_files) + end + + context 'when dry_run is set to false' do + subject { described_class.new.run!(dry_run: false) } + + it 'moves files that are not in uploads table' do + expect(remote_files[0]).not_to receive(:copy) + expect(remote_files[0]).not_to receive(:destroy) + expect(remote_files[1]).to receive(:copy) + expect(remote_files[1]).to receive(:destroy) + expect(remote_files[2]).to receive(:copy) + expect(remote_files[2]).to receive(:destroy) + expect(remote_files[3]).not_to receive(:copy) + expect(remote_files[3]).not_to receive(:destroy) + + subject + end + end + + context 'when dry_run is set to true' do + subject { described_class.new.run!(dry_run: true) } + + it 'does not move filese' do + expect(remote_files[0]).not_to receive(:copy) + expect(remote_files[0]).not_to receive(:destroy) + expect(remote_files[1]).not_to receive(:copy) + expect(remote_files[1]).not_to receive(:destroy) + expect(remote_files[2]).not_to receive(:copy) + expect(remote_files[2]).not_to receive(:destroy) + expect(remote_files[3]).not_to receive(:copy) + expect(remote_files[3]).not_to receive(:destroy) + + subject + end + end + end + + context 'when object_storage is not enabled' do + it 'does not connect to any storage' do + expect(::Fog::Storage).not_to receive(:new) + + subject + end + end +end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 8d4862932b2..1f35d1e4880 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::ClosingIssueExtractor do before do project.add_developer(project.creator) project.add_developer(project2.creator) - project2.add_master(project.creator) + project2.add_maintainer(project.creator) end describe "#closed_by_message" do @@ -298,7 +298,7 @@ describe Gitlab::ClosingIssueExtractor do context 'with an external issue tracker reference' do it 'extracts the referenced issue' do jira_project = create(:jira_project, name: 'JIRA_EXT1') - jira_project.add_master(jira_project.creator) + jira_project.add_maintainer(jira_project.creator) jira_issue = ExternalIssue.new("#{jira_project.name}-1", project: jira_project) closing_issue_extractor = described_class.new(jira_project, jira_project.creator) message = "Resolve #{jira_issue.to_reference}" @@ -379,6 +379,20 @@ describe Gitlab::ClosingIssueExtractor do .to match_array([issue, other_issue, third_issue]) end + it 'allows non-comma-separated issue numbers in single line message' do + message = "Closes #{reference} #{reference2} #{reference3}" + + expect(subject.closed_by_message(message)) + .to match_array([issue, other_issue, third_issue]) + end + + it 'allows mixed comma-separated and non-comma-separated issue numbers in single line message' do + message = "Closes #{reference}, #{reference2} and #{reference3}" + + expect(subject.closed_by_message(message)) + .to match_array([issue, other_issue, third_issue]) + end + it 'fetches issues in multi-line message' do message = "Awesome commit (closes #{reference})\nAlso fixes #{reference2}" diff --git a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb index 6de4bd3dc7c..f670c7f6c75 100644 --- a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb @@ -36,9 +36,9 @@ describe Gitlab::CycleAnalytics::Permissions do end end - context 'user is master' do + context 'user is maintainer' do before do - project.add_master(user) + project.add_maintainer(user) end it 'has permissions to issue stage' do diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb index b411aaa19da..0a8c77b0ad9 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb @@ -281,7 +281,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : it "doesn't break when the namespace was renamed" do subject.rename_namespace(namespace) - namespace.update_attributes!(path: 'renamed-afterwards') + namespace.update!(path: 'renamed-afterwards') expect { subject.revert_renames }.not_to raise_error end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb index b4896d69077..d4d7a83921c 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb @@ -169,7 +169,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :de it "doesn't break when the project was renamed" do subject.rename_project(project) - project.update_attributes!(path: 'renamed-afterwards') + project.update!(path: 'renamed-afterwards') expect { subject.revert_renames }.not_to raise_error end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 8bb246aa4bd..7d76519dddd 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -65,6 +65,28 @@ describe Gitlab::Database do end end + describe '.postgresql_9_or_less?' do + it 'returns false when using MySQL' do + allow(described_class).to receive(:postgresql?).and_return(false) + + expect(described_class.postgresql_9_or_less?).to eq(false) + end + + it 'returns true when using PostgreSQL 9.6' do + allow(described_class).to receive(:postgresql?).and_return(true) + allow(described_class).to receive(:version).and_return('9.6') + + expect(described_class.postgresql_9_or_less?).to eq(true) + end + + it 'returns false when using PostgreSQL 10 or newer' do + allow(described_class).to receive(:postgresql?).and_return(true) + allow(described_class).to receive(:version).and_return('10') + + expect(described_class.postgresql_9_or_less?).to eq(false) + end + end + describe '.join_lateral_supported?' do it 'returns false when using MySQL' do allow(described_class).to receive(:postgresql?).and_return(false) @@ -109,6 +131,70 @@ describe Gitlab::Database do end end + describe '.pg_wal_lsn_diff' do + it 'returns old name when using PostgreSQL 9.6' do + allow(described_class).to receive(:postgresql?).and_return(true) + allow(described_class).to receive(:version).and_return('9.6') + + expect(described_class.pg_wal_lsn_diff).to eq('pg_xlog_location_diff') + end + + it 'returns new name when using PostgreSQL 10 or newer' do + allow(described_class).to receive(:postgresql?).and_return(true) + allow(described_class).to receive(:version).and_return('10') + + expect(described_class.pg_wal_lsn_diff).to eq('pg_wal_lsn_diff') + end + end + + describe '.pg_current_wal_insert_lsn' do + it 'returns old name when using PostgreSQL 9.6' do + allow(described_class).to receive(:postgresql?).and_return(true) + allow(described_class).to receive(:version).and_return('9.6') + + expect(described_class.pg_current_wal_insert_lsn).to eq('pg_current_xlog_insert_location') + end + + it 'returns new name when using PostgreSQL 10 or newer' do + allow(described_class).to receive(:postgresql?).and_return(true) + allow(described_class).to receive(:version).and_return('10') + + expect(described_class.pg_current_wal_insert_lsn).to eq('pg_current_wal_insert_lsn') + end + end + + describe '.pg_last_wal_receive_lsn' do + it 'returns old name when using PostgreSQL 9.6' do + allow(described_class).to receive(:postgresql?).and_return(true) + allow(described_class).to receive(:version).and_return('9.6') + + expect(described_class.pg_last_wal_receive_lsn).to eq('pg_last_xlog_receive_location') + end + + it 'returns new name when using PostgreSQL 10 or newer' do + allow(described_class).to receive(:postgresql?).and_return(true) + allow(described_class).to receive(:version).and_return('10') + + expect(described_class.pg_last_wal_receive_lsn).to eq('pg_last_wal_receive_lsn') + end + end + + describe '.pg_last_wal_replay_lsn' do + it 'returns old name when using PostgreSQL 9.6' do + allow(described_class).to receive(:postgresql?).and_return(true) + allow(described_class).to receive(:version).and_return('9.6') + + expect(described_class.pg_last_wal_replay_lsn).to eq('pg_last_xlog_replay_location') + end + + it 'returns new name when using PostgreSQL 10 or newer' do + allow(described_class).to receive(:postgresql?).and_return(true) + allow(described_class).to receive(:version).and_return('10') + + expect(described_class.pg_last_wal_replay_lsn).to eq('pg_last_wal_replay_lsn') + end + end + describe '.nulls_last_order' do context 'when using PostgreSQL' do before do @@ -357,6 +443,35 @@ describe Gitlab::Database do end end + describe '.db_read_only?' do + context 'when using PostgreSQL' do + before do + allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original + expect(described_class).to receive(:postgresql?).and_return(true) + end + + it 'detects a read only database' do + allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "t" }]) + + expect(described_class.db_read_only?).to be_truthy + end + + it 'detects a read write database' do + allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "f" }]) + + expect(described_class.db_read_only?).to be_falsey + end + end + + context 'when using MySQL' do + before do + expect(described_class).to receive(:postgresql?).and_return(false) + end + + it { expect(described_class.db_read_only?).to be_falsey } + end + end + describe '#sanitize_timestamp' do let(:max_timestamp) { Time.at((1 << 31) - 1) } diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb index f48ee8924e8..79287021981 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -20,6 +20,15 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do diff_files end + it 'it uses a different cache key if diff line keys change' do + mr_diff = described_class.new(merge_request.merge_request_diff, diff_options: nil) + key = mr_diff.cache_key + + stub_const('Gitlab::Diff::Line::SERIALIZE_KEYS', [:foo]) + + expect(mr_diff.cache_key).not_to eq(key) + end + shared_examples 'initializes a DiffCollection' do it 'returns a valid instance of a DiffCollection' do expect(diff_files).to be_a(Gitlab::Git::DiffCollection) diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 5dfbb8e71f8..ebeb05d6e02 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -26,6 +26,21 @@ describe Gitlab::Diff::File do end end + describe '#diff_lines_for_serializer' do + it 'includes bottom match line if not in the end' do + expect(diff_file.diff_lines_for_serializer.last.type).to eq('match') + end + + context 'when deleted' do + let(:commit) { project.commit('d59c60028b053793cecfb4022de34602e1a9218e') } + let(:diff_file) { commit.diffs.diff_file_with_old_path('files/js/commit.js.coffee') } + + it 'does not include bottom match line' do + expect(diff_file.diff_lines_for_serializer.last.type).not_to eq('match') + end + end + end + describe '#mode_changed?' do it { expect(diff_file.mode_changed?).to be_falsey } end diff --git a/spec/lib/additional_email_headers_interceptor_spec.rb b/spec/lib/gitlab/email/hook/additional_headers_interceptor_spec.rb index b5c1a360ba9..ae61ece8029 100644 --- a/spec/lib/additional_email_headers_interceptor_spec.rb +++ b/spec/lib/gitlab/email/hook/additional_headers_interceptor_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe AdditionalEmailHeadersInterceptor do +describe Gitlab::Email::Hook::AdditionalHeadersInterceptor do let(:mail) do ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello') end diff --git a/spec/lib/gitlab/email/hook/delivery_metrics_observer_spec.rb b/spec/lib/gitlab/email/hook/delivery_metrics_observer_spec.rb new file mode 100644 index 00000000000..4497d4002da --- /dev/null +++ b/spec/lib/gitlab/email/hook/delivery_metrics_observer_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Gitlab::Email::Hook::DeliveryMetricsObserver do + let(:email) do + ActionMailer::Base.mail(to: 'test@example.com', + from: 'info@example.com', + body: 'hello') + end + + context 'when email has been delivered' do + it 'increments both email delivery metrics' do + expect(described_class.delivery_attempts_counter).to receive(:increment) + expect(described_class.delivered_emails_counter).to receive(:increment) + + email.deliver_now + end + end + + context 'when email has not been delivered due to an error' do + before do + allow(email.delivery_method).to receive(:deliver!) + .and_raise(StandardError, 'Some SMTP error') + end + + it 'increments only delivery attempt metric' do + expect(described_class.delivery_attempts_counter) + .to receive(:increment) + expect(described_class.delivered_emails_counter) + .not_to receive(:increment) + + expect { email.deliver_now } + .to raise_error(StandardError, 'Some SMTP error') + end + end +end diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb index 3652d928c43..91aa3bc7c2e 100644 --- a/spec/lib/disable_email_interceptor_spec.rb +++ b/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe DisableEmailInterceptor do +describe Gitlab::Email::Hook::DisableEmailInterceptor do before do Mail.register_interceptor(described_class) end diff --git a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb new file mode 100644 index 00000000000..2e3656b52fb --- /dev/null +++ b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state do + include ::ExclusiveLeaseHelpers + + let(:class_instance) { (Class.new { include ::Gitlab::ExclusiveLeaseHelpers }).new } + let(:unique_key) { SecureRandom.hex(10) } + + describe '#in_lock' do + subject { class_instance.in_lock(unique_key, **options) { } } + + let(:options) { {} } + + context 'when the lease is not obtained yet' do + before do + stub_exclusive_lease(unique_key, 'uuid') + end + + it 'calls the given block' do + expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_control.once + end + + it 'calls the given block continuously' do + expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_control.once + expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_control.once + expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_control.once + end + + it 'cancels the exclusive lease after the block' do + expect_to_cancel_exclusive_lease(unique_key, 'uuid') + + subject + end + end + + context 'when the lease is obtained already' do + let!(:lease) { stub_exclusive_lease_taken(unique_key) } + + it 'retries to obtain a lease and raises an error' do + expect(lease).to receive(:try_obtain).exactly(11).times + + expect { subject }.to raise_error('Failed to obtain a lock') + end + + context 'when ttl is specified' do + let(:options) { { ttl: 10.minutes } } + + it 'receives the specified argument' do + expect(Gitlab::ExclusiveLease).to receive(:new).with(unique_key, { timeout: 10.minutes } ) + + expect { subject }.to raise_error('Failed to obtain a lock') + end + end + + context 'when retry count is specified' do + let(:options) { { retries: 3 } } + + it 'retries for the specified times' do + expect(lease).to receive(:try_obtain).exactly(4).times + + expect { subject }.to raise_error('Failed to obtain a lock') + end + end + + context 'when sleep second is specified' do + let(:options) { { retries: 0, sleep_sec: 0.05.seconds } } + + it 'receives the specified argument' do + expect(class_instance).to receive(:sleep).with(0.05.seconds).once + + expect { subject }.to raise_error('Failed to obtain a lock') + end + end + end + end +end diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb index 13df8531b63..ef52a25f47e 100644 --- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb @@ -20,37 +20,55 @@ describe Gitlab::Gfm::UploadsRewriter do "Text and #{image_uploader.markdown_link} and #{zip_uploader.markdown_link}" end - describe '#rewrite' do - let!(:new_text) { rewriter.rewrite(new_project) } + shared_examples "files are accessible" do + describe '#rewrite' do + let!(:new_text) { rewriter.rewrite(new_project) } - let(:old_files) { [image_uploader, zip_uploader].map(&:file) } - let(:new_files) do - described_class.new(new_text, new_project, user).files - end + let(:old_files) { [image_uploader, zip_uploader] } + let(:new_files) do + described_class.new(new_text, new_project, user).files + end - let(:old_paths) { old_files.map(&:path) } - let(:new_paths) { new_files.map(&:path) } + let(:old_paths) { old_files.map(&:path) } + let(:new_paths) { new_files.map(&:path) } - it 'rewrites content' do - expect(new_text).not_to eq text - expect(new_text.length).to eq text.length - end + it 'rewrites content' do + expect(new_text).not_to eq text + expect(new_text.length).to eq text.length + end - it 'copies files' do - expect(new_files).to all(exist) - expect(old_paths).not_to match_array new_paths - expect(old_paths).to all(include(old_project.disk_path)) - expect(new_paths).to all(include(new_project.disk_path)) - end + it 'copies files' do + expect(new_files).to all(exist) + expect(old_paths).not_to match_array new_paths + expect(old_paths).to all(include(old_project.disk_path)) + expect(new_paths).to all(include(new_project.disk_path)) + end - it 'does not remove old files' do - expect(old_files).to all(exist) + it 'does not remove old files' do + expect(old_files).to all(exist) + end + + it 'generates a new secret for each file' do + expect(new_paths).not_to include image_uploader.secret + expect(new_paths).not_to include zip_uploader.secret + end end + end - it 'generates a new secret for each file' do - expect(new_paths).not_to include image_uploader.secret - expect(new_paths).not_to include zip_uploader.secret + context "file are stored locally" do + include_examples "files are accessible" + end + + context "files are stored remotely" do + before do + stub_uploads_object_storage(FileUploader) + + old_files.each do |file| + file.migrate!(ObjectStorage::Store::REMOTE) + end end + + include_examples "files are accessible" end describe '#needs_rewrite?' do diff --git a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb index 5d22dcfb508..ca067a29174 100644 --- a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb +++ b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Git::AttributesAtRefParser, seed_helper: true do +describe Gitlab::Git::AttributesAtRefParser, :seed_helper do let(:project) { create(:project, :repository) } let(:repository) { project.repository } diff --git a/spec/lib/gitlab/git/attributes_parser_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb index 2d103123998..18ebfef38f0 100644 --- a/spec/lib/gitlab/git/attributes_parser_spec.rb +++ b/spec/lib/gitlab/git/attributes_parser_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Git::AttributesParser, seed_helper: true do +describe Gitlab::Git::AttributesParser, :seed_helper do let(:attributes_path) { File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info', 'attributes') } let(:data) { File.read(attributes_path) } diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index ba790b717ae..e704d1c673c 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -1,7 +1,7 @@ # coding: utf-8 require "spec_helper" -describe Gitlab::Git::Blame, seed_helper: true do +describe Gitlab::Git::Blame, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } let(:blame) do Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md") diff --git a/spec/lib/gitlab/git/blob_snippet_spec.rb b/spec/lib/gitlab/git/blob_snippet_spec.rb index d6d365f6492..6effec8295c 100644 --- a/spec/lib/gitlab/git/blob_snippet_spec.rb +++ b/spec/lib/gitlab/git/blob_snippet_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe Gitlab::Git::BlobSnippet, seed_helper: true do +describe Gitlab::Git::BlobSnippet, :seed_helper do describe '#data' do context 'empty lines' do let(:snippet) { Gitlab::Git::BlobSnippet.new('master', nil, nil, nil) } diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index b6061df349d..ea49502ae2e 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe Gitlab::Git::Blob, seed_helper: true do +describe Gitlab::Git::Blob, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } describe 'initialize' do @@ -178,77 +178,67 @@ describe Gitlab::Git::Blob, seed_helper: true do end describe '.batch' do - shared_examples 'loading blobs in batch' do - let(:blob_references) do - [ - [SeedRepo::Commit::ID, "files/ruby/popen.rb"], - [SeedRepo::Commit::ID, 'six'] - ] - end + let(:blob_references) do + [ + [SeedRepo::Commit::ID, "files/ruby/popen.rb"], + [SeedRepo::Commit::ID, 'six'] + ] + end - subject { described_class.batch(repository, blob_references) } + subject { described_class.batch(repository, blob_references) } - it { expect(subject.size).to eq(blob_references.size) } + it { expect(subject.size).to eq(blob_references.size) } - context 'first blob' do - let(:blob) { subject[0] } + context 'first blob' do + let(:blob) { subject[0] } - 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 + 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 'second blob' do - let(:blob) { subject[1] } + context 'second blob' do + let(:blob) { subject[1] } - it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') } - it { expect(blob.data).to eq('') } - it 'does not mark the blob as binary' do - expect(blob).not_to be_binary - end + it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') } + it { expect(blob.data).to eq('') } + it 'does not mark the blob as binary' do + expect(blob).not_to be_binary end + end - context 'limiting' do - subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) } + context 'limiting' do + subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) } - context 'positive' do - let(:blob_size_limit) { 10 } + context 'positive' do + let(:blob_size_limit) { 10 } - it { expect(subject.first.data.size).to eq(10) } - end + it { expect(subject.first.data.size).to eq(10) } + end - context 'zero' do - let(:blob_size_limit) { 0 } + context 'zero' do + let(:blob_size_limit) { 0 } - it 'only loads the metadata' do - expect(subject.first.size).not_to be(0) - expect(subject.first.data).to eq('') - end + it 'only loads the metadata' do + expect(subject.first.size).not_to be(0) + expect(subject.first.data).to eq('') end + end - context 'negative' do - let(:blob_size_limit) { -1 } + context 'negative' do + let(:blob_size_limit) { -1 } - it 'ignores MAX_DATA_DISPLAY_SIZE' do - stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100) + it 'ignores MAX_DATA_DISPLAY_SIZE' do + stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100) - expect(subject.first.data.size).to eq(669) - end + expect(subject.first.data.size).to eq(669) end end end - - context 'when Gitaly list_blobs_by_sha_path feature is enabled' do - it_behaves_like 'loading blobs in batch' - end - - context 'when Gitaly list_blobs_by_sha_path feature is disabled', :disable_gitaly do - it_behaves_like 'loading blobs in batch' - end end describe '.batch_metadata' do @@ -294,58 +284,48 @@ describe Gitlab::Git::Blob, seed_helper: true do ) end - shared_examples 'fetching batch of LFS pointers' do - it 'returns a list of Gitlab::Git::Blob' do - blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id]) - - expect(blobs.count).to eq(1) - expect(blobs).to all( be_a(Gitlab::Git::Blob) ) - expect(blobs).to be_an(Array) - end - - it 'accepts blob IDs as a lazy enumerator' do - blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id].lazy) - - expect(blobs.count).to eq(1) - expect(blobs).to all( be_a(Gitlab::Git::Blob) ) - end + it 'returns a list of Gitlab::Git::Blob' do + blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id]) - it 'handles empty list of IDs gracefully' do - blobs_1 = described_class.batch_lfs_pointers(repository, [].lazy) - blobs_2 = described_class.batch_lfs_pointers(repository, []) + expect(blobs.count).to eq(1) + expect(blobs).to all( be_a(Gitlab::Git::Blob) ) + expect(blobs).to be_an(Array) + end - expect(blobs_1).to eq([]) - expect(blobs_2).to eq([]) - end + it 'accepts blob IDs as a lazy enumerator' do + blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id].lazy) - it 'silently ignores tree objects' do - blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid]) + expect(blobs.count).to eq(1) + expect(blobs).to all( be_a(Gitlab::Git::Blob) ) + end - expect(blobs).to eq([]) - end + it 'handles empty list of IDs gracefully' do + blobs_1 = described_class.batch_lfs_pointers(repository, [].lazy) + blobs_2 = described_class.batch_lfs_pointers(repository, []) - it 'silently ignores non lfs objects' do - blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id]) + expect(blobs_1).to eq([]) + expect(blobs_2).to eq([]) + end - expect(blobs).to eq([]) - end + it 'silently ignores tree objects' do + blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid]) - it 'avoids loading large blobs into memory' do - # This line could call `lookup` on `repository`, so do here before mocking. - non_lfs_blob_id = non_lfs_blob.id + expect(blobs).to eq([]) + end - expect(repository).not_to receive(:lookup) + it 'silently ignores non lfs objects' do + blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id]) - described_class.batch_lfs_pointers(repository, [non_lfs_blob_id]) - end + expect(blobs).to eq([]) end - context 'when Gitaly batch_lfs_pointers is enabled' do - it_behaves_like 'fetching batch of LFS pointers' - end + it 'avoids loading large blobs into memory' do + # This line could call `lookup` on `repository`, so do here before mocking. + non_lfs_blob_id = non_lfs_blob.id + + expect(repository).not_to receive(:lookup) - context 'when Gitaly batch_lfs_pointers is disabled', :disable_gitaly do - it_behaves_like 'fetching batch of LFS pointers' + described_class.batch_lfs_pointers(repository, [non_lfs_blob_id]) end end @@ -532,8 +512,8 @@ describe Gitlab::Git::Blob, seed_helper: true do subject { blob.load_all_data!(repository) } it 'loads missing data' do - expect(Gitlab::GitalyClient).to receive(:migrate) - .with(:git_blob_load_all_data).and_return(full_data) + expect(repository.gitaly_blob_client).to receive(:get_blob) + .and_return(double(:response, data: full_data)) subject @@ -544,8 +524,7 @@ describe Gitlab::Git::Blob, seed_helper: true do let(:blob) { Gitlab::Git::Blob.new(name: 'test', size: 4, data: full_data) } it "doesn't perform any loading" do - expect(Gitlab::GitalyClient).not_to receive(:migrate) - .with(:git_blob_load_all_data) + expect(repository.gitaly_blob_client).not_to receive(:get_blob) subject diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index ec1a684cfbc..79ccbb79966 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -1,7 +1,12 @@ require "spec_helper" -describe Gitlab::Git::Branch, seed_helper: true do +describe Gitlab::Git::Branch, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:rugged) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.rugged + end + end subject { repository.branches } @@ -124,6 +129,7 @@ describe Gitlab::Git::Branch, seed_helper: true do it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) } def create_commit - repository.create_commit(params.merge(committer: committer.merge(time: Time.now))) + params[:message].delete!("\r") + Rugged::Commit.create(rugged, params.merge(committer: committer.merge(time: Time.now))) end end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index ee74c2769eb..2718a3c5e49 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Gitlab::Git::Commit, seed_helper: true do +describe Gitlab::Git::Commit, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } let(:commit) { described_class.find(repository, SeedRepo::Commit::ID) } let(:rugged_commit) do @@ -27,7 +27,7 @@ describe Gitlab::Git::Commit, seed_helper: true do } @parents = [repo.head.target] - @gitlab_parents = @parents.map { |c| described_class.decorate(repository, c) } + @gitlab_parents = @parents.map { |c| described_class.find(repository, c.oid) } @tree = @parents.first.tree sha = Rugged::Commit.create( @@ -41,7 +41,7 @@ describe Gitlab::Git::Commit, seed_helper: true do ) @raw_commit = repo.lookup(sha) - @commit = described_class.new(repository, @raw_commit) + @commit = described_class.find(repository, sha) end it { expect(@commit.short_id).to eq(@raw_commit.oid[0..10]) } @@ -488,13 +488,15 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe '#init_from_rugged' do - let(:gitlab_commit) { described_class.new(repository, rugged_commit) } - subject { gitlab_commit } + skip 'move this test to gitaly-ruby' do + describe '#init_from_rugged' do + let(:gitlab_commit) { described_class.new(repository, rugged_commit) } + subject { gitlab_commit } - describe '#id' do - subject { super().id } - it { is_expected.to eq(SeedRepo::Commit::ID) } + describe '#id' do + subject { super().id } + it { is_expected.to eq(SeedRepo::Commit::ID) } + end end end diff --git a/spec/lib/gitlab/git/committer_with_hooks_spec.rb b/spec/lib/gitlab/git/committer_with_hooks_spec.rb index 2100690f873..c7626058acd 100644 --- a/spec/lib/gitlab/git/committer_with_hooks_spec.rb +++ b/spec/lib/gitlab/git/committer_with_hooks_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Git::CommitterWithHooks, seed_helper: true do +describe Gitlab::Git::CommitterWithHooks, :seed_helper do # TODO https://gitlab.com/gitlab-org/gitaly/issues/1234 skip 'needs to be moved to gitaly-ruby test suite' do shared_examples 'calling wiki hooks' do diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb index b6a42e422b5..7cc6f52f8ee 100644 --- a/spec/lib/gitlab/git/compare_spec.rb +++ b/spec/lib/gitlab/git/compare_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Gitlab::Git::Compare, seed_helper: true do +describe Gitlab::Git::Compare, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: false) } let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: true) } diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 65edc750f39..81658874be7 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Git::DiffCollection, seed_helper: true do +describe Gitlab::Git::DiffCollection, :seed_helper do subject do Gitlab::Git::DiffCollection.new( iterator, diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 3bb0b5be15b..87d9fcee39e 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Gitlab::Git::Diff, seed_helper: true do +describe Gitlab::Git::Diff, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } before do @@ -27,6 +27,7 @@ EOT too_large: false } + # TODO use a Gitaly diff object instead @rugged_diff = Gitlab::GitalyClient::StorageSettings.allow_disk_access do repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths: [".gitmodules"]).patches.first @@ -266,8 +267,12 @@ EOT describe '#submodule?' do before do - commit = repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e') - @diffs = commit.parents[0].diff(commit).patches + # TODO use a Gitaly diff object instead + rugged_commit = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.rugged.rev_parse('5937ac0a7beb003549fc5fd26fc247adbce4a52e') + end + + @diffs = rugged_commit.parents[0].diff(rugged_commit).patches end it { expect(described_class.new(@diffs[0]).submodule?).to eq(false) } diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb index 9337aa39e13..55ffced36ac 100644 --- a/spec/lib/gitlab/git/hooks_service_spec.rb +++ b/spec/lib/gitlab/git/hooks_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Git::HooksService, seed_helper: true do +describe Gitlab::Git::HooksService, :seed_helper do let(:gl_id) { 'user-456' } let(:gl_username) { 'janedoe' } let(:user) { Gitlab::Git::User.new(gl_username, 'Jane Doe', 'janedoe@example.com', gl_id) } diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb index 16e6bd35449..c4edd6961e1 100644 --- a/spec/lib/gitlab/git/index_spec.rb +++ b/spec/lib/gitlab/git/index_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' -describe Gitlab::Git::Index, seed_helper: true do +describe Gitlab::Git::Index, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } let(:index) { described_class.new(repository) } before do - index.read_tree(repository.lookup('master').tree) + index.read_tree(lookup('master').tree) end around do |example| @@ -30,7 +30,7 @@ describe Gitlab::Git::Index, seed_helper: true do entry = index.get(options[:file_path]) expect(entry).not_to be_nil - expect(repository.lookup(entry[:oid]).content).to eq(options[:content]) + expect(lookup(entry[:oid]).content).to eq(options[:content]) end end @@ -54,7 +54,7 @@ describe Gitlab::Git::Index, seed_helper: true do index.create(options) entry = index.get(options[:file_path]) - expect(repository.lookup(entry[:oid]).content).to eq(Base64.decode64(options[:content])) + expect(lookup(entry[:oid]).content).to eq(Base64.decode64(options[:content])) end end @@ -68,7 +68,7 @@ describe Gitlab::Git::Index, seed_helper: true do index.create(options) entry = index.get(options[:file_path]) - expect(repository.lookup(entry[:oid]).content).to eq("Hello,\nWorld") + expect(lookup(entry[:oid]).content).to eq("Hello,\nWorld") end end end @@ -135,7 +135,7 @@ describe Gitlab::Git::Index, seed_helper: true do entry = index.get(options[:file_path]) - expect(repository.lookup(entry[:oid]).content).to eq(options[:content]) + expect(lookup(entry[:oid]).content).to eq(options[:content]) end it 'preserves file mode' do @@ -190,7 +190,7 @@ describe Gitlab::Git::Index, seed_helper: true do entry = index.get(options[:file_path]) expect(entry).not_to be_nil - expect(repository.lookup(entry[:oid]).content).to eq(options[:content]) + expect(lookup(entry[:oid]).content).to eq(options[:content]) end it 'preserves file mode' do @@ -232,4 +232,8 @@ describe Gitlab::Git::Index, seed_helper: true do end end end + + def lookup(revision) + repository.rugged.rev_parse(revision) + end end diff --git a/spec/lib/gitlab/git/popen_spec.rb b/spec/lib/gitlab/git/popen_spec.rb index b033ede9062..074e66d2a5d 100644 --- a/spec/lib/gitlab/git/popen_spec.rb +++ b/spec/lib/gitlab/git/popen_spec.rb @@ -2,6 +2,9 @@ require 'spec_helper' describe 'Gitlab::Git::Popen' do let(:path) { Rails.root.join('tmp').to_s } + let(:test_string) { 'The quick brown fox jumped over the lazy dog' } + # The pipe buffer is typically 64K. This string is about 440K. + let(:spew_command) { ['bash', '-c', "for i in {1..10000}; do echo '#{test_string}' 1>&2; done"] } let(:klass) do Class.new(Object) do @@ -70,6 +73,15 @@ describe 'Gitlab::Git::Popen' do end end end + + context 'with a process that writes a lot of data to stderr' do + it 'returns zero' do + output, status = klass.new.popen(spew_command, path) + + expect(output).to include(test_string) + expect(status).to eq(0) + end + end end context 'popen_with_timeout' do @@ -85,6 +97,17 @@ describe 'Gitlab::Git::Popen' do it { expect(output).to include('tests') } end + context 'multi-line string' do + let(:test_string) { "this is 1 line\n2nd line\n3rd line\n" } + let(:result) { klass.new.popen_with_timeout(['echo', test_string], timeout, path) } + let(:output) { result.first } + let(:status) { result.last } + + it { expect(status).to be_zero } + # echo adds its own line + it { expect(output).to eq(test_string + "\n") } + end + context 'non-zero status' do let(:result) { klass.new.popen_with_timeout(%w(cat NOTHING), timeout, path) } let(:output) { result.first } @@ -110,6 +133,13 @@ describe 'Gitlab::Git::Popen' do it "handles processes that do not shutdown correctly" do expect { klass.new.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error) end + + it 'handles process that writes a lot of data to stderr' do + output, status = klass.new.popen_with_timeout(spew_command, timeout, path) + + expect(output).to include(test_string) + expect(status).to eq(0) + end end context 'timeout period' do diff --git a/spec/lib/gitlab/git/remote_repository_spec.rb b/spec/lib/gitlab/git/remote_repository_spec.rb index eb148cc3804..53ed7c5a13a 100644 --- a/spec/lib/gitlab/git/remote_repository_spec.rb +++ b/spec/lib/gitlab/git/remote_repository_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Git::RemoteRepository, seed_helper: true do +describe Gitlab::Git::RemoteRepository, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } subject { described_class.new(repository) } diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 6ec4b90d70c..35a6fc94753 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1,7 +1,7 @@ # coding: utf-8 require "spec_helper" -describe Gitlab::Git::Repository, seed_helper: true do +describe Gitlab::Git::Repository, :seed_helper do include Gitlab::EncodingHelper using RSpec::Parameterized::TableSyntax @@ -321,90 +321,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - context '#submodules' do - around do |example| - # TODO #submodules will be removed, has been migrated to gitaly - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } - - context 'where repo has submodules' do - let(:submodules) { repository.send(: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", - "name" => "six", - "url" => "git://github.com/randx/six.git" - } - ]) - end - - it 'should handle nested submodules correctly' do - nested = submodules['nested/six'] - expect(nested['name']).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['name']).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.send(:submodules, 'fix-existing-submodule-dir') - expect(submodules).not_to have_key('submodule-existing-dir') - end - - it 'should handle tags correctly' do - submodules = repository.send(:submodules, 'v1.2.1') - - expect(submodules.first).to eq([ - "six", { - "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d", - "name" => "six", - "url" => "git://github.com/randx/six.git" - } - ]) - end - - it 'should not break on invalid syntax' do - allow(repository).to receive(:blob_content).and_return(<<-GITMODULES.strip_heredoc) - [submodule "six"] - path = six - url = git://github.com/randx/six.git - - [submodule] - foo = bar - GITMODULES - - expect(submodules).to have_key('six') - end - end - - context 'where repo doesn\'t have submodules' do - let(:submodules) { repository.send(:submodules, '6d39438') } - it 'should return an empty hash' do - expect(submodules).to be_empty - end - end - end - describe '#commit_count' do shared_examples 'simple commit counting' do it { expect(repository.commit_count("master")).to eq(25) } @@ -611,38 +527,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "#remove_remote" do - before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.remove_remote("expendable") - end - - it "should remove the remote" do - expect(@repo.rugged.remotes).not_to include("expendable") - end - - after(:all) do - ensure_seeds - end - end - - describe "#remote_update" do - before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH) - end - - it "should add the remote" do - expect(@repo.rugged.remotes["expendable"].url).to( - eq(TEST_NORMAL_REPO_PATH) - ) - end - - after(:all) do - ensure_seeds - end - end - describe '#fetch_repository_as_mirror' do let(:new_repository) do Gitlab::Git::Repository.new('default', 'my_project.git', '') @@ -699,58 +583,24 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#remote_tags' do - let(:remote_name) { 'upstream' } - let(:target_commit_id) { SeedRepo::Commit::ID } - let(:tag_name) { 'v0.0.1' } - let(:tag_message) { 'My tag' } - let(:remote_repository) do - Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - end - - around do |example| - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - - subject { repository.remote_tags(remote_name) } - - before do - remote_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { remote_repository.path } - repository.add_remote(remote_name, remote_repository_path) - remote_repository.add_tag(tag_name, user: user, target: target_commit_id) - end - - after do - ensure_seeds - end - - it 'gets the remote tags' do - expect(subject.first).to be_an_instance_of(Gitlab::Git::Tag) - expect(subject.first.name).to eq(tag_name) - expect(subject.first.dereferenced_target.id).to eq(target_commit_id) - end - end - describe "#log" do shared_examples 'repository log' do let(:commit_with_old_name) do - Gitlab::Git::Commit.decorate(repository, @commit_with_old_name_id) + Gitlab::Git::Commit.find(repository, @commit_with_old_name_id) end let(:commit_with_new_name) do - Gitlab::Git::Commit.decorate(repository, @commit_with_new_name_id) + Gitlab::Git::Commit.find(repository, @commit_with_new_name_id) end let(:rename_commit) do - Gitlab::Git::Commit.decorate(repository, @rename_commit_id) + Gitlab::Git::Commit.find(repository, @rename_commit_id) end before(:context) do # Add new commits so that there's a renamed file in the commit history repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged - @commit_with_old_name_id = new_commit_edit_old_file(repo) - @rename_commit_id = new_commit_move_file(repo) - @commit_with_new_name_id = new_commit_edit_new_file(repo) + @commit_with_old_name_id = new_commit_edit_old_file(repo).oid + @rename_commit_id = new_commit_move_file(repo).oid + @commit_with_new_name_id = new_commit_edit_new_file(repo).oid end after(:context) do @@ -952,8 +802,8 @@ describe Gitlab::Git::Repository, seed_helper: true do def commit_files(commit) Gitlab::GitalyClient::StorageSettings.allow_disk_access do - commit.rugged_diff_from_parent.deltas.flat_map do |delta| - [delta.old_file[:path], delta.new_file[:path]].uniq.compact + commit.deltas.flat_map do |delta| + [delta.old_path, delta.new_path].uniq.compact end end end @@ -990,10 +840,6 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'when Gitaly find_commits feature is enabled' do it_behaves_like 'repository log' end - - context 'when Gitaly find_commits feature is disabled', :disable_gitaly do - it_behaves_like 'repository log' - end end describe '#count_commits_between' do @@ -1157,6 +1003,13 @@ describe Gitlab::Git::Repository, seed_helper: true do @repo.rugged.config['core.autocrlf'] = true end + around do |example| + # OK because autocrlf is only used in gitaly-ruby + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + example.run + end + end + it 'return the value of the autocrlf option' do expect(@repo.autocrlf).to be(true) end @@ -1172,6 +1025,13 @@ describe Gitlab::Git::Repository, seed_helper: true do @repo.rugged.config['core.autocrlf'] = false end + around do |example| + # OK because autocrlf= is only used in gitaly-ruby + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + example.run + end + end + it 'should set the autocrlf option to the provided option' do @repo.autocrlf = :input @@ -1186,50 +1046,17 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#find_branch' do - shared_examples 'finding a 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') + it 'should return a Branch for master' do + branch = repository.find_branch('master') - expect(branch).to eq(nil) - end - end - - context 'when Gitaly find_branch feature is enabled' do - it_behaves_like 'finding a branch' + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') end - context 'when Gitaly find_branch feature is disabled', :skip_gitaly_mock do - it_behaves_like 'finding a branch' - - context 'force_reload is true' do - it 'should reload Rugged::Repository' do - expect(Rugged::Repository).to receive(:new).twice.and_call_original + it 'should handle non-existent branch' do + branch = repository.find_branch('this-is-garbage') - 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 - - context 'force_reload is false' do - it 'should not reload Rugged::Repository' do - expect(Rugged::Repository).to receive(:new).once.and_call_original - - branch = repository.find_branch('master', force_reload: false) - - expect(branch).to be_a_kind_of(Gitlab::Git::Branch) - expect(branch.name).to eq('master') - end - end + expect(branch).to eq(nil) end end @@ -1402,94 +1229,84 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#copy_gitattributes" do - shared_examples 'applying git attributes' do - let(:attributes_path) { File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info/attributes') } + let(:attributes_path) { File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info/attributes') } - after do - FileUtils.rm_rf(attributes_path) if Dir.exist?(attributes_path) - end - - it "raises an error with invalid ref" do - expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef) - end - - context 'when forcing encoding issues' do - let(:branch_name) { "ʕ•ᴥ•ʔ" } + after do + FileUtils.rm_rf(attributes_path) if Dir.exist?(attributes_path) + end - before do - repository.create_branch(branch_name, "master") - end + it "raises an error with invalid ref" do + expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef) + end - after do - repository.rm_branch(branch_name, user: build(:admin)) - end + context 'when forcing encoding issues' do + let(:branch_name) { "ʕ•ᴥ•ʔ" } - it "doesn't raise with a valid unicode ref" do - expect { repository.copy_gitattributes(branch_name) }.not_to raise_error + before do + repository.create_branch(branch_name, "master") + end - repository - end + after do + repository.rm_branch(branch_name, user: build(:admin)) end - context "with no .gitattrbutes" do - before do - repository.copy_gitattributes("master") - end + it "doesn't raise with a valid unicode ref" do + expect { repository.copy_gitattributes(branch_name) }.not_to raise_error - it "does not have an info/attributes" do - expect(File.exist?(attributes_path)).to be_falsey - end + repository end + end - context "with .gitattrbutes" do - before do - repository.copy_gitattributes("gitattributes") - end + context "with no .gitattrbutes" do + before do + repository.copy_gitattributes("master") + end - it "has an info/attributes" do - expect(File.exist?(attributes_path)).to be_truthy - end + it "does not have an info/attributes" do + expect(File.exist?(attributes_path)).to be_falsey + end + 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 + context "with .gitattrbutes" do + before do + repository.copy_gitattributes("gitattributes") end - context "with updated .gitattrbutes" do - before 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 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 + end - it "has the updated content in info/attributes" do - contents = File.read(attributes_path) - expect(contents).to eq("*.txt binary\n") - end + context "with updated .gitattrbutes" do + before do + repository.copy_gitattributes("gitattributes") + repository.copy_gitattributes("gitattributes-updated") end - context "with no .gitattrbutes in HEAD but with previous info/attributes" do - before do - repository.copy_gitattributes("gitattributes") - repository.copy_gitattributes("master") - end + it "has an info/attributes" do + expect(File.exist?(attributes_path)).to be_truthy + end - it "does not have an info/attributes" do - expect(File.exist?(attributes_path)).to be_falsey - end + it "has the updated content in info/attributes" do + contents = File.read(attributes_path) + expect(contents).to eq("*.txt binary\n") end end - context 'when gitaly is enabled' do - it_behaves_like 'applying git attributes' - end + context "with no .gitattrbutes in HEAD but with previous info/attributes" do + before do + repository.copy_gitattributes("gitattributes") + repository.copy_gitattributes("master") + end - context 'when gitaly is disabled', :disable_gitaly do - it_behaves_like 'applying git attributes' + it "does not have an info/attributes" do + expect(File.exist?(attributes_path)).to be_falsey + end end end @@ -1567,31 +1384,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#batch_existence' do - let(:refs) { ['deadbeef', SeedRepo::RubyBlob::ID, '909e6157199'] } - - around do |example| - # TODO #batch_existence isn't used anywhere, can we remove it? - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - - it 'returns existing refs back' do - result = repository.batch_existence(refs) - - expect(result).to eq([SeedRepo::RubyBlob::ID]) - end - - context 'existing: true' do - it 'inverts meaning and returns non-existing refs' do - result = repository.batch_existence(refs, existing: false) - - expect(result).to eq(%w(deadbeef 909e6157199)) - end - end - end - describe '#local_branches' do before(:all) do @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') @@ -1726,59 +1518,51 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#fetch_source_branch!' do - shared_examples '#fetch_source_branch!' do - let(:local_ref) { 'refs/merge-requests/1/head' } - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } - let(:source_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - - after do - ensure_seeds - end + let(:local_ref) { 'refs/merge-requests/1/head' } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:source_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - context 'when the branch exists' do - context 'when the commit does not exist locally' do - let(:source_branch) { 'new-branch-for-fetch-source-branch' } - let(:source_rugged) { Gitlab::GitalyClient::StorageSettings.allow_disk_access { source_repository.rugged } } - let(:new_oid) { new_commit_edit_old_file(source_rugged).oid } + after do + ensure_seeds + end - before do - source_rugged.branches.create(source_branch, new_oid) - end + context 'when the branch exists' do + context 'when the commit does not exist locally' do + let(:source_branch) { 'new-branch-for-fetch-source-branch' } + let(:source_rugged) { Gitlab::GitalyClient::StorageSettings.allow_disk_access { source_repository.rugged } } + let(:new_oid) { new_commit_edit_old_file(source_rugged).oid } - it 'writes the ref' do - expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true) - expect(repository.commit(local_ref).sha).to eq(new_oid) - end + before do + source_rugged.branches.create(source_branch, new_oid) end - context 'when the commit exists locally' do - let(:source_branch) { 'master' } - let(:expected_oid) { SeedRepo::LastCommit::ID } - - it 'writes the ref' do - # Sanity check: the commit should already exist - expect(repository.commit(expected_oid)).not_to be_nil - - expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true) - expect(repository.commit(local_ref).sha).to eq(expected_oid) - end + it 'writes the ref' do + expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true) + expect(repository.commit(local_ref).sha).to eq(new_oid) end end - context 'when the branch does not exist' do - let(:source_branch) { 'definitely-not-master' } + context 'when the commit exists locally' do + let(:source_branch) { 'master' } + let(:expected_oid) { SeedRepo::LastCommit::ID } - it 'does not write the ref' do - expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(false) - expect(repository.commit(local_ref)).to be_nil + it 'writes the ref' do + # Sanity check: the commit should already exist + expect(repository.commit(expected_oid)).not_to be_nil + + expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true) + expect(repository.commit(local_ref).sha).to eq(expected_oid) end end end - it_behaves_like '#fetch_source_branch!' + context 'when the branch does not exist' do + let(:source_branch) { 'definitely-not-master' } - context 'without gitaly', :skip_gitaly_mock do - it_behaves_like '#fetch_source_branch!' + it 'does not write the ref' do + expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(false) + expect(repository.commit(local_ref)).to be_nil + end end end @@ -1866,6 +1650,54 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#set_config' do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } + let(:rugged) { repository_rugged } + let(:entries) do + { + 'test.foo1' => 'bla bla', + 'test.foo2' => 1234, + 'test.foo3' => true + } + end + + it 'can set config settings' do + expect(repository.set_config(entries)).to be_nil + + expect(rugged.config['test.foo1']).to eq('bla bla') + expect(rugged.config['test.foo2']).to eq('1234') + expect(rugged.config['test.foo3']).to eq('true') + end + + after do + entries.keys.each { |k| rugged.config.delete(k) } + end + end + + describe '#delete_config' do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } + let(:rugged) { repository_rugged } + let(:entries) do + { + 'test.foo1' => 'bla bla', + 'test.foo2' => 1234, + 'test.foo3' => true + } + end + + it 'can delete config settings' do + entries.each do |key, value| + rugged.config[key] = value + end + + expect(repository.delete_config(*%w[does.not.exist test.foo1 test.foo2])).to be_nil + + config_keys = rugged.config.each_key.to_a + expect(config_keys).not_to include('test.foo1') + expect(config_keys).not_to include('test.foo2') + end + end + describe '#merge' do let(:repository) do Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') @@ -1971,21 +1803,15 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - context 'with gitaly' do - it "calls Gitaly's OperationService" do - expect_any_instance_of(Gitlab::GitalyClient::OperationService) - .to receive(:user_ff_branch).with(user, source_sha, target_branch) - .and_return(nil) - - subject - end + it "calls Gitaly's OperationService" do + expect_any_instance_of(Gitlab::GitalyClient::OperationService) + .to receive(:user_ff_branch).with(user, source_sha, target_branch) + .and_return(nil) - it_behaves_like '#ff_merge' + subject end - context 'without gitaly', :skip_gitaly_mock do - it_behaves_like '#ff_merge' - end + it_behaves_like '#ff_merge' end describe '#delete_all_refs_except' do @@ -2018,54 +1844,61 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:repository) do Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') end + let(:rugged) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository.rugged } + end let(:remote_name) { 'my-remote' } + let(:url) { 'http://my-repo.git' } after do ensure_seeds end describe '#add_remote' do - let(:url) { 'http://my-repo.git' } let(:mirror_refmap) { '+refs/*:refs/*' } - it 'creates a new remote via Gitaly' do - expect_any_instance_of(Gitlab::GitalyClient::RemoteService) - .to receive(:add_remote).with(remote_name, url, mirror_refmap) + shared_examples 'add_remote' do + it 'added the remote' do + begin + rugged.remotes.delete(remote_name) + rescue Rugged::ConfigError + end + + repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap) - repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap) + expect(rugged.remotes[remote_name]).not_to be_nil + expect(rugged.config["remote.#{remote_name}.mirror"]).to eq('true') + expect(rugged.config["remote.#{remote_name}.prune"]).to eq('true') + expect(rugged.config["remote.#{remote_name}.fetch"]).to eq(mirror_refmap) + end end - context 'with Gitaly disabled', :skip_gitaly_mock do - it 'creates a new remote via Rugged' do - expect_any_instance_of(Rugged::RemoteCollection).to receive(:create) - .with(remote_name, url) - expect_any_instance_of(Rugged::Config).to receive(:[]=) - .with("remote.#{remote_name}.mirror", true) - expect_any_instance_of(Rugged::Config).to receive(:[]=) - .with("remote.#{remote_name}.prune", true) - expect_any_instance_of(Rugged::Config).to receive(:[]=) - .with("remote.#{remote_name}.fetch", mirror_refmap) + context 'using Gitaly' do + it_behaves_like 'add_remote' + end - repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap) - end + context 'with Gitaly disabled', :disable_gitaly do + it_behaves_like 'add_remote' end end describe '#remove_remote' do - it 'removes the remote via Gitaly' do - expect_any_instance_of(Gitlab::GitalyClient::RemoteService) - .to receive(:remove_remote).with(remote_name) + shared_examples 'remove_remote' do + it 'removes the remote' do + rugged.remotes.create(remote_name, url) + + repository.remove_remote(remote_name) - repository.remove_remote(remote_name) + expect(rugged.remotes[remote_name]).to be_nil + end end - context 'with Gitaly disabled', :skip_gitaly_mock do - it 'removes the remote via Rugged' do - expect_any_instance_of(Rugged::RemoteCollection).to receive(:delete) - .with(remote_name) + context 'using Gitaly' do + it_behaves_like 'remove_remote' + end - repository.remove_remote(remote_name) - end + context 'with Gitaly disabled', :disable_gitaly do + it_behaves_like 'remove_remote' end end end @@ -2257,20 +2090,25 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:worktree_path) { File.join(repository_path, 'worktrees', 'delete-me') } it 'cleans up the files' do - repository.with_worktree(worktree_path, 'master', env: ENV) do - FileUtils.touch(worktree_path, mtime: Time.now - 8.hours) - # git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object, - # but the HEAD must be 40 characters long or git will ignore it. - File.write(File.join(worktree_path, 'HEAD'), Gitlab::Git::BLANK_SHA) + create_worktree = %W[git -C #{repository_path} worktree add --detach #{worktree_path} master] + raise 'preparation failed' unless system(*create_worktree, err: '/dev/null') - # git 2.16 fails with "fatal: bad object HEAD" - expect { repository.rev_list(including: :all) }.to raise_error(Gitlab::Git::Repository::GitError) + FileUtils.touch(worktree_path, mtime: Time.now - 8.hours) + # git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object, + # but the HEAD must be 40 characters long or git will ignore it. + File.write(File.join(worktree_path, 'HEAD'), Gitlab::Git::BLANK_SHA) - repository.clean_stale_repository_files + # git 2.16 fails with "fatal: bad object HEAD" + expect(rev_list_all).to be false - expect { repository.rev_list(including: :all) }.not_to raise_error - expect(File.exist?(worktree_path)).to be_falsey - end + repository.clean_stale_repository_files + + expect(rev_list_all).to be true + expect(File.exist?(worktree_path)).to be_falsey + end + + def rev_list_all + system(*%W[git -C #{repository_path} rev-list --all], out: '/dev/null', err: '/dev/null') end it 'increments a counter upon an error' do @@ -2308,92 +2146,95 @@ describe Gitlab::Git::Repository, seed_helper: true do expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error') end end + end - describe '#squash' do - let(:squash_id) { '1' } - let(:branch_name) { 'fix' } - let(:start_sha) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } - let(:end_sha) { '12d65c8dd2b2676fa3ac47d955accc085a37a9c1' } + describe '#squash' do + let(:squash_id) { '1' } + let(:branch_name) { 'fix' } + let(:start_sha) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } + let(:end_sha) { '12d65c8dd2b2676fa3ac47d955accc085a37a9c1' } - subject do - opts = { - branch: branch_name, - start_sha: start_sha, - end_sha: end_sha, - author: user, - message: 'Squash commit message' - } + subject do + opts = { + branch: branch_name, + start_sha: start_sha, + end_sha: end_sha, + author: user, + message: 'Squash commit message' + } + + repository.squash(user, squash_id, opts) + end + + # Should be ported to gitaly-ruby rspec suite https://gitlab.com/gitlab-org/gitaly/issues/1234 + skip 'sparse checkout' do + let(:expected_files) { %w(files files/js files/js/application.js) } + + it 'checks out only the files in the diff' do + allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args| + m.call(*args) do + worktree_path = args[0] + files_pattern = File.join(worktree_path, '**', '*') + expected = expected_files.map do |path| + File.expand_path(path, worktree_path) + end + + expect(Dir[files_pattern]).to eq(expected) + end + end - repository.squash(user, squash_id, opts) + subject end - context 'sparse checkout', :skip_gitaly_mock do - let(:expected_files) { %w(files files/js files/js/application.js) } + context 'when the diff contains a rename' do + let(:repo) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged } + let(:end_sha) { new_commit_move_file(repo).oid } + + after do + # Erase our commits so other tests get the original repo + repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged + repo.references.update('refs/heads/master', SeedRepo::LastCommit::ID) + end - it 'checks out only the files in the diff' do + it 'does not include the renamed file in the sparse checkout' do allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args| m.call(*args) do worktree_path = args[0] files_pattern = File.join(worktree_path, '**', '*') - expected = expected_files.map do |path| - File.expand_path(path, worktree_path) - end - expect(Dir[files_pattern]).to eq(expected) + expect(Dir[files_pattern]).not_to include('CHANGELOG') + expect(Dir[files_pattern]).not_to include('encoding/CHANGELOG') end end subject end - - context 'when the diff contains a rename' do - let(:repo) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged } - let(:end_sha) { new_commit_move_file(repo).oid } - - after do - # Erase our commits so other tests get the original repo - repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged - repo.references.update('refs/heads/master', SeedRepo::LastCommit::ID) - end - - it 'does not include the renamed file in the sparse checkout' do - allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args| - m.call(*args) do - worktree_path = args[0] - files_pattern = File.join(worktree_path, '**', '*') - - expect(Dir[files_pattern]).not_to include('CHANGELOG') - expect(Dir[files_pattern]).not_to include('encoding/CHANGELOG') - end - end - - subject - end - end end + end - context 'with an ASCII-8BIT diff', :skip_gitaly_mock do - let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+✓ testme\n ======\n \n Sample repo for testing gitlab features\n" } + # Should be ported to gitaly-ruby rspec suite https://gitlab.com/gitlab-org/gitaly/issues/1234 + skip 'with an ASCII-8BIT diff' do + let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+✓ testme\n ======\n \n Sample repo for testing gitlab features\n" } - it 'applies a ASCII-8BIT diff' do - allow(repository).to receive(:run_git!).and_call_original - allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT')) + it 'applies a ASCII-8BIT diff' do + allow(repository).to receive(:run_git!).and_call_original + allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT')) - expect(subject).to match(/\h{40}/) - end + expect(subject).to match(/\h{40}/) end + end - context 'with trailing whitespace in an invalid patch', :skip_gitaly_mock do - let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+ \n ====== \n \n Sample repo for testing gitlab features\n" } + # Should be ported to gitaly-ruby rspec suite https://gitlab.com/gitlab-org/gitaly/issues/1234 + skip 'with trailing whitespace in an invalid patch' do + let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+ \n ====== \n \n Sample repo for testing gitlab features\n" } - it 'does not include whitespace warnings in the error' do - allow(repository).to receive(:run_git!).and_call_original - allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT')) + it 'does not include whitespace warnings in the error' do + allow(repository).to receive(:run_git!).and_call_original + allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT')) - expect { subject }.to raise_error do |error| - expect(error).to be_a(described_class::GitError) - expect(error.message).not_to include('trailing whitespace') - end + expect { subject }.to raise_error do |error| + expect(error).to be_a(described_class::GitError) + expect(error.message).not_to include('trailing whitespace') end end end diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb deleted file mode 100644 index b752c3e8341..00000000000 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::RevList do - let(:repository) { create(:project, :repository).repository.raw } - let(:rev_list) { described_class.new(repository, newrev: 'newrev') } - - def args_for_popen(args_list) - [Gitlab.config.git.bin_path, 'rev-list', *args_list] - end - - def stub_popen_rev_list(*additional_args, with_lazy_block: true, output:) - repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository.path } - - params = [ - args_for_popen(additional_args), - repo_path, - {}, - hash_including(lazy_block: with_lazy_block ? anything : nil) - ] - - expect(repository).to receive(:popen).with(*params) do |*_, lazy_block:| - output = lazy_block.call(output.lines.lazy.map(&:chomp)) if with_lazy_block - - [output, 0] - end - end - - context "#new_refs" do - it 'calls out to `popen`' do - stub_popen_rev_list('newrev', '--not', '--all', with_lazy_block: false, output: "sha1\nsha2") - - expect(rev_list.new_refs).to eq(%w[sha1 sha2]) - end - end - - context '#new_objects' do - it 'fetches list of newly pushed objects using rev-list' do - stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") - - expect { |b| rev_list.new_objects(&b) }.to yield_with_args(%w[sha1 sha2]) - end - - it 'can skip pathless objects' do - stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2 path/to/file") - - expect { |b| rev_list.new_objects(require_path: true, &b) }.to yield_with_args(%w[sha2]) - end - - it 'can handle non utf-8 paths' do - non_utf_char = [0x89].pack("c*").force_encoding("UTF-8") - stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha2 πå†h/†ø/ƒîlé#{non_utf_char}\nsha1") - - rev_list.new_objects(require_path: true) do |object_ids| - expect(object_ids.force).to eq(%w[sha2]) - end - end - - it 'can yield a lazy enumerator' do - stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") - - rev_list.new_objects do |object_ids| - expect(object_ids).to be_a Enumerator::Lazy - end - end - - it 'returns the result of the block when given' do - stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") - - objects = rev_list.new_objects do |object_ids| - object_ids.first - end - - expect(objects).to eq 'sha1' - end - - it 'can accept list of references to exclude' do - stub_popen_rev_list('newrev', '--not', 'master', '--objects', output: "sha1\nsha2") - - expect { |b| rev_list.new_objects(not_in: ['master'], &b) }.to yield_with_args(%w[sha1 sha2]) - end - - it 'handles empty list of references to exclude as listing all known objects' do - stub_popen_rev_list('newrev', '--objects', output: "sha1\nsha2") - - expect { |b| rev_list.new_objects(not_in: [], &b) }.to yield_with_args(%w[sha1 sha2]) - end - end - - context '#all_objects' do - it 'fetches list of all pushed objects using rev-list' do - stub_popen_rev_list('--all', '--objects', output: "sha1\nsha2") - - expect { |b| rev_list.all_objects(&b) }.to yield_with_args(%w[sha1 sha2]) - end - end -end diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb index be2f5bfb819..2d9db576a6c 100644 --- a/spec/lib/gitlab/git/tag_spec.rb +++ b/spec/lib/gitlab/git/tag_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Gitlab::Git::Tag, seed_helper: true do +describe Gitlab::Git::Tag, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } shared_examples 'Gitlab::Git::Repository#tags' do diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 001e406a930..3792d6bf67b 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Gitlab::Git::Tree, seed_helper: true do +describe Gitlab::Git::Tree, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } context :repo do diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb index b63658e1b3b..c5666e4ec61 100644 --- a/spec/lib/gitlab/git/wiki_spec.rb +++ b/spec/lib/gitlab/git/wiki_spec.rb @@ -6,6 +6,31 @@ describe Gitlab::Git::Wiki do let(:project_wiki) { ProjectWiki.new(project, user) } subject { project_wiki.wiki } + describe '#pages' do + before do + create_page('page1', 'content') + create_page('page2', 'content2') + end + + after do + destroy_page('page1') + destroy_page('page2') + end + + it 'returns all the pages' do + expect(subject.pages.count).to eq(2) + expect(subject.pages.first.title).to eq 'page1' + expect(subject.pages.last.title).to eq 'page2' + end + + it 'returns only one page' do + pages = subject.pages(limit: 1) + + expect(pages.count).to eq(1) + expect(pages.first.title).to eq 'page1' + end + end + describe '#page' do before do create_page('page1', 'content') diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index ff32025253a..dbd64c4bec0 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -13,14 +13,6 @@ describe Gitlab::GitAccess do let(:authentication_abilities) { %i[read_project download_code push_code] } let(:redirected_path) { nil } let(:auth_result_type) { nil } - - let(:access) do - described_class.new(actor, project, - protocol, authentication_abilities: authentication_abilities, - namespace_path: namespace_path, project_path: project_path, - redirected_path: redirected_path, auth_result_type: auth_result_type) - end - let(:changes) { '_any' } let(:push_access_check) { access.check('git-receive-pack', changes) } let(:pull_access_check) { access.check('git-upload-pack', changes) } @@ -48,7 +40,7 @@ describe Gitlab::GitAccess do before do disable_protocol('http') - project.add_master(user) + project.add_maintainer(user) end it 'blocks http push and pull' do @@ -113,7 +105,7 @@ describe Gitlab::GitAccess do context 'when actor is a User' do context 'when the User can read the project' do before do - project.add_master(user) + project.add_maintainer(user) end it 'allows push and pull access' do @@ -254,7 +246,7 @@ describe Gitlab::GitAccess do shared_examples '#check with a key that is not valid' do before do - project.add_master(user) + project.add_maintainer(user) end context 'key is too small' do @@ -307,7 +299,7 @@ describe Gitlab::GitAccess do describe '#add_project_moved_message!', :clean_gitlab_redis_shared_state do before do - project.add_master(user) + project.add_maintainer(user) end context 'when a redirect was not followed to find the project' do @@ -335,7 +327,7 @@ describe Gitlab::GitAccess do describe '#check_authentication_abilities!' do before do - project.add_master(user) + project.add_maintainer(user) end context 'when download' do @@ -381,7 +373,7 @@ describe Gitlab::GitAccess do describe '#check_command_disabled!' do before do - project.add_master(user) + project.add_maintainer(user) end context 'over http' do @@ -529,8 +521,8 @@ describe Gitlab::GitAccess do end describe '#check_download_access!' do - it 'allows masters to pull' do - project.add_master(user) + it 'allows maintainers to pull' do + project.add_maintainer(user) expect { pull_access_check }.not_to raise_error end @@ -542,7 +534,7 @@ describe Gitlab::GitAccess do end it 'disallows blocked users to pull' do - project.add_master(user) + project.add_maintainer(user) user.block expect { pull_access_check }.to raise_unauthorized('Your account has been blocked.') @@ -724,10 +716,11 @@ describe Gitlab::GitAccess do end describe '#check_push_access!' do + let(:unprotected_branch) { 'unprotected_branch' } + before do merge_into_protected_branch end - let(:unprotected_branch) { 'unprotected_branch' } let(:changes) do { push_new_branch: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow", @@ -741,26 +734,18 @@ describe Gitlab::GitAccess do merge_into_protected_branch: "0b4bc9a #{merge_into_protected_branch} refs/heads/feature" } end - def stub_git_hooks - # Running the `pre-receive` hook is expensive, and not necessary for this test. - allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) do |service, &block| - block.call(service) - end - end - def merge_into_protected_branch @protected_branch_merge_commit ||= begin Gitlab::GitalyClient::StorageSettings.allow_disk_access do - stub_git_hooks project.repository.add_branch(user, unprotected_branch, 'feature') - target_branch = project.repository.lookup('feature') + rugged = project.repository.rugged + target_branch = rugged.rev_parse('feature') source_branch = project.repository.create_file( user, 'filename', 'This is the file content', message: 'This is a good commit message', branch_name: unprotected_branch) - rugged = project.repository.rugged author = { email: "email@example.com", time: Time.now, name: "Example Git User" } merge_index = rugged.merge_commits(target_branch, source_branch) @@ -785,7 +770,7 @@ describe Gitlab::GitAccess do aggregate_failures do matrix.each do |action, allowed| - check = -> { access.send(:check_push_access!, changes[action]) } + check = -> { push_changes(changes[action]) } if allowed expect(&check).not_to raise_error, @@ -812,7 +797,7 @@ describe Gitlab::GitAccess do merge_into_protected_branch: true }, - master: { + maintainer: { push_new_branch: true, push_master: true, push_protected_branch: true, @@ -917,7 +902,7 @@ describe Gitlab::GitAccess do end run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, - master: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, + maintainer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) end end @@ -989,7 +974,7 @@ describe Gitlab::GitAccess do let(:project) { create(:project, :repository, :read_only) } it 'denies push access' do - project.add_master(user) + project.add_maintainer(user) expect { push_access_check }.to raise_unauthorized('The repository is temporarily read-only. Please try again later.') end @@ -1119,9 +1104,9 @@ describe Gitlab::GitAccess do it_behaves_like 'access after accepting terms' end - describe 'as a master of the project' do + describe 'as a maintainer of the project' do before do - project.add_master(user) + project.add_maintainer(user) end it_behaves_like 'access after accepting terms' @@ -1152,6 +1137,17 @@ describe Gitlab::GitAccess do private + def access + described_class.new(actor, project, protocol, + authentication_abilities: authentication_abilities, + namespace_path: namespace_path, project_path: project_path, + redirected_path: redirected_path, auth_result_type: auth_result_type) + end + + def push_changes(changes) + access.check('git-receive-pack', changes) + end + def raise_unauthorized(message) raise_error(Gitlab::GitAccess::UnauthorizedError, message) end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 7951cbe7b1d..54f2ea33f90 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -17,7 +17,7 @@ describe Gitlab::GitalyClient::CommitService do repository: repository_message, left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660', right_commit_id: commit.id, - collapse_diffs: true, + collapse_diffs: false, enforce_limits: true, **Gitlab::Git::DiffCollection.collection_limits.to_h ) @@ -35,7 +35,7 @@ describe Gitlab::GitalyClient::CommitService do repository: repository_message, left_commit_id: Gitlab::Git::EMPTY_TREE_ID, right_commit_id: initial_commit.id, - collapse_diffs: true, + collapse_diffs: false, enforce_limits: true, **Gitlab::Git::DiffCollection.collection_limits.to_h ) diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 9709f1f5646..eaf64e3c9b4 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' describe Gitlab::GitalyClient::OperationService do - let(:project) { create(:project) } + set(:project) { create(:project, :repository) } let(:repository) { project.repository.raw } let(:client) { described_class.new(repository) } - let(:user) { create(:user) } + set(:user) { create(:user) } let(:gitaly_user) { Gitlab::Git::User.from_gitlab(user).to_gitaly } describe '#user_create_branch' do @@ -53,6 +53,47 @@ describe Gitlab::GitalyClient::OperationService do end end + describe '#user_update_branch' do + let(:branch_name) { 'my-branch' } + let(:newrev) { '01e' } + let(:oldrev) { '01d' } + let(:request) do + Gitaly::UserUpdateBranchRequest.new( + repository: repository.gitaly_repository, + branch_name: branch_name, + newrev: newrev, + oldrev: oldrev, + user: gitaly_user + ) + end + let(:response) { Gitaly::UserUpdateBranchResponse.new } + + subject { client.user_update_branch(branch_name, user, newrev, oldrev) } + + it 'sends a user_update_branch message' do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_update_branch).with(request, kind_of(Hash)) + .and_return(response) + + subject + end + + context "when pre_receive_error is present" do + let(:response) do + Gitaly::UserUpdateBranchResponse.new(pre_receive_error: "something failed") + end + + it "throws a PreReceive exception" do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_update_branch).with(request, kind_of(Hash)) + .and_return(response) + + expect { subject }.to raise_error( + Gitlab::Git::PreReceiveError, "something failed") + end + end + end + describe '#user_delete_branch' do let(:branch_name) { 'my-branch' } let(:request) do @@ -110,18 +151,104 @@ describe Gitlab::GitalyClient::OperationService do end let(:response) { Gitaly::UserFFBranchResponse.new(branch_update: branch_update) } - subject { client.user_ff_branch(user, source_sha, target_branch) } - - it 'sends a user_ff_branch message and returns a BranchUpdate object' do + before do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_ff_branch).with(request, kind_of(Hash)) .and_return(response) + end + subject { client.user_ff_branch(user, source_sha, target_branch) } + + it 'sends a user_ff_branch message and returns a BranchUpdate object' do expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate) expect(subject.newrev).to eq(source_sha) expect(subject.repo_created).to be(false) expect(subject.branch_created).to be(false) end + + context 'when the response has no branch_update' do + let(:response) { Gitaly::UserFFBranchResponse.new } + + it { expect(subject).to be_nil } + end + end + + shared_examples 'cherry pick and revert errors' do + context 'when a pre_receive_error is present' do + let(:response) { response_class.new(pre_receive_error: "something failed") } + + it 'raises a PreReceiveError' do + expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "something failed") + end + end + + context 'when a commit_error is present' do + let(:response) { response_class.new(commit_error: "something failed") } + + it 'raises a CommitError' do + expect { subject }.to raise_error(Gitlab::Git::CommitError, "something failed") + end + end + + context 'when a create_tree_error is present' do + let(:response) { response_class.new(create_tree_error: "something failed") } + + it 'raises a CreateTreeError' do + expect { subject }.to raise_error(Gitlab::Git::Repository::CreateTreeError, "something failed") + end + end + + context 'when branch_update is nil' do + let(:response) { response_class.new } + + it { expect(subject).to be_nil } + end + end + + describe '#user_cherry_pick' do + let(:response_class) { Gitaly::UserCherryPickResponse } + + subject do + client.user_cherry_pick( + user: user, + commit: repository.commit, + branch_name: 'master', + message: 'Cherry-pick message', + start_branch_name: 'master', + start_repository: repository + ) + end + + before do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_cherry_pick).with(kind_of(Gitaly::UserCherryPickRequest), kind_of(Hash)) + .and_return(response) + end + + it_behaves_like 'cherry pick and revert errors' + end + + describe '#user_revert' do + let(:response_class) { Gitaly::UserRevertResponse } + + subject do + client.user_revert( + user: user, + commit: repository.commit, + branch_name: 'master', + message: 'Revert message', + start_branch_name: 'master', + start_repository: repository + ) + end + + before do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_revert).with(kind_of(Gitaly::UserRevertRequest), kind_of(Hash)) + .and_return(response) + end + + it_behaves_like 'cherry pick and revert errors' end describe '#user_squash' do @@ -162,7 +289,7 @@ describe Gitlab::GitalyClient::OperationService do Gitaly::UserSquashResponse.new(git_error: "something failed") end - it "throws a PreReceive exception" do + it "raises a GitError exception" do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_squash).with(request, kind_of(Hash)) .and_return(response) @@ -171,5 +298,41 @@ describe Gitlab::GitalyClient::OperationService do Gitlab::Git::Repository::GitError, "something failed") end end + + describe '#user_commit_files' do + subject do + client.user_commit_files( + gitaly_user, 'my-branch', 'Commit files message', [], 'janedoe@example.com', 'Jane Doe', + 'master', repository) + end + + before do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash)) + .and_return(response) + end + + context 'when a pre_receive_error is present' do + let(:response) { Gitaly::UserCommitFilesResponse.new(pre_receive_error: "something failed") } + + it 'raises a PreReceiveError' do + expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "something failed") + end + end + + context 'when an index_error is present' do + let(:response) { Gitaly::UserCommitFilesResponse.new(index_error: "something failed") } + + it 'raises a PreReceiveError' do + expect { subject }.to raise_error(Gitlab::Git::Index::IndexError, "something failed") + end + end + + context 'when branch_update is nil' do + let(:response) { Gitaly::UserCommitFilesResponse.new } + + it { expect(subject).to be_nil } + end + end end end diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 257e4c50f2d..400d426c949 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -18,6 +18,44 @@ describe Gitlab::GitalyClient::RefService do end end + describe '#remote_branches' do + let(:remote_name) { 'my_remote' } + subject { client.remote_branches(remote_name) } + + it 'sends a find_all_remote_branches message' do + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:find_all_remote_branches) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return([]) + + subject + end + + it 'concantes and returns the response branches as Gitlab::Git::Branch objects' do + target_commits = create_list(:gitaly_commit, 4) + response_branches = target_commits.each_with_index.map do |gitaly_commit, i| + Gitaly::Branch.new(name: "#{remote_name}/#{i}", target_commit: gitaly_commit) + end + response = [ + Gitaly::FindAllRemoteBranchesResponse.new(branches: response_branches[0, 2]), + Gitaly::FindAllRemoteBranchesResponse.new(branches: response_branches[2, 2]) + ] + + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:find_all_remote_branches).and_return(response) + + expect(subject.length).to be(response_branches.length) + + response_branches.each_with_index do |gitaly_branch, i| + branch = subject[i] + commit = Gitlab::Git::Commit.new(repository, gitaly_branch.target_commit) + + expect(branch.name).to eq(i.to_s) # It removes the `remote/` prefix + expect(branch.dereferenced_target).to eq(commit) + end + end + end + describe '#branch_names' do it 'sends a find_all_branch_names message' do expect_any_instance_of(Gitaly::RefService::Stub) diff --git a/spec/lib/gitlab/gitaly_client/storage_service_spec.rb b/spec/lib/gitlab/gitaly_client/storage_service_spec.rb new file mode 100644 index 00000000000..6c25e2d6ebd --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/storage_service_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::StorageService do + describe '#delete_all_repositories' do + let!(:project) { create(:project, :repository) } + + it 'removes all repositories' do + described_class.new(project.repository_storage).delete_all_repositories + + expect(project.repository.exists?).to be(false) + end + end +end diff --git a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb index 6ad9f5ef766..5f67fe6b952 100644 --- a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb @@ -70,6 +70,15 @@ describe Gitlab::GitalyClient::WikiService do subject end + it 'sends a limit of 0 to wiki_get_all_pages' do + expect_any_instance_of(Gitaly::WikiService::Stub) + .to receive(:wiki_get_all_pages) + .with(gitaly_request_with_params(limit: 0), kind_of(Hash)) + .and_return([].each) + + subject + end + it 'concatenates the raw data and returns a pair of WikiPage and WikiPageVersion for each page' do expect_any_instance_of(Gitaly::WikiService::Stub) .to receive(:wiki_get_all_pages) @@ -84,5 +93,18 @@ describe Gitlab::GitalyClient::WikiService do expect(wiki_page_2.raw_data).to eq('cd') expect(wiki_page_2_version.format).to eq('markdown') end + + context 'with limits' do + subject { client.get_all_pages(limit: 1) } + + it 'sends a request with the limit' do + expect_any_instance_of(Gitaly::WikiService::Stub) + .to receive(:wiki_get_all_pages) + .with(gitaly_request_with_params(limit: 1), kind_of(Hash)) + .and_return([].each) + + subject + end + end end end diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb index 51fad6c6838..c51985f00a2 100644 --- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb @@ -27,9 +27,9 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do milestone: double(:milestone, number: 4), user: double(:user, id: 4, login: 'alice'), assignee: double(:user, id: 4, login: 'alice'), - created_at: Time.zone.now, - updated_at: Time.zone.now, - merged_at: Time.zone.now + created_at: 1.second.ago, + updated_at: 1.second.ago, + merged_at: 1.second.ago ) end @@ -158,7 +158,6 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do expect(importer.repository_updates_counter) .to receive(:increment) - .with(project: project.path_with_namespace) .and_call_original Timecop.freeze do diff --git a/spec/lib/gitlab/gitlab_import/client_spec.rb b/spec/lib/gitlab/gitlab_import/client_spec.rb index 50e8d7183ce..22ad88e28cb 100644 --- a/spec/lib/gitlab/gitlab_import/client_spec.rb +++ b/spec/lib/gitlab/gitlab_import/client_spec.rb @@ -15,4 +15,88 @@ describe Gitlab::GitlabImport::Client do expect(key).to be_kind_of(Symbol) end end + + it 'uses membership and simple flags' do + stub_request('/api/v4/projects?membership=true&page=1&per_page=100&simple=true') + + expect_any_instance_of(OAuth2::Response).to receive(:parsed).and_return([]) + + expect(client.projects.to_a).to eq [] + end + + shared_examples 'pagination params' do + before do + allow_any_instance_of(OAuth2::Response).to receive(:parsed).and_return([]) + end + + it 'allows page_limit param' do + allow_any_instance_of(OAuth2::Response).to receive(:parsed).and_return(element_list) + + expect(client).to receive(:lazy_page_iterator).with(hash_including(page_limit: 2)).and_call_original + + client.send(method, *args, page_limit: 2, per_page: 1).to_a + end + + it 'allows per_page param' do + expect(client).to receive(:lazy_page_iterator).with(hash_including(per_page: 2)).and_call_original + + client.send(method, *args, per_page: 2).to_a + end + + it 'allows starting_page param' do + expect(client).to receive(:lazy_page_iterator).with(hash_including(starting_page: 3)).and_call_original + + client.send(method, *args, starting_page: 3).to_a + end + end + + describe '#projects' do + subject(:method) { :projects } + let(:args) { [] } + let(:element_list) { build_list(:project, 2) } + + before do + stub_request('/api/v4/projects?membership=true&page=1&per_page=1&simple=true') + stub_request('/api/v4/projects?membership=true&page=2&per_page=1&simple=true') + stub_request('/api/v4/projects?membership=true&page=1&per_page=2&simple=true') + stub_request('/api/v4/projects?membership=true&page=3&per_page=100&simple=true') + end + + it_behaves_like 'pagination params' + end + + describe '#issues' do + subject(:method) { :issues } + let(:args) { [1] } + let(:element_list) { build_list(:issue, 2) } + + before do + stub_request('/api/v4/projects/1/issues?page=1&per_page=1') + stub_request('/api/v4/projects/1/issues?page=2&per_page=1') + stub_request('/api/v4/projects/1/issues?page=1&per_page=2') + stub_request('/api/v4/projects/1/issues?page=3&per_page=100') + end + + it_behaves_like 'pagination params' + end + + describe '#issue_comments' do + subject(:method) { :issue_comments } + let(:args) { [1, 1] } + let(:element_list) { build_list(:note_on_issue, 2) } + + before do + stub_request('/api/v4/projects/1/issues/1/notes?page=1&per_page=1') + stub_request('/api/v4/projects/1/issues/1/notes?page=2&per_page=1') + stub_request('/api/v4/projects/1/issues/1/notes?page=1&per_page=2') + stub_request('/api/v4/projects/1/issues/1/notes?page=3&per_page=100') + end + + it_behaves_like 'pagination params' + end + + def stub_request(path) + WebMock.stub_request(:get, "https://gitlab.com#{path}") + .to_return(status: 200) + end end diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb index 017facd0f5e..031f57dbc65 100644 --- a/spec/lib/gitlab/google_code_import/importer_spec.rb +++ b/spec/lib/gitlab/google_code_import/importer_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::GoogleCodeImport::Importer do subject { described_class.new(project) } before do - project.add_master(project.creator) + project.add_maintainer(project.creator) project.create_import_data(data: import_data) end diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index 6fbffc38444..1a2c6ef25c4 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -141,7 +141,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do expect(invalid_gpg_signature.reload.verification_status).to eq 'unverified_key' # InvalidGpgSignatureUpdater is called by the after_update hook - user.update_attributes!(email: GpgHelpers::User1.emails.first) + user.update!(email: GpgHelpers::User1.emails.first) expect(invalid_gpg_signature.reload).to have_attributes( project: project, @@ -166,7 +166,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do ) # InvalidGpgSignatureUpdater is called by the after_update hook - user.update_attributes!(email: 'still.unrelated@example.com') + user.update!(email: 'still.unrelated@example.com') expect(invalid_gpg_signature.reload).to have_attributes( project: project, diff --git a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb new file mode 100644 index 00000000000..95bf7685ade --- /dev/null +++ b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +describe Gitlab::Graphql::Authorize::AuthorizeResource do + let(:fake_class) do + Class.new do + include Gitlab::Graphql::Authorize::AuthorizeResource + + attr_reader :user, :found_object + + authorize :read_the_thing + + def initialize(user, found_object) + @user, @found_object = user, found_object + end + + def find_object + found_object + end + + def current_user + user + end + end + end + + let(:user) { build(:user) } + let(:project) { build(:project) } + subject(:loading_resource) { fake_class.new(user, project) } + + context 'when the user is allowed to perform the action' do + before do + allow(Ability).to receive(:allowed?).with(user, :read_the_thing, project, scope: :user) do + true + end + end + + describe '#authorized_find' do + it 'returns the object' do + expect(loading_resource.authorized_find).to eq(project) + end + end + + describe '#authorized_find!' do + it 'returns the object' do + expect(loading_resource.authorized_find!).to eq(project) + end + end + + describe '#authorize!' do + it 'does not raise an error' do + expect { loading_resource.authorize!(project) }.not_to raise_error + end + end + + describe '#authorized?' do + it 'is true' do + expect(loading_resource.authorized?(project)).to be(true) + end + end + end + + context 'when the user is not allowed to perform the action' do + before do + allow(Ability).to receive(:allowed?).with(user, :read_the_thing, project, scope: :user) do + false + end + end + + describe '#authorized_find' do + it 'returns `nil`' do + expect(loading_resource.authorized_find).to be_nil + end + end + + describe '#authorized_find!' do + it 'raises an error' do + expect { loading_resource.authorize!(project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + describe '#authorize!' do + it 'does not raise an error' do + expect { loading_resource.authorize!(project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + describe '#authorized?' do + it 'is false' do + expect(loading_resource.authorized?(project)).to be(false) + end + end + end + + context 'when the class does not define #find_object' do + let(:fake_class) do + Class.new { include Gitlab::Graphql::Authorize::AuthorizeResource } + end + + it 'raises a comprehensive error message' do + expect { fake_class.new.find_object }.to raise_error(/Implement #find_object in #{fake_class.name}/) + end + end +end diff --git a/spec/lib/gitlab/graphql/authorize_spec.rb b/spec/lib/gitlab/graphql/authorize_spec.rb new file mode 100644 index 00000000000..9c17a3b0e4b --- /dev/null +++ b/spec/lib/gitlab/graphql/authorize_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Gitlab::Graphql::Authorize do + describe '#authorize' do + it 'adds permissions from subclasses to those of superclasses when used on classes' do + base_class = Class.new do + extend Gitlab::Graphql::Authorize + + authorize :base_authorization + end + sub_class = Class.new(base_class) do + authorize :sub_authorization + end + + expect(base_class.required_permissions).to contain_exactly(:base_authorization) + expect(sub_class.required_permissions) + .to contain_exactly(:base_authorization, :sub_authorization) + end + end +end diff --git a/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb new file mode 100644 index 00000000000..96615ae80de --- /dev/null +++ b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' + +describe Gitlab::Graphql::Connections::KeysetConnection do + let(:nodes) { Project.all.order(id: :asc) } + let(:arguments) { {} } + subject(:connection) do + described_class.new(nodes, arguments, max_page_size: 3) + end + + def encoded_property(value) + Base64.strict_encode64(value.to_s) + end + + describe '#cursor_from_nodes' do + let(:project) { create(:project) } + + it 'returns an encoded ID' do + expect(connection.cursor_from_node(project)) + .to eq(encoded_property(project.id)) + end + + context 'when an order was specified' do + let(:nodes) { Project.order(:updated_at) } + + it 'returns the encoded value of the order' do + expect(connection.cursor_from_node(project)) + .to eq(encoded_property(project.updated_at)) + end + end + end + + describe '#sliced_nodes' do + let(:projects) { create_list(:project, 4) } + + context 'when before is passed' do + let(:arguments) { { before: encoded_property(projects[1].id) } } + + it 'only returns the project before the selected one' do + expect(subject.sliced_nodes).to contain_exactly(projects.first) + end + + context 'when the sort order is descending' do + let(:nodes) { Project.all.order(id: :desc) } + + it 'returns the correct nodes' do + expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1]) + end + end + end + + context 'when after is passed' do + let(:arguments) { { after: encoded_property(projects[1].id) } } + + it 'only returns the project before the selected one' do + expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1]) + end + + context 'when the sort order is descending' do + let(:nodes) { Project.all.order(id: :desc) } + + it 'returns the correct nodes' do + expect(subject.sliced_nodes).to contain_exactly(projects.first) + end + end + end + + context 'when both before and after are passed' do + let(:arguments) do + { + after: encoded_property(projects[1].id), + before: encoded_property(projects[3].id) + } + end + + it 'returns the expected set' do + expect(subject.sliced_nodes).to contain_exactly(projects[2]) + end + end + end + + describe '#paged_nodes' do + let!(:projects) { create_list(:project, 5) } + + it 'returns the collection limited to max page size' do + expect(subject.paged_nodes.size).to eq(3) + end + + context 'when `first` is passed' do + let(:arguments) { { first: 2 } } + + it 'returns only the first elements' do + expect(subject.paged_nodes).to contain_exactly(projects.first, projects.second) + end + end + + context 'when `last` is passed' do + let(:arguments) { { last: 2 } } + + it 'returns only the last elements' do + expect(subject.paged_nodes).to contain_exactly(projects[3], projects[4]) + end + end + + context 'when both are passed' do + let(:arguments) { { first: 2, last: 2 } } + + it 'raises an error' do + expect { subject.paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end + end + end +end diff --git a/spec/lib/gitlab/graphs/commits_spec.rb b/spec/lib/gitlab/graphs/commits_spec.rb index b2084f56640..530d4a981bf 100644 --- a/spec/lib/gitlab/graphs/commits_spec.rb +++ b/spec/lib/gitlab/graphs/commits_spec.rb @@ -29,7 +29,7 @@ describe Gitlab::Graphs::Commits do context 'with commits from yesterday and today' do subject { described_class.new([commit2, commit1_yesterday]) } describe '#commit_per_day' do - it { expect(subject.commit_per_day).to eq 1 } + it { expect(subject.commit_per_day).to eq 1.0 } end describe '#duration' do diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb index 813ae43b4d3..7eac2cacb90 100644 --- a/spec/lib/gitlab/hashed_storage/migrator_spec.rb +++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb @@ -65,7 +65,7 @@ describe Gitlab::HashedStorage::Migrator do end it 'migrate project' do - Sidekiq::Testing.inline! do + perform_enqueued_jobs do subject.migrate(project) end diff --git a/spec/lib/gitlab/hook_data/base_builder_spec.rb b/spec/lib/gitlab/hook_data/base_builder_spec.rb new file mode 100644 index 00000000000..a921dd766c3 --- /dev/null +++ b/spec/lib/gitlab/hook_data/base_builder_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe Gitlab::HookData::BaseBuilder do + describe '#absolute_image_urls' do + let(:subclass) do + Class.new(described_class) do + public :absolute_image_urls + end + end + + subject { subclass.new(nil) } + + using RSpec::Parameterized::TableSyntax + + where do + { + 'relative image URL' => { + input: '![an image](foo.png)', + output: "![an image](#{Gitlab.config.gitlab.url}/foo.png)" + }, + 'HTTP URL' => { + input: '![an image](http://example.com/foo.png)', + output: '![an image](http://example.com/foo.png)' + }, + 'HTTPS URL' => { + input: '![an image](https://example.com/foo.png)', + output: '![an image](https://example.com/foo.png)' + }, + 'protocol-relative URL' => { + input: '![an image](//example.com/foo.png)', + output: '![an image](//example.com/foo.png)' + }, + 'URL reference by title' => { + input: "![foo]\n\n[foo]: foo.png", + output: "![foo]\n\n[foo]: foo.png" + }, + 'URL reference by label' => { + input: "![][foo]\n\n[foo]: foo.png", + output: "![][foo]\n\n[foo]: foo.png" + }, + 'in Markdown inline code block' => { + input: '`![an image](foo.png)`', + output: "`![an image](#{Gitlab.config.gitlab.url}/foo.png)`" + }, + 'in HTML tag on the same line' => { + input: '<p>![an image](foo.png)</p>', + output: "<p>![an image](#{Gitlab.config.gitlab.url}/foo.png)</p>" + }, + 'in Markdown multi-line code block' => { + input: "```\n![an image](foo.png)\n```", + output: "```\n![an image](foo.png)\n```" + }, + 'in HTML tag on different lines' => { + input: "<p>\n![an image](foo.png)\n</p>", + output: "<p>\n![an image](foo.png)\n</p>" + } + } + end + + with_them do + it { expect(subject.absolute_image_urls(input)).to eq(output) } + end + end +end diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb index 506b2c0be20..60093474f8a 100644 --- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb @@ -40,5 +40,14 @@ describe Gitlab::HookData::IssueBuilder do expect(data).to include(:human_total_time_spent) expect(data).to include(:assignee_ids) end + + context 'when the issue has an image in the description' do + let(:issue_with_description) { create(:issue, description: 'test![Issue_Image](/uploads/abc/Issue_Image.png)') } + let(:builder) { described_class.new(issue_with_description) } + + it 'sets the image to use an absolute URL' do + expect(data[:description]).to eq("test![Issue_Image](#{Settings.gitlab.url}/uploads/abc/Issue_Image.png)") + end + end end end diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index b61614e4790..dd586af6118 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -56,5 +56,14 @@ describe Gitlab::HookData::MergeRequestBuilder do expect(data).to include(:human_time_estimate) expect(data).to include(:human_total_time_spent) end + + context 'when the MR has an image in the description' do + let(:mr_with_description) { create(:merge_request, description: 'test![Issue_Image](/uploads/abc/Issue_Image.png)') } + let(:builder) { described_class.new(mr_with_description) } + + it 'sets the image to use an absolute URL' do + expect(data[:description]).to eq("test![Issue_Image](#{Settings.gitlab.url}/uploads/abc/Issue_Image.png)") + end + end end end diff --git a/spec/lib/gitlab/ci/trace/http_io_spec.rb b/spec/lib/gitlab/http_io_spec.rb index 5474e2f518c..788bddb8f59 100644 --- a/spec/lib/gitlab/ci/trace/http_io_spec.rb +++ b/spec/lib/gitlab/http_io_spec.rb @@ -1,11 +1,14 @@ require 'spec_helper' -describe Gitlab::Ci::Trace::HttpIO do +describe Gitlab::HttpIO do include HttpIOHelpers let(:http_io) { described_class.new(url, size) } - let(:url) { remote_trace_url } - let(:size) { remote_trace_size } + + let(:url) { 'http://object-storage/trace' } + let(:file_path) { expand_fixture_path('trace/sample_trace') } + let(:file_body) { File.read(file_path).force_encoding(Encoding::BINARY) } + let(:size) { File.size(file_path) } describe '#close' do subject { http_io.close } @@ -86,10 +89,10 @@ describe Gitlab::Ci::Trace::HttpIO do describe '#each_line' do subject { http_io.each_line } - let(:string_io) { StringIO.new(remote_trace_body) } + let(:string_io) { StringIO.new(file_body) } before do - stub_remote_trace_206 + stub_remote_url_206(url, file_path) end it 'yields lines' do @@ -99,7 +102,7 @@ describe Gitlab::Ci::Trace::HttpIO do context 'when buckets on GCS' do context 'when BUFFER_SIZE is larger than file size' do before do - stub_remote_trace_200 + stub_remote_url_200(url, file_path) set_larger_buffer_size_than(size) end @@ -117,7 +120,7 @@ describe Gitlab::Ci::Trace::HttpIO do context 'when there are no network issue' do before do - stub_remote_trace_206 + stub_remote_url_206(url, file_path) end context 'when read whole size' do @@ -129,7 +132,7 @@ describe Gitlab::Ci::Trace::HttpIO do end it 'reads a trace' do - is_expected.to eq(remote_trace_body) + is_expected.to eq(file_body) end end @@ -139,7 +142,7 @@ describe Gitlab::Ci::Trace::HttpIO do end it 'reads a trace' do - is_expected.to eq(remote_trace_body) + is_expected.to eq(file_body) end end end @@ -153,7 +156,7 @@ describe Gitlab::Ci::Trace::HttpIO do end it 'reads a trace' do - is_expected.to eq(remote_trace_body[0, length]) + is_expected.to eq(file_body[0, length]) end end @@ -163,7 +166,7 @@ describe Gitlab::Ci::Trace::HttpIO do end it 'reads a trace' do - is_expected.to eq(remote_trace_body[0, length]) + is_expected.to eq(file_body[0, length]) end end end @@ -177,7 +180,7 @@ describe Gitlab::Ci::Trace::HttpIO do end it 'reads a trace' do - is_expected.to eq(remote_trace_body) + is_expected.to eq(file_body) end end @@ -187,7 +190,7 @@ describe Gitlab::Ci::Trace::HttpIO do end it 'reads a trace' do - is_expected.to eq(remote_trace_body) + is_expected.to eq(file_body) end end end @@ -221,11 +224,11 @@ describe Gitlab::Ci::Trace::HttpIO do let(:length) { nil } before do - stub_remote_trace_500 + stub_remote_url_500(url) end it 'reads a trace' do - expect { subject }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError) + expect { subject }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError) end end end @@ -233,15 +236,15 @@ describe Gitlab::Ci::Trace::HttpIO do describe '#readline' do subject { http_io.readline } - let(:string_io) { StringIO.new(remote_trace_body) } + let(:string_io) { StringIO.new(file_body) } before do - stub_remote_trace_206 + stub_remote_url_206(url, file_path) end shared_examples 'all line matching' do it 'reads a line' do - (0...remote_trace_body.lines.count).each do + (0...file_body.lines.count).each do expect(http_io.readline).to eq(string_io.readline) end end @@ -251,11 +254,11 @@ describe Gitlab::Ci::Trace::HttpIO do let(:length) { nil } before do - stub_remote_trace_500 + stub_remote_url_500(url) end it 'reads a trace' do - expect { subject }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError) + expect { subject }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError) end end diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb new file mode 100644 index 00000000000..5059d68e54b --- /dev/null +++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do + let!(:service) { described_class.new } + let!(:project) { create(:project, :with_object_export) } + let(:shared) { project.import_export_shared } + let!(:user) { create(:user) } + + describe '#execute' do + before do + allow(service).to receive(:strategy_execute) + stub_feature_flags(import_export_object_storage: true) + end + + it 'returns if project exported file is not found' do + allow(project).to receive(:export_project_object_exists?).and_return(false) + + expect(service).not_to receive(:strategy_execute) + + service.execute(user, project) + end + + it 'creates a lock file in the export dir' do + allow(service).to receive(:delete_after_export_lock) + + service.execute(user, project) + + expect(lock_path_exist?).to be_truthy + end + + context 'when the method succeeds' do + it 'removes the lock file' do + service.execute(user, project) + + expect(lock_path_exist?).to be_falsey + end + end + + context 'when the method fails' do + before do + allow(service).to receive(:strategy_execute).and_call_original + end + + context 'when validation fails' do + before do + allow(service).to receive(:invalid?).and_return(true) + end + + it 'does not create the lock file' do + expect(service).not_to receive(:create_or_update_after_export_lock) + + service.execute(user, project) + end + + it 'does not execute main logic' do + expect(service).not_to receive(:strategy_execute) + + service.execute(user, project) + end + + it 'logs validation errors in shared context' do + expect(service).to receive(:log_validation_errors) + + service.execute(user, project) + end + end + + context 'when an exception is raised' do + it 'removes the lock' do + expect { service.execute(user, project) }.to raise_error(NotImplementedError) + + expect(lock_path_exist?).to be_falsey + end + end + end + end + + describe '#log_validation_errors' do + it 'add the message to the shared context' do + errors = %w(test_message test_message2) + + allow(service).to receive(:invalid?).and_return(true) + allow(service.errors).to receive(:full_messages).and_return(errors) + + expect(shared).to receive(:add_error_message).twice.and_call_original + + service.execute(user, project) + + expect(shared.errors).to eq errors + end + end + + describe '#to_json' do + it 'adds the current strategy class to the serialized attributes' do + params = { param1: 1 } + result = params.merge(klass: described_class.to_s).to_json + + expect(described_class.new(params).to_json).to eq result + end + end + + def lock_path_exist? + File.exist?(described_class.lock_file_path(project)) + end +end diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb index ed54d87de4a..566b7f46c87 100644 --- a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb +++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb @@ -9,6 +9,7 @@ describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do describe '#execute' do before do allow(service).to receive(:strategy_execute) + stub_feature_flags(import_export_object_storage: false) end it 'returns if project exported file is not found' do diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb index 5fe57d9987b..7f2e0a4ee2c 100644 --- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb +++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb @@ -24,13 +24,34 @@ describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do end describe '#execute' do - it 'removes the exported project file after the upload' do - allow(strategy).to receive(:send_file) - allow(strategy).to receive(:handle_response_error) + context 'without object storage' do + before do + stub_feature_flags(import_export_object_storage: false) + end + + it 'removes the exported project file after the upload' do + allow(strategy).to receive(:send_file) + allow(strategy).to receive(:handle_response_error) + + expect(project).to receive(:remove_exported_project_file) + + strategy.execute(user, project) + end + end + + context 'with object storage' do + before do + stub_feature_flags(import_export_object_storage: true) + end - expect(project).to receive(:remove_exported_project_file) + it 'removes the exported project file after the upload' do + allow(strategy).to receive(:send_file) + allow(strategy).to receive(:handle_response_error) - strategy.execute(user, project) + expect(project).to receive(:remove_exported_project_file) + + strategy.execute(user, project) + end end end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 2ea66479c1b..e9a1932407d 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -7,6 +7,7 @@ issues: - updated_by - milestone - notes +- resource_label_events - label_links - labels - last_edited_by @@ -76,6 +77,7 @@ merge_requests: - updated_by - milestone - notes +- resource_label_events - label_links - labels - last_edited_by @@ -87,6 +89,7 @@ merge_requests: - merge_request_diff - events - merge_requests_closing_issues +- cached_closes_issues - metrics - timelogs - head_pipeline @@ -211,6 +214,7 @@ project: - slack_service - microsoft_teams_service - mattermost_service +- hangouts_chat_service - buildkite_service - bamboo_service - teamcity_service @@ -293,6 +297,8 @@ project: - deploy_tokens - settings - ci_cd_settings +- import_export_upload +- repository_languages award_emoji: - awardable - user diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb index 2223f163177..90e6d653d34 100644 --- a/spec/lib/gitlab/import_export/avatar_saver_spec.rb +++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb @@ -9,6 +9,7 @@ describe Gitlab::ImportExport::AvatarSaver do before do FileUtils.mkdir_p("#{shared.export_path}/avatar/") allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + stub_feature_flags(import_export_object_storage: false) end after do diff --git a/spec/lib/gitlab/import_export/file_importer_object_storage_spec.rb b/spec/lib/gitlab/import_export/file_importer_object_storage_spec.rb new file mode 100644 index 00000000000..287745eb40e --- /dev/null +++ b/spec/lib/gitlab/import_export/file_importer_object_storage_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::FileImporter do + let(:shared) { Gitlab::ImportExport::Shared.new(nil) } + let(:storage_path) { "#{Dir.tmpdir}/file_importer_spec" } + let(:valid_file) { "#{shared.export_path}/valid.json" } + let(:symlink_file) { "#{shared.export_path}/invalid.json" } + let(:hidden_symlink_file) { "#{shared.export_path}/.hidden" } + let(:subfolder_symlink_file) { "#{shared.export_path}/subfolder/invalid.json" } + let(:evil_symlink_file) { "#{shared.export_path}/.\nevil" } + + before do + stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0) + stub_feature_flags(import_export_object_storage: true) + stub_uploads_object_storage(FileUploader) + + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(storage_path) + allow_any_instance_of(Gitlab::ImportExport::CommandLineUtil).to receive(:untar_zxf).and_return(true) + allow_any_instance_of(Gitlab::ImportExport::Shared).to receive(:relative_archive_path).and_return('test') + allow(SecureRandom).to receive(:hex).and_return('abcd') + setup_files + end + + after do + FileUtils.rm_rf(storage_path) + end + + context 'normal run' do + before do + described_class.import(project: build(:project), archive_file: '', shared: shared) + end + + it 'removes symlinks in root folder' do + expect(File.exist?(symlink_file)).to be false + end + + it 'removes hidden symlinks in root folder' do + expect(File.exist?(hidden_symlink_file)).to be false + end + + it 'removes evil symlinks in root folder' do + expect(File.exist?(evil_symlink_file)).to be false + end + + it 'removes symlinks in subfolders' do + expect(File.exist?(subfolder_symlink_file)).to be false + end + + it 'does not remove a valid file' do + expect(File.exist?(valid_file)).to be true + end + + it 'creates the file in the right subfolder' do + expect(shared.export_path).to include('test/abcd') + end + end + + context 'error' do + before do + allow_any_instance_of(described_class).to receive(:wait_for_archived_file).and_raise(StandardError) + described_class.import(project: build(:project), archive_file: '', shared: shared) + end + + it 'removes symlinks in root folder' do + expect(File.exist?(symlink_file)).to be false + end + + it 'removes hidden symlinks in root folder' do + expect(File.exist?(hidden_symlink_file)).to be false + end + + it 'removes symlinks in subfolders' do + expect(File.exist?(subfolder_symlink_file)).to be false + end + + it 'does not remove a valid file' do + expect(File.exist?(valid_file)).to be true + end + end + + def setup_files + FileUtils.mkdir_p("#{shared.export_path}/subfolder/") + FileUtils.touch(valid_file) + FileUtils.ln_s(valid_file, symlink_file) + FileUtils.ln_s(valid_file, subfolder_symlink_file) + FileUtils.ln_s(valid_file, hidden_symlink_file) + FileUtils.ln_s(valid_file, evil_symlink_file) + end +end diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb index 58b9fb06cc5..78fccdf1dfc 100644 --- a/spec/lib/gitlab/import_export/file_importer_spec.rb +++ b/spec/lib/gitlab/import_export/file_importer_spec.rb @@ -7,6 +7,7 @@ describe Gitlab::ImportExport::FileImporter do let(:symlink_file) { "#{shared.export_path}/invalid.json" } let(:hidden_symlink_file) { "#{shared.export_path}/.hidden" } let(:subfolder_symlink_file) { "#{shared.export_path}/subfolder/invalid.json" } + let(:evil_symlink_file) { "#{shared.export_path}/.\nevil" } before do stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0) @@ -23,7 +24,7 @@ describe Gitlab::ImportExport::FileImporter do context 'normal run' do before do - described_class.import(archive_file: '', shared: shared) + described_class.import(project: nil, archive_file: '', shared: shared) end it 'removes symlinks in root folder' do @@ -34,6 +35,10 @@ describe Gitlab::ImportExport::FileImporter do expect(File.exist?(hidden_symlink_file)).to be false end + it 'removes evil symlinks in root folder' do + expect(File.exist?(evil_symlink_file)).to be false + end + it 'removes symlinks in subfolders' do expect(File.exist?(subfolder_symlink_file)).to be false end @@ -50,7 +55,7 @@ describe Gitlab::ImportExport::FileImporter do context 'error' do before do allow_any_instance_of(described_class).to receive(:wait_for_archived_file).and_raise(StandardError) - described_class.import(archive_file: '', shared: shared) + described_class.import(project: nil, archive_file: '', shared: shared) end it 'removes symlinks in root folder' do @@ -75,5 +80,7 @@ describe Gitlab::ImportExport::FileImporter do FileUtils.touch(valid_file) FileUtils.ln_s(valid_file, symlink_file) FileUtils.ln_s(valid_file, subfolder_symlink_file) + FileUtils.ln_s(valid_file, hidden_symlink_file) + FileUtils.ln_s(valid_file, evil_symlink_file) end end diff --git a/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb new file mode 100644 index 00000000000..6a803c48b34 --- /dev/null +++ b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::GroupProjectObjectBuilder do + let(:project) do + create(:project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: create(:group)) + end + + context 'labels' do + it 'finds the right group label' do + group_label = create(:group_label, 'name': 'group label', 'group': project.group) + + expect(described_class.build(Label, + 'title' => 'group label', + 'project' => project, + 'group' => project.group)).to eq(group_label) + end + + it 'creates a new label' do + label = described_class.build(Label, + 'title' => 'group label', + 'project' => project, + 'group' => project.group) + + expect(label.persisted?).to be true + end + end + + context 'milestones' do + it 'finds the right group milestone' do + milestone = create(:milestone, 'name' => 'group milestone', 'group' => project.group) + + expect(described_class.build(Milestone, + 'title' => 'group milestone', + 'project' => project, + 'group' => project.group)).to eq(milestone) + end + + it 'creates a new milestone' do + milestone = described_class.build(Milestone, + 'title' => 'group milestone', + 'project' => project, + 'group' => project.group) + + expect(milestone.persisted?).to be true + end + end +end diff --git a/spec/lib/gitlab/import_export/importer_object_storage_spec.rb b/spec/lib/gitlab/import_export/importer_object_storage_spec.rb new file mode 100644 index 00000000000..24a994b3611 --- /dev/null +++ b/spec/lib/gitlab/import_export/importer_object_storage_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::Importer do + let(:user) { create(:user) } + let(:test_path) { "#{Dir.tmpdir}/importer_spec" } + let(:shared) { project.import_export_shared } + let(:project) { create(:project) } + let(:import_file) { fixture_file_upload('spec/features/projects/import_export/test_project_export.tar.gz') } + + subject(:importer) { described_class.new(project) } + + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path) + allow_any_instance_of(Gitlab::ImportExport::FileImporter).to receive(:remove_import_file) + stub_feature_flags(import_export_object_storage: true) + stub_uploads_object_storage(FileUploader) + + FileUtils.mkdir_p(shared.export_path) + ImportExportUpload.create(project: project, import_file: import_file) + end + + after do + FileUtils.rm_rf(test_path) + end + + describe '#execute' do + it 'succeeds' do + importer.execute + + expect(shared.errors).to be_empty + end + + it 'extracts the archive' do + expect(Gitlab::ImportExport::FileImporter).to receive(:import).and_call_original + + importer.execute + end + + it 'checks the version' do + expect(Gitlab::ImportExport::VersionChecker).to receive(:check!).and_call_original + + importer.execute + end + + context 'all restores are executed' do + [ + Gitlab::ImportExport::AvatarRestorer, + Gitlab::ImportExport::RepoRestorer, + Gitlab::ImportExport::WikiRestorer, + Gitlab::ImportExport::UploadsRestorer, + Gitlab::ImportExport::LfsRestorer, + Gitlab::ImportExport::StatisticsRestorer + ].each do |restorer| + it "calls the #{restorer}" do + fake_restorer = double(restorer.to_s) + + expect(fake_restorer).to receive(:restore).and_return(true).at_least(1) + expect(restorer).to receive(:new).and_return(fake_restorer).at_least(1) + + importer.execute + end + end + + it 'restores the ProjectTree' do + expect(Gitlab::ImportExport::ProjectTreeRestorer).to receive(:new).and_call_original + + importer.execute + end + + it 'removes the import file' do + expect(importer).to receive(:remove_import_file).and_call_original + + importer.execute + + expect(project.import_export_upload.import_file&.file).to be_nil + end + end + + context 'when project successfully restored' do + let!(:existing_project) { create(:project, namespace: user.namespace) } + let(:project) { create(:project, namespace: user.namespace, name: 'whatever', path: 'whatever') } + + before do + restorers = double(:restorers, all?: true) + + allow(subject).to receive(:import_file).and_return(true) + allow(subject).to receive(:check_version!).and_return(true) + allow(subject).to receive(:restorers).and_return(restorers) + allow(project).to receive(:import_data).and_return(double(data: { 'original_path' => existing_project.path })) + end + + context 'when import_data' do + context 'has original_path' do + it 'overwrites existing project' do + expect_any_instance_of(::Projects::OverwriteProjectService).to receive(:execute).with(existing_project) + + subject.execute + end + end + + context 'has not original_path' do + before do + allow(project).to receive(:import_data).and_return(double(data: {})) + end + + it 'does not call the overwrite service' do + expect_any_instance_of(::Projects::OverwriteProjectService).not_to receive(:execute).with(existing_project) + + subject.execute + end + end + end + end + end +end diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb index c074e61da26..f07946824c4 100644 --- a/spec/lib/gitlab/import_export/importer_spec.rb +++ b/spec/lib/gitlab/import_export/importer_spec.rb @@ -10,9 +10,10 @@ describe Gitlab::ImportExport::Importer do before do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path) + allow_any_instance_of(Gitlab::ImportExport::FileImporter).to receive(:remove_import_file) + FileUtils.mkdir_p(shared.export_path) FileUtils.cp(Rails.root.join('spec/features/projects/import_export/test_project_export.tar.gz'), test_path) - allow(subject).to receive(:remove_import_file) end after do @@ -69,7 +70,7 @@ describe Gitlab::ImportExport::Importer do let(:project) { create(:project, namespace: user.namespace, name: 'whatever', path: 'whatever') } before do - restorers = double + restorers = double(:restorers, all?: true) allow(subject).to receive(:import_file).and_return(true) allow(subject).to receive(:check_version!).and_return(true) diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index 246f009ad27..67e4c289906 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -111,7 +111,7 @@ describe Gitlab::ImportExport::MembersMapper do end it 'maps the project member if it already exists' do - project.add_master(user2) + project.add_maintainer(user2) expect(members_mapper.map[exported_user_id]).to eq(user2.id) end diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json index c13cf4a0507..ba2248073f5 100644 --- a/spec/lib/gitlab/import_export/project.light.json +++ b/spec/lib/gitlab/import_export/project.light.json @@ -7,7 +7,7 @@ "milestones": [ { "id": 1, - "title": "Project milestone", + "title": "A milestone", "project_id": 8, "description": "Project-level milestone", "due_date": null, @@ -66,8 +66,8 @@ "group_milestone_id": null, "milestone": { "id": 1, - "title": "Project milestone", - "project_id": 8, + "title": "A milestone", + "group_id": 8, "description": "Project-level milestone", "due_date": null, "created_at": "2016-06-14T15:02:04.415Z", @@ -86,7 +86,7 @@ "updated_at": "2017-08-15T18:37:40.795Z", "label": { "id": 6, - "title": "Another project label", + "title": "Another label", "color": "#A8D695", "project_id": null, "created_at": "2017-08-15T18:37:19.698Z", diff --git a/spec/lib/gitlab/import_export/project.milestone-iid.json b/spec/lib/gitlab/import_export/project.milestone-iid.json new file mode 100644 index 00000000000..b028147b5eb --- /dev/null +++ b/spec/lib/gitlab/import_export/project.milestone-iid.json @@ -0,0 +1,80 @@ +{ + "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", + "import_type": "gitlab_project", + "creator_id": 123, + "visibility_level": 10, + "archived": false, + "issues": [ + { + "id": 1, + "title": "Fugiat est minima quae maxime non similique.", + "assignee_id": null, + "project_id": 8, + "author_id": 1, + "created_at": "2017-07-07T18:13:01.138Z", + "updated_at": "2017-08-15T18:37:40.807Z", + "branch_name": null, + "description": "Quam totam fuga numquam in eveniet.", + "state": "opened", + "iid": 20, + "updated_by_id": 1, + "confidential": false, + "due_date": null, + "moved_to_id": null, + "lock_version": null, + "time_estimate": 0, + "closed_at": null, + "last_edited_at": null, + "last_edited_by_id": null, + "group_milestone_id": null, + "milestone": { + "id": 1, + "title": "Group-level milestone", + "description": "Group-level milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "group_id": 8 + } + }, + { + "id": 2, + "title": "est minima quae maxime non similique.", + "assignee_id": null, + "project_id": 8, + "author_id": 1, + "created_at": "2017-07-07T18:13:01.138Z", + "updated_at": "2017-08-15T18:37:40.807Z", + "branch_name": null, + "description": "Quam totam fuga numquam in eveniet.", + "state": "opened", + "iid": 21, + "updated_by_id": 1, + "confidential": false, + "due_date": null, + "moved_to_id": null, + "lock_version": null, + "time_estimate": 0, + "closed_at": null, + "last_edited_at": null, + "last_edited_by_id": null, + "group_milestone_id": null, + "milestone": { + "id": 2, + "title": "Another milestone", + "project_id": 8, + "description": "milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "group_id": null + } + } + ], + "snippets": [], + "hooks": [] +} diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 68ddc947e02..a88ac0a091e 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do @shared = @project.import_export_shared allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') - allow_any_instance_of(Repository).to receive(:fetch_ref).and_return(true) + allow_any_instance_of(Repository).to receive(:fetch_source_branch!).and_return(true) allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false) expect_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch).with('feature', 'DCBA') @@ -189,8 +189,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do @project.pipelines.zip([2, 2, 2, 2, 2]) .each do |(pipeline, expected_status_size)| - expect(pipeline.statuses.size).to eq(expected_status_size) - end + expect(pipeline.statuses.size).to eq(expected_status_size) + end end end @@ -246,13 +246,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(project.issues.size).to eq(results.fetch(:issues, 0)) end - it 'has issue with group label and project label' do - labels = project.issues.first.labels - - expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0)) - expect(labels.where(type: "ProjectLabel").where.not(group_id: nil).count).to eq(0) - end - it 'does not set params that are excluded from import_export settings' do expect(project.import_type).to be_nil expect(project.creator_id).not_to eq 123 @@ -268,12 +261,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do it 'has group milestone' do expect(project.group.milestones.size).to eq(results.fetch(:milestones, 0)) end - - it 'has issue with group label' do - labels = project.issues.first.labels - - expect(labels.where(type: "GroupLabel").count).to eq(results.fetch(:first_issue_labels, 0)) - end end context 'Light JSON' do @@ -360,13 +347,72 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do it_behaves_like 'restores project correctly', issues: 2, labels: 1, - milestones: 1, + milestones: 2, first_issue_labels: 1 it_behaves_like 'restores group correctly', - labels: 1, - milestones: 1, + labels: 0, + milestones: 0, first_issue_labels: 1 end + + context 'with existing group models' do + let!(:project) do + create(:project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: create(:group)) + end + + before do + project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json") + end + + it 'imports labels' do + create(:group_label, name: 'Another label', group: project.group) + + expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error) + + restored_project_json + + expect(project.labels.count).to eq(1) + end + + it 'imports milestones' do + create(:milestone, name: 'A milestone', group: project.group) + + expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error) + + restored_project_json + + expect(project.group.milestones.count).to eq(1) + expect(project.milestones.count).to eq(0) + end + end + + context 'with clashing milestones on IID' do + let!(:project) do + create(:project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: create(:group)) + end + + it 'preserves the project milestone IID' do + project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.milestone-iid.json") + + expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error) + + restored_project_json + + expect(project.milestones.count).to eq(2) + expect(Milestone.find_by_title('Another milestone').iid).to eq(1) + expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2) + end + end end end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 2b8a11ce8f9..fec8a2af9ab 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do let!(:project) { setup_project } before do - project.add_master(user) + project.add_maintainer(user) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) allow_any_instance_of(MergeRequest).to receive(:source_branch_sha).and_return('ABCD') allow_any_instance_of(MergeRequest).to receive(:target_branch_sha).and_return('DCBA') @@ -217,8 +217,8 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(member_emails).not_to include('group@member.com') end - it 'does not export group members as master' do - Group.first.add_master(user) + it 'does not export group members as maintainer' do + Group.first.add_maintainer(user) expect(member_emails).not_to include('group@member.com') end diff --git a/spec/lib/gitlab/import_export/repo_saver_spec.rb b/spec/lib/gitlab/import_export/repo_saver_spec.rb index 187ec8fcfa2..5a646b4aac8 100644 --- a/spec/lib/gitlab/import_export/repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/repo_saver_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::ImportExport::RepoSaver do let(:bundler) { described_class.new(project: project, shared: shared) } before do - project.add_master(user) + project.add_maintainer(user) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) end diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb new file mode 100644 index 00000000000..02f1a4b81aa --- /dev/null +++ b/spec/lib/gitlab/import_export/saver_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' +require 'fileutils' + +describe Gitlab::ImportExport::Saver do + let!(:project) { create(:project, :public, name: 'project') } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let(:shared) { project.import_export_shared } + subject { described_class.new(project: project, shared: shared) } + + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + + FileUtils.mkdir_p(shared.export_path) + FileUtils.touch("#{shared.export_path}/tmp.bundle") + end + + after do + FileUtils.rm_rf(export_path) + end + + context 'local archive' do + it 'saves the repo to disk' do + stub_feature_flags(import_export_object_storage: false) + + subject.save + + expect(shared.errors).to be_empty + expect(Dir.empty?(shared.archive_path)).to be false + end + end + + context 'object storage' do + it 'saves the repo using object storage' do + stub_feature_flags(import_export_object_storage: true) + stub_uploads_object_storage(ImportExportUploader) + + subject.save + + expect(ImportExportUpload.find_by(project: project).export_file.url) + .to match(%r[\/uploads\/-\/system\/import_export_upload\/export_file.*]) + end + end +end diff --git a/spec/lib/gitlab/import_export/uploads_manager_spec.rb b/spec/lib/gitlab/import_export/uploads_manager_spec.rb new file mode 100644 index 00000000000..9c3870a0af8 --- /dev/null +++ b/spec/lib/gitlab/import_export/uploads_manager_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::UploadsManager do + let(:shared) { project.import_export_shared } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let(:project) { create(:project) } + let(:exported_file_path) { "#{shared.export_path}/uploads/#{upload.secret}/#{File.basename(upload.path)}" } + + subject(:manager) { described_class.new(project: project, shared: shared) } + + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + FileUtils.mkdir_p(shared.export_path) + end + + after do + FileUtils.rm_rf(shared.export_path) + end + + describe '#save' do + context 'when the project has uploads locally stored' do + let(:upload) { create(:upload, :issuable_upload, :with_file, model: project) } + + before do + project.uploads << upload + end + + it 'does not cause errors' do + manager.save + + expect(shared.errors).to be_empty + end + + it 'copies the file in the correct location when there is an upload' do + manager.save + + expect(File).to exist(exported_file_path) + end + end + + context 'using object storage' do + let!(:upload) { create(:upload, :issuable_upload, :object_storage, model: project) } + + before do + stub_feature_flags(import_export_object_storage: true) + stub_uploads_object_storage(FileUploader) + end + + it 'saves the file' do + fake_uri = double + + expect(fake_uri).to receive(:open).and_return(StringIO.new('File content')) + expect(URI).to receive(:parse).and_return(fake_uri) + + manager.save + + expect(File.read(exported_file_path)).to eq('File content') + end + end + + describe '#restore' do + context 'using object storage' do + before do + stub_feature_flags(import_export_object_storage: true) + stub_uploads_object_storage(FileUploader) + + FileUtils.mkdir_p(File.join(shared.export_path, 'uploads/72a497a02fe3ee09edae2ed06d390038')) + FileUtils.touch(File.join(shared.export_path, 'uploads/72a497a02fe3ee09edae2ed06d390038', "dummy.txt")) + end + + it 'restores the file' do + manager.restore + + expect(project.uploads.size).to eq(1) + expect(project.uploads.first.build_uploader.filename).to eq('dummy.txt') + end + end + end + end +end diff --git a/spec/lib/gitlab/import_export/uploads_saver_spec.rb b/spec/lib/gitlab/import_export/uploads_saver_spec.rb index 095687fa89d..c716edd9397 100644 --- a/spec/lib/gitlab/import_export/uploads_saver_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_saver_spec.rb @@ -7,6 +7,7 @@ describe Gitlab::ImportExport::UploadsSaver do let(:shared) { project.import_export_shared } before do + stub_feature_flags(import_export_object_storage: false) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) end @@ -30,7 +31,7 @@ describe Gitlab::ImportExport::UploadsSaver do it 'copies the uploads to the export path' do saver.save - uploads = Dir.glob(File.join(saver.uploads_export_path, '**/*')).map { |file| File.basename(file) } + uploads = Dir.glob(File.join(shared.export_path, 'uploads/**/*')).map { |file| File.basename(file) } expect(uploads).to include('banana_sample.gif') end @@ -52,7 +53,7 @@ describe Gitlab::ImportExport::UploadsSaver do it 'copies the uploads to the export path' do saver.save - uploads = Dir.glob(File.join(saver.uploads_export_path, '**/*')).map { |file| File.basename(file) } + uploads = Dir.glob(File.join(shared.export_path, 'uploads/**/*')).map { |file| File.basename(file) } expect(uploads).to include('banana_sample.gif') end diff --git a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb index 24bc231d5a0..441aa1defe6 100644 --- a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb @@ -10,7 +10,7 @@ describe Gitlab::ImportExport::WikiRepoSaver do let!(:project_wiki) { ProjectWiki.new(project, user) } before do - project.add_master(user) + project.add_maintainer(user) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) project_wiki.wiki project_wiki.create_page("index", "test content") diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb index 10341486512..94abf9679c4 100644 --- a/spec/lib/gitlab/import_sources_spec.rb +++ b/spec/lib/gitlab/import_sources_spec.rb @@ -5,14 +5,16 @@ describe Gitlab::ImportSources 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' + 'GitHub' => 'github', + 'Bitbucket Cloud' => 'bitbucket', + 'Bitbucket Server' => 'bitbucket_server', + 'GitLab.com' => 'gitlab', + 'Google Code' => 'google_code', + 'FogBugz' => 'fogbugz', + 'Repo by URL' => 'git', + 'GitLab export' => 'gitlab_project', + 'Gitea' => 'gitea', + 'Manifest file' => 'manifest' } expect(described_class.options).to eq(expected) @@ -25,12 +27,14 @@ describe Gitlab::ImportSources do %w( github bitbucket + bitbucket_server gitlab google_code fogbugz git gitlab_project gitea + manifest ) expect(described_class.values).to eq(expected) @@ -43,6 +47,7 @@ describe Gitlab::ImportSources do %w( github bitbucket + bitbucket_server gitlab google_code fogbugz @@ -58,12 +63,14 @@ describe Gitlab::ImportSources do import_sources = { 'github' => Gitlab::GithubImport::ParallelImporter, 'bitbucket' => Gitlab::BitbucketImport::Importer, + 'bitbucket_server' => Gitlab::BitbucketServerImport::Importer, 'gitlab' => Gitlab::GitlabImport::Importer, 'google_code' => Gitlab::GoogleCodeImport::Importer, 'fogbugz' => Gitlab::FogbugzImport::Importer, 'git' => nil, 'gitlab_project' => Gitlab::ImportExport::Importer, - 'gitea' => Gitlab::LegacyGithubImport::Importer + 'gitea' => Gitlab::LegacyGithubImport::Importer, + 'manifest' => nil } import_sources.each do |name, klass| @@ -76,13 +83,15 @@ describe Gitlab::ImportSources do describe '.title' do import_sources = { 'github' => 'GitHub', - 'bitbucket' => 'Bitbucket', + 'bitbucket' => 'Bitbucket Cloud', + 'bitbucket_server' => 'Bitbucket Server', 'gitlab' => 'GitLab.com', 'google_code' => 'Google Code', 'fogbugz' => 'FogBugz', 'git' => 'Repo by URL', 'gitlab_project' => 'GitLab export', - 'gitea' => 'Gitea' + 'gitea' => 'Gitea', + 'manifest' => 'Manifest file' } import_sources.each do |name, title| @@ -93,7 +102,7 @@ describe Gitlab::ImportSources do end describe 'imports_repository? checker' do - let(:allowed_importers) { %w[github gitlab_project] } + let(:allowed_importers) { %w[github gitlab_project bitbucket_server] } it 'fails if any importer other than the allowed ones implements this method' do current_importers = described_class.values.select { |kind| described_class.importer(kind).try(:imports_repository?) } diff --git a/spec/lib/gitlab/json_logger_spec.rb b/spec/lib/gitlab/json_logger_spec.rb new file mode 100644 index 00000000000..0a62785f880 --- /dev/null +++ b/spec/lib/gitlab/json_logger_spec.rb @@ -0,0 +1,29 @@ +# coding: utf-8 +require 'spec_helper' + +describe Gitlab::JsonLogger do + subject { described_class.new('/dev/null') } + + let(:now) { Time.now } + + describe '#format_message' do + it 'formats strings' do + output = subject.format_message('INFO', now, 'test', 'Hello world') + data = JSON.parse(output) + + expect(data['severity']).to eq('INFO') + expect(data['time']).to eq(now.utc.iso8601(3)) + expect(data['message']).to eq('Hello world') + end + + it 'formats hashes' do + output = subject.format_message('INFO', now, 'test', { hello: 1 }) + data = JSON.parse(output) + + expect(data['severity']).to eq('INFO') + expect(data['time']).to eq(now.utc.iso8601(3)) + expect(data['hello']).to eq(1) + expect(data['message']).to be_nil + end + end +end diff --git a/spec/lib/gitlab/kubernetes/config_map_spec.rb b/spec/lib/gitlab/kubernetes/config_map_spec.rb index 33dfa461202..fe65d03875f 100644 --- a/spec/lib/gitlab/kubernetes/config_map_spec.rb +++ b/spec/lib/gitlab/kubernetes/config_map_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::Kubernetes::ConfigMap do let(:kubeclient) { double('kubernetes client') } let(:application) { create(:clusters_applications_prometheus) } - let(:config_map) { described_class.new(application.name, application.values) } + let(:config_map) { described_class.new(application.name, application.files) } let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } let(:metadata) do @@ -15,11 +15,17 @@ describe Gitlab::Kubernetes::ConfigMap do end describe '#generate' do - let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: { values: application.values }) } + let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: application.files) } subject { config_map.generate } it 'should build a Kubeclient Resource' do is_expected.to eq(resource) end end + + describe '#config_map_name' do + it 'returns the config_map name' do + expect(config_map.config_map_name).to eq("values-content-configuration-#{application.name}") + end + end end diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index aa7e43dfb16..341f71a3e49 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -39,7 +39,7 @@ describe Gitlab::Kubernetes::Helm::Api do end context 'with a ConfigMap' do - let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application.name, application.values).generate } + let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application.name, application.files).generate } it 'creates a ConfigMap on kubeclient' do expect(client).to receive(:create_config_map).with(resource).once @@ -49,33 +49,33 @@ describe Gitlab::Kubernetes::Helm::Api do end end - describe '#installation_status' do + describe '#status' do let(:phase) { Gitlab::Kubernetes::Pod::RUNNING } let(:pod) { Kubeclient::Resource.new(status: { phase: phase }) } # partial representation it 'fetches POD phase from kubernetes cluster' do expect(client).to receive(:get_pod).with(command.pod_name, gitlab_namespace).once.and_return(pod) - expect(subject.installation_status(command.pod_name)).to eq(phase) + expect(subject.status(command.pod_name)).to eq(phase) end end - describe '#installation_log' do + describe '#log' do let(:log) { 'some output' } let(:response) { RestClient::Response.new(log) } it 'fetches POD phase from kubernetes cluster' do expect(client).to receive(:get_pod_log).with(command.pod_name, gitlab_namespace).once.and_return(response) - expect(subject.installation_log(command.pod_name)).to eq(log) + expect(subject.log(command.pod_name)).to eq(log) end end - describe '#delete_installation_pod!' do + describe '#delete_pod!' do it 'deletes the POD from kubernetes cluster' do expect(client).to receive(:delete_pod).with(command.pod_name, gitlab_namespace).once - subject.delete_installation_pod!(command.pod_name) + subject.delete_pod!(command.pod_name) end end end diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb index 7be8be54d5e..d50616e95e8 100644 --- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb @@ -2,7 +2,25 @@ require 'spec_helper' describe Gitlab::Kubernetes::Helm::BaseCommand do let(:application) { create(:clusters_applications_helm) } - let(:base_command) { described_class.new(application.name) } + let(:test_class) do + Class.new do + include Gitlab::Kubernetes::Helm::BaseCommand + + def name + "test-class-name" + end + + def files + { + some: 'value' + } + end + end + end + + let(:base_command) do + test_class.new + end subject { base_command } @@ -18,15 +36,9 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do end end - describe '#config_map?' do - subject { base_command.config_map? } - - it { is_expected.to be_falsy } - end - describe '#pod_name' do subject { base_command.pod_name } - it { is_expected.to eq('install-helm') } + it { is_expected.to eq('install-test-class-name') } end end diff --git a/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb b/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb new file mode 100644 index 00000000000..167bee22fc3 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::Kubernetes::Helm::Certificate do + describe '.generate_root' do + subject { described_class.generate_root } + + it 'should generate a root CA that expires a long way in the future' do + expect(subject.cert.not_after).to be > 999.years.from_now + end + end + + describe '#issue' do + subject { described_class.generate_root.issue } + + it 'should generate a cert that expires soon' do + expect(subject.cert.not_after).to be < 60.minutes.from_now + end + + context 'passing in INFINITE_EXPIRY' do + subject { described_class.generate_root.issue(expires_in: described_class::INFINITE_EXPIRY) } + + it 'should generate a cert that expires a long way in the future' do + expect(subject.cert.not_after).to be > 999.years.from_now + end + end + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb index 89e36a298f8..dcbc046cf00 100644 --- a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe Gitlab::Kubernetes::Helm::InitCommand do let(:application) { create(:clusters_applications_helm) } - let(:commands) { 'helm init >/dev/null' } + let(:commands) { 'helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem >/dev/null' } - subject { described_class.new(application.name) } + subject { described_class.new(name: application.name, files: {}) } it_behaves_like 'helm commands' end diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb index 25c6fa3b9a3..982e2f41043 100644 --- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -1,83 +1,82 @@ require 'rails_helper' describe Gitlab::Kubernetes::Helm::InstallCommand do - let(:application) { create(:clusters_applications_prometheus) } - let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } - let(:install_command) { application.install_command } + let(:files) { { 'ca.pem': 'some file content' } } + let(:repository) { 'https://repository.example.com' } + let(:version) { '1.2.3' } + + let(:install_command) do + described_class.new( + name: 'app-name', + chart: 'chart-name', + files: files, + version: version, repository: repository + ) + end subject { install_command } - context 'for ingress' do - let(:application) { create(:clusters_applications_ingress) } - - it_behaves_like 'helm commands' do - let(:commands) do - <<~EOS - helm init --client-only >/dev/null - helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null - EOS - end + it_behaves_like 'helm commands' do + let(:commands) do + <<~EOS + helm init --client-only >/dev/null + helm repo add app-name https://repository.example.com + helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null + EOS end end - context 'for prometheus' do - let(:application) { create(:clusters_applications_prometheus) } + context 'when there is no repository' do + let(:repository) { nil } it_behaves_like 'helm commands' do let(:commands) do <<~EOS helm init --client-only >/dev/null - helm install #{application.chart} --name #{application.name} --version #{application.version} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null + helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null EOS end end end - context 'for runner' do - let(:ci_runner) { create(:ci_runner) } - let(:application) { create(:clusters_applications_runner, runner: ci_runner) } + context 'when there is no ca.pem file' do + let(:files) { { 'file.txt': 'some content' } } it_behaves_like 'helm commands' do let(:commands) do <<~EOS helm init --client-only >/dev/null - helm repo add #{application.name} #{application.repository} - helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null + helm repo add app-name https://repository.example.com + helm install chart-name --name app-name --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null EOS end end end - context 'for jupyter' do - let(:application) { create(:clusters_applications_jupyter) } + context 'when there is no version' do + let(:version) { nil } it_behaves_like 'helm commands' do let(:commands) do <<~EOS helm init --client-only >/dev/null - helm repo add #{application.name} #{application.repository} - helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null + helm repo add app-name https://repository.example.com + helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null EOS end end end - describe '#config_map?' do - subject { install_command.config_map? } - - it { is_expected.to be_truthy } - end - describe '#config_map_resource' do let(:metadata) do { - name: "values-content-configuration-#{application.name}", - namespace: namespace, - labels: { name: "values-content-configuration-#{application.name}" } + name: "values-content-configuration-app-name", + namespace: 'gitlab-managed-apps', + labels: { name: "values-content-configuration-app-name" } } end - let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: { values: application.values }) } + let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) } subject { install_command.config_map_resource } diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index 43adc80d576..ec64193c0b2 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -2,14 +2,13 @@ require 'rails_helper' describe Gitlab::Kubernetes::Helm::Pod do describe '#generate' do - let(:cluster) { create(:cluster) } - let(:app) { create(:clusters_applications_prometheus, cluster: cluster) } + let(:app) { create(:clusters_applications_prometheus) } let(:command) { app.install_command } let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } subject { described_class.new(command, namespace) } - shared_examples 'helm pod' do + context 'with a command' do it 'should generate a Kubeclient::Resource' do expect(subject.generate).to be_a_kind_of(Kubeclient::Resource) end @@ -41,10 +40,6 @@ describe Gitlab::Kubernetes::Helm::Pod do spec = subject.generate.spec expect(spec.restartPolicy).to eq('Never') end - end - - context 'with a install command' do - it_behaves_like 'helm pod' it 'should include volumes for the container' do container = subject.generate.spec.containers.first @@ -60,24 +55,8 @@ describe Gitlab::Kubernetes::Helm::Pod do it 'should mount configMap specification in the volume' do volume = subject.generate.spec.volumes.first expect(volume.configMap['name']).to eq("values-content-configuration-#{app.name}") - expect(volume.configMap['items'].first['key']).to eq('values') - expect(volume.configMap['items'].first['path']).to eq('values.yaml') - end - end - - context 'with a init command' do - let(:app) { create(:clusters_applications_helm, cluster: cluster) } - - it_behaves_like 'helm pod' - - it 'should not include volumeMounts inside the container' do - container = subject.generate.spec.containers.first - expect(container.volumeMounts).to be_nil - end - - it 'should not a volume inside the specification' do - spec = subject.generate.spec - expect(spec.volumes).to be_nil + expect(volume.configMap['items'].first['key']).to eq(:'values.yaml') + expect(volume.configMap['items'].first['path']).to eq(:'values.yaml') end end end diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb index 34b33772578..5c03a2ce7d3 100644 --- a/spec/lib/gitlab/kubernetes_spec.rb +++ b/spec/lib/gitlab/kubernetes_spec.rb @@ -70,4 +70,19 @@ describe Gitlab::Kubernetes do it { is_expected.to eq(YAML.load_file(path)) } end end + + describe '#add_terminal_auth' do + it 'adds authentication parameters to a hash' do + terminal = { original: 'value' } + + add_terminal_auth(terminal, token: 'foo', max_session_time: 0, ca_pem: 'bar') + + expect(terminal).to eq( + original: 'value', + headers: { 'Authorization' => ['Bearer foo'] }, + max_session_time: 0, + ca_pem: 'bar' + ) + end + end end diff --git a/spec/lib/gitlab/language_detection_spec.rb b/spec/lib/gitlab/language_detection_spec.rb new file mode 100644 index 00000000000..9636fbd401b --- /dev/null +++ b/spec/lib/gitlab/language_detection_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Gitlab::LanguageDetection do + set(:project) { create(:project, :repository) } + set(:ruby) { create(:programming_language, name: 'Ruby') } + set(:haskell) { create(:programming_language, name: 'Haskell') } + let(:repository) { project.repository } + let(:detection) do + [{ value: 66.63, label: "Ruby", color: "#701516", highlight: "#701516" }, + { value: 12.96, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" }, + { value: 7.9, label: "Elixir", color: "#e34c26", highlight: "#e34c26" }, + { value: 2.51, label: "CoffeeScript", color: "#244776", highlight: "#244776" }, + { value: 1.51, label: "Go", color: "#2a4776", highlight: "#244776" }, + { value: 1.1, label: "MepmepLang", color: "#2a4776", highlight: "#244776" }] + end + let(:repository_languages) do + [RepositoryLanguage.new(share: 10, programming_language: ruby)] + end + + subject { described_class.new(repository, repository_languages) } + + before do + allow(repository).to receive(:languages).and_return(detection) + end + + describe '#languages' do + it 'returns the language names' do + expect(subject.languages).to eq(%w[Ruby JavaScript Elixir CoffeeScript Go]) + end + end + + describe '#insertions' do + let(:programming_languages) { [ruby, haskell] } + let(:detection) do + [{ value: 10, label: haskell.name, color: haskell.color }] + end + + it 'only includes new languages' do + insertions = subject.insertions(programming_languages) + + expect(insertions).not_to be_empty + expect(insertions.first[:project_id]).to be(project.id) + expect(insertions.first[:programming_language_id]).to be(haskell.id) + expect(insertions.first[:share]).to be(10) + end + end + + describe '#updates' do + it 'updates the share of languages' do + first_update = subject.updates.first + + expect(first_update).not_to be_nil + expect(first_update[:programming_language_id]).to eq(ruby.id) + expect(first_update[:share]).to eq(66.63) + end + + it 'does not include languages to be removed' do + ids = subject.updates.map { |h| h[:programming_language_id] } + + expect(ids).not_to include(haskell.id) + end + + context 'when silent writes occur' do + let(:repository_languages) do + [RepositoryLanguage.new(share: 66.63, programming_language: ruby)] + end + + it "doesn't include them in the result" do + expect(subject.updates).to be_empty + end + end + end + + describe '#deletions' do + let(:repository_languages) do + [RepositoryLanguage.new(share: 10, programming_language: ruby), + RepositoryLanguage.new(share: 5, programming_language: haskell)] + end + + it 'lists undetected languages' do + expect(subject.deletions).not_to be_empty + expect(subject.deletions).to include(haskell.id) + end + end +end diff --git a/spec/lib/gitlab/manifest_import/manifest_spec.rb b/spec/lib/gitlab/manifest_import/manifest_spec.rb new file mode 100644 index 00000000000..ab305fb2316 --- /dev/null +++ b/spec/lib/gitlab/manifest_import/manifest_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::ManifestImport::Manifest, :postgresql do + let(:file) { File.open(Rails.root.join('spec/fixtures/aosp_manifest.xml')) } + let(:manifest) { described_class.new(file) } + + describe '#valid?' do + context 'valid file' do + it { expect(manifest.valid?).to be true } + end + + context 'missing or invalid attributes' do + let(:file) { Tempfile.new('foo') } + + before do + content = <<~EOS + <manifest> + <remote review="invalid-url" /> + <project name="platform/build"/> + </manifest> + EOS + + file.write(content) + file.rewind + end + + it { expect(manifest.valid?).to be false } + + describe 'errors' do + before do + manifest.valid? + end + + it { expect(manifest.errors).to include('Make sure a <remote> tag is present and is valid.') } + it { expect(manifest.errors).to include('Make sure every <project> tag has name and path attributes.') } + end + end + end + + describe '#projects' do + it { expect(manifest.projects.size).to eq(660) } + it { expect(manifest.projects[0][:name]).to eq('platform/build') } + it { expect(manifest.projects[0][:path]).to eq('build/make') } + it { expect(manifest.projects[0][:url]).to eq('https://android-review.googlesource.com/platform/build') } + end +end diff --git a/spec/lib/gitlab/manifest_import/project_creator_spec.rb b/spec/lib/gitlab/manifest_import/project_creator_spec.rb new file mode 100644 index 00000000000..1d01d437535 --- /dev/null +++ b/spec/lib/gitlab/manifest_import/project_creator_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Gitlab::ManifestImport::ProjectCreator, :postgresql do + let(:group) { create(:group) } + let(:user) { create(:user) } + let(:repository) do + { + path: 'device/common', + url: 'https://android-review.googlesource.com/device/common' + } + end + + before do + group.add_owner(user) + end + + subject { described_class.new(repository, group, user) } + + describe '#execute' do + it { expect(subject.execute).to be_a(Project) } + it { expect { subject.execute }.to change { Project.count }.by(1) } + it { expect { subject.execute }.to change { Group.count }.by(1) } + + it 'creates project with valid full path and import url' do + subject.execute + + project = Project.last + + expect(project.full_path).to eq(File.join(group.path, 'device/common')) + expect(project.import_url).to eq('https://android-review.googlesource.com/device/common') + end + end +end diff --git a/spec/lib/gitlab/middleware/basic_health_check_spec.rb b/spec/lib/gitlab/middleware/basic_health_check_spec.rb new file mode 100644 index 00000000000..187d903a5e1 --- /dev/null +++ b/spec/lib/gitlab/middleware/basic_health_check_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Gitlab::Middleware::BasicHealthCheck do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + let(:env) { {} } + + describe '#call' do + context 'outside IP' do + before do + env['REMOTE_ADDR'] = '8.8.8.8' + end + + it 'returns a 404' do + env['PATH_INFO'] = described_class::HEALTH_PATH + + response = middleware.call(env) + + expect(response[0]).to eq(404) + end + + it 'forwards the call for other paths' do + env['PATH_INFO'] = '/' + + expect(app).to receive(:call) + + middleware.call(env) + end + end + + context 'whitelisted IP' do + before do + env['REMOTE_ADDR'] = '127.0.0.1' + end + + it 'returns 200 response when endpoint is hit' do + env['PATH_INFO'] = described_class::HEALTH_PATH + + expect(app).not_to receive(:call) + + response = middleware.call(env) + + expect(response[0]).to eq(200) + expect(response[1]).to eq({ 'Content-Type' => 'text/plain' }) + expect(response[2]).to eq(['GitLab OK']) + end + + it 'forwards the call for other paths' do + env['PATH_INFO'] = '/-/readiness' + + expect(app).to receive(:call) + + middleware.call(env) + end + end + end +end diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index b24c9882c0c..7a3a9ab875b 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -79,7 +79,7 @@ describe Gitlab::Middleware::Go do let(:current_user) { project.creator } before do - project.team.add_master(current_user) + project.team.add_maintainer(current_user) end shared_examples 'authenticated' do diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb index a2ba91dae80..f788f8ee276 100644 --- a/spec/lib/gitlab/middleware/multipart_spec.rb +++ b/spec/lib/gitlab/middleware/multipart_spec.rb @@ -7,18 +7,47 @@ describe Gitlab::Middleware::Multipart do let(:middleware) { described_class.new(app) } let(:original_filename) { 'filename' } - it 'opens top-level files' do - Tempfile.open('top-level') do |tempfile| - env = post_env({ 'file' => tempfile.path }, { 'file.name' => original_filename }, Gitlab::Workhorse.secret, 'gitlab-workhorse') + shared_examples_for 'multipart upload files' do + it 'opens top-level files' do + Tempfile.open('top-level') do |tempfile| + env = post_env({ 'file' => tempfile.path }, { 'file.name' => original_filename, 'file.path' => tempfile.path, 'file.remote_id' => remote_id }, Gitlab::Workhorse.secret, 'gitlab-workhorse') + expect_uploaded_file(tempfile, %w(file)) + + middleware.call(env) + end + end + + it 'opens files one level deep' do + Tempfile.open('one-level') do |tempfile| + in_params = { 'user' => { 'avatar' => { '.name' => original_filename, '.path' => tempfile.path, '.remote_id' => remote_id } } } + env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect_uploaded_file(tempfile, %w(user avatar)) + + middleware.call(env) + end + end + + it 'opens files two levels deep' do + Tempfile.open('two-levels') do |tempfile| + in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => original_filename, '.path' => tempfile.path, '.remote_id' => remote_id } } } } + env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect_uploaded_file(tempfile, %w(project milestone themesong)) + + middleware.call(env) + end + end + + def expect_uploaded_file(tempfile, path, remote: false) expect(app).to receive(:call) do |env| - file = Rack::Request.new(env).params['file'] + file = Rack::Request.new(env).params.dig(*path) expect(file).to be_a(::UploadedFile) expect(file.path).to eq(tempfile.path) expect(file.original_filename).to eq(original_filename) + expect(file.remote_id).to eq(remote_id) end - - middleware.call(env) end end @@ -34,32 +63,39 @@ describe Gitlab::Middleware::Multipart do 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' => original_filename } } } - env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + context 'with remote file' do + let(:remote_id) { 'someid' } - 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) - expect(file.original_filename).to eq(original_filename) - end + it_behaves_like 'multipart upload files' + end - middleware.call(env) - end + context 'with local file' do + let(:remote_id) { nil } + + it_behaves_like 'multipart upload files' end - it 'opens files two levels deep' do + it 'allows symlinks for uploads dir' do Tempfile.open('two-levels') do |tempfile| - in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => original_filename } } } } - env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + symlinked_dir = '/some/dir/uploads' + symlinked_path = File.join(symlinked_dir, File.basename(tempfile.path)) + env = post_env({ 'file' => symlinked_path }, { 'file.name' => original_filename, 'file.path' => symlinked_path }, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + allow(FileUploader).to receive(:root).and_return(symlinked_dir) + allow(UploadedFile).to receive(:allowed_paths).and_return([symlinked_dir, Gitlab.config.uploads.storage_path]) + allow(File).to receive(:realpath).and_call_original + allow(File).to receive(:realpath).with(symlinked_dir).and_return(Dir.tmpdir) + allow(File).to receive(:realpath).with(symlinked_path).and_return(tempfile.path) + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:exist?).with(symlinked_dir).and_return(true) + + # override Dir.tmpdir because this dir is in the list of allowed paths + # and it would match FileUploader.root path (which in this test is linked + # to /tmp too) + allow(Dir).to receive(:tmpdir).and_return(File.join(Dir.tmpdir, 'tmpsubdir')) 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) - expect(file.original_filename).to eq(original_filename) + expect(Rack::Request.new(env).params['file']).to be_a(::UploadedFile) end middleware.call(env) diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb index 39ec2f37a83..8fbeaa065fa 100644 --- a/spec/lib/gitlab/middleware/read_only_spec.rb +++ b/spec/lib/gitlab/middleware/read_only_spec.rb @@ -2,28 +2,7 @@ require 'spec_helper' describe Gitlab::Middleware::ReadOnly do include Rack::Test::Methods - - RSpec::Matchers.define :be_a_redirect do - match do |response| - response.status == 301 - end - end - - RSpec::Matchers.define :disallow_request do - match do |middleware| - alert = middleware.env['rack.session'].to_hash - .dig('flash', 'flashes', 'alert') - - alert&.include?('You cannot perform write operations') - end - end - - RSpec::Matchers.define :disallow_request_in_json do - match do |response| - json_response = JSON.parse(response.body) - response.body.include?('You cannot perform write operations') && json_response.key?('message') - end - end + using RSpec::Parameterized::TableSyntax let(:rack_stack) do rack = Rack::Builder.new do @@ -65,38 +44,38 @@ describe Gitlab::Middleware::ReadOnly do it 'expects PATCH requests to be disallowed' do response = request.patch('/test_request') - expect(response).to be_a_redirect + expect(response).to be_redirect expect(subject).to disallow_request end it 'expects PUT requests to be disallowed' do response = request.put('/test_request') - expect(response).to be_a_redirect + expect(response).to be_redirect expect(subject).to disallow_request end it 'expects POST requests to be disallowed' do response = request.post('/test_request') - expect(response).to be_a_redirect + expect(response).to be_redirect expect(subject).to disallow_request end it 'expects a internal POST request to be allowed after a disallowed request' do response = request.post('/test_request') - expect(response).to be_a_redirect + expect(response).to be_redirect response = request.post("/api/#{API::API.version}/internal") - expect(response).not_to be_a_redirect + expect(response).not_to be_redirect end it 'expects DELETE requests to be disallowed' do response = request.delete('/test_request') - expect(response).to be_a_redirect + expect(response).to be_redirect expect(subject).to disallow_request end @@ -104,7 +83,7 @@ describe Gitlab::Middleware::ReadOnly do expect(Rails.application.routes).to receive(:recognize_path).and_call_original response = request.post('/root/gitlab-ce/new/master/app/info/lfs/objects/batch') - expect(response).to be_a_redirect + expect(response).to be_redirect expect(subject).to disallow_request end @@ -117,39 +96,41 @@ describe Gitlab::Middleware::ReadOnly do context 'whitelisted requests' do it 'expects a POST internal request to be allowed' do expect(Rails.application.routes).not_to receive(:recognize_path) - response = request.post("/api/#{API::API.version}/internal") - expect(response).not_to be_a_redirect + expect(response).not_to be_redirect expect(subject).not_to disallow_request end - it 'expects a POST LFS request to batch URL to be allowed' do - expect(Rails.application.routes).to receive(:recognize_path).and_call_original - response = request.post('/root/rouge.git/info/lfs/objects/batch') + it 'expects requests to sidekiq admin to be allowed' do + response = request.post('/admin/sidekiq') - expect(response).not_to be_a_redirect + expect(response).not_to be_redirect expect(subject).not_to disallow_request - end - it 'expects a POST request to git-upload-pack URL to be allowed' do - expect(Rails.application.routes).to receive(:recognize_path).and_call_original - response = request.post('/root/rouge.git/git-upload-pack') + response = request.get('/admin/sidekiq') - expect(response).not_to be_a_redirect + expect(response).not_to be_redirect expect(subject).not_to disallow_request end - it 'expects requests to sidekiq admin to be allowed' do - response = request.post('/admin/sidekiq') + where(:description, :path) do + 'LFS request to batch' | '/root/rouge.git/info/lfs/objects/batch' + 'LFS request to locks verify' | '/root/rouge.git/info/lfs/locks/verify' + 'LFS request to locks create' | '/root/rouge.git/info/lfs/locks' + 'LFS request to locks unlock' | '/root/rouge.git/info/lfs/locks/1/unlock' + 'request to git-upload-pack' | '/root/rouge.git/git-upload-pack' + 'request to git-receive-pack' | '/root/rouge.git/git-receive-pack' + end - expect(response).not_to be_a_redirect - expect(subject).not_to disallow_request + with_them do + it "expects a POST #{description} URL to be allowed" do + expect(Rails.application.routes).to receive(:recognize_path).and_call_original + response = request.post(path) - response = request.get('/admin/sidekiq') - - expect(response).not_to be_a_redirect - expect(subject).not_to disallow_request + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end end end end diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb index 1dbead16d5b..c1b84e9f077 100644 --- a/spec/lib/gitlab/popen_spec.rb +++ b/spec/lib/gitlab/popen_spec.rb @@ -55,6 +55,19 @@ describe Gitlab::Popen do end end + context 'with a process that writes a lot of data to stderr' do + let(:test_string) { 'The quick brown fox jumped over the lazy dog' } + # The pipe buffer is typically 64K. This string is about 440K. + let(:spew_command) { ['bash', '-c', "for i in {1..10000}; do echo '#{test_string}' 1>&2; done"] } + + it 'returns zero' do + output, status = @klass.new.popen(spew_command, path) + + expect(output).to include(test_string) + expect(status).to eq(0) + end + end + context 'without a directory argument' do before do @output, @status = @klass.new.popen(%w(ls)) diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb index f3cd6961e94..00c62c7bf96 100644 --- a/spec/lib/gitlab/project_authorizations_spec.rb +++ b/spec/lib/gitlab/project_authorizations_spec.rb @@ -41,7 +41,7 @@ describe Gitlab::ProjectAuthorizations do it 'includes the correct access levels' do mapping = map_access_levels(authorizations) - expect(mapping[owned_project.id]).to eq(Gitlab::Access::MASTER) + expect(mapping[owned_project.id]).to eq(Gitlab::Access::MAINTAINER) expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER) expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER) end @@ -62,11 +62,11 @@ describe Gitlab::ProjectAuthorizations do end it 'uses the greatest access level when a user is a member of a nested group' do - nested_group.add_master(user) + nested_group.add_maintainer(user) mapping = map_access_levels(authorizations) - expect(mapping[nested_project.id]).to eq(Gitlab::Access::MASTER) + expect(mapping[nested_project.id]).to eq(Gitlab::Access::MAINTAINER) end end end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 50224bde722..4a0dc3686ec 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -64,6 +64,49 @@ describe Gitlab::ProjectSearchResults do end end + shared_examples 'blob search repository ref' do |entity_type| + let(:query) { 'files' } + let(:file_finder) { double } + let(:project_branch) { 'project_branch' } + + subject(:results) { described_class.new(user, project, query, repository_ref).objects(blob_type) } + + before do + allow(entity).to receive(:default_branch).and_return(project_branch) + allow(file_finder).to receive(:find).and_return([]) + end + + context 'when repository_ref exists' do + let(:repository_ref) { 'ref_branch' } + + it 'uses it' do + expect(Gitlab::FileFinder).to receive(:new).with(project, repository_ref).and_return(file_finder) + + results + end + end + + context 'when repository_ref is not present' do + let(:repository_ref) { nil } + + it "uses #{entity_type} repository default reference" do + expect(Gitlab::FileFinder).to receive(:new).with(project, project_branch).and_return(file_finder) + + results + end + end + + context 'when repository_ref is blank' do + let(:repository_ref) { '' } + + it "uses #{entity_type} repository default reference" do + expect(Gitlab::FileFinder).to receive(:new).with(project, project_branch).and_return(file_finder) + + results + end + end + end + describe 'blob search' do let(:project) { create(:project, :public, :repository) } @@ -75,6 +118,11 @@ describe Gitlab::ProjectSearchResults do let(:expected_file_by_content) { 'CHANGELOG' } end + it_behaves_like 'blob search repository ref', 'project' do + let(:blob_type) { 'blobs' } + let(:entity) { project } + end + describe 'parsing results' do let(:results) { project.repository.search_files_by_content('feature', 'master') } let(:search_result) { results.first } @@ -212,6 +260,11 @@ describe Gitlab::ProjectSearchResults do let(:expected_file_by_name) { 'Files/Title.md' } let(:expected_file_by_content) { 'CHANGELOG.md' } end + + it_behaves_like 'blob search repository ref', 'wiki' do + let(:blob_type) { 'wiki_blobs' } + let(:entity) { project.wiki } + end end it 'does not list issues on private projects' do @@ -385,7 +438,7 @@ describe Gitlab::ProjectSearchResults do let!(:private_project) { create(:project, :private, :repository, creator: creator, namespace: creator.namespace) } let(:team_master) do user = create(:user, username: 'private-project-master') - private_project.add_master(user) + private_project.add_maintainer(user) user end let(:team_reporter) do diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb index 85971f2a7ef..5bd4d6c6a48 100644 --- a/spec/lib/gitlab/repository_cache_adapter_spec.rb +++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb @@ -67,10 +67,18 @@ describe Gitlab::RepositoryCacheAdapter do describe '#expire_method_caches' do it 'expires the caches of the given methods' do - expect(cache).to receive(:expire).with(:readme) + expect(cache).to receive(:expire).with(:rendered_readme) expect(cache).to receive(:expire).with(:gitignore) - repository.expire_method_caches(%i(readme gitignore)) + repository.expire_method_caches(%i(rendered_readme gitignore)) + end + + it 'does not expire caches for non-existent methods' do + expect(cache).not_to receive(:expire).with(:nonexistent) + expect(Rails.logger).to( + receive(:error).with("Requested to expire non-existent method 'nonexistent' for Repository")) + + repository.expire_method_caches(%i(nonexistent)) end end end diff --git a/spec/lib/gitlab/sanitizers/svg_spec.rb b/spec/lib/gitlab/sanitizers/svg_spec.rb index 030c2063ab2..df46a874528 100644 --- a/spec/lib/gitlab/sanitizers/svg_spec.rb +++ b/spec/lib/gitlab/sanitizers/svg_spec.rb @@ -7,9 +7,9 @@ describe Gitlab::Sanitizers::SVG do describe '.clean' do let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') } - let(:data) { open(input_svg_path).read } + let(:data) { File.read(input_svg_path) } let(:sanitized_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') } - let(:sanitized) { open(sanitized_svg_path).read } + let(:sanitized) { File.read(sanitized_svg_path) } it 'delegates sanitization to scrubber' do expect_any_instance_of(Gitlab::Sanitizers::SVG::Scrubber).to receive(:scrub).at_least(:once) diff --git a/spec/lib/gitlab/serializer/ci/variables_spec.rb b/spec/lib/gitlab/serializer/ci/variables_spec.rb index c4b7fda5dbb..1d1fd5b0763 100644 --- a/spec/lib/gitlab/serializer/ci/variables_spec.rb +++ b/spec/lib/gitlab/serializer/ci/variables_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require 'fast_spec_helper' describe Gitlab::Serializer::Ci::Variables do subject do @@ -6,11 +6,11 @@ describe Gitlab::Serializer::Ci::Variables do end let(:object) do - [{ key: :key, value: 'value', public: true }, + [{ 'key' => :key, 'value' => 'value', 'public' => true }, { key: 'wee', value: 1, public: false }] end - it 'converts keys into strings' do + it 'converts keys into strings and symbolizes hash' do is_expected.to eq([ { key: 'key', value: 'value', public: true }, { key: 'wee', value: 1, public: false } diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index c435f988cdd..f8bf896950e 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -403,46 +403,36 @@ describe Gitlab::Shell do end describe '#create_repository' do - shared_examples '#create_repository' do - let(:repository_storage) { 'default' } - let(:repository_storage_path) do - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - Gitlab.config.repositories.storages[repository_storage].legacy_disk_path - end - end - let(:repo_name) { 'project/path' } - let(:created_path) { File.join(repository_storage_path, repo_name + '.git') } - - after do - FileUtils.rm_rf(created_path) + let(:repository_storage) { 'default' } + let(:repository_storage_path) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages[repository_storage].legacy_disk_path end + end + let(:repo_name) { 'project/path' } + let(:created_path) { File.join(repository_storage_path, repo_name + '.git') } - it 'creates a repository' do - expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_truthy - - expect(File.stat(created_path).mode & 0o777).to eq(0o770) + after do + FileUtils.rm_rf(created_path) + end - hooks_path = File.join(created_path, 'hooks') - expect(File.lstat(hooks_path)).to be_symlink - expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path) - end + it 'creates a repository' do + expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_truthy - it 'returns false when the command fails' do - FileUtils.mkdir_p(File.dirname(created_path)) - # This file will block the creation of the repo's .git directory. That - # should cause #create_repository to fail. - FileUtils.touch(created_path) + expect(File.stat(created_path).mode & 0o777).to eq(0o770) - expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_falsy - end + hooks_path = File.join(created_path, 'hooks') + expect(File.lstat(hooks_path)).to be_symlink + expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path) end - context 'with gitaly' do - it_behaves_like '#create_repository' - end + it 'returns false when the command fails' do + FileUtils.mkdir_p(File.dirname(created_path)) + # This file will block the creation of the repo's .git directory. That + # should cause #create_repository to fail. + FileUtils.touch(created_path) - context 'without gitaly', :skip_gitaly_mock do - it_behaves_like '#create_repository' + expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_falsy end end @@ -513,22 +503,12 @@ describe Gitlab::Shell do end end - shared_examples 'fetch_remote' do |gitaly_on| + describe '#fetch_remote' do def fetch_remote(ssh_auth = nil, prune = true) gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', ssh_auth: ssh_auth, prune: prune) end - def expect_gitlab_projects(fail = false, options = {}) - expect(gitlab_projects).to receive(:fetch_remote).with( - 'remote-name', - timeout, - options - ).and_return(!fail) - - allow(gitlab_projects).to receive(:output).and_return('error') if fail - end - - def expect_gitaly_call(fail, options = {}) + def expect_call(fail, options = {}) receive_fetch_remote = if fail receive(:fetch_remote).and_raise(GRPC::NotFound) @@ -539,16 +519,6 @@ describe Gitlab::Shell do expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive_fetch_remote end - if gitaly_on - def expect_call(fail, options = {}) - expect_gitaly_call(fail, options) - end - else - def expect_call(fail, options = {}) - expect_gitlab_projects(fail, options) - end - end - def build_ssh_auth(opts = {}) defaults = { ssh_import?: true, @@ -634,14 +604,6 @@ describe Gitlab::Shell do expect(fetch_remote(ssh_auth)).to be_truthy end end - end - - describe '#fetch_remote local', :skip_gitaly_mock do - it_should_behave_like 'fetch_remote', false - end - - describe '#fetch_remote gitaly' do - it_should_behave_like 'fetch_remote', true context 'gitaly call' do let(:remote_name) { 'remote-name' } @@ -683,25 +645,6 @@ describe Gitlab::Shell do end.to raise_error(Gitlab::Shell::Error, "error") end end - - context 'without gitaly', :disable_gitaly do - it 'returns true when the command succeeds' do - expect(gitlab_projects).to receive(:import_project).with(import_url, timeout) { true } - - result = gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url) - - expect(result).to be_truthy - end - - it 'raises an exception when the command fails' do - allow(gitlab_projects).to receive(:output) { 'error' } - expect(gitlab_projects).to receive(:import_project) { false } - - expect do - gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url) - end.to raise_error(Gitlab::Shell::Error, "error") - end - end end end diff --git a/spec/lib/gitlab/slash_commands/issue_move_spec.rb b/spec/lib/gitlab/slash_commands/issue_move_spec.rb index d41441c9472..9a990e1fad7 100644 --- a/spec/lib/gitlab/slash_commands/issue_move_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_move_spec.rb @@ -27,7 +27,7 @@ describe Gitlab::SlashCommands::IssueMove, service: true do set(:other_project) { create(:project, namespace: project.namespace) } before do - [project, other_project].each { |prj| prj.add_master(user) } + [project, other_project].each { |prj| prj.add_maintainer(user) } end subject { described_class.new(project, chat_name) } diff --git a/spec/lib/gitlab/slash_commands/issue_new_spec.rb b/spec/lib/gitlab/slash_commands/issue_new_spec.rb index 8e7df946529..724c76ade6e 100644 --- a/spec/lib/gitlab/slash_commands/issue_new_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_new_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::SlashCommands::IssueNew do let(:regex_match) { described_class.match("issue create bird is the word") } before do - project.add_master(user) + project.add_maintainer(user) end subject do diff --git a/spec/lib/gitlab/slash_commands/issue_search_spec.rb b/spec/lib/gitlab/slash_commands/issue_search_spec.rb index 189e9592f1b..47787307990 100644 --- a/spec/lib/gitlab/slash_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_search_spec.rb @@ -22,7 +22,7 @@ describe Gitlab::SlashCommands::IssueSearch do context 'the user has access' do before do - project.add_master(user) + project.add_maintainer(user) end it 'returns all results' do diff --git a/spec/lib/gitlab/slash_commands/issue_show_spec.rb b/spec/lib/gitlab/slash_commands/issue_show_spec.rb index b1db1638237..5c4ba2736ba 100644 --- a/spec/lib/gitlab/slash_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_show_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::SlashCommands::IssueShow do let(:regex_match) { described_class.match("issue show #{issue.iid}") } before do - project.add_master(user) + project.add_maintainer(user) end subject do diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index fc8991fd31f..b41a81a8167 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -92,6 +92,7 @@ describe Gitlab::UrlSanitizer do context 'credentials in URL' do where(:url, :credentials) do 'http://foo:bar@example.com' | { user: 'foo', password: 'bar' } + 'http://foo:bar:baz@example.com' | { user: 'foo', password: 'bar:baz' } 'http://:bar@example.com' | { user: nil, password: 'bar' } 'http://foo:@example.com' | { user: 'foo', password: nil } 'http://foo@example.com' | { user: 'foo', password: nil } @@ -144,6 +145,10 @@ describe Gitlab::UrlSanitizer do 'http://foo:@example.com' | 'http://foo@example.com' 'http://:bar@example.com' | :same 'http://foo:bar@example.com' | :same + 'http://foo:g p@example.com' | 'http://foo:g%20p@example.com' + 'http://foo:s/h@example.com' | 'http://foo:s%2Fh@example.com' + 'http://t u:a#b@example.com' | 'http://t%20u:a%23b@example.com' + 'http://t+u:a#b@example.com' | 'http://t%2Bu:a%23b@example.com' end with_them do @@ -159,7 +164,7 @@ describe Gitlab::UrlSanitizer do url_sanitizer = described_class.new("https://foo:b?r@github.com/me/project.git") expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") - expect(url_sanitizer.full_url).to eq("https://foo:b?r@github.com/me/project.git") + expect(url_sanitizer.full_url).to eq("https://foo:b%3Fr@github.com/me/project.git") end end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 20def4fefe2..a19b3c0ba66 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -133,7 +133,7 @@ describe Gitlab::UsageData do expect(subject[:signup_enabled]).to eq(Gitlab::CurrentSettings.allow_signup?) expect(subject[:ldap_enabled]).to eq(Gitlab.config.ldap.enabled) expect(subject[:gravatar_enabled]).to eq(Gitlab::CurrentSettings.gravatar_enabled?) - expect(subject[:omniauth_enabled]).to eq(Gitlab.config.omniauth.enabled) + expect(subject[:omniauth_enabled]).to eq(Gitlab::Auth.omniauth_enabled?) expect(subject[:reply_by_email_enabled]).to eq(Gitlab::IncomingEmail.enabled?) expect(subject[:container_registry_enabled]).to eq(Gitlab.config.registry.enabled) expect(subject[:gitlab_shared_runners_enabled]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled) diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index 0469d984a40..9da06bb40f4 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -9,8 +9,8 @@ describe Gitlab::UserAccess do describe '#can_push_to_branch?' do describe 'push to none protected branch' do - it 'returns true if user is a master' do - project.add_master(user) + it 'returns true if user is a maintainer' do + project.add_maintainer(user) expect(access.can_push_to_branch?('random_branch')).to be_truthy end @@ -38,8 +38,8 @@ describe Gitlab::UserAccess do expect(access.can_push_to_branch?('master')).to be_truthy end - it 'returns true if user is master' do - empty_project.add_master(user) + it 'returns true if user is maintainer' do + empty_project.add_maintainer(user) expect(project_access.can_push_to_branch?('master')).to be_truthy end @@ -83,8 +83,8 @@ describe Gitlab::UserAccess do expect(access.can_push_to_branch?(branch.name)).to be_truthy end - it 'returns true if user is a master' do - project.add_master(user) + it 'returns true if user is a maintainer' do + project.add_maintainer(user) expect(access.can_push_to_branch?(branch.name)).to be_truthy end @@ -113,8 +113,8 @@ describe Gitlab::UserAccess do @branch = create :protected_branch, :developers_can_push, project: project end - it 'returns true if user is a master' do - project.add_master(user) + it 'returns true if user is a maintainer' do + project.add_maintainer(user) expect(access.can_push_to_branch?(@branch.name)).to be_truthy end @@ -170,8 +170,8 @@ describe Gitlab::UserAccess do @branch = create :protected_branch, :developers_can_merge, project: project end - it 'returns true if user is a master' do - project.add_master(user) + it 'returns true if user is a maintainer' do + project.add_maintainer(user) expect(access.can_merge_to_branch?(@branch.name)).to be_truthy end @@ -192,8 +192,8 @@ describe Gitlab::UserAccess do describe '#can_create_tag?' do describe 'push to none protected tag' do - it 'returns true if user is a master' do - project.add_user(user, :master) + it 'returns true if user is a maintainer' do + project.add_user(user, :maintainer) expect(access.can_create_tag?('random_tag')).to be_truthy end @@ -215,8 +215,8 @@ describe Gitlab::UserAccess do let(:tag) { create(:protected_tag, project: project, name: "test") } let(:not_existing_tag) { create :protected_tag, project: project } - it 'returns true if user is a master' do - project.add_user(user, :master) + it 'returns true if user is a maintainer' do + project.add_user(user, :maintainer) expect(access.can_create_tag?(tag.name)).to be_truthy end @@ -239,8 +239,8 @@ describe Gitlab::UserAccess do @tag = create(:protected_tag, :developers_can_create, project: project) end - it 'returns true if user is a master' do - project.add_user(user, :master) + it 'returns true if user is a maintainer' do + project.add_user(user, :maintainer) expect(access.can_create_tag?(@tag.name)).to be_truthy end @@ -261,8 +261,8 @@ describe Gitlab::UserAccess do describe '#can_delete_branch?' do describe 'delete unprotected branch' do - it 'returns true if user is a master' do - project.add_user(user, :master) + it 'returns true if user is a maintainer' do + project.add_user(user, :maintainer) expect(access.can_delete_branch?('random_branch')).to be_truthy end @@ -283,8 +283,8 @@ describe Gitlab::UserAccess do describe 'delete protected branch' do let(:branch) { create(:protected_branch, project: project, name: "test") } - it 'returns true if user is a master' do - project.add_user(user, :master) + it 'returns true if user is a maintainer' do + project.add_user(user, :maintainer) expect(access.can_delete_branch?(branch.name)).to be_truthy end diff --git a/spec/lib/gitlab/user_activities_spec.rb b/spec/lib/gitlab/user_activities_spec.rb deleted file mode 100644 index 6bce2ee13cf..00000000000 --- a/spec/lib/gitlab/user_activities_spec.rb +++ /dev/null @@ -1,127 +0,0 @@ -require 'spec_helper' - -describe Gitlab::UserActivities, :clean_gitlab_redis_shared_state do - let(:now) { Time.now } - - describe '.record' do - context 'with no time given' do - it 'uses Time.now and records an activity in SharedState' do - Timecop.freeze do - now # eager-load now - described_class.record(42) - end - - Gitlab::Redis::SharedState.with do |redis| - expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]]) - end - end - end - - context 'with a time given' do - it 'uses the given time and records an activity in SharedState' do - described_class.record(42, now) - - Gitlab::Redis::SharedState.with do |redis| - expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]]) - end - end - end - end - - describe '.delete' do - context 'with a single key' do - context 'and key exists' do - it 'removes the pair from SharedState' do - described_class.record(42, now) - - Gitlab::Redis::SharedState.with do |redis| - expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]]) - end - - subject.delete(42) - - Gitlab::Redis::SharedState.with do |redis| - expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) - end - end - end - - context 'and key does not exist' do - it 'removes the pair from SharedState' do - Gitlab::Redis::SharedState.with do |redis| - expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) - end - - subject.delete(42) - - Gitlab::Redis::SharedState.with do |redis| - expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) - end - end - end - end - - context 'with multiple keys' do - context 'and all keys exist' do - it 'removes the pair from SharedState' do - described_class.record(41, now) - described_class.record(42, now) - - Gitlab::Redis::SharedState.with do |redis| - expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['41', now.to_i.to_s], ['42', now.to_i.to_s]]]) - end - - subject.delete(41, 42) - - Gitlab::Redis::SharedState.with do |redis| - expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) - end - end - end - - context 'and some keys does not exist' do - it 'removes the existing pair from SharedState' do - described_class.record(42, now) - - Gitlab::Redis::SharedState.with do |redis| - expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]]) - end - - subject.delete(41, 42) - - Gitlab::Redis::SharedState.with do |redis| - expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) - end - end - end - end - end - - describe 'Enumerable' do - before do - described_class.record(40, now) - described_class.record(41, now) - described_class.record(42, now) - end - - it 'allows to read the activities sequentially' do - expected = { '40' => now.to_i.to_s, '41' => now.to_i.to_s, '42' => now.to_i.to_s } - - actual = described_class.new.each_with_object({}) do |(key, time), actual| - actual[key] = time - end - - expect(actual).to eq(expected) - end - - context 'with many records' do - before do - 1_000.times { |i| described_class.record(i, now) } - end - - it 'is possible to loop through all the records' do - expect(described_class.new.count).to eq(1_000) - end - end - end -end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 660671cefaf..23869f3d2da 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -36,22 +36,20 @@ describe Gitlab::Workhorse do allow(described_class).to receive(:git_archive_cache_disabled?).and_return(cache_disabled) end - context 'when Gitaly workhorse_archive feature is enabled' do - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) - expect(key).to eq('Gitlab-Workhorse-Send-Data') - expect(command).to eq('git-archive') - expect(params).to include(gitaly_params) - end + expect(key).to eq('Gitlab-Workhorse-Send-Data') + expect(command).to eq('git-archive') + expect(params).to include(gitaly_params) + end - context 'when archive caching is disabled' do - let(:cache_disabled) { true } + context 'when archive caching is disabled' do + let(:cache_disabled) { true } - it 'tells workhorse not to use the cache' do - _, _, params = decode_workhorse_header(subject) - expect(params).to include({ 'DisableCache' => true }) - end + it 'tells workhorse not to use the cache' do + _, _, params = decode_workhorse_header(subject) + expect(params).to include({ 'DisableCache' => true }) end end @@ -70,34 +68,22 @@ describe Gitlab::Workhorse do let(:diff_refs) { double(base_sha: "base", head_sha: "head") } subject { described_class.send_git_patch(repository, diff_refs) } - context 'when Gitaly workhorse_send_git_patch feature is enabled' do - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) - - expect(key).to eq("Gitlab-Workhorse-Send-Data") - expect(command).to eq("git-format-patch") - expect(params).to eq({ - 'GitalyServer' => { - address: Gitlab::GitalyClient.address(project.repository_storage), - token: Gitlab::GitalyClient.token(project.repository_storage) - }, - 'RawPatchRequest' => Gitaly::RawPatchRequest.new( - repository: repository.gitaly_repository, - left_commit_id: 'base', - right_commit_id: 'head' - ).to_json - }.deep_stringify_keys) - end - end - - context 'when Gitaly workhorse_send_git_patch feature is disabled', :disable_gitaly do - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) - expect(key).to eq("Gitlab-Workhorse-Send-Data") - expect(command).to eq("git-format-patch") - expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head") - end + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("git-format-patch") + expect(params).to eq({ + 'GitalyServer' => { + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + }, + 'RawPatchRequest' => Gitaly::RawPatchRequest.new( + repository: repository.gitaly_repository, + left_commit_id: 'base', + right_commit_id: 'head' + ).to_json + }.deep_stringify_keys) end end @@ -143,34 +129,22 @@ describe Gitlab::Workhorse do let(:diff_refs) { double(base_sha: "base", head_sha: "head") } subject { described_class.send_git_diff(repository, diff_refs) } - context 'when Gitaly workhorse_send_git_diff feature is enabled' do - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) - - expect(key).to eq("Gitlab-Workhorse-Send-Data") - expect(command).to eq("git-diff") - expect(params).to eq({ - 'GitalyServer' => { - address: Gitlab::GitalyClient.address(project.repository_storage), - token: Gitlab::GitalyClient.token(project.repository_storage) - }, - 'RawDiffRequest' => Gitaly::RawDiffRequest.new( - repository: repository.gitaly_repository, - left_commit_id: 'base', - right_commit_id: 'head' - ).to_json - }.deep_stringify_keys) - end - end - - context 'when Gitaly workhorse_send_git_diff feature is disabled', :disable_gitaly do - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) - expect(key).to eq("Gitlab-Workhorse-Send-Data") - expect(command).to eq("git-diff") - expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head") - end + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("git-diff") + expect(params).to eq({ + 'GitalyServer' => { + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + }, + 'RawDiffRequest' => Gitaly::RawDiffRequest.new( + repository: repository.gitaly_repository, + left_commit_id: 'base', + right_commit_id: 'head' + ).to_json + }.deep_stringify_keys) end end @@ -189,7 +163,7 @@ describe Gitlab::Workhorse do end it 'accepts a trailing newline' do - open(described_class.secret_path, 'a') { |f| f.write "\n" } + File.open(described_class.secret_path, 'a') { |f| f.write "\n" } expect(subject.length).to eq(32) end @@ -425,34 +399,22 @@ describe Gitlab::Workhorse do subject { described_class.send_git_blob(repository, blob) } - context 'when Gitaly workhorse_raw_show feature is enabled' do - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) - - expect(key).to eq('Gitlab-Workhorse-Send-Data') - expect(command).to eq('git-blob') - expect(params).to eq({ - 'GitalyServer' => { - address: Gitlab::GitalyClient.address(project.repository_storage), - token: Gitlab::GitalyClient.token(project.repository_storage) - }, - 'GetBlobRequest' => { - repository: repository.gitaly_repository.to_h, - oid: blob.id, - limit: -1 - } - }.deep_stringify_keys) - end - end - - context 'when Gitaly workhorse_raw_show feature is disabled', :disable_gitaly do - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) - expect(key).to eq('Gitlab-Workhorse-Send-Data') - expect(command).to eq('git-blob') - expect(params).to eq('RepoPath' => repository.path_to_repo, 'BlobId' => blob.id) - end + expect(key).to eq('Gitlab-Workhorse-Send-Data') + expect(command).to eq('git-blob') + expect(params).to eq({ + 'GitalyServer' => { + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + }, + 'GetBlobRequest' => { + repository: repository.gitaly_repository.to_h, + oid: blob.id, + limit: -1 + } + }.deep_stringify_keys) end end diff --git a/spec/lib/uploaded_file_spec.rb b/spec/lib/uploaded_file_spec.rb index cc99e7e8911..a2f5c2e7121 100644 --- a/spec/lib/uploaded_file_spec.rb +++ b/spec/lib/uploaded_file_spec.rb @@ -1,24 +1,28 @@ require 'spec_helper' describe UploadedFile do - describe ".from_params" do - let(:temp_dir) { Dir.tmpdir } - let(:temp_file) { Tempfile.new("test", temp_dir) } - let(:upload_path) { nil } + let(:temp_dir) { Dir.tmpdir } + let(:temp_file) { Tempfile.new("test", temp_dir) } - subject do - described_class.from_params(params, :file, upload_path) - end + before do + FileUtils.touch(temp_file) + end - before do - FileUtils.touch(temp_file) - end + after do + FileUtils.rm_f(temp_file) + end + + describe ".from_params" do + let(:upload_path) { nil } after do - FileUtils.rm_f(temp_file) FileUtils.rm_r(upload_path) if upload_path end + subject do + described_class.from_params(params, :file, upload_path) + end + context 'when valid file is specified' do context 'only local path is specified' do let(:params) do @@ -37,7 +41,7 @@ describe UploadedFile do context 'all parameters are specified' do let(:params) do { 'file.path' => temp_file.path, - 'file.name' => 'my_file.txt', + 'file.name' => 'dir/my file&.txt', 'file.type' => 'my/type', 'file.sha256' => 'sha256', 'file.remote_id' => 'remote_id' } @@ -48,7 +52,7 @@ describe UploadedFile do end it "generates filename from path" do - expect(subject.original_filename).to eq('my_file.txt') + expect(subject.original_filename).to eq('my_file_.txt') expect(subject.content_type).to eq('my/type') expect(subject.sha256).to eq('sha256') expect(subject.remote_id).to eq('remote_id') @@ -113,4 +117,11 @@ describe UploadedFile do end end end + + describe '#sanitize_filename' do + it { expect(described_class.new(temp_file.path).sanitize_filename('spaced name')).to eq('spaced_name') } + it { expect(described_class.new(temp_file.path).sanitize_filename('#$%^&')).to eq('_____') } + it { expect(described_class.new(temp_file.path).sanitize_filename('..')).to eq('_..') } + it { expect(described_class.new(temp_file.path).sanitize_filename('')).to eq('unnamed') } + end end |