diff options
Diffstat (limited to 'spec')
39 files changed, 966 insertions, 451 deletions
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 570d9fa43f8..c9584ddf18c 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -4,6 +4,28 @@ describe UploadsController do let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } describe "GET show" do + context 'Content-Disposition security measures' do + let(:project) { create(:empty_project, :public) } + + context 'for PNG files' do + it 'returns Content-Disposition: inline' do + note = create(:note, :with_attachment, project: project) + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + + expect(response['Content-Disposition']).to start_with('inline;') + end + end + + context 'for SVG files' do + it 'returns Content-Disposition: attachment' do + note = create(:note, :with_svg_attachment, project: project) + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.svg' + + expect(response['Content-Disposition']).to start_with('attachment;') + end + end + end + context "when viewing a user avatar" do context "when signed in" do before do diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index a21da7074f9..5c50cd7f4ad 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -97,7 +97,11 @@ FactoryGirl.define do end trait :with_attachment do - attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } + attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") } + end + + trait :with_svg_attachment do + attachment { fixture_file_upload(Rails.root + "spec/fixtures/unsanitized.svg", "image/svg+xml") } end end end diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb index efa6cbe5bb1..4105f59e289 100644 --- a/spec/factories/wiki_pages.rb +++ b/spec/factories/wiki_pages.rb @@ -2,8 +2,26 @@ require 'ostruct' FactoryGirl.define do factory :wiki_page do + transient do + attrs do + { + title: 'Title', + content: 'Content for wiki page', + format: 'markdown' + } + end + end + page { OpenStruct.new(url_path: 'some-name') } association :wiki, factory: :project_wiki, strategy: :build initialize_with { new(wiki, page, true) } + + before(:create) do |page, evaluator| + page.attributes = evaluator.attrs + end + + to_create do |page| + page.create + end end end diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index 3e0b6364e0d..35d090c4b7f 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -1,9 +1,11 @@ require 'spec_helper' -feature 'Contributions Calendar', js: true, feature: true do +feature 'Contributions Calendar', :feature, :js do include WaitForAjax + let(:user) { create(:user) } let(:contributed_project) { create(:project, :public) } + let(:issue_note) { create(:note, project: contributed_project) } # Ex/ Sunday Jan 1, 2016 date_format = '%A %b %-d, %Y' @@ -12,31 +14,31 @@ feature 'Contributions Calendar', js: true, feature: true do issue_params = { title: issue_title } def get_cell_color_selector(contributions) - contribution_cell = '.user-contrib-cell' - activity_colors = Array['#ededed', '#acd5f2', '#7fa8c9', '#527ba0', '#254e77'] - activity_colors_index = 0 - - if contributions > 0 && contributions < 10 - activity_colors_index = 1 - elsif contributions >= 10 && contributions < 20 - activity_colors_index = 2 - elsif contributions >= 20 && contributions < 30 - activity_colors_index = 3 - elsif contributions >= 30 - activity_colors_index = 4 - end + activity_colors = %w[#ededed #acd5f2 #7fa8c9 #527ba0 #254e77] + # We currently don't actually test the cases with contributions >= 20 + activity_colors_index = + if contributions > 0 && contributions < 10 + 1 + elsif contributions >= 10 && contributions < 20 + 2 + elsif contributions >= 20 && contributions < 30 + 3 + elsif contributions >= 30 + 4 + else + 0 + end - "#{contribution_cell}[fill='#{activity_colors[activity_colors_index]}']" + ".user-contrib-cell[fill='#{activity_colors[activity_colors_index]}']" end def get_cell_date_selector(contributions, date) - contribution_text = 'No contributions' - - if contributions === 1 - contribution_text = '1 contribution' - elsif contributions > 1 - contribution_text = "#{contributions} contributions" - end + contribution_text = + if contributions.zero? + 'No contributions' + else + "#{contributions} #{'contribution'.pluralize(contributions)}" + end "#{get_cell_color_selector(contributions)}[data-original-title='#{contribution_text}<br />#{date}']" end @@ -45,129 +47,155 @@ feature 'Contributions Calendar', js: true, feature: true do push_params = { project: contributed_project, action: Event::PUSHED, - author_id: @user.id, + author_id: user.id, data: { commit_count: 3 } } Event.create(push_params) end - def get_first_cell_content - find('.user-calendar-activities').text - end + def note_comment_contribution + note_comment_params = { + project: contributed_project, + action: Event::COMMENTED, + target: issue_note, + author_id: user.id + } - before do - login_as :user - visit @user.username - wait_for_ajax + Event.create(note_comment_params) end - it 'displays calendar', js: true do - expect(page).to have_css('.js-contrib-calendar') + def selected_day_activities + find('.user-calendar-activities').text end - describe 'select calendar day', js: true do - let(:cells) { page.all('.user-contrib-cell') } - let(:first_cell_content_before) { get_first_cell_content } + before do + login_as user + end + describe 'calendar day selection' do before do - cells[0].click + visit user.username wait_for_ajax - first_cell_content_before end - it 'displays calendar day activities', js: true do - expect(get_first_cell_content).not_to eq('') + it 'displays calendar' do + expect(page).to have_css('.js-contrib-calendar') end - describe 'select another calendar day', js: true do + describe 'select calendar day' do + let(:cells) { page.all('.user-contrib-cell') } + before do - cells[1].click + cells[0].click wait_for_ajax + @first_day_activities = selected_day_activities end - it 'displays different calendar day activities', js: true do - expect(get_first_cell_content).not_to eq(first_cell_content_before) + it 'displays calendar day activities' do + expect(selected_day_activities).not_to be_empty end - end - describe 'deselect calendar day', js: true do - before do - cells[0].click - wait_for_ajax + describe 'select another calendar day' do + before do + cells[1].click + wait_for_ajax + end + + it 'displays different calendar day activities' do + expect(selected_day_activities).not_to eq(@first_day_activities) + end end - it 'hides calendar day activities', js: true do - expect(get_first_cell_content).to eq('') + describe 'deselect calendar day' do + before do + cells[0].click + wait_for_ajax + end + + it 'hides calendar day activities' do + expect(selected_day_activities).to be_empty + end end end end - describe '1 calendar activity' do - before do - Issues::CreateService.new(contributed_project, @user, issue_params).execute - visit @user.username - wait_for_ajax + describe 'calendar daily activities' do + shared_context 'visit user page' do + before do + visit user.username + wait_for_ajax + end end - it 'displays calendar activity log', js: true do - expect(find('.content_list .event-note')).to have_content issue_title - end + shared_examples 'a day with activity' do |contribution_count:| + include_context 'visit user page' - it 'displays calendar activity square color for 1 contribution', js: true do - expect(page).to have_selector(get_cell_color_selector(1), count: 1) - end + it 'displays calendar activity square color for 1 contribution' do + expect(page).to have_selector(get_cell_color_selector(contribution_count), count: 1) + end - it 'displays calendar activity square on the correct date', js: true do - today = Date.today.strftime(date_format) - expect(page).to have_selector(get_cell_date_selector(1, today), count: 1) + it 'displays calendar activity square on the correct date' do + today = Date.today.strftime(date_format) + expect(page).to have_selector(get_cell_date_selector(contribution_count, today), count: 1) + end end - end - describe '10 calendar activities' do - before do - (0..9).each do |i| - push_code_contribution() + describe '1 issue creation calendar activity' do + before do + Issues::CreateService.new(contributed_project, user, issue_params).execute end - visit @user.username - wait_for_ajax - end + it_behaves_like 'a day with activity', contribution_count: 1 - it 'displays calendar activity square color for 10 contributions', js: true do - expect(page).to have_selector(get_cell_color_selector(10), count: 1) - end + describe 'issue title is shown on activity page' do + include_context 'visit user page' - it 'displays calendar activity square on the correct date', js: true do - today = Date.today.strftime(date_format) - expect(page).to have_selector(get_cell_date_selector(10, today), count: 1) + it 'displays calendar activity log' do + expect(find('.content_list .event-note')).to have_content issue_title + end + end end - end - describe 'calendar activity on two days' do - before do - push_code_contribution() - - Timecop.freeze(Date.yesterday) - Issues::CreateService.new(contributed_project, @user, issue_params).execute - Timecop.return + describe '1 comment calendar activity' do + before do + note_comment_contribution + end - visit @user.username - wait_for_ajax + it_behaves_like 'a day with activity', contribution_count: 1 end - it 'displays calendar activity squares for both days', js: true do - expect(page).to have_selector(get_cell_color_selector(1), count: 2) - end + describe '10 calendar activities' do + before do + 10.times { push_code_contribution } + end - it 'displays calendar activity square for yesterday', js: true do - yesterday = Date.yesterday.strftime(date_format) - expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1) + it_behaves_like 'a day with activity', contribution_count: 10 end - it 'displays calendar activity square for today', js: true do - today = Date.today.strftime(date_format) - expect(page).to have_selector(get_cell_date_selector(1, today), count: 1) + describe 'calendar activity on two days' do + before do + push_code_contribution + + Timecop.freeze(Date.yesterday) do + Issues::CreateService.new(contributed_project, user, issue_params).execute + end + end + include_context 'visit user page' + + it 'displays calendar activity squares for both days' do + expect(page).to have_selector(get_cell_color_selector(1), count: 2) + end + + it 'displays calendar activity square for yesterday' do + yesterday = Date.yesterday.strftime(date_format) + expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1) + end + + it 'displays calendar activity square for today' do + today = Date.today.strftime(date_format) + expect(page).to have_selector(get_cell_date_selector(1, today), count: 1) + end end end end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index e853fb7e016..0832a3656a8 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Create New Merge Request', feature: true, js: true do + include WaitForVueResource + let(:user) { create(:user) } let(:project) { create(:project, :public) } @@ -99,6 +101,7 @@ feature 'Create New Merge Request', feature: true, js: true do page.within('.merge-request') do click_link 'Pipelines' + wait_for_vue_resource expect(page).to have_content "##{pipeline.id}" end diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index 38fe2d92885..4eafac1acd8 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -20,9 +20,9 @@ feature 'Ref switcher', feature: true, js: true do input.set 'binary' wait_for_ajax - input.native.send_keys :down - input.native.send_keys :down - input.native.send_keys :enter + page.within '.dropdown-content ul' do + input.native.send_keys :enter + end end expect(page).to have_title 'binary-encoding' diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index a958ac76e66..631fca06514 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -4,18 +4,19 @@ require('~/behaviors/requires_input'); (function() { describe('requiresInput', function() { - preloadFixtures('static/behaviors/requires_input.html.raw'); + preloadFixtures('branches/new_branch.html.raw'); beforeEach(function() { - return loadFixtures('static/behaviors/requires_input.html.raw'); + loadFixtures('branches/new_branch.html.raw'); + this.submitButton = $('button[type="submit"]'); }); it('disables submit when any field is required', function() { $('.js-requires-input').requiresInput(); - return expect($('.submit')).toBeDisabled(); + return expect(this.submitButton).toBeDisabled(); }); it('enables submit when no field is required', function() { $('*[required=required]').removeAttr('required'); $('.js-requires-input').requiresInput(); - return expect($('.submit')).not.toBeDisabled(); + return expect(this.submitButton).not.toBeDisabled(); }); it('enables submit when all required fields are pre-filled', function() { $('*[required=required]').remove(); @@ -25,9 +26,9 @@ require('~/behaviors/requires_input'); it('enables submit when all required fields receive input', function() { $('.js-requires-input').requiresInput(); $('#required1').val('input1').change(); - expect($('.submit')).toBeDisabled(); + expect(this.submitButton).toBeDisabled(); $('#optional1').val('input1').change(); - expect($('.submit')).toBeDisabled(); + expect(this.submitButton).toBeDisabled(); $('#required2').val('input2').change(); $('#required3').val('input3').change(); $('#required4').val('input4').change(); diff --git a/spec/javascripts/fixtures/.gitignore b/spec/javascripts/fixtures/.gitignore index 009b68d5d1c..0c35cdd778e 100644 --- a/spec/javascripts/fixtures/.gitignore +++ b/spec/javascripts/fixtures/.gitignore @@ -1 +1,2 @@ *.html.raw +*.json diff --git a/spec/javascripts/fixtures/behaviors/requires_input.html.haml b/spec/javascripts/fixtures/behaviors/requires_input.html.haml deleted file mode 100644 index c3f905e912e..00000000000 --- a/spec/javascripts/fixtures/behaviors/requires_input.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -%form.js-requires-input - %input{type: 'text', id: 'required1', required: 'required'} - %input{type: 'text', id: 'required2', required: 'required'} - %input{type: 'text', id: 'required3', required: 'required', value: 'Pre-filled'} - %input{type: 'text', id: 'optional1'} - - %textarea{id: 'required4', required: 'required'} - %textarea{id: 'optional2'} - - %select{id: 'required5', required: 'required'} - %option Zero - %option{value: '1'} One - %select{id: 'optional3', required: 'required'} - %option Zero - %option{value: '1'} One - - %button.submit{type: 'submit', value: 'Submit'} - %input.submit{type: 'submit', value: 'Submit'} diff --git a/spec/javascripts/fixtures/todos.json b/spec/javascripts/fixtures/todos.json deleted file mode 100644 index 62c2387d515..00000000000 --- a/spec/javascripts/fixtures/todos.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "count": 1, - "delete_path": "/dashboard/todos/1" -}
\ No newline at end of file diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb new file mode 100644 index 00000000000..2c08b06ea9e --- /dev/null +++ b/spec/javascripts/fixtures/todos.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe 'Todos (JavaScript fixtures)' do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') } + let(:issue_1) { create(:issue, title: 'issue_1', project: project) } + let!(:todo_1) { create(:todo, user: admin, project: project, target: issue_1, created_at: 5.hours.ago) } + let(:issue_2) { create(:issue, title: 'issue_2', project: project) } + let!(:todo_2) { create(:todo, :done, user: admin, project: project, target: issue_2, created_at: 50.hours.ago) } + + before(:all) do + clean_frontend_fixtures('todos/') + end + + describe Dashboard::TodosController, '(JavaScript fixtures)', type: :controller do + render_views + + before(:each) do + sign_in(admin) + end + + it 'todos/todos.html.raw' do |example| + get :index + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end + + describe Projects::TodosController, '(JavaScript fixtures)', type: :controller do + render_views + + before(:each) do + sign_in(admin) + end + + it 'todos/todos.json' do |example| + post :create, + namespace_id: namespace.path, + project_id: project.path, + issuable_type: 'issue', + issuable_id: issue_2.id, + format: 'json' + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end +end diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 317f38c5888..c207fb00a47 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -139,6 +139,14 @@ require('~/lib/utils/url_utility'); this.dropdownButtonElement.click(); }); + it('should show loading indicator while search results are being fetched by backend', () => { + const dropdownMenu = document.querySelector('.dropdown-menu'); + + expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true); + remoteCallback(); + expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false); + }); + it('should not focus search input while remote task is not complete', () => { expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR)); remoteCallback(); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 3810991f104..5b0c124962c 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -62,19 +62,47 @@ require('vendor/jquery.scrollTo'); }); }); describe('#opensInNewTab', function () { - var commitsLink; var tabUrl; + var windowTarget = '_blank'; beforeEach(function () { - commitsLink = '.commits-tab li a'; - tabUrl = $(commitsLink).attr('href'); + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + + tabUrl = $('.commits-tab a').attr('href'); spyOn($.fn, 'attr').and.returnValue(tabUrl); }); + + describe('meta click', () => { + beforeEach(function () { + spyOn(gl.utils, 'isMetaClick').and.returnValue(true); + }); + + it('opens page when commits link is clicked', function () { + spyOn(window, 'open').and.callFake(function (url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual(windowTarget); + }); + + this.class.bindEvents(); + document.querySelector('.merge-request-tabs .commits-tab a').click(); + }); + + it('opens page when commits badge is clicked', function () { + spyOn(window, 'open').and.callFake(function (url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual(windowTarget); + }); + + this.class.bindEvents(); + document.querySelector('.merge-request-tabs .commits-tab a .badge').click(); + }); + }); + it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () { spyOn(window, 'open').and.callFake(function (url, name) { expect(url).toEqual(tabUrl); - expect(name).toEqual('_blank'); + expect(name).toEqual(windowTarget); }); this.class.clickTab({ @@ -87,7 +115,7 @@ require('vendor/jquery.scrollTo'); it('opens page tab in a new browser tab with Cmd+Click - Mac', function () { spyOn(window, 'open').and.callFake(function (url, name) { expect(url).toEqual(tabUrl); - expect(name).toEqual('_blank'); + expect(name).toEqual(windowTarget); }); this.class.clickTab({ @@ -100,7 +128,7 @@ require('vendor/jquery.scrollTo'); it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () { spyOn(window, 'open').and.callFake(function (url, name) { expect(url).toEqual(tabUrl); - expect(name).toEqual('_blank'); + expect(name).toEqual(windowTarget); }); this.class.clickTab({ diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index f7636865aa1..9284af8a8d9 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -34,7 +34,7 @@ require('~/extensions/jquery.js'); describe('RightSidebar', function() { var fixtureName = 'issues/open-issue.html.raw'; preloadFixtures(fixtureName); - loadJSONFixtures('todos.json'); + loadJSONFixtures('todos/todos.json'); beforeEach(function() { loadFixtures(fixtureName); @@ -64,7 +64,7 @@ require('~/extensions/jquery.js'); }); it('should broadcast todo:toggle event when add todo clicked', function() { - var todos = getJSONFixture('todos.json'); + var todos = getJSONFixture('todos/todos.json'); spyOn(jQuery, 'ajax').and.callFake(function() { var d = $.Deferred(); var response = todos; diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 030d2de090a..ca707d872a4 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -42,3 +42,38 @@ testsContext.keys().forEach(function (path) { }); } }); + +// workaround: include all source files to find files with 0% coverage +// see also https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15 +describe('Uncovered files', function () { + // the following files throw errors because of undefined variables + const troubleMakers = [ + './blob_edit/blob_edit_bundle.js', + './cycle_analytics/components/stage_plan_component.js', + './cycle_analytics/components/stage_staging_component.js', + './cycle_analytics/components/stage_test_component.js', + './diff_notes/components/jump_to_discussion.js', + './diff_notes/components/resolve_count.js', + './merge_conflicts/components/inline_conflict_lines.js', + './merge_conflicts/components/parallel_conflict_lines.js', + './network/branch_graph.js', + ]; + + const sourceFiles = require.context('~', true, /^\.\/(?!application\.js).*\.(js|es6)$/); + sourceFiles.keys().forEach(function (path) { + // ignore if there is a matching spec file + if (testsContext.keys().indexOf(`${path.replace(/\.js(\.es6)?$/, '')}_spec`) > -1) { + return; + } + + it(`includes '${path}'`, function () { + try { + sourceFiles(path); + } catch (err) { + if (troubleMakers.indexOf(path) === -1) { + expect(err).toBeNull(); + } + } + }); + }); +}); diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js new file mode 100644 index 00000000000..66e4fbd6304 --- /dev/null +++ b/spec/javascripts/todos_spec.js @@ -0,0 +1,63 @@ +require('~/todos'); +require('~/lib/utils/common_utils'); + +describe('Todos', () => { + preloadFixtures('todos/todos.html.raw'); + let todoItem; + + beforeEach(() => { + loadFixtures('todos/todos.html.raw'); + todoItem = document.querySelector('.todos-list .todo'); + + return new gl.Todos(); + }); + + describe('goToTodoUrl', () => { + it('opens the todo url', (done) => { + const todoLink = todoItem.dataset.url; + + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + expect(url).toEqual(todoLink); + done(); + }); + + todoItem.click(); + }); + + describe('meta click', () => { + let visitUrlSpy; + + beforeEach(() => { + spyOn(gl.utils, 'isMetaClick').and.returnValue(true); + visitUrlSpy = spyOn(gl.utils, 'visitUrl').and.callFake(() => {}); + }); + + it('opens the todo url in another tab', (done) => { + const todoLink = todoItem.dataset.url; + + spyOn(window, 'open').and.callFake((url, target) => { + expect(todoLink).toEqual(url); + expect(target).toEqual('_blank'); + done(); + }); + + todoItem.click(); + expect(visitUrlSpy).not.toHaveBeenCalled(); + }); + + it('opens the avatar\'s url in another tab when the avatar is clicked', (done) => { + const avatarImage = todoItem.querySelector('img'); + const avatarUrl = avatarImage.parentElement.getAttribute('href'); + + spyOn(window, 'open').and.callFake((url, target) => { + expect(avatarUrl).toEqual(url); + expect(target).toEqual('_blank'); + done(); + }); + + avatarImage.click(); + expect(visitUrlSpy).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 3e1ac9fb2b2..d5d128c1907 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -112,6 +112,19 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end end + context 'mentioning a nested group' do + it_behaves_like 'a reference containing an element node' + + let(:group) { create(:group, :nested) } + let(:reference) { group.to_reference } + + it 'links to the nested group' do + doc = reference_filter("Hey #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) + end + end + it 'links with adjacent text' do doc = reference_filter("Mention me (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index ba199917f5c..bca57105d1d 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -41,6 +41,29 @@ module Gitlab render(input, context, asciidoc_opts) end end + + context "XSS" do + links = { + 'links' => { + input: 'link:mylink"onmouseover="alert(1)[Click Here]', + output: "<div>\n<p><a href=\"mylink\">Click Here</a></p>\n</div>" + }, + 'images' => { + input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]', + output: "<div>\n<p><span><img src=\"https://localhost.com/image.png\" alt=\"Alt text\"></span></p>\n</div>" + }, + 'pre' => { + input: '```mypre"><script>alert(3)</script>', + output: "<div>\n<div>\n<pre lang=\"mypre\">\"><code></code></pre>\n</div>\n</div>" + } + } + + links.each do |name, data| + it "does not convert dangerous #{name} into HTML" do + expect(render(data[:input], context)).to eql data[:output] + end + end + end end def render(*args) diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index b142b3a2781..f01c42aff91 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -5,6 +5,12 @@ class MigrationTest end describe Gitlab::Database, lib: true do + describe '.adapter_name' do + it 'returns the name of the adapter' do + expect(described_class.adapter_name).to be_an_instance_of(String) + end + end + # These are just simple smoke tests to check if the methods work (regardless # of what they may return). describe '.mysql?' do @@ -71,6 +77,54 @@ describe Gitlab::Database, lib: true do end end + describe '.with_connection_pool' do + it 'creates a new connection pool and disconnect it after used' do + closed_pool = nil + + described_class.with_connection_pool(1) do |pool| + pool.with_connection do |connection| + connection.execute('SELECT 1 AS value') + end + + expect(pool).to be_connected + + closed_pool = pool + end + + expect(closed_pool).not_to be_connected + end + + it 'disconnects the pool even an exception was raised' do + error = Class.new(RuntimeError) + closed_pool = nil + + begin + described_class.with_connection_pool(1) do |pool| + pool.with_connection do |connection| + connection.execute('SELECT 1 AS value') + end + + closed_pool = pool + + raise error.new('boom') + end + rescue error + end + + expect(closed_pool).not_to be_connected + end + end + + describe '.create_connection_pool' do + it 'creates a new connection pool with specific pool size' do + pool = described_class.create_connection_pool(5) + + expect(pool) + .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool) + expect(pool.spec.config[:pool]).to eq(5) + end + end + describe '#true_value' do it 'returns correct value for PostgreSQL' do expect(described_class).to receive(:postgresql?).and_return(true) diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb index 53f7d244d88..20743811dab 100644 --- a/spec/lib/gitlab/import_export/import_export_spec.rb +++ b/spec/lib/gitlab/import_export/import_export_spec.rb @@ -2,14 +2,15 @@ require 'spec_helper' describe Gitlab::ImportExport, services: true do describe 'export filename' do - let(:project) { create(:empty_project, :public, path: 'project-path') } + let(:group) { create(:group, :nested) } + let(:project) { create(:empty_project, :public, path: 'project-path', namespace: group) } it 'contains the project path' do expect(described_class.export_filename(project: project)).to include(project.path) end it 'contains the namespace path' do - expect(described_class.export_filename(project: project)).to include(project.namespace.path) + expect(described_class.export_filename(project: project)).to include(project.namespace.full_path) end it 'does not go over a certain length' do diff --git a/spec/lib/gitlab/other_markup.rb b/spec/lib/gitlab/other_markup.rb new file mode 100644 index 00000000000..8f5a353b381 --- /dev/null +++ b/spec/lib/gitlab/other_markup.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Gitlab::OtherMarkup, lib: true do + context "XSS Checks" do + links = { + 'links' => { + file: 'file.rdoc', + input: 'XSS[JaVaScriPt:alert(1)]', + output: '<p><a>XSS</a></p>' + } + } + links.each do |name, data| + it "does not convert dangerous #{name} into HTML" do + expect(render(data[:file], data[:input], context)).to eql data[:output] + end + end + end + + def render(*args) + described_class.render(*args) + end +end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 1dbc2f6eb13..089ec4e2737 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -50,4 +50,16 @@ describe Gitlab::Regex, lib: true do it { is_expected.not_to match('9foo') } it { is_expected.not_to match('foo-') } end + + describe 'NAMESPACE_REF_REGEX_STR' do + subject { %r{\A#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR}\z} } + + it { is_expected.to match('gitlab.org') } + it { is_expected.to match('gitlab.org/gitlab-git') } + it { is_expected.not_to match('gitlab.org.') } + it { is_expected.not_to match('gitlab.org/') } + it { is_expected.not_to match('/gitlab.org') } + it { is_expected.not_to match('gitlab.git') } + it { is_expected.not_to match('gitlab git') } + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 35f3dd00870..b0087a9e15d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1852,8 +1852,8 @@ describe Project, models: true do end describe '#pages_url' do - let(:group) { create :group, name: group_name } - let(:project) { create :empty_project, namespace: group, name: project_name } + let(:group) { create :group, name: 'Group' } + let(:nested_group) { create :group, parent: group } let(:domain) { 'Example.com' } subject { project.pages_url } @@ -1863,18 +1863,37 @@ describe Project, models: true do allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com') end - context 'group page' do - let(:group_name) { 'Group' } - let(:project_name) { 'group.example.com' } + context 'top-level group' do + let(:project) { create :empty_project, namespace: group, name: project_name } - it { is_expected.to eq("http://group.example.com") } + context 'group page' do + let(:project_name) { 'group.example.com' } + + it { is_expected.to eq("http://group.example.com") } + end + + context 'project page' do + let(:project_name) { 'Project' } + + it { is_expected.to eq("http://group.example.com/project") } + end end - context 'project page' do - let(:group_name) { 'Group' } - let(:project_name) { 'Project' } + context 'nested group' do + let(:project) { create :empty_project, namespace: nested_group, name: project_name } + let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" } - it { is_expected.to eq("http://group.example.com/project") } + context 'group page' do + let(:project_name) { 'group.example.com' } + + it { is_expected.to eq(expected_url) } + end + + context 'project page' do + let(:project_name) { 'Project' } + + it { is_expected.to eq(expected_url) } + end end end end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 579ebac7afb..753dc938c52 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -318,6 +318,19 @@ describe WikiPage, models: true do end end + describe '#==' do + let(:original_wiki_page) { create(:wiki_page) } + + it 'returns true for identical wiki page' do + expect(original_wiki_page).to eq(original_wiki_page) + end + + it 'returns false for updated wiki page' do + updated_wiki_page = original_wiki_page.update("Updated content") + expect(original_wiki_page).not_to eq(updated_wiki_page) + end + end + private def remove_temp_repo(path) diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 5a3ffc284f2..3e66236f6ae 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -31,7 +31,18 @@ describe API::Branches, api: true do expect(response).to have_http_status(200) expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) + json_commit = json_response['commit'] + expect(json_commit['id']).to eq(branch_sha) + expect(json_commit).to have_key('short_id') + expect(json_commit).to have_key('title') + expect(json_commit).to have_key('message') + expect(json_commit).to have_key('author_name') + expect(json_commit).to have_key('author_email') + expect(json_commit).to have_key('authored_date') + expect(json_commit).to have_key('committer_name') + expect(json_commit).to have_key('committer_email') + expect(json_commit).to have_key('committed_date') + expect(json_commit).to have_key('parent_ids') expect(json_response['merged']).to eq(false) expect(json_response['protected']).to eq(false) expect(json_response['developers_can_push']).to eq(false) diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 88361def3cf..eb53fd71872 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -156,6 +156,7 @@ describe API::CommitStatuses, api: true do context: 'coverage', ref: 'develop', description: 'test', + coverage: 80.0, target_url: 'http://gitlab.com/status' } post api(post_url, developer), optional_params @@ -167,6 +168,7 @@ describe API::CommitStatuses, api: true do expect(json_response['status']).to eq('success') expect(json_response['name']).to eq('coverage') expect(json_response['ref']).to eq('develop') + expect(json_response['coverage']).to eq(80.0) expect(json_response['description']).to eq('test') expect(json_response['target_url']).to eq('http://gitlab.com/status') end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index af9028a8978..3d0d6735359 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -367,11 +367,21 @@ describe API::Commits, api: true do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) expect(response).to have_http_status(200) - expect(json_response['id']).to eq(project.repository.commit.id) - expect(json_response['title']).to eq(project.repository.commit.title) - expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions) - expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions) - expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total) + commit = project.repository.commit + expect(json_response['id']).to eq(commit.id) + expect(json_response['short_id']).to eq(commit.short_id) + expect(json_response['title']).to eq(commit.title) + expect(json_response['message']).to eq(commit.safe_message) + expect(json_response['author_name']).to eq(commit.author_name) + expect(json_response['author_email']).to eq(commit.author_email) + expect(json_response['authored_date']).to eq(commit.authored_date.iso8601(3)) + expect(json_response['committer_name']).to eq(commit.committer_name) + expect(json_response['committer_email']).to eq(commit.committer_email) + expect(json_response['committed_date']).to eq(commit.committed_date.iso8601(3)) + expect(json_response['parent_ids']).to eq(commit.parent_ids) + expect(json_response['stats']['additions']).to eq(commit.stats.additions) + expect(json_response['stats']['deletions']).to eq(commit.stats.deletions) + expect(json_response['stats']['total']).to eq(commit.stats.total) end it "returns a 404 error if not found" do diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb deleted file mode 100644 index 92ac4fd334d..00000000000 --- a/spec/requests/api/fork_spec.rb +++ /dev/null @@ -1,134 +0,0 @@ -require 'spec_helper' - -describe API::Projects, api: true do - include ApiHelpers - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:admin) { create(:admin) } - let(:group) { create(:group) } - let(:group2) do - group = create(:group, name: 'group2_name') - group.add_owner(user2) - group - end - - describe 'POST /projects/fork/:id' do - let(:project) do - create(:project, :repository, creator: user, namespace: user.namespace) - end - - before do - project.add_reporter(user2) - end - - context 'when authenticated' do - it 'forks if user has sufficient access to project' do - post api("/projects/fork/#{project.id}", user2) - - expect(response).to have_http_status(201) - expect(json_response['name']).to eq(project.name) - expect(json_response['path']).to eq(project.path) - expect(json_response['owner']['id']).to eq(user2.id) - expect(json_response['namespace']['id']).to eq(user2.namespace.id) - expect(json_response['forked_from_project']['id']).to eq(project.id) - end - - it 'forks if user is admin' do - post api("/projects/fork/#{project.id}", admin) - - expect(response).to have_http_status(201) - expect(json_response['name']).to eq(project.name) - expect(json_response['path']).to eq(project.path) - expect(json_response['owner']['id']).to eq(admin.id) - expect(json_response['namespace']['id']).to eq(admin.namespace.id) - expect(json_response['forked_from_project']['id']).to eq(project.id) - end - - it 'fails on missing project access for the project to fork' do - new_user = create(:user) - post api("/projects/fork/#{project.id}", new_user) - - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - - it 'fails if forked project exists in the user namespace' do - post api("/projects/fork/#{project.id}", user) - - expect(response).to have_http_status(409) - expect(json_response['message']['name']).to eq(['has already been taken']) - expect(json_response['message']['path']).to eq(['has already been taken']) - end - - it 'fails if project to fork from does not exist' do - post api('/projects/fork/424242', user) - - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - - it 'forks with explicit own user namespace id' do - post api("/projects/fork/#{project.id}", user2), namespace: user2.namespace.id - - expect(response).to have_http_status(201) - expect(json_response['owner']['id']).to eq(user2.id) - end - - it 'forks with explicit own user name as namespace' do - post api("/projects/fork/#{project.id}", user2), namespace: user2.username - - expect(response).to have_http_status(201) - expect(json_response['owner']['id']).to eq(user2.id) - end - - it 'forks to another user when admin' do - post api("/projects/fork/#{project.id}", admin), namespace: user2.username - - expect(response).to have_http_status(201) - expect(json_response['owner']['id']).to eq(user2.id) - end - - it 'fails if trying to fork to another user when not admin' do - post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id - - expect(response).to have_http_status(404) - end - - it 'fails if trying to fork to non-existent namespace' do - post api("/projects/fork/#{project.id}", user2), namespace: 42424242 - - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Target Namespace Not Found') - end - - it 'forks to owned group' do - post api("/projects/fork/#{project.id}", user2), namespace: group2.name - - expect(response).to have_http_status(201) - expect(json_response['namespace']['name']).to eq(group2.name) - end - - it 'fails to fork to not owned group' do - post api("/projects/fork/#{project.id}", user2), namespace: group.name - - expect(response).to have_http_status(404) - end - - it 'forks to not owned group when admin' do - post api("/projects/fork/#{project.id}", admin), namespace: group.name - - expect(response).to have_http_status(201) - expect(json_response['namespace']['name']).to eq(group.name) - end - end - - context 'when unauthenticated' do - it 'returns authentication error' do - post api("/projects/fork/#{project.id}") - - expect(response).to have_http_status(401) - expect(json_response['message']).to eq('401 Unauthorized') - end - end - end -end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index f78bde6f53a..ccd7898586c 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -338,6 +338,26 @@ describe API::Groups, api: true do expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(project3.name) end + + it 'only returns the projects owned by user' do + project2.group.add_owner(user3) + + get api("/groups/#{project2.group.id}/projects", user3), owned: true + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project2.name) + end + + it 'only returns the projects starred by user' do + user1.starred_projects = [project1] + + get api("/groups/#{group1.id}/projects", user1), starred: true + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project1.name) + end end context "when authenticated as admin" do diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index c1edf384d5c..a945d56f529 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -5,7 +5,7 @@ describe API::Namespaces, api: true do let(:admin) { create(:admin) } let(:user) { create(:user) } let!(:group1) { create(:group) } - let!(:group2) { create(:group) } + let!(:group2) { create(:group, :nested) } describe "GET /namespaces" do context "when unauthenticated" do @@ -25,11 +25,13 @@ describe API::Namespaces, api: true do end it "admin: returns an array of matched namespaces" do - get api("/namespaces?search=#{group1.name}", admin) + get api("/namespaces?search=#{group2.name}", admin) expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) + expect(json_response.last['path']).to eq(group2.path) + expect(json_response.last['full_path']).to eq(group2.full_path) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index ac0bbec44e0..741815a780e 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -41,26 +41,40 @@ describe API::Projects, api: true do end describe 'GET /projects' do - before { project } + shared_examples_for 'projects response' do + it 'returns an array of projects' do + get api('/projects', current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) + end + end + + let!(:public_project) { create(:empty_project, :public, name: 'public_project') } + before do + project + project2 + project3 + project4 + end context 'when unauthenticated' do - it 'returns authentication error' do - get api('/projects') - expect(response).to have_http_status(401) + it_behaves_like 'projects response' do + let(:current_user) { nil } + let(:projects) { [public_project] } end end context 'when authenticated as regular user' do - it 'returns an array of projects' do - get api('/projects', user) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(project.name) - expect(json_response.first['owner']['username']).to eq(user.username) + it_behaves_like 'projects response' do + let(:current_user) { user } + let(:projects) { [public_project, project, project2, project3] } end it 'includes the project labels as the tag_list' do get api('/projects', user) + expect(response.status).to eq 200 expect(json_response).to be_an Array expect(json_response.first.keys).to include('tag_list') @@ -68,21 +82,39 @@ describe API::Projects, api: true do it 'includes open_issues_count' do get api('/projects', user) + expect(response.status).to eq 200 expect(json_response).to be_an Array expect(json_response.first.keys).to include('open_issues_count') end - it 'does not include open_issues_count' do + it 'does not include open_issues_count if issues are disabled' do project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) get api('/projects', user) + expect(response.status).to eq 200 expect(json_response).to be_an Array - expect(json_response.first.keys).not_to include('open_issues_count') + expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count') + end + + it "does not include statistics by default" do + get api('/projects', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).not_to include('statistics') end - context 'GET /projects?simple=true' do + it "includes statistics if requested" do + get api('/projects', user), statistics: true + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).to include 'statistics' + end + + context 'and with simple=true' do it 'returns a simplified version of all the projects' do expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"] @@ -97,6 +129,7 @@ describe API::Projects, api: true do context 'and using search' do it 'returns searched project' do get api('/projects', user), { search: project.name } + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -106,196 +139,109 @@ describe API::Projects, api: true do context 'and using the visibility filter' do it 'filters based on private visibility param' do get api('/projects', user), { visibility: 'private' } + expect(response).to have_http_status(200) expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count) + expect(json_response.map { |p| p['id'] }).to contain_exactly(project.id, project2.id, project3.id) end it 'filters based on internal visibility param' do + project2.update_attribute(:visibility_level, Gitlab::VisibilityLevel::INTERNAL) + get api('/projects', user), { visibility: 'internal' } + expect(response).to have_http_status(200) expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count) + expect(json_response.map { |p| p['id'] }).to contain_exactly(project2.id) end it 'filters based on public visibility param' do get api('/projects', user), { visibility: 'public' } + expect(response).to have_http_status(200) expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count) + expect(json_response.map { |p| p['id'] }).to contain_exactly(public_project.id) end end context 'and using sorting' do - before do - project2 - project3 - end - it 'returns the correct order when sorted by id' do get api('/projects', user), { order_by: 'id', sort: 'desc' } + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.first['id']).to eq(project3.id) end end - end - end - - describe 'GET /projects/all' do - before { project } - - context 'when unauthenticated' do - it 'returns authentication error' do - get api('/projects/all') - expect(response).to have_http_status(401) - end - end - context 'when authenticated as regular user' do - it 'returns authentication error' do - get api('/projects/all', user) - expect(response).to have_http_status(403) - end - end + context 'and with owned=true' do + it 'returns an array of projects the user owns' do + get api('/projects', user4), owned: true - context 'when authenticated as admin' do - it 'returns an array of all projects' do - get api('/projects/all', admin) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - - expect(json_response).to satisfy do |response| - response.one? do |entry| - entry.has_key?('permissions') && - entry['name'] == project.name && - entry['owner']['username'] == user.username - end + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project4.name) + expect(json_response.first['owner']['username']).to eq(user4.username) end end - it "does not include statistics by default" do - get api('/projects/all', admin) - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include('statistics') - end - - it "includes statistics if requested" do - get api('/projects/all', admin), statistics: true - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).to include 'statistics' - end - end - end - - describe 'GET /projects/owned' do - before do - project3 - project4 - end + context 'and with starred=true' do + let(:public_project) { create(:empty_project, :public) } - context 'when unauthenticated' do - it 'returns authentication error' do - get api('/projects/owned') - expect(response).to have_http_status(401) - end - end - - context 'when authenticated as project owner' do - it 'returns an array of projects the user owns' do - get api('/projects/owned', user4) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(project4.name) - expect(json_response.first['owner']['username']).to eq(user4.username) - end - - it "does not include statistics by default" do - get api('/projects/owned', user4) - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include('statistics') - end - - it "includes statistics if requested" do - attributes = { - commit_count: 23, - storage_size: 702, - repository_size: 123, - lfs_objects_size: 234, - build_artifacts_size: 345, - } - - project4.statistics.update!(attributes) + before do + project_member2 + user3.update_attributes(starred_projects: [project, project2, project3, public_project]) + end - get api('/projects/owned', user4), statistics: true + it 'returns the starred projects viewable by the user' do + get api('/projects', user3), starred: true - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['statistics']).to eq attributes.stringify_keys + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) + end end - end - end - describe 'GET /projects/visible' do - shared_examples_for 'visible projects response' do - it 'returns the visible projects' do - get api('/projects/visible', current_user) + context 'and with all query parameters' do + # | | project5 | project6 | project7 | project8 | project9 | + # |---------+----------+----------+----------+----------+----------| + # | search | x | | x | x | x | + # | starred | x | x | | x | x | + # | public | x | x | x | | x | + # | owned | x | x | x | x | | + let!(:project5) { create(:empty_project, :public, path: 'gitlab5', namespace: user.namespace) } + let!(:project6) { create(:empty_project, :public, path: 'project6', namespace: user.namespace) } + let!(:project7) { create(:empty_project, :public, path: 'gitlab7', namespace: user.namespace) } + let!(:project8) { create(:empty_project, path: 'gitlab8', namespace: user.namespace) } + let!(:project9) { create(:empty_project, :public, path: 'gitlab9') } - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) - end - end - - let!(:public_project) { create(:empty_project, :public) } - before do - project - project2 - project3 - project4 - end + before do + user.update_attributes(starred_projects: [project5, project6, project8, project9]) + end - context 'when unauthenticated' do - it_behaves_like 'visible projects response' do - let(:current_user) { nil } - let(:projects) { [public_project] } - end - end + it 'returns only projects that satify all query parameters' do + get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' } - context 'when authenticated' do - it_behaves_like 'visible projects response' do - let(:current_user) { user } - let(:projects) { [public_project, project, project2, project3] } + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(project5.id) + end end end context 'when authenticated as a different user' do - it_behaves_like 'visible projects response' do + it_behaves_like 'projects response' do let(:current_user) { user2 } let(:projects) { [public_project] } end end - end - - describe 'GET /projects/starred' do - let(:public_project) { create(:empty_project, :public) } - - before do - project_member2 - user3.update_attributes(starred_projects: [project, project2, project3, public_project]) - end - it 'returns the starred projects viewable by the user' do - get api('/projects/starred', user3) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) + context 'when authenticated as admin' do + it_behaves_like 'projects response' do + let(:current_user) { admin } + let(:projects) { Project.all } + end end end @@ -639,6 +585,7 @@ describe API::Projects, api: true do 'name' => user.namespace.name, 'path' => user.namespace.path, 'kind' => user.namespace.kind, + 'full_path' => user.namespace.full_path, }) end @@ -1332,4 +1279,130 @@ describe API::Projects, api: true do end end end + + describe 'POST /projects/:id/fork' do + let(:project) do + create(:project, :repository, creator: user, namespace: user.namespace) + end + let(:group) { create(:group) } + let(:group2) do + group = create(:group, name: 'group2_name') + group.add_owner(user2) + group + end + + before do + project.add_reporter(user2) + end + + context 'when authenticated' do + it 'forks if user has sufficient access to project' do + post api("/projects/#{project.id}/fork", user2) + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to eq(project.path) + expect(json_response['owner']['id']).to eq(user2.id) + expect(json_response['namespace']['id']).to eq(user2.namespace.id) + expect(json_response['forked_from_project']['id']).to eq(project.id) + end + + it 'forks if user is admin' do + post api("/projects/#{project.id}/fork", admin) + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to eq(project.path) + expect(json_response['owner']['id']).to eq(admin.id) + expect(json_response['namespace']['id']).to eq(admin.namespace.id) + expect(json_response['forked_from_project']['id']).to eq(project.id) + end + + it 'fails on missing project access for the project to fork' do + new_user = create(:user) + post api("/projects/#{project.id}/fork", new_user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'fails if forked project exists in the user namespace' do + post api("/projects/#{project.id}/fork", user) + + expect(response).to have_http_status(409) + expect(json_response['message']['name']).to eq(['has already been taken']) + expect(json_response['message']['path']).to eq(['has already been taken']) + end + + it 'fails if project to fork from does not exist' do + post api('/projects/424242/fork', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'forks with explicit own user namespace id' do + post api("/projects/#{project.id}/fork", user2), namespace: user2.namespace.id + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'forks with explicit own user name as namespace' do + post api("/projects/#{project.id}/fork", user2), namespace: user2.username + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'forks to another user when admin' do + post api("/projects/#{project.id}/fork", admin), namespace: user2.username + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'fails if trying to fork to another user when not admin' do + post api("/projects/#{project.id}/fork", user2), namespace: admin.namespace.id + + expect(response).to have_http_status(404) + end + + it 'fails if trying to fork to non-existent namespace' do + post api("/projects/#{project.id}/fork", user2), namespace: 42424242 + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Target Namespace Not Found') + end + + it 'forks to owned group' do + post api("/projects/#{project.id}/fork", user2), namespace: group2.name + + expect(response).to have_http_status(201) + expect(json_response['namespace']['name']).to eq(group2.name) + end + + it 'fails to fork to not owned group' do + post api("/projects/#{project.id}/fork", user2), namespace: group.name + + expect(response).to have_http_status(404) + end + + it 'forks to not owned group when admin' do + post api("/projects/#{project.id}/fork", admin), namespace: group.name + + expect(response).to have_http_status(201) + expect(json_response['namespace']['name']).to eq(group.name) + end + end + + context 'when unauthenticated' do + it 'returns authentication error' do + post api("/projects/#{project.id}/fork") + + expect(response).to have_http_status(401) + expect(json_response['message']).to eq('401 Unauthorized') + end + end + end end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index a495122bba7..36d99d80e79 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -682,6 +682,7 @@ describe API::V3::Projects, api: true do 'name' => user.namespace.name, 'path' => user.namespace.path, 'kind' => user.namespace.kind, + 'full_path' => user.namespace.full_path, }) end diff --git a/spec/services/create_tag_service_spec.rb b/spec/services/tags/create_service_spec.rb index 7dc43c50b0d..5478b8c9ec0 100644 --- a/spec/services/create_tag_service_spec.rb +++ b/spec/services/tags/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CreateTagService, services: true do +describe Tags::CreateService, services: true do let(:project) { create(:project) } let(:repository) { project.repository } let(:user) { create(:user) } diff --git a/spec/services/delete_tag_service_spec.rb b/spec/services/tags/destroy_service_spec.rb index 477551f5036..a388c93379a 100644 --- a/spec/services/delete_tag_service_spec.rb +++ b/spec/services/tags/destroy_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe DeleteTagService, services: true do +describe Tags::DestroyService, services: true do let(:project) { create(:project) } let(:repository) { project.repository } let(:user) { create(:user) } diff --git a/spec/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb new file mode 100644 index 00000000000..5341ba3d261 --- /dev/null +++ b/spec/services/wiki_pages/create_service_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe WikiPages::CreateService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:opts) do + { + title: 'Title', + content: 'Content for wiki page', + format: 'markdown' + } + end + let(:service) { described_class.new(project, user, opts) } + + describe '#execute' do + context "valid params" do + before do + allow(service).to receive(:execute_hooks) + project.add_master(user) + end + + subject { service.execute } + + it 'creates a valid wiki page' do + is_expected.to be_valid + expect(subject.title).to eq(opts[:title]) + expect(subject.content).to eq(opts[:content]) + expect(subject.format).to eq(opts[:format].to_sym) + end + + it 'executes webhooks' do + expect(service).to have_received(:execute_hooks).once.with(subject, 'create') + end + end + end +end diff --git a/spec/services/wiki_pages/destroy_service_spec.rb b/spec/services/wiki_pages/destroy_service_spec.rb new file mode 100644 index 00000000000..a4b9a390fe2 --- /dev/null +++ b/spec/services/wiki_pages/destroy_service_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe WikiPages::DestroyService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:wiki_page) { create(:wiki_page) } + let(:service) { described_class.new(project, user) } + + describe '#execute' do + before do + allow(service).to receive(:execute_hooks) + project.add_master(user) + end + + it 'executes webhooks' do + service.execute(wiki_page) + + expect(service).to have_received(:execute_hooks).once.with(wiki_page, 'delete') + end + end +end diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb new file mode 100644 index 00000000000..2bccca764d7 --- /dev/null +++ b/spec/services/wiki_pages/update_service_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe WikiPages::UpdateService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:wiki_page) { create(:wiki_page) } + let(:opts) do + { + content: 'New content for wiki page', + format: 'markdown', + message: 'New wiki message' + } + end + let(:service) { described_class.new(project, user, opts) } + + describe '#execute' do + context "valid params" do + before do + allow(service).to receive(:execute_hooks) + project.add_master(user) + end + + subject { service.execute(wiki_page) } + + it 'updates the wiki page' do + is_expected.to be_valid + expect(subject.content).to eq(opts[:content]) + expect(subject.format).to eq(opts[:format].to_sym) + expect(subject.message).to eq(opts[:message]) + end + + it 'executes webhooks' do + expect(service).to have_received(:execute_hooks).once.with(subject, 'update') + end + end + end +end diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb new file mode 100644 index 00000000000..f5381a48207 --- /dev/null +++ b/spec/views/projects/_home_panel.html.haml_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe 'projects/_home_panel', :view do + let(:project) { create(:empty_project, :public) } + + let(:notification_settings) do + user&.notification_settings_for(project) + end + + before do + assign(:project, project) + assign(:notification_setting, notification_settings) + + allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:can?).and_return(false) + end + + context 'when user is signed in' do + let(:user) { create(:user) } + + it 'makes it possible to set notification level' do + render + + expect(view).to render_template('shared/notifications/_button') + expect(rendered).to have_selector('.notification-dropdown') + end + end + + context 'when user is signed out' do + let(:user) { nil } + + it 'is not possible to set notification level' do + render + + expect(rendered).not_to have_selector('.notification_dropdown') + end + end +end diff --git a/spec/views/projects/notes/_form.html.haml_spec.rb b/spec/views/projects/notes/_form.html.haml_spec.rb index b14b1ece2d0..b61f016967f 100644 --- a/spec/views/projects/notes/_form.html.haml_spec.rb +++ b/spec/views/projects/notes/_form.html.haml_spec.rb @@ -21,7 +21,7 @@ describe 'projects/notes/_form' do let(:note) { build(:"note_on_#{noteable}", project: project) } it 'says that only markdown is supported, not slash commands' do - expect(rendered).to have_content('Styling with Markdown and slash commands are supported') + expect(rendered).to have_content('Markdown and slash commands are supported') end end end @@ -30,7 +30,7 @@ describe 'projects/notes/_form' do let(:note) { build(:note_on_commit, project: project) } it 'says that only markdown is supported, not slash commands' do - expect(rendered).to have_content('Styling with Markdown is supported') + expect(rendered).to have_content('Markdown is supported') end end end |